Skip to content
Clone
dd4650a
221 lines 8.6 KB Rust
use crate::session::{RequestId, Session};
use actix_web::web;
use uuid::Uuid;
use crate::auth::AuthSvc;
use crate::auth::access_token::types::{
AccessTokenCreateRequest, AccessTokenCreateResponse, AccessTokenSummary,
};
use crate::auth::errors::AuthError;
use crate::auth::share::types::ListQuery;
use crate::envelope::Envelope;
use crate::envelope::NoData;
#[utoipa::path(
get,
path = "/api/v1/oauth/access-tokens",
tag = "auth",
operation_id = "auth_access_token_list",
summary = "列示访问令牌",
description = "按创建时间倒序返回当前账号的令牌摘要,只给指纹与状态,不回吐令牌原文。分页参数由 `page` / `page_size` 控制。查询串不是合法分页参数或路径/方法不匹配时返回 400 / 404 由 actix-web 直接给出。",
params(
("page" = Option<u64>, Query, description = "页码,从 1 开始;默认 1"),
("page_size" = Option<u64>, Query, description = "每页条数,默认 20,最多 100")
),
responses(
(
status = 200,
description = "令牌摘要列表",
body = Envelope<Vec<AccessTokenSummary>>,
example = json!({"code":200,"message":"success","data":[{"id":"018f2b1c-4d5e-7f80-9a1b-2c3d4e5f6071","name":"cli","description":"本机命令行","fingerprint":"3f9c1a7b2d4e6058","scope":["ssh_key","assets"],"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 access_token_list(
svc: web::Data<AuthSvc>,
session: Session,
query: web::Query<ListQuery>,
request_id: RequestId,
) -> Result<Envelope<Vec<AccessTokenSummary>>, AuthError> {
let tokens = svc
.list_access_tokens(session, query.into_inner(), request_id.0)
.await?;
Ok(Envelope::ok(tokens))
}
/// `security_version` 与账号当前值绑定,账号级撤销会让这批令牌一起失效。
#[utoipa::path(
post,
path = "/api/v1/oauth/access-tokens",
tag = "auth",
operation_id = "auth_access_token_create",
summary = "签发访问令牌",
description = "名称 1~64 字符、备注最长 255 字符,`scope` 必须全部命中白名单(最多 32 项),`expires_in_secs` 取值 60 ~ 31536000(不填表示长期有效)。同一账号未吊销的令牌上限 50 个。签发成功记 `AccessKeyAdded` 审计事件。",
request_body = AccessTokenCreateRequest,
responses(
(
status = 200,
description = "令牌已签发,`token` 只在此响应里出现一次",
body = Envelope<AccessTokenCreateResponse>,
example = json!({"code":200,"message":"success","data":{"id":"018f2b1c-4d5e-7f80-9a1b-2c3d4e5f6071","name":"cli","token":"Zk9tQ2h5cFJ2TjB3U3gxbUhhcTRkRmc2WXpVMEJqTDlLbVNu","scope":["ssh_key","assets"],"created_at":"2026-01-02T03:04:05Z","expires_at":null}})
),
(
status = 400,
description = "名称、备注、范围或有效期不合法(`invalid_access_key_name`、`malformed_request`、`invalid_scope`、`invalid_expiry`)",
body = Envelope<NoData>,
example = json!({"code": "invalid_scope", "message": "access token scope is invalid"})
),
(
status = 401,
description = "会话未绑定账号,或账号已删除 / 不允许使用(`unauthenticated`)",
body = Envelope<NoData>,
example = json!({"code": "unauthenticated", "message": "authentication required"})
),
(
status = 429,
description = "未吊销的令牌已达上限(`too_many_access_keys`)",
body = Envelope<NoData>,
example = json!({"code": "too_many_access_keys", "message": "too many access keys"})
),
(
status = 500,
description = "服务端故障(`internal`、`database`、`session`、`serialization`)",
body = Envelope<NoData>,
example = json!({"code": "internal", "message": "internal server error"})
)
)
)]
pub async fn access_token_create(
svc: web::Data<AuthSvc>,
session: Session,
body: web::Json<AccessTokenCreateRequest>,
request_id: RequestId,
) -> Result<Envelope<AccessTokenCreateResponse>, AuthError> {
let user_agent = session.user_agent()?;
let ip_addr = session.client_ip()?;
let response = svc
.create_access_token(
session,
body.into_inner(),
user_agent,
ip_addr,
request_id.0,
)
.await?;
Ok(Envelope::ok(response))
}
#[utoipa::path(
delete,
path = "/api/v1/oauth/access-tokens/{token_id}",
tag = "auth",
operation_id = "auth_access_token_revoke",
summary = "吊销访问令牌",
description = "把令牌标记为已吊销,记录保留不硬删;重复调用幂等。令牌不存在或不属于当前账号返回 `access_token_not_found`(按 `errors.rs` 的归类为 401)。吊销成功记 `AccessKeyRemoved` 审计事件。",
params(
("token_id" = Uuid, Path, description = "令牌编号")
),
responses(
(
status = 200,
description = "已吊销;重复调用同样返回成功",
body = Envelope<NoData>,
example = json!({"code":200,"message":"success","data":null})
),
(
status = 401,
description = "会话未绑定账号(`unauthenticated`),或令牌不属于当前账号 / 不存在(`access_token_not_found`)",
body = Envelope<NoData>,
example = json!({"code": "access_token_not_found", "message": "access token does not exist"})
),
(
status = 500,
description = "服务端故障(`internal`、`database`、`session`、`serialization`)",
body = Envelope<NoData>,
example = json!({"code": "internal", "message": "internal server error"})
)
)
)]
pub async fn access_token_revoke(
svc: web::Data<AuthSvc>,
session: Session,
token_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_access_token(
session,
token_id.into_inner(),
user_agent,
ip_addr,
request_id.0,
)
.await?;
Ok(Envelope::empty())
}
/// 轮换当前账号名下的一枚访问令牌:旧密文立即失效,返回新的明文(只出现一次)。
#[utoipa::path(
post,
path = "/api/v1/oauth/access-tokens/{token_id}/rotate",
tag = "auth",
operation_id = "auth_access_token_rotate",
summary = "轮换访问令牌",
description = "为已签发的令牌换一枚新的明文,保留编号、名称与 scope,摘要与指纹重算。令牌不存在或不属于当前账号返回 `access_token_not_found`。新明文只在此响应里出现一次。",
params(
("token_id" = Uuid, Path, description = "令牌编号")
),
responses(
(
status = 200,
description = "已轮换,`token` 只在此响应里出现一次",
body = Envelope<AccessTokenCreateResponse>
),
(
status = 401,
description = "会话未绑定账号,或令牌不属于当前账号 / 不存在",
body = Envelope<NoData>
),
(
status = 500,
description = "服务端故障",
body = Envelope<NoData>
)
)
)]
pub async fn access_token_rotate(
svc: web::Data<AuthSvc>,
session: Session,
token_id: web::Path<Uuid>,
request_id: RequestId,
) -> Result<Envelope<AccessTokenCreateResponse>, AuthError> {
let user_id = svc
.require_session(&session, Some(request_id.0.as_str()))
.await?;
let user_agent = session.user_agent()?;
let ip_addr = session.client_ip()?;
let response = svc
.rotate_access_token_for_user(
user_id,
token_id.into_inner(),
true,
ip_addr,
user_agent,
request_id.0,
)
.await?;
Ok(Envelope::ok(response))
}