use crate::errors::ApiError; use crate::services::ssh_key::types::{SshKeyAddRequest, SshKeySummary}; use api::http::{Json, Path, Query, State}; use api::middleware::RequestId; use api::pagination::{Page, PageQuery}; use api::rest::{AppResponseData, NoData}; use session::Session; use uuid::Uuid; #[utoipa::path( get, path = "/auth/ssh-keys", tag = "auth", operation_id = "auth_ssh_key_list", summary = "列示 SSH 公钥", description = "按创建时间倒序返回当前账号的公钥,含已吊销与已过期的记录。使用 `limit` / `offset` 分页;`limit` 必须在 1..=100、`offset` 不得超过 5000,越界返回 400。", params(PageQuery), responses( ( status = 200, description = "公钥列表", body = AppResponseData>, example = json!({"code":200,"message":"success","data":{"items":[{"id":"018f2b1c-4d5e-7f80-9a1b-2c3d4e5f6072","name":"thinkpad","description":"笔记本","algorithm":"ed25519","fingerprint":"SHA256:9pQ2mLk1v3sT6yZx8cV0bN4mR7wQ2eH5jK9aD1sF3gU","public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL9kQ2mXv7Tb1sYz8cV0bN4mR7wQ2eH5jK9aD1sF3gU thinkpad","created_at":"2026-01-02T03:04:05Z","expires_at":null,"last_used_at":null,"revoked_at":null}],"total":1,"limit":50,"offset":0}}) ), ( status = 401, description = "会话未绑定账号(`unauthenticated`)", body = AppResponseData, example = json!({"code": "unauthenticated", "message": "authentication required"}) ), ( status = 500, description = "服务端故障(`internal`、`database`、`session`、`serialization`)", body = AppResponseData, example = json!({"code": "internal", "message": "internal server error"}) ) ) )] pub async fn ssh_key_list( State(svc): State, session: Session, Query(query): Query, request_id: RequestId, ) -> Result>, ApiError> { let svc = crate::services::ssh_key::SshKeyService::new(svc.accounts.clone()); let keys = svc.list_ssh_keys(session, query, request_id.0).await?; Ok(AppResponseData::ok(keys)) } #[utoipa::path( post, path = "/auth/ssh-keys", tag = "auth", operation_id = "auth_ssh_key_add", summary = "添加 SSH 公钥", description = "名称 1~64 字符、备注最长 255 字符,公钥 32~8192 字节且必须是受支持的单行 OpenSSH 公钥,`expires_at` 必须晚于当前时间。重复公钥(指纹唯一)返回 `ssh_key_taken`。添加成功记 `PublicKeyAdded` 审计事件。", request_body = SshKeyAddRequest, responses( ( status = 200, description = "公钥已登记", body = AppResponseData, example = json!({"code":200,"message":"success","data":{"id":"018f2b1c-4d5e-7f80-9a1b-2c3d4e5f6072","name":"thinkpad","description":"笔记本","algorithm":"ed25519","fingerprint":"SHA256:9pQ2mLk1v3sT6yZx8cV0bN4mR7wQ2eH5jK9aD1sF3gU","public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL9kQ2mXv7Tb1sYz8cV0bN4mR7wQ2eH5jK9aD1sF3gU thinkpad","created_at":"2026-01-02T03:04:05Z","expires_at":null,"last_used_at":null,"revoked_at":null}}) ), ( status = 400, description = "名称、备注、公钥或过期时间不合法(`ssh_key_name_invalid`、`malformed_request`、`ssh_key_invalid`、`invalid_expiry`)", body = AppResponseData, example = json!({"code": "ssh_key_invalid", "message": "public key is invalid"}) ), ( status = 401, description = "会话未绑定账号,或账号已删除 / 不允许使用(`unauthenticated`)", body = AppResponseData, example = json!({"code": "unauthenticated", "message": "authentication required"}) ), ( status = 409, description = "同一把公钥已经登记过(`ssh_key_taken`)", body = AppResponseData, example = json!({"code": "ssh_key_taken", "message": "public key is already registered"}) ), ( status = 500, description = "服务端故障(`internal`、`database`、`session`、`serialization`)", body = AppResponseData, example = json!({"code": "internal", "message": "internal server error"}) ) ) )] pub async fn ssh_key_add( State(svc): State, session: Session, body: Json, request_id: RequestId, ) -> Result, ApiError> { let svc = crate::services::ssh_key::SshKeyService::new(svc.accounts.clone()); let user_agent = session.user_agent()?; let ip_addr = session.client_ip()?; let response = svc .add_ssh_key( session, body.into_inner(), user_agent, ip_addr, request_id.0, ) .await?; Ok(AppResponseData::ok(response)) } /// 只动自己名下的记录,已在吊销状态时重复调用照样返回成功;吊销与递增账号 `security_version` #[utoipa::path( delete, path = "/auth/ssh-keys/{key_id}", tag = "auth", operation_id = "auth_ssh_key_delete", summary = "吊销 SSH 公钥", description = "把公钥标记为已吊销,记录保留不硬删;重复调用幂等。公钥不存在或不属于当前账号返回 `ssh_key_not_found`。吊销成功记 `PublicKeyRemoved` 审计事件,并递增 `security_version` 让该账号既有的访问令牌一并失效。`key_id` 不是合法 UUID 时由 actix-web 的路径提取器直接返回 400。", params( ("key_id" = Uuid, Path, description = "公钥编号") ), responses( ( status = 200, description = "已吊销;重复调用同样返回成功", body = AppResponseData, example = json!({"code":200,"message":"success","data":null}) ), ( status = 401, description = "会话未绑定账号(`unauthenticated`)", body = AppResponseData, example = json!({"code": "unauthenticated", "message": "authentication required"}) ), ( status = 404, description = "公钥不存在或不属于当前账号(`ssh_key_not_found`)", body = AppResponseData, example = json!({"code": "ssh_key_not_found", "message": "ssh key does not exist"}) ), ( status = 500, description = "服务端故障(`internal`、`database`、`session`、`serialization`)", body = AppResponseData, example = json!({"code": "internal", "message": "internal server error"}) ) ) )] pub async fn ssh_key_delete( State(svc): State, session: Session, Path(key_id): Path, request_id: RequestId, ) -> Result, ApiError> { let svc = crate::services::ssh_key::SshKeyService::new(svc.accounts.clone()); let user_agent = session.user_agent()?; let ip_addr = session.client_ip()?; svc.revoke_ssh_key(session, key_id, user_agent, ip_addr, request_id.0) .await?; Ok(AppResponseData::empty()) }