Neo platform - First Version

This commit is contained in:
Francisco Gaona
2025-11-25 12:21:14 +01:00
commit 484af68571
59 changed files with 3699 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AppBuilderService } from './app-builder.service';
import { RuntimeAppController } from './runtime-app.controller';
import { SetupAppController } from './setup-app.controller';
@Module({
providers: [AppBuilderService],
controllers: [RuntimeAppController, SetupAppController],
exports: [AppBuilderService],
})
export class AppBuilderModule {}

View File

@@ -0,0 +1,242 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class AppBuilderService {
constructor(private prisma: PrismaService) {}
// Runtime endpoints
async getApps(tenantId: string, userId: string) {
// For now, return all active apps for the tenant
// In production, you'd filter by user permissions
return this.prisma.app.findMany({
where: {
tenantId,
isActive: true,
},
include: {
pages: {
where: { isActive: true },
orderBy: { sortOrder: 'asc' },
},
},
orderBy: { label: 'asc' },
});
}
async getApp(tenantId: string, slug: string, userId: string) {
const app = await this.prisma.app.findUnique({
where: {
tenantId_slug: {
tenantId,
slug,
},
},
include: {
pages: {
where: { isActive: true },
orderBy: { sortOrder: 'asc' },
},
},
});
if (!app) {
throw new NotFoundException(`App ${slug} not found`);
}
return app;
}
async getPage(
tenantId: string,
appSlug: string,
pageSlug: string,
userId: string,
) {
const app = await this.getApp(tenantId, appSlug, userId);
const page = await this.prisma.appPage.findFirst({
where: {
appId: app.id,
slug: pageSlug,
isActive: true,
},
include: {
object: {
include: {
fields: {
where: { isActive: true },
},
},
},
},
});
if (!page) {
throw new NotFoundException(`Page ${pageSlug} not found`);
}
return page;
}
// Setup endpoints
async getAllApps(tenantId: string) {
return this.prisma.app.findMany({
where: { tenantId },
include: {
pages: {
orderBy: { sortOrder: 'asc' },
},
},
orderBy: { label: 'asc' },
});
}
async getAppForSetup(tenantId: string, slug: string) {
const app = await this.prisma.app.findUnique({
where: {
tenantId_slug: {
tenantId,
slug,
},
},
include: {
pages: {
orderBy: { sortOrder: 'asc' },
},
},
});
if (!app) {
throw new NotFoundException(`App ${slug} not found`);
}
return app;
}
async createApp(
tenantId: string,
data: {
slug: string;
label: string;
description?: string;
icon?: string;
},
) {
return this.prisma.app.create({
data: {
tenantId,
...data,
},
});
}
async updateApp(
tenantId: string,
slug: string,
data: {
label?: string;
description?: string;
icon?: string;
isActive?: boolean;
},
) {
const app = await this.getAppForSetup(tenantId, slug);
return this.prisma.app.update({
where: { id: app.id },
data,
});
}
async createPage(
tenantId: string,
appSlug: string,
data: {
slug: string;
label: string;
type: string;
objectApiName?: string;
config?: any;
sortOrder?: number;
},
) {
const app = await this.getAppForSetup(tenantId, appSlug);
// If objectApiName is provided, find the object
let objectId: string | undefined;
if (data.objectApiName) {
const obj = await this.prisma.objectDefinition.findUnique({
where: {
tenantId_apiName: {
tenantId,
apiName: data.objectApiName,
},
},
});
objectId = obj?.id;
}
return this.prisma.appPage.create({
data: {
appId: app.id,
slug: data.slug,
label: data.label,
type: data.type,
objectApiName: data.objectApiName,
objectId,
config: data.config,
sortOrder: data.sortOrder || 0,
},
});
}
async updatePage(
tenantId: string,
appSlug: string,
pageSlug: string,
data: {
label?: string;
type?: string;
objectApiName?: string;
config?: any;
sortOrder?: number;
isActive?: boolean;
},
) {
const app = await this.getAppForSetup(tenantId, appSlug);
const page = await this.prisma.appPage.findFirst({
where: {
appId: app.id,
slug: pageSlug,
},
});
if (!page) {
throw new NotFoundException(`Page ${pageSlug} not found`);
}
// If objectApiName is provided, find the object
let objectId: string | undefined;
if (data.objectApiName) {
const obj = await this.prisma.objectDefinition.findUnique({
where: {
tenantId_apiName: {
tenantId,
apiName: data.objectApiName,
},
},
});
objectId = obj?.id;
}
return this.prisma.appPage.update({
where: { id: page.id },
data: {
...data,
objectId,
},
});
}
}

