diff --git a/services/platform-service/src/modules/auth/oauth/routes.ts b/services/platform-service/src/modules/auth/oauth/routes.ts index d17a5b4b..cfe2018a 100644 --- a/services/platform-service/src/modules/auth/oauth/routes.ts +++ b/services/platform-service/src/modules/auth/oauth/routes.ts @@ -23,6 +23,8 @@ import { OAuthGoogleSchema, OAuthMicrosoftSchema, OAuthAppleSchema } from './typ import type { UserDoc, AuthProviderDoc } from '../types.js'; import * as subscriptionRepo from '../../subscriptions/repository.js'; import * as licenseRepo from '../../licenses/repository.js'; +import * as loginEventRepo from '../login-events/repository.js'; +import { scoreLoginRisk } from '../login-events/risk-scorer.js'; export async function oauthRoutes(app: FastifyInstance) { // ── Shared OAuth login helper ───────────────────────────── @@ -176,6 +178,37 @@ export async function oauthRoutes(app: FastifyInstance) { }); const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId }); + // Record OAuth login event (best-effort) + const ip = req.ip || 'unknown'; + const method = `oauth_${provider}` as 'oauth_google' | 'oauth_microsoft' | 'oauth_apple'; + try { + const recentFailures = await loginEventRepo.countRecentFailures(user.id, 15 * 60 * 1000); + const risk = scoreLoginRisk({ + ip, + isNewIp: true, + isNewDevice: true, + isDeviceTrusted: false, + recentFailures, + method, + hourOfDay: new Date().getHours(), + }); + await loginEventRepo.record({ + id: `le_${crypto.randomUUID()}`, + userId: user.id, + productId, + result: 'success', + method, + riskLevel: risk.level, + riskScore: risk.score, + ip, + userAgent: req.headers['user-agent'] as string | undefined, + riskFlags: risk.flags, + createdAt: new Date().toISOString(), + }); + } catch { + /* best-effort */ + } + return { accessToken, refreshToken,