summaryrefslogtreecommitdiff
path: root/src/ui.rs
blob: 91d0eb818abb8b629bec05ff7ebd11e783cc28b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use axum::{
    extract::{Path, Request, State},
    http::{header, StatusCode},
    middleware::{self, Next},
    response::{IntoResponse, Redirect, Response},
    routing::get,
    Router,
};
use mime_guess::Mime;
use rust_embed::EmbeddedFile;

use crate::{app::App, channel, error::Internal, invite, login::Login};

#[derive(rust_embed::Embed)]
#[folder = "target/ui"]
struct Assets;

impl Assets {
    fn load(path: impl AsRef<str>) -> Result<Asset, NotFound<String>> {
        let path = path.as_ref();
        let mime = mime_guess::from_path(path).first_or_octet_stream();

        Self::get(path)
            .map(|file| Asset(mime, file))
            .ok_or(NotFound(format!("not found: {path}")))
    }

    fn index() -> Result<Asset, Internal> {
        // "not found" in this case really is an internal error, as it should
        // never happen. `index.html` is a known-valid path.
        Ok(Self::load("index.html")?)
    }
}

pub fn router(app: &App) -> Router<App> {
    [
        Router::new()
            .route("/*path", get(asset))
            .route("/setup", get(setup)),
        Router::new()
            .route("/", get(root))
            .route("/login", get(login))
            .route("/ch/:channel", get(channel))
            .route("/invite/:invite", get(invite))
            .route_layer(middleware::from_fn_with_state(app.clone(), setup_required)),
    ]
    .into_iter()
    .fold(Router::default(), Router::merge)
}

async fn asset(Path(path): Path<String>) -> Result<Asset, NotFound<String>> {
    Assets::load(path)
}

async fn root(login: Option<Login>) -> Result<impl IntoResponse, Internal> {
    if login.is_none() {
        Ok(Redirect::temporary("/login").into_response())
    } else {
        Ok(Assets::index()?.into_response())
    }
}

async fn login() -> Result<impl IntoResponse, Internal> {
    Assets::index()
}

async fn setup(State(app): State<App>) -> Result<impl IntoResponse, Internal> {
    if app.setup().completed().await? {
        Ok(Redirect::to("/login").into_response())
    } else {
        Ok(Assets::index().into_response())
    }
}

async fn channel(
    State(app): State<App>,
    login: Option<Login>,
    Path(channel): Path<channel::Id>,
) -> Result<impl IntoResponse, Internal> {
    if login.is_none() {
        Ok(Redirect::temporary("/").into_response())
    } else if app.channels().get(&channel).await?.is_none() {
        Ok(NotFound(Assets::index()?).into_response())
    } else {
        Ok(Assets::index()?.into_response())
    }
}

async fn invite(
    State(app): State<App>,
    Path(invite): Path<invite::Id>,
) -> Result<impl IntoResponse, Internal> {
    match app.invites().get(&invite).await {
        Ok(_) => Ok(Assets::index()?.into_response()),
        Err(invite::app::Error::NotFound(_)) => Ok(NotFound(Assets::index()?).into_response()),
        Err(other) => Err(Internal::from(other)),
    }
}

struct Asset(Mime, EmbeddedFile);

impl IntoResponse for Asset {
    fn into_response(self) -> Response {
        let Self(mime, file) = self;
        (
            StatusCode::OK,
            [(header::CONTENT_TYPE, mime.as_ref())],
            file.data,
        )
            .into_response()
    }
}

#[derive(Debug, thiserror::Error)]
#[error("{0}")]
struct NotFound<E>(pub E);

impl<E> IntoResponse for NotFound<E>
where
    E: IntoResponse,
{
    fn into_response(self) -> Response {
        let Self(response) = self;
        (StatusCode::NOT_FOUND, response).into_response()
    }
}

pub async fn setup_required(State(app): State<App>, request: Request, next: Next) -> Response {
    match app.setup().completed().await {
        Ok(true) => next.run(request).await,
        Ok(false) => Redirect::to("/setup").into_response(),
        Err(error) => Internal::from(error).into_response(),
    }
}