View File

@@ -0,0 +1,45 @@
import {
Controller,
Get,
Param,
UseGuards,
} from '@nestjs/common';
import { AppBuilderService } from './app-builder.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { TenantId } from '../tenant/tenant.decorator';
@Controller('runtime/apps')
@UseGuards(JwtAuthGuard)
export class RuntimeAppController {
constructor(private appBuilderService: AppBuilderService) {}
@Get()
async getApps(@TenantId() tenantId: string, @CurrentUser() user: any) {
return this.appBuilderService.getApps(tenantId, user.userId);
}
@Get(':appSlug')
async getApp(
@TenantId() tenantId: string,
@Param('appSlug') appSlug: string,
@CurrentUser() user: any,
) {
return this.appBuilderService.getApp(tenantId, appSlug, user.userId);
}
@Get(':appSlug/pages/:pageSlug')
async getPage(
@TenantId() tenantId: string,
@Param('appSlug') appSlug: string,
@Param('pageSlug') pageSlug: string,
@CurrentUser() user: any,
) {
return this.appBuilderService.getPage(
tenantId,
appSlug,
pageSlug,
user.userId,
);
}
}

View File

@@ -0,0 +1,69 @@
import {
Controller,
Get,
Post,
Put,
Param,
Body,
UseGuards,
} from '@nestjs/common';
import { AppBuilderService } from './app-builder.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { TenantId } from '../tenant/tenant.decorator';
@Controller('setup/apps')
@UseGuards(JwtAuthGuard)
export class SetupAppController {
constructor(private appBuilderService: AppBuilderService) {}
@Get()
async getAllApps(@TenantId() tenantId: string) {
return this.appBuilderService.getAllApps(tenantId);
}
@Get(':appSlug')
async getApp(
@TenantId() tenantId: string,
@Param('appSlug') appSlug: string,
) {
return this.appBuilderService.getAppForSetup(tenantId, appSlug);
}
@Post()
async createApp(@TenantId() tenantId: string, @Body() data: any) {
return this.appBuilderService.createApp(tenantId, data);
}
@Put(':appSlug')
async updateApp(
@TenantId() tenantId: string,
@Param('appSlug') appSlug: string,
@Body() data: any,
) {
return this.appBuilderService.updateApp(tenantId, appSlug, data);
}
@Post(':appSlug/pages')
async createPage(
@TenantId() tenantId: string,
@Param('appSlug') appSlug: string,
@Body() data: any,
) {
return this.appBuilderService.createPage(tenantId, appSlug, data);
}
@Put(':appSlug/pages/:pageSlug')
async updatePage(
@TenantId() tenantId: string,
@Param('appSlug') appSlug: string,
@Param('pageSlug') pageSlug: string,
@Body() data: any,
) {
return this.appBuilderService.updatePage(
tenantId,
appSlug,
pageSlug,
data,
);
}
}

23
backend/src/app.module.ts Normal file
View File

@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PrismaModule } from './prisma/prisma.module';
import { TenantModule } from './tenant/tenant.module';
import { AuthModule } from './auth/auth.module';
import { RbacModule } from './rbac/rbac.module';
import { ObjectModule } from './object/object.module';
import { AppBuilderModule } from './app-builder/app-builder.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
PrismaModule,
TenantModule,
AuthModule,
RbacModule,
ObjectModule,
AppBuilderModule,
],
})
export class AppModule {}

View File

