Files
HealthLog/src/lib/auth/security-activity.ts
T
Marc-André Bombeck 9269043e91 feat(security): list active sessions, sign out everywhere, and show security activity
Add the user-facing account-security surface in Settings:

- Active sessions (closes #64): GET /api/auth/me/sessions lists each browser
  login with a coarse device label, masked IP, resolved location, the sliding
  last-active stamp, and the current-session marker. DELETE the collection
  signs out everywhere else (keeps the current session, revokes other sessions
  and native refresh tokens, leaves API tokens alone); DELETE one revokes a
  single session, scoped to the owner so a foreign id 404s.
- A sliding last-active stamp on the session, written throttled and
  fire-and-forget off the request-resolution path.
- Security activity: GET /api/auth/me/security-activity, the shared read over
  the audit log filtered to sign-ins, second-factor changes, password changes,
  session revocations, exports, and deletions, with masked IPs.

OpenAPI regenerated for the new endpoints.
2026-06-27 20:34:43 +02:00

42 lines
1.5 KiB
TypeScript

/**
* v1.23 — security-activity feed. The SHARED read both the account-security
* surface and the data-sovereignty privacy dashboard consume: a user-scoped,
* read-only window onto the `AuditLog` rows that matter for account security —
* logins, MFA events, password changes, token/session revocations, exports,
* and deletions. No new storage: it reuses the existing
* `@@index([userId, action, createdAt(sort: Desc)])`.
*/
/**
* Whether an audit action belongs in the user-facing security-activity feed.
* Pure so the contract can be unit-tested without a DB:
* - every `auth.*` event (login, mfa, password, token, bearer, session,
* register),
* - every export action (`export.download`, `health-record.export`,
* `user.export.*` — all contain "export"),
* - the two erasure actions (`user.account.delete`, `user.data.clear`).
*/
export function matchesSecurityActivity(action: string): boolean {
return (
action.startsWith("auth.") ||
action.includes("export") ||
action === "user.account.delete" ||
action === "user.data.clear"
);
}
/**
* Prisma `where` mirror of `matchesSecurityActivity`, scoped to one user. Kept
* alongside the predicate so the two never drift.
*/
export function securityActivityWhere(userId: string) {
return {
userId,
OR: [
{ action: { startsWith: "auth." } },
{ action: { contains: "export" } },
{ action: { in: ["user.account.delete", "user.data.clear"] } },
],
};
}