this repo has no description
1use crate::api::error::ApiError;
2use crate::types::Did;
3use axum::{Json, extract::State};
4use serde::{Deserialize, Serialize};
5use tracing::{error, info, warn};
6
7use crate::auth::verification_token::{
8 VerificationPurpose, normalize_token_input, verify_token_signature,
9};
10use crate::state::AppState;
11
12#[derive(Deserialize, Clone)]
13#[serde(rename_all = "camelCase")]
14pub struct VerifyTokenInput {
15 pub token: String,
16 pub identifier: String,
17}
18
19#[derive(Serialize, Clone)]
20#[serde(rename_all = "camelCase")]
21pub struct VerifyTokenOutput {
22 pub success: bool,
23 pub did: Did,
24 pub purpose: String,
25 pub channel: String,
26}
27
28pub async fn verify_token(
29 State(state): State<AppState>,
30 Json(input): Json<VerifyTokenInput>,
31) -> Result<Json<VerifyTokenOutput>, ApiError> {
32 verify_token_internal(&state, input).await
33}
34
35pub async fn verify_token_internal(
36 state: &AppState,
37 input: VerifyTokenInput,
38) -> Result<Json<VerifyTokenOutput>, ApiError> {
39 let normalized_token = normalize_token_input(&input.token);
40 let identifier = input.identifier.trim().to_lowercase();
41
42 let token_data = verify_token_signature(&normalized_token).map_err(|e| {
43 warn!(error = ?e, "Token verification failed");
44 ApiError::from(e)
45 })?;
46
47 let expected_hash = crate::auth::verification_token::hash_identifier(&identifier);
48 if token_data.identifier_hash != expected_hash {
49 return Err(ApiError::IdentifierMismatch);
50 }
51
52 match token_data.purpose {
53 VerificationPurpose::Migration => {
54 handle_migration_verification(state, &token_data.did, &token_data.channel, &identifier)
55 .await
56 }
57 VerificationPurpose::ChannelUpdate => {
58 handle_channel_update(state, &token_data.did, &token_data.channel, &identifier).await
59 }
60 VerificationPurpose::Signup => {
61 handle_signup_verification(state, &token_data.did, &token_data.channel, &identifier)
62 .await
63 }
64 }
65}
66
67async fn handle_migration_verification(
68 state: &AppState,
69 did: &str,
70 channel: &str,
71 identifier: &str,
72) -> Result<Json<VerifyTokenOutput>, ApiError> {
73 if channel != "email" {
74 return Err(ApiError::InvalidChannel);
75 }
76
77 let user = sqlx::query!(
78 "SELECT id, email, email_verified FROM users WHERE did = $1",
79 did
80 )
81 .fetch_optional(&state.db)
82 .await
83 .map_err(|e| {
84 warn!(error = %e, "Database error during migration verification");
85 ApiError::InternalError(None)
86 })?;
87
88 let user = user.ok_or(ApiError::AccountNotFound)?;
89
90 if user.email.as_ref().map(|e| e.to_lowercase()) != Some(identifier.to_string()) {
91 return Err(ApiError::IdentifierMismatch);
92 }
93
94 if !user.email_verified {
95 sqlx::query!(
96 "UPDATE users SET email_verified = true WHERE id = $1",
97 user.id
98 )
99 .execute(&state.db)
100 .await
101 .map_err(|e| {
102 warn!(error = %e, "Failed to update email_verified status");
103 ApiError::InternalError(None)
104 })?;
105 }
106
107 info!(did = %did, "Migration email verified successfully");
108
109 Ok(Json(VerifyTokenOutput {
110 success: true,
111 did: did.to_string().into(),
112 purpose: "migration".to_string(),
113 channel: channel.to_string(),
114 }))
115}
116
117async fn handle_channel_update(
118 state: &AppState,
119 did: &str,
120 channel: &str,
121 identifier: &str,
122) -> Result<Json<VerifyTokenOutput>, ApiError> {
123 let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
124 .fetch_one(&state.db)
125 .await
126 .map_err(|_| ApiError::InternalError(None))?;
127
128 let update_result = match channel {
129 "email" => sqlx::query!(
130 "UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2",
131 identifier,
132 user_id
133 ).execute(&state.db).await,
134 "discord" => sqlx::query!(
135 "UPDATE users SET discord_id = $1, discord_verified = TRUE, updated_at = NOW() WHERE id = $2",
136 identifier,
137 user_id
138 ).execute(&state.db).await,
139 "telegram" => sqlx::query!(
140 "UPDATE users SET telegram_username = $1, telegram_verified = TRUE, updated_at = NOW() WHERE id = $2",
141 identifier,
142 user_id
143 ).execute(&state.db).await,
144 "signal" => sqlx::query!(
145 "UPDATE users SET signal_number = $1, signal_verified = TRUE, updated_at = NOW() WHERE id = $2",
146 identifier,
147 user_id
148 ).execute(&state.db).await,
149 _ => {
150 return Err(ApiError::InvalidChannel);
151 }
152 };
153
154 if let Err(e) = update_result {
155 error!("Failed to update user channel: {:?}", e);
156 if channel == "email"
157 && e.as_database_error()
158 .map(|db| db.is_unique_violation())
159 .unwrap_or(false)
160 {
161 return Err(ApiError::EmailTaken);
162 }
163 return Err(ApiError::InternalError(None));
164 }
165
166 info!(did = %did, channel = %channel, "Channel verified successfully");
167
168 Ok(Json(VerifyTokenOutput {
169 success: true,
170 did: did.to_string().into(),
171 purpose: "channel_update".to_string(),
172 channel: channel.to_string(),
173 }))
174}
175
176async fn handle_signup_verification(
177 state: &AppState,
178 did: &str,
179 channel: &str,
180 _identifier: &str,
181) -> Result<Json<VerifyTokenOutput>, ApiError> {
182 let user = sqlx::query!(
183 "SELECT id, handle, email, email_verified, discord_verified, telegram_verified, signal_verified FROM users WHERE did = $1",
184 did
185 )
186 .fetch_optional(&state.db)
187 .await
188 .map_err(|e| {
189 warn!(error = %e, "Database error during signup verification");
190 ApiError::InternalError(None)
191 })?;
192
193 let user = user.ok_or(ApiError::AccountNotFound)?;
194
195 let is_verified = user.email_verified
196 || user.discord_verified
197 || user.telegram_verified
198 || user.signal_verified;
199 if is_verified {
200 info!(did = %did, "Account already verified");
201 return Ok(Json(VerifyTokenOutput {
202 success: true,
203 did: did.to_string().into(),
204 purpose: "signup".to_string(),
205 channel: channel.to_string(),
206 }));
207 }
208
209 let update_result = match channel {
210 "email" => {
211 sqlx::query!(
212 "UPDATE users SET email_verified = TRUE WHERE id = $1",
213 user.id
214 )
215 .execute(&state.db)
216 .await
217 }
218 "discord" => {
219 sqlx::query!(
220 "UPDATE users SET discord_verified = TRUE WHERE id = $1",
221 user.id
222 )
223 .execute(&state.db)
224 .await
225 }
226 "telegram" => {
227 sqlx::query!(
228 "UPDATE users SET telegram_verified = TRUE WHERE id = $1",
229 user.id
230 )
231 .execute(&state.db)
232 .await
233 }
234 "signal" => {
235 sqlx::query!(
236 "UPDATE users SET signal_verified = TRUE WHERE id = $1",
237 user.id
238 )
239 .execute(&state.db)
240 .await
241 }
242 _ => {
243 return Err(ApiError::InvalidChannel);
244 }
245 };
246
247 update_result.map_err(|e| {
248 warn!(error = %e, "Failed to update channel verified status");
249 ApiError::InternalError(None)
250 })?;
251
252 info!(did = %did, channel = %channel, "Signup verified successfully");
253
254 Ok(Json(VerifyTokenOutput {
255 success: true,
256 did: did.to_string().into(),
257 purpose: "signup".to_string(),
258 channel: channel.to_string(),
259 }))
260}