refactor(api): update delegation, notification prefs, email, meta, and age assurance endpoints

This commit is contained in:
Lewis
2026-03-20 13:39:11 +00:00
committed by Tangled
parent d123168787
commit c8bb4c9076
6 changed files with 397 additions and 407 deletions
+57 -79
View File
@@ -12,8 +12,11 @@ use subtle::ConstantTimeEq;
use tracing::{error, info, warn};
use tranquil_db_traits::CommsChannel;
use tranquil_pds::api::error::{ApiError, DbResultExt};
use tranquil_pds::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse};
use tranquil_pds::api::{
EmailUpdateStatusOutput, EmptyResponse, InUseOutput, TokenRequiredResponse, VerifiedResponse,
};
use tranquil_pds::auth::{Auth, NotTakendown};
use tranquil_pds::oauth::scopes::{AccountAction, AccountAttr};
use tranquil_pds::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit};
use tranquil_pds::state::AppState;
@@ -48,15 +51,8 @@ pub async fn request_email_update(
_rate_limit: RateLimited<EmailUpdateLimit>,
auth: Auth<NotTakendown>,
input: Option<Json<RequestEmailUpdateInput>>,
) -> Result<Response, ApiError> {
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
&auth.auth_source,
auth.scope.as_deref(),
tranquil_pds::oauth::scopes::AccountAttr::Email,
tranquil_pds::oauth::scopes::AccountAction::Manage,
) {
return Ok(e);
}
) -> Result<Json<TokenRequiredResponse>, ApiError> {
auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?;
let user = state
.user_repo
@@ -119,7 +115,7 @@ pub async fn request_email_update(
}
info!("Email update requested for user {}", user.id);
Ok(TokenRequiredResponse::response(token_required).into_response())
Ok(Json(TokenRequiredResponse { token_required }))
}
#[derive(Deserialize)]
@@ -134,15 +130,8 @@ pub async fn confirm_email(
_rate_limit: RateLimited<EmailUpdateLimit>,
auth: Auth<NotTakendown>,
Json(input): Json<ConfirmEmailInput>,
) -> Result<Response, ApiError> {
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
&auth.auth_source,
auth.scope.as_deref(),
tranquil_pds::oauth::scopes::AccountAttr::Email,
tranquil_pds::oauth::scopes::AccountAction::Manage,
) {
return Ok(e);
}
) -> Result<Json<EmptyResponse>, ApiError> {
auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?;
let did = &auth.did;
let user = state
@@ -163,7 +152,7 @@ pub async fn confirm_email(
}
if user.email_verified {
return Ok(EmptyResponse::ok().into_response());
return Ok(Json(EmptyResponse {}));
}
let confirmation_code =
@@ -196,7 +185,7 @@ pub async fn confirm_email(
.log_db_err("confirming email")?;
info!("Email confirmed for user {}", user.id);
Ok(EmptyResponse::ok().into_response())
Ok(Json(EmptyResponse {}))
}
#[derive(Deserialize)]
@@ -212,15 +201,8 @@ pub async fn update_email(
State(state): State<AppState>,
auth: Auth<NotTakendown>,
Json(input): Json<UpdateEmailInput>,
) -> Result<Response, ApiError> {
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
&auth.auth_source,
auth.scope.as_deref(),
tranquil_pds::oauth::scopes::AccountAttr::Email,
tranquil_pds::oauth::scopes::AccountAction::Manage,
) {
return Ok(e);
}
) -> Result<Json<EmptyResponse>, ApiError> {
auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?;
let did = &auth.did;
let user = state
@@ -279,7 +261,7 @@ pub async fn update_email(
ApiError::InternalError(Some("Failed to update 2FA setting".into()))
})?;
}
return Ok(EmptyResponse::ok().into_response());
return Ok(Json(EmptyResponse {}));
}
if email_verified {
@@ -394,7 +376,7 @@ pub async fn update_email(
}
info!("Email updated for user {}", user_id);
Ok(EmptyResponse::ok().into_response())
Ok(Json(EmptyResponse {}))
}
#[derive(Deserialize)]
@@ -406,19 +388,18 @@ pub async fn check_email_verified(
State(state): State<AppState>,
_rate_limit: RateLimited<VerificationCheckLimit>,
Json(input): Json<CheckEmailVerifiedInput>,
) -> Response {
match state
) -> Result<Json<VerifiedResponse>, ApiError> {
let verified = state
.user_repo
.check_email_verified_by_identifier(&input.identifier)
.await
{
Ok(Some(verified)) => VerifiedResponse::response(verified).into_response(),
Ok(None) => ApiError::AccountNotFound.into_response(),
Err(e) => {
.map_err(|e| {
error!("DB error checking email verified: {:?}", e);
ApiError::InternalError(None).into_response()
}
}
ApiError::InternalError(None)
})?
.ok_or(ApiError::AccountNotFound)?;
Ok(Json(VerifiedResponse { verified }))
}
#[derive(Deserialize)]
@@ -431,19 +412,18 @@ pub async fn check_channel_verified(
State(state): State<AppState>,
_rate_limit: RateLimited<VerificationCheckLimit>,
Json(input): Json<CheckChannelVerifiedInput>,
) -> Response {
match state
) -> Result<Json<VerifiedResponse>, ApiError> {
let verified = state
.user_repo
.check_channel_verified_by_did(&input.did, input.channel)
.await
{
Ok(Some(verified)) => VerifiedResponse::response(verified).into_response(),
Ok(None) => ApiError::AccountNotFound.into_response(),
Err(e) => {
.map_err(|e| {
error!("DB error checking channel verified: {:?}", e);
ApiError::InternalError(None).into_response()
}
}
ApiError::InternalError(None)
})?
.ok_or(ApiError::AccountNotFound)?;
Ok(Json(VerifiedResponse { verified }))
}
#[derive(Deserialize)]
@@ -545,37 +525,37 @@ pub async fn check_email_update_status(
State(state): State<AppState>,
_rate_limit: RateLimited<VerificationCheckLimit>,
auth: Auth<NotTakendown>,
) -> Result<Response, ApiError> {
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
&auth.auth_source,
auth.scope.as_deref(),
tranquil_pds::oauth::scopes::AccountAttr::Email,
tranquil_pds::oauth::scopes::AccountAction::Read,
) {
return Ok(e);
}
) -> Result<Json<EmailUpdateStatusOutput>, ApiError> {
auth.check_account_scope(AccountAttr::Email, AccountAction::Read)?;
let cache_key = email_update_cache_key(&auth.did);
let pending_json = match state.cache.get(&cache_key).await {
Some(json) => json,
None => {
return Ok(Json(json!({ "pending": false, "authorized": false })).into_response());
return Ok(Json(EmailUpdateStatusOutput {
pending: false,
authorized: false,
new_email: None,
}));
}
};
let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) {
Ok(p) => p,
Err(_) => {
return Ok(Json(json!({ "pending": false, "authorized": false })).into_response());
return Ok(Json(EmailUpdateStatusOutput {
pending: false,
authorized: false,
new_email: None,
}));
}
};
Ok(Json(json!({
"pending": true,
"authorized": pending.authorized,
"newEmail": pending.new_email,
Ok(Json(EmailUpdateStatusOutput {
pending: true,
authorized: pending.authorized,
new_email: Some(pending.new_email),
}))
.into_response())
}
#[derive(Deserialize)]
@@ -587,22 +567,20 @@ pub async fn check_email_in_use(
State(state): State<AppState>,
_rate_limit: RateLimited<VerificationCheckLimit>,
Json(input): Json<CheckEmailInUseInput>,
) -> Response {
) -> Result<Json<InUseOutput>, ApiError> {
let email = input.email.trim().to_lowercase();
if email.is_empty() {
return ApiError::InvalidRequest("email is required".into()).into_response();
return Err(ApiError::InvalidRequest("email is required".into()));
}
let count = match state.user_repo.count_accounts_by_email(&email).await {
Ok(c) => c,
Err(e) => {
let count = state
.user_repo
.count_accounts_by_email(&email)
.await
.map_err(|e| {
error!("DB error checking email usage: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
ApiError::InternalError(None)
})?;
Json(json!({
"inUse": count > 0,
}))
.into_response()
Ok(Json(InUseOutput { in_use: count > 0 }))
}
+74 -46
View File
@@ -1,11 +1,11 @@
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use serde_json::json;
use serde::Serialize;
use tranquil_db_traits::CommsChannel;
use tranquil_pds::BUILD_VERSION;
use tranquil_pds::state::AppState;
use tranquil_pds::util::{discord_app_id, discord_bot_username, telegram_bot_username};
fn get_available_comms_channels() -> Vec<tranquil_db_traits::CommsChannel> {
use tranquil_db_traits::CommsChannel;
fn get_available_comms_channels() -> Vec<CommsChannel> {
let cfg = tranquil_config::get();
let mut channels = vec![CommsChannel::Email];
if cfg.discord.bot_token.is_some() {
@@ -31,59 +31,87 @@ pub fn is_self_hosted_did_web_enabled() -> bool {
tranquil_config::get().server.enable_pds_hosted_did_web
}
pub async fn describe_server() -> impl IntoResponse {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DescribeServerLinks {
#[serde(skip_serializing_if = "Option::is_none")]
pub privacy_policy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terms_of_service: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DescribeServerContact {
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DescribeServerOutput {
pub available_user_domains: Vec<String>,
pub invite_code_required: bool,
pub did: String,
pub links: DescribeServerLinks,
pub contact: DescribeServerContact,
pub version: &'static str,
pub available_comms_channels: Vec<CommsChannel>,
pub self_hosted_did_web_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub discord_bot_username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discord_app_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub telegram_bot_username: Option<String>,
}
pub async fn describe_server() -> Json<DescribeServerOutput> {
let cfg = tranquil_config::get();
let pds_hostname = &cfg.server.hostname;
let domains = cfg.server.user_handle_domain_list();
let invite_code_required = cfg.server.invite_code_required;
let privacy_policy = cfg.server.privacy_policy_url.clone();
let terms_of_service = cfg.server.terms_of_service_url.clone();
let contact_email = cfg.server.contact_email.clone();
let mut links = serde_json::Map::new();
if let Some(pp) = privacy_policy {
links.insert("privacyPolicy".to_string(), json!(pp));
}
if let Some(tos) = terms_of_service {
links.insert("termsOfService".to_string(), json!(tos));
}
let mut contact = serde_json::Map::new();
if let Some(email) = contact_email {
contact.insert("email".to_string(), json!(email));
}
let mut response = json!({
"availableUserDomains": domains,
"inviteCodeRequired": invite_code_required,
"did": format!("did:web:{}", pds_hostname),
"links": links,
"contact": contact,
"version": BUILD_VERSION,
"availableCommsChannels": get_available_comms_channels(),
"selfHostedDidWebEnabled": is_self_hosted_did_web_enabled()
});
if let Some(bot_username) = discord_bot_username() {
response["discordBotUsername"] = json!(bot_username);
}
if let Some(app_id) = discord_app_id() {
response["discordAppId"] = json!(app_id);
}
if let Some(bot_username) = telegram_bot_username() {
response["telegramBotUsername"] = json!(bot_username);
}
Json(response)
Json(DescribeServerOutput {
available_user_domains: cfg.server.user_handle_domain_list(),
invite_code_required: cfg.server.invite_code_required,
did: format!("did:web:{}", pds_hostname),
links: DescribeServerLinks {
privacy_policy: cfg.server.privacy_policy_url.clone(),
terms_of_service: cfg.server.terms_of_service_url.clone(),
},
contact: DescribeServerContact {
email: cfg.server.contact_email.clone(),
},
version: BUILD_VERSION,
available_comms_channels: get_available_comms_channels(),
self_hosted_did_web_enabled: is_self_hosted_did_web_enabled(),
discord_bot_username: discord_bot_username().map(String::from),
discord_app_id: discord_app_id().map(String::from),
telegram_bot_username: telegram_bot_username().map(String::from),
})
}
#[derive(Serialize)]
pub struct HealthOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<&'static str>,
}
pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
match state.infra_repo.health_check().await {
Ok(true) => (
StatusCode::OK,
Json(json!({
"version": format!("tranquil {}", BUILD_VERSION)
})),
Json(HealthOutput {
version: Some(format!("tranquil {}", BUILD_VERSION)),
error: None,
}),
),
_ => (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"error": "Service Unavailable"
})),
Json(HealthOutput {
version: None,
error: Some("Service Unavailable"),
}),
),
}
}