Conversation
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>
Bugbot couldn't run - usage limit reachedBugbot 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) |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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
}
}| 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]); |
There was a problem hiding this comment.
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}> |
There was a problem hiding this comment.
Prevent users from submitting empty or whitespace-only organization names by adding validation to the button's disabled prop.
| <Button type="button" className="mt-[18px]" onClick={create} disabled={loading}> | |
| <Button type="button" className="mt-[18px]" onClick={create} disabled={loading || !name.trim()}> |
| const createOrg = useCallback(() => { | ||
| modals.openModal({ | ||
| classNames: { | ||
| modal: 'bg-transparent text-textColor', | ||
| }, | ||
| title: t('create_new_organization', 'Create New Organization'), | ||
| withCloseButton: true, | ||
| children: <CreateOrganization />, | ||
| }); | ||
| }, [t]); |
There was a problem hiding this comment.
The modals object is used inside the createOrg callback but is missing from the dependency array of useCallback.
| 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]); |
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:
organization.selector.tsxwithout external redirects or popups.POST /user/organizationsinusers.controller.ts, delegating creation toapi.dos.me/organizationswhen authenticated with DOS ID.id,name, and assigns the creator asSUPERADMIN.Other information:
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/organizationsaccepts an optionalname(CreateOrganizationDto) and callsOrganizationService.createOrgForUser. When the request includes aBearertoken, creation is delegated toPOSTIZ_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, setsallowTrial/isTrailingto false, and links the user as SUPERADMIN;getOrganizationNameis added for lookup.Frontend:
CreateOrganizationmodal posts to/user/organizations, switches org via/user/change-org, and reloads. The org selector stays visible for single-org users (no longer hidden whendata.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.