atagen

malice

public
Star0
Watch0
973f8fbdaf80e2ef6679dd071233fe6b853344b2
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: crates/malice-server/src/routes.rs

973f8fbdaf80atagen2026-09-28 09:10 UTC1
use std::sync::Arc;
973f8fbdaf80atagen2026-09-28 09:10 UTC2
973f8fbdaf80atagen2026-09-28 09:10 UTC3
use axum::{
973f8fbdaf80atagen2026-09-28 09:10 UTC4
    Router,
973f8fbdaf80atagen2026-09-28 09:10 UTC5
    extract::{DefaultBodyLimit, Multipart, Query, State},
973f8fbdaf80atagen2026-09-28 09:10 UTC6
    http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header},
973f8fbdaf80atagen2026-09-28 09:10 UTC7
    middleware,
973f8fbdaf80atagen2026-09-28 09:10 UTC8
    middleware::Next,
973f8fbdaf80atagen2026-09-28 09:10 UTC9
    response::{IntoResponse, Response, Sse, sse::Event as SseEvent},
973f8fbdaf80atagen2026-09-28 09:10 UTC10
    routing::{any, get, post},
973f8fbdaf80atagen2026-09-28 09:10 UTC11
};
973f8fbdaf80atagen2026-09-28 09:10 UTC12
use futures_util::StreamExt;
973f8fbdaf80atagen2026-09-28 09:10 UTC13
use macltok::Retain;
973f8fbdaf80atagen2026-09-28 09:10 UTC14
use malice_core::{
973f8fbdaf80atagen2026-09-28 09:10 UTC15
    feed::subscribe,
973f8fbdaf80atagen2026-09-28 09:10 UTC16
    login::{LoginInput, sanitise_auth_error},
973f8fbdaf80atagen2026-09-28 09:10 UTC17
    request::{Reply, Request, dispatch},
973f8fbdaf80atagen2026-09-28 09:10 UTC18
};
973f8fbdaf80atagen2026-09-28 09:10 UTC19
use serde::{Deserialize, Serialize};
973f8fbdaf80atagen2026-09-28 09:10 UTC20
use tower_http::{
973f8fbdaf80atagen2026-09-28 09:10 UTC21
    services::{ServeDir, ServeFile},
973f8fbdaf80atagen2026-09-28 09:10 UTC22
    trace::TraceLayer,
973f8fbdaf80atagen2026-09-28 09:10 UTC23
};
973f8fbdaf80atagen2026-09-28 09:10 UTC24
973f8fbdaf80atagen2026-09-28 09:10 UTC25
use crate::{
973f8fbdaf80atagen2026-09-28 09:10 UTC26
    csrf::{csrf_guard, session_sid, set_cookie_value},
973f8fbdaf80atagen2026-09-28 09:10 UTC27
    error::{ApiError, json_ok, no_content},
973f8fbdaf80atagen2026-09-28 09:10 UTC28
    state::{AppState, RestoreError, UserSession},
973f8fbdaf80atagen2026-09-28 09:10 UTC29
};
973f8fbdaf80atagen2026-09-28 09:10 UTC30
973f8fbdaf80atagen2026-09-28 09:10 UTC31
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC32
struct RoomIdBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC33
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC34
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC35
}
973f8fbdaf80atagen2026-09-28 09:10 UTC36
973f8fbdaf80atagen2026-09-28 09:10 UTC37
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC38
struct SendBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC39
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC40
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC41
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC42
    body: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC43
}
973f8fbdaf80atagen2026-09-28 09:10 UTC44
973f8fbdaf80atagen2026-09-28 09:10 UTC45
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC46
struct PaginateBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC47
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC48
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC49
}
973f8fbdaf80atagen2026-09-28 09:10 UTC50
973f8fbdaf80atagen2026-09-28 09:10 UTC51
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC52
struct JoinBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC53
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC54
    room: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC55
}
973f8fbdaf80atagen2026-09-28 09:10 UTC56
973f8fbdaf80atagen2026-09-28 09:10 UTC57
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC58
struct CreateBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC59
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC60
    name: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC61
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC62
    topic: Option<String>,
973f8fbdaf80atagen2026-09-28 09:10 UTC63
}
973f8fbdaf80atagen2026-09-28 09:10 UTC64
973f8fbdaf80atagen2026-09-28 09:10 UTC65
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC66
struct ReactBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC67
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC68
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC69
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC70
    event_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC71
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC72
    key: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC73
}
973f8fbdaf80atagen2026-09-28 09:10 UTC74
973f8fbdaf80atagen2026-09-28 09:10 UTC75
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC76
struct ReadBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC77
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC78
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC79
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC80
    event_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC81
}
973f8fbdaf80atagen2026-09-28 09:10 UTC82
973f8fbdaf80atagen2026-09-28 09:10 UTC83
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC84
struct MembersQuery {
973f8fbdaf80atagen2026-09-28 09:10 UTC85
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC86
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC87
}
973f8fbdaf80atagen2026-09-28 09:10 UTC88
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC89
struct RecoverBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC90
    recovery_key: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC91
}
973f8fbdaf80atagen2026-09-28 09:10 UTC92
973f8fbdaf80atagen2026-09-28 09:10 UTC93
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC94
struct VerifyBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC95
    action: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC96
}
973f8fbdaf80atagen2026-09-28 09:10 UTC97
973f8fbdaf80atagen2026-09-28 09:10 UTC98
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC99
struct MediaQuery {
973f8fbdaf80atagen2026-09-28 09:10 UTC100
    mxc: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC101
}
973f8fbdaf80atagen2026-09-28 09:10 UTC102
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC103
struct MessageMediaQuery {
973f8fbdaf80atagen2026-09-28 09:10 UTC104
    media_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC105
    size: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC106
}
973f8fbdaf80atagen2026-09-28 09:10 UTC107
973f8fbdaf80atagen2026-09-28 09:10 UTC108
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC109
struct TypingBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC110
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC111
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC112
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC113
    typing: bool,
