Skip to content

feat(org): implement outbound organization creation with API delegation - #6

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 & Architecture

Why was this change needed?

Implements Section 2 (Outbound Creation API / Method 3: Delegated Creation) from the DOS / Crove Organization Management Standard:

  1. Frontend: Integrates the native in-app "+ Create New Organization" modal inside organization.selector.tsx without external redirects or popups.
  2. Backend: Implements POST /user/organizations in users.controller.ts, delegating creation to api.dos.me/organizations when authenticated with DOS ID.
  3. Database: Saves the canonical DOS-Me organization id, name, and assigns the creator as SUPERADMIN.

Other information:

  • Seamless integration with DOS-Me Single Source of Truth (SSOT).

Checklist:


Note

Medium Risk
Any authenticated user can create orgs without extra policy checks, and Bearer tokens are forwarded to an external DOS API with silent fallback to local IDs if delegation fails, which can affect org identity consistency.

Overview
Adds end-to-end “create organization” so users can spin up a new org from the app instead of only switching between existing ones.

Backend: New POST /user/organizations accepts an optional name (CreateOrganizationDto) and calls OrganizationService.createOrgForUser. When the request includes a Bearer token, creation is delegated to POSTIZ_OAUTH_URL / https://api.dos.me/organizations; on success the returned canonical org id and name are used when persisting locally. If delegation fails or there is no Bearer header, a local org is created with default name “New Organization”. The repository path creates the org (optional fixed id), generates an API key, sets allowTrial/isTrailing to false, and links the user as SUPERADMIN; getOrganizationName is added for lookup.

Frontend: CreateOrganization modal posts to /user/organizations, switches org via /user/change-org, and reloads. The org selector stays visible for single-org users (no longer hidden when data.length === 1) and exposes “Create New Organization +” in the dropdown (non–open-select mode).

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

Add native in-app Create Organization modal in organization selector and delegate creation to api.dos.me via POST /user/organizations (Method 3: Delegated Creation).

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_0544c03a-3338-464d-a20e-0ff5300b906d)

@JOY
JOY (JOY) merged commit 630aab3 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 introduces the ability for users to create new organizations from both the frontend and backend, including support for delegated creation via an external API. The review feedback highlights several critical improvements: adding a timeout to the external API request to prevent blocking, implementing proper error handling and state resetting in the frontend creation flow, validating organization names to prevent empty submissions, and correcting missing dependencies in React's useCallback hooks.

Comment on lines +231 to +253
if (userAuthHeader && userAuthHeader.startsWith('Bearer ')) {
try {
const response = await fetch(`${apiUrl}/organizations`, {
method: 'POST',
headers: {
Authorization: userAuthHeader,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: orgName,
}),
});
if (response.ok) {
const data = await response.json();
if (data?.id) {
orgId = data.id;
orgName = data.name || orgName;
}
}
} catch (err) {
// Fallback to local creation if remote call fails
}
}

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 external fetch call to api.dos.me does not have a timeout configured. If the external service hangs, this request will block the event loop indefinitely. It is recommended to use an AbortController to enforce a timeout.

    if (userAuthHeader && userAuthHeader.startsWith('Bearer ')) {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 5000);
      try {
        const response = await fetch(apiUrl + '/organizations', {
          method: 'POST',
          headers: {
            Authorization: userAuthHeader,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            name: orgName,
          }),
          signal: controller.signal,
        });
        clearTimeout(timeoutId);
        if (response.ok) {
          const data = await response.json();
          if (data?.id) {
            orgId = data.id;
            orgName = data.name || orgName;
          }
        }
      } catch (err) {
        clearTimeout(timeoutId);
        // Fallback to local creation if remote call fails
      }
    }

Comment on lines +19 to +33
const create = useCallback(async () => {
setLoading(true);
const { id } = await (
await fetch('/user/organizations', {
method: 'POST',
body: JSON.stringify({ name }),
})
).json();
await fetch('/user/change-org', {
method: 'POST',
body: JSON.stringify({ id }),
});
modals.closeAll();
window.location.reload();
}, [name]);

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 create callback lacks error handling and does not reset the loading state if the request fails, which would leave the UI permanently disabled. Additionally, fetch and modals are missing from the dependency array of useCallback.

  const create = useCallback(async () => {
    setLoading(true);
    try {
      const response = await fetch('/user/organizations', {
        method: 'POST',
        body: JSON.stringify({ name }),
      });
      if (!response.ok) {
        throw new Error('Failed to create organization');
      }
      const data = await response.json();
      if (data?.id) {
        await fetch('/user/change-org', {
          method: 'POST',
          body: JSON.stringify({ id: data.id }),
        });
        modals.closeAll();
        window.location.reload();
      } else {
        setLoading(false);
      }
    } catch (error) {
      setLoading(false);
    }
  }, [name, fetch, modals]);

label={t('organization_name', 'Organization name')}
placeholder={t('organization_name', 'Organization name')}
/>
<Button type="button" className="mt-[18px]" onClick={create} disabled={loading}>

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

Prevent users from submitting empty or whitespace-only organization names by adding validation to the button's disabled prop.

Suggested change
<Button type="button" className="mt-[18px]" onClick={create} disabled={loading}>
<Button type="button" className="mt-[18px]" onClick={create} disabled={loading || !name.trim()}>

Comment on lines +87 to +96
const createOrg = useCallback(() => {
modals.openModal({
classNames: {
modal: 'bg-transparent text-textColor',
},
title: t('create_new_organization', 'Create New Organization'),
withCloseButton: true,
children: <CreateOrganization />,
});
}, [t]);

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 modals object is used inside the createOrg callback but is missing from the dependency array of useCallback.

Suggested change
const createOrg = useCallback(() => {
modals.openModal({
classNames: {
modal: 'bg-transparent text-textColor',
},
title: t('create_new_organization', 'Create New Organization'),
withCloseButton: true,
children: <CreateOrganization />,
});
}, [t]);
const createOrg = useCallback(() => {
modals.openModal({
classNames: {
modal: 'bg-transparent text-textColor',
},
title: t('create_new_organization', 'Create New Organization'),
withCloseButton: true,
children: <CreateOrganization />,
});
}, [t, modals]);

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