feat(platform-service): add SSO login endpoint (/auth/sso)

- POST /auth/sso — accepts verified email + provider + productId
- Creates user if not exists (with subscription + license provisioning)
- Issues platform JWT tokens for existing SSO users
- Supports Microsoft and Google OAuth providers
- Added SsoLoginSchema to types.ts
This commit is contained in:
saravanakumardb1 2026-02-15 16:38:10 -08:00
parent c7fb2eb357
commit be3f5459bd
2 changed files with 117 additions and 0 deletions

View File

@ -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 };

View File

@ -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<typeof LoginSchema>;
export type RegisterInput = z.infer<typeof RegisterSchema>;
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
export type SsoLoginInput = z.infer<typeof SsoLoginSchema>;