218 lines
7.4 KiB
Python
Executable File
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,
|
|
}
|
|
})
|