this repo has no description
1use axum::{Json, extract::State, http::StatusCode};
2use serde::{Deserialize, Serialize};
3use serde_json::json;
4use tracing::{error, info, warn};
5
6use crate::auth::verification_token::{
7 VerificationPurpose, VerifyError, normalize_token_input, verify_token_signature,
8};
9use crate::state::AppState;
10
11#[derive(Deserialize, Clone)]
12#[serde(rename_all = "camelCase")]
13pub struct VerifyTokenInput {
14 pub token: String,
15 pub identifier: String,
16}
17
18#[derive(Serialize, Clone)]
19#[serde(rename_all = "camelCase")]
20pub struct VerifyTokenOutput {
21 pub success: bool,
22 pub did: String,
23 pub purpose: String,
24 pub channel: String,
25}
26
27pub async fn verify_token(
28 State(state): State<AppState>,
29 Json(input): Json<VerifyTokenInput>,
30) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> {
31 verify_token_internal(&state, input).await
32}
33
34pub async fn verify_token_internal(
35 state: &AppState,
36 input: VerifyTokenInput,
37) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> {
38 let normalized_token = normalize_token_input(&input.token);
39 let identifier = input.identifier.trim().to_lowercase();
40
41 let token_data = match verify_token_signature(&normalized_token) {
42 Ok(data) => data,
43 Err(e) => {
44 let (status, error, message) = match e {
45 VerifyError::InvalidFormat => (
46 StatusCode::BAD_REQUEST,
47 "InvalidToken",
48 "The verification token is invalid or malformed",
49 ),
50 VerifyError::UnsupportedVersion => (
51 StatusCode::BAD_REQUEST,
52 "InvalidToken",
53 "This verification token version is not supported",
54 ),
55 VerifyError::Expired => (
56 StatusCode::BAD_REQUEST,
57 "ExpiredToken",
58 "The verification token has expired. Please request a new one.",
59 ),
60 VerifyError::InvalidSignature => (
61 StatusCode::BAD_REQUEST,
62 "InvalidToken",
63 "The verification token signature is invalid",
64 ),
65 _ => (
66 StatusCode::BAD_REQUEST,
67 "InvalidToken",
68 "The verification token is not valid",
69 ),
70 };
71 warn!(error = ?e, "Token verification failed");
72 return Err((status, Json(json!({ "error": error, "message": message }))));
73 }
74 };
75
76 let expected_hash = crate::auth::verification_token::hash_identifier(&identifier);
77 if token_data.identifier_hash != expected_hash {
78 return Err((
79 StatusCode::BAD_REQUEST,
80 Json(
81 json!({ "error": "IdentifierMismatch", "message": "The identifier does not match the verification token" }),
82 ),
83 ));
84 }
85
86 match token_data.purpose {
87 VerificationPurpose::Migration => {
88 handle_migration_verification(state, &token_data.did, &token_data.channel, &identifier)
89 .await
90 }
91 VerificationPurpose::ChannelUpdate => {
92 handle_channel_update(state, &token_data.did, &token_data.channel, &identifier).await
93 }
94 VerificationPurpose::Signup => {
95 handle_signup_verification(state, &token_data.did, &token_data.channel, &identifier)
96 .await
97 }
98 }
99}
100
101async fn handle_migration_verification(
102 state: &AppState,
103 did: &str,
104 channel: &str,
105 identifier: &str,
106) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> {
107 if channel != "email" {
108 return Err((
109 StatusCode::BAD_REQUEST,
110 Json(
111 json!({ "error": "InvalidChannel", "message": "Migration verification is only supported for email" }),
112 ),
113 ));
114 }
115
116 let user = sqlx::query!(
117 "SELECT id, email, email_verified FROM users WHERE did = $1",
118 did
119 )
120 .fetch_optional(&state.db)
121 .await
122 .map_err(|e| {
123 warn!(error = %e, "Database error during migration verification");
124 (
125 StatusCode::INTERNAL_SERVER_ERROR,
126 Json(json!({ "error": "InternalError", "message": "Database error" })),
127 )
128 })?;
129
130 let user = user.ok_or_else(|| {
131 (
132 StatusCode::NOT_FOUND,
133 Json(json!({ "error": "AccountNotFound", "message": "No account found for this verification token" })),
134 )
135 })?;
136
137 if user.email.as_ref().map(|e| e.to_lowercase()) != Some(identifier.to_string()) {
138 return Err((
139 StatusCode::BAD_REQUEST,
140 Json(
141 json!({ "error": "IdentifierMismatch", "message": "The email address does not match the account" }),
142 ),
143 ));
144 }
145
146 if !user.email_verified {
147 sqlx::query!(
148 "UPDATE users SET email_verified = true WHERE id = $1",
149 user.id
150 )
151 .execute(&state.db)
152 .await
153 .map_err(|e| {
154 warn!(error = %e, "Failed to update email_verified status");
155 (
156 StatusCode::INTERNAL_SERVER_ERROR,
157 Json(json!({ "error": "InternalError", "message": "Failed to verify email" })),
158 )
159 })?;
160 }
161
162 info!(did = %did, "Migration email verified successfully");
163
164 Ok(Json(VerifyTokenOutput {
165 success: true,
166 did: did.to_string(),
167 purpose: "migration".to_string(),
168 channel: channel.to_string(),
169 }))
170}
171
172async fn handle_channel_update(
173 state: &AppState,
174 did: &str,
175 channel: &str,
176 identifier: &str,
177) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> {
178 let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
179 .fetch_one(&state.db)
180 .await
181 .map_err(|_| {
182 (
183 StatusCode::INTERNAL_SERVER_ERROR,
184 Json(json!({ "error": "InternalError", "message": "User not found" })),
185 )
186 })?;
187
188 let update_result = match channel {
189 "email" => sqlx::query!(
190 "UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2",
191 identifier,
192 user_id
193 ).execute(&state.db).await,
194 "discord" => sqlx::query!(
195 "UPDATE users SET discord_id = $1, discord_verified = TRUE, updated_at = NOW() WHERE id = $2",
196 identifier,
197 user_id
198 ).execute(&state.db).await,
199 "telegram" => sqlx::query!(
200 "UPDATE users SET telegram_username = $1, telegram_verified = TRUE, updated_at = NOW() WHERE id = $2",
201 identifier,
202 user_id
203 ).execute(&state.db).await,
204 "signal" => sqlx::query!(
205 "UPDATE users SET signal_number = $1, signal_verified = TRUE, updated_at = NOW() WHERE id = $2",
206 identifier,
207 user_id
208 ).execute(&state.db).await,
209 _ => {
210 return Err((
211 StatusCode::BAD_REQUEST,
212 Json(json!({ "error": "InvalidChannel", "message": "Invalid channel" })),
213 ));
214 }
215 };
216
217 if let Err(e) = update_result {
218 error!("Failed to update user channel: {:?}", e);
219 if channel == "email"
220 && e.as_database_error()
221 .map(|db| db.is_unique_violation())
222 .unwrap_or(false)
223 {
224 return Err((
225 StatusCode::BAD_REQUEST,
226 Json(json!({ "error": "EmailTaken", "message": "Email already in use" })),
227 ));
228 }
229 return Err((
230 StatusCode::INTERNAL_SERVER_ERROR,
231 Json(json!({ "error": "InternalError", "message": "Failed to update channel" })),
232 ));
233 }
234
235 info!(did = %did, channel = %channel, "Channel verified successfully");
236
237 Ok(Json(VerifyTokenOutput {
238 success: true,
239 did: did.to_string(),
240 purpose: "channel_update".to_string(),
241 channel: channel.to_string(),
242 }))
243}
244
245async fn handle_signup_verification(
246 state: &AppState,
247 did: &str,
248 channel: &str,
249 _identifier: &str,
250) -> Result<Json<VerifyTokenOutput>, (StatusCode, Json<serde_json::Value>)> {
251 let user = sqlx::query!(
252 "SELECT id, handle, email, email_verified, discord_verified, telegram_verified, signal_verified FROM users WHERE did = $1",
253 did
254 )
255 .fetch_optional(&state.db)
256 .await
257 .map_err(|e| {
258 warn!(error = %e, "Database error during signup verification");
259 (
260 StatusCode::INTERNAL_SERVER_ERROR,
261 Json(json!({ "error": "InternalError", "message": "Database error" })),
262 )
263 })?;
264
265 let user = user.ok_or_else(|| {
266 (
267 StatusCode::NOT_FOUND,
268 Json(json!({ "error": "AccountNotFound", "message": "No account found for this verification token" })),
269 )
270 })?;
271
272 let is_verified = user.email_verified
273 || user.discord_verified
274 || user.telegram_verified
275 || user.signal_verified;
276 if is_verified {
277 info!(did = %did, "Account already verified");
278 return Ok(Json(VerifyTokenOutput {
279 success: true,
280 did: did.to_string(),
281 purpose: "signup".to_string(),
282 channel: channel.to_string(),
283 }));
284 }
285
286 let update_result = match channel {
287 "email" => {
288 sqlx::query!(
289 "UPDATE users SET email_verified = TRUE WHERE id = $1",
290 user.id
291 )
292 .execute(&state.db)
293 .await
294 }
295 "discord" => {
296 sqlx::query!(
297 "UPDATE users SET discord_verified = TRUE WHERE id = $1",
298 user.id
299 )
300 .execute(&state.db)
301 .await
302 }
303 "telegram" => {
304 sqlx::query!(
305 "UPDATE users SET telegram_verified = TRUE WHERE id = $1",
306 user.id
307 )
308 .execute(&state.db)
309 .await
310 }
311 "signal" => {
312 sqlx::query!(
313 "UPDATE users SET signal_verified = TRUE WHERE id = $1",
314 user.id
315 )
316 .execute(&state.db)
317 .await
318 }
319 _ => {
320 return Err((
321 StatusCode::BAD_REQUEST,
322 Json(json!({ "error": "InvalidChannel", "message": "Invalid channel" })),
323 ));
324 }
325 };
326
327 update_result.map_err(|e| {
328 warn!(error = %e, "Failed to update channel verified status");
329 (
330 StatusCode::INTERNAL_SERVER_ERROR,
331 Json(json!({ "error": "InternalError", "message": "Failed to verify channel" })),
332 )
333 })?;
334
335 info!(did = %did, channel = %channel, "Signup verified successfully");
336
337 Ok(Json(VerifyTokenOutput {
338 success: true,
339 did: did.to_string(),
340 purpose: "signup".to_string(),
341 channel: channel.to_string(),
342 }))
343}