summaryrefslogtreecommitdiff
path: root/src/invite/app.rs
blob: 6f58d0ac9c6adce489219ed59af7d665b3edccc0 (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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use chrono::TimeDelta;
use sqlx::sqlite::SqlitePool;

use super::{Id, Invite, Summary, repo::Provider as _};
use crate::{
    clock::DateTime,
    db::{Duplicate as _, NotFound as _},
    event::{Broadcaster, repo::Provider as _},
    login::Login,
    name::{self, Name},
    password::Password,
    token::{Secret, Token, repo::Provider as _},
    user::{
        self,
        create::{self, Create},
        repo::{LoadError, Provider as _},
    },
};

pub struct Invites {
    db: SqlitePool,
    events: Broadcaster,
}

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

    pub async fn issue(&self, issuer: &Login, issued_at: &DateTime) -> Result<Invite, Error> {
        let issuer_not_found = || Error::IssuerNotFound(issuer.id.clone().into());
        let issuer_deleted = || Error::IssuerDeleted(issuer.id.clone().into());

        let mut tx = self.db.begin().await?;
        let issuer = tx
            .users()
            .by_login(issuer)
            .await
            .not_found(issuer_not_found)?;
        let now = tx.sequence().current().await?;
        let issuer = issuer.as_of(now).ok_or_else(issuer_deleted)?;
        let invite = tx.invites().create(&issuer, issued_at).await?;
        tx.commit().await?;

        Ok(invite)
    }

    pub async fn get(&self, invite: &Id) -> Result<Option<Summary>, sqlx::Error> {
        let mut tx = self.db.begin().await?;
        let invite = tx.invites().summary(invite).await.optional()?;
        tx.commit().await?;

        Ok(invite)
    }

    pub async fn accept(
        &self,
        invite: &Id,
        name: &Name,
        password: &Password,
        accepted_at: &DateTime,
    ) -> Result<Secret, AcceptError> {
        let create = Create::begin(name, password, accepted_at);

        let mut tx = self.db.begin().await?;
        let invite = tx
            .invites()
            .by_id(invite)
            .await
            .not_found(|| AcceptError::NotFound(invite.clone()))?;
        // Split the tx here so we don't block writes while we deal with the password,
        // and don't deal with the password until we're fairly confident we can accept
        // the invite. Final validation is in the next tx.
        tx.commit().await?;

        let validated = create.validate()?;

        let mut tx = self.db.begin().await?;
        // If the invite has been deleted or accepted in the interim, this step will
        // catch it.
        tx.invites().accept(&invite).await?;
        let stored = validated
            .store(&mut tx)
            .await
            .duplicate(|| AcceptError::DuplicateLogin(name.clone()))?;
        let login = stored.login();
        let (token, secret) = Token::generate(login, accepted_at);
        tx.tokens().create(&token, &secret).await?;
        tx.commit().await?;

        stored.publish(&self.events);

        Ok(secret)
    }

    pub async fn expire(&self, relative_to: &DateTime) -> Result<(), sqlx::Error> {
        // Somewhat arbitrarily, expire after one day.
        let expire_at = relative_to.to_owned() - TimeDelta::days(1);

        let mut tx = self.db.begin().await?;
        tx.invites().expire(&expire_at).await?;
        tx.commit().await?;

        Ok(())
    }
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("issuing user {0} not found")]
    IssuerNotFound(user::Id),
    #[error("issuing user {0} deleted")]
    IssuerDeleted(user::Id),
    #[error(transparent)]
    Database(#[from] sqlx::Error),
    #[error(transparent)]
    Name(#[from] name::Error),
}

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

#[derive(Debug, thiserror::Error)]
pub enum AcceptError {
    #[error("invite not found: {0}")]
    NotFound(Id),
    #[error("invalid user name: {0}")]
    InvalidName(Name),
    #[error("name in use: {0}")]
    DuplicateLogin(Name),
    #[error(transparent)]
    Database(#[from] sqlx::Error),
    #[error(transparent)]
    PasswordHash(#[from] password_hash::Error),
}

impl From<create::Error> for AcceptError {
    fn from(error: create::Error) -> Self {
        match error {
            create::Error::InvalidName(name) => Self::InvalidName(name),
            create::Error::PasswordHash(error) => Self::PasswordHash(error),
        }
    }
}