@@ -0,0 +1,82 @@
import {
Controller,
Post,
Body,
UnauthorizedException,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
import { AuthService } from './auth.service';
import { TenantId } from '../tenant/tenant.decorator';
class LoginDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
}
class RegisterDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsOptional()
@IsString()
firstName?: string;
@IsOptional()
@IsString()
lastName?: string;
}
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
async login(@TenantId() tenantId: string, @Body() loginDto: LoginDto) {
if (!tenantId) {
throw new UnauthorizedException('Tenant ID is required');
}
const user = await this.authService.validateUser(
tenantId,
loginDto.email,
loginDto.password,
);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return this.authService.login(user);
}
@Post('register')
async register(
@TenantId() tenantId: string,
@Body() registerDto: RegisterDto,
) {
if (!tenantId) {
throw new UnauthorizedException('Tenant ID is required');
}
const user = await this.authService.register(
tenantId,
registerDto.email,
registerDto.password,
registerDto.firstName,
registerDto.lastName,
);
return user;
}
}

View File

@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET', 'devsecret'),
signOptions: { expiresIn: '24h' },
}),
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,92 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async validateUser(
tenantId: string,
email: string,
password: string,
): Promise<any> {
const user = await this.prisma.user.findUnique({
where: {
tenantId_email: {
tenantId,
email,
},
},
include: {
tenant: true,
userRoles: {
include: {
role: {
include: {
rolePermissions: {
include: {
permission: true,
},
},
},
},
},
},
},
});
if (user && (await bcrypt.compare(password, user.password))) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = {
sub: user.id,
email: user.email,
tenantId: user.tenantId,
};
return {
access_token: this.jwtService.sign(payload),
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
tenantId: user.tenantId,
},
};
}
async register(
tenantId: string,
email: string,
password: string,
firstName?: string,
lastName?: string,
) {
const hashedPassword = await bcrypt.hash(password, 10);
const user = await this.prisma.user.create({
data: {
tenantId,
email,
password: hashedPassword,
firstName,
lastName,
},
});
const { password: _, ...result } = user;
return result;
}
}

View File

@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@@ -0,0 +1,30 @@
import { Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { FastifyRequest } from 'fastify';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: (request: FastifyRequest) => {
const authHeader = request.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
},
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET', 'devsecret'),
});
}
async validate(payload: any) {
return {
userId: payload.sub,
email: payload.email,
tenantId: payload.tenantId,
};
}
}

39
backend/src/main.ts Normal file
View File

@@ -0,0 +1,39 @@
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
// Global validation pipe
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
// Enable CORS
app.enableCors({
origin: true,
credentials: true,
});
// Global prefix
app.setGlobalPrefix('api');
const port = process.env.PORT || 3000;
await app.listen(port, '0.0.0.0');
console.log(`🚀 Application is running on: http://localhost:${port}/api`);
}
bootstrap();

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ObjectService } from './object.service';
import { RuntimeObjectController } from './runtime-object.controller';
import { SetupObjectController } from './setup-object.controller';
@Module({
providers: [ObjectService],
controllers: [RuntimeObjectController, SetupObjectController],
exports: [ObjectService],
})
export class ObjectModule {}

View File

