Multi-environment and multi-user construction

This commit is contained in:
2026-09-15 21:37:58 +03:30
parent e61fad95c6
commit d3f97b9dc4
37 changed files with 1349 additions and 201 deletions
+11
View File
@@ -45,6 +45,17 @@ export class UpdateOrgCurrenciesDto {
rates?: ExchangeRateInputDto[];
}
export class CreateOrganizationDto {
@IsString()
@MinLength(2)
@MaxLength(120)
name!: string;
@IsOptional()
@IsIn([...BUSINESS_FIELDS])
businessField?: (typeof BUSINESS_FIELDS)[number];
}
export class UpdateOrganizationDto {
@IsOptional()
@IsString()
@@ -1,15 +1,32 @@
import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Patch, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator';
import { CurrentUser } from '../auth/current-user.decorator';
import { UpdateOrgCurrenciesDto, UpdateOrganizationDto } from './dto';
import { AuthService } from '../auth/auth.service';
import {
CreateOrganizationDto,
UpdateOrgCurrenciesDto,
UpdateOrganizationDto,
} from './dto';
import { OrganizationsService } from './organizations.service';
@Controller('organizations')
@UseGuards(JwtAuthGuard, RolesGuard)
export class OrganizationsController {
constructor(private readonly organizationsService: OrganizationsService) {}
constructor(
private readonly organizationsService: OrganizationsService,
private readonly authService: AuthService,
) {}
@Post()
async create(
@CurrentUser() user: { userId: string },
@Body() dto: CreateOrganizationDto,
) {
const org = await this.organizationsService.createForUser(user.userId, dto);
return this.authService.switchOrganization(user.userId, org.id);
}
@Get('current')
current(@CurrentUser() user: { organizationId: string }) {
@@ -1,8 +1,13 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { MembershipsModule } from '../memberships/memberships.module';
import { OrgRolesModule } from '../org-roles/org-roles.module';
import { PipelinesModule } from '../pipelines/pipelines.module';
import { OrganizationsController } from './organizations.controller';
import { OrganizationsService } from './organizations.service';
@Module({
imports: [AuthModule, MembershipsModule, OrgRolesModule, PipelinesModule],
controllers: [OrganizationsController],
providers: [OrganizationsService],
})
@@ -2,6 +2,7 @@ import {
BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import type {
OrganizationDto,
@@ -11,10 +12,74 @@ import { CURRENCY_CATALOG, isBusinessField } from '@mento/shared';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { isKnownCurrency } from '../common/currency';
import { MembershipsService } from '../memberships/memberships.service';
import { OrgRolesService } from '../org-roles/org-roles.service';
import { PipelinesService } from '../pipelines/pipelines.service';
function slugify(input: string): string {
return (
input
.trim()
.toLowerCase()
.replace(/[^\w\u0600-\u06FF]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 48) || `org-${Date.now()}`
);
}
@Injectable()
export class OrganizationsService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly memberships: MembershipsService,
private readonly orgRoles: OrgRolesService,
private readonly pipelines: PipelinesService,
) {}
async createForUser(
userId: string,
dto: { name: string; businessField?: string },
): Promise<{ id: string }> {
const name = dto.name.trim();
if (name.length < 2) {
throw new BadRequestException('نام محیط معتبر نیست');
}
const businessField =
dto.businessField && isBusinessField(dto.businessField)
? dto.businessField
: 'personal';
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true, isActive: true },
});
if (!user || user.isActive === false) {
throw new UnauthorizedException();
}
const baseSlug = slugify(name);
let slug = baseSlug;
let i = 1;
while (await this.prisma.organization.findUnique({ where: { slug } })) {
slug = `${baseSlug}-${i++}`;
}
const organization = await this.prisma.organization.create({
data: {
name,
slug,
businessField,
},
});
await this.memberships.createMembership({
userId,
organizationId: organization.id,
role: 'owner',
});
await this.orgRoles.ensureDefaultRoles(organization.id);
await this.pipelines.ensureDefaultPipeline(organization.id);
return { id: organization.id };
}
async getCurrent(organizationId: string): Promise<OrganizationDto> {
const org = await this.prisma.organization.findUnique({