summaryrefslogtreecommitdiff
path: root/src/login/app.rs
blob: 8cc8cd0fe4c5fc2b4eafbde12d4238ac2bd0638a (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
use sqlx::sqlite::SqlitePool;

use crate::{
    clock::DateTime,
    db::NotFound as _,
    login::{self, Login, repo::Provider as _},
    name::{self, Name},
    password::Password,
    push::repo::Provider as _,
    token::{Broadcaster, Event as TokenEvent, Secret, Token, repo::Provider as _},
};

pub struct Logins {
    db: SqlitePool,
    token_events: Broadcaster,
}

impl Logins {
    pub const fn new(db: SqlitePool, token_events: Broadcaster) -> Self {
        Self { db, token_events }
    }

    pub async fn with_password(
        &self,
        name: &Name,
        candidate: &Password,
        login_at: &DateTime,
    ) -> Result<Secret, LoginError> {
        let mut tx = self.db.begin().await?;
        let (login, password) = tx
            .logins()
            .by_name(name)
            .await
            .not_found(|| LoginError::Rejected)?;
        // Split the transaction here to avoid holding the tx open (potentially blocking
        // other writes) while we do the fairly expensive task of verifying the
        // password. It's okay if the token issuance transaction happens some notional
        // amount of time after retrieving the login, as inserting the token will fail
        // if the account is deleted during that time.
        tx.commit().await?;

        if password.verify(candidate)? {
            let mut tx = self.db.begin().await?;
            let (token, secret) = Token::generate(&login, login_at);
            tx.tokens().create(&token, &secret).await?;
            tx.commit().await?;
            Ok(secret)
        } else {
            Err(LoginError::Rejected)
        }
    }

    pub async fn change_password(
        &self,
        login: &Login,
        from: &Password,
        to: &Password,
        changed_at: &DateTime,
    ) -> Result<Secret, LoginError> {
        let mut tx = self.db.begin().await?;
        let (login, password) = tx
            .logins()
            .by_id(&login.id)
            .await
            .not_found(|| LoginError::Rejected)?;
        // Split the transaction here to avoid holding the tx open (potentially blocking
        // other writes) while we do the fairly expensive task of verifying the
        // password. It's okay if the token issuance transaction happens some notional
        // amount of time after retrieving the login, as inserting the token will fail
        // if the account is deleted during that time.
        tx.commit().await?;

        if password.verify(from)? {
            let to_hash = to.hash()?;
            let (token, secret) = Token::generate(&login, changed_at);

            let mut tx = self.db.begin().await?;
            tx.logins().set_password(&login, &to_hash).await?;

            tx.push().unsubscribe_login(&login).await?;
            let revoked = tx.tokens().revoke_all(&login).await?;
            tx.tokens().create(&token, &secret).await?;
            tx.commit().await?;

            revoked
                .into_iter()
                .map(TokenEvent::Revoked)
                .for_each(|event| self.token_events.broadcast(event));

            Ok(secret)
        } else {
            Err(LoginError::Rejected)
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum LoginError {
    #[error("invalid login")]
    Rejected,
    #[error(transparent)]
    Database(#[from] sqlx::Error),
    #[error(transparent)]
    Name(#[from] name::Error),
    #[error(transparent)]
    PasswordHash(#[from] password_hash::Error),
}

impl From<login::repo::LoadError> for LoginError {
    fn from(error: login::repo::LoadError) -> Self {
        use login::repo::LoadError;
        match error {
            LoadError::Database(error) => error.into(),
            LoadError::Name(error) => error.into(),
        }
    }
}