From 95ed1bd4abbab323e7b5d3941d1122e52640e536 Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 18 May 2026 13:37:20 +0330 Subject: [PATCH] bugfix: create organization button is now hidden for none owner users. --- backend/src/modules/auth/auth.controller.ts | 6 ++- backend/src/modules/auth/auth.service.ts | 29 ++++++++++- frontend/src/components/shared/permissions.ts | 5 ++ .../OrganizationSelectorContent.tsx | 51 +++++++++++++------ 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/backend/src/modules/auth/auth.controller.ts b/backend/src/modules/auth/auth.controller.ts index 860aade..1e68517 100644 --- a/backend/src/modules/auth/auth.controller.ts +++ b/backend/src/modules/auth/auth.controller.ts @@ -126,7 +126,11 @@ export class AuthController { @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Create organization for current user' }) async createOrganization(@Req() req, @Body() dto: CreateOrganizationDto) { - return this.authService.createOrganization(req.user.id, dto); + return this.authService.createOrganization( + req.user.id, + req.user.organizationId, + dto, + ); } // ========================= diff --git a/backend/src/modules/auth/auth.service.ts b/backend/src/modules/auth/auth.service.ts index 7e3a4a1..102e59c 100644 --- a/backend/src/modules/auth/auth.service.ts +++ b/backend/src/modules/auth/auth.service.ts @@ -4,6 +4,7 @@ import { UnauthorizedException, BadRequestException, ConflictException, + ForbiddenException, InternalServerErrorException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; @@ -270,7 +271,11 @@ export class AuthService { return this.login({ email, password } as any, validatedUser); } - async createOrganization(userId: string, dto: CreateOrganizationDto) { + async createOrganization( + userId: string, + currentOrganizationId: string | undefined, + dto: CreateOrganizationDto, + ) { const owner = await this.prisma.user.findUnique({ where: { id: userId }, select: { id: true }, @@ -280,6 +285,28 @@ export class AuthService { throw new UnauthorizedException('User not found'); } + if (!currentOrganizationId) { + throw new ForbiddenException( + 'Select an organization before creating a new one.', + ); + } + + const currentMembership = await this.prisma.membership.findUnique({ + where: { + userId_organizationId: { + userId, + organizationId: currentOrganizationId, + }, + }, + select: { isOwner: true }, + }); + + if (!currentMembership?.isOwner) { + throw new ForbiddenException( + 'Only owners of the current organization can create new organizations.', + ); + } + const organization = await this.prisma.$transaction(async (tx) => { const createdOrganization = await tx.organization.create({ data: { diff --git a/frontend/src/components/shared/permissions.ts b/frontend/src/components/shared/permissions.ts index 753e73f..2d1733c 100644 --- a/frontend/src/components/shared/permissions.ts +++ b/frontend/src/components/shared/permissions.ts @@ -16,6 +16,11 @@ export function hasPermission(org: Organization | null, permission: string): boo return Boolean(org.permissions?.includes(permission)); } +/** True when the user is owner of the currently selected organization. */ +export function canCreateOrganizationFromCurrentOrg(org: Organization | null): boolean { + return Boolean(org?.isOwner); +} + /** Sidebar / route guard: READ access to a tab */ export function canViewTab(org: Organization | null, readPermission: string): boolean { return hasPermission(org, readPermission); diff --git a/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx b/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx index 4e70b45..bfe1140 100644 --- a/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx +++ b/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx @@ -1,13 +1,26 @@ 'use client'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useAuth } from '@/lib/hooks/useAuth'; +import { canCreateOrganizationFromCurrentOrg } from '@/components/shared/permissions'; import { Building2, Beaker, Mail } from 'lucide-react'; import { Input } from '@/components/ui/shared/Input'; import { Button } from '@/components/ui/shared/Button'; export function OrganizationSelectorContent() { - const { organizations, selectOrganization, createOrganization, isLoading, error, clearError } = useAuth(); + const { + organizations, + currentOrganization, + selectOrganization, + createOrganization, + isLoading, + error, + clearError, + } = useAuth(); + const canCreateOrganization = useMemo( + () => canCreateOrganizationFromCurrentOrg(currentOrganization), + [currentOrganization], + ); const [isCreateOpen, setIsCreateOpen] = useState(false); const [organizationName, setOrganizationName] = useState(''); const [organizationEmail, setOrganizationEmail] = useState(''); @@ -44,22 +57,26 @@ export function OrganizationSelectorContent() {

Organizations

- Select an organization to continue, or create a new one. + {canCreateOrganization + ? 'Select an organization to continue, or create a new one.' + : 'Select an organization to continue.'}

- + {canCreateOrganization && ( + + )} - {isCreateOpen && ( + {canCreateOrganization && isCreateOpen && (
-

No organizations found. Create your first one to continue.

+

+ {canCreateOrganization + ? 'No organizations found. Create your first one to continue.' + : 'No organizations found. Ask an organization owner to invite you.'} +

) : (