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

use crate::{
    clock::DateTime,
    db,
    db::NotFound as _,
    error::failed::{Failed, ResultExt as _},
    login::{Login, repo::Provider as _},
    name::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.fail(db::failed::BEGIN)?;
        let (login, password) = tx
            .logins()
            .by_name(name)
            .await
            .optional()
            .fail("Failed to load login")?
            .ok_or_else(|| 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.fail(db::failed::COMMIT)?;

        if password
            .verify(candidate)
            .fail("Failed to verify password")?
        {
            let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?;
            let (token, secret) = Token::generate(&login, login_at);
            tx.tokens()
                .create(&token, &secret)
                .await
                .fail("Failed to create token")?;
            tx.commit().await.fail(db::failed::COMMIT)?;
            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.fail(db::failed::BEGIN)?;
        let (login, password) = tx
            .logins()
            .by_id(&login.id)
            .await
            .optional()
            .fail("Failed to load login")?
            .ok_or_else(|| 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.fail(db::failed::COMMIT)?;

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

            let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?;
            tx.logins()
                .set_password(&login, &to_hash)
                .await
                .fail("Failed to store new password")?;
            tx.push()
                .unsubscribe_login(&login)
                .await
                .fail("Failed to remove push notification subscriptions")?;
            let revoked = tx
                .tokens()
                .revoke_all(&login)
                .await
                .fail("Failed to revoke existing tokens")?;
            tx.tokens()
                .create(&token, &secret)
                .await
                .fail("Failed to create new token")?;
            tx.commit().await.fail(db::failed::COMMIT)?;

            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)]
    Failed(#[from] Failed),
}