| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package auth |
| 4 | |
| 5 | import ( |
| 6 | "net/http" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 10 | ) |
| 11 | |
| 12 | // settingsSessionsList renders GET /settings/sessions. |
| 13 | // |
| 14 | // V1 surfaces only the current session (we use AEAD-encrypted cookies, |
| 15 | // no server-side session table — there's no enumerable list). The page's |
| 16 | // real value is the "Sign out everywhere" affordance, which bumps |
| 17 | // users.session_epoch and invalidates every cookie that carries the old |
| 18 | // epoch on its next request. |
| 19 | func (h *Handlers) settingsSessionsList(w http.ResponseWriter, r *http.Request) { |
| 20 | h.renderSessionsList(w, r, "") |
| 21 | } |
| 22 | |
| 23 | // settingsSessionsLogoutAll handles POST /settings/sessions/logout-everywhere. |
| 24 | func (h *Handlers) settingsSessionsLogoutAll(w http.ResponseWriter, r *http.Request) { |
| 25 | user := middleware.CurrentUserFromContext(r.Context()) |
| 26 | |
| 27 | if err := h.q.BumpUserSessionEpoch(r.Context(), h.d.Pool, user.ID); err != nil { |
| 28 | h.d.Logger.ErrorContext(r.Context(), "sessions: bump", "error", err) |
| 29 | h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "") |
| 30 | return |
| 31 | } |
| 32 | |
| 33 | // Re-sync the current session's epoch so this browser doesn't sign |
| 34 | // itself out alongside the others. |
| 35 | epoch, err := h.q.GetUserSessionEpoch(r.Context(), h.d.Pool, user.ID) |
| 36 | if err != nil { |
| 37 | h.d.Logger.ErrorContext(r.Context(), "sessions: read epoch", "error", err) |
| 38 | h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "") |
| 39 | return |
| 40 | } |
| 41 | s := middleware.SessionFromContext(r.Context()) |
| 42 | s.Epoch = epoch |
| 43 | if err := h.d.SessionStore.Save(w, r, s); err != nil { |
| 44 | h.d.Logger.ErrorContext(r.Context(), "sessions: save", "error", err) |
| 45 | h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "") |
| 46 | return |
| 47 | } |
| 48 | |
| 49 | h.renderSessionsList(w, r, "Signed out of every other session.") |
| 50 | } |
| 51 | |
| 52 | // renderSessionsList is the shared render path. The "current session" |
| 53 | // row pulls IssuedAt out of the loaded session struct. |
| 54 | func (h *Handlers) renderSessionsList(w http.ResponseWriter, r *http.Request, successMsg string) { |
| 55 | s := middleware.SessionFromContext(r.Context()) |
| 56 | issued := time.Unix(s.IssuedAt, 0) |
| 57 | |
| 58 | h.renderPage(w, r, "settings/sessions", map[string]any{ |
| 59 | "Title": "Sessions", |
| 60 | "CSRFToken": middleware.CSRFTokenForRequest(r), |
| 61 | "SettingsActive": "sessions", |
| 62 | "Issued": issued, |
| 63 | "UserAgent": r.Header.Get("User-Agent"), |
| 64 | "ClientIP": clientIP(r), |
| 65 | "Success": successMsg, |
| 66 | }) |
| 67 | } |
| 68 |