atagen

malice

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