import { Injectable, Logger, BadRequestException } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { UsersService } from '../users/users.service'; interface SsoProfile { provider: string; providerId: string; email: string; firstName?: string; lastName?: string; } @Injectable() export class SsoService { private readonly logger = new Logger(SsoService.name); constructor( private dataSource: DataSource, private usersService: UsersService, ) {} /** * Find existing user by SSO provider+id, or by email match, or create new. */ async findOrCreateSsoUser(profile: SsoProfile) { // 1. Try to find by provider + provider ID const byProvider = await this.dataSource.query( `SELECT * FROM shared.users WHERE oauth_provider = $1 AND oauth_provider_id = $2`, [profile.provider, profile.providerId], ); if (byProvider.length > 0) { return this.usersService.findByIdWithOrgs(byProvider[0].id); } // 2. Try to find by email match (link accounts) const byEmail = await this.usersService.findByEmail(profile.email); if (byEmail) { // Link the SSO provider to existing account await this.linkSsoAccount(byEmail.id, profile.provider, profile.providerId); return this.usersService.findByIdWithOrgs(byEmail.id); } // 3. Create new user const newUser = await this.dataSource.query( `INSERT INTO shared.users (email, first_name, last_name, oauth_provider, oauth_provider_id, is_email_verified) VALUES ($1, $2, $3, $4, $5, true) RETURNING id`, [profile.email, profile.firstName || '', profile.lastName || '', profile.provider, profile.providerId], ); return this.usersService.findByIdWithOrgs(newUser[0].id); } /** * Link an SSO provider to an existing user. */ async linkSsoAccount(userId: string, provider: string, providerId: string): Promise { await this.dataSource.query( `UPDATE shared.users SET oauth_provider = $1, oauth_provider_id = $2, updated_at = NOW() WHERE id = $3`, [provider, providerId, userId], ); this.logger.log(`Linked ${provider} SSO to user ${userId}`); } /** * Unlink SSO from a user (only if they have a password set). */ async unlinkSsoAccount(userId: string, provider: string): Promise { const rows = await this.dataSource.query( `SELECT password_hash, oauth_provider FROM shared.users WHERE id = $1`, [userId], ); if (rows.length === 0) throw new BadRequestException('User not found'); if (!rows[0].password_hash) { throw new BadRequestException('Cannot unlink SSO — you must set a password first'); } if (rows[0].oauth_provider !== provider) { throw new BadRequestException('SSO provider mismatch'); } await this.dataSource.query( `UPDATE shared.users SET oauth_provider = NULL, oauth_provider_id = NULL, updated_at = NOW() WHERE id = $1`, [userId], ); this.logger.log(`Unlinked ${provider} SSO from user ${userId}`); } /** * Get which SSO providers are configured. */ getAvailableProviders(): { google: boolean; azure: boolean } { return { google: !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET), azure: !!(process.env.AZURE_CLIENT_ID && process.env.AZURE_CLIENT_SECRET), }; } }