Coverage for webapp/decorators.py: 94%
54 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-18 22:11 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-18 22:11 +0000
1# Core packages
2import os
3import functools
4import logging
5from datetime import datetime, timezone
7# Third party packages
8import flask
9from webapp import authentication
10from webapp.helpers import param_redirect_capture, param_redirect_exec
12logger = logging.getLogger(__name__)
15def strtobool(s: str) -> bool:
16 """Convert a string representation of truth to true (1) or false (0)."""
17 return s.lower() in ("yes", "true", "on", "1")
20def cached_redirect(func):
21 """
22 Decorator that check for a param_redirect cookie and redirects
23 to the appropriate URL.
24 """
26 @functools.wraps(func)
27 def param_redirect(*args, **kwargs):
28 resp = param_redirect_exec(
29 req=flask.request,
30 make_response=flask.make_response,
31 redirect=flask.redirect,
32 )
33 if resp:
34 return resp
35 return func(*args, **kwargs)
37 return param_redirect
40def login_required(func):
41 """
42 Decorator that checks if a user is logged in, and redirects
43 to login page if not.
44 """
46 @functools.wraps(func)
47 def is_user_logged_in(*args, **kwargs):
48 date = datetime.now(timezone.utc)
49 date_str = date.strftime("%Y-%m-%dT%H:%M:%S")
51 if not authentication.is_authenticated(flask.session):
52 logger.warning(
53 "User login failed",
54 extra={
55 "datetime": date_str,
56 "appid": "charmhub-io",
57 "event": "authn_login_fail",
58 },
59 )
61 response = flask.make_response(
62 flask.redirect("/login?next=" + flask.request.path)
63 )
65 response = param_redirect_capture(flask.request, response)
67 return response
69 account = flask.session.get("account")
70 user = account["email"]
72 logger.info(
73 f"User {user} login successfully",
74 extra={
75 "datetime": date_str,
76 "appid": "charmhub-io",
77 "event": f"authn_login_successafterfail:{user}",
78 },
79 )
81 return func(*args, **kwargs)
83 return is_user_logged_in
86def store_maintenance(func):
87 """
88 Decorator that checks if the maintence mode is enabled
89 """
91 @functools.wraps(func)
92 def is_store_in_maintenance(*args, **kwargs):
93 # TODO: this will be a config option for the charm
94 # or used from the app config using from_prefexed_env
95 if strtobool(os.getenv("MAINTENANCE", "false")):
96 return flask.render_template("maintenance.html")
98 return func(*args, **kwargs)
100 return is_store_in_maintenance
103def redirect_uppercase_to_lowercase(func):
104 """
105 Decorator that redirect package names containing upper case
106 to the lower case URL
108 The route must have the entity_name parameter
109 """
111 @functools.wraps(func)
112 def is_uppercase(*args, **kwargs):
113 if "entity_name" in kwargs:
114 name = kwargs["entity_name"]
115 else:
116 # For solutions - fallback to name if entity_name is not provided
117 name = kwargs["name"]
119 ENV = os.getenv("ENVIRONMENT", "devel").strip()
120 redirect = flask.request.url.lower()
122 if any(char.isupper() for char in name):
123 if (
124 (ENV == "devel" and redirect.startswith("http://localhost:"))
125 or (
126 ENV == "production"
127 and redirect.startswith("https://charmhub.io/")
128 )
129 or (
130 ENV == "staging"
131 and redirect.startswith("https://staging.charmhub.io/")
132 )
133 ):
134 return flask.redirect(redirect)
135 else:
136 flask.abort(404)
138 return func(*args, **kwargs)
140 return is_uppercase