Users & Groups fixes/changes

Signed-off-by: apeters <apeters@korves.net>
This commit is contained in:
apeters 2025-05-23 08:47:10 +00:00
parent ee1f3e59b5
commit 36e606693b
16 changed files with 397 additions and 240 deletions

View File

@ -27,23 +27,26 @@ async def cli_processor(streams: tuple[asyncio.StreamReader, asyncio.StreamWrite
await writer.drain() await writer.drain()
elif cmd == b"\x98": elif cmd == b"\x98":
awaiting = dict() awaiting = dict()
idx = 1 tokens = (
for k, v in IN_MEMORY_DB.items(): IN_MEMORY_DB["TOKENS"]["LOGIN"] | IN_MEMORY_DB["TOKENS"]["REGISTER"]
if ( )
isinstance(v, dict) for idx, (k, v) in enumerate(tokens.items(), start=1):
and v.get("token_type") == "cli_confirmation" awaiting[idx] = (k, v["intention"])
):
awaiting[idx] = (k, v["intention"])
idx += 1
writer.write(f"{json.dumps(awaiting)}\n".encode("ascii")) writer.write(f"{json.dumps(awaiting)}\n".encode("ascii"))
await writer.drain() await writer.drain()
elif cmd == b"\x99": elif cmd == b"\x99":
data = await reader.readexactly(14) data = await reader.readexactly(14)
confirmed = data.strip().decode("ascii") confirmed = data.strip().decode("ascii")
code = "%06d" % random.randint(0, 999999) code = "%06d" % random.randint(0, 999999)
IN_MEMORY_DB.get(confirmed, {}).update( if confirmed in IN_MEMORY_DB["TOKENS"]["LOGIN"]:
{"status": "confirmed", "code": code} IN_MEMORY_DB["TOKENS"]["LOGIN"].get(confirmed, {}).update(
) {"status": "confirmed", "code": code}
)
elif confirmed in IN_MEMORY_DB["TOKENS"]["REGISTER"]:
IN_MEMORY_DB["TOKENS"]["REGISTER"].get(confirmed, {}).update(
{"status": "confirmed", "code": code}
)
writer.write(f"{code}\n".encode("ascii")) writer.write(f"{code}\n".encode("ascii"))
await writer.drain() await writer.drain()
except Exception as e: except Exception as e:

View File

@ -23,6 +23,7 @@ app.register_blueprint(objects.blueprint)
app.register_blueprint(profile.blueprint) app.register_blueprint(profile.blueprint)
app.register_blueprint(system.blueprint) app.register_blueprint(system.blueprint)
app.register_blueprint(users.blueprint) app.register_blueprint(users.blueprint)
app.register_blueprint(groups.blueprint)
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = defaults.SEND_FILE_MAX_AGE_DEFAULT app.config["SEND_FILE_MAX_AGE_DEFAULT"] = defaults.SEND_FILE_MAX_AGE_DEFAULT
app.config["SECRET_KEY"] = defaults.SECRET_KEY app.config["SECRET_KEY"] = defaults.SECRET_KEY
@ -35,6 +36,10 @@ IN_MEMORY_DB["FORM_OPTIONS_CACHE"] = dict()
IN_MEMORY_DB["OBJECTS_CACHE"] = dict() IN_MEMORY_DB["OBJECTS_CACHE"] = dict()
IN_MEMORY_DB["APP_LOGS_FULL_PULL"] = dict() IN_MEMORY_DB["APP_LOGS_FULL_PULL"] = dict()
IN_MEMORY_DB["PROMOTE_USERS"] = set() IN_MEMORY_DB["PROMOTE_USERS"] = set()
IN_MEMORY_DB["TOKENS"] = {
"REGISTER": dict(),
"LOGIN": dict(),
}
modifying_request_limiter = asyncio.Semaphore(app.config["MOD_REQ_LIMIT"]) modifying_request_limiter = asyncio.Semaphore(app.config["MOD_REQ_LIMIT"])

View File

@ -4,3 +4,4 @@ from components.web.blueprints import profile
from components.web.blueprints import root from components.web.blueprints import root
from components.web.blueprints import system from components.web.blueprints import system
from components.web.blueprints import users from components.web.blueprints import users
from components.web.blueprints import groups

View File

@ -51,11 +51,13 @@ async def login_request_confirm(request_token: str):
except: except:
return "", 200, {"HX-Redirect": "/"} return "", 200, {"HX-Redirect": "/"}
token_status = IN_MEMORY_DB.get(request_token, {}).get("status") token_status = IN_MEMORY_DB["TOKENS"]["LOGIN"].get(request_token, {}).get("status")
if token_status == "awaiting": if token_status == "awaiting":
session["request_token"] = request_token session["request_token"] = request_token
requested_login = IN_MEMORY_DB[request_token]["requested_login"] requested_login = IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token][
"requested_login"
]
return await render_template( return await render_template(
"auth/login/request/confirm.html", "auth/login/request/confirm.html",
@ -87,10 +89,10 @@ async def login_request_confirm_modal(request_token: str):
if request.method == "POST": if request.method == "POST":
if ( if (
request_token in IN_MEMORY_DB request_token in IN_MEMORY_DB["TOKENS"]["LOGIN"]
and IN_MEMORY_DB[request_token]["status"] == "awaiting" and IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token]["status"] == "awaiting"
): ):
IN_MEMORY_DB[request_token].update( IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token].update(
{ {
"status": "confirmed", "status": "confirmed",
"credential_id": "", "credential_id": "",
@ -98,7 +100,7 @@ async def login_request_confirm_modal(request_token: str):
) )
current_app.add_background_task( current_app.add_background_task(
expire_key, expire_key,
IN_MEMORY_DB, IN_MEMORY_DB["TOKENS"]["LOGIN"],
request_token, request_token,
10, 10,
) )
@ -136,16 +138,16 @@ async def login_request_start():
request_token = token_urlsafe() request_token = token_urlsafe()
IN_MEMORY_DB[request_token] = { IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token] = {
"intention": f"Authenticate user: {request_data.login}", "intention": f"Authenticate user: {request_data.login}",
"created": utc_now_as_str(),
"status": "awaiting", "status": "awaiting",
"token_type": "web_confirmation",
"requested_login": request_data.login, "requested_login": request_data.login,
} }
current_app.add_background_task( current_app.add_background_task(
expire_key, expire_key,
IN_MEMORY_DB, IN_MEMORY_DB["TOKENS"]["LOGIN"],
request_token, request_token,
defaults.AUTH_REQUEST_TIMEOUT, defaults.AUTH_REQUEST_TIMEOUT,
) )
@ -177,7 +179,7 @@ async def login_request_check(request_token: str):
return "", 200, {"HX-Redirect": "/"} return "", 200, {"HX-Redirect": "/"}
token_status, requested_login, credential_id = map( token_status, requested_login, credential_id = map(
IN_MEMORY_DB.get(request_token, {}).get, IN_MEMORY_DB["TOKENS"]["LOGIN"].get(request_token, {}).get,
["status", "requested_login", "credential_id"], ["status", "requested_login", "credential_id"],
) )
@ -216,15 +218,15 @@ async def login_token():
try: try:
request_data = AuthToken.parse_obj(request.form_parsed) request_data = AuthToken.parse_obj(request.form_parsed)
token = request_data.token token = request_data.token
IN_MEMORY_DB[token] = { IN_MEMORY_DB["TOKENS"]["LOGIN"][token] = {
"intention": f"Authenticate user: {request_data.login}", "intention": f"Authenticate user: {request_data.login}",
"created": utc_now_as_str(),
"status": "awaiting", "status": "awaiting",
"token_type": "cli_confirmation",
"login": request_data.login, "login": request_data.login,
} }
current_app.add_background_task( current_app.add_background_task(
expire_key, expire_key,
IN_MEMORY_DB, IN_MEMORY_DB["TOKENS"]["LOGIN"],
token, token,
120, 120,
) )
@ -244,10 +246,10 @@ async def login_token_verify():
request_data = TokenConfirmation.parse_obj(request.form_parsed) request_data = TokenConfirmation.parse_obj(request.form_parsed)
token_status, token_login, token_confirmation_code = map( token_status, token_login, token_confirmation_code = map(
IN_MEMORY_DB.get(request_data.token, {}).get, IN_MEMORY_DB["TOKENS"]["LOGIN"].get(request_data.token, {}).get,
["status", "login", "code"], ["status", "login", "code"],
) )
IN_MEMORY_DB.pop(request_data.token, None) IN_MEMORY_DB["TOKENS"]["LOGIN"].pop(request_data.token, None)
if ( if (
token_status != "confirmed" token_status != "confirmed"
@ -328,17 +330,17 @@ async def register_token():
try: try:
request_data = AuthToken.parse_obj(request.form_parsed) request_data = AuthToken.parse_obj(request.form_parsed)
token = request_data.token token = request_data.token
IN_MEMORY_DB[token] = { IN_MEMORY_DB["TOKENS"]["REGISTER"][token] = {
"intention": f"Register user: {request_data.login}", "intention": f"Register user: {request_data.login}",
"created": utc_now_as_str(),
"status": "awaiting", "status": "awaiting",
"token_type": "cli_confirmation",
"login": request_data.login, "login": request_data.login,
} }
current_app.add_background_task( current_app.add_background_task(
expire_key, expire_key,
IN_MEMORY_DB, IN_MEMORY_DB["TOKENS"]["REGISTER"],
token, token,
120, defaults.REGISTER_REQUEST_TIMEOUT,
) )
await ws_htmx( await ws_htmx(
"_system", "_system",
@ -365,10 +367,10 @@ async def register_webauthn_options():
return validation_error(e.errors()) return validation_error(e.errors())
token_status, token_login, token_confirmation_code = map( token_status, token_login, token_confirmation_code = map(
IN_MEMORY_DB.get(request_data.token, {}).get, IN_MEMORY_DB["TOKENS"]["REGISTER"].get(request_data.token, {}).get,
["status", "login", "code"], ["status", "login", "code"],
) )
IN_MEMORY_DB.pop(request_data.token, None) IN_MEMORY_DB["TOKENS"]["REGISTER"].pop(request_data.token, None)
if ( if (
token_status != "confirmed" token_status != "confirmed"
@ -614,7 +616,7 @@ async def auth_login_verify():
Not setting session login and id for device that is confirming the proxy authentication Not setting session login and id for device that is confirming the proxy authentication
Gracing 10s for the awaiting party to catch up an almost expired key Gracing 10s for the awaiting party to catch up an almost expired key
""" """
IN_MEMORY_DB[request_token].update( IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token].update(
{ {
"status": "confirmed", "status": "confirmed",
"credential_id": credential.raw_id.hex(), "credential_id": credential.raw_id.hex(),
@ -622,7 +624,7 @@ async def auth_login_verify():
) )
current_app.add_background_task( current_app.add_background_task(
expire_key, expire_key,
IN_MEMORY_DB, IN_MEMORY_DB["TOKENS"]["LOGIN"],
request_token, request_token,
10, 10,
) )

View File

@ -0,0 +1,65 @@
import components.users
from components.models.users import UserGroups
from components.web.utils import *
blueprint = Blueprint("groups", __name__, url_prefix="/system/groups")
@blueprint.context_processor
def load_context():
from components.models.users import UserProfile
context = dict()
context["schemas"] = {"user_profile": UserProfile.model_json_schema()}
return context
@blueprint.route("/", methods=["PATCH"])
@acl("system")
async def user_group():
try:
request_data = UserGroups.parse_obj(request.form_parsed)
assigned_to = [
u
for u in await components.users.search(name="", join_credentials=False)
if request_data.name in u.groups
]
assign_to = []
for user_id in request_data.members:
assign_to.append(
await components.users.get(user_id=user_id, join_credentials=False)
)
_all = assigned_to + assign_to
async with ClusterLock(["users", "credentials"], current_app):
for user in _all:
user_dict = user.model_dump(mode="json")
if request_data.name in user_dict["groups"]:
user_dict["groups"].remove(request_data.name)
if (
request_data.new_name not in user_dict["groups"]
and user in assign_to
):
user_dict["groups"].append(request_data.new_name)
await components.users.patch(user_id=user.id, data=user_dict)
return "", 204
except ValidationError as e:
return validation_error(e.errors())
except ValueError as e:
name, message = e.args
return validation_error([{"loc": [name], "msg": message}])
@blueprint.route("/")
@acl("system")
@formoptions(["users"])
async def get_groups():
return await render_template("system/groups.html", data={})

View File

@ -1,7 +1,6 @@
import components.users import components.users
from components.utils import batch, ensure_list from components.utils import batch, ensure_list
from components.database import IN_MEMORY_DB from components.database import IN_MEMORY_DB
from components.models.users import UserGroups
from components.web.utils import * from components.web.utils import *
@ -17,49 +16,6 @@ def load_context():
return context return context
@blueprint.route("/groups", methods=["PATCH"])
@acl("system")
async def user_group():
try:
request_data = UserGroups.parse_obj(request.form_parsed)
assigned_to = [
u
for u in await components.users.search(name="", join_credentials=False)
if request_data.name in u.groups
]
assign_to = []
for user_id in request_data.members:
assign_to.append(
await components.users.get(user_id=user_id, join_credentials=False)
)
_all = assigned_to + assign_to
async with ClusterLock(["users", "credentials"], current_app):
for user in _all:
user_dict = user.model_dump(mode="json")
if request_data.name in user_dict["groups"]:
user_dict["groups"].remove(request_data.name)
if (
request_data.new_name not in user_dict["groups"]
and user in assign_to
):
user_dict["groups"].append(request_data.new_name)
await components.users.patch(user_id=user.id, data=user_dict)
return "", 204
except ValidationError as e:
return validation_error(e.errors())
except ValueError as e:
name, message = e.args
return validation_error([{"loc": [name], "msg": message}])
@blueprint.route("/<user_id>") @blueprint.route("/<user_id>")
@acl("system") @acl("system")
async def get_user(user_id: str): async def get_user(user_id: str):
@ -128,7 +84,9 @@ async def get_users():
}, },
) )
else: else:
return await render_template("system/users.html", data={}) return await render_template(
"system/users.html", data={"tokens": IN_MEMORY_DB["TOKENS"]}
)
@blueprint.route("/delete", methods=["POST"]) @blueprint.route("/delete", methods=["POST"])

View File

@ -1,12 +1,13 @@
@charset "UTF-8"; @charset "UTF-8";
/*! /*!
* Pico CSS v2.0.6 (https://picocss.com) * Pico CSS v2.1.1 (https://picocss.com)
* Copyright 2019-2024 - Licensed under MIT * Copyright 2019-2025 - Licensed under MIT
*/ */
/** /**
* Styles * Styles
*/ */
:root { :root,
:host {
--pico-font-family-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --pico-font-family-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--pico-font-family-sans-serif: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji); --pico-font-family-sans-serif: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji);
--pico-font-family-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--pico-font-family-emoji); --pico-font-family-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--pico-font-family-emoji);
@ -271,12 +272,14 @@ details summary[role=button]:not(.outline)::after {
* Color schemes * Color schemes
*/ */
[data-theme=light], [data-theme=light],
:root:not([data-theme=dark]) { :root:not([data-theme=dark]),
:host(:not([data-theme=dark])) {
color-scheme: light;
--pico-background-color: #fff; --pico-background-color: #fff;
--pico-color: #373c44; --pico-color: #373c44;
--pico-text-selection-color: rgba(129, 145, 181, 0.25); --pico-text-selection-color: rgba(129, 145, 181, 0.25);
--pico-muted-color: #646b79; --pico-muted-color: #646b79;
--pico-muted-border-color: #e7eaf0; --pico-muted-border-color: rgb(231, 234, 239.5);
--pico-primary: #5d6b89; --pico-primary: #5d6b89;
--pico-primary-background: #525f7a; --pico-primary-background: #525f7a;
--pico-primary-border: var(--pico-primary-background); --pico-primary-border: var(--pico-primary-background);
@ -314,21 +317,21 @@ details summary[role=button]:not(.outline)::after {
--pico-h4-color: #4d535e; --pico-h4-color: #4d535e;
--pico-h5-color: #5c6370; --pico-h5-color: #5c6370;
--pico-h6-color: #646b79; --pico-h6-color: #646b79;
--pico-mark-background-color: #fde7c0; --pico-mark-background-color: rgb(252.5, 230.5, 191.5);
--pico-mark-color: #0f1114; --pico-mark-color: #0f1114;
--pico-ins-color: #1d6a54; --pico-ins-color: rgb(28.5, 105.5, 84);
--pico-del-color: #883935; --pico-del-color: rgb(136, 56.5, 53);
--pico-blockquote-border-color: var(--pico-muted-border-color); --pico-blockquote-border-color: var(--pico-muted-border-color);
--pico-blockquote-footer-color: var(--pico-muted-color); --pico-blockquote-footer-color: var(--pico-muted-color);
--pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
--pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
--pico-table-border-color: var(--pico-muted-border-color); --pico-table-border-color: var(--pico-muted-border-color);
--pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375);
--pico-code-background-color: #f3f5f7; --pico-code-background-color: rgb(243, 244.5, 246.75);
--pico-code-color: #646b79; --pico-code-color: #646b79;
--pico-code-kbd-background-color: var(--pico-color); --pico-code-kbd-background-color: var(--pico-color);
--pico-code-kbd-color: var(--pico-background-color); --pico-code-kbd-color: var(--pico-background-color);
--pico-form-element-background-color: #fbfcfc; --pico-form-element-background-color: rgb(251, 251.5, 252.25);
--pico-form-element-selected-background-color: #dfe3eb; --pico-form-element-selected-background-color: #dfe3eb;
--pico-form-element-border-color: #cfd5e2; --pico-form-element-border-color: #cfd5e2;
--pico-form-element-color: #23262c; --pico-form-element-color: #23262c;
@ -337,11 +340,11 @@ details summary[role=button]:not(.outline)::after {
--pico-form-element-active-border-color: var(--pico-primary-border); --pico-form-element-active-border-color: var(--pico-primary-border);
--pico-form-element-focus-color: var(--pico-primary-border); --pico-form-element-focus-color: var(--pico-primary-border);
--pico-form-element-disabled-opacity: 0.5; --pico-form-element-disabled-opacity: 0.5;
--pico-form-element-invalid-border-color: #b86a6b; --pico-form-element-invalid-border-color: rgb(183.5, 105.5, 106.5);
--pico-form-element-invalid-active-border-color: #c84f48; --pico-form-element-invalid-active-border-color: rgb(200.25, 79.25, 72.25);
--pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color);
--pico-form-element-valid-border-color: #4c9b8a; --pico-form-element-valid-border-color: rgb(76, 154.5, 137.5);
--pico-form-element-valid-active-border-color: #279977; --pico-form-element-valid-active-border-color: rgb(39, 152.75, 118.75);
--pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color);
--pico-switch-background-color: #bfc7d9; --pico-switch-background-color: #bfc7d9;
--pico-switch-checked-background-color: var(--pico-primary-background); --pico-switch-checked-background-color: var(--pico-primary-background);
@ -359,7 +362,7 @@ details summary[role=button]:not(.outline)::after {
--pico-card-background-color: var(--pico-background-color); --pico-card-background-color: var(--pico-background-color);
--pico-card-border-color: var(--pico-muted-border-color); --pico-card-border-color: var(--pico-muted-border-color);
--pico-card-box-shadow: var(--pico-box-shadow); --pico-card-box-shadow: var(--pico-box-shadow);
--pico-card-sectioning-background-color: #fbfcfc; --pico-card-sectioning-background-color: rgb(251, 251.5, 252.25);
--pico-dropdown-background-color: #fff; --pico-dropdown-background-color: #fff;
--pico-dropdown-border-color: #eff1f4; --pico-dropdown-border-color: #eff1f4;
--pico-dropdown-box-shadow: var(--pico-box-shadow); --pico-dropdown-box-shadow: var(--pico-box-shadow);
@ -371,9 +374,8 @@ details summary[role=button]:not(.outline)::after {
--pico-progress-color: var(--pico-primary-background); --pico-progress-color: var(--pico-primary-background);
--pico-tooltip-background-color: var(--pico-contrast-background); --pico-tooltip-background-color: var(--pico-contrast-background);
--pico-tooltip-color: var(--pico-contrast-inverse); --pico-tooltip-color: var(--pico-contrast-inverse);
--pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 155, 138)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
--pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200, 79, 72)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
color-scheme: light;
} }
[data-theme=light] input:is([type=submit], [data-theme=light] input:is([type=submit],
[type=button], [type=button],
@ -386,13 +388,21 @@ details summary[role=button]:not(.outline)::after {
[type=reset], [type=reset],
[type=checkbox], [type=checkbox],
[type=radio], [type=radio],
[type=file]),
:host(:not([data-theme=dark])) input:is([type=submit],
[type=button],
[type=reset],
[type=checkbox],
[type=radio],
[type=file]) { [type=file]) {
--pico-form-element-focus-color: var(--pico-primary-focus); --pico-form-element-focus-color: var(--pico-primary-focus);
} }
@media only screen and (prefers-color-scheme: dark) { @media only screen and (prefers-color-scheme: dark) {
:root:not([data-theme]) { :root:not([data-theme]),
--pico-background-color: #13171f; :host(:not([data-theme])) {
color-scheme: dark;
--pico-background-color: rgb(19, 22.5, 30.5);
--pico-color: #c2c7d0; --pico-color: #c2c7d0;
--pico-text-selection-color: rgba(144, 158, 190, 0.1875); --pico-text-selection-color: rgba(144, 158, 190, 0.1875);
--pico-muted-color: #7b8495; --pico-muted-color: #7b8495;
@ -427,7 +437,7 @@ details summary[role=button]:not(.outline)::after {
--pico-contrast-hover-underline: var(--pico-contrast-hover); --pico-contrast-hover-underline: var(--pico-contrast-hover);
--pico-contrast-focus: rgba(207, 213, 226, 0.25); --pico-contrast-focus: rgba(207, 213, 226, 0.25);
--pico-contrast-inverse: #000; --pico-contrast-inverse: #000;
--pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 9, 12, 0.06), 0 0 0 0.0625rem rgba(7, 9, 12, 0.015); --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06), 0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);
--pico-h1-color: #f0f1f3; --pico-h1-color: #f0f1f3;
--pico-h2-color: #e0e3e7; --pico-h2-color: #e0e3e7;
--pico-h3-color: #c2c7d0; --pico-h3-color: #c2c7d0;
@ -437,31 +447,31 @@ details summary[role=button]:not(.outline)::after {
--pico-mark-background-color: #014063; --pico-mark-background-color: #014063;
--pico-mark-color: #fff; --pico-mark-color: #fff;
--pico-ins-color: #62af9a; --pico-ins-color: #62af9a;
--pico-del-color: #ce7e7b; --pico-del-color: rgb(205.5, 126, 123);
--pico-blockquote-border-color: var(--pico-muted-border-color); --pico-blockquote-border-color: var(--pico-muted-border-color);
--pico-blockquote-footer-color: var(--pico-muted-color); --pico-blockquote-footer-color: var(--pico-muted-color);
--pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
--pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
--pico-table-border-color: var(--pico-muted-border-color); --pico-table-border-color: var(--pico-muted-border-color);
--pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375);
--pico-code-background-color: #1a1f28; --pico-code-background-color: rgb(26, 30.5, 40.25);
--pico-code-color: #8891a4; --pico-code-color: #8891a4;
--pico-code-kbd-background-color: var(--pico-color); --pico-code-kbd-background-color: var(--pico-color);
--pico-code-kbd-color: var(--pico-background-color); --pico-code-kbd-color: var(--pico-background-color);
--pico-form-element-background-color: #1c212c; --pico-form-element-background-color: rgb(28, 33, 43.5);
--pico-form-element-selected-background-color: #2a3140; --pico-form-element-selected-background-color: #2a3140;
--pico-form-element-border-color: #2a3140; --pico-form-element-border-color: #2a3140;
--pico-form-element-color: #e0e3e7; --pico-form-element-color: #e0e3e7;
--pico-form-element-placeholder-color: #8891a4; --pico-form-element-placeholder-color: #8891a4;
--pico-form-element-active-background-color: #1a1f28; --pico-form-element-active-background-color: rgb(26, 30.5, 40.25);
--pico-form-element-active-border-color: var(--pico-primary-border); --pico-form-element-active-border-color: var(--pico-primary-border);
--pico-form-element-focus-color: var(--pico-primary-border); --pico-form-element-focus-color: var(--pico-primary-border);
--pico-form-element-disabled-opacity: 0.5; --pico-form-element-disabled-opacity: 0.5;
--pico-form-element-invalid-border-color: #964a50; --pico-form-element-invalid-border-color: rgb(149.5, 74, 80);
--pico-form-element-invalid-active-border-color: #b7403b; --pico-form-element-invalid-active-border-color: rgb(183.25, 63.5, 59);
--pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color);
--pico-form-element-valid-border-color: #2a7b6f; --pico-form-element-valid-border-color: #2a7b6f;
--pico-form-element-valid-active-border-color: #16896a; --pico-form-element-valid-active-border-color: rgb(22, 137, 105.5);
--pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color);
--pico-switch-background-color: #333c4e; --pico-switch-background-color: #333c4e;
--pico-switch-checked-background-color: var(--pico-primary-background); --pico-switch-checked-background-color: var(--pico-primary-background);
@ -479,43 +489,55 @@ details summary[role=button]:not(.outline)::after {
--pico-card-background-color: #181c25; --pico-card-background-color: #181c25;
--pico-card-border-color: var(--pico-card-background-color); --pico-card-border-color: var(--pico-card-background-color);
--pico-card-box-shadow: var(--pico-box-shadow); --pico-card-box-shadow: var(--pico-box-shadow);
--pico-card-sectioning-background-color: #1a1f28; --pico-card-sectioning-background-color: rgb(26, 30.5, 40.25);
--pico-dropdown-background-color: #181c25; --pico-dropdown-background-color: #181c25;
--pico-dropdown-border-color: #202632; --pico-dropdown-border-color: #202632;
--pico-dropdown-box-shadow: var(--pico-box-shadow); --pico-dropdown-box-shadow: var(--pico-box-shadow);
--pico-dropdown-color: var(--pico-color); --pico-dropdown-color: var(--pico-color);
--pico-dropdown-hover-background-color: #202632; --pico-dropdown-hover-background-color: #202632;
--pico-loading-spinner-opacity: 0.5; --pico-loading-spinner-opacity: 0.5;
--pico-modal-overlay-background-color: rgba(8, 9, 10, 0.75); --pico-modal-overlay-background-color: rgba(7.5, 8.5, 10, 0.75);
--pico-progress-background-color: #202632; --pico-progress-background-color: #202632;
--pico-progress-color: var(--pico-primary-background); --pico-progress-color: var(--pico-primary-background);
--pico-tooltip-background-color: var(--pico-contrast-background); --pico-tooltip-background-color: var(--pico-contrast-background);
--pico-tooltip-color: var(--pico-contrast-inverse); --pico-tooltip-color: var(--pico-contrast-inverse);
--pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
--pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
color-scheme: dark;
} }
:root:not([data-theme]) input:is([type=submit], :root:not([data-theme]) input:is([type=submit],
[type=button], [type=button],
[type=reset], [type=reset],
[type=checkbox], [type=checkbox],
[type=radio], [type=radio],
[type=file]),
:host(:not([data-theme])) input:is([type=submit],
[type=button],
[type=reset],
[type=checkbox],
[type=radio],
[type=file]) { [type=file]) {
--pico-form-element-focus-color: var(--pico-primary-focus); --pico-form-element-focus-color: var(--pico-primary-focus);
} }
:root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after { :root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after,
:host(:not([data-theme])) details summary[role=button].contrast:not(.outline)::after {
filter: brightness(0); filter: brightness(0);
} }
:root:not([data-theme]) [aria-busy=true]:not(input, select, textarea).contrast:is(button, :root:not([data-theme]) [aria-busy=true]:not(input, select, textarea).contrast:is(button,
[type=submit], [type=submit],
[type=button], [type=button],
[type=reset], [type=reset],
[role=button]):not(.outline)::before,
:host(:not([data-theme])) [aria-busy=true]:not(input, select, textarea).contrast:is(button,
[type=submit],
[type=button],
[type=reset],
[role=button]):not(.outline)::before { [role=button]):not(.outline)::before {
filter: brightness(0); filter: brightness(0);
} }
} }
[data-theme=dark] { [data-theme=dark] {
--pico-background-color: #13171f; color-scheme: dark;
--pico-background-color: rgb(19, 22.5, 30.5);
--pico-color: #c2c7d0; --pico-color: #c2c7d0;
--pico-text-selection-color: rgba(144, 158, 190, 0.1875); --pico-text-selection-color: rgba(144, 158, 190, 0.1875);
--pico-muted-color: #7b8495; --pico-muted-color: #7b8495;
@ -550,7 +572,7 @@ details summary[role=button]:not(.outline)::after {
--pico-contrast-hover-underline: var(--pico-contrast-hover); --pico-contrast-hover-underline: var(--pico-contrast-hover);
--pico-contrast-focus: rgba(207, 213, 226, 0.25); --pico-contrast-focus: rgba(207, 213, 226, 0.25);
--pico-contrast-inverse: #000; --pico-contrast-inverse: #000;
--pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 9, 12, 0.06), 0 0 0 0.0625rem rgba(7, 9, 12, 0.015); --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06), 0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);
--pico-h1-color: #f0f1f3; --pico-h1-color: #f0f1f3;
--pico-h2-color: #e0e3e7; --pico-h2-color: #e0e3e7;
--pico-h3-color: #c2c7d0; --pico-h3-color: #c2c7d0;
@ -560,31 +582,31 @@ details summary[role=button]:not(.outline)::after {
--pico-mark-background-color: #014063; --pico-mark-background-color: #014063;
--pico-mark-color: #fff; --pico-mark-color: #fff;
--pico-ins-color: #62af9a; --pico-ins-color: #62af9a;
--pico-del-color: #ce7e7b; --pico-del-color: rgb(205.5, 126, 123);
--pico-blockquote-border-color: var(--pico-muted-border-color); --pico-blockquote-border-color: var(--pico-muted-border-color);
--pico-blockquote-footer-color: var(--pico-muted-color); --pico-blockquote-footer-color: var(--pico-muted-color);
--pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
--pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
--pico-table-border-color: var(--pico-muted-border-color); --pico-table-border-color: var(--pico-muted-border-color);
--pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375);
--pico-code-background-color: #1a1f28; --pico-code-background-color: rgb(26, 30.5, 40.25);
--pico-code-color: #8891a4; --pico-code-color: #8891a4;
--pico-code-kbd-background-color: var(--pico-color); --pico-code-kbd-background-color: var(--pico-color);
--pico-code-kbd-color: var(--pico-background-color); --pico-code-kbd-color: var(--pico-background-color);
--pico-form-element-background-color: #1c212c; --pico-form-element-background-color: rgb(28, 33, 43.5);
--pico-form-element-selected-background-color: #2a3140; --pico-form-element-selected-background-color: #2a3140;
--pico-form-element-border-color: #2a3140; --pico-form-element-border-color: #2a3140;
--pico-form-element-color: #e0e3e7; --pico-form-element-color: #e0e3e7;
--pico-form-element-placeholder-color: #8891a4; --pico-form-element-placeholder-color: #8891a4;
--pico-form-element-active-background-color: #1a1f28; --pico-form-element-active-background-color: rgb(26, 30.5, 40.25);
--pico-form-element-active-border-color: var(--pico-primary-border); --pico-form-element-active-border-color: var(--pico-primary-border);
--pico-form-element-focus-color: var(--pico-primary-border); --pico-form-element-focus-color: var(--pico-primary-border);
--pico-form-element-disabled-opacity: 0.5; --pico-form-element-disabled-opacity: 0.5;
--pico-form-element-invalid-border-color: #964a50; --pico-form-element-invalid-border-color: rgb(149.5, 74, 80);
--pico-form-element-invalid-active-border-color: #b7403b; --pico-form-element-invalid-active-border-color: rgb(183.25, 63.5, 59);
--pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color);
--pico-form-element-valid-border-color: #2a7b6f; --pico-form-element-valid-border-color: #2a7b6f;
--pico-form-element-valid-active-border-color: #16896a; --pico-form-element-valid-active-border-color: rgb(22, 137, 105.5);
--pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color);
--pico-switch-background-color: #333c4e; --pico-switch-background-color: #333c4e;
--pico-switch-checked-background-color: var(--pico-primary-background); --pico-switch-checked-background-color: var(--pico-primary-background);
@ -602,21 +624,20 @@ details summary[role=button]:not(.outline)::after {
--pico-card-background-color: #181c25; --pico-card-background-color: #181c25;
--pico-card-border-color: var(--pico-card-background-color); --pico-card-border-color: var(--pico-card-background-color);
--pico-card-box-shadow: var(--pico-box-shadow); --pico-card-box-shadow: var(--pico-box-shadow);
--pico-card-sectioning-background-color: #1a1f28; --pico-card-sectioning-background-color: rgb(26, 30.5, 40.25);
--pico-dropdown-background-color: #181c25; --pico-dropdown-background-color: #181c25;
--pico-dropdown-border-color: #202632; --pico-dropdown-border-color: #202632;
--pico-dropdown-box-shadow: var(--pico-box-shadow); --pico-dropdown-box-shadow: var(--pico-box-shadow);
--pico-dropdown-color: var(--pico-color); --pico-dropdown-color: var(--pico-color);
--pico-dropdown-hover-background-color: #202632; --pico-dropdown-hover-background-color: #202632;
--pico-loading-spinner-opacity: 0.5; --pico-loading-spinner-opacity: 0.5;
--pico-modal-overlay-background-color: rgba(8, 9, 10, 0.75); --pico-modal-overlay-background-color: rgba(7.5, 8.5, 10, 0.75);
--pico-progress-background-color: #202632; --pico-progress-background-color: #202632;
--pico-progress-color: var(--pico-primary-background); --pico-progress-color: var(--pico-primary-background);
--pico-tooltip-background-color: var(--pico-contrast-background); --pico-tooltip-background-color: var(--pico-contrast-background);
--pico-tooltip-color: var(--pico-contrast-inverse); --pico-tooltip-color: var(--pico-contrast-inverse);
--pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
--pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
color-scheme: dark;
} }
[data-theme=dark] input:is([type=submit], [data-theme=dark] input:is([type=submit],
[type=button], [type=button],
@ -661,7 +682,8 @@ progress,
vertical-align: inherit; vertical-align: inherit;
} }
:where(:root) { :where(:root),
:where(:host) {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
text-size-adjust: 100%; text-size-adjust: 100%;
@ -1229,7 +1251,8 @@ img {
fill: currentColor; fill: currentColor;
} }
svg:not(:root) { svg:not(:root),
svg:not(:host) {
overflow: hidden; overflow: hidden;
} }
@ -1244,7 +1267,8 @@ samp {
font-family: var(--pico-font-family); font-family: var(--pico-font-family);
} }
pre code { pre code,
pre samp {
font-size: inherit; font-size: inherit;
font-family: inherit; font-family: inherit;
} }
@ -1256,7 +1280,8 @@ pre {
pre, pre,
code, code,
kbd { kbd,
samp {
border-radius: var(--pico-border-radius); border-radius: var(--pico-border-radius);
background: var(--pico-code-background-color); background: var(--pico-code-background-color);
color: var(--pico-code-color); color: var(--pico-code-color);
@ -1265,7 +1290,8 @@ kbd {
} }
code, code,
kbd { kbd,
samp {
display: inline-block; display: inline-block;
padding: 0.375rem; padding: 0.375rem;
} }
@ -1275,7 +1301,8 @@ pre {
margin-bottom: var(--pico-spacing); margin-bottom: var(--pico-spacing);
overflow-x: auto; overflow-x: auto;
} }
pre > code { pre > code,
pre > samp {
display: block; display: block;
padding: var(--pico-spacing); padding: var(--pico-spacing);
background: none; background: none;
@ -1302,7 +1329,7 @@ figure figcaption {
} }
/** /**
* Miscs * Misc
*/ */
hr { hr {
height: 0; height: 0;
@ -2059,7 +2086,7 @@ details.dropdown {
position: relative; position: relative;
border-bottom: none; border-bottom: none;
} }
details.dropdown summary::after, details.dropdown > summary::after,
details.dropdown > button::after, details.dropdown > button::after,
details.dropdown > a::after { details.dropdown > a::after {
display: block; display: block;
@ -2079,7 +2106,7 @@ nav details.dropdown {
margin-bottom: 0; margin-bottom: 0;
} }
details.dropdown summary:not([role]) { details.dropdown > summary:not([role]) {
height: calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2); height: calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);
padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);
border: var(--pico-border-width) solid var(--pico-form-element-border-color); border: var(--pico-border-width) solid var(--pico-form-element-border-color);
@ -2091,22 +2118,22 @@ details.dropdown summary:not([role]) {
user-select: none; user-select: none;
transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition); transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition);
} }
details.dropdown summary:not([role]):active, details.dropdown summary:not([role]):focus { details.dropdown > summary:not([role]):active, details.dropdown > summary:not([role]):focus {
border-color: var(--pico-form-element-active-border-color); border-color: var(--pico-form-element-active-border-color);
background-color: var(--pico-form-element-active-background-color); background-color: var(--pico-form-element-active-background-color);
} }
details.dropdown summary:not([role]):focus { details.dropdown > summary:not([role]):focus {
box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color); box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color);
} }
details.dropdown summary:not([role]):focus-visible { details.dropdown > summary:not([role]):focus-visible {
outline: none; outline: none;
} }
details.dropdown summary:not([role])[aria-invalid=false] { details.dropdown > summary:not([role])[aria-invalid=false] {
--pico-form-element-border-color: var(--pico-form-element-valid-border-color); --pico-form-element-border-color: var(--pico-form-element-valid-border-color);
--pico-form-element-active-border-color: var(--pico-form-element-valid-focus-color); --pico-form-element-active-border-color: var(--pico-form-element-valid-focus-color);
--pico-form-element-focus-color: var(--pico-form-element-valid-focus-color); --pico-form-element-focus-color: var(--pico-form-element-valid-focus-color);
} }
details.dropdown summary:not([role])[aria-invalid=true] { details.dropdown > summary:not([role])[aria-invalid=true] {
--pico-form-element-border-color: var(--pico-form-element-invalid-border-color); --pico-form-element-border-color: var(--pico-form-element-invalid-border-color);
--pico-form-element-active-border-color: var(--pico-form-element-invalid-focus-color); --pico-form-element-active-border-color: var(--pico-form-element-invalid-focus-color);
--pico-form-element-focus-color: var(--pico-form-element-invalid-focus-color); --pico-form-element-focus-color: var(--pico-form-element-invalid-focus-color);
@ -2116,18 +2143,18 @@ nav details.dropdown {
display: inline; display: inline;
margin: calc(var(--pico-nav-element-spacing-vertical) * -1) 0; margin: calc(var(--pico-nav-element-spacing-vertical) * -1) 0;
} }
nav details.dropdown summary::after { nav details.dropdown > summary::after {
transform: rotate(0deg) translateX(0rem); transform: rotate(0deg) translateX(0rem);
} }
nav details.dropdown summary:not([role]) { nav details.dropdown > summary:not([role]) {
height: calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2); height: calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2);
padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal); padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal);
} }
nav details.dropdown summary:not([role]):focus-visible { nav details.dropdown > summary:not([role]):focus-visible {
box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus);
} }
details.dropdown summary + ul { details.dropdown > summary + ul {
display: flex; display: flex;
z-index: 99; z-index: 99;
position: absolute; position: absolute;
@ -2147,23 +2174,23 @@ details.dropdown summary + ul {
opacity: 0; opacity: 0;
transition: opacity var(--pico-transition), transform 0s ease-in-out 1s; transition: opacity var(--pico-transition), transform 0s ease-in-out 1s;
} }
details.dropdown summary + ul[dir=rtl] { details.dropdown > summary + ul[dir=rtl] {
right: 0; right: 0;
left: auto; left: auto;
} }
details.dropdown summary + ul li { details.dropdown > summary + ul li {
width: 100%; width: 100%;
margin-bottom: 0; margin-bottom: 0;
padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal);
list-style: none; list-style: none;
} }
details.dropdown summary + ul li:first-of-type { details.dropdown > summary + ul li:first-of-type {
margin-top: calc(var(--pico-form-element-spacing-vertical) * 0.5); margin-top: calc(var(--pico-form-element-spacing-vertical) * 0.5);
} }
details.dropdown summary + ul li:last-of-type { details.dropdown > summary + ul li:last-of-type {
margin-bottom: calc(var(--pico-form-element-spacing-vertical) * 0.5); margin-bottom: calc(var(--pico-form-element-spacing-vertical) * 0.5);
} }
details.dropdown summary + ul li a { details.dropdown > summary + ul li a {
display: block; display: block;
margin: calc(var(--pico-form-element-spacing-vertical) * -0.5) calc(var(--pico-form-element-spacing-horizontal) * -1); margin: calc(var(--pico-form-element-spacing-vertical) * -0.5) calc(var(--pico-form-element-spacing-horizontal) * -1);
padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal);
@ -2173,27 +2200,27 @@ details.dropdown summary + ul li a {
text-decoration: none; text-decoration: none;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
details.dropdown summary + ul li a:hover, details.dropdown summary + ul li a:focus, details.dropdown summary + ul li a:active, details.dropdown summary + ul li a:focus-visible, details.dropdown summary + ul li a[aria-current]:not([aria-current=false]) { details.dropdown > summary + ul li a:hover, details.dropdown > summary + ul li a:focus, details.dropdown > summary + ul li a:active, details.dropdown > summary + ul li a:focus-visible, details.dropdown > summary + ul li a[aria-current]:not([aria-current=false]) {
background-color: var(--pico-dropdown-hover-background-color); background-color: var(--pico-dropdown-hover-background-color);
} }
details.dropdown summary + ul li label { details.dropdown > summary + ul li label {
width: 100%; width: 100%;
} }
details.dropdown summary + ul li:has(label):hover { details.dropdown > summary + ul li:has(label):hover {
background-color: var(--pico-dropdown-hover-background-color); background-color: var(--pico-dropdown-hover-background-color);
} }
details.dropdown[open] summary { details.dropdown[open] > summary {
margin-bottom: 0; margin-bottom: 0;
} }
details.dropdown[open] summary + ul { details.dropdown[open] > summary + ul {
transform: scaleY(1); transform: scaleY(1);
opacity: 1; opacity: 1;
transition: opacity var(--pico-transition), transform 0s ease-in-out 0s; transition: opacity var(--pico-transition), transform 0s ease-in-out 0s;
} }
details.dropdown[open] summary::before { details.dropdown[open] > summary::before {
display: block; display: block;
z-index: 1; z-index: 1;
position: fixed; position: fixed;
@ -2340,10 +2367,10 @@ label > details.dropdown {
/** /**
* Loading ([aria-busy=true]) * Loading ([aria-busy=true])
*/ */
[aria-busy=true]:not(input, select, textarea, html) { [aria-busy=true]:not(input, select, textarea, html, form) {
white-space: nowrap; white-space: nowrap;
} }
[aria-busy=true]:not(input, select, textarea, html)::before { [aria-busy=true]:not(input, select, textarea, html, form)::before {
display: inline-block; display: inline-block;
width: 1em; width: 1em;
height: 1em; height: 1em;
@ -2353,10 +2380,10 @@ label > details.dropdown {
content: ""; content: "";
vertical-align: -0.125em; vertical-align: -0.125em;
} }
[aria-busy=true]:not(input, select, textarea, html):not(:empty)::before { [aria-busy=true]:not(input, select, textarea, html, form):not(:empty)::before {
margin-inline-end: calc(var(--pico-spacing) * 0.5); margin-inline-end: calc(var(--pico-spacing) * 0.5);
} }
[aria-busy=true]:not(input, select, textarea, html):empty { [aria-busy=true]:not(input, select, textarea, html, form):empty {
text-align: center; text-align: center;
} }
@ -2372,7 +2399,8 @@ a[aria-busy=true] {
/** /**
* Modal (<dialog>) * Modal (<dialog>)
*/ */
:root { :root,
:host {
--pico-scrollbar-width: 0px; --pico-scrollbar-width: 0px;
} }
@ -2396,43 +2424,43 @@ dialog {
background-color: var(--pico-modal-overlay-background-color); background-color: var(--pico-modal-overlay-background-color);
color: var(--pico-color); color: var(--pico-color);
} }
dialog article { dialog > article {
width: 100%; width: 100%;
max-height: calc(100vh - var(--pico-spacing) * 2); max-height: calc(100vh - var(--pico-spacing) * 2);
margin: var(--pico-spacing); margin: var(--pico-spacing);
overflow: auto; overflow: auto;
} }
@media (min-width: 576px) { @media (min-width: 576px) {
dialog article { dialog > article {
max-width: 95%; max-width: 95%;
} }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
dialog article { dialog > article {
max-width: 95%; max-width: 95%;
} }
} }
dialog article > header > * { dialog > article > header > * {
margin-bottom: 0; margin-bottom: 0;
} }
dialog article > header .close, dialog article > header :is(a, button)[rel=prev] { dialog > article > header .close, dialog > article > header :is(a, button)[rel=prev] {
margin: 0; margin: 0;
margin-left: var(--pico-spacing); margin-left: var(--pico-spacing);
padding: 0; padding: 0;
float: right; float: right;
} }
dialog article > footer { dialog > article > footer {
text-align: right; text-align: right;
} }
dialog article > footer button, dialog > article > footer button,
dialog article > footer [role=button] { dialog > article > footer [role=button] {
margin-bottom: 0; margin-bottom: 0;
} }
dialog article > footer button:not(:first-of-type), dialog > article > footer button:not(:first-of-type),
dialog article > footer [role=button]:not(:first-of-type) { dialog > article > footer [role=button]:not(:first-of-type) {
margin-left: calc(var(--pico-spacing) * 0.5); margin-left: calc(var(--pico-spacing) * 0.5);
} }
dialog article .close, dialog article :is(a, button)[rel=prev] { dialog > article .close, dialog > article :is(a, button)[rel=prev] {
display: block; display: block;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@ -2448,7 +2476,7 @@ dialog article .close, dialog article :is(a, button)[rel=prev] {
opacity: 0.5; opacity: 0.5;
transition: opacity var(--pico-transition); transition: opacity var(--pico-transition);
} }
dialog article .close:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), dialog article :is(a, button)[rel=prev]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { dialog > article .close:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), dialog > article :is(a, button)[rel=prev]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) {
opacity: 1; opacity: 1;
} }
dialog:not([open]), dialog[open=false] { dialog:not([open]), dialog[open=false] {
@ -2671,7 +2699,7 @@ progress::-moz-progress-bar {
[data-tooltip] { [data-tooltip] {
position: relative; position: relative;
} }
[data-tooltip]:not(a, button, input) { [data-tooltip]:not(a, button, input, [role=button]) {
border-bottom: 1px dotted; border-bottom: 1px dotted;
text-decoration: none; text-decoration: none;
cursor: help; cursor: help;
@ -3220,6 +3248,28 @@ fieldset.vault-unlock {
padding: var(--pico-spacing) 0; padding: var(--pico-spacing) 0;
} }
article.user-group {
background-color: var(--pico-form-element-background-color);
}
hgroup ol, hgroup ul {
margin-block: calc(var(--pico-spacing) / 2);
}
@media (max-width: 1536px) {
.grid-auto-cols {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: calc(var(--pico-spacing) / 2);
}
}
@media (max-width: 1280px) {
.grid-auto-cols {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: calc(var(--pico-spacing) / 2);
}
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.split-grid.grid { .split-grid.grid {
gap: calc(var(--pico-spacing) / 2); gap: calc(var(--pico-spacing) / 2);
@ -3231,17 +3281,9 @@ fieldset.vault-unlock {
} }
.grid-auto-cols { .grid-auto-cols {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(2, 1fr);
gap: calc(var(--pico-spacing) / 2); gap: calc(var(--pico-spacing) / 2);
} }
.grid-auto-cols .grid-span-all {
grid-column: 1/-1;
}
.grid-auto-cols > article {
margin-bottom: 0;
padding-bottom: var(--pico-form-element-spacing-vertical);
--pico-border-radius: .5rem;
}
th, td { th, td {
--pico-spacing: 0.75rem; --pico-spacing: 0.75rem;
} }
@ -3260,13 +3302,6 @@ fieldset.vault-unlock {
grid-template-columns: repeat(1, 1fr); grid-template-columns: repeat(1, 1fr);
gap: calc(var(--pico-spacing) / 2); gap: calc(var(--pico-spacing) / 2);
} }
.grid-auto-cols .grid-span-all {
grid-column: 1/-1;
}
.grid-auto-cols > article {
margin-bottom: 0;
--pico-border-radius: .5rem;
}
} }
.user-group { .user-group {
opacity: 0; opacity: 0;

View File

@ -101,6 +101,8 @@
$sm-breakpoint: map-get(map-get($breakpoints, sm), breakpoint); $sm-breakpoint: map-get(map-get($breakpoints, sm), breakpoint);
$md-breakpoint: map-get(map-get($breakpoints, md), breakpoint); $md-breakpoint: map-get(map-get($breakpoints, md), breakpoint);
$lg-breakpoint: map-get(map-get($breakpoints, lg), breakpoint); $lg-breakpoint: map-get(map-get($breakpoints, lg), breakpoint);
$xl-breakpoint: map-get(map-get($breakpoints, xl), breakpoint);
$xxl-breakpoint: map-get(map-get($breakpoints, xxl), breakpoint);
small { small {
font-size: .875rem; font-size: .875rem;
@ -353,9 +355,27 @@ fieldset.vault-unlock {
} }
article.user-group { article.user-group {
background-color: var(--pico-form-element-background-color);
} }
hgroup ol, hgroup ul {
margin-block: calc(var(--pico-spacing)/2);
}
@media (max-width: $xxl-breakpoint) {
.grid-auto-cols {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: calc(var(--pico-spacing)/2);
}
}
@media (max-width: $xl-breakpoint) {
.grid-auto-cols {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: calc(var(--pico-spacing)/2);
}
}
@media (max-width: $lg-breakpoint) { @media (max-width: $lg-breakpoint) {
.split-grid.grid { .split-grid.grid {
gap: calc(var(--pico-spacing)/2); gap: calc(var(--pico-spacing)/2);
@ -367,17 +387,9 @@ article.user-group {
} }
.grid-auto-cols { .grid-auto-cols {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(2, 1fr);
gap: calc(var(--pico-spacing)/2); gap: calc(var(--pico-spacing)/2);
} }
.grid-auto-cols .grid-span-all {
grid-column: 1 / -1;
}
.grid-auto-cols > article {
margin-bottom: 0;
padding-bottom: var(--pico-form-element-spacing-vertical);
--pico-border-radius: .5rem;
}
th, td { th, td {
--pico-spacing: 0.75rem; --pico-spacing: 0.75rem;
} }
@ -396,13 +408,6 @@ article.user-group {
grid-template-columns: repeat(1, 1fr); grid-template-columns: repeat(1, 1fr);
gap: calc(var(--pico-spacing)/2); gap: calc(var(--pico-spacing)/2);
} }
.grid-auto-cols .grid-span-all {
grid-column: 1 / -1;
}
.grid-auto-cols > article {
margin-bottom: 0;
--pico-border-radius: .5rem;
}
} }
.user-group { .user-group {
@ -504,3 +509,4 @@ fieldset.keypair, fieldset.tresor {
} }
} }
} }

View File

@ -51,6 +51,9 @@
<li> <li>
<a href="#" hx-get="/system/users">Users</a> <a href="#" hx-get="/system/users">Users</a>
</li> </li>
<li>
<a href="#" hx-get="/system/groups">Groups</a>
</li>
<li> <li>
<a href="#" hx-get="/system/logs">Logs</a> <a href="#" hx-get="/system/logs">Logs</a>
</li> </li>

View File

@ -0,0 +1,49 @@
{% if not request.headers.get("Hx-Request") %}
{% extends "base.html" %}
{% endif %}
{% block breadcrumb %}
<nav aria-label="breadcrumb" id="nav-breadcrumb" hx-swap-oob="true">
<ul>
<li>System</li>
<li><a href="#" hx-target="#body-main" hx-get="{{ request.path }}">Users</a></li>
</ul>
</nav>
{% endblock breadcrumb %}
{% block body %}
<h4>Groups</h4>
<div class="grid split-grid">
<article class="hide-below-lg">
<hgroup>
<h5>Groups object</h5>
<p>Create an object by defining a unique name.</p>
</hgroup>
<form hx-disable _="on submit
halt the event
add @disabled to <fieldset/> in me
if $names does not contain #group.value and #group.value is not empty
append #group.value to $names
render #group-template with (name: #group.value, members: [], newGroup: true)
put the result at the end of #user-groups
call htmx.process(#user-groups)
add @hidden to .no-user-groups
transition .user-group's opacity to 1 over 175ms
end
remove @disabled from <fieldset/> in me
end">
<fieldset>
<input autocomplete="off" id="group" name="group" type="text" placeholder="New group name" />
<button type="submit">Create</button>
</fieldset>
</form>
</article>
<article>
{% include "system/includes/users/groups.html" %}
</article>
</div>
{% endblock body %}

View File

@ -31,12 +31,14 @@
set group to closest .user-group to event.target set group to closest .user-group to event.target
set value of <[name=name]/> in group to value of <[name=new_name]/> in group set value of <[name=name]/> in group to value of <[name=new_name]/> in group
if <[name=members] option:checked/> in group is empty if <[name=members] option:checked/> in group is empty
log event.deta
add @hidden to group add @hidden to group
settle settle
remove group remove group
trigger deleteName trigger deleteName
else else
add @hidden to .user-group-unsaved in group add @hidden to .user-group-unsaved in group
remove .user-group-new
end end
end end
on change on change
@ -53,31 +55,12 @@
</mark> </mark>
</p> </p>
<section>
<form hx-disable _="on submit
halt the event
add @disabled to <fieldset/> in me
if $names does not contain #group.value and #group.value is not empty
append #group.value to $names
render #group-template with (name: #group.value, members: [])
put the result at the end of #user-groups
call htmx.process(#user-groups)
add @hidden to .no-user-groups
transition .user-group's opacity to 1 over 175ms
end
remove @disabled from <fieldset/> in me
end">
<fieldset>
<input autocomplete="off" id="group" name="group" type="text" placeholder="New group name" />
<button type="submit">Create</button>
</fieldset>
</form>
</section>
<template id="group-template"> <template id="group-template">
@set hidden to "hidden" unless members is empty @if members is not empty or newGroup
@set hidden to `hidden`
@end
<article class="user-group"> <article class="user-group">
<form hx-patch="/system/users/groups"> <form hx-patch="/system/groups">
<input type="hidden" name="name" value="${name}"/> <input type="hidden" name="name" value="${name}"/>
<fieldset data-loading-disable> <fieldset data-loading-disable>
<label>Group name</label> <label>Group name</label>
@ -98,14 +81,26 @@
{% endfor %} {% endfor %}
</select> </select>
</fieldset> </fieldset>
@if newGroup
<small class="color-orange-400 user-group-new"
_="on change from closest <form/>
if <[name=members] option:checked/> in event.target is not empty
remove me
end">❗ New group, select members and save group to apply</small>
@end
<small class="color-yellow-200 user-group-unsaved" ${hidden}>⚠️ unsaved</small> <small class="color-yellow-200 user-group-unsaved" ${hidden}>⚠️ unsaved</small>
<div class="grid-space-between"> <div class="grid-space-between">
<a href="#" _="install confirmButton <a href="#" _="install confirmButton
on confirmedButton on confirmedButton
set selectedIndex of <select/> in closest .user-group to -1 set selectedIndex of <select/> in closest <form/> to -1
trigger submit on closest <form/> trigger submit on closest <form/>
end">Remove</a> end">Remove</a>
<button data-loading-disable <button data-loading-disable _="on click
if (selectedIndex of <select/> in closest <form/>) < 0
trigger notification(level: 'validationError', message: 'Missing members', duration: 3000, locations: ['members'])
halt the event
exit
end"
class="button-green user-group-unsaved" class="button-green user-group-unsaved"
${hidden} ${hidden}
type="submit"> type="submit">

View File

@ -0,0 +1,31 @@
<hgroup>
<h6>Pending logins</h6>
{% if not data.tokens["LOGIN"] %}
<p>No pending requests</p>
{% else %}
<p>
<ol>
{% for token, token_data in data.tokens["LOGIN"].items() %}
<li><b>{{ token_data.intention }}</b> on <small _="init js return new Date('{{ token_data.created }}').toLocaleString() end then put result into me">{{ token_data.created }}</small></li>
{% endfor %}
</ol>
</p>
{% endif %}
</hgroup>
<hr>
<hgroup>
<h6>Pending registrations</h6>
{% if not data.tokens["REGISTER"] %}
<p>No pending requests</p>
{% else %}
<p>
<ol>
{% for token, token_data in data.tokens["REGISTER"].items() %}
<li><b>{{ token_data.intention }}</b> on <small _="init js return new Date('{{ token_data.created }}').toLocaleString() end then put result into me">{{ token_data.created }}</small></li>
{% endfor %}
</ol>
</p>
{% endif %}
</hgroup>

View File

@ -1,5 +1,5 @@
<tr <tr
hx-on:click="event.target.nodeName!=='INPUT'?(this.nextElementSibling.toggleAttribute('hidden'),this.nextElementSibling.hidden?null:this.scrollIntoView({behavior: 'smooth'})):null" hx-on:click="event.target.nodeName!=='INPUT'?this.nextElementSibling.toggleAttribute('hidden'):null"
hx-disinherit="*" hx-disinherit="*"
hx-get="/system/users/{{ user.id }}" hx-get="/system/users/{{ user.id }}"
hx-trigger="htmx:afterRequest[event.detail.successful==true] from:next .user-details" hx-trigger="htmx:afterRequest[event.detail.successful==true] from:next .user-details"

View File

@ -13,20 +13,23 @@
{% block body %} {% block body %}
<h4>Users and groups</h4> <h4>Users</h4>
<section> <details class="show-below-lg">
<h5>Groups</h5> <summary role="button" class="button-slate-800">Pending logins and registrations</summary>
{% include "system/includes/users/groups.html" %} <article>
</section> {% include "system/includes/users/pending_tokens.html" %}
</article>
<hr> </details>
<div class="grid split-grid"> <div class="grid split-grid">
<article class="hide-below-lg">
{% include "system/includes/users/pending_tokens.html" %}
</article>
<article id="system-users-full-table"> <article id="system-users-full-table">
<h5>Users</h5>
{% include "system/includes/users/table.html" %} {% include "system/includes/users/table.html" %}
</article> </article>
</div> </div>
{% endblock body %} {% endblock body %}

View File

@ -4,6 +4,7 @@ from pydantic import constr
ACCEPT_LANGUAGES = ["en", "de"] ACCEPT_LANGUAGES = ["en", "de"]
WEBAUTHN_CHALLENGE_TIMEOUT = 30 # seconds WEBAUTHN_CHALLENGE_TIMEOUT = 30 # seconds
AUTH_REQUEST_TIMEOUT = 300 # seconds AUTH_REQUEST_TIMEOUT = 300 # seconds
REGISTER_REQUEST_TIMEOUT = 300 # seconds
TABLE_PAGE_SIZE = 20 TABLE_PAGE_SIZE = 20
TRUSTED_PROXIES = ["127.0.0.1", "::1"] TRUSTED_PROXIES = ["127.0.0.1", "::1"]
TEMPLATES_AUTO_RELOAD = True TEMPLATES_AUTO_RELOAD = True

2
ctrl
View File

@ -23,7 +23,7 @@ parser.add_argument(
) )
parser.add_argument( parser.add_argument(
"-t", "-t",
"--confirm_token", "--confirm-token",
action="store_true", action="store_true",
help="Generate a token when prompted by the application", help="Generate a token when prompted by the application",
) )