Sessions & Login History — Technical Reference
← Back to Sessions & Login History
Where this lives
Backend
apps/backend/graphql/resolvers/user-sessions.resolver.js— implementsactiveSessions,sessionDetails,loginHistory,getCurrentSession,revokeSession,revokeAllOtherSessions, and theterminateSession/terminateAllSessions/terminateOtherSessionsaliases (moved here from the old monolithicuser.resolver.js, which no longer exists — it was split into per-concern resolver files). All schema-declared session fields now have a resolver wired up, includingsessionDetailsand theterminate*aliasesapps/backend/graphql/resolvers/admin-user.resolver.js— admin-side session management:adminActiveSessions,adminRevokeSession,adminRevokeAllSessionsapps/backend/graphql/types/sessions.type.js— schema definitionsapps/backend/managers/user-managers/sessions-devices.manager.js— session listing/revocation business logicapps/backend/managers/admin-managers/admin-session.manager.js— admin session management logicapps/backend/data-access-services/user/user-session.access-service.jsandadmin/admin-session.access-service.js— session database access (both moved intouser//admin/subfolders as part of a repo-wide access-service reorganization)
Frontend
apps/frontend-nextjs/src/page-components/settings/SessionsSettingsPage.tsx(routeapp/settings/sessions) — lists active sessions, a "View details" expander per session, revoke-one and revoke-all-others buttons, and a collapsible login-history panel. It declares its own inlinegqlqueries/mutations rather than using generated hooks.apps/frontend-admin/src/app/account/page.tsx— the admin's own "Mi cuenta" page has a sessions section wired toadminActiveSessions/adminRevokeSession/adminRevokeAllSessions(the admin's own login sessions, not a regular user's — see Admin Accounts for the full write-up of that flow).
Technical implementation checklist
-
activeSessions— resolver wired inuser-sessions.resolver.js;SessionsSettingsPage.tsxqueries it live -
getCurrentSession— resolver wired. Fixed this session:sessions-devices.manager.js#getCurrentSessionused 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 passescontext.sessionId— decoded straight off the caller's JWT ingraphql/context/auth-helper.js— to the manager, which fetches that session by ID (scoped to the authenticated user) and returns the flatUserSessionshape the schema expects, withisCurrentalwaystruesince the lookup is keyed by the requesting session itself. Still not called anywhere inSessionsSettingsPage.tsx -
revokeSession— resolver wired;SessionsSettingsPage.tsxwires it to a real button.terminateSessionis wired as a genuine alias (same underlyinguserManager.revokeSessioncall) -
revokeAllOtherSessions— resolver wired;terminateOtherSessionsis wired as an alias (same underlyinguserManager.revokeAllOtherSessionscall);SessionsSettingsPage.tsxwiresrevokeAllOtherSessionsto a real button. Fixed this session: the manager used to return its count asrevoked_count, which didn't match the schema'ssessionsTerminatedfield name, sosessionsTerminatedalways resolved tonulland the frontend'sres?.sessionsTerminated ?? 0always showed "0" even when sessions were revoked. BothrevokeAllOtherSessionsandterminateAllSessions(see next item) now returnsessionsTerminateddirectly, matching the schema. -
terminateAllSessions— Fixed this session: this used to just be an alias ofrevokeAllOtherSessions, so despite its name it never actually signed out the calling device — it only revoked every other session, same asterminateOtherSessions.userManager.terminateAllSessionsis now a genuinely distinct method (sessions-devices.manager.js#terminateAllSessions, backed byuserSessionAccessService.revokeAllSessions) that terminates every session for the user, including the one making the request. -
loginHistory— resolver wired andSessionsSettingsPage.tsxqueries it live. Fixed this session:sessions-devices.manager.js#getLoginHistoryused to return{ login_history: [...], total, limit, offset }while the schema expects{ entries: [...], total, limit, offset }— the key mismatch meantentries(non-null) resolved tonull, 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'sid/userId/loginMethod/deviceName/deviceType/browser/os/ipAddress/location/success/failureReason/createdAtfield names per entry, sourced from realUserSessionrows.loginMethod/success/failureReasonare 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 inuser-sessions.resolver.js; returnsnull(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 inSessionsSettingsPage.tsx -
adminActiveSessions/adminRevokeSession/adminRevokeAllSessions— resolvers wired inadmin-user.resolver.js; no UI inapps/frontend-nextjs, but wired to the admin's own "Mi cuenta" page inapps/frontend-admin(app/account/page.tsx) — see Admin Accounts -
logout— added 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#verifyTokenAndGetUseronly checked the JWT's signature/expiry - it never consulteduser_session, so a revoked/logged-out token kept authenticating every request until it naturally expired (up to 7 days in production). It now also callsuserSessionAccessService.isValid(decoded.sessionId)and rejects the request if that session is inactive, revoked, or pastexpiresAt(skipped for legacy tokens with nosessionIdclaim).logoutis a new, argument-free, self-service mutation added so a client never has to know/pass its own session id: it revokescontext.sessionId(the requesting session itself) via the sameuserManager.revokeSessionused byrevokeSession/terminateSession.AuthContext.tsx'slogout()/logoutAccount()/logoutAll()now call it (or, forlogoutAccount/logoutAll, a rawfetchcarrying 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
| Field | Description |
|---|---|
deviceName | e.g. "iPhone 15 Pro" |
deviceType | e.g. "mobile", "desktop" |
browser | Browser name |
os | Operating system |
ipAddress | IP at time of login |
location | Geo-derived city/country |
isCurrent | True for the calling session |
expiresAt | When 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
loginHistorychecklist item above for the shape bug that was fixed).loginMethod,success, andfailureReasonare still placeholder values today ('password',true,nullon 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.