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(),
}
}
}
|