Keep when each account was added and when it last signed in
users.created comes back, beside a new last_login, for whoever maintains the server. A password sign-in, the token link and a request through the proxy all count, recorded to the hour so the proxy's per-request vouching is not a write each time. Settings -> Users and ipx user list show both. The three user queries now share one row mapping. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
This commit is contained in:
20
src/web.rs
20
src/web.rs
@@ -133,6 +133,11 @@ async fn auth(State(state): State<WebState>, mut req: Request, next: Next) -> Re
|
||||
None
|
||||
}
|
||||
};
|
||||
// Every request comes vouched for; signed_in keeps one an hour. Failing to note the time
|
||||
// must not turn anyone away, so its error goes unanswered.
|
||||
if let Some(u) = &user {
|
||||
let _ = state.ctx.db.signed_in(u.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. A session cookie from signing in here.
|
||||
@@ -157,6 +162,10 @@ async fn auth(State(state): State<WebState>, mut req: Request, next: Next) -> Re
|
||||
if supplied.is_some_and(|t| constant_time_eq(&t, &token)) {
|
||||
user = admin_user(&state);
|
||||
if from_query.is_some() {
|
||||
// The token link is a sign-in; the cookie it leaves behind is not one each time.
|
||||
if let Some(u) = &user {
|
||||
let _ = state.ctx.db.signed_in(u.id);
|
||||
}
|
||||
set_cookie = Some(format!(
|
||||
"{COOKIE}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=31536000"
|
||||
));
|
||||
@@ -253,6 +262,7 @@ async fn login(
|
||||
let user = user.expect("verified above");
|
||||
let token = crate::auth::new_session_token();
|
||||
state.ctx.db.create_session(user.id, &token)?;
|
||||
state.ctx.db.signed_in(user.id)?;
|
||||
tracing::info!(user = %user.name, "signed in");
|
||||
|
||||
let days = state.ctx.cfg().web.session_days.max(1);
|
||||
@@ -317,6 +327,7 @@ async fn list_users(
|
||||
.map(|u| {
|
||||
serde_json::json!({
|
||||
"id": u.id, "name": u.name, "admin": u.is_admin, "password": u.pass_hash.is_some(),
|
||||
"created": u.created, "last_login": u.last_login,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -748,7 +759,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn only_the_last_admin_is_protected() {
|
||||
let u = |id, is_admin| crate::db::User { id, name: format!("u{id}"), pass_hash: None, is_admin };
|
||||
let u = |id, is_admin| crate::db::User {
|
||||
id,
|
||||
name: format!("u{id}"),
|
||||
pass_hash: None,
|
||||
is_admin,
|
||||
created: None,
|
||||
last_login: None,
|
||||
};
|
||||
assert!(last_admin(&[u(1, true), u(2, false)], 1));
|
||||
assert!(!last_admin(&[u(1, true), u(2, true)], 1), "another admin remains");
|
||||
assert!(!last_admin(&[u(1, true), u(2, false)], 2), "not an admin at all");
|
||||
|
||||
Reference in New Issue
Block a user