31 lines
1.3 KiB
Go
31 lines
1.3 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"felis.lolicon.best/internal/metrics"
|
|
)
|
|
|
|
// Passwordless auth handlers (spec §B). Staff (Owner/Operator) authenticate via
|
|
// email-OTP / passkey + in-game approve on op.console; players via bind code or
|
|
// email-OTP on console. There is NO password login path. This file holds only the
|
|
// logout handler — the login doors live in handlers_auth_email.go (email OTP),
|
|
// handlers_onboard.go (bind code), and the deferred passkey-login slice.
|
|
|
|
// handleLogout revokes the presented session and clears the cookie (spec §B). It
|
|
// is mounted Public and idempotent: it reads the cookie directly, so it works even
|
|
// when the session has already expired and never errors on a missing one. Ending
|
|
// a live session is audited under its account; a dead cookie leaves no row.
|
|
func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
if c, err := r.Cookie(sessionCookieName); err == nil && c.Value != "" {
|
|
hash := hashCookie(c.Value)
|
|
u, uerr := a.Repo.SessionUser(r.Context(), hash, a.now())
|
|
if err := a.Repo.RevokeSession(r.Context(), hash); err == nil && uerr == nil {
|
|
metrics.SessionsRevokedTotal.WithLabelValues("logout").Inc()
|
|
a.auditEntry(r, AuditEntry{Actor: u.Username, ActorUserID: u.ID, Action: "auth.logout"})
|
|
}
|
|
}
|
|
clearSessionCookie(w)
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|