use crate::errors::ApiError; pub use crate::services::login::*; use api::http::{Json, State}; use api::middleware::RequestId; use api::rest::{AppResponseData, NoData}; use session::Session; #[utoipa::path( post, path = "/auth/login", tag = "auth", operation_id = "auth_login", summary = "口令登录", description = "用口令登录。`identity` 可以是用户名、邮箱(大小写不敏感)或用户编号,`password_hash` 是客户端先算好的口令摘要,工作量证明的载荷必须是 `identity`。账号启用了多因子时返回 `mfa_required: true` 与 `challenge`,此时会话尚未绑定,需再调 `/api/v1/auth/login/verify`。", request_body = UserLoginRequest, responses( (status = 200, description = "登录成功;`mfa_required` 为真时还需二次验证", body = AppResponseData, example = json!({"code":200,"message":"success","data":{"user_id":1024,"username":"ada","displayname":"Ada","mfa_required":false,"challenge":null}})), (status = 400, description = "工作量证明过期、载荷不匹配或请求不合法(`expired`、`invalid_proof`、`invalid_payload`、`malformed_request`)", body = AppResponseData, example = json!({"code": "expired", "message": "challenge expired"})), (status = 401, description = "身份或口令不正确(`invalid_credentials`)", body = AppResponseData, example = json!({"code": "invalid_credentials", "message": "invalid credentials"})), (status = 403, description = "账号被禁止登录,原因见 `message`(`login_forbidden`)", body = AppResponseData, example = json!({"code": "login_forbidden", "message": "account is banned"})), (status = 429, description = "失败次数超出配额(`rate_limited`)", body = AppResponseData, example = json!({"code": "rate_limited", "message": "too many attempts"})), (status = 500, description = "服务端故障(`internal`、`database`、`cache`、`session`、`serialization`)", body = AppResponseData, example = json!({"code": "internal", "message": "internal server error"})) ) )] pub async fn login( State(svc): State, session: Session, body: Json, request_id: RequestId, ) -> Result, ApiError> { let response = svc.login(session, body.into_inner(), request_id.0).await?; Ok(AppResponseData::ok(response)) } #[utoipa::path( post, path = "/auth/login/code", tag = "auth", operation_id = "auth_login_code", summary = "发送邮箱登录验证码", description = "向账号邮箱发送 6 位登录验证码,`expires_in_secs` 为有效期。工作量证明的载荷必须是 `email`。同一账号的发码间隔受 `email_code_rate_limit_secs` 限制。", request_body = LoginCodeRequest, responses( (status = 200, description = "验证码已发送", body = AppResponseData, example = json!({"code":200,"message":"success","data":{"expires_in_secs":600}})), (status = 400, description = "邮箱格式不合法或工作量证明过期、载荷不匹配(`invalid_email`、`expired`、`invalid_proof`、`invalid_payload`、`malformed_request`)", body = AppResponseData, example = json!({"code": "invalid_email", "message": "invalid email address"})), (status = 401, description = "邮箱未注册或账号不可用(`invalid_credentials`)", body = AppResponseData, example = json!({"code": "invalid_credentials", "message": "invalid credentials"})), (status = 403, description = "账号被禁止登录,原因见 `message`(`login_forbidden`)", body = AppResponseData, example = json!({"code": "login_forbidden", "message": "account is banned"})), (status = 429, description = "发码过于频繁(`rate_limited`)", body = AppResponseData, example = json!({"code": "rate_limited", "message": "too many requests"})), (status = 500, description = "服务端故障,含邮件发送失败(`internal`、`database`、`cache`、`session`、`email`、`serialization`)", body = AppResponseData, example = json!({"code": "internal", "message": "internal server error"})) ) )] pub async fn login_code( State(svc): State, session: Session, body: Json, request_id: RequestId, ) -> Result, ApiError> { let response = svc .login_code(session, body.into_inner(), request_id.0) .await?; Ok(AppResponseData::ok(response)) } #[utoipa::path( post, path = "/auth/login/code/verify", tag = "auth", operation_id = "auth_login_code_verify", summary = "邮箱验证码换会话", description = "用邮箱登录验证码换会话。工作量证明的载荷必须是 `email`。验证码一旦核对通过即作废,连续输错 `max_verify_attempts` 次后需重新申请。账号启用了多因子时返回 `mfa_required: true` 与 `challenge`。", request_body = LoginCodeVerifyRequest, responses( (status = 200, description = "登录成功;`mfa_required` 为真时还需二次验证", body = AppResponseData, example = json!({"code":200,"message":"success","data":{"user_id":1024,"username":"ada","displayname":"Ada","mfa_required":false,"challenge":null}})), (status = 400, description = "验证码不正确或已过期,或邮箱格式、工作量证明不合法(`invalid_email_code`、`invalid_email`、`expired`、`invalid_proof`、`invalid_payload`、`malformed_request`)", body = AppResponseData, example = json!({"code": "invalid_email_code", "message": "invalid email code"})), (status = 401, description = "邮箱未注册或账号不可用(`invalid_credentials`)", body = AppResponseData, example = json!({"code": "invalid_credentials", "message": "invalid credentials"})), (status = 403, description = "账号被禁止登录,原因见 `message`(`login_forbidden`)", body = AppResponseData, example = json!({"code": "login_forbidden", "message": "account is banned"})), (status = 429, description = "验证码尝试次数超限或请求过频(`email_code_attempts_exceeded`、`rate_limited`)", body = AppResponseData, example = json!({"code": "email_code_attempts_exceeded", "message": "too many attempts"})), (status = 500, description = "服务端故障(`internal`、`database`、`cache`、`session`、`serialization`)", body = AppResponseData, example = json!({"code": "internal", "message": "internal server error"})) ) )] pub async fn login_code_verify( State(svc): State, session: Session, body: Json, request_id: RequestId, ) -> Result, ApiError> { let response = svc .login_code_verify(session, body.into_inner(), request_id.0) .await?; Ok(AppResponseData::ok(response)) } #[utoipa::path( post, path = "/auth/login/verify", tag = "auth", operation_id = "auth_login_verify", summary = "二次验证换会话", description = "用 6 位 TOTP 动态码或一枚未使用的恢复码完成二次验证并绑定会话。`challenge` 必须与 `/api/v1/auth/login` 或 `/api/v1/auth/login/code/verify` 返回的一致,挑战有效期 300 秒,同一动态码时间步不能重复使用,恢复码用掉即作废。", request_body = LoginVerifyRequest, responses( (status = 200, description = "登录成功,会话已绑定", body = AppResponseData, example = json!({"code":200,"message":"success","data":{"user_id":1024,"username":"ada","displayname":"Ada","mfa_required":false,"challenge":null}})), (status = 400, description = "挑战过期或对不上、动态码错误、动态码时间步已用过、恢复码无效(`expired`、`malformed_request`、`totp_code_invalid`、`totp_step_reused`、`recovery_code_invalid`)", body = AppResponseData, example = json!({"code": "totp_code_invalid", "message": "invalid totp code"})), (status = 401, description = "会话里没有待验证挑战、账号不可用或多因子记录已被撤销(`unauthenticated`、`invalid_credentials`、`mfa_required`)", body = AppResponseData, example = json!({"code": "mfa_required", "message": "multi-factor authentication required"})), (status = 403, description = "账号被禁止登录,原因见 `message`(`login_forbidden`)", body = AppResponseData, example = json!({"code": "login_forbidden", "message": "account is banned"})), (status = 429, description = "尝试次数超限或恢复码已用尽(`rate_limited`、`recovery_codes_exhausted`)", body = AppResponseData, example = json!({"code": "rate_limited", "message": "too many attempts"})), (status = 500, description = "服务端故障(`internal`、`database`、`cache`、`session`、`serialization`)", body = AppResponseData, example = json!({"code": "internal", "message": "internal server error"})) ) )] pub async fn login_verify( State(svc): State, session: Session, body: Json, request_id: RequestId, ) -> Result, ApiError> { let response = svc .login_verify(session, body.into_inner(), request_id.0) .await?; Ok(AppResponseData::ok(response)) }