improvement: multi organization possibility implemented for users (owners and staffs)

This commit is contained in:
2026-04-29 20:19:40 +03:30
parent c154b6dabf
commit ca9e684ab4
14 changed files with 317 additions and 65 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE "users"
ADD COLUMN "trialUsedAt" TIMESTAMP(3);

View File

@@ -15,6 +15,7 @@ model User {
googleId String? @unique
facebookId String? @unique
name String
trialUsedAt DateTime?
memberships Membership[]
ownedOrganizations Organization[] @relation("OrganizationOwner")

View File

@@ -25,6 +25,7 @@ import {
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { CreateOrganizationDto } from './dto/create-organization.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { LocalAuthGuard } from './guards/local-auth.guard';
@@ -120,6 +121,14 @@ export class AuthController {
};
}
@Post('organizations')
@UseGuards(JwtAuthGuard)
@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);
}
// =========================
// PROFILE
// =========================

View File

@@ -12,6 +12,7 @@ import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { CreateOrganizationDto } from './dto/create-organization.dto';
import { JwtPayload } from './interfaces/jwt-payload.interface';
const ALL_PERMISSIONS = [
@@ -176,7 +177,7 @@ export class AuthService {
* @returns Created user info without password
*/
async register(registerDto: RegisterDto) {
const { email, password, name, organizationName, organizationType } = registerDto;
const { email, password, name, organizationName, organizationEmail, organizationType } = registerDto;
// 1. Check existing user
const existingUser = await this.prisma.user.findUnique({
@@ -184,7 +185,7 @@ export class AuthService {
});
if (existingUser) {
throw new ConflictException('User already exists');
throw new ConflictException('User already exists. Please login and create a new organization from your account.');
}
// 2. Hash password
@@ -198,16 +199,15 @@ export class AuthService {
email,
passwordHash: hashedPassword,
name,
trialUsedAt: new Date(),
},
});
// Create organization
const organization = await tx.organization.create({
data: {
name: registerDto.organizationName,
// REQUIRED FIELDS 👇
email: registerDto.email, // or separate org email if you have one
name: organizationName,
email: organizationEmail,
owner: {
connect: { id: user.id },
@@ -219,7 +219,7 @@ export class AuthService {
type: {
connect: {
name: registerDto.organizationType, // 'CLINIC' | 'LAB'
name: organizationType, // 'CLINIC' | 'LAB'
},
},
},
@@ -247,6 +247,66 @@ export class AuthService {
return this.login({ email, password } as any, validatedUser);
}
async createOrganization(userId: string, dto: CreateOrganizationDto) {
const owner = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true, trialUsedAt: true },
});
if (!owner) {
throw new UnauthorizedException('User not found');
}
const planName = dto.planName?.trim() || 'Small';
const effectivePlanName = owner.trialUsedAt ? planName : 'trial';
const organization = await this.prisma.$transaction(async (tx) => {
const createdOrganization = await tx.organization.create({
data: {
name: dto.organizationName,
email: dto.organizationEmail,
owner: {
connect: { id: userId },
},
plan: {
connect: { name: effectivePlanName },
},
type: {
connect: { name: dto.organizationType },
},
},
});
await tx.membership.create({
data: {
userId,
organizationId: createdOrganization.id,
isOwner: true,
},
});
if (!owner.trialUsedAt) {
await tx.user.update({
where: { id: userId },
data: { trialUsedAt: new Date() },
});
}
return createdOrganization;
});
return {
success: true,
data: {
organization: {
id: organization.id,
name: organization.name,
email: organization.email,
},
},
};
}
/**
* Get user profile with all memberships and permissions
* @param userId - User ID from JWT token
@@ -617,6 +677,7 @@ export class AuthService {
organizationId,
},
include: {
user: true,
organization: {
include: {
type: true,
@@ -638,7 +699,7 @@ export class AuthService {
// 2. Build payload WITH org context
const payload = {
sub: userId,
email: membership.organization.email,
email: membership.user.email,
organizationId: membership.organizationId,
type: 'access',
};

View File

@@ -0,0 +1,16 @@
import { IsEmail, IsEnum, IsOptional, IsString } from 'class-validator';
export class CreateOrganizationDto {
@IsString()
organizationName: string;
@IsEmail()
organizationEmail: string;
@IsEnum(['CLINIC', 'LAB'])
organizationType: 'CLINIC' | 'LAB';
@IsOptional()
@IsString()
planName?: string;
}

View File

@@ -14,6 +14,9 @@ export class RegisterDto {
@IsString()
organizationName: string;
@IsEmail()
organizationEmail: string;
@IsEnum(['CLINIC', 'LAB'])
organizationType: 'CLINIC' | 'LAB';
}

View File

@@ -2,6 +2,7 @@
export interface JwtPayload {
sub: string; // user id
email: string;
organizationId?: string;
type?: 'access' | 'refresh';
}