{ "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" } } } } } }