mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-06 10:16:59 +00:00
refactor(api): simplify passkey account creation and auth-adjacent server endpoints
This commit is contained in:
@@ -1,8 +1,4 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tracing::error;
|
||||
@@ -34,7 +30,7 @@ pub struct ListAppPasswordsOutput {
|
||||
pub async fn list_app_passwords(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Permissive>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<ListAppPasswordsOutput>, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(&auth.did)
|
||||
@@ -60,7 +56,7 @@ pub async fn list_app_passwords(
|
||||
.map(|d| d.to_string()),
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(ListAppPasswordsOutput { passwords }).into_response())
|
||||
Ok(Json(ListAppPasswordsOutput { passwords }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -86,7 +82,7 @@ pub async fn create_app_password(
|
||||
_rate_limit: RateLimited<AppPasswordLimit>,
|
||||
auth: Auth<NotTakendown>,
|
||||
Json(input): Json<CreateAppPasswordInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<CreateAppPasswordOutput>, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(&auth.did)
|
||||
@@ -194,8 +190,7 @@ pub async fn create_app_password(
|
||||
created_at: created_at.to_rfc3339(),
|
||||
privileged: privilege.is_privileged(),
|
||||
scopes: final_scopes,
|
||||
})
|
||||
.into_response())
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -207,7 +202,7 @@ pub async fn revoke_app_password(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Permissive>,
|
||||
Json(input): Json<RevokeAppPasswordInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<EmptyResponse>, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(&auth.did)
|
||||
@@ -247,5 +242,5 @@ pub async fn revoke_app_password(
|
||||
.await
|
||||
.log_db_err("revoking app password")?;
|
||||
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
Ok(Json(EmptyResponse {}))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum::{Json, extract::State};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
@@ -46,7 +42,7 @@ pub async fn create_invite_code(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Admin>,
|
||||
Json(input): Json<CreateInviteCodeInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<CreateInviteCodeOutput>, ApiError> {
|
||||
if input.use_count < 1 {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"useCount must be at least 1".into(),
|
||||
@@ -66,7 +62,7 @@ pub async fn create_invite_code(
|
||||
.create_invite_code(&code, input.use_count, Some(&for_account))
|
||||
.await
|
||||
{
|
||||
Ok(true) => Ok(Json(CreateInviteCodeOutput { code }).into_response()),
|
||||
Ok(true) => Ok(Json(CreateInviteCodeOutput { code })),
|
||||
Ok(false) => {
|
||||
error!("No admin user found to create invite code");
|
||||
Err(ApiError::InternalError(None))
|
||||
@@ -101,7 +97,7 @@ pub async fn create_invite_codes(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Admin>,
|
||||
Json(input): Json<CreateInviteCodesInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<CreateInviteCodesOutput>, ApiError> {
|
||||
if input.use_count < 1 {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"useCount must be at least 1".into(),
|
||||
@@ -147,8 +143,7 @@ pub async fn create_invite_codes(
|
||||
match result {
|
||||
Ok(result_codes) => Ok(Json(CreateInviteCodesOutput {
|
||||
codes: result_codes,
|
||||
})
|
||||
.into_response()),
|
||||
})),
|
||||
Err(e) => {
|
||||
error!("DB error creating invite codes: {:?}", e);
|
||||
Err(ApiError::InternalError(None))
|
||||
@@ -193,7 +188,7 @@ pub async fn get_account_invite_codes(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<NotTakendown>,
|
||||
axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<GetAccountInviteCodesOutput>, ApiError> {
|
||||
let include_used = params.include_used.unwrap_or(true);
|
||||
|
||||
let codes_info = state
|
||||
@@ -247,5 +242,5 @@ pub async fn get_account_invite_codes(
|
||||
.await;
|
||||
|
||||
let codes: Vec<InviteCode> = codes.into_iter().flatten().collect();
|
||||
Ok(Json(GetAccountInviteCodesOutput { codes }).into_response())
|
||||
Ok(Json(GetAccountInviteCodesOutput { codes }))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tranquil_pds::api::ApiError;
|
||||
@@ -39,7 +34,7 @@ pub async fn update_did_document(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateDidDocumentInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<UpdateDidDocumentOutput>, ApiError> {
|
||||
if !auth.did.starts_with("did:web:") {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"DID document updates are only available for did:web accounts".into(),
|
||||
@@ -120,20 +115,16 @@ pub async fn update_did_document(
|
||||
|
||||
tracing::info!("Updated DID document for {}", &auth.did);
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(UpdateDidDocumentOutput {
|
||||
success: true,
|
||||
did_document: did_doc,
|
||||
}),
|
||||
)
|
||||
.into_response())
|
||||
Ok(Json(UpdateDidDocumentOutput {
|
||||
success: true,
|
||||
did_document: did_doc,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_did_document(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
if !auth.did.starts_with("did:web:") {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"This endpoint is only available for did:web accounts".into(),
|
||||
@@ -142,7 +133,7 @@ pub async fn get_did_document(
|
||||
|
||||
let did_doc = build_did_document(&state, &auth.did).await;
|
||||
|
||||
Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response())
|
||||
Ok(Json(serde_json::json!({ "didDocument": did_doc })))
|
||||
}
|
||||
|
||||
async fn build_did_document(state: &AppState, did: &tranquil_pds::types::Did) -> serde_json::Value {
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use crate::common;
|
||||
use axum::{Json, extract::State, http::HeaderMap};
|
||||
use chrono::{Duration, Utc};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tranquil_db_traits::WebauthnChallengeType;
|
||||
use tranquil_pds::api::SuccessResponse;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::api::{OptionsResponse, SuccessResponse};
|
||||
use tranquil_pds::auth::NormalizedLoginIdentifier;
|
||||
use uuid::Uuid;
|
||||
|
||||
use tranquil_pds::auth::{ServiceTokenVerifier, generate_app_password, is_service_token};
|
||||
use tranquil_pds::rate_limit::{AccountCreationLimit, PasswordResetLimit, RateLimited};
|
||||
use tranquil_pds::repo_ops::create_signed_commit;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle, PlainPassword};
|
||||
use tranquil_pds::validation::validate_password;
|
||||
@@ -57,7 +47,7 @@ pub struct CreatePasskeyAccountInput {
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePasskeyAccountResponse {
|
||||
pub struct CreatePasskeyAccountOutput {
|
||||
pub did: Did,
|
||||
pub handle: Handle,
|
||||
pub setup_token: String,
|
||||
@@ -71,7 +61,7 @@ pub async fn create_passkey_account(
|
||||
_rate_limit: RateLimited<AccountCreationLimit>,
|
||||
headers: HeaderMap,
|
||||
Json(input): Json<CreatePasskeyAccountInput>,
|
||||
) -> Response {
|
||||
) -> Result<Json<CreatePasskeyAccountOutput>, ApiError> {
|
||||
let byod_auth = if let Some(extracted) = tranquil_pds::auth::extract_auth_token_from_header(
|
||||
tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION),
|
||||
) {
|
||||
@@ -91,11 +81,10 @@ pub async fn create_passkey_account(
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Service token verification failed: {:?}", e);
|
||||
return ApiError::AuthenticationFailed(Some(format!(
|
||||
return Err(ApiError::AuthenticationFailed(Some(format!(
|
||||
"Service token verification failed: {}",
|
||||
e
|
||||
)))
|
||||
.into_response();
|
||||
))));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -116,7 +105,7 @@ pub async fn create_passkey_account(
|
||||
let hostname = &cfg.server.hostname;
|
||||
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(None).into_response(),
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
};
|
||||
|
||||
let email = input
|
||||
@@ -127,7 +116,7 @@ pub async fn create_passkey_account(
|
||||
if let Some(ref email) = email
|
||||
&& !tranquil_pds::api::validation::is_valid_email(email)
|
||||
{
|
||||
return ApiError::InvalidEmail.into_response();
|
||||
return Err(ApiError::InvalidEmail);
|
||||
}
|
||||
|
||||
let is_bootstrap = state.bootstrap_invite_code.is_some()
|
||||
@@ -136,17 +125,17 @@ pub async fn create_passkey_account(
|
||||
let _validated_invite_code = if is_bootstrap {
|
||||
match input.invite_code.as_deref() {
|
||||
Some(code) if Some(code) == state.bootstrap_invite_code.as_deref() => None,
|
||||
_ => return ApiError::InvalidInviteCode.into_response(),
|
||||
_ => return Err(ApiError::InvalidInviteCode),
|
||||
}
|
||||
} else if let Some(ref code) = input.invite_code {
|
||||
match state.infra_repo.validate_invite_code(code).await {
|
||||
Ok(validated) => Some(validated),
|
||||
Err(_) => return ApiError::InvalidInviteCode.into_response(),
|
||||
Err(_) => return Err(ApiError::InvalidInviteCode),
|
||||
}
|
||||
} else {
|
||||
let invite_required = tranquil_config::get().server.invite_code_required;
|
||||
if invite_required {
|
||||
return ApiError::InviteCodeRequired.into_response();
|
||||
return Err(ApiError::InviteCodeRequired);
|
||||
}
|
||||
None
|
||||
};
|
||||
@@ -154,85 +143,39 @@ pub async fn create_passkey_account(
|
||||
let verification_channel = input
|
||||
.verification_channel
|
||||
.unwrap_or(tranquil_db_traits::CommsChannel::Email);
|
||||
let verification_recipient = match verification_channel {
|
||||
tranquil_db_traits::CommsChannel::Email => match &email {
|
||||
Some(e) if !e.is_empty() => e.clone(),
|
||||
_ => return ApiError::MissingEmail.into_response(),
|
||||
},
|
||||
tranquil_db_traits::CommsChannel::Discord => match &input.discord_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
let clean = username.trim().to_lowercase();
|
||||
if !tranquil_pds::api::validation::is_valid_discord_username(&clean) {
|
||||
return ApiError::InvalidRequest(
|
||||
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
|
||||
).into_response();
|
||||
}
|
||||
clean
|
||||
}
|
||||
_ => return ApiError::MissingDiscordId.into_response(),
|
||||
},
|
||||
tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
let clean = username.trim().trim_start_matches('@');
|
||||
if !tranquil_pds::api::validation::is_valid_telegram_username(clean) {
|
||||
return ApiError::InvalidRequest(
|
||||
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
|
||||
).into_response();
|
||||
}
|
||||
clean.to_string()
|
||||
}
|
||||
_ => return ApiError::MissingTelegramUsername.into_response(),
|
||||
},
|
||||
tranquil_db_traits::CommsChannel::Signal => match &input.signal_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
username.trim().trim_start_matches('@').to_lowercase()
|
||||
}
|
||||
_ => return ApiError::MissingSignalNumber.into_response(),
|
||||
let verification_recipient = match common::extract_verification_recipient(
|
||||
verification_channel,
|
||||
&common::ChannelInput {
|
||||
email: email.as_deref(),
|
||||
discord_username: input.discord_username.as_deref(),
|
||||
telegram_username: input.telegram_username.as_deref(),
|
||||
signal_username: input.signal_username.as_deref(),
|
||||
},
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
use k256::ecdsa::SigningKey;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let did_type = input.did_type.as_deref().unwrap_or("plc");
|
||||
|
||||
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<Uuid>) =
|
||||
if let Some(signing_key_did) = &input.signing_key {
|
||||
match state
|
||||
.infra_repo
|
||||
.get_reserved_signing_key(signing_key_did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(reserved)) => (reserved.private_key_bytes, Some(reserved.id)),
|
||||
Ok(None) => {
|
||||
return ApiError::InvalidSigningKey.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error looking up reserved signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let secret_key = k256::SecretKey::random(&mut OsRng);
|
||||
(secret_key.to_bytes().to_vec(), None)
|
||||
let key_result =
|
||||
match crate::identity::provision::resolve_signing_key(&state, input.signing_key.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(k) => k,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let secret_key = match SigningKey::from_slice(&secret_key_bytes) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Error creating signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let secret_key_bytes = key_result.secret_key_bytes;
|
||||
let secret_key = key_result.signing_key;
|
||||
let reserved_key_id = key_result.reserved_key_id;
|
||||
|
||||
let did = match did_type {
|
||||
"web" => {
|
||||
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
|
||||
return ApiError::SelfHostedDidWebDisabled.into_response();
|
||||
}
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
let self_hosted_did = format!("did:web:{}", encoded_handle);
|
||||
let self_hosted_did = match common::create_self_hosted_did_web(&handle) {
|
||||
Ok(d) => d,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
info!(did = %self_hosted_did, "Creating self-hosted did:web passkey account");
|
||||
self_hosted_did
|
||||
}
|
||||
@@ -240,25 +183,24 @@ pub async fn create_passkey_account(
|
||||
let d = match &input.did {
|
||||
Some(d) if !d.trim().is_empty() => d.trim(),
|
||||
_ => {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"External did:web requires the 'did' field to be provided".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
};
|
||||
if !d.starts_with("did:web:") {
|
||||
return ApiError::InvalidDid("External DID must be a did:web".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidDid(
|
||||
"External DID must be a did:web".into(),
|
||||
));
|
||||
}
|
||||
if is_byod_did_web {
|
||||
if let Some(ref auth_did) = byod_auth
|
||||
&& d != auth_did.as_str()
|
||||
{
|
||||
return ApiError::AuthorizationError(format!(
|
||||
return Err(ApiError::AuthorizationError(format!(
|
||||
"Service token issuer {} does not match DID {}",
|
||||
auth_did, d
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
info!(did = %d, "Creating external did:web passkey account (BYOD key)");
|
||||
} else {
|
||||
@@ -270,7 +212,7 @@ pub async fn create_passkey_account(
|
||||
)
|
||||
.await
|
||||
{
|
||||
return ApiError::InvalidDid(e.to_string()).into_response();
|
||||
return Err(ApiError::InvalidDid(e.to_string()));
|
||||
}
|
||||
info!(did = %d, "Creating external did:web passkey account (reserved key)");
|
||||
}
|
||||
@@ -281,25 +223,22 @@ pub async fn create_passkey_account(
|
||||
if let Some(ref provided_did) = input.did {
|
||||
if provided_did.starts_with("did:plc:") {
|
||||
if provided_did != auth_did.as_str() {
|
||||
return ApiError::AuthorizationError(format!(
|
||||
return Err(ApiError::AuthorizationError(format!(
|
||||
"Service token issuer {} does not match DID {}",
|
||||
auth_did, provided_did
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
info!(did = %provided_did, "Creating BYOD did:plc passkey account (migration)");
|
||||
provided_did.clone()
|
||||
} else {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"BYOD migration requires a did:plc or did:web DID".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"BYOD migration requires the 'did' field".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let rotation_key = tranquil_config::get()
|
||||
@@ -317,10 +256,9 @@ pub async fn create_passkey_account(
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error creating PLC genesis operation: {:?}", e);
|
||||
return ApiError::InternalError(Some(
|
||||
return Err(ApiError::InternalError(Some(
|
||||
"Failed to create PLC operation".into(),
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -331,11 +269,10 @@ pub async fn create_passkey_account(
|
||||
.await
|
||||
{
|
||||
error!("Failed to submit PLC genesis operation: {:?}", e);
|
||||
return ApiError::UpstreamErrorMsg(format!(
|
||||
return Err(ApiError::UpstreamErrorMsg(format!(
|
||||
"Failed to register DID with PLC directory: {}",
|
||||
e
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
genesis_result.did
|
||||
}
|
||||
@@ -345,13 +282,7 @@ pub async fn create_passkey_account(
|
||||
info!(did = %did, handle = %handle, "Created DID for passkey-only account");
|
||||
|
||||
let setup_token = generate_setup_token();
|
||||
let setup_token_hash = match hash(&setup_token, DEFAULT_COST) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
error!("Error hashing setup token: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let setup_token_hash = common::hash_or_internal_error(&setup_token)?;
|
||||
let setup_expires_at = Utc::now() + Duration::hours(1);
|
||||
|
||||
let deactivated_at: Option<chrono::DateTime<Utc>> = if is_byod_did_web {
|
||||
@@ -360,43 +291,21 @@ pub async fn create_passkey_account(
|
||||
None
|
||||
};
|
||||
|
||||
let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Error encrypting signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mst = Mst::new(Arc::new(state.block_store.clone()));
|
||||
let mst_root = match mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Error persisting MST: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
let did_typed: Did = match did.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InternalError(Some("Invalid DID".into())).into_response(),
|
||||
Err(_) => return Err(ApiError::InternalError(Some("Invalid DID".into()))),
|
||||
};
|
||||
let (commit_bytes, _sig) =
|
||||
match create_signed_commit(&did_typed, mst_root, rev.as_ref(), None, &secret_key) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("Error creating genesis commit: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Error saving genesis commit: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let repo = match crate::identity::provision::init_genesis_repo(
|
||||
&state,
|
||||
&did_typed,
|
||||
&secret_key,
|
||||
&secret_key_bytes,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
|
||||
|
||||
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
|
||||
Some(json!({
|
||||
@@ -409,39 +318,31 @@ pub async fn create_passkey_account(
|
||||
|
||||
let handle_typed: Handle = match handle.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(None).into_response(),
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
};
|
||||
let repo_for_seq = repo.clone();
|
||||
let comms = crate::identity::provision::normalize_comms_usernames(
|
||||
input.discord_username.as_deref(),
|
||||
input.telegram_username.as_deref(),
|
||||
input.signal_username.as_deref(),
|
||||
);
|
||||
let create_input = tranquil_db_traits::CreatePasskeyAccountInput {
|
||||
handle: handle_typed.clone(),
|
||||
email: email.clone().unwrap_or_default(),
|
||||
did: did_typed.clone(),
|
||||
preferred_comms_channel: verification_channel,
|
||||
discord_username: input
|
||||
.discord_username
|
||||
.as_deref()
|
||||
.map(|s| s.trim().to_lowercase())
|
||||
.filter(|s| !s.is_empty()),
|
||||
telegram_username: input
|
||||
.telegram_username
|
||||
.as_deref()
|
||||
.map(|s| s.trim().trim_start_matches('@'))
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
signal_username: input
|
||||
.signal_username
|
||||
.as_deref()
|
||||
.map(|s| s.trim().trim_start_matches('@'))
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_lowercase()),
|
||||
discord_username: comms.discord,
|
||||
telegram_username: comms.telegram,
|
||||
signal_username: comms.signal,
|
||||
setup_token_hash,
|
||||
setup_expires_at,
|
||||
deactivated_at,
|
||||
encrypted_key_bytes,
|
||||
encrypted_key_bytes: repo.encrypted_key_bytes,
|
||||
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
|
||||
reserved_key_id,
|
||||
commit_cid: commit_cid.to_string(),
|
||||
repo_rev: rev.as_ref().to_string(),
|
||||
genesis_block_cids,
|
||||
commit_cid: repo.commit_cid.to_string(),
|
||||
repo_rev: repo.repo_rev.clone(),
|
||||
genesis_block_cids: repo.genesis_block_cids,
|
||||
invite_code: if is_bootstrap {
|
||||
None
|
||||
} else {
|
||||
@@ -453,71 +354,37 @@ pub async fn create_passkey_account(
|
||||
let create_result = match state.user_repo.create_passkey_account(&create_input).await {
|
||||
Ok(r) => r,
|
||||
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
|
||||
return ApiError::HandleNotAvailable(None).into_response();
|
||||
return Err(ApiError::HandleNotAvailable(None));
|
||||
}
|
||||
Err(tranquil_db_traits::CreateAccountError::EmailTaken) => {
|
||||
return ApiError::EmailTaken.into_response();
|
||||
return Err(ApiError::EmailTaken);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error creating passkey account: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
let user_id = create_result.user_id;
|
||||
|
||||
if !is_byod_did_web {
|
||||
if let Err(e) =
|
||||
tranquil_pds::repo_ops::sequence_identity_event(&state, &did_typed, Some(&handle_typed))
|
||||
.await
|
||||
{
|
||||
warn!("Failed to sequence identity event for {}: {}", did, e);
|
||||
}
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
crate::identity::provision::sequence_new_account(
|
||||
&state,
|
||||
&did_typed,
|
||||
tranquil_db_traits::AccountStatus::Active,
|
||||
&handle_typed,
|
||||
&repo_for_seq,
|
||||
&handle,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to sequence account event for {}: {}", did, e);
|
||||
}
|
||||
let profile_record = serde_json::json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did_typed,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to create default profile for {}: {}", did, e);
|
||||
}
|
||||
.await;
|
||||
}
|
||||
|
||||
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
|
||||
crate::identity::provision::enqueue_signup_verification(
|
||||
&state,
|
||||
user_id,
|
||||
&did_typed,
|
||||
verification_channel,
|
||||
&verification_recipient,
|
||||
);
|
||||
let formatted_token =
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
verification_channel,
|
||||
&verification_recipient,
|
||||
&formatted_token,
|
||||
hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to enqueue signup verification: {:?}", e);
|
||||
}
|
||||
.await;
|
||||
|
||||
info!(did = %did, handle = %handle, "Passkey-only account created, awaiting setup completion");
|
||||
|
||||
@@ -553,14 +420,13 @@ pub async fn create_passkey_account(
|
||||
None
|
||||
};
|
||||
|
||||
Json(CreatePasskeyAccountResponse {
|
||||
Ok(Json(CreatePasskeyAccountOutput {
|
||||
did: did.into(),
|
||||
handle: handle.into(),
|
||||
setup_token,
|
||||
setup_expires_at,
|
||||
access_jwt,
|
||||
})
|
||||
.into_response()
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -574,7 +440,7 @@ pub struct CompletePasskeySetupInput {
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompletePasskeySetupResponse {
|
||||
pub struct CompletePasskeySetupOutput {
|
||||
pub did: Did,
|
||||
pub handle: Handle,
|
||||
pub app_password: String,
|
||||
@@ -584,38 +450,36 @@ pub struct CompletePasskeySetupResponse {
|
||||
pub async fn complete_passkey_setup(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<CompletePasskeySetupInput>,
|
||||
) -> Response {
|
||||
) -> Result<Json<CompletePasskeySetupOutput>, ApiError> {
|
||||
let user = match state.user_repo.get_user_for_passkey_setup(&input.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
return Err(ApiError::AccountNotFound);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
if user.password_required {
|
||||
return ApiError::InvalidAccount.into_response();
|
||||
return Err(ApiError::InvalidAccount);
|
||||
}
|
||||
|
||||
let token_hash = match &user.recovery_token {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
return ApiError::SetupExpired.into_response();
|
||||
return Err(ApiError::SetupExpired);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(expires_at) = user.recovery_token_expires_at
|
||||
&& expires_at < Utc::now()
|
||||
{
|
||||
return ApiError::SetupExpired.into_response();
|
||||
}
|
||||
|
||||
if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
common::validate_token_hash(
|
||||
user.recovery_token_expires_at,
|
||||
token_hash,
|
||||
&input.setup_token,
|
||||
ApiError::SetupExpired,
|
||||
ApiError::InvalidToken(None),
|
||||
)?;
|
||||
|
||||
let webauthn = &state.webauthn_config;
|
||||
|
||||
@@ -628,15 +492,15 @@ pub async fn complete_passkey_setup(
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Error deserializing registration state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
return ApiError::NoChallengeInProgress.into_response();
|
||||
return Err(ApiError::NoChallengeInProgress);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error loading registration state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -645,7 +509,7 @@ pub async fn complete_passkey_setup(
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse credential: {:?}", e);
|
||||
return ApiError::InvalidCredential.into_response();
|
||||
return Err(ApiError::InvalidCredential);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -653,7 +517,7 @@ pub async fn complete_passkey_setup(
|
||||
Ok(sk) => sk,
|
||||
Err(e) => {
|
||||
warn!("Passkey registration failed: {:?}", e);
|
||||
return ApiError::RegistrationFailed.into_response();
|
||||
return Err(ApiError::RegistrationFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -662,7 +526,7 @@ pub async fn complete_passkey_setup(
|
||||
Ok(pk) => pk,
|
||||
Err(e) => {
|
||||
error!("Error serializing security key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
if let Err(e) = state
|
||||
@@ -676,18 +540,12 @@ pub async fn complete_passkey_setup(
|
||||
.await
|
||||
{
|
||||
error!("Error saving passkey: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
|
||||
let app_password = generate_app_password();
|
||||
let app_password_name = "bsky.app".to_string();
|
||||
let password_hash = match hash(&app_password, DEFAULT_COST) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
error!("Error hashing app password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let password_hash = common::hash_or_internal_error(&app_password)?;
|
||||
|
||||
let setup_input = tranquil_db_traits::CompletePasskeySetupInput {
|
||||
user_id: user.id,
|
||||
@@ -697,7 +555,7 @@ pub async fn complete_passkey_setup(
|
||||
};
|
||||
if let Err(e) = state.user_repo.complete_passkey_setup(&setup_input).await {
|
||||
error!("Error completing passkey setup: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
|
||||
let _ = state
|
||||
@@ -707,50 +565,47 @@ pub async fn complete_passkey_setup(
|
||||
|
||||
info!(did = %input.did, "Passkey-only account setup completed");
|
||||
|
||||
Json(CompletePasskeySetupResponse {
|
||||
Ok(Json(CompletePasskeySetupOutput {
|
||||
did: input.did.clone(),
|
||||
handle: user.handle,
|
||||
app_password,
|
||||
app_password_name,
|
||||
})
|
||||
.into_response()
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn start_passkey_registration_for_setup(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<StartPasskeyRegistrationInput>,
|
||||
) -> Response {
|
||||
) -> Result<Json<OptionsResponse<serde_json::Value>>, ApiError> {
|
||||
let user = match state.user_repo.get_user_for_passkey_setup(&input.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
return Err(ApiError::AccountNotFound);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
if user.password_required {
|
||||
return ApiError::InvalidAccount.into_response();
|
||||
return Err(ApiError::InvalidAccount);
|
||||
}
|
||||
|
||||
let token_hash = match &user.recovery_token {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
return ApiError::SetupExpired.into_response();
|
||||
return Err(ApiError::SetupExpired);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(expires_at) = user.recovery_token_expires_at
|
||||
&& expires_at < Utc::now()
|
||||
{
|
||||
return ApiError::SetupExpired.into_response();
|
||||
}
|
||||
|
||||
if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
common::validate_token_hash(
|
||||
user.recovery_token_expires_at,
|
||||
token_hash,
|
||||
&input.setup_token,
|
||||
ApiError::SetupExpired,
|
||||
ApiError::InvalidToken(None),
|
||||
)?;
|
||||
|
||||
let webauthn = &state.webauthn_config;
|
||||
|
||||
@@ -776,7 +631,7 @@ pub async fn start_passkey_registration_for_setup(
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("Failed to start passkey registration: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -784,7 +639,7 @@ pub async fn start_passkey_registration_for_setup(
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
error!("Failed to serialize registration state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
if let Err(e) = state
|
||||
@@ -793,11 +648,11 @@ pub async fn start_passkey_registration_for_setup(
|
||||
.await
|
||||
{
|
||||
error!("Failed to save registration state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
|
||||
let options = serde_json::to_value(&ccr).unwrap_or(json!({}));
|
||||
Json(json!({"options": options})).into_response()
|
||||
Ok(OptionsResponse::new(options))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -819,7 +674,7 @@ pub async fn request_passkey_recovery(
|
||||
State(state): State<AppState>,
|
||||
_rate_limit: RateLimited<PasswordResetLimit>,
|
||||
Json(input): Json<RequestPasskeyRecoveryInput>,
|
||||
) -> Response {
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let identifier = input.email.trim().to_lowercase();
|
||||
let identifier = identifier.strip_prefix('@').unwrap_or(&identifier);
|
||||
@@ -833,17 +688,12 @@ pub async fn request_passkey_recovery(
|
||||
{
|
||||
Ok(Some(u)) if !u.password_required => u,
|
||||
_ => {
|
||||
return SuccessResponse::ok().into_response();
|
||||
return Ok(Json(SuccessResponse { success: true }));
|
||||
}
|
||||
};
|
||||
|
||||
let recovery_token = generate_setup_token();
|
||||
let recovery_token_hash = match hash(&recovery_token, DEFAULT_COST) {
|
||||
Ok(h) => h,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let recovery_token_hash = common::hash_or_internal_error(&recovery_token)?;
|
||||
let expires_at = Utc::now() + Duration::hours(1);
|
||||
|
||||
if let Err(e) = state
|
||||
@@ -852,7 +702,7 @@ pub async fn request_passkey_recovery(
|
||||
.await
|
||||
{
|
||||
error!("Error updating recovery token: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
@@ -873,7 +723,7 @@ pub async fn request_passkey_recovery(
|
||||
.await;
|
||||
|
||||
info!(did = %user.did, "Passkey recovery requested");
|
||||
SuccessResponse::ok().into_response()
|
||||
Ok(Json(SuccessResponse { success: true }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -887,41 +737,34 @@ pub struct RecoverPasskeyAccountInput {
|
||||
pub async fn recover_passkey_account(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<RecoverPasskeyAccountInput>,
|
||||
) -> Response {
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
if let Err(e) = validate_password(&input.new_password) {
|
||||
return ApiError::InvalidRequest(e.to_string()).into_response();
|
||||
return Err(ApiError::InvalidRequest(e.to_string()));
|
||||
}
|
||||
|
||||
let user = match state.user_repo.get_user_for_recovery(&input.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
_ => {
|
||||
return ApiError::InvalidRecoveryLink.into_response();
|
||||
return Err(ApiError::InvalidRecoveryLink);
|
||||
}
|
||||
};
|
||||
|
||||
let token_hash = match &user.recovery_token {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
return ApiError::InvalidRecoveryLink.into_response();
|
||||
return Err(ApiError::InvalidRecoveryLink);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(expires_at) = user.recovery_token_expires_at
|
||||
&& expires_at < Utc::now()
|
||||
{
|
||||
return ApiError::RecoveryLinkExpired.into_response();
|
||||
}
|
||||
common::validate_token_hash(
|
||||
user.recovery_token_expires_at,
|
||||
token_hash,
|
||||
&input.recovery_token,
|
||||
ApiError::RecoveryLinkExpired,
|
||||
ApiError::InvalidRecoveryLink,
|
||||
)?;
|
||||
|
||||
if !bcrypt::verify(&input.recovery_token, token_hash).unwrap_or(false) {
|
||||
return ApiError::InvalidRecoveryLink.into_response();
|
||||
}
|
||||
|
||||
let password_hash = match hash(&input.new_password, DEFAULT_COST) {
|
||||
Ok(h) => h,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let password_hash = common::hash_or_internal_error(&input.new_password)?;
|
||||
|
||||
let recover_input = tranquil_db_traits::RecoverPasskeyAccountInput {
|
||||
did: input.did.clone(),
|
||||
@@ -935,7 +778,7 @@ pub async fn recover_passkey_account(
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error recovering passkey account: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -957,5 +800,5 @@ pub async fn recover_passkey_account(
|
||||
}
|
||||
}
|
||||
info!(did = %input.did, "Passkey-only account recovered with temporary password");
|
||||
SuccessResponse::ok().into_response()
|
||||
Ok(Json(SuccessResponse { success: true }))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info, warn};
|
||||
use tranquil_db_traits::WebauthnChallengeType;
|
||||
@@ -20,7 +16,7 @@ pub struct StartRegistrationInput {
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartRegistrationResponse {
|
||||
pub struct StartRegistrationOutput {
|
||||
pub options: serde_json::Value,
|
||||
}
|
||||
|
||||
@@ -28,7 +24,7 @@ pub async fn start_passkey_registration(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<StartRegistrationInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<StartRegistrationOutput>, ApiError> {
|
||||
let webauthn = &state.webauthn_config;
|
||||
|
||||
let handle = state
|
||||
@@ -73,7 +69,7 @@ pub async fn start_passkey_registration(
|
||||
|
||||
info!(did = %auth.did, "Passkey registration started");
|
||||
|
||||
Ok(Json(StartRegistrationResponse { options }).into_response())
|
||||
Ok(Json(StartRegistrationOutput { options }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -85,7 +81,7 @@ pub struct FinishRegistrationInput {
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FinishRegistrationResponse {
|
||||
pub struct FinishRegistrationOutput {
|
||||
pub id: String,
|
||||
pub credential_id: String,
|
||||
}
|
||||
@@ -94,7 +90,7 @@ pub async fn finish_passkey_registration(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<FinishRegistrationInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<FinishRegistrationOutput>, ApiError> {
|
||||
let webauthn = &state.webauthn_config;
|
||||
|
||||
let reg_state_json = state
|
||||
@@ -154,11 +150,10 @@ pub async fn finish_passkey_registration(
|
||||
|
||||
info!(did = %auth.did, passkey_id = %passkey_id, "Passkey registered");
|
||||
|
||||
Ok(Json(FinishRegistrationResponse {
|
||||
Ok(Json(FinishRegistrationOutput {
|
||||
id: passkey_id.to_string(),
|
||||
credential_id: credential_id_base64,
|
||||
})
|
||||
.into_response())
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -173,14 +168,14 @@ pub struct PasskeyInfo {
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListPasskeysResponse {
|
||||
pub struct ListPasskeysOutput {
|
||||
pub passkeys: Vec<PasskeyInfo>,
|
||||
}
|
||||
|
||||
pub async fn list_passkeys(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<ListPasskeysOutput>, ApiError> {
|
||||
let passkeys = state
|
||||
.user_repo
|
||||
.get_passkeys_for_user(&auth.did)
|
||||
@@ -198,10 +193,9 @@ pub async fn list_passkeys(
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(ListPasskeysResponse {
|
||||
Ok(Json(ListPasskeysOutput {
|
||||
passkeys: passkey_infos,
|
||||
})
|
||||
.into_response())
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -214,23 +208,17 @@ pub async fn delete_passkey(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<DeletePasskeyInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let session_mfa = match require_legacy_session_mfa(&state, &auth).await {
|
||||
Ok(proof) => proof,
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
) -> Result<Json<EmptyResponse>, ApiError> {
|
||||
let session_mfa = require_legacy_session_mfa(&state, &auth).await?;
|
||||
|
||||
let reauth_mfa = match require_reauth_window(&state, &auth).await {
|
||||
Ok(proof) => proof,
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
let reauth_mfa = require_reauth_window(&state, &auth).await?;
|
||||
|
||||
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
|
||||
|
||||
match state.user_repo.delete_passkey(id, reauth_mfa.did()).await {
|
||||
Ok(true) => {
|
||||
info!(did = %session_mfa.did(), passkey_id = %id, "Passkey deleted");
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
Ok(Json(EmptyResponse {}))
|
||||
}
|
||||
Ok(false) => Err(ApiError::PasskeyNotFound),
|
||||
Err(e) => {
|
||||
@@ -251,7 +239,7 @@ pub async fn update_passkey(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdatePasskeyInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<EmptyResponse>, ApiError> {
|
||||
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
|
||||
|
||||
match state
|
||||
@@ -261,7 +249,7 @@ pub async fn update_passkey(
|
||||
{
|
||||
Ok(true) => {
|
||||
info!(did = %auth.did, passkey_id = %id, "Passkey renamed");
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
Ok(Json(EmptyResponse {}))
|
||||
}
|
||||
Ok(false) => Err(ApiError::PasskeyNotFound),
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum::{Json, extract::State};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info};
|
||||
@@ -67,14 +63,14 @@ pub struct TrustedDevice {
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListTrustedDevicesResponse {
|
||||
pub struct ListTrustedDevicesOutput {
|
||||
pub devices: Vec<TrustedDevice>,
|
||||
}
|
||||
|
||||
pub async fn list_trusted_devices(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<ListTrustedDevicesOutput>, ApiError> {
|
||||
let rows = state
|
||||
.oauth_repo
|
||||
.list_trusted_devices(&auth.did)
|
||||
@@ -97,7 +93,7 @@ pub async fn list_trusted_devices(
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(ListTrustedDevicesResponse { devices }).into_response())
|
||||
Ok(Json(ListTrustedDevicesOutput { devices }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -110,7 +106,7 @@ pub async fn revoke_trusted_device(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<RevokeTrustedDeviceInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.oauth_repo
|
||||
.device_belongs_to_user(&input.device_id, &auth.did)
|
||||
@@ -133,7 +129,7 @@ pub async fn revoke_trusted_device(
|
||||
.log_db_err("revoking device trust")?;
|
||||
|
||||
info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked");
|
||||
Ok(SuccessResponse::ok().into_response())
|
||||
Ok(Json(SuccessResponse { success: true }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -147,7 +143,7 @@ pub async fn update_trusted_device(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateTrustedDeviceInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.oauth_repo
|
||||
.device_belongs_to_user(&input.device_id, &auth.did)
|
||||
@@ -170,7 +166,7 @@ pub async fn update_trusted_device(
|
||||
.log_db_err("updating device friendly name")?;
|
||||
|
||||
info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated");
|
||||
Ok(SuccessResponse::ok().into_response())
|
||||
Ok(Json(SuccessResponse { success: true }))
|
||||
}
|
||||
|
||||
pub async fn get_device_trust_state(
|
||||
|
||||
Reference in New Issue
Block a user