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