summaryrefslogtreecommitdiff
path: root/src/invite/app.rs
blob: 54272bc254fccd2fa9453af36184e8723b355c1a (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
152
153
154
use chrono::TimeDelta;
use sqlx::sqlite::SqlitePool;

use super::{Id, Invite, Summary, repo::Provider as _};
use crate::{
    clock::DateTime,
    db::{self, NotFound as _},
    error::failed::{ErrorExt as _, Failed, ResultExt as _},
    event::{Broadcaster, repo::Provider as _},
    login::Login,
    name::Name,
    password::Password,
    token::{Secret, Token, repo::Provider as _},
    user::{self, create, create::Create, repo::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, IssueError> {
        let issuer_not_found = || IssueError::IssuerNotFound(issuer.id.clone().into());
        let issuer_deleted = || IssueError::IssuerDeleted(issuer.id.clone().into());

        let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?;
        let issuer = tx
            .users()
            .by_login(issuer)
            .await
            .optional()
            .fail("Failed to load issuing user")?
            .ok_or_else(issuer_not_found)?;
        let now = tx
            .sequence()
            .current()
            .await
            .fail("Failed to find event sequence number")?;
        let issuer = issuer.as_of(now).ok_or_else(issuer_deleted)?;
        let invite = tx
            .invites()
            .create(&issuer, issued_at)
            .await
            .fail("Failed to store new invitation")?;
        tx.commit().await.fail(db::failed::COMMIT)?;

        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 to_not_found = || AcceptError::NotFound(invite.clone());

        let create = Create::begin(name, password, accepted_at);

        let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?;
        let invite = tx
            .invites()
            .by_id(invite)
            .await
            .optional()
            .fail("Failed to load invitation")?
            .ok_or_else(to_not_found)?;
        // 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.fail(db::failed::COMMIT)?;

        let validated = create.validate().map_err(|err| match err {
            create::Error::InvalidName(name) => AcceptError::InvalidName(name),
            create::Error::Failed(_) => err.fail("Failed to validate invited user"),
        })?;

        let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?;
        // If the invite has been deleted or accepted in the interim, this step will
        // catch it.
        tx.invites()
            .accept(&invite)
            .await
            .fail("Failed to remove accepted invitation")?;
        let stored =
            validated
                .store(&mut tx)
                .await
                .map_err(|err| match err.as_database_error() {
                    Some(err) if err.is_unique_violation() => {
                        AcceptError::DuplicateLogin(name.clone())
                    }
                    _ => err.fail("Failed to store invited user"),
                })?;
        let login = stored.login();
        let (token, secret) = Token::generate(login, accepted_at);
        tx.tokens()
            .create(&token, &secret)
            .await
            .fail("Failed to issue token for invited user")?;
        tx.commit().await.fail(db::failed::COMMIT)?;

        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 IssueError {
    #[error("issuing user {0} not found")]
    IssuerNotFound(user::Id),
    #[error("issuing user {0} deleted")]
    IssuerDeleted(user::Id),
    #[error(transparent)]
    Failed(#[from] Failed),
}

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