+397
-407
Diff
round #0
+3
-3
crates/tranquil-api/src/actor/preferences.rs
+3
-3
crates/tranquil-api/src/actor/preferences.rs
···
69
69
if let Some(age) = personal_details_pref
70
70
.as_ref()
71
71
.and_then(|pref| pref.get("birthDate"))
72
-
.and_then(|v| v.as_str())
72
+
.and_then(Value::as_str)
73
73
.and_then(get_age_from_datestring)
74
74
{
75
75
let declared_age_pref = serde_json::json!({
···
122
122
if pref_str.len() > MAX_PREFERENCE_SIZE {
123
123
return PrefValidation::TooLarge(pref_str.len());
124
124
}
125
-
let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
125
+
let pref_type = match pref.get("$type").and_then(Value::as_str) {
126
126
Some(t) => t,
127
127
None => return PrefValidation::MissingType,
128
128
};
···
179
179
.preferences
180
180
.into_iter()
181
181
.filter_map(|pref| {
182
-
let pref_type = pref.get("$type").and_then(|t| t.as_str())?;
182
+
let pref_type = pref.get("$type").and_then(Value::as_str)?;
183
183
if pref_type == DECLARED_AGE_PREF {
184
184
return None;
185
185
}
+43
-20
crates/tranquil-api/src/age_assurance.rs
+43
-20
crates/tranquil-api/src/age_assurance.rs
···
1
1
use axum::{
2
2
Json,
3
3
extract::State,
4
-
http::{HeaderMap, Method, StatusCode},
5
-
response::{IntoResponse, Response},
4
+
http::{HeaderMap, Method},
6
5
};
7
-
use serde_json::json;
6
+
use serde::Serialize;
8
7
use tranquil_pds::auth::{
9
8
AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop,
10
9
};
11
10
use tranquil_pds::state::AppState;
12
11
13
-
pub async fn get_state(State(state): State<AppState>, headers: HeaderMap) -> Response {
12
+
#[derive(Serialize)]
13
+
#[serde(rename_all = "camelCase")]
14
+
pub struct AgeAssuranceState {
15
+
pub status: &'static str,
16
+
pub access: &'static str,
17
+
pub last_initiated_at: String,
18
+
}
19
+
20
+
#[derive(Serialize)]
21
+
#[serde(rename_all = "camelCase")]
22
+
pub struct AgeAssuranceMetadata {
23
+
pub account_created_at: Option<String>,
24
+
}
25
+
26
+
#[derive(Serialize)]
27
+
pub struct GetAgeAssuranceOutput {
28
+
pub state: AgeAssuranceState,
29
+
pub metadata: AgeAssuranceMetadata,
30
+
}
31
+
32
+
#[derive(Serialize)]
33
+
pub struct AgeAssuranceStatusOutput {
34
+
pub status: &'static str,
35
+
}
36
+
37
+
pub async fn get_state(
38
+
State(state): State<AppState>,
39
+
headers: HeaderMap,
40
+
) -> Json<GetAgeAssuranceOutput> {
14
41
let created_at = get_account_created_at(&state, &headers).await;
15
42
let now = chrono::Utc::now().to_rfc3339();
16
43
17
-
(
18
-
StatusCode::OK,
19
-
Json(json!({
20
-
"state": {
21
-
"status": "assured",
22
-
"access": "full",
23
-
"lastInitiatedAt": now
24
-
},
25
-
"metadata": {
26
-
"accountCreatedAt": created_at
27
-
}
28
-
})),
29
-
)
30
-
.into_response()
44
+
Json(GetAgeAssuranceOutput {
45
+
state: AgeAssuranceState {
46
+
status: "assured",
47
+
access: "full",
48
+
last_initiated_at: now,
49
+
},
50
+
metadata: AgeAssuranceMetadata {
51
+
account_created_at: created_at,
52
+
},
53
+
})
31
54
}
32
55
33
-
pub async fn get_age_assurance_state() -> Response {
34
-
(StatusCode::OK, Json(json!({"status": "assured"}))).into_response()
56
+
pub async fn get_age_assurance_state() -> Json<AgeAssuranceStatusOutput> {
57
+
Json(AgeAssuranceStatusOutput { status: "assured" })
35
58
}
36
59
37
60
async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> {
+75
-135
crates/tranquil-api/src/delegation.rs
+75
-135
crates/tranquil-api/src/delegation.rs
···
2
2
use axum::{
3
3
Json,
4
4
extract::{Query, State},
5
-
http::StatusCode,
6
-
response::{IntoResponse, Response},
7
5
};
8
6
use serde::{Deserialize, Serialize};
9
7
use serde_json::json;
10
8
use tracing::{error, info, warn};
11
9
use tranquil_pds::api::error::ApiError;
10
+
use tranquil_pds::api::{
11
+
AccountsOutput, AuditLogOutput, ControllersOutput, PresetsOutput, SuccessResponse,
12
+
};
12
13
use tranquil_pds::auth::{Active, Auth};
13
14
use tranquil_pds::delegation::{
14
15
DelegationActionType, SCOPE_PRESETS, ValidatedDelegationScope, verify_can_add_controllers,
···
21
22
pub async fn list_controllers(
22
23
State(state): State<AppState>,
23
24
auth: Auth<Active>,
24
-
) -> Result<Response, ApiError> {
25
-
let controllers = match state
25
+
) -> Result<Json<ControllersOutput<Vec<tranquil_db_traits::ControllerInfo>>>, ApiError> {
26
+
let controllers = state
26
27
.delegation_repo
27
28
.get_delegations_for_account(&auth.did)
28
29
.await
29
-
{
30
-
Ok(c) => c,
31
-
Err(e) => {
30
+
.map_err(|e| {
32
31
tracing::error!("Failed to list controllers: {:?}", e);
33
-
return Ok(
34
-
ApiError::InternalError(Some("Failed to list controllers".into())).into_response(),
35
-
);
36
-
}
37
-
};
32
+
ApiError::InternalError(Some("Failed to list controllers".into()))
33
+
})?;
38
34
39
35
let resolve_futures = controllers.into_iter().map(|mut c| {
40
36
let did_resolver = state.did_resolver.clone();
···
44
40
.resolve_did_document(c.did.as_str())
45
41
.await
46
42
.and_then(|doc| tranquil_types::did_doc::extract_handle(&doc))
47
-
.map(|h| h.into());
43
+
.map(Into::into);
48
44
}
49
45
c
50
46
}
···
52
48
53
49
let controllers = futures::future::join_all(resolve_futures).await;
54
50
55
-
Ok(Json(serde_json::json!({ "controllers": controllers })).into_response())
51
+
Ok(Json(ControllersOutput { controllers }))
56
52
}
57
53
58
54
#[derive(Debug, Deserialize)]
···
65
61
State(state): State<AppState>,
66
62
auth: Auth<Active>,
67
63
Json(input): Json<AddControllerInput>,
68
-
) -> Result<Response, ApiError> {
64
+
) -> Result<Json<SuccessResponse>, ApiError> {
69
65
let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did)
70
66
.await
71
67
.ok_or(ApiError::ControllerNotFound)?;
···
74
70
&& let Some(ref pds_url) = resolved.pds_url
75
71
{
76
72
if !pds_url.starts_with("https://") {
77
-
return Ok(
78
-
ApiError::InvalidDelegation("Controller PDS must use HTTPS".into()).into_response(),
79
-
);
73
+
return Err(ApiError::InvalidDelegation(
74
+
"Controller PDS must use HTTPS".into(),
75
+
));
80
76
}
81
77
match state
82
78
.cross_pds_oauth
···
84
80
.await
85
81
{
86
82
Some(true) => {
87
-
return Ok(ApiError::InvalidDelegation(
83
+
return Err(ApiError::InvalidDelegation(
88
84
"Cannot add a delegated account from another PDS as a controller".into(),
89
-
)
90
-
.into_response());
85
+
));
91
86
}
92
87
Some(false) => {}
93
88
None => {
···
100
95
}
101
96
}
102
97
103
-
let can_add = match verify_can_add_controllers(&state, &auth).await {
104
-
Ok(proof) => proof,
105
-
Err(response) => return Ok(response),
106
-
};
98
+
let can_add = verify_can_add_controllers(&state, &auth).await?;
107
99
108
100
if resolved.is_local
109
101
&& state
···
112
104
.await
113
105
.unwrap_or(false)
114
106
{
115
-
return Ok(ApiError::InvalidDelegation(
107
+
return Err(ApiError::InvalidDelegation(
116
108
"Cannot add a controlled account as a controller".into(),
117
-
)
118
-
.into_response());
109
+
));
119
110
}
120
111
121
112
match state
···
136
127
can_add.did(),
137
128
Some(&input.controller_did),
138
129
DelegationActionType::GrantCreated,
139
-
Some(serde_json::json!({
130
+
Some(json!({
140
131
"granted_scopes": input.granted_scopes.as_str(),
141
132
"is_local": resolved.is_local
142
133
})),
···
145
136
)
146
137
.await;
147
138
148
-
Ok((
149
-
StatusCode::OK,
150
-
Json(serde_json::json!({
151
-
"success": true
152
-
})),
153
-
)
154
-
.into_response())
139
+
Ok(Json(SuccessResponse { success: true }))
155
140
}
156
141
Err(e) => {
157
142
tracing::error!("Failed to add controller: {:?}", e);
158
-
Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response())
143
+
Err(ApiError::InternalError(Some(
144
+
"Failed to add controller".into(),
145
+
)))
159
146
}
160
147
}
161
148
}
···
169
156
State(state): State<AppState>,
170
157
auth: Auth<Active>,
171
158
Json(input): Json<RemoveControllerInput>,
172
-
) -> Result<Response, ApiError> {
159
+
) -> Result<Json<SuccessResponse>, ApiError> {
173
160
match state
174
161
.delegation_repo
175
162
.revoke_delegation(&auth.did, &input.controller_did, &auth.did)
···
197
184
&auth.did,
198
185
Some(&input.controller_did),
199
186
DelegationActionType::GrantRevoked,
200
-
Some(serde_json::json!({
187
+
Some(json!({
201
188
"revoked_app_passwords": revoked_app_passwords,
202
189
"revoked_oauth_tokens": revoked_oauth_tokens
203
190
})),
···
206
193
)
207
194
.await;
208
195
209
-
Ok((
210
-
StatusCode::OK,
211
-
Json(serde_json::json!({
212
-
"success": true
213
-
})),
214
-
)
215
-
.into_response())
196
+
Ok(Json(SuccessResponse { success: true }))
216
197
}
217
-
Ok(false) => Ok(ApiError::DelegationNotFound.into_response()),
198
+
Ok(false) => Err(ApiError::DelegationNotFound),
218
199
Err(e) => {
219
200
tracing::error!("Failed to remove controller: {:?}", e);
220
-
Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response())
201
+
Err(ApiError::InternalError(Some(
202
+
"Failed to remove controller".into(),
203
+
)))
221
204
}
222
205
}
223
206
}
···
232
215
State(state): State<AppState>,
233
216
auth: Auth<Active>,
234
217
Json(input): Json<UpdateControllerScopesInput>,
235
-
) -> Result<Response, ApiError> {
218
+
) -> Result<Json<SuccessResponse>, ApiError> {
236
219
match state
237
220
.delegation_repo
238
221
.update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes)
···
246
229
&auth.did,
247
230
Some(&input.controller_did),
248
231
DelegationActionType::ScopesModified,
249
-
Some(serde_json::json!({
232
+
Some(json!({
250
233
"new_scopes": input.granted_scopes.as_str()
251
234
})),
252
235
None,
···
254
237
)
255
238
.await;
256
239
257
-
Ok((
258
-
StatusCode::OK,
259
-
Json(serde_json::json!({
260
-
"success": true
261
-
})),
262
-
)
263
-
.into_response())
240
+
Ok(Json(SuccessResponse { success: true }))
264
241
}
265
-
Ok(false) => Ok(ApiError::DelegationNotFound.into_response()),
242
+
Ok(false) => Err(ApiError::DelegationNotFound),
266
243
Err(e) => {
267
244
tracing::error!("Failed to update controller scopes: {:?}", e);
268
-
Ok(
269
-
ApiError::InternalError(Some("Failed to update controller scopes".into()))
270
-
.into_response(),
271
-
)
245
+
Err(ApiError::InternalError(Some(
246
+
"Failed to update controller scopes".into(),
247
+
)))
272
248
}
273
249
}
274
250
}
···
276
252
pub async fn list_controlled_accounts(
277
253
State(state): State<AppState>,
278
254
auth: Auth<Active>,
279
-
) -> Result<Response, ApiError> {
280
-
let accounts = match state
255
+
) -> Result<Json<AccountsOutput<Vec<tranquil_db_traits::DelegatedAccountInfo>>>, ApiError> {
256
+
let accounts = state
281
257
.delegation_repo
282
258
.get_accounts_controlled_by(&auth.did)
283
259
.await
284
-
{
285
-
Ok(a) => a,
286
-
Err(e) => {
260
+
.map_err(|e| {
287
261
tracing::error!("Failed to list controlled accounts: {:?}", e);
288
-
return Ok(
289
-
ApiError::InternalError(Some("Failed to list controlled accounts".into()))
290
-
.into_response(),
291
-
);
292
-
}
293
-
};
262
+
ApiError::InternalError(Some("Failed to list controlled accounts".into()))
263
+
})?;
294
264
295
-
Ok(Json(serde_json::json!({ "accounts": accounts })).into_response())
265
+
Ok(Json(AccountsOutput { accounts }))
296
266
}
297
267
298
268
#[derive(Debug, Deserialize)]
···
311
281
State(state): State<AppState>,
312
282
auth: Auth<Active>,
313
283
Query(params): Query<AuditLogParams>,
314
-
) -> Result<Response, ApiError> {
284
+
) -> Result<Json<AuditLogOutput<Vec<tranquil_db_traits::AuditLogEntry>>>, ApiError> {
315
285
let limit = params.limit.clamp(1, 100);
316
286
let offset = params.offset.max(0);
317
287
318
-
let entries = match state
288
+
let entries = state
319
289
.delegation_repo
320
290
.get_audit_log_for_account(&auth.did, limit, offset)
321
291
.await
322
-
{
323
-
Ok(e) => e,
324
-
Err(e) => {
292
+
.map_err(|e| {
325
293
tracing::error!("Failed to get audit log: {:?}", e);
326
-
return Ok(
327
-
ApiError::InternalError(Some("Failed to get audit log".into())).into_response(),
328
-
);
329
-
}
330
-
};
294
+
ApiError::InternalError(Some("Failed to get audit log".into()))
295
+
})?;
331
296
332
297
let total = state
333
298
.delegation_repo
···
335
300
.await
336
301
.unwrap_or_default();
337
302
338
-
Ok(Json(serde_json::json!({ "entries": entries, "total": total })).into_response())
303
+
Ok(Json(AuditLogOutput { entries, total }))
339
304
}
340
305
341
-
pub async fn get_scope_presets() -> Response {
342
-
Json(serde_json::json!({ "presets": SCOPE_PRESETS })).into_response()
306
+
pub async fn get_scope_presets()
307
+
-> Json<PresetsOutput<&'static [tranquil_pds::delegation::ScopePreset]>> {
308
+
Json(PresetsOutput {
309
+
presets: SCOPE_PRESETS,
310
+
})
343
311
}
344
312
345
313
#[derive(Debug, Deserialize)]
···
353
321
354
322
#[derive(Debug, Serialize)]
355
323
#[serde(rename_all = "camelCase")]
356
-
pub struct CreateDelegatedAccountResponse {
324
+
pub struct CreateDelegatedAccountOutput {
357
325
pub did: Did,
358
326
pub handle: Handle,
359
327
}
···
363
331
_rate_limit: RateLimited<AccountCreationLimit>,
364
332
auth: Auth<Active>,
365
333
Json(input): Json<CreateDelegatedAccountInput>,
366
-
) -> Result<Response, ApiError> {
367
-
let can_control = match verify_can_control_accounts(&state, &auth).await {
368
-
Ok(proof) => proof,
369
-
Err(response) => return Ok(response),
370
-
};
334
+
) -> Result<Json<CreateDelegatedAccountOutput>, ApiError> {
335
+
let can_control = verify_can_control_accounts(&state, &auth).await?;
371
336
372
-
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
373
-
Ok(h) => h,
374
-
Err(e) => {
375
-
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
376
-
}
377
-
};
337
+
let handle = tranquil_pds::api::validation::resolve_handle_input(&input.handle)
338
+
.map_err(|e| ApiError::InvalidRequest(e.to_string()))?;
378
339
379
340
let email = input
380
341
.email
···
384
345
if let Some(ref email) = email
385
346
&& !tranquil_pds::api::validation::is_valid_email(email)
386
347
{
387
-
return Ok(ApiError::InvalidEmail.into_response());
348
+
return Err(ApiError::InvalidEmail);
388
349
}
389
350
390
351
let validated_invite_code = if let Some(ref code) = input.invite_code {
391
352
match state.infra_repo.validate_invite_code(code).await {
392
353
Ok(validated) => Some(validated),
393
-
Err(_) => return Ok(ApiError::InvalidInviteCode.into_response()),
354
+
Err(_) => return Err(ApiError::InvalidInviteCode),
394
355
}
395
356
} else {
396
357
let invite_required = tranquil_config::get().server.invite_code_required;
397
358
if invite_required {
398
-
return Ok(ApiError::InviteCodeRequired.into_response());
359
+
return Err(ApiError::InviteCodeRequired);
399
360
}
400
361
None
401
362
};
···
409
370
info!(did = %did, handle = %handle, controller = %can_control.did(), "Created DID for delegated account");
410
371
411
372
let repo = init_genesis_repo(&state, &did, &plc.signing_key, &plc.signing_key_bytes).await?;
373
+
let repo_for_seq = repo.clone();
412
374
413
375
let create_input = tranquil_db_traits::CreateDelegatedAccountInput {
414
376
handle: handle.clone(),
···
431
393
{
432
394
Ok(id) => id,
433
395
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
434
-
return Ok(ApiError::HandleNotAvailable(None).into_response());
396
+
return Err(ApiError::HandleNotAvailable(None));
435
397
}
436
398
Err(tranquil_db_traits::CreateAccountError::EmailTaken) => {
437
-
return Ok(ApiError::EmailTaken.into_response());
399
+
return Err(ApiError::EmailTaken);
438
400
}
439
401
Err(e) => {
440
402
error!("Error creating delegated account: {:?}", e);
441
-
return Ok(ApiError::InternalError(None).into_response());
403
+
return Err(ApiError::InternalError(None));
442
404
}
443
405
};
444
406
···
451
413
warn!("Failed to record invite code use for {}: {:?}", did, e);
452
414
}
453
415
454
-
if let Err(e) =
455
-
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await
456
-
{
457
-
warn!("Failed to sequence identity event for {}: {}", did, e);
458
-
}
459
-
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
416
+
crate::identity::provision::sequence_new_account(
460
417
&state,
461
418
&did,
462
-
tranquil_db_traits::AccountStatus::Active,
419
+
&handle,
420
+
&repo_for_seq,
421
+
handle.as_str(),
463
422
)
464
-
.await
465
-
{
466
-
warn!("Failed to sequence account event for {}: {}", did, e);
467
-
}
468
-
469
-
let profile_record = json!({
470
-
"$type": "app.bsky.actor.profile",
471
-
"displayName": handle
472
-
});
473
-
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
474
-
&state,
475
-
&did,
476
-
&tranquil_pds::types::PROFILE_COLLECTION,
477
-
&tranquil_pds::types::PROFILE_RKEY,
478
-
&profile_record,
479
-
)
480
-
.await
481
-
{
482
-
warn!("Failed to create default profile for {}: {}", did, e);
483
-
}
423
+
.await;
484
424
485
425
let _ = state
486
426
.delegation_repo
···
500
440
501
441
info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created");
502
442
503
-
Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response())
443
+
Ok(Json(CreateDelegatedAccountOutput { did, handle }))
504
444
}
505
445
506
446
#[derive(Debug, Deserialize)]
···
511
451
pub async fn resolve_controller(
512
452
State(state): State<AppState>,
513
453
Query(params): Query<ResolveControllerParams>,
514
-
) -> Result<Response, ApiError> {
454
+
) -> Result<Json<tranquil_pds::delegation::ResolvedIdentity>, ApiError> {
515
455
let identifier = params.identifier.trim().trim_start_matches('@');
516
456
517
457
let did: Did = if identifier.starts_with("did:") {
···
538
478
.await
539
479
.ok_or(ApiError::ControllerNotFound)?;
540
480
541
-
Ok(Json(resolved).into_response())
481
+
Ok(Json(resolved))
542
482
}
+145
-124
crates/tranquil-api/src/notification_prefs.rs
+145
-124
crates/tranquil-api/src/notification_prefs.rs
···
1
-
use axum::{
2
-
Json,
3
-
extract::State,
4
-
response::{IntoResponse, Response},
5
-
};
1
+
use axum::{Json, extract::State};
6
2
use serde::{Deserialize, Serialize};
7
3
use serde_json::json;
8
4
use tracing::info;
9
5
use tranquil_db_traits::{CommsChannel, CommsStatus, CommsType};
10
-
use tranquil_pds::api::error::ApiError;
6
+
use tranquil_pds::api::error::{ApiError, DbResultExt};
11
7
use tranquil_pds::auth::{Active, Auth};
12
8
use tranquil_pds::state::AppState;
13
9
use tranquil_types::Did;
14
10
15
11
#[derive(Serialize)]
16
12
#[serde(rename_all = "camelCase")]
17
-
pub struct NotificationPrefsResponse {
13
+
pub struct NotificationPrefsOutput {
18
14
pub preferred_channel: CommsChannel,
19
15
pub email: String,
20
16
pub discord_username: Option<String>,
···
28
24
pub async fn get_notification_prefs(
29
25
State(state): State<AppState>,
30
26
auth: Auth<Active>,
31
-
) -> Result<Response, ApiError> {
27
+
) -> Result<Json<NotificationPrefsOutput>, ApiError> {
32
28
let prefs = state
33
29
.user_repo
34
30
.get_notification_prefs(&auth.did)
35
31
.await
36
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
32
+
.log_db_err("get notification prefs")?
37
33
.ok_or(ApiError::AccountNotFound)?;
38
-
Ok(Json(NotificationPrefsResponse {
34
+
Ok(Json(NotificationPrefsOutput {
39
35
preferred_channel: prefs.preferred_channel,
40
36
email: prefs.email,
41
37
discord_username: prefs.discord_username,
···
44
40
telegram_verified: prefs.telegram_verified,
45
41
signal_username: prefs.signal_username,
46
42
signal_verified: prefs.signal_verified,
47
-
})
48
-
.into_response())
43
+
}))
49
44
}
50
45
51
46
#[derive(Serialize)]
···
61
56
62
57
#[derive(Serialize)]
63
58
#[serde(rename_all = "camelCase")]
64
-
pub struct GetNotificationHistoryResponse {
59
+
pub struct GetNotificationHistoryOutput {
65
60
pub notifications: Vec<NotificationHistoryEntry>,
66
61
}
67
62
68
63
pub async fn get_notification_history(
69
64
State(state): State<AppState>,
70
65
auth: Auth<Active>,
71
-
) -> Result<Response, ApiError> {
66
+
) -> Result<Json<GetNotificationHistoryOutput>, ApiError> {
72
67
let user_id = state
73
68
.user_repo
74
69
.get_id_by_did(&auth.did)
75
70
.await
76
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
71
+
.log_db_err("get user id by did")?
77
72
.ok_or(ApiError::AccountNotFound)?;
78
73
79
74
let rows = state
80
75
.infra_repo
81
76
.get_notification_history(user_id, 50)
82
77
.await
83
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
78
+
.log_db_err("get notification history")?;
84
79
85
80
let sensitive_types = [
86
81
CommsType::EmailVerification,
···
112
107
})
113
108
.collect();
114
109
115
-
Ok(Json(GetNotificationHistoryResponse { notifications }).into_response())
110
+
Ok(Json(GetNotificationHistoryOutput { notifications }))
116
111
}
117
112
118
113
#[derive(Deserialize)]
···
127
122
128
123
#[derive(Serialize)]
129
124
#[serde(rename_all = "camelCase")]
130
-
pub struct UpdateNotificationPrefsResponse {
125
+
pub struct UpdateNotificationPrefsOutput {
131
126
pub success: bool,
132
127
#[serde(skip_serializing_if = "Vec::is_empty")]
133
128
pub verification_required: Vec<CommsChannel>,
···
159
154
hostname,
160
155
)
161
156
.await
162
-
.map_err(|e| {
163
-
ApiError::InternalError(Some(format!(
164
-
"Failed to enqueue email notification: {}",
165
-
e
166
-
)))
167
-
})?;
157
+
.log_db_err("enqueue email verification")?;
168
158
}
169
159
_ => {
170
160
let hostname = &tranquil_config::get().server.hostname;
···
216
206
Some(json!({"code": formatted_token})),
217
207
)
218
208
.await
219
-
.map_err(|e| {
220
-
ApiError::InternalError(Some(format!("Failed to enqueue notification: {}", e)))
221
-
})?;
209
+
.log_db_err("enqueue channel verification")?;
222
210
}
223
211
}
224
212
225
213
Ok(token)
226
214
}
227
215
216
+
async fn process_messaging_channel_update(
217
+
state: &AppState,
218
+
user_id: uuid::Uuid,
219
+
did: &Did,
220
+
channel: CommsChannel,
221
+
raw_value: &str,
222
+
effective_channel: CommsChannel,
223
+
verification_required: &mut Vec<CommsChannel>,
224
+
) -> Result<(), ApiError> {
225
+
let clean = match channel {
226
+
CommsChannel::Discord => raw_value.trim().to_lowercase(),
227
+
CommsChannel::Telegram => raw_value.trim_start_matches('@').to_string(),
228
+
CommsChannel::Signal => raw_value.trim().trim_start_matches('@').to_lowercase(),
229
+
CommsChannel::Email => raw_value.trim().to_lowercase(),
230
+
};
231
+
232
+
if clean.is_empty() {
233
+
if effective_channel == channel {
234
+
return Err(ApiError::InvalidRequest(format!(
235
+
"Cannot remove {:?} while it is the preferred notification channel",
236
+
channel
237
+
)));
238
+
}
239
+
match channel {
240
+
CommsChannel::Discord => state
241
+
.user_repo
242
+
.clear_discord(user_id)
243
+
.await
244
+
.log_db_err("clear discord")?,
245
+
CommsChannel::Telegram => state
246
+
.user_repo
247
+
.clear_telegram(user_id)
248
+
.await
249
+
.log_db_err("clear telegram")?,
250
+
CommsChannel::Signal => state
251
+
.user_repo
252
+
.clear_signal(user_id)
253
+
.await
254
+
.log_db_err("clear signal")?,
255
+
CommsChannel::Email => {}
256
+
};
257
+
info!(did = %did, channel = ?channel, "Cleared channel");
258
+
return Ok(());
259
+
}
260
+
261
+
let valid = match channel {
262
+
CommsChannel::Discord => tranquil_pds::api::validation::is_valid_discord_username(&clean),
263
+
CommsChannel::Telegram => tranquil_pds::api::validation::is_valid_telegram_username(&clean),
264
+
CommsChannel::Signal => tranquil_pds::comms::is_valid_signal_username(&clean),
265
+
CommsChannel::Email => tranquil_pds::api::validation::is_valid_email(&clean),
266
+
};
267
+
if !valid {
268
+
return Err(match channel {
269
+
CommsChannel::Discord => ApiError::InvalidRequest(
270
+
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
271
+
),
272
+
CommsChannel::Telegram => ApiError::InvalidRequest(
273
+
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
274
+
),
275
+
CommsChannel::Signal => ApiError::InvalidRequest(
276
+
"Invalid Signal username. Must be 3-32 characters followed by .XX (e.g. username.01)".into(),
277
+
),
278
+
CommsChannel::Email => ApiError::InvalidEmail,
279
+
});
280
+
}
281
+
282
+
match channel {
283
+
CommsChannel::Discord => state
284
+
.user_repo
285
+
.set_unverified_discord(user_id, &clean)
286
+
.await
287
+
.log_db_err("set unverified discord")?,
288
+
CommsChannel::Telegram => state
289
+
.user_repo
290
+
.set_unverified_telegram(user_id, &clean)
291
+
.await
292
+
.log_db_err("set unverified telegram")?,
293
+
CommsChannel::Signal => state
294
+
.user_repo
295
+
.set_unverified_signal(user_id, &clean)
296
+
.await
297
+
.log_db_err("set unverified signal")?,
298
+
CommsChannel::Email => {}
299
+
};
300
+
301
+
if matches!(channel, CommsChannel::Signal) {
302
+
request_channel_verification(state, user_id, did, channel, &clean, None).await?;
303
+
}
304
+
305
+
verification_required.push(channel);
306
+
info!(did = %did, channel = ?channel, value = %clean, "Stored unverified channel username");
307
+
Ok(())
308
+
}
309
+
228
310
pub async fn update_notification_prefs(
229
311
State(state): State<AppState>,
230
312
auth: Auth<Active>,
231
313
Json(input): Json<UpdateNotificationPrefsInput>,
232
-
) -> Result<Response, ApiError> {
314
+
) -> Result<Json<UpdateNotificationPrefsOutput>, ApiError> {
233
315
let user_row = state
234
316
.user_repo
235
317
.get_id_handle_email_by_did(&auth.did)
236
318
.await
237
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
319
+
.log_db_err("get user by did")?
238
320
.ok_or(ApiError::AccountNotFound)?;
239
321
240
322
let user_id = user_row.id;
···
245
327
.user_repo
246
328
.get_notification_prefs(&auth.did)
247
329
.await
248
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
330
+
.log_db_err("get notification prefs for update")?
249
331
.ok_or(ApiError::AccountNotFound)?;
250
332
251
333
let effective_channel = input
···
268
350
.user_repo
269
351
.update_preferred_comms_channel(&auth.did, effective_channel)
270
352
.await
271
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
353
+
.log_db_err("update preferred channel")?;
272
354
info!(did = %auth.did, channel = ?effective_channel, "Updated preferred notification channel");
273
355
}
274
356
···
298
380
}
299
381
300
382
if let Some(ref discord_username) = input.discord_username {
301
-
let discord_clean = discord_username.trim().to_lowercase();
302
-
if discord_clean.is_empty() {
303
-
if effective_channel == CommsChannel::Discord {
304
-
return Err(ApiError::InvalidRequest(
305
-
"Cannot remove Discord while it is the preferred notification channel".into(),
306
-
));
307
-
}
308
-
state
309
-
.user_repo
310
-
.clear_discord(user_id)
311
-
.await
312
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
313
-
info!(did = %auth.did, "Cleared Discord");
314
-
} else if !tranquil_pds::api::validation::is_valid_discord_username(&discord_clean) {
315
-
return Err(ApiError::InvalidRequest(
316
-
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)"
317
-
.into(),
318
-
));
319
-
} else {
320
-
state
321
-
.user_repo
322
-
.set_unverified_discord(user_id, &discord_clean)
323
-
.await
324
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
325
-
verification_required.push(CommsChannel::Discord);
326
-
info!(did = %auth.did, discord_username = %discord_clean, "Stored unverified Discord username");
327
-
}
383
+
process_messaging_channel_update(
384
+
&state,
385
+
user_id,
386
+
&auth.did,
387
+
CommsChannel::Discord,
388
+
discord_username,
389
+
effective_channel,
390
+
&mut verification_required,
391
+
)
392
+
.await?;
328
393
}
329
394
330
395
if let Some(ref telegram) = input.telegram_username {
331
-
let telegram_clean = telegram.trim_start_matches('@');
332
-
if telegram_clean.is_empty() {
333
-
if effective_channel == CommsChannel::Telegram {
334
-
return Err(ApiError::InvalidRequest(
335
-
"Cannot remove Telegram while it is the preferred notification channel".into(),
336
-
));
337
-
}
338
-
state
339
-
.user_repo
340
-
.clear_telegram(user_id)
341
-
.await
342
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
343
-
info!(did = %auth.did, "Cleared Telegram username");
344
-
} else if !tranquil_pds::api::validation::is_valid_telegram_username(telegram_clean) {
345
-
return Err(ApiError::InvalidRequest(
346
-
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore"
347
-
.into(),
348
-
));
349
-
} else {
350
-
state
351
-
.user_repo
352
-
.set_unverified_telegram(user_id, telegram_clean)
353
-
.await
354
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
355
-
verification_required.push(CommsChannel::Telegram);
356
-
info!(did = %auth.did, telegram_username = %telegram_clean, "Stored unverified Telegram username");
357
-
}
396
+
process_messaging_channel_update(
397
+
&state,
398
+
user_id,
399
+
&auth.did,
400
+
CommsChannel::Telegram,
401
+
telegram,
402
+
effective_channel,
403
+
&mut verification_required,
404
+
)
405
+
.await?;
358
406
}
359
407
360
408
if let Some(ref signal) = input.signal_username {
361
-
let signal_clean = signal.trim().trim_start_matches('@').to_lowercase();
362
-
if signal_clean.is_empty() {
363
-
if effective_channel == CommsChannel::Signal {
364
-
return Err(ApiError::InvalidRequest(
365
-
"Cannot remove Signal while it is the preferred notification channel".into(),
366
-
));
367
-
}
368
-
state
369
-
.user_repo
370
-
.clear_signal(user_id)
371
-
.await
372
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
373
-
info!(did = %auth.did, "Cleared Signal username");
374
-
} else if !tranquil_pds::comms::is_valid_signal_username(&signal_clean) {
375
-
return Err(ApiError::InvalidRequest(
376
-
"Invalid Signal username. Must be 3-32 characters followed by .XX (e.g. username.01)"
377
-
.into(),
378
-
));
379
-
} else {
380
-
state
381
-
.user_repo
382
-
.set_unverified_signal(user_id, &signal_clean)
383
-
.await
384
-
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
385
-
request_channel_verification(
386
-
&state,
387
-
user_id,
388
-
&auth.did,
389
-
CommsChannel::Signal,
390
-
&signal_clean,
391
-
None,
392
-
)
393
-
.await?;
394
-
verification_required.push(CommsChannel::Signal);
395
-
info!(did = %auth.did, signal_username = %signal_clean, "Stored unverified Signal username");
396
-
}
409
+
process_messaging_channel_update(
410
+
&state,
411
+
user_id,
412
+
&auth.did,
413
+
CommsChannel::Signal,
414
+
signal,
415
+
effective_channel,
416
+
&mut verification_required,
417
+
)
418
+
.await?;
397
419
}
398
420
399
-
Ok(Json(UpdateNotificationPrefsResponse {
421
+
Ok(Json(UpdateNotificationPrefsOutput {
400
422
success: true,
401
423
verification_required,
402
-
})
403
-
.into_response())
424
+
}))
404
425
}
+57
-79
crates/tranquil-api/src/server/email.rs
+57
-79
crates/tranquil-api/src/server/email.rs
···
12
12
use tracing::{error, info, warn};
13
13
use tranquil_db_traits::CommsChannel;
14
14
use tranquil_pds::api::error::{ApiError, DbResultExt};
15
-
use tranquil_pds::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse};
15
+
use tranquil_pds::api::{
16
+
EmailUpdateStatusOutput, EmptyResponse, InUseOutput, TokenRequiredResponse, VerifiedResponse,
17
+
};
16
18
use tranquil_pds::auth::{Auth, NotTakendown};
19
+
use tranquil_pds::oauth::scopes::{AccountAction, AccountAttr};
17
20
use tranquil_pds::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit};
18
21
use tranquil_pds::state::AppState;
19
22
···
48
51
_rate_limit: RateLimited<EmailUpdateLimit>,
49
52
auth: Auth<NotTakendown>,
50
53
input: Option<Json<RequestEmailUpdateInput>>,
51
-
) -> Result<Response, ApiError> {
52
-
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
53
-
&auth.auth_source,
54
-
auth.scope.as_deref(),
55
-
tranquil_pds::oauth::scopes::AccountAttr::Email,
56
-
tranquil_pds::oauth::scopes::AccountAction::Manage,
57
-
) {
58
-
return Ok(e);
59
-
}
54
+
) -> Result<Json<TokenRequiredResponse>, ApiError> {
55
+
auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?;
60
56
61
57
let user = state
62
58
.user_repo
···
119
115
}
120
116
121
117
info!("Email update requested for user {}", user.id);
122
-
Ok(TokenRequiredResponse::response(token_required).into_response())
118
+
Ok(Json(TokenRequiredResponse { token_required }))
123
119
}
124
120
125
121
#[derive(Deserialize)]
···
134
130
_rate_limit: RateLimited<EmailUpdateLimit>,
135
131
auth: Auth<NotTakendown>,
136
132
Json(input): Json<ConfirmEmailInput>,
137
-
) -> Result<Response, ApiError> {
138
-
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
139
-
&auth.auth_source,
140
-
auth.scope.as_deref(),
141
-
tranquil_pds::oauth::scopes::AccountAttr::Email,
142
-
tranquil_pds::oauth::scopes::AccountAction::Manage,
143
-
) {
144
-
return Ok(e);
145
-
}
133
+
) -> Result<Json<EmptyResponse>, ApiError> {
134
+
auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?;
146
135
147
136
let did = &auth.did;
148
137
let user = state
···
163
152
}
164
153
165
154
if user.email_verified {
166
-
return Ok(EmptyResponse::ok().into_response());
155
+
return Ok(Json(EmptyResponse {}));
167
156
}
168
157
169
158
let confirmation_code =
···
196
185
.log_db_err("confirming email")?;
197
186
198
187
info!("Email confirmed for user {}", user.id);
199
-
Ok(EmptyResponse::ok().into_response())
188
+
Ok(Json(EmptyResponse {}))
200
189
}
201
190
202
191
#[derive(Deserialize)]
···
212
201
State(state): State<AppState>,
213
202
auth: Auth<NotTakendown>,
214
203
Json(input): Json<UpdateEmailInput>,
215
-
) -> Result<Response, ApiError> {
216
-
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
217
-
&auth.auth_source,
218
-
auth.scope.as_deref(),
219
-
tranquil_pds::oauth::scopes::AccountAttr::Email,
220
-
tranquil_pds::oauth::scopes::AccountAction::Manage,
221
-
) {
222
-
return Ok(e);
223
-
}
204
+
) -> Result<Json<EmptyResponse>, ApiError> {
205
+
auth.check_account_scope(AccountAttr::Email, AccountAction::Manage)?;
224
206
225
207
let did = &auth.did;
226
208
let user = state
···
279
261
ApiError::InternalError(Some("Failed to update 2FA setting".into()))
280
262
})?;
281
263
}
282
-
return Ok(EmptyResponse::ok().into_response());
264
+
return Ok(Json(EmptyResponse {}));
283
265
}
284
266
285
267
if email_verified {
···
394
376
}
395
377
396
378
info!("Email updated for user {}", user_id);
397
-
Ok(EmptyResponse::ok().into_response())
379
+
Ok(Json(EmptyResponse {}))
398
380
}
399
381
400
382
#[derive(Deserialize)]
···
406
388
State(state): State<AppState>,
407
389
_rate_limit: RateLimited<VerificationCheckLimit>,
408
390
Json(input): Json<CheckEmailVerifiedInput>,
409
-
) -> Response {
410
-
match state
391
+
) -> Result<Json<VerifiedResponse>, ApiError> {
392
+
let verified = state
411
393
.user_repo
412
394
.check_email_verified_by_identifier(&input.identifier)
413
395
.await
414
-
{
415
-
Ok(Some(verified)) => VerifiedResponse::response(verified).into_response(),
416
-
Ok(None) => ApiError::AccountNotFound.into_response(),
417
-
Err(e) => {
396
+
.map_err(|e| {
418
397
error!("DB error checking email verified: {:?}", e);
419
-
ApiError::InternalError(None).into_response()
420
-
}
421
-
}
398
+
ApiError::InternalError(None)
399
+
})?
400
+
.ok_or(ApiError::AccountNotFound)?;
401
+
402
+
Ok(Json(VerifiedResponse { verified }))
422
403
}
423
404
424
405
#[derive(Deserialize)]
···
431
412
State(state): State<AppState>,
432
413
_rate_limit: RateLimited<VerificationCheckLimit>,
433
414
Json(input): Json<CheckChannelVerifiedInput>,
434
-
) -> Response {
435
-
match state
415
+
) -> Result<Json<VerifiedResponse>, ApiError> {
416
+
let verified = state
436
417
.user_repo
437
418
.check_channel_verified_by_did(&input.did, input.channel)
438
419
.await
439
-
{
440
-
Ok(Some(verified)) => VerifiedResponse::response(verified).into_response(),
441
-
Ok(None) => ApiError::AccountNotFound.into_response(),
442
-
Err(e) => {
420
+
.map_err(|e| {
443
421
error!("DB error checking channel verified: {:?}", e);
444
-
ApiError::InternalError(None).into_response()
445
-
}
446
-
}
422
+
ApiError::InternalError(None)
423
+
})?
424
+
.ok_or(ApiError::AccountNotFound)?;
425
+
426
+
Ok(Json(VerifiedResponse { verified }))
447
427
}
448
428
449
429
#[derive(Deserialize)]
···
545
525
State(state): State<AppState>,
546
526
_rate_limit: RateLimited<VerificationCheckLimit>,
547
527
auth: Auth<NotTakendown>,
548
-
) -> Result<Response, ApiError> {
549
-
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
550
-
&auth.auth_source,
551
-
auth.scope.as_deref(),
552
-
tranquil_pds::oauth::scopes::AccountAttr::Email,
553
-
tranquil_pds::oauth::scopes::AccountAction::Read,
554
-
) {
555
-
return Ok(e);
556
-
}
528
+
) -> Result<Json<EmailUpdateStatusOutput>, ApiError> {
529
+
auth.check_account_scope(AccountAttr::Email, AccountAction::Read)?;
557
530
558
531
let cache_key = email_update_cache_key(&auth.did);
559
532
let pending_json = match state.cache.get(&cache_key).await {
560
533
Some(json) => json,
561
534
None => {
562
-
return Ok(Json(json!({ "pending": false, "authorized": false })).into_response());
535
+
return Ok(Json(EmailUpdateStatusOutput {
536
+
pending: false,
537
+
authorized: false,
538
+
new_email: None,
539
+
}));
563
540
}
564
541
};
565
542
566
543
let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) {
567
544
Ok(p) => p,
568
545
Err(_) => {
569
-
return Ok(Json(json!({ "pending": false, "authorized": false })).into_response());
546
+
return Ok(Json(EmailUpdateStatusOutput {
547
+
pending: false,
548
+
authorized: false,
549
+
new_email: None,
550
+
}));
570
551
}
571
552
};
572
553
573
-
Ok(Json(json!({
574
-
"pending": true,
575
-
"authorized": pending.authorized,
576
-
"newEmail": pending.new_email,
554
+
Ok(Json(EmailUpdateStatusOutput {
555
+
pending: true,
556
+
authorized: pending.authorized,
557
+
new_email: Some(pending.new_email),
577
558
}))
578
-
.into_response())
579
559
}
580
560
581
561
#[derive(Deserialize)]
···
587
567
State(state): State<AppState>,
588
568
_rate_limit: RateLimited<VerificationCheckLimit>,
589
569
Json(input): Json<CheckEmailInUseInput>,
590
-
) -> Response {
570
+
) -> Result<Json<InUseOutput>, ApiError> {
591
571
let email = input.email.trim().to_lowercase();
592
572
if email.is_empty() {
593
-
return ApiError::InvalidRequest("email is required".into()).into_response();
573
+
return Err(ApiError::InvalidRequest("email is required".into()));
594
574
}
595
575
596
-
let count = match state.user_repo.count_accounts_by_email(&email).await {
597
-
Ok(c) => c,
598
-
Err(e) => {
576
+
let count = state
577
+
.user_repo
578
+
.count_accounts_by_email(&email)
579
+
.await
580
+
.map_err(|e| {
599
581
error!("DB error checking email usage: {:?}", e);
600
-
return ApiError::InternalError(None).into_response();
601
-
}
602
-
};
582
+
ApiError::InternalError(None)
583
+
})?;
603
584
604
-
Json(json!({
605
-
"inUse": count > 0,
606
-
}))
607
-
.into_response()
585
+
Ok(Json(InUseOutput { in_use: count > 0 }))
608
586
}
+74
-46
crates/tranquil-api/src/server/meta.rs
+74
-46
crates/tranquil-api/src/server/meta.rs
···
1
1
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
2
-
use serde_json::json;
2
+
use serde::Serialize;
3
+
use tranquil_db_traits::CommsChannel;
3
4
use tranquil_pds::BUILD_VERSION;
4
5
use tranquil_pds::state::AppState;
5
6
use tranquil_pds::util::{discord_app_id, discord_bot_username, telegram_bot_username};
6
7
7
-
fn get_available_comms_channels() -> Vec<tranquil_db_traits::CommsChannel> {
8
-
use tranquil_db_traits::CommsChannel;
8
+
fn get_available_comms_channels() -> Vec<CommsChannel> {
9
9
let cfg = tranquil_config::get();
10
10
let mut channels = vec![CommsChannel::Email];
11
11
if cfg.discord.bot_token.is_some() {
···
31
31
tranquil_config::get().server.enable_pds_hosted_did_web
32
32
}
33
33
34
-
pub async fn describe_server() -> impl IntoResponse {
34
+
#[derive(Serialize)]
35
+
#[serde(rename_all = "camelCase")]
36
+
pub struct DescribeServerLinks {
37
+
#[serde(skip_serializing_if = "Option::is_none")]
38
+
pub privacy_policy: Option<String>,
39
+
#[serde(skip_serializing_if = "Option::is_none")]
40
+
pub terms_of_service: Option<String>,
41
+
}
42
+
43
+
#[derive(Serialize)]
44
+
#[serde(rename_all = "camelCase")]
45
+
pub struct DescribeServerContact {
46
+
#[serde(skip_serializing_if = "Option::is_none")]
47
+
pub email: Option<String>,
48
+
}
49
+
50
+
#[derive(Serialize)]
51
+
#[serde(rename_all = "camelCase")]
52
+
pub struct DescribeServerOutput {
53
+
pub available_user_domains: Vec<String>,
54
+
pub invite_code_required: bool,
55
+
pub did: String,
56
+
pub links: DescribeServerLinks,
57
+
pub contact: DescribeServerContact,
58
+
pub version: &'static str,
59
+
pub available_comms_channels: Vec<CommsChannel>,
60
+
pub self_hosted_did_web_enabled: bool,
61
+
#[serde(skip_serializing_if = "Option::is_none")]
62
+
pub discord_bot_username: Option<String>,
63
+
#[serde(skip_serializing_if = "Option::is_none")]
64
+
pub discord_app_id: Option<String>,
65
+
#[serde(skip_serializing_if = "Option::is_none")]
66
+
pub telegram_bot_username: Option<String>,
67
+
}
68
+
69
+
pub async fn describe_server() -> Json<DescribeServerOutput> {
35
70
let cfg = tranquil_config::get();
36
71
let pds_hostname = &cfg.server.hostname;
37
-
let domains = cfg.server.user_handle_domain_list();
38
-
let invite_code_required = cfg.server.invite_code_required;
39
-
let privacy_policy = cfg.server.privacy_policy_url.clone();
40
-
let terms_of_service = cfg.server.terms_of_service_url.clone();
41
-
let contact_email = cfg.server.contact_email.clone();
42
-
let mut links = serde_json::Map::new();
43
-
if let Some(pp) = privacy_policy {
44
-
links.insert("privacyPolicy".to_string(), json!(pp));
45
-
}
46
-
if let Some(tos) = terms_of_service {
47
-
links.insert("termsOfService".to_string(), json!(tos));
48
-
}
49
-
let mut contact = serde_json::Map::new();
50
-
if let Some(email) = contact_email {
51
-
contact.insert("email".to_string(), json!(email));
52
-
}
53
-
let mut response = json!({
54
-
"availableUserDomains": domains,
55
-
"inviteCodeRequired": invite_code_required,
56
-
"did": format!("did:web:{}", pds_hostname),
57
-
"links": links,
58
-
"contact": contact,
59
-
"version": BUILD_VERSION,
60
-
"availableCommsChannels": get_available_comms_channels(),
61
-
"selfHostedDidWebEnabled": is_self_hosted_did_web_enabled()
62
-
});
63
-
if let Some(bot_username) = discord_bot_username() {
64
-
response["discordBotUsername"] = json!(bot_username);
65
-
}
66
-
if let Some(app_id) = discord_app_id() {
67
-
response["discordAppId"] = json!(app_id);
68
-
}
69
-
if let Some(bot_username) = telegram_bot_username() {
70
-
response["telegramBotUsername"] = json!(bot_username);
71
-
}
72
-
Json(response)
72
+
73
+
Json(DescribeServerOutput {
74
+
available_user_domains: cfg.server.user_handle_domain_list(),
75
+
invite_code_required: cfg.server.invite_code_required,
76
+
did: format!("did:web:{}", pds_hostname),
77
+
links: DescribeServerLinks {
78
+
privacy_policy: cfg.server.privacy_policy_url.clone(),
79
+
terms_of_service: cfg.server.terms_of_service_url.clone(),
80
+
},
81
+
contact: DescribeServerContact {
82
+
email: cfg.server.contact_email.clone(),
83
+
},
84
+
version: BUILD_VERSION,
85
+
available_comms_channels: get_available_comms_channels(),
86
+
self_hosted_did_web_enabled: is_self_hosted_did_web_enabled(),
87
+
discord_bot_username: discord_bot_username().map(String::from),
88
+
discord_app_id: discord_app_id().map(String::from),
89
+
telegram_bot_username: telegram_bot_username().map(String::from),
90
+
})
91
+
}
92
+
#[derive(Serialize)]
93
+
pub struct HealthOutput {
94
+
#[serde(skip_serializing_if = "Option::is_none")]
95
+
pub version: Option<String>,
96
+
#[serde(skip_serializing_if = "Option::is_none")]
97
+
pub error: Option<&'static str>,
73
98
}
99
+
74
100
pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
75
101
match state.infra_repo.health_check().await {
76
102
Ok(true) => (
77
103
StatusCode::OK,
78
-
Json(json!({
79
-
"version": format!("tranquil {}", BUILD_VERSION)
80
-
})),
104
+
Json(HealthOutput {
105
+
version: Some(format!("tranquil {}", BUILD_VERSION)),
106
+
error: None,
107
+
}),
81
108
),
82
109
_ => (
83
110
StatusCode::SERVICE_UNAVAILABLE,
84
-
Json(json!({
85
-
"error": "Service Unavailable"
86
-
})),
111
+
Json(HealthOutput {
112
+
version: None,
113
+
error: Some("Service Unavailable"),
114
+
}),
87
115
),
88
116
}
89
117
}
History
1 round
0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
refactor(api): update delegation, notification prefs, email, meta, and age assurance endpoints
expand 0 comments
pull request successfully merged