@@ -0,0 +1,191 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class ObjectService {
constructor(private prisma: PrismaService) {}
// Setup endpoints - Object metadata management
async getObjectDefinitions(tenantId: string) {
return this.prisma.objectDefinition.findMany({
where: { tenantId },
include: {
fields: true,
},
orderBy: { label: 'asc' },
});
}
async getObjectDefinition(tenantId: string, apiName: string) {
const obj = await this.prisma.objectDefinition.findUnique({
where: {
tenantId_apiName: {
tenantId,
apiName,
},
},
include: {
fields: {
where: { isActive: true },
orderBy: { label: 'asc' },
},
},
});
if (!obj) {
throw new NotFoundException(`Object ${apiName} not found`);
}
return obj;
}
async createObjectDefinition(
tenantId: string,
data: {
apiName: string;
label: string;
pluralLabel?: string;
description?: string;
isSystem?: boolean;
},
) {
return this.prisma.objectDefinition.create({
data: {
tenantId,
...data,
tableName: `custom_${data.apiName.toLowerCase()}`,
},
});
}
async createFieldDefinition(
tenantId: string,
objectApiName: string,
data: {
apiName: string;
label: string;
type: string;
description?: string;
isRequired?: boolean;
isUnique?: boolean;
isLookup?: boolean;
referenceTo?: string;
defaultValue?: string;
options?: any;
},
) {
const obj = await this.getObjectDefinition(tenantId, objectApiName);
return this.prisma.fieldDefinition.create({
data: {
objectId: obj.id,
...data,
},
});
}
// Runtime endpoints - CRUD operations
async getRecords(
tenantId: string,
objectApiName: string,
userId: string,
filters?: any,
) {
// For demonstration, using Account as example static object
if (objectApiName === 'Account') {
return this.prisma.account.findMany({
where: {
tenantId,
ownerId: userId, // Basic sharing rule
...filters,
},
});
}
// For custom objects, you'd need dynamic query building
// This is a simplified version
throw new Error(`Runtime queries for ${objectApiName} not yet implemented`);
}
async getRecord(
tenantId: string,
objectApiName: string,
recordId: string,
userId: string,
) {
if (objectApiName === 'Account') {
const record = await this.prisma.account.findFirst({
where: {
id: recordId,
tenantId,
ownerId: userId,
},
});
if (!record) {
throw new NotFoundException('Record not found');
}
return record;
}
throw new Error(`Runtime queries for ${objectApiName} not yet implemented`);
}
async createRecord(
tenantId: string,
objectApiName: string,
data: any,
userId: string,
) {
if (objectApiName === 'Account') {
return this.prisma.account.create({
data: {
tenantId,
ownerId: userId,
...data,
},
});
}
throw new Error(`Runtime queries for ${objectApiName} not yet implemented`);
}
async updateRecord(
tenantId: string,
objectApiName: string,
recordId: string,
data: any,
userId: string,
) {
if (objectApiName === 'Account') {
// Verify ownership
await this.getRecord(tenantId, objectApiName, recordId, userId);
return this.prisma.account.update({
where: { id: recordId },
data,
});
}
throw new Error(`Runtime queries for ${objectApiName} not yet implemented`);
}
async deleteRecord(
tenantId: string,
objectApiName: string,
recordId: string,
userId: string,
) {
if (objectApiName === 'Account') {
// Verify ownership
await this.getRecord(tenantId, objectApiName, recordId, userId);
return this.prisma.account.delete({
where: { id: recordId },
});
}
throw new Error(`Runtime queries for ${objectApiName} not yet implemented`);
}
}

View File

@@ -0,0 +1,98 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
Query,
UseGuards,
} from '@nestjs/common';
import { ObjectService } from './object.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { TenantId } from '../tenant/tenant.decorator';
@Controller('runtime/objects')
@UseGuards(JwtAuthGuard)
export class RuntimeObjectController {
constructor(private objectService: ObjectService) {}
@Get(':objectApiName/records')
async getRecords(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
@CurrentUser() user: any,
@Query() query: any,
) {
return this.objectService.getRecords(
tenantId,
objectApiName,
user.userId,
query,
);
}
@Get(':objectApiName/records/:id')
async getRecord(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
@Param('id') id: string,
@CurrentUser() user: any,
) {
return this.objectService.getRecord(
tenantId,
objectApiName,
id,
user.userId,
);
}
@Post(':objectApiName/records')
async createRecord(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
@Body() data: any,
@CurrentUser() user: any,
) {
return this.objectService.createRecord(
tenantId,
objectApiName,
data,
user.userId,
);
}
@Put(':objectApiName/records/:id')
async updateRecord(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
@Param('id') id: string,
@Body() data: any,
@CurrentUser() user: any,
) {
return this.objectService.updateRecord(
tenantId,
objectApiName,
id,
data,
user.userId,
);
}
@Delete(':objectApiName/records/:id')
async deleteRecord(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
@Param('id') id: string,
@CurrentUser() user: any,
) {
return this.objectService.deleteRecord(
tenantId,
objectApiName,
id,
user.userId,
);
}
}

View File

