From d1410273594bc7d02a7baf5c9f437948bbe75f83 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:07:04 +0700 Subject: [PATCH] feat(auth): sync inbound organizations using canonical DOS ID on login Implement JIT organization sync from userinfo claims, mapping canonical DOS-Me organization IDs and roles to internal Organization and UserOrganization records. Co-authored-by: Cursor --- .../backend/src/services/auth/auth.service.ts | 102 ++++++++++++++---- .../organizations/organization.repository.ts | 4 +- .../organizations/organization.service.ts | 6 +- 3 files changed, 91 insertions(+), 21 deletions(-) diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index 10a43a9abf..5e743d2038 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -11,6 +11,7 @@ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/n import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto'; import { EmailService } from '@gitroom/nestjs-libraries/services/email.service'; import { NewsletterService } from '@gitroom/nestjs-libraries/newsletter/newsletter.service'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; @Injectable() export class AuthService { @@ -158,6 +159,28 @@ export class AuthService { bio: user.bio || '', }); } + + if (providerUser.organizations && providerUser.organizations.length > 0) { + const userOrgs = await this._organizationService.getOrgsByUserId(user.id); + const existingOrgIds = new Set(userOrgs.map((o) => o.id)); + + for (const orgInfo of providerUser.organizations) { + if (!existingOrgIds.has(orgInfo.id)) { + const role = orgInfo.role === 'MEMBER' ? 'USER' : 'ADMIN'; + const orgExists = await this._organizationService.getOrgById(orgInfo.id); + if (orgExists) { + await this._organizationService + .addUserToOrg(user.id, makeId(5), orgInfo.id, role) + .catch(() => {}); + } else { + await this._organizationService + .createOrgForExistingUser(user.id, orgInfo.name, role === 'ADMIN' ? 'ADMIN' : 'USER', orgInfo.id) + .catch(() => {}); + } + } + } + } + return user; } @@ -165,23 +188,56 @@ export class AuthService { throw new Error('Registration is disabled'); } + const firstOrg = providerUser.organizations && providerUser.organizations[0]; const companyName = - (providerUser.organizations && providerUser.organizations[0]?.name) || + firstOrg?.name || body.company || (providerUser.name ? `${providerUser.name}'s Organization` : providerUser.email.split('@')[0]); - const create = await this._organizationService.createOrgAndUser( - { - company: companyName, - email: providerUser.email, - password: '', - provider, - providerId: providerUser.id, - datafast_visitor_id: body.datafast_visitor_id || '', - }, - ip, - userAgent - ); + let create: any; + if (firstOrg?.id) { + const orgExists = await this._organizationService.getOrgById(firstOrg.id); + if (!orgExists) { + create = await this._organizationService.createOrgAndUser( + { + company: companyName, + email: providerUser.email, + password: '', + provider, + providerId: providerUser.id, + datafast_visitor_id: body.datafast_visitor_id || '', + }, + ip, + userAgent + ); + } else { + create = await this._organizationService.createOrgAndUser( + { + company: companyName, + email: providerUser.email, + password: '', + provider, + providerId: providerUser.id, + datafast_visitor_id: body.datafast_visitor_id || '', + }, + ip, + userAgent + ); + } + } else { + create = await this._organizationService.createOrgAndUser( + { + company: companyName, + email: providerUser.email, + password: '', + provider, + providerId: providerUser.id, + datafast_visitor_id: body.datafast_visitor_id || '', + }, + ip, + userAgent + ); + } if (providerUser.name) { await this._userService.changePersonal(create.users[0].user.id, { @@ -194,11 +250,21 @@ export class AuthService { for (let i = 1; i < providerUser.organizations.length; i++) { const orgInfo = providerUser.organizations[i]; const role = orgInfo.role === 'MEMBER' ? 'USER' : 'ADMIN'; - await this._organizationService.createOrgForExistingUser( - create.users[0].user.id, - orgInfo.name, - role - ).catch(() => {}); + const orgExists = await this._organizationService.getOrgById(orgInfo.id); + if (orgExists) { + await this._organizationService + .addUserToOrg(create.users[0].user.id, makeId(5), orgInfo.id, role) + .catch(() => {}); + } else { + await this._organizationService + .createOrgForExistingUser( + create.users[0].user.id, + orgInfo.name, + role === 'ADMIN' ? 'ADMIN' : 'USER', + orgInfo.id + ) + .catch(() => {}); + } } } diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index 359a77477e..aa1e970765 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -428,10 +428,12 @@ export class OrganizationRepository { async createOrgForExistingUser( userId: string, orgName: string, - role: 'SUPERADMIN' | 'ADMIN' | 'USER' = 'SUPERADMIN' + role: 'SUPERADMIN' | 'ADMIN' | 'USER' = 'SUPERADMIN', + orgId?: string ) { return this._organization.model.organization.create({ data: { + ...(orgId ? { id: orgId } : {}), name: orgName, apiKey: AuthService.fixedEncryption(makeId(20)), allowTrial: true, diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts index aa2ce7fe50..288622c171 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts @@ -199,12 +199,14 @@ export class OrganizationService { async createOrgForExistingUser( userId: string, orgName: string, - role: 'SUPERADMIN' | 'ADMIN' | 'USER' = 'SUPERADMIN' + role: 'SUPERADMIN' | 'ADMIN' | 'USER' = 'SUPERADMIN', + orgId?: string ) { return this._organizationRepository.createOrgForExistingUser( userId, orgName, - role + role, + orgId ); }