atagen

malice

public
Star0
Watch0
544a170ead2c7f632e7957c1ed729ba50c80ec51
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
crates/malice-server/src/routes.rsRust835 lines22.5 KB
1
use std::sync::Arc;
2
3
use axum::{
4
    Router,
5
    extract::{DefaultBodyLimit, Multipart, Query, State},
6
    http::{HeaderMap, StatusCode},
7
    middleware,
8
    response::{IntoResponse, Response, Sse, sse::Event as SseEvent},
9
    routing::{get, post},
10
};
11
use futures_util::StreamExt;
12
use macltok::Retain;
13
use malice_core::{
14
    feed::subscribe,
15
    login::LoginInput,
16
    request::{Reply, Request, dispatch},
17
};
18
use serde::{Deserialize, Serialize};
19
use tower_http::{
20
    services::{ServeDir, ServeFile},
21
    trace::TraceLayer,
22
};
23
24
use crate::{
25
    csrf::{csrf_guard, session_sid, set_cookie_value},
26
    error::{ApiError, json_ok, no_content},
27
    state::{AppState, RestoreError, UserSession},
28
};
29
30
#[derive(Debug, Deserialize)]
31
struct RoomIdBody {
32
    #[serde(default)]
33
    room_id: String,
34
}
35
36
#[derive(Debug, Deserialize)]
37
struct SendBody {
38
    #[serde(default)]
39
    room_id: String,
40
    #[serde(default)]
41
    body: String,
42
}
43
44
#[derive(Debug, Deserialize)]
45
struct PaginateBody {
46
    #[serde(default)]
47
    room_id: String,
48
}
49
50
#[derive(Debug, Deserialize)]
51
struct JoinBody {
52
    #[serde(default)]
53
    room: String,
54
}
55
56
#[derive(Debug, Deserialize)]
57
struct CreateBody {
58
    #[serde(default)]
59
    name: String,
60
    #[serde(default)]
61
    topic: Option<String>,
62
}
63
64
#[derive(Debug, Deserialize)]
65
struct ReactBody {
66
    #[serde(default)]
67
    room_id: String,
68
    #[serde(default)]
69
    event_id: String,
70
    #[serde(default)]
71
    key: String,
72
}
73
74
#[derive(Debug, Deserialize)]
75
struct ReadBody {
76
    #[serde(default)]
77
    room_id: String,
78
    #[serde(default)]
79
    event_id: String,
80
}
81
82
#[derive(Debug, Deserialize)]
83
struct MembersQuery {
84
    #[serde(default)]
85
    room_id: String,
86
}
87
#[derive(Debug, Deserialize)]
88
struct RecoverBody {
89
    recovery_key: String,
90
}
91
92
#[derive(Debug, Deserialize)]
93
struct VerifyBody {
94
    action: String,
95
}
96
97
#[derive(Debug, Deserialize)]
98
struct MediaQuery {
99
    mxc: String,
100
}
101
#[derive(Debug, Deserialize)]
102
struct MessageMediaQuery {
103
    media_id: String,
104
    size: String,
105
}
106
107
#[derive(Debug, Deserialize)]
108
struct TypingBody {
109
    #[serde(default)]
110
    room_id: String,
111
    #[serde(default)]
112
    typing: bool,
113
}
114
115
#[derive(Debug, Deserialize)]
116
struct AccountDataQuery {
117
    event_type: String,
118
}
119
120
#[derive(Debug, Deserialize)]
121
struct AccountDataBody {
122
    event_type: String,
123
    content: serde_json::Value,
124
}
125
126
#[derive(Debug, Serialize)]
127
struct SoftLogoutInfo {
128
    user_id: String,
129
    homeserver: String,
130
}
131
132
#[derive(Debug, Serialize)]
133
struct SessionResponse {
134
    user_id: Option<String>,
135
    #[serde(skip_serializing_if = "Option::is_none")]
136
    soft_logout: Option<SoftLogoutInfo>,
137
}
138
139
async fn auth(
140
    state: &AppState,
141
    headers: &HeaderMap,
142
) -> Result<(Arc<UserSession>, String), ApiError> {
143
    let Some(sid) = session_sid(headers) else {
144
        return Err(ApiError::unauthorized("not signed in"));
145
    };
146
    match state.get_or_restore(&sid).await {
147
        Ok(user) => Ok((user, sid)),
148
        Err(RestoreError::NotFound) => Err(ApiError::unauthorized_stale(
149
            "session expired; sign in again",
150
            state.cookie_secure(),
151
        )),
152
        Err(RestoreError::SoftLoggedOut { .. }) => Err(ApiError::unauthorized(
153
            "signed out by the server, sign in again",
154
        )),
155
        Err(RestoreError::Stale(message)) => {
156
            state.remove_session(&sid, Retain::Nothing).await;
157
            Err(ApiError::unauthorized_stale(message, state.cookie_secure()))
158
        }
159
        Err(RestoreError::Unavailable(message)) => {
160
            Err(ApiError::new(StatusCode::BAD_GATEWAY, message))
161
        }
162
    }
163
}
164
165
/// commands answer when they're done, so the browser gets the outcome in the response
166
fn respond(result: Result<Reply, malice_core::error::CoreError>) -> Response {
167
    match result {
168
        Ok(Reply::Done) => no_content(None),
169
        Ok(Reply::Json(value)) => json_ok(&value, None),
170
        Ok(Reply::Avatar {
171
            bytes,
172
            content_type,
173
        }) => (
174
            [
175
                (axum::http::header::CONTENT_TYPE, content_type),
176
                (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),
177
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
178
            ],
179
            bytes,
180
        )
181
            .into_response(),
182
        Ok(Reply::Attachment {
183
            bytes,
184
            content_type,
185
        }) => (
186
            [
187
                (
188
                    axum::http::header::CONTENT_TYPE,
189
                    content_type.unwrap_or("application/octet-stream"),
190
                ),
191
                (
192
                    axum::http::header::CONTENT_DISPOSITION,
193
                    if content_type.is_some() {
194
                        "inline"
195
                    } else {
196
                        "attachment"
197
                    },
198
                ),
199
                (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),
200
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
201
                (axum::http::header::CONTENT_SECURITY_POLICY, "sandbox"),
202
            ],
203
            bytes,
204
        )
205
            .into_response(),
206
        Err(error) => ApiError::from(error).into_response(),
207
    }
208
}
209
210
async fn get_session(State(state): State<AppState>, headers: HeaderMap) -> Response {
211
    let Some(sid) = session_sid(&headers) else {
212
        return json_ok(
213
            &SessionResponse {
214
                user_id: None,
215
                soft_logout: None,
216
            },
217
            None,
218
        );
219
    };
220
    match state.get_or_restore(&sid).await {
221
        Ok(user) => json_ok(
222
            &SessionResponse {
223
                user_id: Some(user.user_id.clone()),
224
                soft_logout: None,
225
            },
226
            None,
227
        ),
228
        Err(RestoreError::NotFound) => json_ok(
229
            &SessionResponse {
230
                user_id: None,
231
                soft_logout: None,
232
            },
233
            None,
234
        ),
235
        Err(RestoreError::SoftLoggedOut {
236
            user_id,
237
            homeserver,
238
        }) => json_ok(
239
            &SessionResponse {
240
                user_id: None,
241
                soft_logout: Some(SoftLogoutInfo {
242
                    user_id,
243
                    homeserver,
244
                }),
245
            },
246
            None,
247
        ),
248
        Err(RestoreError::Stale(message)) => {
249
            state.remove_session(&sid, Retain::Nothing).await;
250
            ApiError::unauthorized_stale(message, state.cookie_secure()).into_response()
251
        }
252
        Err(RestoreError::Unavailable(message)) => {
253
            ApiError::new(StatusCode::BAD_GATEWAY, message).into_response()
254
        }
255
    }
256
}
257
258
async fn post_login(
259
    State(state): State<AppState>,
260
    headers: HeaderMap,
261
    body: axum::Json<LoginInput>,
262
) -> Response {
263
    let username = body.0.username.trim().to_owned();
264
    let password = body.0.password.clone();
265
    let login = match body.0.into_login(state.allow_http_homeserver()) {
266
        Ok(login) => login,
267
        Err(error) => return ApiError::from(error).into_response(),
268
    };
269
    let cookie_sid = session_sid(&headers);
270
    match state
271
        .login(cookie_sid.as_deref(), login, &username, password)
272
        .await
273
    {
274
        Ok(user) => json_ok(
275
            &serde_json::json!({ "user_id": user.user_id }),
276
            Some(set_cookie_value(&user.sid, state.cookie_secure())),
277
        ),
278
        Err(error) => ApiError::from(error).into_response(),
279
    }
280
}
281
282
async fn post_logout(State(state): State<AppState>, headers: HeaderMap) -> Response {
283
    if let Some(sid) = session_sid(&headers) {
284
        state.logout(&sid).await;
285
    }
286
    no_content(Some(crate::csrf::clear_cookie_value(state.cookie_secure())))
287
}
288
289
async fn get_rooms(State(state): State<AppState>, headers: HeaderMap) -> Response {
290
    let (user, _) = match auth(&state, &headers).await {
291
        Ok(pair) => pair,
292
        Err(error) => return error.into_response(),
293
    };
294
    respond(dispatch(&user.handle, &user.mirror, Request::Rooms).await)
295
}
296
297
async fn get_members(
298
    State(state): State<AppState>,
299
    headers: HeaderMap,
300
    Query(query): Query<MembersQuery>,
301
) -> Response {
302
    let (user, _) = match auth(&state, &headers).await {
303
        Ok(pair) => pair,
304
        Err(error) => return error.into_response(),
305
    };
306
    respond(
307
        dispatch(
308
            &user.handle,
309
            &user.mirror,
310
            Request::Members {
311
                room_id: query.room_id,
312
            },
313
        )
314
        .await,
315
    )
316
}
317
318
async fn rooms_open(
319
    State(state): State<AppState>,
320
    headers: HeaderMap,
321
    body: axum::Json<RoomIdBody>,
322
) -> Response {
323
    let (user, _) = match auth(&state, &headers).await {
324
        Ok(pair) => pair,
325
        Err(error) => return error.into_response(),
326
    };
327
    respond(
328
        dispatch(
329
            &user.handle,
330
            &user.mirror,
331
            Request::Open {
332
                room_id: body.0.room_id,
333
            },
334
        )
335
        .await,
336
    )
337
}
338
339
async fn rooms_close(
340
    State(state): State<AppState>,
341
    headers: HeaderMap,
342
    body: axum::Json<RoomIdBody>,
343
) -> Response {
344
    let (user, _) = match auth(&state, &headers).await {
345
        Ok(pair) => pair,
346
        Err(error) => return error.into_response(),
347
    };
348
    respond(
349
        dispatch(
350
            &user.handle,
351
            &user.mirror,
352
            Request::Close {
353
                room_id: body.0.room_id,
354
            },
355
        )
356
        .await,
357
    )
358
}
359
360
async fn rooms_send(
361
    State(state): State<AppState>,
362
    headers: HeaderMap,
363
    body: axum::Json<SendBody>,
364
) -> Response {
365
    let (user, _) = match auth(&state, &headers).await {
366
        Ok(pair) => pair,
367
        Err(error) => return error.into_response(),
368
    };
369
    respond(
370
        dispatch(
371
            &user.handle,
372
            &user.mirror,
373
            Request::Send {
374
                room_id: body.0.room_id,
375
                body: body.0.body,
376
            },
377
        )
378
        .await,
379
    )
380
}
381
382
async fn rooms_upload(
383
    State(state): State<AppState>,
384
    headers: HeaderMap,
385
    mut upload: Multipart,
386
) -> Response {
387
    let (user, _) = match auth(&state, &headers).await {
388
        Ok(pair) => pair,
389
        Err(error) => return error.into_response(),
390
    };
391
    let mut room_id = None;
392
    let mut file = None;
393
    loop {
394
        let field = match upload.next_field().await {
395
            Ok(Some(field)) => field,
396
            Ok(None) => break,
397
            Err(_) => return ApiError::bad_request("invalid upload form").into_response(),
398
        };
399
        match field.name() {
400
            Some("room_id") if room_id.is_none() => {
401
                room_id = match field.text().await {
402
                    Ok(value) => Some(value),
403
                    Err(_) => return ApiError::bad_request("invalid room_id").into_response(),
404
                };
405
            }
406
            Some("file") if file.is_none() => {
407
                let filename = field.file_name().unwrap_or("attachment").to_owned();
408
                let mimetype = field
409
                    .content_type()
410
                    .unwrap_or("application/octet-stream")
411
                    .to_owned();
412
                let bytes = match field.bytes().await {
413
                    Ok(bytes) => bytes,
414
                    Err(_) => return ApiError::bad_request("invalid file").into_response(),
415
                };
416
                if bytes.is_empty() || bytes.len() > 20 * 1024 * 1024 {
417
                    return ApiError::bad_request("file must be between 1 byte and 20 MiB")
418
                        .into_response();
419
                }
420
                file = Some((filename, mimetype, bytes));
421
            }
422
            _ => return ApiError::bad_request("unexpected upload field").into_response(),
423
        }
424
    }
425
    let Some(room_id) = room_id else {
426
        return ApiError::bad_request("room_id is required").into_response();
427
    };
428
    let Some((filename, mimetype, bytes)) = file else {
429
        return ApiError::bad_request("file is required").into_response();
430
    };
431
    respond(
432
        dispatch(
433
            &user.handle,
434
            &user.mirror,
435
            Request::upload(room_id, filename, mimetype, bytes.to_vec()),
436
        )
437
        .await,
438
    )
439
}
440
441
async fn rooms_paginate(
442
    State(state): State<AppState>,
443
    headers: HeaderMap,
444
    body: axum::Json<PaginateBody>,
445
) -> Response {
446
    let (user, _) = match auth(&state, &headers).await {
447
        Ok(pair) => pair,
448
        Err(error) => return error.into_response(),
449
    };
450
    respond(
451
        dispatch(
452
            &user.handle,
453
            &user.mirror,
454
            Request::Paginate {
455
                room_id: body.0.room_id,
456
            },
457
        )
458
        .await,
459
    )
460
}
461
462
async fn rooms_join(
463
    State(state): State<AppState>,
464
    headers: HeaderMap,
465
    body: axum::Json<JoinBody>,
466
) -> Response {
467
    let (user, _) = match auth(&state, &headers).await {
468
        Ok(pair) => pair,
469
        Err(error) => return error.into_response(),
470
    };
471
    respond(
472
        dispatch(
473
            &user.handle,
474
            &user.mirror,
475
            Request::Join { room: body.0.room },
476
        )
477
        .await,
478
    )
479
}
480
481
async fn rooms_create(
482
    State(state): State<AppState>,
483
    headers: HeaderMap,
484
    body: axum::Json<CreateBody>,
485
) -> Response {
486
    let (user, _) = match auth(&state, &headers).await {
487
        Ok(pair) => pair,
488
        Err(error) => return error.into_response(),
489
    };
490
    respond(
491
        dispatch(
492
            &user.handle,
493
            &user.mirror,
494
            Request::Create {
495
                name: body.0.name,
496
                topic: body.0.topic,
497
            },
498
        )
499
        .await,
500
    )
501
}
502
503
async fn rooms_leave(
504
    State(state): State<AppState>,
505
    headers: HeaderMap,
506
    body: axum::Json<RoomIdBody>,
507
) -> Response {
508
    let (user, _) = match auth(&state, &headers).await {
509
        Ok(pair) => pair,
510
        Err(error) => return error.into_response(),
511
    };
512
    respond(
513
        dispatch(
514
            &user.handle,
515
            &user.mirror,
516
            Request::Leave {
517
                room_id: body.0.room_id,
518
            },
519
        )
520
        .await,
521
    )
522
}
523
524
async fn rooms_react(
525
    State(state): State<AppState>,
526
    headers: HeaderMap,
527
    body: axum::Json<ReactBody>,
528
) -> Response {
529
    let (user, _) = match auth(&state, &headers).await {
530
        Ok(pair) => pair,
531
        Err(error) => return error.into_response(),
532
    };
533
    respond(
534
        dispatch(
535
            &user.handle,
536
            &user.mirror,
537
            Request::React {
538
                room_id: body.0.room_id,
539
                event_id: body.0.event_id,
540
                key: body.0.key,
541
            },
542
        )
543
        .await,
544
    )
545
}
546
547
async fn rooms_read(
548
    State(state): State<AppState>,
549
    headers: HeaderMap,
550
    body: axum::Json<ReadBody>,
551
) -> Response {
552
    let (user, _) = match auth(&state, &headers).await {
553
        Ok(pair) => pair,
554
        Err(error) => return error.into_response(),
555
    };
556
    respond(
557
        dispatch(
558
            &user.handle,
559
            &user.mirror,
560
            Request::Read {
561
                room_id: body.0.room_id,
562
                event_id: body.0.event_id,
563
            },
564
        )
565
        .await,
566
    )
567
}
568
569
async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> Response {
570
    let (user, _) = match auth(&state, &headers).await {
571
        Ok(pair) => pair,
572
        Err(error) => return error.into_response(),
573
    };
574
    respond(dispatch(&user.handle, &user.mirror, Request::Profile).await)
575
}
576
577
async fn get_space_children(
578
    State(state): State<AppState>,
579
    headers: HeaderMap,
580
    Query(query): Query<MembersQuery>,
581
) -> Response {
582
    let (user, _) = match auth(&state, &headers).await {
583
        Ok(pair) => pair,
584
        Err(error) => return error.into_response(),
585
    };
586
    respond(
587
        dispatch(
588
            &user.handle,
589
            &user.mirror,
590
            Request::SpaceChildren {
591
                room_id: query.room_id,
592
            },
593
        )
594
        .await,
595
    )
596
}
597
598
async fn get_media(
599
    State(state): State<AppState>,
600
    headers: HeaderMap,
601
    Query(query): Query<MediaQuery>,
602
) -> Response {
603
    let (user, _) = match auth(&state, &headers).await {
604
        Ok(pair) => pair,
605
        Err(error) => return error.into_response(),
606
    };
607
    respond(
608
        dispatch(
609
            &user.handle,
610
            &user.mirror,
611
            Request::Avatar { mxc: query.mxc },
612
        )
613
        .await,
614
    )
615
}
616
617
async fn get_message_media(
618
    State(state): State<AppState>,
619
    headers: HeaderMap,
620
    Query(query): Query<MessageMediaQuery>,
621
) -> Response {
622
    let (user, _) = match auth(&state, &headers).await {
623
        Ok(pair) => pair,
624
        Err(error) => return error.into_response(),
625
    };
626
    // the declared mimetype is the sender's claim, so the bytes get sniffed instead
627
    respond(
628
        dispatch(
629
            &user.handle,
630
            &user.mirror,
631
            Request::Media {
632
                media_id: query.media_id,
633
                size: query.size,
634
            },
635
        )
636
        .await,
637
    )
638
}
639
640
async fn get_encryption(State(state): State<AppState>, headers: HeaderMap) -> Response {
641
    let (user, _) = match auth(&state, &headers).await {
642
        Ok(pair) => pair,
643
        Err(error) => return error.into_response(),
644
    };
645
    respond(dispatch(&user.handle, &user.mirror, Request::Encryption).await)
646
}
647
648
async fn recover_encryption(
649
    State(state): State<AppState>,
650
    headers: HeaderMap,
651
    body: axum::Json<RecoverBody>,
652
) -> Response {
653
    let (user, _) = match auth(&state, &headers).await {
654
        Ok(pair) => pair,
655
        Err(error) => return error.into_response(),
656
    };
657
    respond(
658
        dispatch(
659
            &user.handle,
660
            &user.mirror,
661
            Request::Recover {
662
                recovery_key: body.0.recovery_key,
663
            },
664
        )
665
        .await,
666
    )
667
}
668
669
async fn start_verification(State(state): State<AppState>, headers: HeaderMap) -> Response {
670
    let (user, _) = match auth(&state, &headers).await {
671
        Ok(pair) => pair,
672
        Err(error) => return error.into_response(),
673
    };
674
    respond(dispatch(&user.handle, &user.mirror, Request::StartVerification).await)
675
}
676
677
async fn verify_encryption(
678
    State(state): State<AppState>,
679
    headers: HeaderMap,
680
    body: axum::Json<VerifyBody>,
681
) -> Response {
682
    let (user, _) = match auth(&state, &headers).await {
683
        Ok(pair) => pair,
684
        Err(error) => return error.into_response(),
685
    };
686
    respond(
687
        dispatch(
688
            &user.handle,
689
            &user.mirror,
690
            Request::Verify {
691
                action: body.0.action,
692
            },
693
        )
694
        .await,
695
    )
696
}
697
698
async fn rooms_typing(
699
    State(state): State<AppState>,
700
    headers: HeaderMap,
701
    body: axum::Json<TypingBody>,
702
) -> Response {
703
    let (user, _) = match auth(&state, &headers).await {
704
        Ok(pair) => pair,
705
        Err(error) => return error.into_response(),
706
    };
707
    respond(
708
        dispatch(
709
            &user.handle,
710
            &user.mirror,
711
            Request::Typing {
712
                room_id: body.0.room_id,
713
                typing: body.0.typing,
714
            },
715
        )
716
        .await,
717
    )
718
}
719
720
async fn get_account_data(
721
    State(state): State<AppState>,
722
    headers: HeaderMap,
723
    Query(query): Query<AccountDataQuery>,
724
) -> Response {
725
    let (user, _) = match auth(&state, &headers).await {
726
        Ok(pair) => pair,
727
        Err(error) => return error.into_response(),
728
    };
729
    respond(
730
        dispatch(
731
            &user.handle,
732
            &user.mirror,
733
            Request::GetAccountData {
734
                event_type: query.event_type,
735
            },
736
        )
737
        .await,
738
    )
739
}
740
741
async fn set_account_data(
742
    State(state): State<AppState>,
743
    headers: HeaderMap,
744
    body: axum::Json<AccountDataBody>,
745
) -> Response {
746
    let (user, _) = match auth(&state, &headers).await {
747
        Ok(pair) => pair,
748
        Err(error) => return error.into_response(),
749
    };
750
    let body = body.0;
751
    respond(
752
        dispatch(
753
            &user.handle,
754
            &user.mirror,
755
            Request::SetAccountData {
756
                event_type: body.event_type,
757
                content: body.content,
758
            },
759
        )
760
        .await,
761
    )
762
}
763
764
async fn get_events(State(state): State<AppState>, headers: HeaderMap) -> Response {
765
    let (user, _) = match auth(&state, &headers).await {
766
        Ok(pair) => pair,
767
        Err(error) => return error.into_response(),
768
    };
769
    let body = subscribe(&user.mirror).map(|event| {
770
        let json = serde_json::to_string(&event).unwrap_or_else(|_| "{\"type\":\"error\"}".into());
771
        Ok::<_, std::convert::Infallible>(SseEvent::default().data(json))
772
    });
773
    Sse::new(body).into_response()
774
}
775
776
async fn get_health() -> Response {
777
    json_ok(&serde_json::json!({ "status": "ok" }), None)
778
}
779
780
async fn static_shell() -> Response {
781
    (
782
        StatusCode::SERVICE_UNAVAILABLE,
783
        axum::Json(serde_json::json!({ "message": "frontend assets missing; run npm --prefix web run build or use the Nix package" })),
784
    )
785
        .into_response()
786
}
787
788
pub fn router(state: AppState, web_dir: Option<String>) -> Router {
789
    let api = Router::new()
790
        .route("/session", get(get_session))
791
        .route("/login", post(post_login))
792
        .route("/logout", post(post_logout))
793
        .route("/rooms", get(get_rooms))
794
        .route("/events", get(get_events))
795
        .route("/health", get(get_health))
796
        .route("/rooms/open", post(rooms_open))
797
        .route("/rooms/close", post(rooms_close))
798
        .route("/rooms/send", post(rooms_send))
799
        .route("/rooms/paginate", post(rooms_paginate))
800
        .route(
801
            "/rooms/upload",
802
            post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),
803
        )
