improvement: multi organization possibility implemented for users (owners and staffs)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "users"
|
||||
ADD COLUMN "trialUsedAt" TIMESTAMP(3);
|
||||
@@ -15,6 +15,7 @@ model User {
|
||||
googleId String? @unique
|
||||
facebookId String? @unique
|
||||
name String
|
||||
trialUsedAt DateTime?
|
||||
|
||||
memberships Membership[]
|
||||
ownedOrganizations Organization[] @relation("OrganizationOwner")
|
||||
|
||||
@@ -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
|
||||
// =========================
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
16
backend/src/modules/auth/dto/create-organization.dto.ts
Normal file
16
backend/src/modules/auth/dto/create-organization.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -14,6 +14,9 @@ export class RegisterDto {
|
||||
@IsString()
|
||||
organizationName: string;
|
||||
|
||||
@IsEmail()
|
||||
organizationEmail: string;
|
||||
|
||||
@IsEnum(['CLINIC', 'LAB'])
|
||||
organizationType: 'CLINIC' | 'LAB';
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
export interface JwtPayload {
|
||||
sub: string; // user id
|
||||
email: string;
|
||||
organizationId?: string;
|
||||
type?: 'access' | 'refresh';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user