User Security Logs Implementation
Overview
Replaced the old /me/security/login-log endpoint (which only read from audit_logs table) with a new /me/security/logs endpoint that uses the Internal Logs Service to show users their security-related logs.
Update: The list is implemented via lib/services/me/logs (createUserLogsService(env) and listUserLogs(userId, options)). The route parses query params and calls the logs service; the service uses getService('internalLogger').getLogs(...) with per-user index (when userId is set) and cursor-based pagination (default limit 100, max 500). Delete/clear endpoints return a policy message (retention and who may delete to be defined by institution).
See also: Internal logger improvement plan — per-user index, cursor API, and service adoption.
What Changed
1. List implementation (service + route)
Service: apps/worker/src/lib/services/me/logs/index.ts — listUserLogs(userId, { limit, cursor, level, category }); cursor-based; uses internal logger’s per-user index.
Route: apps/worker/src/routes/me/security.ts — parses query and calls logsService.list(userId, options).
GET
/me/security/logs: List user's security logs (lockouts, login failures, session tampering, etc.)- Query params:
limit(default 100, max 500),cursor(optional),level(optional),category(optional:auth|security) - Returns
{ data: InternalLogEntry[], pagination: { limit, nextCursor?, hasMore } }
- Query params:
DELETE
/me/security/logsand DELETE/me/security/logs/{id}: Return 403 with a policy message (retention and who may delete to be defined by institution).
2. Enhanced Internal Logs Filtering
Updated Files:
apps/worker/src/types/cache/internal.ts: AddeduserIdtoInternalLogFiltersapps/worker/src/lib/services/cache/multi-shard-reader/index.ts: AddeduserIdfilter supportapps/worker/src/lib/services/cache/internal/index.ts: UpdatedgetLogs()to filter byuserId
3. Enhanced Logging Methods
Updated Files:
apps/worker/src/lib/services/cache/internal/index.ts:- Added
logAuth()method for auth events (category:auth, supportsuserId) - Updated
logWarning(),logError()to acceptuserIdandipAddressparameters
- Added
apps/worker/src/lib/services/internal/index.ts:- Updated
warning(),error(),info()to acceptuserIdandipAddressparameters info()now useslogAuth()for auth-related events (e.g.,login_success)
- Updated
apps/worker/src/lib/services/auth/login/index.ts:- Updated all logging calls to include
userIdandipAddresswhere applicable - Changed invalid password attempts from
warning()tosuspicious()(includes userId) - Login success now includes userId via
info()with userId parameter
- Updated all logging calls to include
What Logs Are Included
Users can now see all security-related logs that include their userId:
Lockout events (
category: 'security',level: 'suspicious')- Account locked due to too many failed attempts
- Lockout reason and remaining time
Login failures (
category: 'security',level: 'suspicious')- Invalid password attempts
- Attempt count
Login success (
category: 'auth',level: 'info')- Successful login events
- Device info, location, risk level
Session tampering (future)
- Suspicious session activity
- Session revocation events
Other security events (
category: 'security')- Any other security-related logs that include the user's
userId
- Any other security-related logs that include the user's
Filtering logic and consistency
- Internal logger: All entries are stored with optional
userId(andcategory,level, etc.). There is no index by userId in the cache; the index is per-shard by log id + timestamp (newest first). - Multi-shard reader (
getLogs({ userId, category, level, limit })): For each shard it walks the full index (id + timestamp), fetches each entry from the DO cache, then applies:userId: entry is kept only ifentry.userId === filters.userIdcustomFilter: level and category (and optional service) on the entry So we do not read “only the user’s slice” from storage: we read a bounded window of the global stream (by limit), then filter by userId and category/level. That makes listing consistent and reliable (same data as written, correct filtering), but not “faster” than reading all logs: we still scan index entries and fetch until we have enough matches. For a single user with many logs, we may fetch more entries thanlimitto fill one page.
- services/me/logs (
listUserLogs): CallsinternalLogger.getLogs({ userId, category, level, limit: limit * page }), then restricts toauth/securitywhencategoryis not set, and slices for the requested page. So all user actions that are logged withuserIdset on the internal log entry are accessible via this service; behavior is consistent and reliable. Performance is dominated by the multi-shard read (no separate “user log” store).
Pagination
Currently offset-based:
- Request asks for
limit * pageentries from the internal logger (with userId + optional category/level). - Service then slices to the requested page:
skip = (page - 1) * limit.
Future improvement: Use cursor-based pagination from MultiShardReader (e.g. nextCursor) so we don’t over-fetch for high page numbers.
Who writes internal logs with userId (user-visible in GET /me/security/logs)
Only entries that have userId set on the log entry are returned when filtering by that user. Currently:
| Service / area | Method | category | userId set? | Visible in /me/security/logs |
|---|---|---|---|---|
Auth login (auth/login/index.ts) | logger.info, logger.suspicious, logger.warning | auth / security | Yes (when user exists) | Yes |
| Auth register | logger.logAuthEvent('register_success', userId, …) | auth | Yes | Yes |
| Auth logout | logger.logAuthEvent('logout_success', session.userId, …) | auth | Yes | Yes |
| Auth profile | logger.logSystemEvent(...) | system (error) | No | No |
| Maintenance core | internalLogger.log(service, message, details, context) | — | No (signature has no userId; currently passes userId as message by mistake) | No |
So today login, register, and logout events that pass userId are visible. Maintenance actions are not tied to userId on the entry; to include them in user logs, maintenance should call a logger method that accepts userId (e.g. internalLogger.warning('maintenance', message, details, context, userId, ipAddress)) and ensure the stored entry has userId set.
Where else to use the internal logger
Consider using the internal logger (with userId where relevant) for:
- Security / 2FA: backup code use, 2FA enable/disable, secure-action verification (so they appear in user security logs).
- Sensitive account actions: email change, password change, session revoke (already have some logging; ensure
userIdis set). - Maintenance: as above, pass
userId(and optionallyipAddress) so per-user maintenance actions show under/me/security/logsif desired. - User management (admin): admin actions on a user (suspend, reset, etc.) — can log with that user’s id for audit and optional “view as user” logs later.
- API / rate limits: critical or suspicious API abuse with optional
userIdwhen authenticated. - System health: keep using without
userIdfor system-wide issues (these stay out of user-scoped lists).
TODOs
Log Deletion: Implement actual deletion via DO endpoint
- Currently returns
501 Not Implemented - Requires DO storage API to delete entries and update index
- Currently returns
Log Clearing: Implement clearing all user logs via DO endpoint
- Currently returns
501 Not Implemented - Requires iterating through all shards and deleting entries
- Currently returns
Cursor Pagination: Switch from offset-based to cursor-based pagination
- Use
MultiShardReadercursor support for better performance
- Use
Benefits
- Unified Logging: All security logs in one place (internal logs cache)
- Better Performance: DO cache is faster than DB queries for read-heavy operations
- More Comprehensive: Users see all security events, not just login attempts
- Real-time: Logs are available immediately (no DB write delay)
- Scalable: Multi-shard support handles large log volumes
Types
- User logs list:
UserLogsListOptions,UserLogsListResultlive insrc/types/me/logs.ts(domain: me). Re-exported from@/types/meand fromlib/services/me/logsfor convenience. - Internal log entry and filters:
InternalLogEntry,InternalLogFiltersinsrc/types/cache/internal.ts.
Migration Notes
- Old endpoint
/me/security/login-logis replaced by/me/security/logs - List is implemented by
lib/services/me/logs(user logs service) androutes/me/security.ts - Schema:
SecurityLogsQuerySchemainlib/services/zod/me/security.ts(optionallevel,category; limit default 100, max 500)