diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts
index 896cc03b17..5ff100a353 100644
--- a/apps/backend/src/api/routes/users.controller.ts
+++ b/apps/backend/src/api/routes/users.controller.ts
@@ -23,6 +23,7 @@ import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.man
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
import { ApiTags } from '@nestjs/swagger';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
+import { CreateOrganizationDto } from '@gitroom/nestjs-libraries/dtos/organizations/create.organization.dto';
import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto';
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
@@ -304,6 +305,16 @@ export class UsersController {
);
}
+ @Post('/organizations')
+ createOrg(
+ @GetUserFromRequest() user: User,
+ @Body() body: CreateOrganizationDto,
+ @Req() req: Request
+ ) {
+ const authHeader = (req.headers.authorization as string) || '';
+ return this._orgService.createOrgForUser(user.id, body, authHeader);
+ }
+
@Post('/change-org')
changeOrg(
@Body('id') id: string,
diff --git a/apps/frontend/src/components/layout/organization.selector.tsx b/apps/frontend/src/components/layout/organization.selector.tsx
index 4f072f5938..2960172f82 100644
--- a/apps/frontend/src/components/layout/organization.selector.tsx
+++ b/apps/frontend/src/components/layout/organization.selector.tsx
@@ -1,13 +1,59 @@
'use client';
-import React, { FC, useCallback, useMemo } from 'react';
+import React, { FC, useCallback, useMemo, useState } from 'react';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import clsx from 'clsx';
+import { useModals } from '@gitroom/frontend/components/layout/new-modal';
+import { Input } from '@gitroom/react/form/input';
+import { Button } from '@gitroom/react/form/button';
+import { useT } from '@gitroom/react/translation/get.transation.service.client';
+
+export const CreateOrganization = () => {
+ const t = useT();
+ const fetch = useFetch();
+ const modals = useModals();
+ const [name, setName] = useState('');
+ const [loading, setLoading] = useState(false);
+ 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]);
+ return (
+
+ setName(e.target.value)}
+ name="name"
+ label={t('organization_name', 'Organization name')}
+ placeholder={t('organization_name', 'Organization name')}
+ />
+
+
+ );
+};
+
export const OrganizationSelector: FC<{ asOpenSelect?: boolean }> = ({
asOpenSelect,
}) => {
+ const t = useT();
+ const modals = useModals();
const fetch = useFetch();
const user = useUser();
const load = useCallback(async () => {
@@ -38,7 +84,17 @@ export const OrganizationSelector: FC<{ asOpenSelect?: boolean }> = ({
},
[]
);
- if (isLoading || (!isLoading && data?.length === 1)) {
+ const createOrg = useCallback(() => {
+ modals.openModal({
+ classNames: {
+ modal: 'bg-transparent text-textColor',
+ },
+ title: t('create_new_organization', 'Create New Organization'),
+ withCloseButton: true,
+ children: ,
+ });
+ }, [t]);
+ if (isLoading) {
return null;
}
return (
@@ -68,7 +124,7 @@ export const OrganizationSelector: FC<{ asOpenSelect?: boolean }> = ({
)}
)}
- {data?.length > 1 && (
+ {(data?.length > 1 || !asOpenSelect) && (
= ({
)
)}
+ {!asOpenSelect && (
+
+ {t('create_new_organization', 'Create New Organization')} +
+
+ )}
)}
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 aa1e970765..239219b713 100644
--- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts
@@ -496,4 +496,36 @@ export class OrganizationRepository {
},
});
}
+
+ createOrgForUser(userId: string, name: string, orgId?: string) {
+ return this._organization.model.organization.create({
+ data: {
+ ...(orgId ? { id: orgId } : {}),
+ name,
+ apiKey: AuthService.fixedEncryption(makeId(20)),
+ allowTrial: false,
+ isTrailing: false,
+ users: {
+ create: {
+ role: Role.SUPERADMIN,
+ userId,
+ },
+ },
+ },
+ select: {
+ id: true,
+ },
+ });
+ }
+
+ getOrganizationName(orgId: string) {
+ return this._organization.model.organization.findUnique({
+ where: {
+ id: orgId,
+ },
+ select: {
+ name: 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 288622c171..86769ec69d 100644
--- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts
@@ -218,6 +218,47 @@ export class OrganizationService {
return this._organizationRepository.deleteOrganization(orgId);
}
+ async createOrgForUser(
+ userId: string,
+ body: { name?: string },
+ userAuthHeader?: string
+ ) {
+ let orgId: string | undefined;
+ let orgName = body.name || 'New Organization';
+
+ // If user is authenticated via DOS ID, delegate creation to api.dos.me (Method 3: Delegated Creation)
+ const apiUrl = process.env.POSTIZ_OAUTH_URL || 'https://api.dos.me';
+ 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
+ }
+ }
+
+ return this._organizationRepository.createOrgForUser(userId, orgName, orgId);
+ }
+
+ async getOrganizationName(orgId: string) {
+ return this._organizationRepository.getOrganizationName(orgId);
+ }
+
async findOrgByName(name: string) {
return this._organizationRepository.findOrgByName(name);
}
diff --git a/libraries/nestjs-libraries/src/dtos/organizations/create.organization.dto.ts b/libraries/nestjs-libraries/src/dtos/organizations/create.organization.dto.ts
new file mode 100644
index 0000000000..a1b60bcc0c
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/organizations/create.organization.dto.ts
@@ -0,0 +1,8 @@
+import { IsOptional, IsString, MaxLength } from 'class-validator';
+
+export class CreateOrganizationDto {
+ @IsString()
+ @IsOptional()
+ @MaxLength(100)
+ name?: string;
+}