atagen

malice

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