2026-05-20 23:55:27 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Request, Response
|
|
|
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
|
from starlette.concurrency import run_in_threadpool
|
|
|
|
|
|
|
|
|
|
from ..dependencies import ServiceDep, SettingsDep, StoreDep
|
|
|
|
|
from ..event_statuses import DASHBOARD_PASSWORD_CHANGED, DASHBOARD_PASSWORD_ENABLED, DASHBOARD_PASSWORD_REMOVED
|
2026-05-21 17:23:08 +02:00
|
|
|
from ..security import client_ip, dashboard_csrf_token, require_dashboard_csrf
|
2026-05-20 23:55:27 +02:00
|
|
|
from ..web.forms import first_form_value, request_form
|
|
|
|
|
from ..web.templates_management import _render_management_unlock_page
|
|
|
|
|
from .management_shared import (
|
|
|
|
|
delete_unlock_cookie,
|
|
|
|
|
get_active_management_account_async,
|
|
|
|
|
has_management_access,
|
|
|
|
|
management_error_response_async,
|
|
|
|
|
render_management_page_async,
|
|
|
|
|
set_unlock_cookie,
|
|
|
|
|
unlock_cookie_seconds,
|
|
|
|
|
)
|
2026-05-21 10:05:40 +02:00
|
|
|
from .path_params import ManagementSlugPath
|
2026-05-20 23:55:27 +02:00
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/m/{management_slug}/unlock", include_in_schema=False)
|
2026-05-22 17:05:06 +02:00
|
|
|
async def unlock_dashboard(
|
|
|
|
|
management_slug: ManagementSlugPath, request: Request, settings: SettingsDep, store: StoreDep
|
|
|
|
|
) -> Response:
|
2026-05-20 23:55:27 +02:00
|
|
|
account = await get_active_management_account_async(store, management_slug)
|
|
|
|
|
if not account.get("dashboard_password_hash"):
|
|
|
|
|
return RedirectResponse(f"/m/{management_slug}", status_code=303)
|
|
|
|
|
form = await request_form(request, settings)
|
|
|
|
|
require_dashboard_csrf(management_slug, account, first_form_value(form, "csrf"))
|
|
|
|
|
password = first_form_value(form, "dashboard_password")
|
|
|
|
|
hostname = str(account["hostname"])
|
|
|
|
|
is_valid = await run_in_threadpool(store.verify_dashboard_password, hostname, password)
|
|
|
|
|
if not is_valid:
|
|
|
|
|
return HTMLResponse(
|
2026-05-22 17:05:06 +02:00
|
|
|
_render_management_unlock_page(
|
|
|
|
|
management_slug, account, dashboard_csrf_token(management_slug, account), "Incorrect password."
|
|
|
|
|
),
|
2026-05-20 23:55:27 +02:00
|
|
|
status_code=401,
|
|
|
|
|
)
|
|
|
|
|
response = RedirectResponse(f"/m/{management_slug}", status_code=303)
|
|
|
|
|
set_unlock_cookie(
|
|
|
|
|
response,
|
|
|
|
|
settings,
|
|
|
|
|
management_slug,
|
|
|
|
|
account,
|
|
|
|
|
unlock_cookie_seconds(settings, first_form_value(form, "stay_unlocked")),
|
|
|
|
|
)
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/m/{management_slug}/password", include_in_schema=False)
|
|
|
|
|
async def set_dashboard_password(
|
2026-05-21 10:05:40 +02:00
|
|
|
management_slug: ManagementSlugPath,
|
2026-05-20 23:55:27 +02:00
|
|
|
request: Request,
|
|
|
|
|
settings: SettingsDep,
|
|
|
|
|
store: StoreDep,
|
|
|
|
|
service: ServiceDep,
|
|
|
|
|
) -> Response:
|
|
|
|
|
account = await get_active_management_account_async(store, management_slug)
|
2026-05-21 10:05:40 +02:00
|
|
|
if not has_management_access(request, settings, management_slug, account):
|
2026-05-22 17:05:06 +02:00
|
|
|
return HTMLResponse(
|
|
|
|
|
_render_management_unlock_page(management_slug, account, dashboard_csrf_token(management_slug, account)),
|
|
|
|
|
status_code=401,
|
|
|
|
|
)
|
2026-05-20 23:55:27 +02:00
|
|
|
form = await request_form(request, settings)
|
|
|
|
|
require_dashboard_csrf(management_slug, account, first_form_value(form, "csrf"))
|
|
|
|
|
hostname = str(account["hostname"])
|
|
|
|
|
if account.get("dashboard_password_hash"):
|
|
|
|
|
is_current = await run_in_threadpool(
|
|
|
|
|
store.verify_dashboard_password,
|
|
|
|
|
hostname,
|
|
|
|
|
first_form_value(form, "current_dashboard_password"),
|
|
|
|
|
)
|
|
|
|
|
if not is_current:
|
2026-05-21 17:23:08 +02:00
|
|
|
return await management_error_response_async(
|
|
|
|
|
service,
|
|
|
|
|
settings,
|
|
|
|
|
management_slug,
|
|
|
|
|
account,
|
|
|
|
|
"Current password is incorrect.",
|
|
|
|
|
client_ip(request, settings),
|
|
|
|
|
)
|
2026-05-20 23:55:27 +02:00
|
|
|
password = first_form_value(form, "dashboard_password")
|
|
|
|
|
confirm = first_form_value(form, "dashboard_password_confirm")
|
|
|
|
|
if len(password) < 8:
|
|
|
|
|
page = await render_management_page_async(
|
|
|
|
|
service,
|
|
|
|
|
settings,
|
|
|
|
|
management_slug,
|
|
|
|
|
account,
|
|
|
|
|
dashboard_csrf_token(management_slug, account),
|
2026-05-21 10:05:40 +02:00
|
|
|
message="Password must be at least 8 characters.",
|
2026-05-21 17:23:08 +02:00
|
|
|
request_ip=client_ip(request, settings),
|
2026-05-20 23:55:27 +02:00
|
|
|
)
|
|
|
|
|
return HTMLResponse(page, status_code=400)
|
|
|
|
|
if password != confirm:
|
|
|
|
|
page = await render_management_page_async(
|
|
|
|
|
service,
|
|
|
|
|
settings,
|
|
|
|
|
management_slug,
|
|
|
|
|
account,
|
|
|
|
|
dashboard_csrf_token(management_slug, account),
|
2026-05-21 10:05:40 +02:00
|
|
|
message="The passwords do not match.",
|
2026-05-21 17:23:08 +02:00
|
|
|
request_ip=client_ip(request, settings),
|
2026-05-20 23:55:27 +02:00
|
|
|
)
|
|
|
|
|
return HTMLResponse(page, status_code=400)
|
|
|
|
|
had_password = bool(account.get("dashboard_password_hash"))
|
|
|
|
|
await run_in_threadpool(store.set_dashboard_password, hostname, password)
|
|
|
|
|
account = await get_active_management_account_async(store, management_slug)
|
|
|
|
|
await run_in_threadpool(
|
|
|
|
|
store.log_update_event,
|
|
|
|
|
hostname,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
DASHBOARD_PASSWORD_CHANGED if had_password else DASHBOARD_PASSWORD_ENABLED,
|
|
|
|
|
"dashboard password changed" if had_password else "dashboard password enabled",
|
|
|
|
|
client_ip(request, settings),
|
|
|
|
|
)
|
|
|
|
|
response = RedirectResponse(f"/m/{management_slug}", status_code=303)
|
|
|
|
|
set_unlock_cookie(response, settings, management_slug, account, settings.dashboard_unlock_seconds)
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/m/{management_slug}/password/remove", include_in_schema=False)
|
|
|
|
|
async def remove_dashboard_password(
|
2026-05-21 10:05:40 +02:00
|
|
|
management_slug: ManagementSlugPath,
|
2026-05-20 23:55:27 +02:00
|
|
|
request: Request,
|
|
|
|
|
settings: SettingsDep,
|
|
|
|
|
store: StoreDep,
|
|
|
|
|
service: ServiceDep,
|
|
|
|
|
) -> Response:
|
|
|
|
|
account = await get_active_management_account_async(store, management_slug)
|
2026-05-21 10:05:40 +02:00
|
|
|
if not has_management_access(request, settings, management_slug, account):
|
2026-05-22 17:05:06 +02:00
|
|
|
return HTMLResponse(
|
|
|
|
|
_render_management_unlock_page(management_slug, account, dashboard_csrf_token(management_slug, account)),
|
|
|
|
|
status_code=401,
|
|
|
|
|
)
|
2026-05-20 23:55:27 +02:00
|
|
|
form = await request_form(request, settings)
|
|
|
|
|
require_dashboard_csrf(management_slug, account, first_form_value(form, "csrf"))
|
|
|
|
|
hostname = str(account["hostname"])
|
|
|
|
|
if account.get("dashboard_password_hash"):
|
|
|
|
|
is_current = await run_in_threadpool(
|
|
|
|
|
store.verify_dashboard_password,
|
|
|
|
|
hostname,
|
|
|
|
|
first_form_value(form, "current_dashboard_password"),
|
|
|
|
|
)
|
|
|
|
|
if not is_current:
|
2026-05-21 17:23:08 +02:00
|
|
|
return await management_error_response_async(
|
|
|
|
|
service,
|
|
|
|
|
settings,
|
|
|
|
|
management_slug,
|
|
|
|
|
account,
|
|
|
|
|
"Current password is incorrect.",
|
|
|
|
|
client_ip(request, settings),
|
|
|
|
|
)
|
2026-05-20 23:55:27 +02:00
|
|
|
await run_in_threadpool(store.clear_dashboard_password, hostname)
|
|
|
|
|
await run_in_threadpool(
|
|
|
|
|
store.log_update_event,
|
|
|
|
|
hostname,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
DASHBOARD_PASSWORD_REMOVED,
|
|
|
|
|
"dashboard password removed",
|
|
|
|
|
client_ip(request, settings),
|
|
|
|
|
)
|
|
|
|
|
response = RedirectResponse(f"/m/{management_slug}", status_code=303)
|
|
|
|
|
delete_unlock_cookie(response, management_slug)
|
|
|
|
|
return response
|