ef0b83a
{
"openapi": "3.1.0",
"info": {
"title": "auth",
"description": "认证与账号服务:工作量证明、口令与验证码登录、二次验证、注册、口令重置与 SSH 公钥。",
"license": {
"name": "MIT",
"identifier": "MIT"
},
"version": "1.0.0"
},
"paths": {
"/api/v1/auth/challenge": {
"post": {
"tags": [
"auth"
],
"summary": "签发工作量证明挑战",
"description": "把挑战写进当前会话并返回。客户端需要为 `context` 找出一枚满足 `difficulty`(前导零比特数)的 nonce;`pow.v1` 算法与前端求解器一致。",
"operationId": "auth_challenge",
"responses": {
"200": {
"description": "挑战已签发并写入会话",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_ChallengeResponse"
},
"example": {
"code": 200,
"data": {
"context": "3xJk9Qp2mV7sLbT1rZa8nHc4wDf6YgU0",
"difficulty": 16,
"expires_at": 1767139500,
"issued_at": 1767139200
},
"message": "success"
}
}
}
},
"500": {
"description": "`internal`(挑战无法写入会话)/ `cache` / `session` / `serialization`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/login": {
"post": {
"tags": [
"auth"
],
"summary": "口令登录",
"description": "用口令登录。`identity` 可以是用户名、邮箱(大小写不敏感)或用户编号,`password_hash` 是客户端先算好的口令摘要,工作量证明的载荷必须是 `identity`。账号启用了多因子时返回 `mfa_required: true` 与 `challenge`,此时会话尚未绑定,需再调 `/api/v1/auth/login/verify`。",
"operationId": "auth_login",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserLoginRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "登录成功;`mfa_required` 为真时还需二次验证",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_UserLoginResponse"
},
"example": {
"code": 200,
"data": {
"challenge": null,
"displayname": "Ada",
"mfa_required": false,
"user_id": 1024,
"username": "ada"
},
"message": "success"
}
}
}
},
"400": {
"description": "工作量证明过期、载荷不匹配或请求不合法(`expired`、`invalid_proof`、`invalid_payload`、`malformed_request`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "expired",
"message": "challenge expired"
}
}
}
},
"401": {
"description": "身份或口令不正确(`invalid_credentials`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_credentials",
"message": "invalid credentials"
}
}
}
},
"403": {
"description": "账号被禁止登录,原因见 `message`(`login_forbidden`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "login_forbidden",
"message": "account is banned"
}
}
}
},
"429": {
"description": "失败次数超出配额(`rate_limited`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "rate_limited",
"message": "too many attempts"
}
}
}
},
"500": {
"description": "服务端故障(`internal`、`database`、`cache`、`session`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/login/code": {
"post": {
"tags": [
"auth"
],
"summary": "发送邮箱登录验证码",
"description": "向账号邮箱发送 6 位登录验证码,`expires_in_secs` 为有效期。工作量证明的载荷必须是 `email`。同一账号的发码间隔受 `email_code_rate_limit_secs` 限制。",
"operationId": "auth_login_code",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginCodeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "验证码已发送",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_LoginCodeResponse"
},
"example": {
"code": 200,
"data": {
"expires_in_secs": 600
},
"message": "success"
}
}
}
},
"400": {
"description": "邮箱格式不合法或工作量证明过期、载荷不匹配(`invalid_email`、`expired`、`invalid_proof`、`invalid_payload`、`malformed_request`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_email",
"message": "invalid email address"
}
}
}
},
"401": {
"description": "邮箱未注册或账号不可用(`invalid_credentials`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_credentials",
"message": "invalid credentials"
}
}
}
},
"403": {
"description": "账号被禁止登录,原因见 `message`(`login_forbidden`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "login_forbidden",
"message": "account is banned"
}
}
}
},
"429": {
"description": "发码过于频繁(`rate_limited`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "rate_limited",
"message": "too many requests"
}
}
}
},
"500": {
"description": "服务端故障,含邮件发送失败(`internal`、`database`、`cache`、`session`、`email`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/login/code/verify": {
"post": {
"tags": [
"auth"
],
"summary": "邮箱验证码换会话",
"description": "用邮箱登录验证码换会话。工作量证明的载荷必须是 `email`。验证码一旦核对通过即作废,连续输错 `max_verify_attempts` 次后需重新申请。账号启用了多因子时返回 `mfa_required: true` 与 `challenge`。",
"operationId": "auth_login_code_verify",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginCodeVerifyRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "登录成功;`mfa_required` 为真时还需二次验证",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_UserLoginResponse"
},
"example": {
"code": 200,
"data": {
"challenge": null,
"displayname": "Ada",
"mfa_required": false,
"user_id": 1024,
"username": "ada"
},
"message": "success"
}
}
}
},
"400": {
"description": "验证码不正确或已过期,或邮箱格式、工作量证明不合法(`invalid_email_code`、`invalid_email`、`expired`、`invalid_proof`、`invalid_payload`、`malformed_request`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_email_code",
"message": "invalid email code"
}
}
}
},
"401": {
"description": "邮箱未注册或账号不可用(`invalid_credentials`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_credentials",
"message": "invalid credentials"
}
}
}
},
"403": {
"description": "账号被禁止登录,原因见 `message`(`login_forbidden`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "login_forbidden",
"message": "account is banned"
}
}
}
},
"429": {
"description": "验证码尝试次数超限或请求过频(`email_code_attempts_exceeded`、`rate_limited`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "email_code_attempts_exceeded",
"message": "too many attempts"
}
}
}
},
"500": {
"description": "服务端故障(`internal`、`database`、`cache`、`session`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/login/verify": {
"post": {
"tags": [
"auth"
],
"summary": "二次验证换会话",
"description": "用 6 位 TOTP 动态码或一枚未使用的恢复码完成二次验证并绑定会话。`challenge` 必须与 `/api/v1/auth/login` 或 `/api/v1/auth/login/code/verify` 返回的一致,挑战有效期 300 秒,同一动态码时间步不能重复使用,恢复码用掉即作废。",
"operationId": "auth_login_verify",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginVerifyRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "登录成功,会话已绑定",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_UserLoginResponse"
},
"example": {
"code": 200,
"data": {
"challenge": null,
"displayname": "Ada",
"mfa_required": false,
"user_id": 1024,
"username": "ada"
},
"message": "success"
}
}
}
},
"400": {
"description": "挑战过期或对不上、动态码错误、动态码时间步已用过、恢复码无效(`expired`、`malformed_request`、`totp_code_invalid`、`totp_step_reused`、`recovery_code_invalid`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "totp_code_invalid",
"message": "invalid totp code"
}
}
}
},
"401": {
"description": "会话里没有待验证挑战、账号不可用或多因子记录已被撤销(`unauthenticated`、`invalid_credentials`、`mfa_required`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "mfa_required",
"message": "multi-factor authentication required"
}
}
}
},
"403": {
"description": "账号被禁止登录,原因见 `message`(`login_forbidden`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "login_forbidden",
"message": "account is banned"
}
}
}
},
"429": {
"description": "尝试次数超限或恢复码已用尽(`rate_limited`、`recovery_codes_exhausted`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "rate_limited",
"message": "too many attempts"
}
}
}
},
"500": {
"description": "服务端故障(`internal`、`database`、`cache`、`session`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/me": {
"get": {
"tags": [
"auth"
],
"summary": "当前登录用户",
"description": "通过会话 Cookie 返回用户自述信息。结果在服务端缓存 300 秒,每第 5 次访问回源刷新;登出与改密会主动失效该缓存。",
"operationId": "auth_me",
"responses": {
"200": {
"description": "当前登录用户",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_MeResponse"
},
"example": {
"code": 200,
"data": {
"avatar_url": "https://cdn.example.com/avatar/ada.png",
"created_at": "2025-11-20T02:15:00Z",
"displayname": "Ada",
"email": "ada@example.com",
"email_verified_at": "2026-01-05T08:30:00Z",
"is_admin": false,
"user_id": 1024,
"username": "ada",
"version": 3
},
"message": "success"
}
}
}
},
"401": {
"description": "会话未绑定用户,或账号已被删除(`unauthenticated`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "unauthenticated",
"message": "authentication required"
}
}
}
},
"500": {
"description": "服务端故障(`internal`、`database`、`cache`、`session`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/password/reset": {
"post": {
"tags": [
"auth"
],
"summary": "申请密码重置",
"description": "校验工作量证明(payload = 邮箱的 UTF-8 字节)后,作废该用户其余未消费的重置令牌,签发新令牌(长度 `reset_token_bytes`,有效期 `reset_token_ttl_secs`),库内只存 `sha256_hex(token)` 且 `hash_key_id = \"sha256-v1\"`,同时把摘要写入缓存键 `auth:code:reset:{email}`,发信限流键 `auth:rate:reset:{email}`。",
"operationId": "auth_password_reset_request",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PasswordResetRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "申请已受理;是否真的发信不对外披露",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_PasswordResetResponse"
},
"example": {
"code": 200,
"data": {
"expires_in_secs": 1800
},
"message": "success"
}
}
}
},
"400": {
"description": "`invalid_email` / `expired` / `invalid_payload`(payload 与邮箱不一致)/ `invalid_proof`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_email",
"message": "email is invalid"
}
}
}
},
"429": {
"description": "`rate_limited`(同一邮箱申请过于频繁)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "rate_limited",
"message": "too many requests, retry after 60s"
}
}
}
},
"500": {
"description": "`email`(投递失败)/ `database` / `cache` / `session` / `serialization` / `internal`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/password/reset/confirm": {
"post": {
"tags": [
"auth"
],
"summary": "确认密码重置",
"description": "校验工作量证明(payload = `token` 的 UTF-8 字节)与缓存键 `auth:code:reset:{email}`(尝试上限 `max_verify_attempts`,错误计入 `invalid_email_code`)后,条件消费数据库令牌行(`consumed_at` 单次占用),Argon2 重哈希新口令,更新 `password_hash`/`password_updated_at`/`password_must_change`/`security_version + 1`,作废其余令牌并记 `PasswordChange` 审计事件。",
"operationId": "auth_password_reset",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PasswordResetConfirmRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "口令已改写;旧口令与其余重置令牌失效",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_PasswordResetConfirmResponse"
},
"example": {
"code": 200,
"data": {
"user_id": 1234567
},
"message": "success"
}
}
}
},
"400": {
"description": "`invalid_reset_token`(令牌不存在/已消费/已作废/已过期/账号不可用或凭据代数不匹配)/ `invalid_email_code`(缓存内摘要不匹配或已过期)/ `invalid_password` / `expired` / `invalid_payload` / `invalid_proof`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_reset_token",
"message": "password reset token is invalid"
}
}
}
},
"429": {
"description": "`email_code_attempts_exceeded`(令牌尝试次数用尽)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "email_code_attempts_exceeded",
"message": "too many invalid email verification attempts"
}
}
}
},
"500": {
"description": "`password_hash`(口令重哈希失败)/ `database` / `cache` / `session` / `serialization` / `internal`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/register": {
"post": {
"tags": [
"auth"
],
"summary": "提交注册",
"description": "校验工作量证明(payload = 用户名的 UTF-8 字节)、用户名/展示名/出生日期/邮箱、邮箱验证码(键 `auth:code:register:{email}`,尝试上限 `max_verify_attempts`)后,Argon2 重哈希口令并创建用户与初始档案。成功后写会话(user_id、security_version、renew)并记审计事件。",
"operationId": "auth_register",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRegisterRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "注册成功且已登录",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_UserRegisterResponse"
},
"example": {
"code": 200,
"data": {
"displayname": "Alice",
"user_id": 1234567,
"username": "alice"
},
"message": "success"
}
}
}
},
"400": {
"description": "`invalid_username` / `invalid_displayname` / `invalid_email` / `invalid_birth_date` / `invalid_password` / `invalid_email_code`(验证码错误或已过期)/ `expired` / `invalid_payload` / `invalid_proof`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_email_code",
"message": "email verification code is invalid"
}
}
}
},
"403": {
"description": "`under_minimum_age`(未满最小年龄)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "under_minimum_age",
"message": "you must be at least 13 years old"
}
}
}
},
"409": {
"description": "`username_taken` / `email_taken`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "username_taken",
"message": "username is already taken"
}
}
}
},
"429": {
"description": "`email_code_attempts_exceeded`(验证码尝试次数用尽)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "email_code_attempts_exceeded",
"message": "too many invalid email verification attempts"
}
}
}
},
"500": {
"description": "`password_hash`(口令重哈希失败)/ `database` / `cache` / `session` / `serialization` / `internal`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/register/code": {
"post": {
"tags": [
"auth"
],
"summary": "发送注册验证码",
"description": "校验工作量证明(payload = 邮箱的 UTF-8 字节)与邮箱格式后,把 6 位验证码写入缓存并寄出。缓存键 `auth:code:register:{email}`,有效期由 `verify_code_ttl_secs` 决定,发码限流键 `auth:rate:register:{email}`。",
"operationId": "auth_register_code",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterCodeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "验证码已写入缓存并寄出",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_RegisterCodeResponse"
},
"example": {
"code": 200,
"data": {
"expires_in_secs": 600
},
"message": "success"
}
}
}
},
"400": {
"description": "`invalid_email`(邮箱格式)/ `expired`(挑战过期)/ `invalid_payload`(payload 与邮箱不一致)/ `invalid_proof`(未满足难度)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "invalid_email",
"message": "email is invalid"
}
}
}
},
"409": {
"description": "`email_taken`(邮箱已注册)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "email_taken",
"message": "email is already taken"
}
}
}
},
"429": {
"description": "`rate_limited`(同一邮箱发码过于频繁)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "rate_limited",
"message": "too many requests, retry after 60s"
}
}
}
},
"500": {
"description": "`email`(投递失败)/ `cache` / `session` / `serialization` / `internal`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/session/logout": {
"post": {
"tags": [
"auth"
],
"summary": "登出",
"description": "销毁当前会话,并清掉该用户的 `/api/v1/auth/me` 缓存。匿名调用同样成功,此时 `user_id` 为 `null`。",
"operationId": "auth_logout",
"responses": {
"200": {
"description": "会话已销毁",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_LogoutResponse"
},
"example": {
"code": 200,
"data": {
"user_id": 1024
},
"message": "success"
}
}
}
},
"500": {
"description": "服务端故障(`internal`、`database`、`cache`、`session`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/ssh-keys": {
"get": {
"tags": [
"auth"
],
"summary": "列示 SSH 公钥",
"description": "按创建时间倒序返回当前账号的公钥,含已吊销与已过期的记录。分页参数由 `page` / `page_size` 控制。查询串不是合法分页参数时由 actix-web 直接返回 400。",
"operationId": "auth_ssh_key_list",
"parameters": [
{
"name": "page",
"in": "query",
"description": "页码,从 1 开始;默认 1",
"required": false,
"schema": {
"type": "integer",
"format": "int64",
"minimum": 0
}
},
{
"name": "page_size",
"in": "query",
"description": "每页条数,默认 20,最多 100",
"required": false,
"schema": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "公钥列表",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_Vec_SshKeySummary"
},
"example": {
"code": 200,
"data": [
{
"algorithm": "ed25519",
"created_at": "2026-01-02T03:04:05Z",
"description": "笔记本",
"expires_at": null,
"fingerprint": "SHA256:9pQ2mLk1v3sT6yZx8cV0bN4mR7wQ2eH5jK9aD1sF3gU",
"id": "018f2b1c-4d5e-7f80-9a1b-2c3d4e5f6072",
"last_used_at": null,
"name": "thinkpad",
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL9kQ2mXv7Tb1sYz8cV0bN4mR7wQ2eH5jK9aD1sF3gU thinkpad",
"revoked_at": null
}
],
"message": "success"
}
}
}
},
"401": {
"description": "会话未绑定账号(`unauthenticated`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "unauthenticated",
"message": "authentication required"
}
}
}
},
"500": {
"description": "服务端故障(`internal`、`database`、`session`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
},
"post": {
"tags": [
"auth"
],
"summary": "添加 SSH 公钥",
"description": "名称 1~64 字符、备注最长 255 字符,公钥 32~8192 字节且必须是受支持的单行 OpenSSH 公钥,`expires_at` 必须晚于当前时间。重复公钥(指纹唯一)返回 `ssh_key_taken`。添加成功记 `PublicKeyAdded` 审计事件。",
"operationId": "auth_ssh_key_add",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SshKeyAddRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "公钥已登记",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_SshKeySummary"
},
"example": {
"code": 200,
"data": {
"algorithm": "ed25519",
"created_at": "2026-01-02T03:04:05Z",
"description": "笔记本",
"expires_at": null,
"fingerprint": "SHA256:9pQ2mLk1v3sT6yZx8cV0bN4mR7wQ2eH5jK9aD1sF3gU",
"id": "018f2b1c-4d5e-7f80-9a1b-2c3d4e5f6072",
"last_used_at": null,
"name": "thinkpad",
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL9kQ2mXv7Tb1sYz8cV0bN4mR7wQ2eH5jK9aD1sF3gU thinkpad",
"revoked_at": null
},
"message": "success"
}
}
}
},
"400": {
"description": "名称、备注、公钥或过期时间不合法(`ssh_key_name_invalid`、`malformed_request`、`ssh_key_invalid`、`invalid_expiry`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "ssh_key_invalid",
"message": "public key is invalid"
}
}
}
},
"401": {
"description": "会话未绑定账号,或账号已删除 / 不允许使用(`unauthenticated`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "unauthenticated",
"message": "authentication required"
}
}
}
},
"409": {
"description": "同一把公钥已经登记过(`ssh_key_taken`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "ssh_key_taken",
"message": "public key is already registered"
}
}
}
},
"500": {
"description": "服务端故障(`internal`、`database`、`session`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
},
"/api/v1/auth/ssh-keys/{key_id}": {
"delete": {
"tags": [
"auth"
],
"summary": "吊销 SSH 公钥",
"description": "把公钥标记为已吊销,记录保留不硬删;重复调用幂等。公钥不存在或不属于当前账号返回 `ssh_key_not_found`。吊销成功记 `PublicKeyRemoved` 审计事件,并递增 `security_version` 让该账号既有的访问令牌一并失效。`key_id` 不是合法 UUID 时由 actix-web 的路径提取器直接返回 400。",
"operationId": "auth_ssh_key_delete",
"parameters": [
{
"name": "key_id",
"in": "path",
"description": "公钥编号",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "已吊销;重复调用同样返回成功",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": 200,
"data": null,
"message": "success"
}
}
}
},
"401": {
"description": "会话未绑定账号(`unauthenticated`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "unauthenticated",
"message": "authentication required"
}
}
}
},
"404": {
"description": "公钥不存在或不属于当前账号(`ssh_key_not_found`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "ssh_key_not_found",
"message": "ssh key does not exist"
}
}
}
},
"500": {
"description": "服务端故障(`internal`、`database`、`session`、`serialization`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_NoData"
},
"example": {
"code": "internal",
"message": "internal server error"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"ChallengeResponse": {
"type": "object",
"required": [
"context",
"difficulty",
"issued_at",
"expires_at"
],
"properties": {
"context": {
"type": "string"
},
"difficulty": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"expires_at": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"issued_at": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"Envelope_ChallengeResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"context",
"difficulty",
"issued_at",
"expires_at"
],
"properties": {
"context": {
"type": "string"
},
"difficulty": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"expires_at": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"issued_at": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_LoginCodeResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"expires_in_secs"
],
"properties": {
"expires_in_secs": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_LogoutResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"properties": {
"user_id": {
"type": [
"integer",
"null"
],
"format": "int64"
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_MeResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"user_id",
"username",
"email",
"is_admin",
"created_at",
"version"
],
"properties": {
"avatar_url": {
"type": [
"string",
"null"
]
},
"created_at": {
"type": "string",
"format": "date-time"
},
"displayname": {
"type": [
"string",
"null"
]
},
"email": {
"type": "string"
},
"email_verified_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"is_admin": {
"type": "boolean"
},
"user_id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
},
"version": {
"type": "integer",
"format": "int32"
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_NoData": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"default": null
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_PasswordResetConfirmResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"user_id"
],
"properties": {
"user_id": {
"type": "integer",
"format": "int64"
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_PasswordResetResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"expires_in_secs"
],
"properties": {
"expires_in_secs": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_RegisterCodeResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"expires_in_secs"
],
"properties": {
"expires_in_secs": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_SshKeySummary": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"id",
"name",
"algorithm",
"fingerprint",
"public_key",
"created_at"
],
"properties": {
"algorithm": {
"$ref": "#/components/schemas/SshKeyAlgorithm"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": [
"string",
"null"
]
},
"expires_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"fingerprint": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"last_used_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"name": {
"type": "string"
},
"public_key": {
"type": "string"
},
"revoked_at": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_UserLoginResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"user_id",
"username",
"mfa_required"
],
"properties": {
"challenge": {
"type": [
"string",
"null"
]
},
"displayname": {
"type": [
"string",
"null"
]
},
"mfa_required": {
"type": "boolean"
},
"user_id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_UserRegisterResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "object",
"required": [
"user_id",
"username"
],
"properties": {
"displayname": {
"type": [
"string",
"null"
]
},
"user_id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"Envelope_Vec_SshKeySummary": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"data": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name",
"algorithm",
"fingerprint",
"public_key",
"created_at"
],
"properties": {
"algorithm": {
"$ref": "#/components/schemas/SshKeyAlgorithm"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": [
"string",
"null"
]
},
"expires_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"fingerprint": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"last_used_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"name": {
"type": "string"
},
"public_key": {
"type": "string"
},
"revoked_at": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
}
},
"error": {
"type": [
"string",
"null"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"message": {
"type": "string"
},
"request_id": {
"type": "string"
}
}
},
"LoginCodeRequest": {
"type": "object",
"required": [
"email",
"pow_payload"
],
"properties": {
"email": {
"type": "string"
},
"pow_payload": {
"$ref": "#/components/schemas/PowProofRequest"
}
}
},
"LoginCodeResponse": {
"type": "object",
"required": [
"expires_in_secs"
],
"properties": {
"expires_in_secs": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"LoginCodeVerifyRequest": {
"type": "object",
"required": [
"email",
"email_code",
"pow_payload"
],
"properties": {
"email": {
"type": "string"
},
"email_code": {
"type": "string"
},
"pow_payload": {
"$ref": "#/components/schemas/PowProofRequest"
}
}
},
"LoginVerifyRequest": {
"type": "object",
"required": [
"challenge",
"code"
],
"properties": {
"challenge": {
"type": "string"
},
"code": {
"type": "string"
}
}
},
"LogoutResponse": {
"type": "object",
"properties": {
"user_id": {
"type": [
"integer",
"null"
],
"format": "int64"
}
}
},
"MeResponse": {
"type": "object",
"required": [
"user_id",
"username",
"email",
"is_admin",
"created_at",
"version"
],
"properties": {
"avatar_url": {
"type": [
"string",
"null"
]
},
"created_at": {
"type": "string",
"format": "date-time"
},
"displayname": {
"type": [
"string",
"null"
]
},
"email": {
"type": "string"
},
"email_verified_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"is_admin": {
"type": "boolean"
},
"user_id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
},
"version": {
"type": "integer",
"format": "int32"
}
}
},
"NoData": {
"default": null
},
"PasswordResetConfirmRequest": {
"type": "object",
"required": [
"token",
"password_hash",
"pow_payload"
],
"properties": {
"password_hash": {
"type": "string"
},
"pow_payload": {
"$ref": "#/components/schemas/PowProofRequest"
},
"token": {
"type": "string"
}
}
},
"PasswordResetConfirmResponse": {
"type": "object",
"required": [
"user_id"
],
"properties": {
"user_id": {
"type": "integer",
"format": "int64"
}
}
},
"PasswordResetRequest": {
"type": "object",
"required": [
"email",
"pow_payload"
],
"properties": {
"email": {
"type": "string"
},
"pow_payload": {
"$ref": "#/components/schemas/PowProofRequest"
}
}
},
"PasswordResetResponse": {
"type": "object",
"required": [
"expires_in_secs"
],
"properties": {
"expires_in_secs": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"PowProofRequest": {
"type": "object",
"required": [
"nonce",
"payload"
],
"properties": {
"nonce": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"payload": {
"type": "array",
"items": {
"type": "integer",
"format": "int32",
"minimum": 0
}
}
}
},
"RegisterCodeRequest": {
"type": "object",
"required": [
"email",
"pow_payload"
],
"properties": {
"email": {
"type": "string"
},
"pow_payload": {
"$ref": "#/components/schemas/PowProofRequest"
}
}
},
"RegisterCodeResponse": {
"type": "object",
"required": [
"expires_in_secs"
],
"properties": {
"expires_in_secs": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"SshKeyAddRequest": {
"type": "object",
"required": [
"name",
"public_key"
],
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"expires_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"name": {
"type": "string"
},
"public_key": {
"type": "string"
}
}
},
"SshKeyAlgorithm": {
"type": "string",
"enum": [
"rsa",
"ed25519",
"ecdsa"
]
},
"SshKeySummary": {
"type": "object",
"required": [
"id",
"name",
"algorithm",
"fingerprint",
"public_key",
"created_at"
],
"properties": {
"algorithm": {
"$ref": "#/components/schemas/SshKeyAlgorithm"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": [
"string",
"null"
]
},
"expires_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"fingerprint": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"last_used_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"name": {
"type": "string"
},
"public_key": {
"type": "string"
},
"revoked_at": {
"type": [
"string",
"null"
],
"format": "date-time"
}
}
},
"UserLoginRequest": {
"type": "object",
"required": [
"identity",
"password_hash",
"pow_payload"
],
"properties": {
"identity": {
"type": "string"
},
"password_hash": {
"type": "string"
},
"pow_payload": {
"$ref": "#/components/schemas/PowProofRequest"
}
}
},
"UserLoginResponse": {
"type": "object",
"required": [
"user_id",
"username",
"mfa_required"
],
"properties": {
"challenge": {
"type": [
"string",
"null"
]
},
"displayname": {
"type": [
"string",
"null"
]
},
"mfa_required": {
"type": "boolean"
},
"user_id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
}
}
},
"UserRegisterRequest": {
"type": "object",
"required": [
"username",
"birth_date",
"email",
"password_hash",
"email_code",
"pow_payload"
],
"properties": {
"birth_date": {
"type": "string"
},
"displayname": {
"type": [
"string",
"null"
]
},
"email": {
"type": "string"
},
"email_code": {
"type": "string"
},
"password_hash": {
"type": "string"
},
"pow_payload": {
"$ref": "#/components/schemas/PowProofRequest"
},
"username": {
"type": "string"
}
}
},
"UserRegisterResponse": {
"type": "object",
"required": [
"user_id",
"username"
],
"properties": {
"displayname": {
"type": [
"string",
"null"
]
},
"user_id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
}
}
}
}
}
}