34beb1a7 Andy McCall 1 month ago ↶
1
2
3
use super :: { Error, Page, Shell} ;
4
use crate :: auth:: { self , Viewer} ;
5
use crate :: db:: { self , accounts} ;
6
use crate :: Shared;
7
use askama:: Template;
8
use axum:: extract:: { Form, Path, State} ;
9
use axum:: response:: { IntoResponse, Redirect, Response} ;
10
use serde:: Deserialize;
11
12
const MINUTE : i64 = 60_000 ;
13
pub const VERIFY_TTL_MS : i64 = 48 * 60 * MINUTE ;
14
pub const RESET_TTL_MS : i64 = 60 * MINUTE ;
15
pub const ADMIN_RESET_TTL_MS : i64 = 24 * 60 * MINUTE ;
16
17
pub const MAIL_EVERY_MS : i64 = 2 * MINUTE ;
18
19
pub const VERIFY_PER_HOUR : i64 = 10 ;
20
21
# [ derive ( Template ) ]
22
# [ template ( path = " reset.html" ) ]
23
pub struct RecoveryPage {
24
shell : Shell,
25
26
mode : & 'static str ,
27
mail : bool ,
28
error : Option< String> ,
29
username : String,
30
secret : String,
31
}
32
33
impl RecoveryPage {
34
fn new ( app : & Shared, shell : Shell, mode : & 'static str ) -> RecoveryPage {
35
RecoveryPage { shell, mode, mail: crate :: mail:: enabled( app) , error: None , username: String :: new( ) , secret: String :: new( ) }
36
}
37
}
38
39
40
pub async fn send_verification ( app : & Shared, account : & accounts:: Account) -> Result< ( ) , String> {
41
if account.email.is_empty ( ) | | account.email_verified {
42
return Ok ( ( ) ) ;
43
}
44
if ! crate :: mail:: enabled( app) {
45
return Err ( " This hub cannot send mail, so addresses cannot be verified yet." .into ( ) ) ;
46
}
47
let now = db:: now_ms( ) ;
48
let recent = accounts:: last_link_ms( & app.db, account.id, " verify" , Some ( & account.email) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
49
if recent.is_some_and ( | t | now - t < MAIL_EVERY_MS ) {
50
return Err ( " A verification link was sent less than two minutes ago." .into ( ) ) ;
51
}
52
if accounts:: links_since( & app.db, account.id, " verify" , now - 60 * MINUTE ) .await.map_err ( | e | e.to_string ( ) ) ? > = VERIFY_PER_HOUR {
53
return Err ( " Too many verification links this hour; try again later." .into ( ) ) ;
54
}
55
let secret =
56
accounts:: create_link( & app.db, account.id, " verify" , & account.email, None , VERIFY_TTL_MS ) .await.map_err ( | e | e.to_string ( ) ) ? ;
57
let url = format! ( " {} /verify/{secret} " , app.config.base_url ( ) ) ;
58
let body = format! (
59
" Confirm that {} is the email address of {} on {} :\n \n {url} \n \n The link works for 48 hours. If you did not ask for this, ignore it.\n " ,
60
account.email,
61
account.username,
62
app.config.site.name
63
) ;
cd46e56b Andy McCall 22 days ago ↶
64
let action = crate :: mail:: Action { label: " Confirm my email address" , url: & url } ;
b91e2d81 Andy McCall 22 days ago ↶
65
crate :: mail:: send_letter( app, account.name ( ) , & account.email, " ✉️ Confirm your email address" , & body, Some ( action) , None ) .await
34beb1a7 Andy McCall 1 month ago ↶
66
}
67
68
69
pub async fn verify ( State( app ) : State< Shared> , viewer : Viewer, Path( secret ) : Path< String> ) -> Result< Page< RecoveryPage> , Error> {
70
let shell = Shell:: new( & app, " /verify" , & viewer) ;
71
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
72
let link = accounts:: live_link( & app.db, " verify" , & secret) .await.map_err ( e) ? ;
73
let account = match & link {
74
Some ( l) => accounts:: account_by_id( & app.db, l.account_id) .await.map_err ( e) ? ,
75
None => None ,
76
} ;
77
match ( link, account) {
78
79
( Some ( l) , Some ( a) ) if a.email = = l.email & & accounts:: use_link( & app.db, l.id) .await.map_err ( e) ? => {
80
accounts:: set_email_verified( & app.db, a.id, true ) .await.map_err ( e) ? ;
81
accounts:: audit( & app.db, Some ( & a) , " email.verified" , & a.username, & a.email) .await.ok ( ) ;
82
let mut page = RecoveryPage:: new( & app, shell, " verified" ) ;
83
page.username = a.email;
84
Ok ( Page( page) )
85
}
86
_ => Ok ( Page( RecoveryPage:: new( & app, shell, " verify-invalid" ) ) ) ,
87
}
88
}
89
90
91
pub async fn request_form ( State( app ) : State< Shared> , viewer : Viewer) -> Page< RecoveryPage> {
92
Page( RecoveryPage:: new( & app, Shell:: new( & app, " /login/reset" , & viewer) , " request" ) )
93
}
94
95
# [ derive ( Deserialize ) ]
96
pub struct RequestForm {
97
who : String,
98
}
99
100
101
pub async fn request ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< RequestForm> ) -> Result< Page< RecoveryPage> , Error> {
102
let shell = Shell:: new( & app, " /login/reset" , & viewer) ;
103
if ! crate :: mail:: enabled( & app) {
104
return Ok ( Page( RecoveryPage:: new( & app, shell, " request" ) ) ) ;
105
}
106
let who = f.who.trim ( ) ;
107
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
108
let account = if who.contains ( ' @' ) {
109
accounts:: account_by_verified_email( & app.db, who) .await.map_err ( e) ?
110
} else {
111
accounts:: account_by_name( & app.db, who) .await.map_err ( e) ?
112
} ;
6a178055 Andy McCall 1 month ago ↶
113
if let Some ( a) = account.filter ( | a | a.email_verified & & ! a.email.is_empty ( ) & & ! a.blocked ( ) ) {
34beb1a7 Andy McCall 1 month ago ↶
114
let recent = accounts:: last_link_ms( & app.db, a.id, " reset" , None ) .await.map_err ( e) ? ;
115
if ! recent.is_some_and ( | t | db:: now_ms( ) - t < MAIL_EVERY_MS ) {
116
let secret = accounts:: create_link( & app.db, a.id, " reset" , " " , None , RESET_TTL_MS ) .await.map_err ( e) ? ;
117
let url = format! ( " {} /login/reset/{secret} " , app.config.base_url ( ) ) ;
118
let body = format! (
119
" Someone asked to reset the password of {} on {} . To choose a new password, open:\n \n {url} \n \n The link works once, for one hour. If it was not you, ignore this; your password stays as it is.\n " ,
120
a.username, app.config.site.name
121
) ;
122
123
let ( mailer, name, addr, user) = ( app.clone ( ) , a.name ( ) .to_string ( ) , a.email.clone ( ) , a.username.clone ( ) ) ;
124
tokio:: spawn( async move {
cd46e56b Andy McCall 22 days ago ↶
125
let action = crate :: mail:: Action { label: " Choose a new password" , url: & url } ;
2bbdc026 Andy McCall 22 days ago ↶
126
if let Err ( err) = crate :: mail:: send_letter( & mailer, & name, & addr, " 🔑 Reset your password" , & body, Some ( action) , None ) .await
127
{
34beb1a7 Andy McCall 1 month ago ↶
128
tracing:: warn! ( " reset mail for {user}: {err}" ) ;
129
}
130
} ) ;
131
accounts:: audit( & app.db, Some ( & a) , " password.reset_requested" , & a.username, " " ) .await.ok ( ) ;
132
}
133
}
134
Ok ( Page( RecoveryPage:: new( & app, shell, " sent" ) ) )
135
}
136
137
async fn reset_target ( app : & Shared, secret : & str ) -> sqlx:: Result< Option< ( accounts:: AccountLink, accounts:: Account) > > {
138
let Some ( link) = accounts:: live_link( & app.db, " reset" , secret) .await? else { return Ok ( None ) } ;
139
let account = accounts:: account_by_id( & app.db, link.account_id) .await? ;
6a178055 Andy McCall 1 month ago ↶
140
Ok ( account.filter ( | a | ! a.blocked ( ) ) .map ( | a | ( link, a) ) )
34beb1a7 Andy McCall 1 month ago ↶
141
}
142
143
144
pub async fn new_password_form ( State( app ) : State< Shared> , viewer : Viewer, Path( secret ) : Path< String> ) -> Result< Page< RecoveryPage> , Error> {
145
let shell = Shell:: new( & app, " /login/reset" , & viewer) ;
146
match reset_target ( & app, & secret) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? {
147
Some ( ( _ , a) ) => {
148
let mut page = RecoveryPage:: new( & app, shell, " new" ) ;
149
page.username = a.username;
150
page.secret = secret;
151
Ok ( Page( page) )
152
}
153
None => Ok ( Page( RecoveryPage:: new( & app, shell, " invalid" ) ) ) ,
154
}
155
}
156
157
# [ derive ( Deserialize ) ]
158
pub struct NewPasswordForm {
159
password : String,
160
confirm : String,
161
}
162
163
164
pub async fn set_new_password (
165
State( app ) : State< Shared> ,
166
viewer : Viewer,
167
Path( secret ) : Path< String> ,
168
Form( f ) : Form< NewPasswordForm> ,
169
) -> Result< Response, Error> {
170
let shell = Shell:: new( & app, " /login/reset" , & viewer) ;
171
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
172
let Some ( ( link, account) ) = reset_target ( & app, & secret) .await.map_err ( e) ? else {
173
return Ok ( Page( RecoveryPage:: new( & app, shell, " invalid" ) ) .into_response ( ) ) ;
174
} ;
175
let problem = if f.password ! = f.confirm {
176
Some ( " The two passwords differ." .to_string ( ) )
177
} else {
178
auth:: password_ok( & f.password) .err ( ) .map ( String :: from)
179
} ;
180
if let Some ( msg) = problem {
181
let mut page = RecoveryPage:: new( & app, shell, " new" ) ;
182
page.username = account.username;
183
page.secret = secret;
184
page.error = Some ( msg) ;
185
return Ok ( Page( page) .into_response ( ) ) ;
186
}
187
if ! accounts:: use_link( & app.db, link.id) .await.map_err ( e) ? {
188
return Ok ( Page( RecoveryPage:: new( & app, shell, " invalid" ) ) .into_response ( ) ) ;
189
}
190
let hash = auth:: hash_password( & f.password) .map_err ( | err | Error:: bad( shell.clone ( ) , err.to_string ( ) ) ) ? ;
191
192
accounts:: set_password( & app.db, account.id, & hash, & auth:: random_hex( 16 ) ) .await.map_err ( e) ? ;
193
accounts:: void_links( & app.db, account.id, " reset" ) .await.map_err ( e) ? ;
194
let by = if link.created_by.is_some ( ) { " administrator's link" } else { " emailed link" } ;
195
accounts:: audit( & app.db, Some ( & account) , " password.reset" , & account.username, by) .await.ok ( ) ;
196
Ok ( Redirect:: to( " /login?reset=1" ) .into_response ( ) )
197
}
198
199
200
pub async fn admin_reset_link ( app : & Shared, admin : & accounts:: Account, target : & accounts:: Account) -> sqlx:: Result< String> {
201
let secret = accounts:: create_link( & app.db, target.id, " reset" , " " , Some ( admin.id) , ADMIN_RESET_TTL_MS ) .await? ;
202
accounts:: audit( & app.db, Some ( admin) , " admin.account.reset_link" , & target.username, " valid 24 hours" ) .await.ok ( ) ;
203
Ok ( format! ( " {} /login/reset/{secret} " , app.config.base_url ( ) ) )
204
}