use crate::common; use axum::{ Json, extract::State, response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use tracing::{info, warn}; use tranquil_pds::api::SuccessResponse; use tranquil_pds::api::error::{ApiError, DbResultExt}; use tranquil_pds::comms::comms_repo; use tranquil_pds::types::Did; use tranquil_db_traits::CommsChannel; use tranquil_pds::auth::verification_token::{ VerificationPurpose, normalize_token_input, verify_token_signature, }; use tranquil_pds::state::AppState; #[derive(Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct VerifyTokenInput { pub token: String, pub identifier: String, } #[derive(Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct VerifyTokenOutput { pub success: bool, pub did: Did, pub purpose: VerificationPurpose, pub channel: CommsChannel, } pub async fn verify_token( State(state): State, Json(input): Json, ) -> Result, ApiError> { verify_token_internal(&state, input).await } pub async fn verify_token_internal( state: &AppState, input: VerifyTokenInput, ) -> Result, ApiError> { let normalized_token = normalize_token_input(&input.token); let identifier = input.identifier.trim().to_lowercase(); let token_data = verify_token_signature(&normalized_token).map_err(|e| { warn!(error = ?e, "Token verification failed"); ApiError::from(e) })?; let expected_hash = tranquil_pds::auth::verification_token::hash_identifier(&identifier); if token_data.identifier_hash != expected_hash { return Err(ApiError::IdentifierMismatch); } match token_data.purpose { VerificationPurpose::Migration => { handle_migration_verification(state, &token_data.did, token_data.channel, &identifier) .await } VerificationPurpose::ChannelUpdate => { handle_channel_update(state, &token_data.did, token_data.channel, &identifier).await } VerificationPurpose::Signup => { handle_signup_verification(state, &token_data.did, token_data.channel, &identifier) .await } } } async fn handle_migration_verification( state: &AppState, did: &Did, channel: CommsChannel, identifier: &str, ) -> Result, ApiError> { let user = state .repos .user .get_verification_info(did) .await .log_db_err("during migration verification")? .ok_or(ApiError::AccountNotFound)?; match channel { CommsChannel::Email => { if user.email.as_ref().map(|e| e.to_lowercase()) != Some(identifier.to_string()) { return Err(ApiError::IdentifierMismatch); } if !user.channel_verification.email { state .repos .user .set_email_verified_flag(user.id) .await .log_db_err("updating email_verified status")?; } } _ => common::set_channel_verified_flag(state.repos.user.as_ref(), user.id, channel).await?, }; info!(did = %did, channel = ?channel, "Migration verification completed successfully"); Ok(Json(VerifyTokenOutput { success: true, did: did.clone(), purpose: VerificationPurpose::Migration, channel, })) } async fn handle_channel_update( state: &AppState, did: &Did, channel: CommsChannel, identifier: &str, ) -> Result, ApiError> { let user_id = state .repos .user .get_id_by_did(did) .await .log_db_err("fetching user id")? .ok_or(ApiError::AccountNotFound)?; match channel { CommsChannel::Email => { let success = state .repos .user .verify_email_channel(user_id, identifier) .await .log_db_err("updating email channel")?; if !success { return Err(ApiError::EmailTaken); } } CommsChannel::Discord => { state .repos .user .verify_discord_channel(user_id, identifier) .await .log_db_err("updating discord channel")?; } CommsChannel::Telegram => { state .repos .user .verify_telegram_channel(user_id, identifier) .await .log_db_err("updating telegram channel")?; } CommsChannel::Signal => { state .repos .user .verify_signal_channel(user_id, identifier) .await .log_db_err("updating signal channel")?; } }; info!(did = %did, channel = ?channel, "Channel verified successfully"); notify_channel_verified(state, user_id, channel, identifier).await; Ok(Json(VerifyTokenOutput { success: true, did: did.clone(), purpose: VerificationPurpose::ChannelUpdate, channel, })) } async fn notify_channel_verified( state: &AppState, user_id: uuid::Uuid, channel: CommsChannel, identifier: &str, ) { let recipient = match channel { CommsChannel::Telegram => state .repos .user .get_telegram_chat_id(user_id) .await .ok() .flatten() .map(|id| id.to_string()) .unwrap_or_else(|| identifier.to_string()), _ => identifier.to_string(), }; if let Err(e) = comms_repo::enqueue_channel_verified( state.repos.user.as_ref(), state.repos.infra.as_ref(), user_id, channel, &recipient, &tranquil_config::get().server.hostname, ) .await { warn!(error = %e, "Failed to enqueue channel verified notification"); } } async fn handle_signup_verification( state: &AppState, did: &Did, channel: CommsChannel, identifier: &str, ) -> Result, ApiError> { let user = state .repos .user .get_verification_info(did) .await .log_db_err("during signup verification")? .ok_or(ApiError::AccountNotFound)?; let is_verified = user.channel_verification.has_any_verified(); if is_verified { info!(did = %did, "Account already verified"); return Ok(Json(VerifyTokenOutput { success: true, did: did.clone(), purpose: VerificationPurpose::Signup, channel, })); } common::set_channel_verified_flag(state.repos.user.as_ref(), user.id, channel).await?; info!(did = %did, channel = ?channel, "Signup verified successfully"); notify_channel_verified(state, user.id, channel, identifier).await; Ok(Json(VerifyTokenOutput { success: true, did: did.clone(), purpose: VerificationPurpose::Signup, channel, })) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConfirmChannelVerificationInput { pub channel: CommsChannel, pub identifier: String, pub code: String, } pub async fn confirm_channel_verification( State(state): State, Json(input): Json, ) -> Response { let token_input = VerifyTokenInput { token: input.code, identifier: input.identifier, }; match verify_token_internal(&state, token_input).await { Ok(_output) => SuccessResponse::ok().into_response(), Err(e) => e.into_response(), } }