diff --git a/docs/login.md b/docs/login.md index 55ca69e..3a92ae4 100644 --- a/docs/login.md +++ b/docs/login.md @@ -7,6 +7,7 @@ The broader access model is documented in [Access and Permissions](access.md). ## Fields - **Username**: Enter your admin username. - **Password**: Enter your password. +- **Remember me on this browser**: Keeps the admin session after the browser is closed. ## Features - **Validation**: Both fields are required. @@ -17,6 +18,8 @@ The broader access model is documented in [Access and Permissions](access.md). - Only administrators can log in to the management interface. - Non-admin users are shown an error message and cannot access the interface. - After successful login, users are redirected to the Dashboard. +- If **Remember me on this browser** is not checked, the login uses a normal browser session cookie. +- If **Remember me on this browser** is checked, the signed session cookie lasts for 14 days. The app does not store the password in the browser, and protected pages still check that the signed-in user is still an administrator. --- diff --git a/simple_safer_server/app_factory.py b/simple_safer_server/app_factory.py index 79a6e9d..9879f36 100644 --- a/simple_safer_server/app_factory.py +++ b/simple_safer_server/app_factory.py @@ -1,5 +1,6 @@ import logging import os +from datetime import timedelta from logging.handlers import RotatingFileHandler from flask import ( @@ -53,6 +54,8 @@ UnauthorizedProblem, ) +REMEMBER_ME_SESSION_DAYS = 14 + def create_app() -> Flask: runtime = get_runtime() @@ -61,6 +64,10 @@ def create_app() -> Flask: # Keep the session secret stable across deploys so a restart does not # invalidate every login cookie when the app's config directory persists. app.secret_key = get_flask_secret_key(runtime) + # "Remember me" uses Flask's normal signed session cookie. No password or + # separate login token is stored, and protected pages still re-check admin + # status on each request in case roles change after sign-in. + app.permanent_session_lifetime = timedelta(days=REMEMBER_ME_SESSION_DAYS) user_manager = UserManager(runtime=runtime) system_utils = SystemUtils(runtime=runtime) @@ -208,6 +215,7 @@ def login(): if user_manager.verify_user(username, password): if user_manager.is_admin(username): + session.permanent = request.form.get("remember_me") == "on" session["username"] = username session.pop("skip_login_disabled", None) if request.accept_mimetypes.best == "application/json": diff --git a/static/css/login.css b/static/css/login.css index dce7ac0..1986dde 100644 --- a/static/css/login.css +++ b/static/css/login.css @@ -152,6 +152,29 @@ background: var(--bg-root); } +.login-remember-option { + display: flex; + align-items: center; + gap: var(--sp-2); + margin: calc(-1 * var(--sp-1)) 0 var(--sp-5); + color: var(--text-secondary); + font-size: var(--text-sm); + line-height: 1.4; + cursor: pointer; + user-select: none; +} + +.login-remember-checkbox { + width: 16px; + height: 16px; + flex: 0 0 auto; + accent-color: var(--accent); +} + +.login-remember-option:focus-within { + color: var(--text-primary); +} + .login-form .btn-submit { margin-top: var(--sp-2); height: 48px; diff --git a/templates/login.html b/templates/login.html index 47e94c7..3f73aa0 100644 --- a/templates/login.html +++ b/templates/login.html @@ -55,6 +55,11 @@

SimpleSaferServer

+ +