Files
LitePan/api/auth.py
T
2026-06-06 20:31:37 +08:00

218 lines
7.4 KiB
Python
Executable File

from fastapi import APIRouter, Request, Form
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import Optional
from core.session_manager import session_manager
from core.security import check_password_hash, assess_admin_credential_state, generate_password_hash, is_password_hash
from core.error_handler import raise_unauthorized
from api.deps import is_public_index_enabled
router = APIRouter()
def get_admin_credentials():
from config import config_manager
username = config_manager.get('admin_username') or "admin"
password = config_manager.get('admin_password') or "admin"
return username, password
def get_admin_credential_state():
username, password = get_admin_credentials()
return assess_admin_credential_state(username, password)
class AuthResponse(BaseModel):
success: bool
data: Optional[dict] = None
message: str = ""
def _now_ts() -> int:
import time
return int(time.time())
def _get_temp_password_state() -> dict:
from config import config_manager
hashed = config_manager.get("admin_temp_password_hash") or ""
expires_at = int(config_manager.get("admin_temp_password_expires_at") or 0)
last_reset_at = int(config_manager.get("admin_temp_password_last_reset_at") or 0)
now = _now_ts()
valid = bool(hashed) and expires_at > now
return {
"hash": str(hashed),
"expires_at": expires_at,
"last_reset_at": last_reset_at,
"valid": valid,
}
_reset_ip_cooldown: dict = {}
@router.post("/login")
async def login(
request: Request,
username: str = Form(...),
password: str = Form(...),
remember: str = Form("")
):
stored_username, stored_password = get_admin_credentials()
credential_state = assess_admin_credential_state(stored_username, stored_password)
temp_state = _get_temp_password_state()
password_match = False
if is_password_hash(stored_password):
password_match = check_password_hash(stored_password, password)
else:
password_match = (password == stored_password)
temp_password_match = False
if not password_match and username == stored_username and temp_state["valid"] and temp_state["hash"]:
temp_password_match = check_password_hash(temp_state["hash"], password)
if username == stored_username and (password_match or temp_password_match):
must_change_password = bool(credential_state["must_change_password"] or temp_password_match)
password_change_reason = (
"temporary_password" if temp_password_match else credential_state["password_change_reason"]
)
response_data = {
"success": True,
"data": {
"username": username,
"is_admin": True,
"must_change_password": must_change_password,
"password_change_reason": password_change_reason,
},
"message": "登录成功"
}
response = JSONResponse(content=response_data)
session_manager.create_session(
response=response,
user_data={
'is_admin': True,
'username': username,
'must_change_password': must_change_password,
'password_change_reason': password_change_reason,
},
remember=(remember == "1"),
request=request
)
return response
else:
raise_unauthorized("用户名或密码错误")
@router.post("/logout")
async def logout(request: Request):
response_data = {"success": True, "message": "登出成功"}
response = JSONResponse(content=response_data)
session_manager.clear_session(response)
return response
@router.get("/status")
async def get_auth_status(request: Request) -> AuthResponse:
session_data = session_manager.get_session(request)
is_admin = session_data.get('is_admin', False)
public_index_enabled = await is_public_index_enabled()
if is_admin and bool(session_data.get("must_change_password")):
credential_state = {
"must_change_password": True,
"password_change_reason": str(session_data.get("password_change_reason") or "temporary_password"),
}
else:
credential_state = get_admin_credential_state() if is_admin else {
"must_change_password": False,
"password_change_reason": "",
}
return AuthResponse(
success=True,
data={
"is_admin": is_admin,
"username": session_data.get('username') if is_admin else None,
"public_index_enabled": public_index_enabled,
"must_change_password": credential_state["must_change_password"],
"password_change_reason": credential_state["password_change_reason"],
},
message="获取认证状态成功"
)
@router.post("/reset-password")
async def reset_password(request: Request):
"""生成临时密码写入配置(哈希 + 过期时间),不修改原管理员密码。"""
import secrets
import string
import time
from config import config_manager
from core.log_manager import get_writer, LogModule
now = int(time.time())
cooldown_seconds = 60
ttl_seconds = 600
ip = ""
try:
ip = (request.client.host if request.client else "") or ""
except Exception:
ip = ""
if ip:
last_ip = int(_reset_ip_cooldown.get(ip) or 0)
if now - last_ip < cooldown_seconds:
return JSONResponse(content={
"success": False,
"message": "操作过于频繁,请稍后再试。"
})
temp_state = _get_temp_password_state()
if temp_state["valid"]:
remaining = max(0, temp_state["expires_at"] - now)
return JSONResponse(content={
"success": True,
"message": "临时密码仍在有效期内,请查看容器日志获取临时密码。",
"data": {
"expires_at": temp_state["expires_at"],
"remaining_seconds": remaining,
"ttl_seconds": ttl_seconds,
}
})
last_reset_at = int(temp_state["last_reset_at"] or 0)
if now - last_reset_at < cooldown_seconds:
return JSONResponse(content={
"success": False,
"message": "操作过于频繁,请稍后再试。"
})
alphabet = string.ascii_letters + string.digits
temp_password = ''.join(secrets.choice(alphabet) for _ in range(12))
temp_password_hash = generate_password_hash(temp_password)
expires_at = now + ttl_seconds
await config_manager.set_async("admin_temp_password_hash", temp_password_hash)
await config_manager.set_async("admin_temp_password_expires_at", expires_at)
await config_manager.set_async("admin_temp_password_last_reset_at", now)
if ip:
_reset_ip_cooldown[ip] = now
get_writer(LogModule.SYSTEM).warning("管理员临时密码已生成,请尽快登录并修改密码。")
print(
f"\n[重置密码] 临时管理员密码: {temp_password} (有效期 {ttl_seconds//60} 分钟,过期后失效;原密码仍可用;使用临时密码登录后需修改密码)\n"
)
return JSONResponse(content={
"success": True,
"message": "已生成临时密码,请查看容器控制台日志获取临时密码。",
"data": {
"expires_at": expires_at,
"remaining_seconds": ttl_seconds,
"ttl_seconds": ttl_seconds,
}
})