use crate::session::{RequestId, Session}; use actix_web::web; use uuid::Uuid; use crate::envelope::{Envelope, NoData}; use crate::server::AuthSvc; use crate::server::errors::AuthError; use crate::server::share::types::ListQuery; use crate::server::ssh_key::types::{SshKeyAddRequest, SshKeySummary}; #[utoipa::path( get, path = "/api/v1/oauth/ssh-keys", tag = "auth", operation_id = "auth_ssh_key_list", summary = "列示 SSH 公钥", description = "按创建时间倒序返回当前账号的公钥,含已吊销与已过期的记录。分页参数由 `page` / `page_size` 控制。查询串不是合法分页参数时由 actix-web 直接返回 400。", params( ("page" = Option, Query, description = "页码,从 1 开始;默认 1"), ("page_size" = Option, Query, description = "每页条数,默认 20,最多 100") ), responses( ( status = 200, description = "公钥列表", body = Envelope>, 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 = 401, description = "会话未绑定账号(`unauthenticated`)", body = Envelope, example = json!({"code": "unauthenticated", "message": "authentication required"}) ), ( status = 500, description = "服务端故障(`internal`、`database`、`session`、`serialization`)", body = Envelope, example = json!({"code": "internal", "message": "internal server error"}) ) ) )] pub async fn ssh_key_list( svc: web::Data, session: Session, query: web::Query, request_id: RequestId, ) -> Result>, AuthError> { let keys = svc .list_ssh_keys(session, query.into_inner(), request_id.0) .await?; Ok(Envelope::ok(keys)) } #[utoipa::path( post, path = "/api/v1/oauth/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 = Envelope, 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 = Envelope, example = json!({"code": "ssh_key_invalid", "message": "public key is invalid"}) ), ( status = 401, description = "会话未绑定账号,或账号已删除 / 不允许使用(`unauthenticated`)", body = Envelope, example = json!({"code": "unauthenticated", "message": "authentication required"}) ), ( status = 409, description = "同一把公钥已经登记过(`ssh_key_taken`)", body = Envelope, example = json!({"code": "ssh_key_taken", "message": "public key is already registered"}) ), ( status = 500, description = "服务端故障(`internal`、`database`、`session`、`serialization`)", body = Envelope, example = json!({"code": "internal", "message": "internal server error"}) ) ) )] pub async fn ssh_key_add( svc: web::Data, session: Session, body: web::Json, request_id: RequestId, ) -> Result, AuthError> { 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(Envelope::ok(response)) } /// 只动自己名下的记录,已在吊销状态时重复调用照样返回成功;吊销与递增账号 `security_version` #[utoipa::path( delete, path = "/api/v1/oauth/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 = Envelope, example = json!({"code":200,"message":"success","data":null}) ), ( status = 401, description = "会话未绑定账号(`unauthenticated`)", body = Envelope, example = json!({"code": "unauthenticated", "message": "authentication required"}) ), ( status = 404, description = "公钥不存在或不属于当前账号(`ssh_key_not_found`)", body = Envelope, example = json!({"code": "ssh_key_not_found", "message": "ssh key does not exist"}) ), ( status = 500, description = "服务端故障(`internal`、`database`、`session`、`serialization`)", body = Envelope, example = json!({"code": "internal", "message": "internal server error"}) ) ) )] pub async fn ssh_key_delete( svc: web::Data, session: Session, key_id: web::Path, request_id: RequestId, ) -> Result, AuthError> { let user_agent = session.user_agent()?; let ip_addr = session.client_ip()?; svc.revoke_ssh_key( session, key_id.into_inner(), user_agent, ip_addr, request_id.0, ) .await?; Ok(Envelope::empty()) }