import { FastifyPluginAsync } from 'fastify'; import { randomUUID } from 'node:crypto'; import { verifyUserProof, fetchOculusProfile } from '../../services/meta-auth.service.js'; import { signAccessToken } from '../../plugins/auth.js'; import { hashToken } from '../../services/crypto.service.js'; import { config } from '../../config.js'; import { AppError } from '../../plugins/error-handler.js'; import type { MetaCallbackBody, AuthTokensResponse } from '../../types/api.js'; const metaAuthRoutes: FastifyPluginAsync = async (fastify) => { fastify.post<{ Body: MetaCallbackBody }>('/auth/meta/callback', { config: { rateLimit: { max: 10, timeWindow: '1 minute' }, }, schema: { body: { type: 'object', required: ['userId'], properties: { userId: { type: 'string', minLength: 1 }, nonce: { type: 'string' }, deviceInfo: { type: 'string' }, }, additionalProperties: false, }, }, }, async (request, reply) => { const { userId, nonce, deviceInfo } = request.body; request.log.info({ userId, hasNonce: !!nonce, deviceInfo }, 'Meta callback received'); // Nonce validation requires a numeric user ID from Meta's graph API. // When DUC hasn't granted numeric ID access, the SDK returns oculusId (a string username). const isNumericId = /^\d+$/.test(userId); if (nonce && isNumericId) { const isValid = await verifyUserProof(userId, nonce); if (!isValid) { throw new AppError(401, 'Invalid user proof'); } request.log.info('Nonce verified successfully'); } else { request.log.warn({ isNumericId, hasNonce: !!nonce }, 'Skipping nonce verification (non-numeric userId or missing nonce)'); } // Try to fetch profile from Oculus graph; fall back to userId as display name let metaId = userId; let displayName = userId; if (isNumericId) { try { const profile = await fetchOculusProfile(userId); metaId = profile.metaId; displayName = profile.displayName; } catch (e) { request.log.warn(e, 'Failed to fetch Oculus profile'); } } // Upsert user const existingUser = await fastify.prisma.user.findUnique({ where: { metaId } }); request.log.info({ metaId, userId: existingUser?.id, isNew: !existingUser }, 'User upsert'); const user = await fastify.prisma.user.upsert({ where: { metaId }, update: { displayName }, create: { metaId, displayName }, }); // Create session with hashed refresh token const refreshToken = randomUUID(); const expiresAt = new Date(Date.now() + config.jwt.refreshTtl * 1000); await fastify.prisma.session.create({ data: { userId: user.id, refreshToken: hashToken(refreshToken), expiresAt, deviceInfo: deviceInfo ?? null, }, }); // Sign JWT const accessToken = await signAccessToken(user.id); const response: AuthTokensResponse = { accessToken, refreshToken, expiresIn: config.jwt.accessTtl, }; reply.status(200).send(response); }); }; export default metaAuthRoutes;