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