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