+256
-289
Diff
round #0
+256
-289
crates/tranquil-api/src/server/session.rs
+256
-289
crates/tranquil-api/src/server/session.rs
···
10
10
use tracing::{error, info, warn};
11
11
use tranquil_db_traits::{SessionId, TokenFamilyId};
12
12
use tranquil_pds::api::error::{ApiError, DbResultExt};
13
-
use tranquil_pds::api::{EmptyResponse, SuccessResponse};
13
+
use tranquil_pds::api::{EmptyResponse, PreferredLocaleOutput, SuccessResponse};
14
14
use tranquil_pds::auth::{
15
15
Active, Auth, NormalizedLoginIdentifier, Permissive, require_legacy_session_mfa,
16
16
require_reauth_window,
···
20
20
use tranquil_pds::types::{AccountState, Did, Handle, PlainPassword};
21
21
use tranquil_types::TokenId;
22
22
23
-
fn full_handle(stored_handle: &str, _pds_hostname: &str) -> String {
24
-
stored_handle.to_string()
25
-
}
26
-
27
23
#[derive(Deserialize)]
28
24
#[serde(rename_all = "camelCase")]
29
25
pub struct CreateSessionInput {
···
59
55
State(state): State<AppState>,
60
56
rate_limit: RateLimited<LoginLimit>,
61
57
Json(input): Json<CreateSessionInput>,
62
-
) -> Response {
58
+
) -> Result<Response, ApiError> {
63
59
let client_ip = rate_limit.client_ip();
64
60
info!(
65
61
"create_session called with identifier: {}",
66
62
input.identifier
67
63
);
68
-
let pds_host = &tranquil_config::get().server.hostname;
69
64
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
70
65
let normalized_identifier =
71
66
NormalizedLoginIdentifier::normalize(&input.identifier, hostname_for_handles);
···
85
80
"$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK",
86
81
);
87
82
warn!("User not found for login attempt");
88
-
return ApiError::AuthenticationFailed(Some("Invalid identifier or password".into()))
89
-
.into_response();
83
+
return Err(ApiError::AuthenticationFailed(Some(
84
+
"Invalid identifier or password".into(),
85
+
)));
90
86
}
91
87
Err(e) => {
92
88
error!("Database error fetching user: {:?}", e);
93
-
return ApiError::InternalError(None).into_response();
89
+
return Err(ApiError::InternalError(None));
94
90
}
95
91
};
96
92
let key_bytes = match tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version)
···
98
94
Ok(k) => k,
99
95
Err(e) => {
100
96
error!("Failed to decrypt user key: {:?}", e);
101
-
return ApiError::InternalError(None).into_response();
97
+
return Err(ApiError::InternalError(None));
102
98
}
103
99
};
104
-
let (password_valid, app_password_name, app_password_scopes, app_password_controller) = if row
105
-
.password_hash
106
-
.as_ref()
107
-
.map(|h| verify(&input.password, h).unwrap_or(false))
108
-
.unwrap_or(false)
109
-
{
110
-
(true, None, None, None)
111
-
} else {
112
-
let app_passwords = state
113
-
.session_repo
114
-
.get_app_passwords_for_login(row.id)
115
-
.await
116
-
.unwrap_or_default();
117
-
let matched = app_passwords
118
-
.iter()
119
-
.find(|app| verify(&input.password, &app.password_hash).unwrap_or(false));
120
-
match matched {
121
-
Some(app) => (
122
-
true,
123
-
Some(app.name.clone()),
124
-
app.scopes.clone(),
125
-
app.created_by_controller_did.clone(),
126
-
),
127
-
None => (false, None, None, None),
100
+
let credential = crate::common::verify_credential(
101
+
state.session_repo.as_ref(),
102
+
row.id,
103
+
&input.password,
104
+
row.password_hash.as_deref(),
105
+
)
106
+
.await;
107
+
let (app_password_name, app_password_scopes, app_password_controller) = match credential {
108
+
Some(crate::common::CredentialMatch::MainPassword) => (None, None, None),
109
+
Some(crate::common::CredentialMatch::AppPassword {
110
+
name,
111
+
scopes,
112
+
controller_did,
113
+
}) => (Some(name), scopes, controller_did),
114
+
None => {
115
+
warn!("Password verification failed for login attempt");
116
+
return Err(ApiError::AuthenticationFailed(Some(
117
+
"Invalid identifier or password".into(),
118
+
)));
128
119
}
129
120
};
130
-
if !password_valid {
131
-
warn!("Password verification failed for login attempt");
132
-
return ApiError::AuthenticationFailed(Some("Invalid identifier or password".into()))
133
-
.into_response();
134
-
}
135
121
let account_state = AccountState::from_db_fields(
136
122
row.deactivated_at,
137
123
row.takedown_ref.clone(),
···
140
126
);
141
127
if account_state.is_takendown() && !input.allow_takendown {
142
128
warn!("Login attempt for takendown account: {}", row.did);
143
-
return ApiError::AccountTakedown.into_response();
129
+
return Err(ApiError::AccountTakedown);
144
130
}
145
131
let is_verified = row.channel_verification.has_any_verified();
146
132
let is_delegated = state
···
159
145
.as_ref()
160
146
.map(|r| r.channel.as_str())
161
147
.unwrap_or(row.preferred_comms_channel.as_str());
162
-
return (
148
+
return Ok((
163
149
StatusCode::FORBIDDEN,
164
150
Json(json!({
165
151
"error": "account_not_verified",
···
169
155
"channel": channel
170
156
})),
171
157
)
172
-
.into_response();
158
+
.into_response());
173
159
}
174
160
let has_totp = row.totp_enabled;
175
161
let email_2fa_enabled = row.email_2fa_enabled;
···
190
176
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::NotRequired) => {}
191
177
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::Blocked) => {
192
178
warn!("Legacy login blocked for TOTP-enabled account: {}", row.did);
193
-
return ApiError::LegacyLoginBlocked.into_response();
179
+
return Err(ApiError::LegacyLoginBlocked);
194
180
}
195
181
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => {
196
182
let hostname = &tranquil_config::get().server.hostname;
···
206
192
error!("Failed to send 2FA code: {:?}", e);
207
193
tranquil_pds::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &row.did)
208
194
.await;
209
-
return ApiError::InternalError(Some(
195
+
return Err(ApiError::InternalError(Some(
210
196
"Failed to send verification code. Please try again.".into(),
211
-
))
212
-
.into_response();
197
+
)));
213
198
}
214
-
return ApiError::AuthFactorTokenRequired.into_response();
199
+
return Err(ApiError::AuthFactorTokenRequired);
215
200
}
216
201
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::Verified) => {}
217
202
Err(tranquil_pds::auth::legacy_2fa::Legacy2faFlowError::Challenge(e)) => {
···
219
204
return match e {
220
205
ChallengeError::CacheUnavailable => {
221
206
error!("Cache unavailable for 2FA, blocking legacy login");
222
-
ApiError::ServiceUnavailable(Some(
207
+
Err(ApiError::ServiceUnavailable(Some(
223
208
"2FA service temporarily unavailable. Please try again later or use an OAuth client.".into(),
224
-
))
225
-
.into_response()
209
+
)))
226
210
}
227
-
ChallengeError::RateLimited => ApiError::RateLimitExceeded(Some(
211
+
ChallengeError::RateLimited => Err(ApiError::RateLimitExceeded(Some(
228
212
"Please wait before requesting a new verification code.".into(),
229
-
))
230
-
.into_response(),
213
+
))),
231
214
ChallengeError::CacheError => {
232
215
error!("Cache error during 2FA challenge creation");
233
-
ApiError::InternalError(None).into_response()
216
+
Err(ApiError::InternalError(None))
234
217
}
235
218
};
236
219
}
···
247
230
| ValidationError::InvalidCode
248
231
| ValidationError::CacheError => "Invalid verification code",
249
232
};
250
-
return ApiError::InvalidCode(Some(msg.into())).into_response();
233
+
return Err(ApiError::InvalidCode(Some(msg.into())));
251
234
}
252
235
}
253
236
let access_meta = match tranquil_pds::auth::create_access_token_with_delegation(
···
260
243
Ok(m) => m,
261
244
Err(e) => {
262
245
error!("Failed to create access token: {:?}", e);
263
-
return ApiError::InternalError(None).into_response();
246
+
return Err(ApiError::InternalError(None));
264
247
}
265
248
};
266
249
let refresh_meta =
···
268
251
Ok(m) => m,
269
252
Err(e) => {
270
253
error!("Failed to create refresh token: {:?}", e);
271
-
return ApiError::InternalError(None).into_response();
254
+
return Err(ApiError::InternalError(None));
272
255
}
273
256
};
274
257
let did_for_doc = row.did.clone();
···
291
274
);
292
275
if let Err(e) = insert_result {
293
276
error!("Failed to insert session: {:?}", e);
294
-
return ApiError::InternalError(None).into_response();
277
+
return Err(ApiError::InternalError(None));
295
278
}
296
279
if is_legacy_login {
297
280
warn!(
···
313
296
error!("Failed to queue legacy login notification: {:?}", e);
314
297
}
315
298
}
316
-
let handle = full_handle(&row.handle, pds_host);
299
+
let handle = row.handle.clone();
317
300
let is_active = account_state.is_active();
318
301
let status = account_state.status_for_session().map(String::from);
319
302
let email_auth_factor_out = if email_2fa_enabled || has_totp {
···
321
304
} else {
322
305
None
323
306
};
324
-
Json(CreateSessionOutput {
325
-
access_jwt: access_meta.token,
326
-
refresh_jwt: refresh_meta.token,
327
-
handle: handle.into(),
328
-
did: row.did,
329
-
did_doc,
330
-
email: row.email,
331
-
email_confirmed: Some(row.channel_verification.email),
332
-
email_auth_factor: email_auth_factor_out,
333
-
active: Some(is_active),
334
-
status,
335
-
})
336
-
.into_response()
307
+
Ok((
308
+
StatusCode::OK,
309
+
Json(CreateSessionOutput {
310
+
access_jwt: access_meta.token,
311
+
refresh_jwt: refresh_meta.token,
312
+
handle,
313
+
did: row.did,
314
+
did_doc,
315
+
email: row.email,
316
+
email_confirmed: Some(row.channel_verification.email),
317
+
email_auth_factor: email_auth_factor_out,
318
+
active: Some(is_active),
319
+
status,
320
+
}),
321
+
)
322
+
.into_response())
323
+
}
324
+
325
+
#[derive(Serialize)]
326
+
#[serde(rename_all = "camelCase")]
327
+
pub struct GetSessionOutput {
328
+
pub handle: Handle,
329
+
pub did: Did,
330
+
pub active: bool,
331
+
pub preferred_channel: String,
332
+
pub preferred_channel_verified: bool,
333
+
#[serde(skip_serializing_if = "Option::is_none")]
334
+
pub preferred_locale: Option<String>,
335
+
pub is_admin: bool,
336
+
#[serde(skip_serializing_if = "Option::is_none")]
337
+
pub email: Option<String>,
338
+
#[serde(skip_serializing_if = "Option::is_none")]
339
+
pub email_confirmed: Option<bool>,
340
+
#[serde(skip_serializing_if = "Option::is_none")]
341
+
pub email_auth_factor: Option<bool>,
342
+
#[serde(skip_serializing_if = "Option::is_none")]
343
+
pub status: Option<String>,
344
+
#[serde(skip_serializing_if = "Option::is_none")]
345
+
pub migrated_to_pds: Option<String>,
346
+
#[serde(skip_serializing_if = "Option::is_none")]
347
+
pub migrated_at: Option<chrono::DateTime<chrono::Utc>>,
348
+
#[serde(skip_serializing_if = "Option::is_none")]
349
+
pub did_doc: Option<serde_json::Value>,
337
350
}
338
351
339
352
pub async fn get_session(
340
353
State(state): State<AppState>,
341
354
auth: Auth<Permissive>,
342
-
) -> Result<Response, ApiError> {
355
+
) -> Result<Json<GetSessionOutput>, ApiError> {
343
356
let permissions = auth.permissions();
344
357
let can_read_email = permissions.allows_email_read();
345
358
···
354
367
let preferred_channel_verified = row
355
368
.channel_verification
356
369
.is_verified(row.preferred_comms_channel);
357
-
let pds_hostname = &tranquil_config::get().server.hostname;
358
-
let handle = full_handle(&row.handle, pds_hostname);
370
+
let handle = row.handle.clone();
359
371
let account_state = AccountState::from_db_fields(
360
372
row.deactivated_at,
361
373
row.takedown_ref.clone(),
362
374
row.migrated_to_pds.clone(),
363
375
row.migrated_at,
364
376
);
365
-
let email_value = if can_read_email {
366
-
row.email.clone()
367
-
} else {
368
-
None
377
+
let email = match can_read_email {
378
+
true => row.email.clone(),
379
+
false => None,
369
380
};
370
-
let email_confirmed_value = can_read_email && row.channel_verification.email;
371
-
let mut response = json!({
372
-
"handle": handle,
373
-
"did": &auth.did,
374
-
"active": account_state.is_active(),
375
-
"preferredChannel": row.preferred_comms_channel.as_str(),
376
-
"preferredChannelVerified": preferred_channel_verified,
377
-
"preferredLocale": row.preferred_locale,
378
-
"isAdmin": row.is_admin
379
-
});
380
-
if can_read_email {
381
-
response["email"] = json!(email_value);
382
-
response["emailConfirmed"] = json!(email_confirmed_value);
383
-
}
384
-
if row.email_2fa_enabled || row.totp_enabled {
385
-
response["emailAuthFactor"] = json!(true);
386
-
}
387
-
if let Some(status) = account_state.status_for_session() {
388
-
response["status"] = json!(status);
389
-
}
390
-
if let AccountState::Migrated { to_pds, at } = &account_state {
391
-
response["migratedToPds"] = json!(to_pds);
392
-
response["migratedAt"] = json!(at);
393
-
}
394
-
if let Some(doc) = did_doc {
395
-
response["didDoc"] = doc;
396
-
}
397
-
Ok(Json(response).into_response())
381
+
let email_confirmed = match can_read_email {
382
+
true => Some(row.channel_verification.email),
383
+
false => None,
384
+
};
385
+
let email_auth_factor = match row.email_2fa_enabled || row.totp_enabled {
386
+
true => Some(true),
387
+
false => None,
388
+
};
389
+
let (migrated_to_pds, migrated_at) = match &account_state {
390
+
AccountState::Migrated { to_pds, at } => (Some(to_pds.clone()), Some(*at)),
391
+
_ => (None, None),
392
+
};
393
+
Ok(Json(GetSessionOutput {
394
+
handle,
395
+
did: auth.did.clone(),
396
+
active: account_state.is_active(),
397
+
preferred_channel: row.preferred_comms_channel.as_str().to_string(),
398
+
preferred_channel_verified,
399
+
preferred_locale: row.preferred_locale,
400
+
is_admin: row.is_admin,
401
+
email,
402
+
email_confirmed,
403
+
email_auth_factor,
404
+
status: account_state.status_for_session().map(String::from),
405
+
migrated_to_pds,
406
+
migrated_at,
407
+
did_doc,
408
+
}))
398
409
}
399
410
Ok(None) => Err(ApiError::AuthenticationFailed(None)),
400
411
Err(e) => {
···
407
418
pub async fn delete_session(
408
419
State(state): State<AppState>,
409
420
headers: axum::http::HeaderMap,
410
-
_auth: Auth<Active>,
411
-
) -> Result<Response, ApiError> {
412
-
let extracted = tranquil_pds::auth::extract_auth_token_from_header(
413
-
tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION),
414
-
)
415
-
.ok_or(ApiError::AuthenticationRequired)?;
416
-
let jti = tranquil_pds::auth::get_jti_from_token(&extracted.token)
417
-
.map_err(|_| ApiError::AuthenticationFailed(None))?;
418
-
let did = tranquil_pds::auth::get_did_from_token(&extracted.token).ok();
421
+
auth: Auth<Active>,
422
+
) -> Result<Json<EmptyResponse>, ApiError> {
423
+
let jti = tranquil_pds::auth::extract_jti_from_headers(&headers)
424
+
.ok_or(ApiError::AuthenticationRequired)?;
419
425
match state.session_repo.delete_session_by_access_jti(&jti).await {
420
426
Ok(rows) if rows > 0 => {
421
-
if let Some(did) = did {
422
-
let session_cache_key = tranquil_pds::cache_keys::session_key(&did, &jti);
423
-
let _ = state.cache.delete(&session_cache_key).await;
424
-
}
425
-
Ok(EmptyResponse::ok().into_response())
427
+
let session_cache_key = tranquil_pds::cache_keys::session_key(&auth.did, &jti);
428
+
let _ = state.cache.delete(&session_cache_key).await;
429
+
Ok(Json(EmptyResponse {}))
426
430
}
427
431
Ok(_) => Err(ApiError::AuthenticationFailed(None)),
428
432
Err(_) => Err(ApiError::AuthenticationFailed(None)),
429
433
}
430
434
}
431
435
436
+
#[derive(Serialize)]
437
+
#[serde(rename_all = "camelCase")]
438
+
pub struct RefreshSessionOutput {
439
+
pub access_jwt: String,
440
+
pub refresh_jwt: String,
441
+
pub handle: Handle,
442
+
pub did: Did,
443
+
#[serde(skip_serializing_if = "Option::is_none")]
444
+
pub email: Option<String>,
445
+
pub email_confirmed: bool,
446
+
pub preferred_channel: String,
447
+
pub preferred_channel_verified: bool,
448
+
#[serde(skip_serializing_if = "Option::is_none")]
449
+
pub preferred_locale: Option<String>,
450
+
pub is_admin: bool,
451
+
pub active: bool,
452
+
#[serde(skip_serializing_if = "Option::is_none")]
453
+
pub did_doc: Option<serde_json::Value>,
454
+
#[serde(skip_serializing_if = "Option::is_none")]
455
+
pub status: Option<String>,
456
+
}
457
+
432
458
pub async fn refresh_session(
433
459
State(state): State<AppState>,
434
460
_rate_limit: RateLimited<RefreshSessionLimit>,
435
461
headers: axum::http::HeaderMap,
436
-
) -> Response {
462
+
) -> Result<Json<RefreshSessionOutput>, ApiError> {
437
463
let extracted = match tranquil_pds::auth::extract_auth_token_from_header(
438
464
tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION),
439
465
) {
440
466
Some(t) => t,
441
-
None => return ApiError::AuthenticationRequired.into_response(),
467
+
None => return Err(ApiError::AuthenticationRequired),
442
468
};
443
469
let refresh_token = extracted.token;
444
470
let refresh_jti = match tranquil_pds::auth::get_jti_from_token(&refresh_token) {
445
471
Ok(jti) => jti,
446
472
Err(_) => {
447
-
return ApiError::AuthenticationFailed(Some("Invalid token format".into()))
448
-
.into_response();
473
+
return Err(ApiError::AuthenticationFailed(Some(
474
+
"Invalid token format".into(),
475
+
)));
449
476
}
450
477
};
451
478
if let Ok(Some(_)) = state
···
454
481
.await
455
482
{
456
483
warn!("Refresh token reuse detected for jti: {}", refresh_jti);
457
-
return ApiError::AuthenticationFailed(Some(
484
+
return Err(ApiError::AuthenticationFailed(Some(
458
485
"Refresh token has been revoked due to suspected compromise".into(),
459
-
))
460
-
.into_response();
486
+
)));
461
487
}
462
488
let session_row = match state
463
489
.session_repo
···
466
492
{
467
493
Ok(Some(row)) => row,
468
494
Ok(None) => {
469
-
return ApiError::AuthenticationFailed(Some("Invalid refresh token".into()))
470
-
.into_response();
495
+
return Err(ApiError::AuthenticationFailed(Some(
496
+
"Invalid refresh token".into(),
497
+
)));
471
498
}
472
499
Err(e) => {
473
500
error!("Database error fetching session: {:?}", e);
474
-
return ApiError::InternalError(None).into_response();
501
+
return Err(ApiError::InternalError(None));
475
502
}
476
503
};
477
504
let key_bytes = match tranquil_pds::config::decrypt_key(
···
481
508
Ok(k) => k,
482
509
Err(e) => {
483
510
error!("Failed to decrypt user key: {:?}", e);
484
-
return ApiError::InternalError(None).into_response();
511
+
return Err(ApiError::InternalError(None));
485
512
}
486
513
};
487
514
if tranquil_pds::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() {
488
-
return ApiError::AuthenticationFailed(Some("Invalid refresh token".into()))
489
-
.into_response();
515
+
return Err(ApiError::AuthenticationFailed(Some(
516
+
"Invalid refresh token".into(),
517
+
)));
490
518
}
491
519
let new_access_meta = match tranquil_pds::auth::create_access_token_with_delegation(
492
520
&session_row.did,
···
498
526
Ok(m) => m,
499
527
Err(e) => {
500
528
error!("Failed to create access token: {:?}", e);
501
-
return ApiError::InternalError(None).into_response();
529
+
return Err(ApiError::InternalError(None));
502
530
}
503
531
};
504
532
let new_refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(
···
508
536
Ok(m) => m,
509
537
Err(e) => {
510
538
error!("Failed to create refresh token: {:?}", e);
511
-
return ApiError::InternalError(None).into_response();
539
+
return Err(ApiError::InternalError(None));
512
540
}
513
541
};
514
542
let refresh_data = tranquil_db_traits::SessionRefreshData {
···
527
555
Ok(tranquil_db_traits::RefreshSessionResult::Success) => {}
528
556
Ok(tranquil_db_traits::RefreshSessionResult::TokenAlreadyUsed) => {
529
557
warn!("Refresh token reuse detected during atomic operation");
530
-
return ApiError::AuthenticationFailed(Some(
558
+
return Err(ApiError::AuthenticationFailed(Some(
531
559
"Refresh token has been revoked due to suspected compromise".into(),
532
-
))
533
-
.into_response();
560
+
)));
534
561
}
535
562
Ok(tranquil_db_traits::RefreshSessionResult::ConcurrentRefresh) => {
536
563
warn!(
537
564
"Concurrent refresh detected for session_id: {}",
538
565
session_row.id
539
566
);
540
-
return ApiError::AuthenticationFailed(Some(
567
+
return Err(ApiError::AuthenticationFailed(Some(
541
568
"Refresh token has been revoked due to suspected compromise".into(),
542
-
))
543
-
.into_response();
569
+
)));
544
570
}
545
571
Err(e) => {
546
572
error!("Database error during session refresh: {:?}", e);
547
-
return ApiError::InternalError(None).into_response();
573
+
return Err(ApiError::InternalError(None));
548
574
}
549
575
}
550
576
let did_for_doc = session_row.did.clone();
···
558
584
let preferred_channel_verified = u
559
585
.channel_verification
560
586
.is_verified(u.preferred_comms_channel);
561
-
let pds_hostname = &tranquil_config::get().server.hostname;
562
-
let handle = full_handle(&u.handle, pds_hostname);
587
+
let handle = u.handle.clone();
563
588
let account_state =
564
589
AccountState::from_db_fields(u.deactivated_at, u.takedown_ref.clone(), None, None);
565
-
let mut response = json!({
566
-
"accessJwt": new_access_meta.token,
567
-
"refreshJwt": new_refresh_meta.token,
568
-
"handle": handle,
569
-
"did": session_row.did,
570
-
"email": u.email,
571
-
"emailConfirmed": u.channel_verification.email,
572
-
"preferredChannel": u.preferred_comms_channel.as_str(),
573
-
"preferredChannelVerified": preferred_channel_verified,
574
-
"preferredLocale": u.preferred_locale,
575
-
"isAdmin": u.is_admin,
576
-
"active": account_state.is_active()
577
-
});
578
-
if let Some(doc) = did_doc {
579
-
response["didDoc"] = doc;
580
-
}
581
-
if let Some(status) = account_state.status_for_session() {
582
-
response["status"] = json!(status);
583
-
}
584
-
Json(response).into_response()
590
+
Ok(Json(RefreshSessionOutput {
591
+
access_jwt: new_access_meta.token,
592
+
refresh_jwt: new_refresh_meta.token,
593
+
handle,
594
+
did: session_row.did,
595
+
email: u.email,
596
+
email_confirmed: u.channel_verification.email,
597
+
preferred_channel: u.preferred_comms_channel.as_str().to_string(),
598
+
preferred_channel_verified,
599
+
preferred_locale: u.preferred_locale,
600
+
is_admin: u.is_admin,
601
+
active: account_state.is_active(),
602
+
did_doc,
603
+
status: account_state.status_for_session().map(String::from),
604
+
}))
585
605
}
586
606
Ok(None) => {
587
607
error!("User not found for existing session: {}", session_row.did);
588
-
ApiError::InternalError(None).into_response()
608
+
Err(ApiError::InternalError(None))
589
609
}
590
610
Err(e) => {
591
611
error!("Database error fetching user: {:?}", e);
592
-
ApiError::InternalError(None).into_response()
612
+
Err(ApiError::InternalError(None))
593
613
}
594
614
}
595
615
}
···
617
637
pub async fn confirm_signup(
618
638
State(state): State<AppState>,
619
639
Json(input): Json<ConfirmSignupInput>,
620
-
) -> Response {
640
+
) -> Result<Json<ConfirmSignupOutput>, ApiError> {
621
641
info!("confirm_signup called for DID: {}", input.did);
622
642
let row = match state.user_repo.get_confirm_signup_by_did(&input.did).await {
623
643
Ok(Some(row)) => row,
624
644
Ok(None) => {
625
645
warn!("User not found for confirm_signup: {}", input.did);
626
-
return ApiError::InvalidRequest("Invalid DID or verification code".into())
627
-
.into_response();
646
+
return Err(ApiError::InvalidRequest(
647
+
"Invalid DID or verification code".into(),
648
+
));
628
649
}
629
650
Err(e) => {
630
651
error!("Database error in confirm_signup: {:?}", e);
631
-
return ApiError::InternalError(None).into_response();
652
+
return Err(ApiError::InternalError(None));
632
653
}
633
654
};
634
655
···
656
677
"Token DID mismatch for confirm_signup: expected {}, got {}",
657
678
input.did, token_data.did
658
679
);
659
-
return ApiError::InvalidRequest("Invalid verification code".into())
660
-
.into_response();
680
+
return Err(ApiError::InvalidRequest("Invalid verification code".into()));
661
681
}
662
682
}
663
683
Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => {
664
684
warn!("Verification code expired for user: {}", input.did);
665
-
return ApiError::ExpiredToken(Some("Verification code has expired".into()))
666
-
.into_response();
685
+
return Err(ApiError::ExpiredToken(Some(
686
+
"Verification code has expired".into(),
687
+
)));
667
688
}
668
689
Err(e) => {
669
690
warn!("Invalid verification code for user {}: {:?}", input.did, e);
670
-
return ApiError::InvalidRequest("Invalid verification code".into()).into_response();
691
+
return Err(ApiError::InvalidRequest("Invalid verification code".into()));
671
692
}
672
693
}
673
694
···
676
697
Ok(k) => k,
677
698
Err(e) => {
678
699
error!("Failed to decrypt user key: {:?}", e);
679
-
return ApiError::InternalError(None).into_response();
700
+
return Err(ApiError::InternalError(None));
680
701
}
681
702
};
682
703
683
-
let access_meta =
684
-
match tranquil_pds::auth::create_access_token_with_metadata(&row.did, &key_bytes) {
685
-
Ok(m) => m,
686
-
Err(e) => {
687
-
error!("Failed to create access token: {:?}", e);
688
-
return ApiError::InternalError(None).into_response();
689
-
}
690
-
};
691
-
let refresh_meta =
692
-
match tranquil_pds::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) {
693
-
Ok(m) => m,
694
-
Err(e) => {
695
-
error!("Failed to create refresh token: {:?}", e);
696
-
return ApiError::InternalError(None).into_response();
697
-
}
698
-
};
699
-
700
704
if let Err(e) = state
701
705
.user_repo
702
706
.set_channel_verified(&input.did, row.channel)
703
707
.await
704
708
{
705
709
error!("Failed to update verification status: {:?}", e);
706
-
return ApiError::InternalError(None).into_response();
710
+
return Err(ApiError::InternalError(None));
707
711
}
708
712
709
-
let session_data = tranquil_db_traits::SessionTokenCreate {
710
-
did: row.did.clone(),
711
-
access_jti: access_meta.jti.clone(),
712
-
refresh_jti: refresh_meta.jti.clone(),
713
-
access_expires_at: access_meta.expires_at,
714
-
refresh_expires_at: refresh_meta.expires_at,
715
-
login_type: tranquil_db_traits::LoginType::Modern,
716
-
mfa_verified: false,
717
-
scope: Some("transition:generic transition:chat.bsky".to_string()),
718
-
controller_did: None,
719
-
app_password_name: None,
713
+
let session = match crate::identity::provision::create_and_store_session(
714
+
&state,
715
+
&row.did,
716
+
&row.did,
717
+
&key_bytes,
718
+
"transition:generic transition:chat.bsky",
719
+
None,
720
+
)
721
+
.await
722
+
{
723
+
Ok(s) => s,
724
+
Err(_) => return Err(ApiError::InternalError(None)),
720
725
};
721
-
if let Err(e) = state.session_repo.create_session(&session_data).await {
722
-
error!("Failed to insert session: {:?}", e);
723
-
return ApiError::InternalError(None).into_response();
724
-
}
725
726
726
727
let hostname = &tranquil_config::get().server.hostname;
727
728
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome(
···
734
735
{
735
736
warn!("Failed to enqueue welcome notification: {:?}", e);
736
737
}
737
-
Json(ConfirmSignupOutput {
738
-
access_jwt: access_meta.token,
739
-
refresh_jwt: refresh_meta.token,
738
+
Ok(Json(ConfirmSignupOutput {
739
+
access_jwt: session.access_jwt,
740
+
refresh_jwt: session.refresh_jwt,
740
741
handle: row.handle,
741
742
did: row.did,
742
743
email: row.email,
743
744
email_verified: matches!(row.channel, tranquil_db_traits::CommsChannel::Email),
744
745
preferred_channel: row.channel,
745
746
preferred_channel_verified: true,
746
-
})
747
-
.into_response()
747
+
}))
748
748
}
749
749
750
750
const AUTO_VERIFY_DEBOUNCE: std::time::Duration = std::time::Duration::from_secs(120);
···
794
794
);
795
795
return Some(result);
796
796
}
797
-
let verification_token =
798
-
tranquil_pds::auth::verification_token::generate_signup_token(did, row.channel, &recipient);
799
-
let formatted_token =
800
-
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
801
-
let hostname = &tranquil_config::get().server.hostname;
802
-
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
803
-
state.user_repo.as_ref(),
804
-
state.infra_repo.as_ref(),
797
+
crate::identity::provision::enqueue_signup_verification(
798
+
state,
805
799
row.id,
800
+
did,
806
801
row.channel,
807
802
&recipient,
808
-
&formatted_token,
809
-
hostname,
810
803
)
811
-
.await
812
-
{
813
-
warn!("Failed to auto-resend verification for {}: {:?}", did, e);
814
-
return Some(result);
815
-
}
804
+
.await;
816
805
let _ = state
817
806
.cache
818
807
.set(&debounce_key, "1", AUTO_VERIFY_DEBOUNCE)
···
829
818
pub async fn resend_verification(
830
819
State(state): State<AppState>,
831
820
Json(input): Json<ResendVerificationInput>,
832
-
) -> Response {
821
+
) -> Result<Json<SuccessResponse>, ApiError> {
833
822
info!("resend_verification called for DID: {}", input.did);
834
823
let row = match state
835
824
.user_repo
···
838
827
{
839
828
Ok(Some(row)) => row,
840
829
Ok(None) => {
841
-
return ApiError::InvalidRequest("User not found".into()).into_response();
830
+
return Err(ApiError::InvalidRequest("User not found".into()));
842
831
}
843
832
Err(e) => {
844
833
error!("Database error in resend_verification: {:?}", e);
845
-
return ApiError::InternalError(None).into_response();
834
+
return Err(ApiError::InternalError(None));
846
835
}
847
836
};
848
837
let is_verified = row.channel_verification.has_any_verified();
849
838
if is_verified {
850
-
return ApiError::InvalidRequest("Account is already verified".into()).into_response();
839
+
return Err(ApiError::InvalidRequest(
840
+
"Account is already verified".into(),
841
+
));
851
842
}
852
843
853
844
let recipient = match row.channel {
···
861
852
tranquil_db_traits::CommsChannel::Signal => row.signal_username.clone().unwrap_or_default(),
862
853
};
863
854
864
-
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
865
-
&input.did,
866
-
row.channel,
867
-
&recipient,
868
-
);
869
-
let formatted_token =
870
-
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
871
-
872
-
let hostname = &tranquil_config::get().server.hostname;
873
-
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
874
-
state.user_repo.as_ref(),
875
-
state.infra_repo.as_ref(),
855
+
crate::identity::provision::enqueue_signup_verification(
856
+
&state,
876
857
row.id,
858
+
&input.did,
877
859
row.channel,
878
860
&recipient,
879
-
&formatted_token,
880
-
hostname,
881
861
)
882
-
.await
883
-
{
884
-
warn!("Failed to enqueue verification notification: {:?}", e);
885
-
}
886
-
SuccessResponse::ok().into_response()
862
+
.await;
863
+
Ok(Json(SuccessResponse { success: true }))
887
864
}
888
865
889
866
#[derive(Serialize)]
···
914
891
State(state): State<AppState>,
915
892
headers: HeaderMap,
916
893
auth: Auth<Active>,
917
-
) -> Result<Response, ApiError> {
918
-
let current_jti = headers
919
-
.get("authorization")
920
-
.and_then(|v| v.to_str().ok())
921
-
.and_then(|v| v.strip_prefix("Bearer "))
922
-
.and_then(|token| tranquil_pds::auth::get_jti_from_token(token).ok());
894
+
) -> Result<Json<ListSessionsOutput>, ApiError> {
895
+
let current_jti = tranquil_pds::auth::extract_jti_from_headers(&headers);
923
896
924
897
let jwt_rows = state
925
898
.session_repo
···
959
932
let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect();
960
933
sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at));
961
934
962
-
Ok((StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response())
935
+
Ok(Json(ListSessionsOutput { sessions }))
963
936
}
964
937
965
938
fn extract_client_name(client_id: &str) -> String {
···
982
955
State(state): State<AppState>,
983
956
auth: Auth<Active>,
984
957
Json(input): Json<RevokeSessionInput>,
985
-
) -> Result<Response, ApiError> {
958
+
) -> Result<Json<EmptyResponse>, ApiError> {
986
959
if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") {
987
960
let session_id = jwt_id
988
961
.parse::<i32>()
···
1021
994
} else {
1022
995
return Err(ApiError::InvalidRequest("Invalid session ID format".into()));
1023
996
}
1024
-
Ok(EmptyResponse::ok().into_response())
997
+
Ok(Json(EmptyResponse {}))
1025
998
}
1026
999
1027
1000
pub async fn revoke_all_sessions(
1028
1001
State(state): State<AppState>,
1029
1002
headers: HeaderMap,
1030
1003
auth: Auth<Active>,
1031
-
) -> Result<Response, ApiError> {
1032
-
let jti = tranquil_pds::auth::extract_auth_token_from_header(
1033
-
headers.get("authorization").and_then(|v| v.to_str().ok()),
1034
-
)
1035
-
.and_then(|extracted| tranquil_pds::auth::get_jti_from_token(&extracted.token).ok())
1036
-
.ok_or(ApiError::InvalidToken(None))?;
1004
+
) -> Result<Json<SuccessResponse>, ApiError> {
1005
+
let jti = tranquil_pds::auth::extract_jti_from_headers(&headers)
1006
+
.ok_or(ApiError::InvalidToken(None))?;
1037
1007
1038
1008
if auth.is_oauth() {
1039
1009
state
···
1061
1031
}
1062
1032
1063
1033
info!(did = %&auth.did, "All other sessions revoked");
1064
-
Ok(SuccessResponse::ok().into_response())
1034
+
Ok(Json(SuccessResponse { success: true }))
1065
1035
}
1066
1036
1067
1037
#[derive(Serialize)]
···
1074
1044
pub async fn get_legacy_login_preference(
1075
1045
State(state): State<AppState>,
1076
1046
auth: Auth<Active>,
1077
-
) -> Result<Response, ApiError> {
1047
+
) -> Result<Json<LegacyLoginPreferenceOutput>, ApiError> {
1078
1048
let pref = state
1079
1049
.user_repo
1080
1050
.get_legacy_login_pref(&auth.did)
···
1084
1054
Ok(Json(LegacyLoginPreferenceOutput {
1085
1055
allow_legacy_login: pref.allow_legacy_login,
1086
1056
has_mfa: pref.has_mfa,
1087
-
})
1088
-
.into_response())
1057
+
}))
1089
1058
}
1090
1059
1091
1060
#[derive(Deserialize)]
···
1094
1063
pub allow_legacy_login: bool,
1095
1064
}
1096
1065
1066
+
#[derive(Serialize)]
1067
+
#[serde(rename_all = "camelCase")]
1068
+
pub struct UpdateLegacyLoginOutput {
1069
+
pub allow_legacy_login: bool,
1070
+
}
1071
+
1097
1072
pub async fn update_legacy_login_preference(
1098
1073
State(state): State<AppState>,
1099
1074
auth: Auth<Active>,
1100
1075
Json(input): Json<UpdateLegacyLoginInput>,
1101
-
) -> Result<Response, ApiError> {
1102
-
let session_mfa = match require_legacy_session_mfa(&state, &auth).await {
1103
-
Ok(proof) => proof,
1104
-
Err(response) => return Ok(response),
1105
-
};
1076
+
) -> Result<Json<UpdateLegacyLoginOutput>, ApiError> {
1077
+
let session_mfa = require_legacy_session_mfa(&state, &auth).await?;
1106
1078
1107
-
let reauth_mfa = match require_reauth_window(&state, &auth).await {
1108
-
Ok(proof) => proof,
1109
-
Err(response) => return Ok(response),
1110
-
};
1079
+
let reauth_mfa = require_reauth_window(&state, &auth).await?;
1111
1080
1112
1081
let updated = state
1113
1082
.user_repo
···
1122
1091
allow_legacy_login = input.allow_legacy_login,
1123
1092
"Legacy login preference updated"
1124
1093
);
1125
-
Ok(Json(json!({
1126
-
"allowLegacyLogin": input.allow_legacy_login
1094
+
Ok(Json(UpdateLegacyLoginOutput {
1095
+
allow_legacy_login: input.allow_legacy_login,
1127
1096
}))
1128
-
.into_response())
1129
1097
}
1130
1098
1131
1099
use tranquil_pds::comms::VALID_LOCALES;
···
1140
1108
State(state): State<AppState>,
1141
1109
auth: Auth<Active>,
1142
1110
Json(input): Json<UpdateLocaleInput>,
1143
-
) -> Result<Response, ApiError> {
1111
+
) -> Result<Json<PreferredLocaleOutput>, ApiError> {
1144
1112
if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) {
1145
1113
return Err(ApiError::InvalidRequest(format!(
1146
1114
"Invalid locale. Valid options: {}",
···
1161
1129
locale = %input.preferred_locale,
1162
1130
"User locale preference updated"
1163
1131
);
1164
-
Ok(Json(json!({
1165
-
"preferredLocale": input.preferred_locale
1132
+
Ok(Json(PreferredLocaleOutput {
1133
+
preferred_locale: Some(input.preferred_locale),
1166
1134
}))
1167
-
.into_response())
1168
1135
}
History
1 round
0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
refactor(api): rework session login flow to use common credential verification
expand 0 comments
pull request successfully merged