19bb31ff Andy McCall 1 month ago ↶
1
2
3
use super :: { Error, Page, Shell} ;
4
use crate :: auth:: { self , totp, Viewer} ;
5
use crate :: db:: { self , accounts} ;
aa0bfbd1 Andy McCall 20 days ago ↶
6
use crate :: throttle:: { self , Key} ;
19bb31ff Andy McCall 1 month ago ↶
7
use crate :: Shared;
8
use askama:: Template;
9
use axum:: extract:: { Form, Query, State} ;
10
use axum:: http:: header;
11
use axum:: response:: { IntoResponse, Redirect, Response} ;
12
use serde:: Deserialize;
13
14
# [ derive ( Template ) ]
15
# [ template ( path = " login.html" ) ]
16
pub struct LoginPage {
17
shell : Shell,
18
error : Option< String> ,
19
next : String,
20
21
totp_for : Option< String> ,
bff9106e Andy McCall 22 days ago ↶
22
23
24
pending : String,
34beb1a7 Andy McCall 1 month ago ↶
25
26
reset : bool ,
83940d53 Andy McCall 1 month ago ↶
27
28
providers : Vec< ( & 'static str , & 'static str ) > ,
a22e4aec Andy McCall 4 days ago ↶
29
30
31
approval : Option< ( String, i64 ) > ,
32
33
phones : bool ,
19bb31ff Andy McCall 1 month ago ↶
34
}
35
36
# [ derive ( Deserialize ) ]
37
pub struct NextQuery {
38
next : Option< String> ,
34beb1a7 Andy McCall 1 month ago ↶
39
reset : Option< String> ,
83940d53 Andy McCall 1 month ago ↶
40
#[serde(default)]
41
error : Option< String> ,
19bb31ff Andy McCall 1 month ago ↶
42
}
43
44
fn safe_next ( next : Option< String> ) -> String {
45
match next {
46
Some ( n) if n.starts_with ( ' /' ) & & ! n.starts_with ( " //" ) => n,
47
_ => " /" .into ( ) ,
48
}
49
}
50
51
pub async fn login_form ( State( app ) : State< Shared> , viewer : Viewer, Query( q ) : Query< NextQuery> ) -> Response {
52
if viewer.signed_in ( ) {
53
return Redirect:: to( & safe_next ( q.next) ) .into_response ( ) ;
54
}
34beb1a7 Andy McCall 1 month ago ↶
55
let reset = q.reset.is_some ( ) ;
83940d53 Andy McCall 1 month ago ↶
56
let providers = super :: signin:: buttons( & app) .await;
57
Page( LoginPage {
58
shell: Shell:: new( & app, " /login" , & viewer) ,
59
error: q.error,
60
next: safe_next ( q.next) ,
61
totp_for: None ,
bff9106e Andy McCall 22 days ago ↶
62
pending: String :: new( ) ,
83940d53 Andy McCall 1 month ago ↶
63
reset,
64
providers,
a22e4aec Andy McCall 4 days ago ↶
65
approval: None ,
66
phones: false ,
83940d53 Andy McCall 1 month ago ↶
67
} )
68
.into_response ( )
19bb31ff Andy McCall 1 month ago ↶
69
}
70
71
# [ derive ( Deserialize ) ]
72
pub struct LoginForm {
73
username : String,
bff9106e Andy McCall 22 days ago ↶
74
#[serde(default)]
19bb31ff Andy McCall 1 month ago ↶
75
password : String,
bff9106e Andy McCall 22 days ago ↶
76
77
#[serde(default)]
78
pending : String,
19bb31ff Andy McCall 1 month ago ↶
79
#[serde(default)]
80
code : String,
81
#[serde(default)]
82
next : String,
a22e4aec Andy McCall 4 days ago ↶
83
84
#[serde(default)]
85
approval : String,
86
87
#[serde(default)]
88
use_code : String,
19bb31ff Andy McCall 1 month ago ↶
89
}
90
aa0bfbd1 Andy McCall 20 days ago ↶
91
pub async fn login (
92
State( app ) : State< Shared> ,
93
viewer : Viewer,
94
headers : axum:: http:: HeaderMap,
95
extensions : axum:: http:: Extensions,
96
Form( f ) : Form< LoginForm> ,
97
) -> Result< Response, Error> {
19bb31ff Andy McCall 1 month ago ↶
98
let shell = Shell:: new( & app, " /login" , & viewer) ;
99
let next = safe_next ( Some ( f.next.clone ( ) ) ) ;
100
let username = f.username.trim ( ) .to_string ( ) ;
101
let account = accounts:: account_by_name( & app.db, & username) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
bff9106e Andy McCall 22 days ago ↶
102
103
104
let now_ms = db:: now_ms( ) ;
105
let proven = ! f.pending.is_empty ( )
106
& & account.as_ref ( ) .is_some_and ( | a | auth:: pending_login_ok( & app.cookie_key, & a.username, & a.session_salt, & f.pending, now_ms) ) ;
107
let pending = account
108
.as_ref ( )
109
.map ( | a | auth:: pending_login( & app.cookie_key, & a.username, & a.session_salt, now_ms + PENDING_LOGIN_MS ) )
110
.unwrap_or_default ( ) ;
a22e4aec Andy McCall 4 days ago ↶
111
let phones = match & account {
112
Some ( a) if a.totp_secret.is_some ( ) => {
113
! db:: approvals:: authenticators( & app.db, a.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? .is_empty ( )
114
}
115
_ => false ,
116
} ;
bff9106e Andy McCall 22 days ago ↶
117
let fail = | msg : & str , totp_for : Option< String> | {
118
Page( LoginPage {
119
shell: shell.clone ( ) ,
a22e4aec Andy McCall 4 days ago ↶
120
error: ( ! msg.is_empty ( ) ) .then ( | | msg.into ( ) ) ,
bff9106e Andy McCall 22 days ago ↶
121
next: next.clone ( ) ,
122
totp_for,
123
pending: pending.clone ( ) ,
124
reset: false ,
125
providers: vec! [ ] ,
a22e4aec Andy McCall 4 days ago ↶
126
approval: None ,
127
phones,
bff9106e Andy McCall 22 days ago ↶
128
} )
129
.into_response ( )
130
} ;
aa0bfbd1 Andy McCall 20 days ago ↶
131
132
133
let addr = crate :: api:: client_addr( & app, & headers, & extensions) ;
134
let keys = [ Key:: account( " login" , & username) , Key:: address( " login" , & addr) ] ;
135
if let Some ( wait) = app.throttle.lock ( ) .unwrap ( ) .wait ( & keys, now_ms) {
136
return Ok ( fail ( & throttle:: message( wait) , proven.then ( | | username.clone ( ) ) ) ) ;
137
}
138
let failed = | app : & Shared| {
139
let wait = app.throttle.lock ( ) .unwrap ( ) .failed ( & keys, now_ms) ;
140
let ( app, username, addr) = ( app.clone ( ) , username.clone ( ) , addr.clone ( ) ) ;
141
async move {
142
if let Some ( wait) = wait {
143
let detail = format! ( " from {} for {} s" , if addr.is_empty ( ) { " an unknown address" } else { & addr } , wait / 1000 ) ;
144
accounts:: audit( & app.db, None , " login.throttled" , & username, & detail) .await.ok ( ) ;
145
}
146
}
147
} ;
bff9106e Andy McCall 22 days ago ↶
148
if ! f.pending.is_empty ( ) & & ! proven {
149
return Ok ( fail ( " That took too long: sign in again." , None ) ) ;
150
}
151
let Some ( account) = account.filter ( | a | proven | | auth:: verify_password( & a.password_hash, & f.password) ) else {
19bb31ff Andy McCall 1 month ago ↶
152
accounts:: audit( & app.db, None , " login.failed" , & username, " " ) .await.ok ( ) ;
aa0bfbd1 Andy McCall 20 days ago ↶
153
failed ( & app) .await;
19bb31ff Andy McCall 1 month ago ↶
154
return Ok ( fail ( " Wrong username or password." , None ) ) ;
155
} ;
156
if account.banned {
157
return Ok ( fail ( " This account is banned." , None ) ) ;
158
}
6a178055 Andy McCall 1 month ago ↶
159
if account.disabled {
160
accounts:: audit( & app.db, None , " login.disabled" , & username, " " ) .await.ok ( ) ;
161
return Ok ( fail ( " Your account has been disabled. Contact the hub's administrator." , None ) ) ;
162
}
19bb31ff Andy McCall 1 month ago ↶
163
if let Some ( secret) = & account.totp_secret {
164
if f.code.trim ( ) .is_empty ( ) {
a22e4aec Andy McCall 4 days ago ↶
165
166
167
if ! f.use_code.is_empty ( ) {
168
return Ok ( fail ( " " , Some ( username) ) ) ;
169
}
170
if ! f.approval.is_empty ( ) {
171
if ! db:: approvals:: take( & app.db, & f.approval, account.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? {
172
return Ok ( fail ( " Your phone has not approved this sign-in. Try again, or use a code." , Some ( username) ) ) ;
173
}
174
app.throttle.lock ( ) .unwrap ( ) .cleared ( & keys[ 0 ] ) ;
175
accounts:: audit( & app.db, Some ( & account) , " login" , & username, " approved on a phone" ) .await.ok ( ) ;
176
return Ok ( signed_in ( & app, & account, & next) .await) ;
177
}
178
if phones {
179
let agent = headers.get ( header:: USER_AGENT ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .unwrap_or ( " " ) ;
180
let from = if addr.is_empty ( ) { String :: new( ) } else { format! ( " , {addr} " ) } ;
181
let what = format! ( " Sign in from {} {from} " , super :: settings:: browser_of( agent) ) ;
182
let a =
183
crate :: auth:: phone:: ask( & app, & account, " signin" , & what, None ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
184
return Ok ( Page( LoginPage {
185
shell: shell.clone ( ) ,
186
error: None ,
187
next: next.clone ( ) ,
188
totp_for: Some ( username) ,
189
pending: pending.clone ( ) ,
190
reset: false ,
191
providers: vec! [ ] ,
192
approval: Some ( ( a.id, a.number) ) ,
193
phones,
194
} )
195
.into_response ( ) ) ;
196
}
19bb31ff Andy McCall 1 month ago ↶
197
return Ok ( fail ( " Enter the code from your authenticator app." , Some ( username) ) ) ;
198
}
199
let now = ( db:: now_ms( ) / 1000 ) as u64 ;
200
if ! totp:: verify( secret, & f.code, now) {
201
match totp:: use_recovery_code( & account.recovery_codes, & f.code) {
202
Some ( rest) => {
203
accounts:: set_totp( & app.db, account.id, Some ( secret) , & rest) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
204
}
205
None => {
206
accounts:: audit( & app.db, Some ( & account) , " login.totp_failed" , & username, " " ) .await.ok ( ) ;
aa0bfbd1 Andy McCall 20 days ago ↶
207
failed ( & app) .await;
19bb31ff Andy McCall 1 month ago ↶
208
return Ok ( fail ( " That code is not right." , Some ( username) ) ) ;
209
}
210
}
211
}
212
}
aa0bfbd1 Andy McCall 20 days ago ↶
213
app.throttle.lock ( ) .unwrap ( ) .cleared ( & keys[ 0 ] ) ;
19bb31ff Andy McCall 1 month ago ↶
214
accounts:: audit( & app.db, Some ( & account) , " login" , & username, " " ) .await.ok ( ) ;
0024ccb1 Andy McCall 20 days ago ↶
215
Ok ( signed_in ( & app, & account, & next) .await)
a22e4aec Andy McCall 4 days ago ↶
216
}
217
218
# [ derive ( Deserialize ) ]
219
pub struct ApprovalPoll {
220
username : String,
221
pending : String,
222
approval : String,
223
}
224
225
226
227
228
pub async fn approval_state ( State( app ) : State< Shared> , Form( f ) : Form< ApprovalPoll> ) -> Response {
229
let now = db:: now_ms( ) ;
230
let state = async {
231
let account = accounts:: account_by_name( & app.db, f.username.trim ( ) ) .await.ok ( ) ? ? ;
232
if ! auth:: pending_login_ok( & app.cookie_key, & account.username, & account.session_salt, & f.pending, now) {
233
return None ;
234
}
235
let a = db:: approvals:: get( & app.db, & f.approval) .await.ok ( ) ? ? ;
236
if a.account_id ! = account.id | | a.kind ! = " signin" {
237
return None ;
238
}
239
Some ( if a.state = = " pending" & & a.expires_ms < = now { " expired" .to_string ( ) } else { a.state } )
240
}
241
.await;
242
match state {
243
Some ( s) => axum:: Json( serde_json:: json! ( { " state" : s } ) ) .into_response ( ) ,
244
None => ( axum:: http:: StatusCode:: NOT_FOUND , axum:: Json( serde_json:: json! ( { " state" : " unknown" } ) ) ) .into_response ( ) ,
245
}
19bb31ff Andy McCall 1 month ago ↶
246
}
bff9106e Andy McCall 22 days ago ↶
247
248
249
const PENDING_LOGIN_MS : i64 = 5 * 60 * 1000 ;
19bb31ff Andy McCall 1 month ago ↶
250
0024ccb1 Andy McCall 20 days ago ↶
251
252
253
254
pub async fn signed_in ( app : & Shared, account : & accounts:: Account, to : & str ) -> Response {
255
let value = auth:: new_session_id( ) ;
256
if let Err ( e) = db:: sessions:: create( & app.db, account.id, & auth:: token_hash( & value) , & account.session_salt) .await {
257
tracing:: warn! ( " session for {}: {e}" , account.username) ;
258
return ( axum:: http:: StatusCode:: SERVICE_UNAVAILABLE , " Signing in failed for a moment: try again." ) .into_response ( ) ;
259
}
19bb31ff Andy McCall 1 month ago ↶
260
let secure = if app.config.base_url ( ) .starts_with ( " https://" ) { " ; Secure" } else { " " } ;
261
let cookie = format! ( " {} ={value} ; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000{secure} " , auth:: SESSION_COOKIE ) ;
262
( [ ( header:: SET_COOKIE , cookie) ] , Redirect:: to( to) ) .into_response ( )
263
}
264
265
# [ derive ( Deserialize ) ]
266
pub struct CsrfForm {
267
#[serde(rename = "_csrf")]
268
pub csrf : String,
269
}
270
271
pub async fn logout ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< CsrfForm> ) -> Response {
272
if ! viewer.csrf_ok ( & f.csrf) {
273
return Redirect:: to( " /" ) .into_response ( ) ;
274
}
0024ccb1 Andy McCall 20 days ago ↶
275
if let Some ( v) = & viewer.0 {
276
db:: sessions:: end_hash( & app.db, & auth:: token_hash( & v.session) ) .await.ok ( ) ;
277
accounts:: audit( & app.db, Some ( & v.account) , " logout" , & v.account.username, " " ) .await.ok ( ) ;
19bb31ff Andy McCall 1 month ago ↶
278
}
9a8e3b44 Andy McCall 20 days ago ↶
279
let secure = if app.config.base_url ( ) .starts_with ( " https://" ) { " ; Secure" } else { " " } ;
280
let cookie = format! ( " {} =; Path=/; HttpOnly; SameSite=Lax; Max-Age=0{secure} " , auth:: SESSION_COOKIE ) ;
19bb31ff Andy McCall 1 month ago ↶
281
( [ ( header:: SET_COOKIE , cookie) ] , Redirect:: to( " /" ) ) .into_response ( )
282
}
283
284
# [ derive ( Template ) ]
285
# [ template ( path = " register.html" ) ]
286
pub struct RegisterPage {
287
shell : Shell,
288
error : Option< String> ,
289
mode : String,
290
first : bool ,
291
invite : String,
3ff52534 Andy McCall 20 days ago ↶
292
293
810896d4 Andy McCall 21 days ago ↶
294
providers : Vec< ( & 'static str , & 'static str ) > ,
3ff52534 Andy McCall 20 days ago ↶
295
296
provider_query : String,
19bb31ff Andy McCall 1 month ago ↶
297
}
298
299
300
pub async fn registration_mode ( app : & Shared) -> String {
301
accounts:: setting( & app.db, " registration_mode" ) .await.ok ( ) .flatten ( ) .unwrap_or_else ( | | app.config.registration.mode.clone ( ) )
302
}
303
304
# [ derive ( Deserialize ) ]
305
pub struct InviteQuery {
306
invite : Option< String> ,
307
}
308
309
pub async fn register_form ( State( app ) : State< Shared> , viewer : Viewer, Query( q ) : Query< InviteQuery> ) -> Result< Response, Error> {
310
if viewer.signed_in ( ) {
311
return Ok ( Redirect:: to( " /" ) .into_response ( ) ) ;
312
}
313
let shell = Shell:: new( & app, " /register" , & viewer) ;
314
let first = accounts:: account_count( & app.db) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? = = 0 ;
810896d4 Andy McCall 21 days ago ↶
315
let mode = registration_mode ( & app) .await;
3ff52534 Andy McCall 20 days ago ↶
316
let invite = q.invite.unwrap_or_default ( ) .trim ( ) .to_string ( ) ;
317
let usable_invite = mode = = " invite"
318
& & super :: signin:: invite_code_ok( & invite)
319
& & accounts:: invitation_unused( & app.db, & invite) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
320
let providers = if ! first & & ( mode = = " open" | | usable_invite) { super :: signin:: buttons( & app) .await } else { vec! [ ] } ;
321
let provider_query = if usable_invite { format! ( " ?invite={invite} " ) } else { String :: new( ) } ;
322
Ok ( Page( RegisterPage { shell, error: None , mode, first, invite, providers, provider_query } ) .into_response ( ) )
19bb31ff Andy McCall 1 month ago ↶
323
}
324
325
# [ derive ( Deserialize ) ]
326
pub struct RegisterForm {
327
username : String,
328
email : String,
329
password : String,
330
#[serde(default)]
331
invite : String,
c8684d44 Andy McCall 20 days ago
332
333
#[serde(default)]
334
setup : String,
19bb31ff Andy McCall 1 month ago ↶
335
}
336
337
pub async fn register ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< RegisterForm> ) -> Result< Response, Error> {
338
let shell = Shell:: new( & app, " /register" , & viewer) ;
339
let first = accounts:: account_count( & app.db) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? = = 0 ;
340
let mode = registration_mode ( & app) .await;
341
let fail = | msg : & str | {
810896d4 Andy McCall 21 days ago ↶
342
Page( RegisterPage {
343
shell: shell.clone ( ) ,
344
error: Some ( msg.into ( ) ) ,
345
mode: mode.clone ( ) ,
346
first,
347
invite: f.invite.clone ( ) ,
348
providers: vec! [ ] ,
3ff52534 Andy McCall 20 days ago ↶
349
provider_query: String :: new( ) ,
810896d4 Andy McCall 21 days ago ↶
350
} )
351
.into_response ( )
19bb31ff Andy McCall 1 month ago ↶
352
} ;
353
let username = f.username.trim ( ) .to_string ( ) ;
354
if ! crate :: repos:: names:: valid_owner( & username) {
355
return Ok ( fail ( " A username is letters, digits, dots, dashes and underscores, and is not a reserved word." ) ) ;
356
}
357
if let Err ( e) = auth:: password_ok( & f.password) {
358
return Ok ( fail ( e) ) ;
c8684d44 Andy McCall 20 days ago
359
}
360
if first & & ! crate :: setup:: matches( & app, & f.setup) {
361
return Ok ( fail (
362
" This hub has no administrator yet. The first account needs the setup code the server logged when it started (also in setup-code in its data directory)." ,
363
) ) ;
19bb31ff Andy McCall 1 month ago ↶
364
}
365
if ! first {
366
match mode.as_str ( ) {
367
" open" => { }
368
" invite" => {
369
let ok = ! f.invite.trim ( ) .is_empty ( )
370
& & accounts:: invitation_unused( & app.db, f.invite.trim ( ) ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
371
if ! ok {
372
return Ok ( fail ( " Registration needs a valid invitation code." ) ) ;
373
}
374
}
375
_ => return Ok ( fail ( " Registration is closed." ) ) ,
376
}
377
}
378
if accounts:: account_by_name( & app.db, & username) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? .is_some ( )
379
| | accounts:: org_by_name( & app.db, & username) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? .is_some ( )
6a178055 Andy McCall 1 month ago ↶
380
| | accounts:: username_retired( & app.db, & username) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
19bb31ff Andy McCall 1 month ago ↶
381
{
382
return Ok ( fail ( " That name is taken." ) ) ;
383
}
384
let hash = auth:: hash_password( & f.password) .map_err ( | e | Error:: bad( shell.clone ( ) , e.to_string ( ) ) ) ? ;
385
let account = accounts:: create_account( & app.db, & username, f.email.trim ( ) , & hash, & auth:: random_hex( 16 ) , first)
386
.await
387
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
388
if ! first & & mode = = " invite" {
389
accounts:: use_invitation( & app.db, f.invite.trim ( ) , account.id) .await.ok ( ) ;
c8684d44 Andy McCall 20 days ago
390
}
391
if first {
392
crate :: setup:: spend( & app) ;
19bb31ff Andy McCall 1 month ago ↶
393
}
394
accounts:: audit( & app.db, Some ( & account) , " register" , & username, if first { " first account: administrator" } else { " " } ) .await.ok ( ) ;
34beb1a7 Andy McCall 1 month ago ↶
395
if let Err ( e) = super :: recovery:: send_verification( & app, & account) .await {
396
tracing:: info! ( " verification for {username}: {e}" ) ;
bff9106e Andy McCall 22 days ago ↶
397
}
398
399
if ! first {
400
let admins: Vec< ( i64 , crate:: db:: notify:: Reason) > = accounts:: accounts( & app.db)
401
.await
402
.unwrap_or_default ( )
403
.into_iter ( )
404
.filter ( | a | a.is_admin )
405
.map ( | a | ( a.id, crate :: db:: notify:: Reason:: Participating) )
406
.collect ( ) ;
407
let title = format! ( " New account: {username} signed up on {} " , app.config.site.name) ;
408
crate :: notify:: tell( & app, " account.registered" , & title, & format! ( " /{username} " ) , & username, None , admins) .await;
34beb1a7 Andy McCall 1 month ago ↶
409
}
0024ccb1 Andy McCall 20 days ago ↶
410
Ok ( signed_in ( & app, & account, " /" ) .await)
19bb31ff Andy McCall 1 month ago ↶
411
}