973f8fbdaf80atagen2026-09-28 09:10 UTC114
}
973f8fbdaf80atagen2026-09-28 09:10 UTC115
973f8fbdaf80atagen2026-09-28 09:10 UTC116
#[derive(Debug, Serialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC117
struct SoftLogoutInfo {
973f8fbdaf80atagen2026-09-28 09:10 UTC118
    user_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC119
    homeserver: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC120
}
973f8fbdaf80atagen2026-09-28 09:10 UTC121
973f8fbdaf80atagen2026-09-28 09:10 UTC122
#[derive(Debug, Serialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC123
struct SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC124
    user_id: Option<String>,
973f8fbdaf80atagen2026-09-28 09:10 UTC125
    #[serde(skip_serializing_if = "Option::is_none")]
973f8fbdaf80atagen2026-09-28 09:10 UTC126
    soft_logout: Option<SoftLogoutInfo>,
973f8fbdaf80atagen2026-09-28 09:10 UTC127
}
973f8fbdaf80atagen2026-09-28 09:10 UTC128
973f8fbdaf80atagen2026-09-28 09:10 UTC129
async fn auth(
973f8fbdaf80atagen2026-09-28 09:10 UTC130
    state: &AppState,
973f8fbdaf80atagen2026-09-28 09:10 UTC131
    headers: &HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC132
) -> Result<(Arc<UserSession>, String), ApiError> {
973f8fbdaf80atagen2026-09-28 09:10 UTC133
    let Some(sid) = session_sid(headers) else {
973f8fbdaf80atagen2026-09-28 09:10 UTC134
        return Err(ApiError::unauthorized("not signed in"));
973f8fbdaf80atagen2026-09-28 09:10 UTC135
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC136
    match state.get_or_restore(&sid).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC137
        Ok(user) => Ok((user, sid)),
973f8fbdaf80atagen2026-09-28 09:10 UTC138
        Err(RestoreError::NotFound) => Err(ApiError::unauthorized_stale(
973f8fbdaf80atagen2026-09-28 09:10 UTC139
            "session expired; sign in again",
973f8fbdaf80atagen2026-09-28 09:10 UTC140
            state.cookie_secure(),
973f8fbdaf80atagen2026-09-28 09:10 UTC141
        )),
973f8fbdaf80atagen2026-09-28 09:10 UTC142
        Err(RestoreError::SoftLoggedOut { .. }) => Err(ApiError::unauthorized(
973f8fbdaf80atagen2026-09-28 09:10 UTC143
            "signed out by the server, sign in again",
973f8fbdaf80atagen2026-09-28 09:10 UTC144
        )),
973f8fbdaf80atagen2026-09-28 09:10 UTC145
        Err(RestoreError::Stale(message)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC146
            state.remove_session(&sid, Retain::Nothing).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC147
            Err(ApiError::unauthorized_stale(message, state.cookie_secure()))
973f8fbdaf80atagen2026-09-28 09:10 UTC148
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC149
        Err(RestoreError::Unavailable(message)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC150
            Err(ApiError::new(StatusCode::BAD_GATEWAY, message))
973f8fbdaf80atagen2026-09-28 09:10 UTC151
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC152
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC153
}
973f8fbdaf80atagen2026-09-28 09:10 UTC154
973f8fbdaf80atagen2026-09-28 09:10 UTC155
/// commands answer when they're done, so the browser gets the outcome in the response
973f8fbdaf80atagen2026-09-28 09:10 UTC156
fn respond(result: Result<Reply, malice_core::error::CoreError>) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC157
    match result {
973f8fbdaf80atagen2026-09-28 09:10 UTC158
        Ok(Reply::Done) => no_content(None),
973f8fbdaf80atagen2026-09-28 09:10 UTC159
        Ok(Reply::Json(value)) => json_ok(&value, None),
973f8fbdaf80atagen2026-09-28 09:10 UTC160
        Ok(Reply::Avatar {
973f8fbdaf80atagen2026-09-28 09:10 UTC161
            bytes,
973f8fbdaf80atagen2026-09-28 09:10 UTC162
            content_type,
973f8fbdaf80atagen2026-09-28 09:10 UTC163
        }) => (
973f8fbdaf80atagen2026-09-28 09:10 UTC164
            [
973f8fbdaf80atagen2026-09-28 09:10 UTC165
                (axum::http::header::CONTENT_TYPE, content_type),
973f8fbdaf80atagen2026-09-28 09:10 UTC166
                (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),
973f8fbdaf80atagen2026-09-28 09:10 UTC167
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
973f8fbdaf80atagen2026-09-28 09:10 UTC168
            ],
973f8fbdaf80atagen2026-09-28 09:10 UTC169
            bytes,
973f8fbdaf80atagen2026-09-28 09:10 UTC170
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC171
            .into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC172
        Ok(Reply::Attachment {
973f8fbdaf80atagen2026-09-28 09:10 UTC173
            bytes,
973f8fbdaf80atagen2026-09-28 09:10 UTC174
            content_type,
973f8fbdaf80atagen2026-09-28 09:10 UTC175
        }) => (
973f8fbdaf80atagen2026-09-28 09:10 UTC176
            [
973f8fbdaf80atagen2026-09-28 09:10 UTC177
                (
973f8fbdaf80atagen2026-09-28 09:10 UTC178
                    axum::http::header::CONTENT_TYPE,
973f8fbdaf80atagen2026-09-28 09:10 UTC179
                    content_type.unwrap_or("application/octet-stream"),
973f8fbdaf80atagen2026-09-28 09:10 UTC180
                ),
973f8fbdaf80atagen2026-09-28 09:10 UTC181
                (
973f8fbdaf80atagen2026-09-28 09:10 UTC182
                    axum::http::header::CONTENT_DISPOSITION,
973f8fbdaf80atagen2026-09-28 09:10 UTC183
                    if content_type.is_some() {
973f8fbdaf80atagen2026-09-28 09:10 UTC184
                        "inline"
973f8fbdaf80atagen2026-09-28 09:10 UTC185
                    } else {
973f8fbdaf80atagen2026-09-28 09:10 UTC186
                        "attachment"
973f8fbdaf80atagen2026-09-28 09:10 UTC187
                    },
973f8fbdaf80atagen2026-09-28 09:10 UTC188
                ),
973f8fbdaf80atagen2026-09-28 09:10 UTC189
                (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),
973f8fbdaf80atagen2026-09-28 09:10 UTC190
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
973f8fbdaf80atagen2026-09-28 09:10 UTC191
                (axum::http::header::CONTENT_SECURITY_POLICY, "sandbox"),
973f8fbdaf80atagen2026-09-28 09:10 UTC192
            ],
973f8fbdaf80atagen2026-09-28 09:10 UTC193
            bytes,
973f8fbdaf80atagen2026-09-28 09:10 UTC194
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC195
            .into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC196
        Err(error) => ApiError::from(error).into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC197
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC198
}
973f8fbdaf80atagen2026-09-28 09:10 UTC199
973f8fbdaf80atagen2026-09-28 09:10 UTC200
async fn get_session(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC201
    let Some(sid) = session_sid(&headers) else {
973f8fbdaf80atagen2026-09-28 09:10 UTC202
        return json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC203
            &SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC204
                user_id: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC205
                soft_logout: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC206
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC207
            None,
973f8fbdaf80atagen2026-09-28 09:10 UTC208
        );
973f8fbdaf80atagen2026-09-28 09:10 UTC209
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC210
    match state.get_or_restore(&sid).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC211
        Ok(user) => json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC212
            &SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC213
                user_id: Some(user.user_id.clone()),
973f8fbdaf80atagen2026-09-28 09:10 UTC214
                soft_logout: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC215
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC216
            None,
973f8fbdaf80atagen2026-09-28 09:10 UTC217
        ),
973f8fbdaf80atagen2026-09-28 09:10 UTC218
        Err(RestoreError::NotFound) => json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC219
            &SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC220
                user_id: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC221
                soft_logout: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC222
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC223
            None,
973f8fbdaf80atagen2026-09-28 09:10 UTC224
        ),
973f8fbdaf80atagen2026-09-28 09:10 UTC225
        Err(RestoreError::SoftLoggedOut {
973f8fbdaf80atagen2026-09-28 09:10 UTC226
            user_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC227
            homeserver,
973f8fbdaf80atagen2026-09-28 09:10 UTC228
        }) => json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC229
            &SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC230
                user_id: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC231
                soft_logout: Some(SoftLogoutInfo {
973f8fbdaf80atagen2026-09-28 09:10 UTC232
                    user_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC233
                    homeserver,
973f8fbdaf80atagen2026-09-28 09:10 UTC234
                }),
973f8fbdaf80atagen2026-09-28 09:10 UTC235
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC236
            None,
973f8fbdaf80atagen2026-09-28 09:10 UTC237
        ),
973f8fbdaf80atagen2026-09-28 09:10 UTC238
        Err(RestoreError::Stale(message)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC239
            state.remove_session(&sid, Retain::Nothing).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC240
            ApiError::unauthorized_stale(message, state.cookie_secure()).into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC241
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC242
        Err(RestoreError::Unavailable(message)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC243
            ApiError::new(StatusCode::BAD_GATEWAY, message).into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC244
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC245
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC246
}
973f8fbdaf80atagen2026-09-28 09:10 UTC247
973f8fbdaf80atagen2026-09-28 09:10 UTC248
async fn post_login(
973f8fbdaf80atagen2026-09-28 09:10 UTC249
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC250
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC251
    body: axum::Json<LoginInput>,
973f8fbdaf80atagen2026-09-28 09:10 UTC252
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC253
    let input = match body.0.validate(state.allow_http_homeserver()) {
973f8fbdaf80atagen2026-09-28 09:10 UTC254
        Ok(input) => input,
973f8fbdaf80atagen2026-09-28 09:10 UTC255
        Err(error) => return ApiError::from(error).into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC256
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC257
    let cookie_sid = session_sid(&headers);
973f8fbdaf80atagen2026-09-28 09:10 UTC258
    let password = input.password.clone();
973f8fbdaf80atagen2026-09-28 09:10 UTC259
    match state
973f8fbdaf80atagen2026-09-28 09:10 UTC260
        .login(
973f8fbdaf80atagen2026-09-28 09:10 UTC261
            cookie_sid.as_deref(),
973f8fbdaf80atagen2026-09-28 09:10 UTC262
            input.homeserver,
973f8fbdaf80atagen2026-09-28 09:10 UTC263
            input.username,
973f8fbdaf80atagen2026-09-28 09:10 UTC264
            input.password,
973f8fbdaf80atagen2026-09-28 09:10 UTC265
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC266
        .await
973f8fbdaf80atagen2026-09-28 09:10 UTC267
    {
973f8fbdaf80atagen2026-09-28 09:10 UTC268
        Ok(user) => json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC269
            &serde_json::json!({ "user_id": user.user_id }),
973f8fbdaf80atagen2026-09-28 09:10 UTC270
            Some(set_cookie_value(&user.sid, state.cookie_secure())),
973f8fbdaf80atagen2026-09-28 09:10 UTC271
        ),
973f8fbdaf80atagen2026-09-28 09:10 UTC272
        Err(message) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC273
            let status = if message.contains("starting sync engine")
973f8fbdaf80atagen2026-09-28 09:10 UTC274
                || message.contains("could not save the session")
973f8fbdaf80atagen2026-09-28 09:10 UTC275
                || message.contains("another engine is using this session")
973f8fbdaf80atagen2026-09-28 09:10 UTC276
            {
973f8fbdaf80atagen2026-09-28 09:10 UTC277
                StatusCode::BAD_GATEWAY
973f8fbdaf80atagen2026-09-28 09:10 UTC278
            } else {
973f8fbdaf80atagen2026-09-28 09:10 UTC279
                StatusCode::UNAUTHORIZED
973f8fbdaf80atagen2026-09-28 09:10 UTC280
            };
973f8fbdaf80atagen2026-09-28 09:10 UTC281
            ApiError::new(status, sanitise_auth_error(&message, &password)).into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC282
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC283
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC284
}
973f8fbdaf80atagen2026-09-28 09:10 UTC285
973f8fbdaf80atagen2026-09-28 09:10 UTC286
async fn post_logout(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC287
    if let Some(sid) = session_sid(&headers) {
973f8fbdaf80atagen2026-09-28 09:10 UTC288
        state.logout(&sid).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC289
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC290
    no_content(Some(crate::csrf::clear_cookie_value(state.cookie_secure())))
973f8fbdaf80atagen2026-09-28 09:10 UTC291
}
973f8fbdaf80atagen2026-09-28 09:10 UTC292
973f8fbdaf80atagen2026-09-28 09:10 UTC293
async fn get_rooms(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC294
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC295
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC296
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC297
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC298
    respond(dispatch(&user.handle, &user.mirror, Request::Rooms).await)
973f8fbdaf80atagen2026-09-28 09:10 UTC299
}
973f8fbdaf80atagen2026-09-28 09:10 UTC300
973f8fbdaf80atagen2026-09-28 09:10 UTC301
async fn get_members(
973f8fbdaf80atagen2026-09-28 09:10 UTC302
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC303
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC304
    Query(query): Query<MembersQuery>,
973f8fbdaf80atagen2026-09-28 09:10 UTC305
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC306
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC307
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC308
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC309
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC310
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC311
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC312
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC313
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC314
            Request::Members {
973f8fbdaf80atagen2026-09-28 09:10 UTC315
                room_id: query.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC316
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC317
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC318
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC319
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC320
}
973f8fbdaf80atagen2026-09-28 09:10 UTC321
973f8fbdaf80atagen2026-09-28 09:10 UTC322
async fn rooms_open(
973f8fbdaf80atagen2026-09-28 09:10 UTC323
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC324
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC325
    body: axum::Json<RoomIdBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC326
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC327
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC328
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC329
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC330
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC331
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC332
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC333
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC334
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC335
            Request::Open {
973f8fbdaf80atagen2026-09-28 09:10 UTC336
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC337
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC338
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC339
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC340
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC341
}
973f8fbdaf80atagen2026-09-28 09:10 UTC342
973f8fbdaf80atagen2026-09-28 09:10 UTC343
async fn rooms_close(
973f8fbdaf80atagen2026-09-28 09:10 UTC344
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC345
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC346
    body: axum::Json<RoomIdBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC347
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC348
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC349
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC350
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC351
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC352
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC353
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC354
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC355
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC356
            Request::Close {
973f8fbdaf80atagen2026-09-28 09:10 UTC357
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC358
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC359
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC360
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC361
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC362
}
973f8fbdaf80atagen2026-09-28 09:10 UTC363
973f8fbdaf80atagen2026-09-28 09:10 UTC364
async fn rooms_send(
973f8fbdaf80atagen2026-09-28 09:10 UTC365
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC366
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC367
    body: axum::Json<SendBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC368
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC369
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC370
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC371
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC372
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC373
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC374
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC375
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC376
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC377
            Request::Send {
973f8fbdaf80atagen2026-09-28 09:10 UTC378
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC379
                body: body.0.body,
973f8fbdaf80atagen2026-09-28 09:10 UTC380
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC381
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC382
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC383
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC384
}
973f8fbdaf80atagen2026-09-28 09:10 UTC385
973f8fbdaf80atagen2026-09-28 09:10 UTC386
async fn rooms_upload(
973f8fbdaf80atagen2026-09-28 09:10 UTC387
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC388
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC389
    mut upload: Multipart,
973f8fbdaf80atagen2026-09-28 09:10 UTC390
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC391
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC392
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC393
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC394
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC395
    let mut room_id = None;
973f8fbdaf80atagen2026-09-28 09:10 UTC396
    let mut file = None;
973f8fbdaf80atagen2026-09-28 09:10 UTC397
    loop {
973f8fbdaf80atagen2026-09-28 09:10 UTC398
        let field = match upload.next_field().await {
973f8fbdaf80atagen2026-09-28 09:10 UTC399
            Ok(Some(field)) => field,
973f8fbdaf80atagen2026-09-28 09:10 UTC400
            Ok(None) => break,
973f8fbdaf80atagen2026-09-28 09:10 UTC401
            Err(_) => return ApiError::bad_request("invalid upload form").into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC402
        };
973f8fbdaf80atagen2026-09-28 09:10 UTC403
        match field.name() {
973f8fbdaf80atagen2026-09-28 09:10 UTC404
            Some("room_id") if room_id.is_none() => {
973f8fbdaf80atagen2026-09-28 09:10 UTC405
                room_id = match field.text().await {
973f8fbdaf80atagen2026-09-28 09:10 UTC406
                    Ok(value) => Some(value),
973f8fbdaf80atagen2026-09-28 09:10 UTC407
                    Err(_) => return ApiError::bad_request("invalid room_id").into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC408
                };
973f8fbdaf80atagen2026-09-28 09:10 UTC409
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC410
            Some("file") if file.is_none() => {
973f8fbdaf80atagen2026-09-28 09:10 UTC411
                let filename = field.file_name().unwrap_or("attachment").to_owned();
973f8fbdaf80atagen2026-09-28 09:10 UTC412
                let mimetype = field
973f8fbdaf80atagen2026-09-28 09:10 UTC413
                    .content_type()
973f8fbdaf80atagen2026-09-28 09:10 UTC414
                    .unwrap_or("application/octet-stream")
973f8fbdaf80atagen2026-09-28 09:10 UTC415
                    .to_owned();
973f8fbdaf80atagen2026-09-28 09:10 UTC416
                let bytes = match field.bytes().await {
973f8fbdaf80atagen2026-09-28 09:10 UTC417
                    Ok(bytes) => bytes,
973f8fbdaf80atagen2026-09-28 09:10 UTC418
                    Err(_) => return ApiError::bad_request("invalid file").into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC419
                };
973f8fbdaf80atagen2026-09-28 09:10 UTC420
                if bytes.is_empty() || bytes.len() > 20 * 1024 * 1024 {
973f8fbdaf80atagen2026-09-28 09:10 UTC421
                    return ApiError::bad_request("file must be between 1 byte and 20 MiB")
973f8fbdaf80atagen2026-09-28 09:10 UTC422
                        .into_response();
973f8fbdaf80atagen2026-09-28 09:10 UTC423
                }
973f8fbdaf80atagen2026-09-28 09:10 UTC424
                file = Some((filename, mimetype, bytes));
973f8fbdaf80atagen2026-09-28 09:10 UTC425
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC426
            _ => return ApiError::bad_request("unexpected upload field").into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC427
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC428
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC429
    let Some(room_id) = room_id else {
973f8fbdaf80atagen2026-09-28 09:10 UTC430
        return ApiError::bad_request("room_id is required").into_response();
973f8fbdaf80atagen2026-09-28 09:10 UTC431
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC432
    let Some((filename, mimetype, bytes)) = file else {
973f8fbdaf80atagen2026-09-28 09:10 UTC433
        return ApiError::bad_request("file is required").into_response();
973f8fbdaf80atagen2026-09-28 09:10 UTC434
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC435
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC436
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC437
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC438
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC439
            Request::upload(room_id, filename, mimetype, bytes.to_vec()),
973f8fbdaf80atagen2026-09-28 09:10 UTC440
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC441
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC442
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC443
}
973f8fbdaf80atagen2026-09-28 09:10 UTC444
973f8fbdaf80atagen2026-09-28 09:10 UTC445
async fn rooms_paginate(
973f8fbdaf80atagen2026-09-28 09:10 UTC446
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC447
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC448
    body: axum::Json<PaginateBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC449
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC450
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC451
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC452
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC453
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC454
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC455
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC456
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC457
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC458
            Request::Paginate {
973f8fbdaf80atagen2026-09-28 09:10 UTC459
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC460
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC461
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC462
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC463
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC464
}
973f8fbdaf80atagen2026-09-28 09:10 UTC465
973f8fbdaf80atagen2026-09-28 09:10 UTC466
async fn rooms_join(
973f8fbdaf80atagen2026-09-28 09:10 UTC467
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC468
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC469
    body: axum::Json<JoinBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC470
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC471
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC472
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC473
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC474
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC475
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC476
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC477
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC478
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC479
            Request::Join { room: body.0.room },
973f8fbdaf80atagen2026-09-28 09:10 UTC480
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC481
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC482
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC483
}
973f8fbdaf80atagen2026-09-28 09:10 UTC484
973f8fbdaf80atagen2026-09-28 09:10 UTC485
async fn rooms_create(
973f8fbdaf80atagen2026-09-28 09:10 UTC486
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC487
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC488
    body: axum::Json<CreateBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC489
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC490
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC491
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC492
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC493
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC494
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC495
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC496
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC497
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC498
            Request::Create {
973f8fbdaf80atagen2026-09-28 09:10 UTC499
                name: body.0.name,
973f8fbdaf80atagen2026-09-28 09:10 UTC500
                topic: body.0.topic,
973f8fbdaf80atagen2026-09-28 09:10 UTC501
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC502
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC503
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC504
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC505
}
973f8fbdaf80atagen2026-09-28 09:10 UTC506
973f8fbdaf80atagen2026-09-28 09:10 UTC507
async fn rooms_leave(
973f8fbdaf80atagen2026-09-28 09:10 UTC508
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC509
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC510
    body: axum::Json<RoomIdBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC511
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC512
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC513
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC514
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC515
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC516
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC517
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC518
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC519
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC520
            Request::Leave {
973f8fbdaf80atagen2026-09-28 09:10 UTC521
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC522
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC523
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC524
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC525
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC526
}
973f8fbdaf80atagen2026-09-28 09:10 UTC527
973f8fbdaf80atagen2026-09-28 09:10 UTC528
async fn rooms_react(
973f8fbdaf80atagen2026-09-28 09:10 UTC529
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC530
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC531
    body: axum::Json<ReactBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC532
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC533
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC534
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC535
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC536
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC537
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC538
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC539
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC540
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC541
            Request::React {
973f8fbdaf80atagen2026-09-28 09:10 UTC542
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC543
                event_id: body.0.event_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC544
                key: body.0.key,
973f8fbdaf80atagen2026-09-28 09:10 UTC545
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC546
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC547
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC548
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC549
}
973f8fbdaf80atagen2026-09-28 09:10 UTC550
973f8fbdaf80atagen2026-09-28 09:10 UTC551
async fn rooms_read(
973f8fbdaf80atagen2026-09-28 09:10 UTC552
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC553
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC554
    body: axum::Json<ReadBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC555
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC556
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC557
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC558
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC559
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC560
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC561
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC562
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC563
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC564
            Request::Read {
973f8fbdaf80atagen2026-09-28 09:10 UTC565
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC566
                event_id: body.0.event_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC567
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC568
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC569
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC570
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC571
}
973f8fbdaf80atagen2026-09-28 09:10 UTC572
973f8fbdaf80atagen2026-09-28 09:10 UTC573
async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC574
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC575
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC576
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC577
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC578
    respond(dispatch(&user.handle, &user.mirror, Request::Profile).await)
973f8fbdaf80atagen2026-09-28 09:10 UTC579
}
973f8fbdaf80atagen2026-09-28 09:10 UTC580
973f8fbdaf80atagen2026-09-28 09:10 UTC581
async fn get_space_children(
973f8fbdaf80atagen2026-09-28 09:10 UTC582
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC583
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC584
    Query(query): Query<MembersQuery>,
973f8fbdaf80atagen2026-09-28 09:10 UTC585
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC586
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC587
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC588
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC589
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC590
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC591
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC592
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC593
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC594
            Request::SpaceChildren {
973f8fbdaf80atagen2026-09-28 09:10 UTC595
                room_id: query.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC596
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC597
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC598
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC599
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC600
}
973f8fbdaf80atagen2026-09-28 09:10 UTC601
973f8fbdaf80atagen2026-09-28 09:10 UTC602
async fn get_media(
973f8fbdaf80atagen2026-09-28 09:10 UTC603
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC604
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC605
    Query(query): Query<MediaQuery>,
973f8fbdaf80atagen2026-09-28 09:10 UTC606
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC607
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC608
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC609
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC610
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC611
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC612
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC613
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC614
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC615
            Request::Avatar { mxc: query.mxc },
973f8fbdaf80atagen2026-09-28 09:10 UTC616
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC617
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC618
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC619
}
973f8fbdaf80atagen2026-09-28 09:10 UTC620
973f8fbdaf80atagen2026-09-28 09:10 UTC621
async fn get_message_media(
973f8fbdaf80atagen2026-09-28 09:10 UTC622
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC623
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC624
    Query(query): Query<MessageMediaQuery>,
973f8fbdaf80atagen2026-09-28 09:10 UTC625
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC626
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC627
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC628
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC629
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC630
    // the declared mimetype is the sender's claim, so the bytes get sniffed instead
973f8fbdaf80atagen2026-09-28 09:10 UTC631
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC632
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC633
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC634
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC635
            Request::Media {
973f8fbdaf80atagen2026-09-28 09:10 UTC636
                media_id: query.media_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC637
                size: query.size,
973f8fbdaf80atagen2026-09-28 09:10 UTC638
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC639
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC640
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC641
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC642
}
973f8fbdaf80atagen2026-09-28 09:10 UTC643
973f8fbdaf80atagen2026-09-28 09:10 UTC644
async fn get_encryption(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC645
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC646
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC647
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC648
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC649
    respond(dispatch(&user.handle, &user.mirror, Request::Encryption).await)
973f8fbdaf80atagen2026-09-28 09:10 UTC650
}
973f8fbdaf80atagen2026-09-28 09:10 UTC651
973f8fbdaf80atagen2026-09-28 09:10 UTC652
async fn recover_encryption(
973f8fbdaf80atagen2026-09-28 09:10 UTC653
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC654
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC655
    body: axum::Json<RecoverBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC656
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC657
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC658
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC659
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC660
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC661
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC662
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC663
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC664
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC665
            Request::Recover {
973f8fbdaf80atagen2026-09-28 09:10 UTC666
                recovery_key: body.0.recovery_key,
973f8fbdaf80atagen2026-09-28 09:10 UTC667
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC668
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC669
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC670
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC671
}
973f8fbdaf80atagen2026-09-28 09:10 UTC672
973f8fbdaf80atagen2026-09-28 09:10 UTC673
async fn start_verification(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC674
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC675
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC676
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC677
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC678
    respond(dispatch(&user.handle, &user.mirror, Request::StartVerification).await)
973f8fbdaf80atagen2026-09-28 09:10 UTC679
}
973f8fbdaf80atagen2026-09-28 09:10 UTC680
973f8fbdaf80atagen2026-09-28 09:10 UTC681
async fn verify_encryption(
973f8fbdaf80atagen2026-09-28 09:10 UTC682
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC683
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC684
    body: axum::Json<VerifyBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC685
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC686
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC687
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC688
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC689
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC690
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC691
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC692
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC693
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC694
            Request::Verify {
973f8fbdaf80atagen2026-09-28 09:10 UTC695
                action: body.0.action,
973f8fbdaf80atagen2026-09-28 09:10 UTC696
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC697
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC698
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC699
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC700
}
973f8fbdaf80atagen2026-09-28 09:10 UTC701
973f8fbdaf80atagen2026-09-28 09:10 UTC702
async fn rooms_typing(
973f8fbdaf80atagen2026-09-28 09:10 UTC703
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC704
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC705
    body: axum::Json<TypingBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC706
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC707
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC708
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC709
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC710
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC711
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC712
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC713
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC714
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC715
            Request::Typing {
973f8fbdaf80atagen2026-09-28 09:10 UTC716
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC717
                typing: body.0.typing,
973f8fbdaf80atagen2026-09-28 09:10 UTC718
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC719
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC720
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC721
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC722
}
973f8fbdaf80atagen2026-09-28 09:10 UTC723
973f8fbdaf80atagen2026-09-28 09:10 UTC724
async fn get_events(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC725
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC726
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC727
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC728
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC729
    let body = subscribe(&user.mirror).map(|event| {
973f8fbdaf80atagen2026-09-28 09:10 UTC730
        let json = serde_json::to_string(&event).unwrap_or_else(|_| "{\"type\":\"error\"}".into());
973f8fbdaf80atagen2026-09-28 09:10 UTC731
        Ok::<_, std::convert::Infallible>(SseEvent::default().data(json))
973f8fbdaf80atagen2026-09-28 09:10 UTC732
    });
973f8fbdaf80atagen2026-09-28 09:10 UTC733
    Sse::new(body).into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC734
}
973f8fbdaf80atagen2026-09-28 09:10 UTC735
973f8fbdaf80atagen2026-09-28 09:10 UTC736
async fn get_health() -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC737
    json_ok(&serde_json::json!({ "status": "ok" }), None)
973f8fbdaf80atagen2026-09-28 09:10 UTC738
}
973f8fbdaf80atagen2026-09-28 09:10 UTC739
973f8fbdaf80atagen2026-09-28 09:10 UTC740
async fn static_shell() -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC741
    (
973f8fbdaf80atagen2026-09-28 09:10 UTC742
        StatusCode::SERVICE_UNAVAILABLE,
973f8fbdaf80atagen2026-09-28 09:10 UTC743
        axum::Json(serde_json::json!({ "message": "frontend assets missing; run npm --prefix web run build or use the Nix package" })),
973f8fbdaf80atagen2026-09-28 09:10 UTC744
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC745
        .into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC746
}
973f8fbdaf80atagen2026-09-28 09:10 UTC747
973f8fbdaf80atagen2026-09-28 09:10 UTC748
pub fn router(state: AppState, web_dir: Option<String>) -> Router {
973f8fbdaf80atagen2026-09-28 09:10 UTC749
    let api = Router::new()
973f8fbdaf80atagen2026-09-28 09:10 UTC750
        .route("/session", get(get_session))
973f8fbdaf80atagen2026-09-28 09:10 UTC751
        .route("/login", post(post_login))
973f8fbdaf80atagen2026-09-28 09:10 UTC752
        .route("/logout", post(post_logout))
973f8fbdaf80atagen2026-09-28 09:10 UTC753
        .route("/rooms", get(get_rooms))
973f8fbdaf80atagen2026-09-28 09:10 UTC754
        .route("/events", get(get_events))
973f8fbdaf80atagen2026-09-28 09:10 UTC755
        .route("/health", get(get_health))
973f8fbdaf80atagen2026-09-28 09:10 UTC756
        .route("/rooms/open", post(rooms_open))
973f8fbdaf80atagen2026-09-28 09:10 UTC757
        .route("/rooms/close", post(rooms_close))
973f8fbdaf80atagen2026-09-28 09:10 UTC758
        .route("/rooms/send", post(rooms_send))
973f8fbdaf80atagen2026-09-28 09:10 UTC759
        .route("/rooms/paginate", post(rooms_paginate))
973f8fbdaf80atagen2026-09-28 09:10 UTC760
        .route(
973f8fbdaf80atagen2026-09-28 09:10 UTC761
            "/rooms/upload",
973f8fbdaf80atagen2026-09-28 09:10 UTC762
            post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),
973f8fbdaf80atagen2026-09-28 09:10 UTC763
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC764
        .route("/rooms/join", post(rooms_join))
973f8fbdaf80atagen2026-09-28 09:10 UTC765
        .route("/rooms/create", post(rooms_create))
973f8fbdaf80atagen2026-09-28 09:10 UTC766
        .route("/rooms/leave", post(rooms_leave))
973f8fbdaf80atagen2026-09-28 09:10 UTC767
        .route("/rooms/react", post(rooms_react))
973f8fbdaf80atagen2026-09-28 09:10 UTC768
        .route("/rooms/read", post(rooms_read))
973f8fbdaf80atagen2026-09-28 09:10 UTC769
        .route("/rooms/typing", post(rooms_typing))
973f8fbdaf80atagen2026-09-28 09:10 UTC770
        .route("/profile", get(get_profile))
973f8fbdaf80atagen2026-09-28 09:10 UTC771
        .route("/media", get(get_media))
973f8fbdaf80atagen2026-09-28 09:10 UTC772
        .route("/message-media", get(get_message_media))
973f8fbdaf80atagen2026-09-28 09:10 UTC773
        .route("/spaces/children", get(get_space_children))
973f8fbdaf80atagen2026-09-28 09:10 UTC774
        .route("/rooms/members", get(get_members))
973f8fbdaf80atagen2026-09-28 09:10 UTC775
        .route("/encryption", get(get_encryption))
973f8fbdaf80atagen2026-09-28 09:10 UTC776
        .route("/encryption/recover", post(recover_encryption))
973f8fbdaf80atagen2026-09-28 09:10 UTC777
        .route("/encryption/start", post(start_verification))
973f8fbdaf80atagen2026-09-28 09:10 UTC778
        .route("/encryption/verify", post(verify_encryption));
973f8fbdaf80atagen2026-09-28 09:10 UTC779
    let api = Router::new()
973f8fbdaf80atagen2026-09-28 09:10 UTC780
        .nest("/api", api)
973f8fbdaf80atagen2026-09-28 09:10 UTC781
        .layer(middleware::from_fn(csrf_guard));
973f8fbdaf80atagen2026-09-28 09:10 UTC782
    let router = api.with_state(state).layer(TraceLayer::new_for_http());
973f8fbdaf80atagen2026-09-28 09:10 UTC783
    match web_dir {
973f8fbdaf80atagen2026-09-28 09:10 UTC784
        Some(dir) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC785
            let files =
973f8fbdaf80atagen2026-09-28 09:10 UTC786
                ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));
973f8fbdaf80atagen2026-09-28 09:10 UTC787
            router.fallback_service(files)
973f8fbdaf80atagen2026-09-28 09:10 UTC788
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC789
        None => router.fallback(static_shell),
973f8fbdaf80atagen2026-09-28 09:10 UTC790
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC791
}
973f8fbdaf80atagen2026-09-28 09:10 UTC792
973f8fbdaf80atagen2026-09-28 09:10 UTC793
async fn api_not_found() -> StatusCode {
973f8fbdaf80atagen2026-09-28 09:10 UTC794
    StatusCode::NOT_FOUND
973f8fbdaf80atagen2026-09-28 09:10 UTC795
}
973f8fbdaf80atagen2026-09-28 09:10 UTC796
973f8fbdaf80atagen2026-09-28 09:10 UTC797
fn is_no_cache_path(path: &str) -> bool {
973f8fbdaf80atagen2026-09-28 09:10 UTC798
    matches!(path, "/" | "/index.html" | "/sw.js")
973f8fbdaf80atagen2026-09-28 09:10 UTC799
}
973f8fbdaf80atagen2026-09-28 09:10 UTC800
973f8fbdaf80atagen2026-09-28 09:10 UTC801
/// index.html and sw.js must never be cached, or a stale service worker or
973f8fbdaf80atagen2026-09-28 09:10 UTC802
/// app shell survives a rebuild; the rest of the bundle is hashed and can be
973f8fbdaf80atagen2026-09-28 09:10 UTC803
/// cached freely.
973f8fbdaf80atagen2026-09-28 09:10 UTC804
async fn static_headers(request: axum::extract::Request, next: Next) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC805
    let no_cache = is_no_cache_path(request.uri().path());
973f8fbdaf80atagen2026-09-28 09:10 UTC806
    let mut response = next.run(request).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC807
    if no_cache {
973f8fbdaf80atagen2026-09-28 09:10 UTC808
        let headers = response.headers_mut();
973f8fbdaf80atagen2026-09-28 09:10 UTC809
        headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
973f8fbdaf80atagen2026-09-28 09:10 UTC810
        headers.insert(
973f8fbdaf80atagen2026-09-28 09:10 UTC811
            HeaderName::from_static("service-worker-allowed"),
973f8fbdaf80atagen2026-09-28 09:10 UTC812
            HeaderValue::from_static("/"),
973f8fbdaf80atagen2026-09-28 09:10 UTC813
        );
973f8fbdaf80atagen2026-09-28 09:10 UTC814
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC815
    response
973f8fbdaf80atagen2026-09-28 09:10 UTC816
}
973f8fbdaf80atagen2026-09-28 09:10 UTC817
973f8fbdaf80atagen2026-09-28 09:10 UTC818
/// Serves only the built frontend: no `/api` routes, no session state, no
973f8fbdaf80atagen2026-09-28 09:10 UTC819
/// data dir. Used for the socket-activated on-device build.
973f8fbdaf80atagen2026-09-28 09:10 UTC820
pub fn static_router(web_dir: Option<String>) -> Router {
973f8fbdaf80atagen2026-09-28 09:10 UTC821
    let router = Router::new()
973f8fbdaf80atagen2026-09-28 09:10 UTC822
        .route("/api", any(api_not_found))
973f8fbdaf80atagen2026-09-28 09:10 UTC823
        .route("/api/{*rest}", any(api_not_found))
973f8fbdaf80atagen2026-09-28 09:10 UTC824
        .layer(TraceLayer::new_for_http());
973f8fbdaf80atagen2026-09-28 09:10 UTC825
    match web_dir {
973f8fbdaf80atagen2026-09-28 09:10 UTC826
        Some(dir) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC827
            let files =
973f8fbdaf80atagen2026-09-28 09:10 UTC828
                ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));
973f8fbdaf80atagen2026-09-28 09:10 UTC829
            router
973f8fbdaf80atagen2026-09-28 09:10 UTC830
                .fallback_service(files)
973f8fbdaf80atagen2026-09-28 09:10 UTC831
                .layer(middleware::from_fn(static_headers))
973f8fbdaf80atagen2026-09-28 09:10 UTC832
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC833
        None => router.fallback(static_shell),
973f8fbdaf80atagen2026-09-28 09:10 UTC834
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC835
}