804
        .route("/rooms/join", post(rooms_join))
805
        .route("/rooms/create", post(rooms_create))
806
        .route("/rooms/leave", post(rooms_leave))
807
        .route("/rooms/react", post(rooms_react))
808
        .route("/rooms/read", post(rooms_read))
809
        .route("/rooms/typing", post(rooms_typing))
810
        .route(
811
            "/account-data",
812
            get(get_account_data).post(set_account_data),
813
        )
814
        .route("/profile", get(get_profile))
815
        .route("/media", get(get_media))
816
        .route("/message-media", get(get_message_media))
817
        .route("/spaces/children", get(get_space_children))
818
        .route("/rooms/members", get(get_members))
819
        .route("/encryption", get(get_encryption))
820
        .route("/encryption/recover", post(recover_encryption))
821
        .route("/encryption/start", post(start_verification))
822
        .route("/encryption/verify", post(verify_encryption));
823
    let api = Router::new()
824
        .nest("/api", api)
825
        .layer(middleware::from_fn(csrf_guard));
826
    let router = api.with_state(state).layer(TraceLayer::new_for_http());
827
    match web_dir {
828
        Some(dir) => {
829
            let files =
830
                ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));
831
            router.fallback_service(files)
832
        }
833
        None => router.fallback(static_shell),
834
    }
835
}