e870254
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<u64>, Query, description = "页码,从 1 开始;默认 1"),
("page_size" = Option<u64>, Query, description = "每页条数,默认 20,最多 100")
),
responses(
(
status = 200,
description = "公钥列表",
body = Envelope<Vec<SshKeySummary>>,
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<NoData>,
example = json!({"code": "unauthenticated", "message": "authentication required"})
),
(
status = 500,
description = "服务端故障(`internal`、`database`、`session`、`serialization`)",
body = Envelope<NoData>,
example = json!({"code": "internal", "message": "internal server error"})
)
)
)]
pub async fn ssh_key_list(
svc: web::Data<AuthSvc>,
session: Session,
query: web::Query<ListQuery>,
request_id: RequestId,
) -> Result<Envelope<Vec<SshKeySummary>>, 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<SshKeySummary>,
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<NoData>,
example = json!({"code": "ssh_key_invalid", "message": "public key is invalid"})
),
(
status = 401,
description = "会话未绑定账号,或账号已删除 / 不允许使用(`unauthenticated`)",
body = Envelope<NoData>,
example = json!({"code": "unauthenticated", "message": "authentication required"})
),
(
status = 409,
description = "同一把公钥已经登记过(`ssh_key_taken`)",
body = Envelope<NoData>,
example = json!({"code": "ssh_key_taken", "message": "public key is already registered"})
),
(
status = 500,
description = "服务端故障(`internal`、`database`、`session`、`serialization`)",
body = Envelope<NoData>,
example = json!({"code": "internal", "message": "internal server error"})
)
)
)]
pub async fn ssh_key_add(
svc: web::Data<AuthSvc>,
session: Session,
body: web::Json<SshKeyAddRequest>,
request_id: RequestId,
) -> Result<Envelope<SshKeySummary>, 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<NoData>,
example = json!({"code":200,"message":"success","data":null})
),
(
status = 401,
description = "会话未绑定账号(`unauthenticated`)",
body = Envelope<NoData>,
example = json!({"code": "unauthenticated", "message": "authentication required"})
),
(
status = 404,
description = "公钥不存在或不属于当前账号(`ssh_key_not_found`)",
body = Envelope<NoData>,
example = json!({"code": "ssh_key_not_found", "message": "ssh key does not exist"})
),
(
status = 500,
description = "服务端故障(`internal`、`database`、`session`、`serialization`)",
body = Envelope<NoData>,
example = json!({"code": "internal", "message": "internal server error"})
)
)
)]
pub async fn ssh_key_delete(
svc: web::Data<AuthSvc>,
session: Session,
key_id: web::Path<Uuid>,
request_id: RequestId,
) -> Result<Envelope<NoData>, 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())
}