mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-07-29 11:52:39 +00:00
refactor(api): update delegation, notification prefs, email, meta, and age assurance endpoints
This commit is contained in:
@@ -69,7 +69,7 @@ pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Permissiv
|
||||
if let Some(age) = personal_details_pref
|
||||
.as_ref()
|
||||
.and_then(|pref| pref.get("birthDate"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(Value::as_str)
|
||||
.and_then(get_age_from_datestring)
|
||||
{
|
||||
let declared_age_pref = serde_json::json!({
|
||||
@@ -122,7 +122,7 @@ pub async fn put_preferences(
|
||||
if pref_str.len() > MAX_PREFERENCE_SIZE {
|
||||
return PrefValidation::TooLarge(pref_str.len());
|
||||
}
|
||||
let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
|
||||
let pref_type = match pref.get("$type").and_then(Value::as_str) {
|
||||
Some(t) => t,
|
||||
None => return PrefValidation::MissingType,
|
||||
};
|
||||
@@ -179,7 +179,7 @@ pub async fn put_preferences(
|
||||
.preferences
|
||||
.into_iter()
|
||||
.filter_map(|pref| {
|
||||
let pref_type = pref.get("$type").and_then(|t| t.as_str())?;
|
||||
let pref_type = pref.get("$type").and_then(Value::as_str)?;
|
||||
if pref_type == DECLARED_AGE_PREF {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,60 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
http::{HeaderMap, Method, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
http::{HeaderMap, Method},
|
||||
};
|
||||
use serde_json::json;
|
||||
use serde::Serialize;
|
||||
use tranquil_pds::auth::{
|
||||
AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop,
|
||||
};
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
pub async fn get_state(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgeAssuranceState {
|
||||
pub status: &'static str,
|
||||
pub access: &'static str,
|
||||
pub last_initiated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgeAssuranceMetadata {
|
||||
pub account_created_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetAgeAssuranceOutput {
|
||||
pub state: AgeAssuranceState,
|
||||
pub metadata: AgeAssuranceMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AgeAssuranceStatusOutput {
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
pub async fn get_state(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Json<GetAgeAssuranceOutput> {
|
||||
let created_at = get_account_created_at(&state, &headers).await;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"state": {
|
||||
"status": "assured",
|
||||
"access": "full",
|
||||
"lastInitiatedAt": now
|
||||
},
|
||||
"metadata": {
|
||||
"accountCreatedAt": created_at
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
Json(GetAgeAssuranceOutput {
|
||||
state: AgeAssuranceState {
|
||||
status: "assured",
|
||||
access: "full",
|
||||
last_initiated_at: now,
|
||||
},
|
||||
metadata: AgeAssuranceMetadata {
|
||||
account_created_at: created_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_age_assurance_state() -> Response {
|
||||
(StatusCode::OK, Json(json!({"status": "assured"}))).into_response()
|
||||
pub async fn get_age_assurance_state() -> Json<AgeAssuranceStatusOutput> {
|
||||
Json(AgeAssuranceStatusOutput { status: "assured" })
|
||||
}
|
||||
|
||||
async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> {
|
||||
|
||||
@@ -2,13 +2,14 @@ use crate::identity::provision::{create_plc_did, init_genesis_repo};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tracing::{error, info, warn};
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::api::{
|
||||
AccountsOutput, AuditLogOutput, ControllersOutput, PresetsOutput, SuccessResponse,
|
||||
};
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::delegation::{
|
||||
DelegationActionType, SCOPE_PRESETS, ValidatedDelegationScope, verify_can_add_controllers,
|
||||
@@ -21,20 +22,15 @@ use tranquil_pds::types::{Did, Handle};
|
||||
pub async fn list_controllers(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let controllers = match state
|
||||
) -> Result<Json<ControllersOutput<Vec<tranquil_db_traits::ControllerInfo>>>, ApiError> {
|
||||
let controllers = state
|
||||
.delegation_repo
|
||||
.get_delegations_for_account(&auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to list controllers: {:?}", e);
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to list controllers".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(Some("Failed to list controllers".into()))
|
||||
})?;
|
||||
|
||||
let resolve_futures = controllers.into_iter().map(|mut c| {
|
||||
let did_resolver = state.did_resolver.clone();
|
||||
@@ -44,7 +40,7 @@ pub async fn list_controllers(
|
||||
.resolve_did_document(c.did.as_str())
|
||||
.await
|
||||
.and_then(|doc| tranquil_types::did_doc::extract_handle(&doc))
|
||||
.map(|h| h.into());
|
||||
.map(Into::into);
|
||||
}
|
||||
c
|
||||
}
|
||||
@@ -52,7 +48,7 @@ pub async fn list_controllers(
|
||||
|
||||
let controllers = futures::future::join_all(resolve_futures).await;
|
||||
|
||||
Ok(Json(serde_json::json!({ "controllers": controllers })).into_response())
|
||||
Ok(Json(ControllersOutput { controllers }))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -65,7 +61,7 @@ pub async fn add_controller(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<AddControllerInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did)
|
||||
.await
|
||||
.ok_or(ApiError::ControllerNotFound)?;
|
||||
@@ -74,9 +70,9 @@ pub async fn add_controller(
|
||||
&& let Some(ref pds_url) = resolved.pds_url
|
||||
{
|
||||
if !pds_url.starts_with("https://") {
|
||||
return Ok(
|
||||
ApiError::InvalidDelegation("Controller PDS must use HTTPS".into()).into_response(),
|
||||
);
|
||||
return Err(ApiError::InvalidDelegation(
|
||||
"Controller PDS must use HTTPS".into(),
|
||||
));
|
||||
}
|
||||
match state
|
||||
.cross_pds_oauth
|
||||
@@ -84,10 +80,9 @@ pub async fn add_controller(
|
||||
.await
|
||||
{
|
||||
Some(true) => {
|
||||
return Ok(ApiError::InvalidDelegation(
|
||||
return Err(ApiError::InvalidDelegation(
|
||||
"Cannot add a delegated account from another PDS as a controller".into(),
|
||||
)
|
||||
.into_response());
|
||||
));
|
||||
}
|
||||
Some(false) => {}
|
||||
None => {
|
||||
@@ -100,10 +95,7 @@ pub async fn add_controller(
|
||||
}
|
||||
}
|
||||
|
||||
let can_add = match verify_can_add_controllers(&state, &auth).await {
|
||||
Ok(proof) => proof,
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
let can_add = verify_can_add_controllers(&state, &auth).await?;
|
||||
|
||||
if resolved.is_local
|
||||
&& state
|
||||
@@ -112,10 +104,9 @@ pub async fn add_controller(
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(ApiError::InvalidDelegation(
|
||||
return Err(ApiError::InvalidDelegation(
|
||||
"Cannot add a controlled account as a controller".into(),
|
||||
)
|
||||
.into_response());
|
||||
));
|
||||
}
|
||||
|
||||
match state
|
||||
@@ -136,7 +127,7 @@ pub async fn add_controller(
|
||||
can_add.did(),
|
||||
Some(&input.controller_did),
|
||||
DelegationActionType::GrantCreated,
|
||||
Some(serde_json::json!({
|
||||
Some(json!({
|
||||
"granted_scopes": input.granted_scopes.as_str(),
|
||||
"is_local": resolved.is_local
|
||||
})),
|
||||
@@ -145,17 +136,13 @@ pub async fn add_controller(
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"success": true
|
||||
})),
|
||||
)
|
||||
.into_response())
|
||||
Ok(Json(SuccessResponse { success: true }))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to add controller: {:?}", e);
|
||||
Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response())
|
||||
Err(ApiError::InternalError(Some(
|
||||
"Failed to add controller".into(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +156,7 @@ pub async fn remove_controller(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<RemoveControllerInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.delegation_repo
|
||||
.revoke_delegation(&auth.did, &input.controller_did, &auth.did)
|
||||
@@ -197,7 +184,7 @@ pub async fn remove_controller(
|
||||
&auth.did,
|
||||
Some(&input.controller_did),
|
||||
DelegationActionType::GrantRevoked,
|
||||
Some(serde_json::json!({
|
||||
Some(json!({
|
||||
"revoked_app_passwords": revoked_app_passwords,
|
||||
"revoked_oauth_tokens": revoked_oauth_tokens
|
||||
})),
|
||||
@@ -206,18 +193,14 @@ pub async fn remove_controller(
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"success": true
|
||||
})),
|
||||
)
|
||||
.into_response())
|
||||
Ok(Json(SuccessResponse { success: true }))
|
||||
}
|
||||
Ok(false) => Ok(ApiError::DelegationNotFound.into_response()),
|
||||
Ok(false) => Err(ApiError::DelegationNotFound),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove controller: {:?}", e);
|
||||
Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response())
|
||||
Err(ApiError::InternalError(Some(
|
||||
"Failed to remove controller".into(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +215,7 @@ pub async fn update_controller_scopes(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateControllerScopesInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.delegation_repo
|
||||
.update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes)
|
||||
@@ -246,7 +229,7 @@ pub async fn update_controller_scopes(
|
||||
&auth.did,
|
||||
Some(&input.controller_did),
|
||||
DelegationActionType::ScopesModified,
|
||||
Some(serde_json::json!({
|
||||
Some(json!({
|
||||
"new_scopes": input.granted_scopes.as_str()
|
||||
})),
|
||||
None,
|
||||
@@ -254,21 +237,14 @@ pub async fn update_controller_scopes(
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"success": true
|
||||
})),
|
||||
)
|
||||
.into_response())
|
||||
Ok(Json(SuccessResponse { success: true }))
|
||||
}
|
||||
Ok(false) => Ok(ApiError::DelegationNotFound.into_response()),
|
||||
Ok(false) => Err(ApiError::DelegationNotFound),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to update controller scopes: {:?}", e);
|
||||
Ok(
|
||||
ApiError::InternalError(Some("Failed to update controller scopes".into()))
|
||||
.into_response(),
|
||||
)
|
||||
Err(ApiError::InternalError(Some(
|
||||
"Failed to update controller scopes".into(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,23 +252,17 @@ pub async fn update_controller_scopes(
|
||||
pub async fn list_controlled_accounts(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let accounts = match state
|
||||
) -> Result<Json<AccountsOutput<Vec<tranquil_db_traits::DelegatedAccountInfo>>>, ApiError> {
|
||||
let accounts = state
|
||||
.delegation_repo
|
||||
.get_accounts_controlled_by(&auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to list controlled accounts: {:?}", e);
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to list controlled accounts".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(Some("Failed to list controlled accounts".into()))
|
||||
})?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "accounts": accounts })).into_response())
|
||||
Ok(Json(AccountsOutput { accounts }))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -311,23 +281,18 @@ pub async fn get_audit_log(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Query(params): Query<AuditLogParams>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<AuditLogOutput<Vec<tranquil_db_traits::AuditLogEntry>>>, ApiError> {
|
||||
let limit = params.limit.clamp(1, 100);
|
||||
let offset = params.offset.max(0);
|
||||
|
||||
let entries = match state
|
||||
let entries = state
|
||||
.delegation_repo
|
||||
.get_audit_log_for_account(&auth.did, limit, offset)
|
||||
.await
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to get audit log: {:?}", e);
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to get audit log".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(Some("Failed to get audit log".into()))
|
||||
})?;
|
||||
|
||||
let total = state
|
||||
.delegation_repo
|
||||
@@ -335,11 +300,14 @@ pub async fn get_audit_log(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Json(serde_json::json!({ "entries": entries, "total": total })).into_response())
|
||||
Ok(Json(AuditLogOutput { entries, total }))
|
||||
}
|
||||
|
||||
pub async fn get_scope_presets() -> Response {
|
||||
Json(serde_json::json!({ "presets": SCOPE_PRESETS })).into_response()
|
||||
pub async fn get_scope_presets()
|
||||
-> Json<PresetsOutput<&'static [tranquil_pds::delegation::ScopePreset]>> {
|
||||
Json(PresetsOutput {
|
||||
presets: SCOPE_PRESETS,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -353,7 +321,7 @@ pub struct CreateDelegatedAccountInput {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateDelegatedAccountResponse {
|
||||
pub struct CreateDelegatedAccountOutput {
|
||||
pub did: Did,
|
||||
pub handle: Handle,
|
||||
}
|
||||
@@ -363,18 +331,11 @@ pub async fn create_delegated_account(
|
||||
_rate_limit: RateLimited<AccountCreationLimit>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<CreateDelegatedAccountInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let can_control = match verify_can_control_accounts(&state, &auth).await {
|
||||
Ok(proof) => proof,
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
) -> Result<Json<CreateDelegatedAccountOutput>, ApiError> {
|
||||
let can_control = verify_can_control_accounts(&state, &auth).await?;
|
||||
|
||||
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
|
||||
}
|
||||
};
|
||||
let handle = tranquil_pds::api::validation::resolve_handle_input(&input.handle)
|
||||
.map_err(|e| ApiError::InvalidRequest(e.to_string()))?;
|
||||
|
||||
let email = input
|
||||
.email
|
||||
@@ -384,18 +345,18 @@ pub async fn create_delegated_account(
|
||||
if let Some(ref email) = email
|
||||
&& !tranquil_pds::api::validation::is_valid_email(email)
|
||||
{
|
||||
return Ok(ApiError::InvalidEmail.into_response());
|
||||
return Err(ApiError::InvalidEmail);
|
||||
}
|
||||
|
||||
let validated_invite_code = if let Some(ref code) = input.invite_code {
|
||||
match state.infra_repo.validate_invite_code(code).await {
|
||||
Ok(validated) => Some(validated),
|
||||
Err(_) => return Ok(ApiError::InvalidInviteCode.into_response()),
|
||||
Err(_) => return Err(ApiError::InvalidInviteCode),
|
||||
}
|
||||
} else {
|
||||
let invite_required = tranquil_config::get().server.invite_code_required;
|
||||
if invite_required {
|
||||
return Ok(ApiError::InviteCodeRequired.into_response());
|
||||
return Err(ApiError::InviteCodeRequired);
|
||||
}
|
||||
None
|
||||
};
|
||||
@@ -409,6 +370,7 @@ pub async fn create_delegated_account(
|
||||
info!(did = %did, handle = %handle, controller = %can_control.did(), "Created DID for delegated account");
|
||||
|
||||
let repo = init_genesis_repo(&state, &did, &plc.signing_key, &plc.signing_key_bytes).await?;
|
||||
let repo_for_seq = repo.clone();
|
||||
|
||||
let create_input = tranquil_db_traits::CreateDelegatedAccountInput {
|
||||
handle: handle.clone(),
|
||||
@@ -431,14 +393,14 @@ pub async fn create_delegated_account(
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
|
||||
return Ok(ApiError::HandleNotAvailable(None).into_response());
|
||||
return Err(ApiError::HandleNotAvailable(None));
|
||||
}
|
||||
Err(tranquil_db_traits::CreateAccountError::EmailTaken) => {
|
||||
return Ok(ApiError::EmailTaken.into_response());
|
||||
return Err(ApiError::EmailTaken);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error creating delegated account: {:?}", e);
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -451,36 +413,14 @@ pub async fn create_delegated_account(
|
||||
warn!("Failed to record invite code use for {}: {:?}", did, e);
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).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,
|
||||
tranquil_db_traits::AccountStatus::Active,
|
||||
&handle,
|
||||
&repo_for_seq,
|
||||
handle.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to sequence account event for {}: {}", did, e);
|
||||
}
|
||||
|
||||
let profile_record = json!({
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle
|
||||
});
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to create default profile for {}: {}", did, e);
|
||||
}
|
||||
.await;
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
@@ -500,7 +440,7 @@ pub async fn create_delegated_account(
|
||||
|
||||
info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created");
|
||||
|
||||
Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response())
|
||||
Ok(Json(CreateDelegatedAccountOutput { did, handle }))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -511,7 +451,7 @@ pub struct ResolveControllerParams {
|
||||
pub async fn resolve_controller(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ResolveControllerParams>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<tranquil_pds::delegation::ResolvedIdentity>, ApiError> {
|
||||
let identifier = params.identifier.trim().trim_start_matches('@');
|
||||
|
||||
let did: Did = if identifier.starts_with("did:") {
|
||||
@@ -538,5 +478,5 @@ pub async fn resolve_controller(
|
||||
.await
|
||||
.ok_or(ApiError::ControllerNotFound)?;
|
||||
|
||||
Ok(Json(resolved).into_response())
|
||||
Ok(Json(resolved))
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tracing::info;
|
||||
use tranquil_db_traits::{CommsChannel, CommsStatus, CommsType};
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_types::Did;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotificationPrefsResponse {
|
||||
pub struct NotificationPrefsOutput {
|
||||
pub preferred_channel: CommsChannel,
|
||||
pub email: String,
|
||||
pub discord_username: Option<String>,
|
||||
@@ -28,14 +24,14 @@ pub struct NotificationPrefsResponse {
|
||||
pub async fn get_notification_prefs(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<NotificationPrefsOutput>, ApiError> {
|
||||
let prefs = state
|
||||
.user_repo
|
||||
.get_notification_prefs(&auth.did)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
|
||||
.log_db_err("get notification prefs")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
Ok(Json(NotificationPrefsResponse {
|
||||
Ok(Json(NotificationPrefsOutput {
|
||||
preferred_channel: prefs.preferred_channel,
|
||||
email: prefs.email,
|
||||
discord_username: prefs.discord_username,
|
||||
@@ -44,8 +40,7 @@ pub async fn get_notification_prefs(
|
||||
telegram_verified: prefs.telegram_verified,
|
||||
signal_username: prefs.signal_username,
|
||||
signal_verified: prefs.signal_verified,
|
||||
})
|
||||
.into_response())
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -61,26 +56,26 @@ pub struct NotificationHistoryEntry {
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetNotificationHistoryResponse {
|
||||
pub struct GetNotificationHistoryOutput {
|
||||
pub notifications: Vec<NotificationHistoryEntry>,
|
||||
}
|
||||
|
||||
pub async fn get_notification_history(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<GetNotificationHistoryOutput>, ApiError> {
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
|
||||
.log_db_err("get user id by did")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let rows = state
|
||||
.infra_repo
|
||||
.get_notification_history(user_id, 50)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
.log_db_err("get notification history")?;
|
||||
|
||||
let sensitive_types = [
|
||||
CommsType::EmailVerification,
|
||||
@@ -112,7 +107,7 @@ pub async fn get_notification_history(
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(GetNotificationHistoryResponse { notifications }).into_response())
|
||||
Ok(Json(GetNotificationHistoryOutput { notifications }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -127,7 +122,7 @@ pub struct UpdateNotificationPrefsInput {
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateNotificationPrefsResponse {
|
||||
pub struct UpdateNotificationPrefsOutput {
|
||||
pub success: bool,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub verification_required: Vec<CommsChannel>,
|
||||
@@ -159,12 +154,7 @@ pub async fn request_channel_verification(
|
||||
hostname,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::InternalError(Some(format!(
|
||||
"Failed to enqueue email notification: {}",
|
||||
e
|
||||
)))
|
||||
})?;
|
||||
.log_db_err("enqueue email verification")?;
|
||||
}
|
||||
_ => {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
@@ -216,25 +206,117 @@ pub async fn request_channel_verification(
|
||||
Some(json!({"code": formatted_token})),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::InternalError(Some(format!("Failed to enqueue notification: {}", e)))
|
||||
})?;
|
||||
.log_db_err("enqueue channel verification")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
async fn process_messaging_channel_update(
|
||||
state: &AppState,
|
||||
user_id: uuid::Uuid,
|
||||
did: &Did,
|
||||
channel: CommsChannel,
|
||||
raw_value: &str,
|
||||
effective_channel: CommsChannel,
|
||||
verification_required: &mut Vec<CommsChannel>,
|
||||
) -> Result<(), ApiError> {
|
||||
let clean = match channel {
|
||||
CommsChannel::Discord => raw_value.trim().to_lowercase(),
|
||||
CommsChannel::Telegram => raw_value.trim_start_matches('@').to_string(),
|
||||
CommsChannel::Signal => raw_value.trim().trim_start_matches('@').to_lowercase(),
|
||||
CommsChannel::Email => raw_value.trim().to_lowercase(),
|
||||
};
|
||||
|
||||
if clean.is_empty() {
|
||||
if effective_channel == channel {
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"Cannot remove {:?} while it is the preferred notification channel",
|
||||
channel
|
||||
)));
|
||||
}
|
||||
match channel {
|
||||
CommsChannel::Discord => state
|
||||
.user_repo
|
||||
.clear_discord(user_id)
|
||||
.await
|
||||
.log_db_err("clear discord")?,
|
||||
CommsChannel::Telegram => state
|
||||
.user_repo
|
||||
.clear_telegram(user_id)
|
||||
.await
|
||||
.log_db_err("clear telegram")?,
|
||||
CommsChannel::Signal => state
|
||||
.user_repo
|
||||
.clear_signal(user_id)
|
||||
.await
|
||||
.log_db_err("clear signal")?,
|
||||
CommsChannel::Email => {}
|
||||
};
|
||||
info!(did = %did, channel = ?channel, "Cleared channel");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let valid = match channel {
|
||||
CommsChannel::Discord => tranquil_pds::api::validation::is_valid_discord_username(&clean),
|
||||
CommsChannel::Telegram => tranquil_pds::api::validation::is_valid_telegram_username(&clean),
|
||||
CommsChannel::Signal => tranquil_pds::comms::is_valid_signal_username(&clean),
|
||||
CommsChannel::Email => tranquil_pds::api::validation::is_valid_email(&clean),
|
||||
};
|
||||
if !valid {
|
||||
return Err(match channel {
|
||||
CommsChannel::Discord => ApiError::InvalidRequest(
|
||||
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
|
||||
),
|
||||
CommsChannel::Telegram => ApiError::InvalidRequest(
|
||||
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
|
||||
),
|
||||
CommsChannel::Signal => ApiError::InvalidRequest(
|
||||
"Invalid Signal username. Must be 3-32 characters followed by .XX (e.g. username.01)".into(),
|
||||
),
|
||||
CommsChannel::Email => ApiError::InvalidEmail,
|
||||
});
|
||||
}
|
||||
|
||||
match channel {
|
||||
CommsChannel::Discord => state
|
||||
.user_repo
|
||||
.set_unverified_discord(user_id, &clean)
|
||||
.await
|
||||
.log_db_err("set unverified discord")?,
|
||||
CommsChannel::Telegram => state
|
||||
.user_repo
|
||||
.set_unverified_telegram(user_id, &clean)
|
||||
.await
|
||||
.log_db_err("set unverified telegram")?,
|
||||
CommsChannel::Signal => state
|
||||
.user_repo
|
||||
.set_unverified_signal(user_id, &clean)
|
||||
.await
|
||||
.log_db_err("set unverified signal")?,
|
||||
CommsChannel::Email => {}
|
||||
};
|
||||
|
||||
if matches!(channel, CommsChannel::Signal) {
|
||||
request_channel_verification(state, user_id, did, channel, &clean, None).await?;
|
||||
}
|
||||
|
||||
verification_required.push(channel);
|
||||
info!(did = %did, channel = ?channel, value = %clean, "Stored unverified channel username");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_notification_prefs(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateNotificationPrefsInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
) -> Result<Json<UpdateNotificationPrefsOutput>, ApiError> {
|
||||
let user_row = state
|
||||
.user_repo
|
||||
.get_id_handle_email_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
|
||||
.log_db_err("get user by did")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let user_id = user_row.id;
|
||||
@@ -245,7 +327,7 @@ pub async fn update_notification_prefs(
|
||||
.user_repo
|
||||
.get_notification_prefs(&auth.did)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
|
||||
.log_db_err("get notification prefs for update")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let effective_channel = input
|
||||
@@ -268,7 +350,7 @@ pub async fn update_notification_prefs(
|
||||
.user_repo
|
||||
.update_preferred_comms_channel(&auth.did, effective_channel)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
.log_db_err("update preferred channel")?;
|
||||
info!(did = %auth.did, channel = ?effective_channel, "Updated preferred notification channel");
|
||||
}
|
||||
|
||||
@@ -298,107 +380,46 @@ pub async fn update_notification_prefs(
|
||||
}
|
||||
|
||||
if let Some(ref discord_username) = input.discord_username {
|
||||
let discord_clean = discord_username.trim().to_lowercase();
|
||||
if discord_clean.is_empty() {
|
||||
if effective_channel == CommsChannel::Discord {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Cannot remove Discord while it is the preferred notification channel".into(),
|
||||
));
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.clear_discord(user_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Discord");
|
||||
} else if !tranquil_pds::api::validation::is_valid_discord_username(&discord_clean) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)"
|
||||
.into(),
|
||||
));
|
||||
} else {
|
||||
state
|
||||
.user_repo
|
||||
.set_unverified_discord(user_id, &discord_clean)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
verification_required.push(CommsChannel::Discord);
|
||||
info!(did = %auth.did, discord_username = %discord_clean, "Stored unverified Discord username");
|
||||
}
|
||||
process_messaging_channel_update(
|
||||
&state,
|
||||
user_id,
|
||||
&auth.did,
|
||||
CommsChannel::Discord,
|
||||
discord_username,
|
||||
effective_channel,
|
||||
&mut verification_required,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(ref telegram) = input.telegram_username {
|
||||
let telegram_clean = telegram.trim_start_matches('@');
|
||||
if telegram_clean.is_empty() {
|
||||
if effective_channel == CommsChannel::Telegram {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Cannot remove Telegram while it is the preferred notification channel".into(),
|
||||
));
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.clear_telegram(user_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Telegram username");
|
||||
} else if !tranquil_pds::api::validation::is_valid_telegram_username(telegram_clean) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore"
|
||||
.into(),
|
||||
));
|
||||
} else {
|
||||
state
|
||||
.user_repo
|
||||
.set_unverified_telegram(user_id, telegram_clean)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
verification_required.push(CommsChannel::Telegram);
|
||||
info!(did = %auth.did, telegram_username = %telegram_clean, "Stored unverified Telegram username");
|
||||
}
|
||||
process_messaging_channel_update(
|
||||
&state,
|
||||
user_id,
|
||||
&auth.did,
|
||||
CommsChannel::Telegram,
|
||||
telegram,
|
||||
effective_channel,
|
||||
&mut verification_required,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(ref signal) = input.signal_username {
|
||||
let signal_clean = signal.trim().trim_start_matches('@').to_lowercase();
|
||||
if signal_clean.is_empty() {
|
||||
if effective_channel == CommsChannel::Signal {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Cannot remove Signal while it is the preferred notification channel".into(),
|
||||
));
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.clear_signal(user_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Signal username");
|
||||
} else if !tranquil_pds::comms::is_valid_signal_username(&signal_clean) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid Signal username. Must be 3-32 characters followed by .XX (e.g. username.01)"
|
||||
.into(),
|
||||
));
|
||||
} else {
|
||||
state
|
||||
.user_repo
|
||||
.set_unverified_signal(user_id, &signal_clean)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
request_channel_verification(
|
||||
&state,
|
||||
user_id,
|
||||
&auth.did,
|
||||
CommsChannel::Signal,
|
||||
&signal_clean,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
verification_required.push(CommsChannel::Signal);
|
||||
info!(did = %auth.did, signal_username = %signal_clean, "Stored unverified Signal username");
|
||||
}
|
||||
process_messaging_channel_update(
|
||||
&state,
|
||||
user_id,
|
||||
&auth.did,
|
||||
CommsChannel::Signal,
|
||||
signal,
|
||||
effective_channel,
|
||||
&mut verification_required,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Json(UpdateNotificationPrefsResponse {
|
||||
Ok(Json(UpdateNotificationPrefsOutput {
|
||||
success: true,
|
||||
verification_required,
|
||||
})
|
||||
.into_response())
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user