diff --git a/services/platform-service/src/modules/auth/routes.ts b/services/platform-service/src/modules/auth/routes.ts index 43283b10..51e1406a 100644 --- a/services/platform-service/src/modules/auth/routes.ts +++ b/services/platform-service/src/modules/auth/routes.ts @@ -5,6 +5,7 @@ * POST /auth/register — register new user * POST /auth/refresh — refresh access token * GET /auth/me — get current user from token + * POST /auth/sso — SSO login (verified external identity) * POST /auth/verify — service-to-service token verification * * Admin user management (requires super_admin or admin role): @@ -27,6 +28,7 @@ import { RegisterSchema, RefreshSchema, UpdateUserSchema, + SsoLoginSchema, type UserDoc, } from './types.js'; @@ -229,6 +231,113 @@ export async function authRoutes(app: FastifyInstance) { } }); + // SSO login — verified external identity (called by dashboard OAuth callbacks) + // The caller has already verified the user's identity via Microsoft/Google OAuth. + // This endpoint finds or creates the user and issues platform JWT tokens. + app.post('/auth/sso', async (req, reply) => { + const parsed = SsoLoginSchema.safeParse(req.body); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const { email, productId, provider, displayName } = parsed.data; + const product = getProduct(productId); + if (!product || product.status !== 'active') { + throw new BadRequestError(`Unknown or disabled product: ${productId}`); + } + + let user = await repo.getByEmail(email, productId); + + if (!user) { + // Auto-create SSO user with a random password (they'll never use it) + const now = new Date().toISOString(); + user = { + id: `usr_${crypto.randomUUID()}`, + productId, + email: email.toLowerCase(), + passwordHash: await repo.hashPassword(crypto.randomUUID()), + plan: product.defaultPlan, + role: 'user', + displayName: displayName || email.split('@')[0], + status: 'active', + lastLoginAt: now, + createdAt: now, + updatedAt: now, + }; + await repo.create(user); + + // Best-effort: provision subscription + license (same as register) + const nowDate = new Date(); + const trialEnd = new Date(nowDate); + trialEnd.setDate(trialEnd.getDate() + product.trialDays); + const hasTrial = product.trialDays > 0; + try { + await subscriptionRepo.createSubscription({ + id: `sub_${user.id}_${Date.now()}`, + productId, + userId: user.id, + plan: product.defaultPlan, + status: hasTrial ? 'trialing' : 'active', + currentPeriodStart: now, + currentPeriodEnd: hasTrial ? trialEnd.toISOString() : now, + cancelAtPeriodEnd: false, + monthlyPrice: 0, + tokensIncluded: 0, + tokensUsed: 0, + createdAt: now, + updatedAt: now, + }); + } catch (err) { + req.log.warn({ err, userId: user.id }, 'SSO subscription provisioning failed'); + } + try { + await licenseRepo.create({ + id: `lic_${crypto.randomUUID()}`, + productId, + key: licenseRepo.generateKey(product.licensePrefix), + userId: user.id, + plan: product.defaultPlan, + status: 'active', + activatedAt: null, + expiresAt: hasTrial ? trialEnd.toISOString() : null, + deviceIds: [], + maxDevices: product.deviceLimits[product.defaultPlan], + createdAt: now, + updatedAt: now, + }); + } catch (err) { + req.log.warn({ err, userId: user.id }, 'SSO license provisioning failed'); + } + + reply.code(201); + } else { + // Existing user — update last login + await repo.updateLastLogin(user.id); + } + + req.log.info({ userId: user.id, provider, email }, 'SSO login'); + + const accessToken = await jwt.createAccessToken({ + sub: user.id, + email: user.email, + role: user.role, + productId, + plan: user.plan, + }); + const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId }); + + return { + accessToken, + refreshToken, + user: { + id: user.id, + email: user.email, + role: user.role, + plan: user.plan, + displayName: user.displayName, + }, + }; + }); + // Service-to-service token verification app.post('/auth/verify', async req => { const { token } = req.body as { token?: string }; diff --git a/services/platform-service/src/modules/auth/types.ts b/services/platform-service/src/modules/auth/types.ts index 0007c8fb..0133eca8 100644 --- a/services/platform-service/src/modules/auth/types.ts +++ b/services/platform-service/src/modules/auth/types.ts @@ -53,6 +53,14 @@ export const UpdateUserSchema = z.object({ status: z.enum(['active', 'disabled']).optional(), }); +export const SsoLoginSchema = z.object({ + email: z.string().email(), + productId: z.string().min(1), + provider: z.enum(['microsoft', 'google']), + displayName: z.string().min(1).optional(), +}); + export type LoginInput = z.infer; export type RegisterInput = z.infer; export type UpdateUserInput = z.infer; +export type SsoLoginInput = z.infer;