@@ -0,0 +1,51 @@
import {
Controller,
Get,
Post,
Param,
Body,
UseGuards,
} from '@nestjs/common';
import { ObjectService } from './object.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { TenantId } from '../tenant/tenant.decorator';
@Controller('setup/objects')
@UseGuards(JwtAuthGuard)
export class SetupObjectController {
constructor(private objectService: ObjectService) {}
@Get()
async getObjectDefinitions(@TenantId() tenantId: string) {
return this.objectService.getObjectDefinitions(tenantId);
}
@Get(':objectApiName')
async getObjectDefinition(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
) {
return this.objectService.getObjectDefinition(tenantId, objectApiName);
}
@Post()
async createObjectDefinition(
@TenantId() tenantId: string,
@Body() data: any,
) {
return this.objectService.createObjectDefinition(tenantId, data);
}
@Post(':objectApiName/fields')
async createFieldDefinition(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
@Body() data: any,
) {
return this.objectService.createFieldDefinition(
tenantId,
objectApiName,
data,
);
}
}

View File

@@ -0,0 +1,9 @@
import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,16 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View File

@@ -0,0 +1,35 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RbacService } from './rbac.service';
import { PERMISSIONS_KEY } from './rbac.decorator';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(
private reflector: Reflector,
private rbacService: RbacService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredPermissions) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false;
}
return this.rbacService.hasAllPermissions(
user.userId,
requiredPermissions,
);
}
}

View File

@@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const Permissions = (...permissions: string[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { RbacService } from './rbac.service';
@Module({
providers: [RbacService],
exports: [RbacService],
})
export class RbacModule {}

View File

@@ -0,0 +1,103 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class RbacService {
constructor(private prisma: PrismaService) {}
async getUserPermissions(userId: string): Promise<string[]> {
const userRoles = await this.prisma.userRole.findMany({
where: { userId },
include: {
role: {
include: {
rolePermissions: {
include: {
permission: true,
},
},
},
},
},
});
const permissions = new Set<string>();
userRoles.forEach((userRole) => {
userRole.role.rolePermissions.forEach((rp) => {
permissions.add(rp.permission.name);
});
});
return Array.from(permissions);
}
async getUserRoles(userId: string): Promise<string[]> {
const userRoles = await this.prisma.userRole.findMany({
where: { userId },
include: {
role: true,
},
});
return userRoles.map((ur) => ur.role.name);
}
async hasPermission(userId: string, permission: string): Promise<boolean> {
const permissions = await this.getUserPermissions(userId);
return permissions.includes(permission);
}
async hasRole(userId: string, role: string): Promise<boolean> {
const roles = await this.getUserRoles(userId);
return roles.includes(role);
}
async hasAnyPermission(
userId: string,
permissions: string[],
): Promise<boolean> {
const userPermissions = await this.getUserPermissions(userId);
return permissions.some((p) => userPermissions.includes(p));
}
async hasAllPermissions(
userId: string,
permissions: string[],
): Promise<boolean> {
const userPermissions = await this.getUserPermissions(userId);
return permissions.every((p) => userPermissions.includes(p));
}
async assignRole(userId: string, roleId: string) {
return this.prisma.userRole.create({
data: {
userId,
roleId,
},
});
}
async removeRole(userId: string, roleId: string) {
return this.prisma.userRole.deleteMany({
where: {
userId,
roleId,
},
});
}
async syncPermissionsToRole(roleId: string, permissionIds: string[]) {
// Remove existing permissions
await this.prisma.rolePermission.deleteMany({
where: { roleId },
});
// Add new permissions
await this.prisma.rolePermission.createMany({
data: permissionIds.map((permissionId) => ({
roleId,
permissionId,
})),
});
}
}

View File

@@ -0,0 +1,33 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RbacService } from './rbac.service';
import { ROLES_KEY } from './rbac.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private rbacService: RbacService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false;
}
const userRoles = await this.rbacService.getUserRoles(user.userId);
return requiredRoles.some((role) => userRoles.includes(role));
}
}

View File

@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const TenantId = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.raw.tenantId;
},
);

View File

@@ -0,0 +1,16 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
const tenantId = req.headers['x-tenant-id'] as string;
if (tenantId) {
// Attach tenantId to request object
(req as any).tenantId = tenantId;
}
next();
}
}

View File

@@ -0,0 +1,9 @@
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { TenantMiddleware } from './tenant.middleware';
@Module({})
export class TenantModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TenantMiddleware).forRoutes('*');
}
}