Skip to content

feat(auth): sync inbound organizations from DOS ID claims - #5

Merged
JOY (JOY) merged 1 commit into
mainfrom
dev
Aug 26, 2026
Merged

JOY (JOY) merged 1 commit into
mainfrom
dev

Conversation

@JOY

@JOY JOY (JOY) commented Aug 26, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

Feature & Identity Sync

Why was this change needed?

Implements Section 3 (Inbound JIT Sync) from the DOS / Crove Organization Management & Sync Standard:

  1. When a user logs in or refreshes their profile via DOS ID, the organizations array from https://api.dos.me/sso/userinfo is evaluated.
  2. For each organization present in the claims:
    • If the organization exists locally, ensure the user is associated with their designated role (OWNER/ADMIN -> ADMIN/SUPERADMIN, MEMBER -> USER).
    • If the organization does not exist locally yet, create it using the canonical DOS-Me id, name, and user role assignment.
  3. Preserves native Postiz UI and logic while binding organization IDs seamlessly to the Single Source of Truth (SSOT).

Other information:

  • Follows the DOS-Me Ecosystem Organization Sync specification.

Checklist:


Note

Medium Risk
Changes auth-time org membership and ID assignment on every SSO login/registration; silent failures could leave users out of expected orgs, and role mapping may not match all claim types.

Overview
Adds just-in-time organization sync during DOS ID (SSO) login and registration so local org membership and IDs stay aligned with userinfo organization claims.

For returning users, any claim org the user is not already in is linked: existing local orgs get addUserToOrg with mapped roles (MEMBERUSER, otherwise ADMIN); missing orgs are created via createOrgForExistingUser using the claim’s canonical id and name. Errors on individual orgs are swallowed with .catch(() => {}).

For new provider registrations, the first claim org still drives the default company name; additional orgs use the same exists-vs-create branching instead of always creating new orgs by name only. createOrgForExistingUser (repository + service) now accepts an optional orgId so new organizations can be persisted with the external SSOT identifier.

Reviewed by Cursor Bugbot for commit d141027. Bugbot is set up for automated code reviews on this repo. Configure here.

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 <cursoragent@cursor.com>
@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0708439f-3849-46c9-b02c-c5f5c19ef33f)

@JOY
JOY (JOY) merged commit db2b170 into main Aug 26, 2026
11 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the authentication service to synchronize provider-associated organizations for both existing and registering users, allowing optional organization IDs to be passed down during creation. The review feedback highlights a major redundancy in auth.service.ts where identical branches are executed regardless of the organization's existence, and points out a redundant ternary expression when determining user roles.

Comment on lines +197 to +240
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
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The if (firstOrg?.id) block contains three identical branches that call this._organizationService.createOrgAndUser with the exact same arguments. This redundancy makes the code hard to maintain and suggests a logical error, as the first organization's canonical ID is never used during registration.

We can simplify this by removing the redundant conditional branches and making a single call to createOrgAndUser. The actual synchronization of the first organization (including its canonical ID) can then be handled by updating the subsequent loop to start at index 0 instead of 1.

    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
    );

Comment on lines +167 to +181
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(() => {});
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ternary expression role === 'ADMIN' ? 'ADMIN' : 'USER' on line 177 is redundant because role is already defined as either 'USER' or 'ADMIN' on line 169. We can simplify this by passing role directly.

Suggested change
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(() => {});
}
}
}
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, orgInfo.id)
.catch(() => {});
}
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant