summaryrefslogtreecommitdiff
path: root/src/login/routes.rs
blob: 2269ea617b207a6a4aaf0216d231d94590ed5e79 (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
use axum::{
    extract::{Form, State},
    http::StatusCode,
    response::IntoResponse,
    routing::post,
    Router,
};
use sqlx::sqlite::SqlitePool;

use crate::{clock::RequestedAt, error::InternalError};

use super::{
    extract::IdentityToken,
    repo::{logins::Provider as _, tokens::Provider as _},
};

pub fn router() -> Router<SqlitePool> {
    Router::new()
        .route("/login", post(on_login))
        .route("/logout", post(on_logout))
}

#[derive(serde::Deserialize)]
struct Login {
    name: String,
    password: String,
}

async fn on_login(
    State(db): State<SqlitePool>,
    RequestedAt(now): RequestedAt,
    identity: IdentityToken,
    Form(form): Form<Login>,
) -> Result<impl IntoResponse, InternalError> {
    if identity.token().is_some() {
        return Ok((StatusCode::BAD_REQUEST, identity, "already logged in"));
    }

    let mut tx = db.begin().await?;

    // Spelling the following in the more conventional form,
    //     if let Some(…) = create().await? {}
    //     else if let Some(…) = validate().await? {}
    //     else {}
    // pushes the specifics of whether the returned error types are Send or not
    // (they aren't) into the type of this function's generated Futures, which
    // in turn makes this function unusable as an Axum handler.
    let login = tx.logins().create(&form.name, &form.password).await?;
    let login = if login.is_some() {
        login
    } else {
        tx.logins().authenticate(&form.name, &form.password).await?
    };

    // If `login` is Some, then we have an identity and can issue an identity
    // token. If `login` is None, then neither creating a new login nor authenticating
    // an existing one succeeded, and we must reject the attempt.
    //
    // These properties will be transferred to `token`, as well.
    let token = if let Some(login) = login {
        Some(tx.tokens().issue(&login.id, now).await?)
    } else {
        None
    };

    tx.commit().await?;

    let resp = if let Some(token) = token {
        let identity = identity.set(&token);
        (StatusCode::OK, identity, "logged in")
    } else {
        (
            StatusCode::UNAUTHORIZED,
            identity,
            "invalid name or password",
        )
    };

    Ok(resp)
}

async fn on_logout(
    State(db): State<SqlitePool>,
    identity: IdentityToken,
) -> Result<impl IntoResponse, InternalError> {
    if let Some(token) = identity.token() {
        let mut tx = db.begin().await?;
        tx.tokens().revoke(token).await?;
        tx.commit().await?;
    }

    let identity = identity.clear();

    Ok((StatusCode::OK, identity, "logged out"))
}