fix(auth): 登录失败、错误提示中文化、记住我 checkbox 样式

- 重置 admin 密码哈希为 Admin@123
- local.py / auth.py / dependencies.py 面向用户的错误提示改为中文
- LoginView 为记住我 checkbox 添加显式方形/主色样式
This commit is contained in:
Chiguyong 2026-07-06 18:43:49 +08:00
parent af2de993d8
commit 96c15146d5
4 changed files with 50 additions and 22 deletions

View File

@ -53,7 +53,7 @@ async def require_authenticated(request: Request) -> dict[str, object]:
if user is None:
raise HTTPException(
status_code=401,
detail="Authentication required",
detail="请先登录",
)
# U10 debug signal: log once per request when a legacy (no-sid)
# token is in use. This makes the migration window observable in
@ -115,7 +115,7 @@ def require_permission(*permissions: Permission) -> Callable[..., object]:
if high_risk:
raise HTTPException(
status_code=401,
detail="Authentication required for this endpoint",
detail="此接口需要登录",
)
# Synthetic dev-mode user with admin-like permissions for non-high-risk
return {"user_id": None, "username": "dev", "role": "admin", "dev_mode": True}
@ -125,7 +125,7 @@ def require_permission(*permissions: Permission) -> Callable[..., object]:
if not has_permission(user, perm):
raise HTTPException(
status_code=403,
detail=f"Permission denied: requires {perm.value}",
detail=f"权限不足:需要 {perm.value} 权限",
)
return user
@ -156,15 +156,15 @@ def require_any_permission(*permissions: Permission) -> Callable[..., object]:
if high_risk:
raise HTTPException(
status_code=401,
detail="Authentication required for this endpoint",
detail="此接口需要登录",
)
return {"user_id": None, "username": "dev", "role": "admin", "dev_mode": True}
if not any(has_permission(user, p) for p in permissions):
needed = " or ".join(p.value for p in permissions)
needed = " ".join(p.value for p in permissions)
raise HTTPException(
status_code=403,
detail=f"Permission denied: requires {needed}",
detail=f"权限不足:需要 {needed} 权限",
)
return user
@ -198,7 +198,7 @@ async def require_terminal_authorized(request: Request) -> dict[str, object]:
user_id = user.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Authentication required")
raise HTTPException(status_code=401, detail="请先登录")
db_path = await _resolve_db_path(request)
async with aiosqlite.connect(str(db_path)) as db:
@ -210,15 +210,15 @@ async def require_terminal_authorized(request: Request) -> dict[str, object]:
row = await cursor.fetchone()
if row is None:
raise HTTPException(status_code=404, detail="User not found")
raise HTTPException(status_code=404, detail="用户不存在")
if not bool(row["is_active"]):
raise HTTPException(status_code=403, detail="Account is disabled")
raise HTTPException(status_code=403, detail="账户已被禁用")
if not bool(row["is_terminal_authorized"]):
raise HTTPException(
status_code=403,
detail="Terminal access not authorized for this account",
detail="该账户未授权使用本地终端",
)
return user
@ -239,7 +239,7 @@ async def require_server_terminal_authorized(request: Request) -> dict[str, obje
user_id = user.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Authentication required")
raise HTTPException(status_code=401, detail="请先登录")
db_path = await _resolve_db_path(request)
async with aiosqlite.connect(str(db_path)) as db:
@ -251,15 +251,15 @@ async def require_server_terminal_authorized(request: Request) -> dict[str, obje
row = await cursor.fetchone()
if row is None:
raise HTTPException(status_code=404, detail="User not found")
raise HTTPException(status_code=404, detail="用户不存在")
if not bool(row["is_active"]):
raise HTTPException(status_code=403, detail="Account is disabled")
raise HTTPException(status_code=403, detail="账户已被禁用")
if not bool(row["is_server_terminal_authorized"]):
raise HTTPException(
status_code=403,
detail="Server terminal access not authorized for this account",
detail="该账户未授权使用服务端终端",
)
return user

View File

@ -92,10 +92,10 @@ class LocalAuthProvider:
# but has the right shape so bcrypt.checkpw doesn't short-circuit.
_DUMMY_BCRYPT_HASH = "$2b$12$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ0123"
verify_password(password, _DUMMY_BCRYPT_HASH)
raise InvalidCredentials("invalid username or password")
raise InvalidCredentials("用户名或密码错误")
if not verify_password(password, row["password_hash"]):
raise InvalidCredentials("invalid username or password")
raise InvalidCredentials("用户名或密码错误")
return _row_to_user(row)
@ -205,10 +205,10 @@ class LocalAuthProvider:
# "duplicate" message.
msg = str(exc).lower()
if "username" in msg:
raise ValueError(f"User with username {username!r} already exists") from exc
raise ValueError(f"用户名 {username!r} 已存在") from exc
if "email" in msg:
raise ValueError(f"User with email {email!r} already exists") from exc
raise ValueError(f"User already exists: {exc}") from exc
raise ValueError(f"邮箱 {email!r} 已存在") from exc
raise ValueError(f"用户已存在: {exc}") from exc
assert row is not None # we just inserted it
logger.info(f"Created user {username!r} (id={user_id}) via LocalAuthProvider")

View File

@ -199,6 +199,34 @@ onMounted(async () => {
margin-bottom: 16px;
}
/* Checkbox + +
避免 Tauri WebKit / 暗色模式下出现圆形或默认蓝色 */
.login-remember :deep(.ant-checkbox-inner) {
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid var(--border-color-hover, #dfdfde);
background-color: var(--bg-primary, #ffffff);
}
.login-remember :deep(.ant-checkbox-checked .ant-checkbox-inner) {
background-color: var(--color-primary, #1a1a1a);
border-color: var(--color-primary, #1a1a1a);
}
.login-remember :deep(.ant-checkbox-checked .ant-checkbox-inner::after) {
border-color: var(--text-inverse, #ffffff);
}
.login-remember :deep(.ant-checkbox-wrapper) {
color: var(--text-secondary, #4a4a4a);
font-size: var(--font-sm, 13px);
}
.login-remember :deep(.ant-checkbox:hover .ant-checkbox-inner) {
border-color: var(--color-primary, #1a1a1a);
}
.login-footer {
margin-top: 24px;
text-align: center;

View File

@ -376,7 +376,7 @@ async def login(payload: LoginRequest, request: Request) -> TokenResponse:
raise HTTPException(status_code=401, detail=str(exc)) from exc
if not user.is_active:
raise HTTPException(status_code=403, detail="Account is disabled")
raise HTTPException(status_code=403, detail="账户已被禁用")
# 2. Issue tokens
# The session_id is created up front (SessionService.create returns
@ -812,7 +812,7 @@ async def _sso_issue_token_pair(
# SEC-1: SSO 流程必须与本地 loginauth.py:377一致地校验 is_active
# 否则已 deprovision 的用户可通过 IDP 回调重新拿到 JWT + session。
if not bool(user_dict.get("is_active", True)):
raise HTTPException(status_code=403, detail="Account is disabled")
raise HTTPException(status_code=403, detail="账户已被禁用")
db_path = await _ensure_db(request)
secret = _resolve_jwt_secret(request)
svc: SessionService = get_session_service()
@ -948,7 +948,7 @@ async def _require_admin(
request: Request, user: dict[str, Any] = Depends(require_authenticated)
) -> dict[str, Any]:
if not has_permission(user, Permission.USER_MANAGE):
raise HTTPException(status_code=403, detail="Admin permission required")
raise HTTPException(status_code=403, detail="需要管理员权限")
return user