feat: add system settings page for admin username and password

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 08:56:54 +08:00
parent 4743f3efdd
commit b5cba83df4
20 changed files with 647 additions and 117 deletions
+17 -7
View File
@@ -2,35 +2,40 @@
from __future__ import annotations
from fastapi import APIRouter, Response
from fastapi import APIRouter, HTTPException, Response, status
from pydantic import BaseModel, Field
from apps.api.auth import COOKIE_NAME, auth_disabled, issue_token
from packages.config import get_settings
router = APIRouter(prefix="/auth", tags=["auth"])
class LoginBody(BaseModel):
username: str = Field(default="admin", min_length=1, max_length=64)
password: str = Field(min_length=1, max_length=256)
@router.get("/status")
def auth_status() -> dict:
s = get_settings()
return {
"auth_required": not auth_disabled(),
"product": "比特骆驼行情采集分析",
"username": s.admin_username,
}
@router.post("/login")
def login(body: LoginBody, response: Response) -> dict:
if auth_disabled():
return {"ok": True, "auth_required": False, "token": None}
token = issue_token(body.password)
return {"ok": True, "auth_required": False, "token": None, "username": body.username}
token = issue_token(body.username.strip(), body.password)
if not token:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid password")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="invalid username or password",
)
response.set_cookie(
key=COOKIE_NAME,
value=token,
@@ -39,7 +44,12 @@ def login(body: LoginBody, response: Response) -> dict:
max_age=7 * 24 * 3600,
path="/",
)
return {"ok": True, "auth_required": True, "token": token}
return {
"ok": True,
"auth_required": True,
"token": token,
"username": body.username.strip(),
}
@router.post("/logout")
+83
View File
@@ -0,0 +1,83 @@
"""系统设置:管理员账号。"""
from __future__ import annotations
import re
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from apps.api.auth import auth_disabled, require_auth, verify_credentials
from packages.config import get_settings, reload_settings
from packages.config.env_file import update_env_file
router = APIRouter(
prefix="/settings",
tags=["settings"],
dependencies=[Depends(require_auth)],
)
_USERNAME_RE = re.compile(r"^[a-zA-Z0-9_\-]{2,32}$")
class UpdateAccountBody(BaseModel):
current_password: str = Field(min_length=1, max_length=256)
new_username: str | None = Field(default=None, max_length=32)
new_password: str | None = Field(default=None, max_length=256)
@router.get("/account")
def get_account() -> dict:
s = get_settings()
return {
"username": s.admin_username,
"auth_required": not auth_disabled(),
"env_file": str(s.env_file_path),
}
@router.put("/account")
def update_account(body: UpdateAccountBody) -> dict:
s = get_settings()
if auth_disabled():
raise HTTPException(status_code=400, detail="auth disabled; edit .env manually")
if not verify_credentials(s.admin_username, body.current_password):
raise HTTPException(status_code=401, detail="current password incorrect")
if body.new_password is not None and len(body.new_password) < 6:
raise HTTPException(status_code=400, detail="password must be at least 6 characters")
new_user = (body.new_username or s.admin_username).strip()
new_pass = body.new_password or s.admin_password
if body.new_username is not None and not _USERNAME_RE.fullmatch(new_user):
raise HTTPException(
status_code=400,
detail="username: 2-32 chars, letters/digits/_/- only",
)
if new_user == s.admin_username and new_pass == s.admin_password:
return {"ok": True, "changed": False, "message": "no changes", "username": new_user}
updates = {
"ADMIN_USERNAME": new_user,
"ADMIN_PASSWORD": new_pass,
}
path = s.env_file_path
try:
update_env_file(path, updates)
except OSError as e:
raise HTTPException(
status_code=500,
detail=f"failed to write {path}: {e}",
) from e
reload_settings()
return {
"ok": True,
"changed": True,
"username": new_user,
"relogin_required": True,
"message": "账号已更新,请重新登录",
}