Skip to main content

Sessions & Login History — Technical Reference

← Back to Sessions & Login History

Where this lives

Backend

Frontend

Technical implementation checklist

  • activeSessions — resolver wired in user-sessions.resolver.js; SessionsSettingsPage.tsx queries it live
  • getCurrentSession — resolver wired. Fixed this session: sessions-devices.manager.js#getCurrentSession used to return hardcoded mock data (a fake "MacBook Pro" session) nested under { session, security } keys the schema never declared. It now does a real lookup: the resolver passes context.sessionId — decoded straight off the caller's JWT in graphql/context/auth-helper.js — to the manager, which fetches that session by ID (scoped to the authenticated user) and returns the flat UserSession shape the schema expects, with isCurrent always true since the lookup is keyed by the requesting session itself. Still not called anywhere in SessionsSettingsPage.tsx
  • revokeSession — resolver wired; SessionsSettingsPage.tsx wires it to a real button. terminateSession is wired as a genuine alias (same underlying userManager.revokeSession call)
  • revokeAllOtherSessions — resolver wired; terminateOtherSessions is wired as an alias (same underlying userManager.revokeAllOtherSessions call); SessionsSettingsPage.tsx wires revokeAllOtherSessions to a real button. Fixed this session: the manager used to return its count as revoked_count, which didn't match the schema's sessionsTerminated field name, so sessionsTerminated always resolved to null and the frontend's res?.sessionsTerminated ?? 0 always showed "0" even when sessions were revoked. Both revokeAllOtherSessions and terminateAllSessions (see next item) now return sessionsTerminated directly, matching the schema.
  • terminateAllSessionsFixed this session: this used to just be an alias of revokeAllOtherSessions, so despite its name it never actually signed out the calling device — it only revoked every other session, same as terminateOtherSessions. userManager.terminateAllSessions is now a genuinely distinct method (sessions-devices.manager.js#terminateAllSessions, backed by userSessionAccessService.revokeAllSessions) that terminates every session for the user, including the one making the request.
  • loginHistory — resolver wired and SessionsSettingsPage.tsx queries it live. Fixed this session: sessions-devices.manager.js#getLoginHistory used to return { login_history: [...], total, limit, offset } while the schema expects { entries: [...], total, limit, offset } — the key mismatch meant entries (non-null) resolved to null, so the query threw a GraphQL error on every call and the UI showed an always-empty panel. It now returns { entries: [...], total, limit, offset } with the schema's id/userId/loginMethod/deviceName/deviceType/browser/os/ipAddress/location/success/failureReason/createdAt field names per entry, sourced from real UserSession rows. loginMethod/success/failureReason are still placeholders (hardcoded to 'password'/true/null) — there's no dedicated login-attempt log in the codebase yet (only successful logins ever create a session row), so failed attempts and real auth methods aren't reported. That's a separate gap from the shape bug, left as-is
  • sessionDetails — resolver wired in user-sessions.resolver.js; returns null (not an error) if the session doesn't exist or belongs to another user, so it can't be used to probe for valid session IDs. Not called anywhere in SessionsSettingsPage.tsx
  • adminActiveSessions / adminRevokeSession / adminRevokeAllSessions — resolvers wired in admin-user.resolver.js; no UI in apps/frontend-nextjs, but wired to the admin's own "Mi cuenta" page in apps/frontend-admin (app/account/page.tsx) — see Admin Accounts
  • logoutadded this session, alongside a real fix: session revocation (revokeSession/terminateAllSessions/etc.) used to be pure theater on the web client. graphql/context/auth-helper.js#verifyTokenAndGetUser only checked the JWT's signature/expiry - it never consulted user_session, so a revoked/logged-out token kept authenticating every request until it naturally expired (up to 7 days in production). It now also calls userSessionAccessService.isValid(decoded.sessionId) and rejects the request if that session is inactive, revoked, or past expiresAt (skipped for legacy tokens with no sessionId claim). logout is a new, argument-free, self-service mutation added so a client never has to know/pass its own session id: it revokes context.sessionId (the requesting session itself) via the same userManager.revokeSession used by revokeSession/terminateSession. AuthContext.tsx's logout()/logoutAccount()/logoutAll() now call it (or, for logoutAccount/logoutAll, a raw fetch carrying that specific stored account's own token, since Apollo's authLink always sends the active account's token) before clearing local state - best-effort, so an offline revoke never blocks signing out client-side. Mirrors the equivalent fix already shipped on the admin side (admin-auth-helper.js/admin-session.manager.js).

Active sessions

activeSessions returns all currently valid sessions for the authenticated user. currentSession is the session making the request — useful for labeling "This device" in the UI. The full sessions list lets users review unfamiliar devices and terminate them.

sessionDetails fetches the metadata for a specific session by ID — useful in a detail view when a user taps on a session row.

getCurrentSession is a lightweight alias that returns only the calling session — use it to populate the "Current device" card without fetching the full session list. It now does a real lookup by the session ID embedded in the caller's JWT (context.sessionId) rather than returning mock data — see the checklist above.

query ActiveSessions {
activeSessions {
total
currentSession { id deviceName deviceType browser os ipAddress location lastActivity }
sessions {
id deviceName deviceType browser os
ipAddress location isCurrent
lastActivity createdAt expiresAt
}
}
}

query SessionDetails($sessionId: ID!) {
sessionDetails(sessionId: $sessionId) { id deviceName deviceType browser os ipAddress location lastActivity }
}

query CurrentSession {
getCurrentSession { id deviceName deviceType browser os ipAddress isCurrent lastActivity }
}

UserSession fields

FieldDescription
deviceNamee.g. "iPhone 15 Pro"
deviceTypee.g. "mobile", "desktop"
browserBrowser name
osOperating system
ipAddressIP at time of login
locationGeo-derived city/country
isCurrentTrue for the calling session
expiresAtWhen the session token expires

Terminating sessions

terminateSession invalidates a single session by ID — the device using that session will be signed out on its next API request.

terminateOtherSessions is implemented in user-sessions.resolver.js as a direct alias of revokeAllOtherSessions (same userManager.revokeAllOtherSessions call) — it only revokes every other session and keeps the current one alive. terminateAllSessions is now a genuinely distinct mutation (userManager.terminateAllSessions): true to its name, it terminates every session for the user, including the one making the request.

revokeSession and revokeAllOtherSessions are the underlying single-session and others-only mutations; terminateSession and terminateOtherSessions are aliases with the same semantics as their revoke* counterparts, added for semantic clarity in security-context flows. terminateAllSessions has no revoke*-named counterpart — it's the only mutation that signs out the current device along with everything else.

Terminating a session forces a sign-out on that device. The user will need to re-authenticate.

# Sign out a specific device
mutation TerminateSession($sessionId: ID!) { terminateSession(sessionId: $sessionId) { success sessionsTerminated } }

# Sign out all devices (including this one)
mutation TerminateAllSessions { terminateAllSessions { success sessionsTerminated } }

# Sign out all OTHER devices, keep the current session
mutation TerminateOtherSessions { terminateOtherSessions { success sessionsTerminated } }

mutation RevokeSession($sessionId: ID!) { revokeSession(sessionId: $sessionId) { success } }
mutation RevokeAllOtherSessions { revokeAllOtherSessions { success sessionsTerminated } }

logout — self-service sign-out (no sessionId argument)

logout revokes the session making the request itself, resolved server-side from context.sessionId (the JWT's own sessionId claim) — a client never passes a session id, so it can't target anyone else's session. This is what actually makes "Log out" invalidate the JWT server-side; without it (before this session), the token stayed valid until its natural expiry regardless of any client-side logout.

mutation Logout { logout { success message } }

apps/frontend-nextjs/src/contexts/AuthContext.tsx calls this from logout() before clearing local state. Every request's JWT signature is now paired with a live-session check (userSessionAccessService.isValid, in graphql/context/auth-helper.js) — see the logout checklist item above for the full before/after.

Login history

loginHistory returns a full audit trail of sign-in attempts — both successful and failed. loginMethod identifies how the user tried to authenticate. Failed attempts include a failureReason (e.g. "invalid_password", "account_suspended"). Use this to surface a "Suspicious activity detected" alert when the user sees failed attempts they don't recognize.

query LoginHistory($limit: Int, $offset: Int) {
loginHistory(limit: $limit, offset: $offset) {
total limit offset
entries {
id loginMethod
deviceName deviceType browser os
ipAddress location
success failureReason
createdAt
}
}
}

loginMethod values match the auth methods: email, google, apple, phone.

The response shape now matches this schema (see the loginHistory checklist item above for the shape bug that was fixed). loginMethod, success, and failureReason are still placeholder values today ('password', true, null on every entry) — there's no failed-login-attempt log backing them yet, so the "Suspicious activity" use case above isn't actually deliverable until that's built.