diff options
| author | kj_sh604 | 2026-04-03 02:27:29 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-04-03 02:27:29 -0400 |
| commit | f22c51507eb59cf843f1baca2ad7626fd351f33d (patch) | |
| tree | 8d62965d408e8b629405ebe8b31a8714ba98bd8e | |
| parent | d6458885e00788e1dec779b6807a79646aac6ed6 (diff) | |
refactor: email usernames
| -rw-r--r-- | auth_backend.py | 42 | ||||
| -rw-r--r-- | shim_app.py | 19 |
2 files changed, 47 insertions, 14 deletions
diff --git a/auth_backend.py b/auth_backend.py index 3902d1e..00a5dbc 100644 --- a/auth_backend.py +++ b/auth_backend.py @@ -13,7 +13,9 @@ from typing import Callable, Optional, Protocol # fixed challenge text used for passphrase verification. # we store encrypted challenge output instead of storing passwords. AUTH_CHALLENGE = "SHIM_AUTH_VALID" -USERNAME_RE = re.compile(r"^[a-z0-9_.-]{2,64}$") +HANDLE_RE = re.compile(r"^[a-z0-9_.-]{2,64}$") +EMAIL_LOCAL_RE = re.compile(r"^[a-z0-9!#$%&'*+/=?^_`{|}~.-]{1,64}$") +EMAIL_DOMAIN_LABEL_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$") UUID_RE = re.compile( r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" ) @@ -49,6 +51,30 @@ def normalize_uuid(value: str) -> Optional[str]: return candidate +def is_valid_email_username(value: str) -> bool: + if len(value) > 254: + return False + if "@" not in value or value.count("@") != 1: + return False + + local_part, domain = value.split("@", 1) + if not local_part or not domain: + return False + if local_part.startswith(".") or local_part.endswith(".") or ".." in local_part: + return False + if not EMAIL_LOCAL_RE.fullmatch(local_part): + return False + + labels = domain.split(".") + if len(labels) < 2: + return False + if any(not label for label in labels): + return False + if len(labels[-1]) < 2: + return False + return all(EMAIL_DOMAIN_LABEL_RE.fullmatch(label) for label in labels) + + def looks_like_python_script(path: Path) -> bool: try: with open(path, "r", encoding="utf-8", errors="ignore") as f: @@ -170,9 +196,17 @@ class LocalMojicryptAuthBackend: return self._run_mojicrypt("encrypt", AUTH_CHALLENGE, password) def _validate_username(self, username: str) -> Optional[str]: - if not USERNAME_RE.fullmatch(username): - return "username must be lowercase and use [a-z0-9_.-], 2-64 chars" - return None + if len(username) < 2: + return "username must be at least 2 characters" + if not username.isascii(): + return "username must use ascii characters only" + if HANDLE_RE.fullmatch(username): + return None + if is_valid_email_username(username): + return None + return ( + "username must be a handle [a-z0-9_.-] (2-64 chars) or a valid email" + ) def _validate_password(self, password: str) -> Optional[str]: if len(password) < 2: diff --git a/shim_app.py b/shim_app.py index 875e21b..6137f15 100644 --- a/shim_app.py +++ b/shim_app.py @@ -165,14 +165,13 @@ SHELL_TEMPLATE = """<!doctype html> <header class="topbar"> <h1>{{ app_name }}</h1> <nav> - <a href="{{ url_for('dashboard') }}">dashboard</a> {% if current_user %} - <span class="quiet">signed in as {{ current_user['username'] }} ({{ current_user['role'] }})</span> + <span class="quiet">{{ current_user['username'] }}{% if current_user['role'] == 'admin' %} ({{ current_user['role'] }}){% endif %}</span> <form method="post" action="{{ url_for('logout') }}" class="inline"> <button type="submit">logout</button> </form> {% else %} - <a href="{{ url_for('login') }}">login</a> + <a href="https://youtu.be/XGxIE1hr0w4" target="_blank">🦄</a> {% endif %} </nav> </header> @@ -197,8 +196,8 @@ SETUP_BODY_TEMPLATE = """ <p>first startup detected. <br><br> create the initial admin account to continue.</p> <form method="post" action="{{ url_for('setup_submit') }}" class="stack" autocomplete="off"> <label> - username - <input name="username" type="text" required minlength="2" maxlength="64" pattern="[a-z0-9_.-]+" placeholder="admin" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> + username or email + <input name="username" type="text" required minlength="2" maxlength="254" placeholder="admin username" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> </label> <label> password @@ -219,8 +218,8 @@ LOGIN_BODY_TEMPLATE = """ <p>a no-frills, "hackfoo" static site hosting solution</p> <form method="post" action="{{ url_for('login_submit') }}" class="stack" autocomplete="off"> <label> - username - <input name="username" type="text" required minlength="2" maxlength="64" pattern="[a-z0-9_.-]+" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> + username or email + <input name="username" type="text" required minlength="2" maxlength="254" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> </label> <label> password @@ -323,8 +322,8 @@ DASHBOARD_BODY_TEMPLATE = """ <h2>create user</h2> <form method="post" action="{{ url_for('admin_create_user') }}" class="stack" autocomplete="off"> <label> - username - <input type="text" name="username" required minlength="2" maxlength="64" pattern="[a-z0-9_.-]+" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> + username or email + <input type="text" name="username" required minlength="2" maxlength="254" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> </label> <label> password @@ -362,7 +361,7 @@ DASHBOARD_BODY_TEMPLATE = """ <td>{{ user['created_at'] }}</td> <td> <form method="post" action="{{ url_for('admin_update_user_username', user_id=user['id']) }}" class="stack" autocomplete="off"> - <input type="text" name="username" value="{{ user['username'] }}" required minlength="2" maxlength="64" pattern="[a-z0-9_.-]+" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" style="all: revert;"> + <input type="text" name="username" value="{{ user['username'] }}" required minlength="2" maxlength="254" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" style="all: revert;"> <button type="submit" style="all: revert;">rename user</button> </form> <br> |
