api-tokens

This commit is contained in:
2025-11-01 21:42:52 +01:00
parent f6ceff5444
commit 00f5d3adea
16 changed files with 999 additions and 556 deletions
+110 -50
View File
@@ -9,24 +9,26 @@ use utoipa::ToSchema;
use uuid::Uuid;
use crate::auth::{
passkeys::PasskeySummary,
webdav_tokens::{
create_webdav_token as issue_token, list_webdav_tokens as load_tokens,
regenerate_webdav_token as rotate_token, revoke_webdav_token as revoke_token,
api_tokens::{
create_api_token as issue_token, list_api_tokens as load_tokens,
regenerate_api_token as rotate_token, revoke_api_token as revoke_token,
update_api_token_capabilities as update_capabilities,
},
passkeys::PasskeySummary,
TenantScopedConn,
};
use crate::error::{AppError, AppResult};
use crate::models::WebdavToken;
use crate::models::{ApiToken, ApiTokenCapability};
use crate::state::AppState;
use crate::utils::{db::no_content, time::to_iso};
#[derive(Debug, Serialize, ToSchema)]
pub struct WebdavTokenResponse {
pub struct ApiTokenResponse {
pub id: Uuid,
pub tenant_id: Uuid,
#[schema(nullable)]
pub label: Option<String>,
pub capabilities: Vec<ApiTokenCapability>,
pub created_at: String,
#[schema(nullable)]
pub last_used_at: Option<String>,
@@ -37,17 +39,25 @@ pub struct WebdavTokenResponse {
}
#[derive(Debug, Serialize, ToSchema)]
pub struct WebdavTokenCreatedResponse {
pub struct ApiTokenCreatedResponse {
pub token: String,
pub token_info: WebdavTokenResponse,
pub token_info: ApiTokenResponse,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateWebdavTokenRequest {
pub struct CreateApiTokenRequest {
#[schema(nullable)]
pub label: Option<String>,
#[schema(nullable)]
pub expires_at: Option<String>,
#[serde(default)]
#[schema(nullable)]
pub capabilities: Option<Vec<ApiTokenCapability>>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateApiTokenCapabilitiesRequest {
pub capabilities: Vec<ApiTokenCapability>,
}
#[derive(Debug, Deserialize, ToSchema)]
@@ -80,55 +90,60 @@ pub async fn list_passkeys(
#[utoipa::path(
get,
path = "/api/profile/webdav-tokens",
responses((status = 200, description = "List WebDAV tokens", body = [WebdavTokenResponse])),
path = "/api/profile/api-tokens",
responses((status = 200, description = "List API tokens", body = [ApiTokenResponse])),
tag = "Profile"
)]
pub async fn list_webdav_tokens(
pub async fn list_api_tokens(
TenantScopedConn {
mut conn,
tenant_id,
user_id,
..
}: TenantScopedConn,
) -> AppResult<Json<Vec<WebdavTokenResponse>>> {
) -> AppResult<Json<Vec<ApiTokenResponse>>> {
let tokens = load_tokens(&mut conn, user_id, Some(tenant_id))?;
let responses = tokens.into_iter().map(webdav_token_to_response).collect();
let responses = tokens.into_iter().map(api_token_to_response).collect();
Ok(Json(responses))
}
#[utoipa::path(
post,
path = "/api/profile/webdav-tokens",
request_body = CreateWebdavTokenRequest,
responses((status = 201, description = "WebDAV token created", body = WebdavTokenCreatedResponse)),
path = "/api/profile/api-tokens",
request_body = CreateApiTokenRequest,
responses((status = 201, description = "API token created", body = ApiTokenCreatedResponse)),
tag = "Profile"
)]
pub async fn create_webdav_token(
pub async fn create_api_token(
TenantScopedConn {
mut conn,
tenant_id,
user_id,
..
}: TenantScopedConn,
Json(payload): Json<CreateWebdavTokenRequest>,
) -> AppResult<(StatusCode, Json<WebdavTokenCreatedResponse>)> {
Json(payload): Json<CreateApiTokenRequest>,
) -> AppResult<(StatusCode, Json<ApiTokenCreatedResponse>)> {
let expires_at = match payload.expires_at {
Some(ref value) => Some(parse_timestamp(value)?),
None => None,
};
let capabilities = payload
.capabilities
.unwrap_or_else(|| vec![ApiTokenCapability::Webdav]);
let issued = issue_token(
&mut conn,
user_id,
tenant_id,
payload.label.clone(),
expires_at,
capabilities,
)?;
let response = WebdavTokenCreatedResponse {
let response = ApiTokenCreatedResponse {
token: issued.token,
token_info: webdav_token_to_response(issued.record),
token_info: api_token_to_response(issued.record),
};
Ok((StatusCode::CREATED, Json(response)))
@@ -136,12 +151,12 @@ pub async fn create_webdav_token(
#[utoipa::path(
post,
path = "/api/profile/webdav-tokens/{id}/regenerate",
params(("id" = Uuid, Path, description = "WebDAV token ID")),
responses((status = 200, description = "WebDAV token regenerated", body = WebdavTokenCreatedResponse)),
path = "/api/profile/api-tokens/{id}/regenerate",
params(("id" = Uuid, Path, description = "API token ID")),
responses((status = 200, description = "API token regenerated", body = ApiTokenCreatedResponse)),
tag = "Profile"
)]
pub async fn regenerate_webdav_token(
pub async fn regenerate_api_token(
TenantScopedConn {
mut conn,
tenant_id,
@@ -149,24 +164,53 @@ pub async fn regenerate_webdav_token(
..
}: TenantScopedConn,
Path(token_id): Path<Uuid>,
) -> AppResult<Json<WebdavTokenCreatedResponse>> {
) -> AppResult<Json<ApiTokenCreatedResponse>> {
let issued = rotate_token(&mut conn, token_id, user_id, Some(tenant_id))?;
let response = WebdavTokenCreatedResponse {
let response = ApiTokenCreatedResponse {
token: issued.token,
token_info: webdav_token_to_response(issued.record),
token_info: api_token_to_response(issued.record),
};
Ok(Json(response))
}
#[utoipa::path(
delete,
path = "/api/profile/webdav-tokens/{id}",
params(("id" = Uuid, Path, description = "WebDAV token ID")),
responses((status = 204, description = "WebDAV token revoked")),
patch,
path = "/api/profile/api-tokens/{id}",
params(("id" = Uuid, Path, description = "API token ID")),
request_body = UpdateApiTokenCapabilitiesRequest,
responses((status = 200, description = "API token updated", body = ApiTokenResponse)),
tag = "Profile"
)]
pub async fn delete_webdav_token(
pub async fn update_api_token(
TenantScopedConn {
mut conn,
tenant_id,
user_id,
..
}: TenantScopedConn,
Path(token_id): Path<Uuid>,
Json(payload): Json<UpdateApiTokenCapabilitiesRequest>,
) -> AppResult<Json<ApiTokenResponse>> {
let updated = update_capabilities(
&mut conn,
token_id,
user_id,
Some(tenant_id),
payload.capabilities,
)?;
Ok(Json(api_token_to_response(updated)))
}
#[utoipa::path(
delete,
path = "/api/profile/api-tokens/{id}",
params(("id" = Uuid, Path, description = "API token ID")),
responses((status = 204, description = "API token revoked")),
tag = "Profile"
)]
pub async fn delete_api_token(
TenantScopedConn {
mut conn, user_id, ..
}: TenantScopedConn,
@@ -210,15 +254,28 @@ pub async fn delete_passkey(
no_content()
}
fn webdav_token_to_response(token: WebdavToken) -> WebdavTokenResponse {
WebdavTokenResponse {
id: token.id,
tenant_id: token.tenant_id,
label: token.label,
created_at: to_iso(token.created_at),
last_used_at: token.last_used_at.map(to_iso),
expires_at: token.expires_at.map(to_iso),
revoked_at: token.revoked_at.map(to_iso),
fn api_token_to_response(token: ApiToken) -> ApiTokenResponse {
let ApiToken {
id,
tenant_id,
label,
created_at,
last_used_at,
expires_at,
revoked_at,
capabilities,
..
} = token;
ApiTokenResponse {
id,
tenant_id,
label,
capabilities,
created_at: to_iso(created_at),
last_used_at: last_used_at.map(to_iso),
expires_at: expires_at.map(to_iso),
revoked_at: revoked_at.map(to_iso),
}
}
@@ -231,17 +288,20 @@ fn parse_timestamp(value: &str) -> AppResult<NaiveDateTime> {
#[derive(utoipa::OpenApi)]
#[openapi(
paths(
crate::routes::profile::list_webdav_tokens,
crate::routes::profile::create_webdav_token,
crate::routes::profile::regenerate_webdav_token,
crate::routes::profile::delete_webdav_token,
crate::routes::profile::list_api_tokens,
crate::routes::profile::create_api_token,
crate::routes::profile::regenerate_api_token,
crate::routes::profile::update_api_token,
crate::routes::profile::delete_api_token,
crate::routes::profile::list_passkeys,
crate::routes::profile::delete_passkey
),
components(schemas(
crate::routes::profile::WebdavTokenResponse,
crate::routes::profile::WebdavTokenCreatedResponse,
crate::routes::profile::CreateWebdavTokenRequest,
crate::models::ApiTokenCapability,
crate::routes::profile::ApiTokenResponse,
crate::routes::profile::ApiTokenCreatedResponse,
crate::routes::profile::CreateApiTokenRequest,
crate::routes::profile::UpdateApiTokenCapabilitiesRequest,
crate::routes::profile::RevokePasskeyQuery,
crate::auth::passkeys::PasskeySummary
))