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
) ;
64
crate :: mail:: send( app, account.name ( ) , & account.email, " Confirm your email address" , & body) .await
65
}
66
67
68
pub async fn verify ( State( app ) : State< Shared> , viewer : Viewer, Path( secret ) : Path< String> ) -> Result< Page< RecoveryPage> , Error> {
69
let shell = Shell:: new( & app, " /verify" , & viewer) ;
70
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
71
let link = accounts:: live_link( & app.db, " verify" , & secret) .await.map_err ( e) ? ;
72
let account = match & link {
73
Some ( l) => accounts:: account_by_id( & app.db, l.account_id) .await.map_err ( e) ? ,
74
None => None ,
75
} ;
76
match ( link, account) {
77
78
( Some ( l) , Some ( a) ) if a.email = = l.email & & accounts:: use_link( & app.db, l.id) .await.map_err ( e) ? => {
79
accounts:: set_email_verified( & app.db, a.id, true ) .await.map_err ( e) ? ;
80
accounts:: audit( & app.db, Some ( & a) , " email.verified" , & a.username, & a.email) .await.ok ( ) ;
81
let mut page = RecoveryPage:: new( & app, shell, " verified" ) ;
82
page.username = a.email;
83
Ok ( Page( page) )
84
}
85
_ => Ok ( Page( RecoveryPage:: new( & app, shell, " verify-invalid" ) ) ) ,
86
}
87
}
88
89
90
pub async fn request_form ( State( app ) : State< Shared> , viewer : Viewer) -> Page< RecoveryPage> {
91
Page( RecoveryPage:: new( & app, Shell:: new( & app, " /login/reset" , & viewer) , " request" ) )
92
}
93
94
# [ derive ( Deserialize ) ]
95
pub struct RequestForm {
96
who : String,
97
}
98
99
100
pub async fn request ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< RequestForm> ) -> Result< Page< RecoveryPage> , Error> {
101
let shell = Shell:: new( & app, " /login/reset" , & viewer) ;
102
if ! crate :: mail:: enabled( & app) {
103
return Ok ( Page( RecoveryPage:: new( & app, shell, " request" ) ) ) ;
104
}
105
let who = f.who.trim ( ) ;
106
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
107
let account = if who.contains ( ' @' ) {
108
accounts:: account_by_verified_email( & app.db, who) .await.map_err ( e) ?
109
} else {
110
accounts:: account_by_name( & app.db, who) .await.map_err ( e) ?
111
} ;
6a178055 Andy McCall 1 month ago ↶
112
if let Some ( a) = account.filter ( | a | a.email_verified & & ! a.email.is_empty ( ) & & ! a.blocked ( ) ) {
34beb1a7 Andy McCall 1 month ago ↶
113
let recent = accounts:: last_link_ms( & app.db, a.id, " reset" , None ) .await.map_err ( e) ? ;
114
if ! recent.is_some_and ( | t | db:: now_ms( ) - t < MAIL_EVERY_MS ) {
115
let secret = accounts:: create_link( & app.db, a.id, " reset" , " " , None , RESET_TTL_MS ) .await.map_err ( e) ? ;
116
let url = format! ( " {} /login/reset/{secret} " , app.config.base_url ( ) ) ;
117
let body = format! (
118
" 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 " ,
119
a.username, app.config.site.name
120
) ;
121
122
let ( mailer, name, addr, user) = ( app.clone ( ) , a.name ( ) .to_string ( ) , a.email.clone ( ) , a.username.clone ( ) ) ;
123
tokio:: spawn( async move {
124
if let Err ( err) = crate :: mail:: send( & mailer, & name, & addr, " Reset your password" , & body) .await {
125
tracing:: warn! ( " reset mail for {user}: {err}" ) ;
126
}
127
} ) ;
128
accounts:: audit( & app.db, Some ( & a) , " password.reset_requested" , & a.username, " " ) .await.ok ( ) ;
129
}
130
}
131
Ok ( Page( RecoveryPage:: new( & app, shell, " sent" ) ) )
132
}
133
134
async fn reset_target ( app : & Shared, secret : & str ) -> sqlx:: Result< Option< ( accounts:: AccountLink, accounts:: Account) > > {
135
let Some ( link) = accounts:: live_link( & app.db, " reset" , secret) .await? else { return Ok ( None ) } ;
136
let account = accounts:: account_by_id( & app.db, link.account_id) .await? ;
6a178055 Andy McCall 1 month ago ↶
137
Ok ( account.filter ( | a | ! a.blocked ( ) ) .map ( | a | ( link, a) ) )
34beb1a7 Andy McCall 1 month ago ↶
138
}
139
140
141
pub async fn new_password_form ( State( app ) : State< Shared> , viewer : Viewer, Path( secret ) : Path< String> ) -> Result< Page< RecoveryPage> , Error> {
142
let shell = Shell:: new( & app, " /login/reset" , & viewer) ;
143
match reset_target ( & app, & secret) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? {
144
Some ( ( _ , a) ) => {
145
let mut page = RecoveryPage:: new( & app, shell, " new" ) ;
146
page.username = a.username;
147
page.secret = secret;
148
Ok ( Page( page) )
149
}
150
None => Ok ( Page( RecoveryPage:: new( & app, shell, " invalid" ) ) ) ,
151
}
152
}
153
154
# [ derive ( Deserialize ) ]
155
pub struct NewPasswordForm {
156
password : String,
157
confirm : String,
158
}
159
160
161
pub async fn set_new_password (
162
State( app ) : State< Shared> ,
163
viewer : Viewer,
164
Path( secret ) : Path< String> ,
165
Form( f ) : Form< NewPasswordForm> ,
166
) -> Result< Response, Error> {
167
let shell = Shell:: new( & app, " /login/reset" , & viewer) ;
168
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
169
let Some ( ( link, account) ) = reset_target ( & app, & secret) .await.map_err ( e) ? else {
170
return Ok ( Page( RecoveryPage:: new( & app, shell, " invalid" ) ) .into_response ( ) ) ;
171
} ;
172
let problem = if f.password ! = f.confirm {
173
Some ( " The two passwords differ." .to_string ( ) )
174
} else {
175
auth:: password_ok( & f.password) .err ( ) .map ( String :: from)
176
} ;
177
if let Some ( msg) = problem {
178
let mut page = RecoveryPage:: new( & app, shell, " new" ) ;
179
page.username = account.username;
180
page.secret = secret;
181
page.error = Some ( msg) ;
182
return Ok ( Page( page) .into_response ( ) ) ;
183
}
184
if ! accounts:: use_link( & app.db, link.id) .await.map_err ( e) ? {
185
return Ok ( Page( RecoveryPage:: new( & app, shell, " invalid" ) ) .into_response ( ) ) ;
186
}
187
let hash = auth:: hash_password( & f.password) .map_err ( | err | Error:: bad( shell.clone ( ) , err.to_string ( ) ) ) ? ;
188
189
accounts:: set_password( & app.db, account.id, & hash, & auth:: random_hex( 16 ) ) .await.map_err ( e) ? ;
190
accounts:: void_links( & app.db, account.id, " reset" ) .await.map_err ( e) ? ;
191
let by = if link.created_by.is_some ( ) { " administrator's link" } else { " emailed link" } ;
192
accounts:: audit( & app.db, Some ( & account) , " password.reset" , & account.username, by) .await.ok ( ) ;
193
Ok ( Redirect:: to( " /login?reset=1" ) .into_response ( ) )
194
}
195
196
197
pub async fn admin_reset_link ( app : & Shared, admin : & accounts:: Account, target : & accounts:: Account) -> sqlx:: Result< String> {
198
let secret = accounts:: create_link( & app.db, target.id, " reset" , " " , Some ( admin.id) , ADMIN_RESET_TTL_MS ) .await? ;
199
accounts:: audit( & app.db, Some ( admin) , " admin.account.reset_link" , & target.username, " valid 24 hours" ) .await.ok ( ) ;
200
Ok ( format! ( " {} /login/reset/{secret} " , app.config.base_url ( ) ) )
201
}