feat: stamp users.last_seen on login and token refresh

One conditional UPDATE writes it at most once per five minutes per
user. A failed write is logged and never fails authentication, and no
user payload carries the column.
This commit is contained in:
Jakub Zych
2026-10-05 13:26:53 +02:00
parent f7ca3c4f59
commit c7d9d7d25d
3 changed files with 272 additions and 1 deletions

View File

@@ -108,6 +108,10 @@ func Login(app *backpack.App) http.HandlerFunc {
_ = gdb.WithContext(ctx).Model(user).Update("password", next).Error
}
}
// A failed last_seen write is logged and never fails the login.
if err := classes.TouchLastSeen(ctx, gdb, user.ID); err != nil {
slog.Default().Warn("golem15.user: stamp last_seen on login", "user_id", user.ID, "err", err)
}
token, err := mintFor(app, secret, user, requestURL(r))
if err != nil {
writeOpaque500(w)
@@ -174,7 +178,7 @@ func Fetch(app *backpack.App) http.HandlerFunc {
// Refresh slides a still-refreshable bearer token.
func Refresh(app *backpack.App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, secret, ok := sessionDeps(w, app)
gdb, secret, ok := sessionDeps(w, app)
if !ok {
return
}
@@ -195,6 +199,15 @@ func Refresh(app *backpack.App) http.HandlerFunc {
})
return
}
// The refresh never loads the user row: the subject comes from the
// new token. A failed last_seen write is logged and never fails it.
if sub, _, _, _, err := bouncer.VerifyClaims(next, secret); err == nil {
if id, err := strconv.ParseUint(sub, 10, 64); err == nil && id != 0 {
if err := classes.TouchLastSeen(r.Context(), gdb, uint(id)); err != nil {
slog.Default().Warn("golem15.user: stamp last_seen on refresh", "user_id", id, "err", err)
}
}
}
writeJSON(w, http.StatusOK, map[string]any{"token": next})
}
}