"""登录与注销接口。""" from __future__ import annotations from datetime import datetime, timezone from flask import request from sqlalchemy import select from dms.api.v1.blueprint import api_v1 from dms.common.enums import AuditAction, AuditResult, EnabledStatus from dms.common.errors import InvalidArgumentError, InvalidCredentialsError, UserDisabledError from dms.common.response import success_response from dms.database.transaction import for_update, transaction from dms.extensions import db from dms.models import User from dms.security.auth_context import get_auth_context from dms.security.decorators import bearer_auth_required from dms.security.jwt_tokens import issue_access_token from dms.security.passwords import verify_password from dms.services.audit_service import auth_audit, client_ip, record_login_failure from dms.services.user_service import user_summary def _login_payload() -> tuple[str, str, bool]: if not request.is_json: raise InvalidArgumentError("请求体必须是JSON对象") body = request.get_json(silent=True) if not isinstance(body, dict): raise InvalidArgumentError("请求体必须是JSON对象") expected = {"username", "password", "keepSignedIn"} if set(body) != expected: raise InvalidArgumentError("请求体必须且只能包含username、password和keepSignedIn") username = body["username"] password = body["password"] keep_signed_in = body["keepSignedIn"] if not isinstance(username, str) or not username.strip(): raise InvalidArgumentError("username不能为空") if not isinstance(password, str) or not password: raise InvalidArgumentError("password不能为空") if not isinstance(keep_signed_in, bool): raise InvalidArgumentError("keepSignedIn必须是布尔值") return username.strip(), password, keep_signed_in @api_v1.post("/auth/login") def login(): username, password, keep_signed_in = _login_payload() user = db.session.scalar( select(User).where(User.username == username, User.is_deleted.is_(False)) ) password_valid = verify_password( user.password_hash if user is not None else None, password, ) if user is None or not password_valid: record_login_failure( user=user, attempted_username=username, failure_reason="INVALID_CREDENTIALS", ) raise InvalidCredentialsError() if user.status != EnabledStatus.ENABLED.value: record_login_failure( user=user, attempted_username=username, failure_reason="USER_DISABLED", ) raise UserDisabledError() token, expires_in = issue_access_token(user, keep_signed_in=keep_signed_in) with transaction() as session: user.last_login_at = datetime.now(timezone.utc).replace(tzinfo=None) user.last_login_ip = client_ip() user.row_version += 1 session.add( auth_audit( action=AuditAction.LOGIN, result=AuditResult.SUCCESS, user=user, detail={"keepSignedIn": keep_signed_in}, ) ) return success_response( { "accessToken": token, "tokenType": "Bearer", "expiresIn": expires_in, "user": user_summary(user), } ) @api_v1.post("/auth/logout") @bearer_auth_required def logout(): context = get_auth_context() with transaction() as session: user = session.scalar(for_update(select(User).where(User.id == context.user_id))) if user is None or user.is_deleted: raise InvalidCredentialsError() user.auth_version += 1 user.row_version += 1 session.add( auth_audit( action=AuditAction.LOGOUT, result=AuditResult.SUCCESS, user=user, detail={"authVersionIncremented": True}, ) ) return success_response()