250 lines
7.0 KiB
Rust
250 lines
7.0 KiB
Rust
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
Json,
|
|
};
|
|
use chrono::{DateTime, NaiveDateTime};
|
|
use serde::{Deserialize, Serialize};
|
|
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,
|
|
},
|
|
TenantScopedConn,
|
|
};
|
|
use crate::error::{AppError, AppResult};
|
|
use crate::models::WebdavToken;
|
|
use crate::state::AppState;
|
|
use crate::utils::{db::no_content, time::to_iso};
|
|
|
|
#[derive(Debug, Serialize, ToSchema)]
|
|
pub struct WebdavTokenResponse {
|
|
pub id: Uuid,
|
|
pub tenant_id: Uuid,
|
|
#[schema(nullable)]
|
|
pub label: Option<String>,
|
|
pub created_at: String,
|
|
#[schema(nullable)]
|
|
pub last_used_at: Option<String>,
|
|
#[schema(nullable)]
|
|
pub expires_at: Option<String>,
|
|
#[schema(nullable)]
|
|
pub revoked_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, ToSchema)]
|
|
pub struct WebdavTokenCreatedResponse {
|
|
pub token: String,
|
|
pub token_info: WebdavTokenResponse,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, ToSchema)]
|
|
pub struct CreateWebdavTokenRequest {
|
|
#[schema(nullable)]
|
|
pub label: Option<String>,
|
|
#[schema(nullable)]
|
|
pub expires_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, ToSchema)]
|
|
pub struct RevokePasskeyQuery {
|
|
#[serde(default)]
|
|
#[schema(nullable)]
|
|
pub reason: Option<String>,
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/profile/passkeys",
|
|
responses((status = 200, description = "List registered passkeys", body = [PasskeySummary])),
|
|
tag = "Profile"
|
|
)]
|
|
pub async fn list_passkeys(
|
|
State(state): State<AppState>,
|
|
TenantScopedConn {
|
|
mut conn, user_id, ..
|
|
}: TenantScopedConn,
|
|
) -> AppResult<Json<Vec<PasskeySummary>>> {
|
|
let service = state
|
|
.passkeys
|
|
.as_ref()
|
|
.ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;
|
|
|
|
let passkeys = service.list_for_user(&mut conn, user_id)?;
|
|
Ok(Json(passkeys))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/profile/webdav-tokens",
|
|
responses((status = 200, description = "List WebDAV tokens", body = [WebdavTokenResponse])),
|
|
tag = "Profile"
|
|
)]
|
|
pub async fn list_webdav_tokens(
|
|
TenantScopedConn {
|
|
mut conn,
|
|
tenant_id,
|
|
user_id,
|
|
..
|
|
}: TenantScopedConn,
|
|
) -> AppResult<Json<Vec<WebdavTokenResponse>>> {
|
|
let tokens = load_tokens(&mut conn, user_id, Some(tenant_id))?;
|
|
let responses = tokens.into_iter().map(webdav_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)),
|
|
tag = "Profile"
|
|
)]
|
|
pub async fn create_webdav_token(
|
|
TenantScopedConn {
|
|
mut conn,
|
|
tenant_id,
|
|
user_id,
|
|
..
|
|
}: TenantScopedConn,
|
|
Json(payload): Json<CreateWebdavTokenRequest>,
|
|
) -> AppResult<(StatusCode, Json<WebdavTokenCreatedResponse>)> {
|
|
let expires_at = match payload.expires_at {
|
|
Some(ref value) => Some(parse_timestamp(value)?),
|
|
None => None,
|
|
};
|
|
|
|
let issued = issue_token(
|
|
&mut conn,
|
|
user_id,
|
|
tenant_id,
|
|
payload.label.clone(),
|
|
expires_at,
|
|
)?;
|
|
|
|
let response = WebdavTokenCreatedResponse {
|
|
token: issued.token,
|
|
token_info: webdav_token_to_response(issued.record),
|
|
};
|
|
|
|
Ok((StatusCode::CREATED, Json(response)))
|
|
}
|
|
|
|
#[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)),
|
|
tag = "Profile"
|
|
)]
|
|
pub async fn regenerate_webdav_token(
|
|
TenantScopedConn {
|
|
mut conn,
|
|
tenant_id,
|
|
user_id,
|
|
..
|
|
}: TenantScopedConn,
|
|
Path(token_id): Path<Uuid>,
|
|
) -> AppResult<Json<WebdavTokenCreatedResponse>> {
|
|
let issued = rotate_token(&mut conn, token_id, user_id, Some(tenant_id))?;
|
|
let response = WebdavTokenCreatedResponse {
|
|
token: issued.token,
|
|
token_info: webdav_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")),
|
|
tag = "Profile"
|
|
)]
|
|
pub async fn delete_webdav_token(
|
|
TenantScopedConn {
|
|
mut conn, user_id, ..
|
|
}: TenantScopedConn,
|
|
Path(token_id): Path<Uuid>,
|
|
) -> AppResult<StatusCode> {
|
|
revoke_token(&mut conn, token_id, user_id)?;
|
|
no_content()
|
|
}
|
|
|
|
#[utoipa::path(
|
|
delete,
|
|
path = "/api/profile/passkeys/{id}",
|
|
params(
|
|
("id" = Uuid, Path, description = "Passkey ID"),
|
|
("reason" = Option<String>, Query, description = "Optional reason for revoking the passkey")
|
|
),
|
|
responses((status = 204, description = "Passkey revoked")),
|
|
tag = "Profile"
|
|
)]
|
|
pub async fn delete_passkey(
|
|
State(state): State<AppState>,
|
|
TenantScopedConn {
|
|
mut conn, user_id, ..
|
|
}: TenantScopedConn,
|
|
Path(passkey_id): Path<Uuid>,
|
|
Query(query): Query<RevokePasskeyQuery>,
|
|
) -> AppResult<StatusCode> {
|
|
let service = state
|
|
.passkeys
|
|
.as_ref()
|
|
.ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;
|
|
|
|
let active_count = service.active_passkey_count(&mut conn, user_id)?;
|
|
if active_count <= 1 {
|
|
return Err(AppError::bad_request(
|
|
"cannot revoke the last remaining passkey",
|
|
));
|
|
}
|
|
|
|
service.revoke_passkey(&mut conn, user_id, passkey_id, query.reason)?;
|
|
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 parse_timestamp(value: &str) -> AppResult<NaiveDateTime> {
|
|
let dt = DateTime::parse_from_rfc3339(value)
|
|
.map_err(|_| AppError::bad_request("invalid expires_at timestamp"))?;
|
|
Ok(dt.naive_utc())
|
|
}
|
|
|
|
#[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_passkeys,
|
|
crate::routes::profile::delete_passkey
|
|
),
|
|
components(schemas(
|
|
crate::routes::profile::WebdavTokenResponse,
|
|
crate::routes::profile::WebdavTokenCreatedResponse,
|
|
crate::routes::profile::CreateWebdavTokenRequest,
|
|
crate::routes::profile::RevokePasskeyQuery,
|
|
crate::auth::passkeys::PasskeySummary
|
|
))
|
|
)]
|
|
pub struct ProfileApiDoc;
|