Neo platform - First Version
This commit is contained in:
25
backend/.eslintrc.js
Normal file
25
backend/.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
4
backend/.prettierrc
Normal file
4
backend/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
25
backend/Dockerfile
Normal file
25
backend/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM node:22-alpine AS base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install OpenSSL and other dependencies required by Prisma
|
||||
RUN apk add --no-cache openssl libc6-compat
|
||||
|
||||
# Install dependencies separately for better caching
|
||||
FROM base AS deps
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
FROM base AS dev
|
||||
ENV NODE_ENV=development
|
||||
|
||||
# Install OpenSSL for Prisma
|
||||
RUN apk add --no-cache openssl libc6-compat
|
||||
|
||||
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Prisma: generate client
|
||||
RUN npx prisma generate || true
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "start:dev"]
|
||||
8
backend/nest-cli.json
Normal file
8
backend/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
82
backend/package.json
Normal file
82
backend/package.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "neo-backend",
|
||||
"version": "0.0.1",
|
||||
"description": "Multi-tenant Nova/Salesforce-like platform backend",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.3.0",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/platform-fastify": "^10.3.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/bullmq": "^10.1.0",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"class-validator": "^0.14.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.3.0",
|
||||
"@nestjs/schematics": "^10.1.0",
|
||||
"@nestjs/testing": "^10.3.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/passport-jwt": "^4.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.2.4",
|
||||
"prisma": "^5.8.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
224
backend/prisma/migrations/20250101000000_init/migration.sql
Normal file
224
backend/prisma/migrations/20250101000000_init/migration.sql
Normal file
@@ -0,0 +1,224 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `tenants` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`slug` VARCHAR(191) NOT NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `tenants_slug_key`(`slug`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `users` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`tenantId` VARCHAR(191) NOT NULL,
|
||||
`email` VARCHAR(191) NOT NULL,
|
||||
`password` VARCHAR(191) NOT NULL,
|
||||
`firstName` VARCHAR(191) NULL,
|
||||
`lastName` VARCHAR(191) NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `users_tenantId_idx`(`tenantId`),
|
||||
UNIQUE INDEX `users_tenantId_email_key`(`tenantId`, `email`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `roles` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`tenantId` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`guardName` VARCHAR(191) NOT NULL DEFAULT 'api',
|
||||
`description` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `roles_tenantId_idx`(`tenantId`),
|
||||
UNIQUE INDEX `roles_tenantId_name_guardName_key`(`tenantId`, `name`, `guardName`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `permissions` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`tenantId` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`guardName` VARCHAR(191) NOT NULL DEFAULT 'api',
|
||||
`description` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `permissions_tenantId_idx`(`tenantId`),
|
||||
UNIQUE INDEX `permissions_tenantId_name_guardName_key`(`tenantId`, `name`, `guardName`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `user_roles` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`userId` VARCHAR(191) NOT NULL,
|
||||
`roleId` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
INDEX `user_roles_userId_idx`(`userId`),
|
||||
INDEX `user_roles_roleId_idx`(`roleId`),
|
||||
UNIQUE INDEX `user_roles_userId_roleId_key`(`userId`, `roleId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `role_permissions` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`roleId` VARCHAR(191) NOT NULL,
|
||||
`permissionId` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
INDEX `role_permissions_roleId_idx`(`roleId`),
|
||||
INDEX `role_permissions_permissionId_idx`(`permissionId`),
|
||||
UNIQUE INDEX `role_permissions_roleId_permissionId_key`(`roleId`, `permissionId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `object_definitions` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`tenantId` VARCHAR(191) NOT NULL,
|
||||
`apiName` VARCHAR(191) NOT NULL,
|
||||
`label` VARCHAR(191) NOT NULL,
|
||||
`pluralLabel` VARCHAR(191) NULL,
|
||||
`description` TEXT NULL,
|
||||
`isSystem` BOOLEAN NOT NULL DEFAULT false,
|
||||
`tableName` VARCHAR(191) NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `object_definitions_tenantId_idx`(`tenantId`),
|
||||
UNIQUE INDEX `object_definitions_tenantId_apiName_key`(`tenantId`, `apiName`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `field_definitions` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`objectId` VARCHAR(191) NOT NULL,
|
||||
`apiName` VARCHAR(191) NOT NULL,
|
||||
`label` VARCHAR(191) NOT NULL,
|
||||
`type` VARCHAR(191) NOT NULL,
|
||||
`description` TEXT NULL,
|
||||
`isRequired` BOOLEAN NOT NULL DEFAULT false,
|
||||
`isUnique` BOOLEAN NOT NULL DEFAULT false,
|
||||
`isReadonly` BOOLEAN NOT NULL DEFAULT false,
|
||||
`isLookup` BOOLEAN NOT NULL DEFAULT false,
|
||||
`referenceTo` VARCHAR(191) NULL,
|
||||
`defaultValue` VARCHAR(191) NULL,
|
||||
`options` JSON NULL,
|
||||
`validationRules` JSON NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `field_definitions_objectId_idx`(`objectId`),
|
||||
UNIQUE INDEX `field_definitions_objectId_apiName_key`(`objectId`, `apiName`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `accounts` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`tenantId` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`status` VARCHAR(191) NOT NULL DEFAULT 'active',
|
||||
`ownerId` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `accounts_tenantId_idx`(`tenantId`),
|
||||
INDEX `accounts_ownerId_idx`(`ownerId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `apps` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`tenantId` VARCHAR(191) NOT NULL,
|
||||
`slug` VARCHAR(191) NOT NULL,
|
||||
`label` VARCHAR(191) NOT NULL,
|
||||
`description` TEXT NULL,
|
||||
`icon` VARCHAR(191) NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `apps_tenantId_idx`(`tenantId`),
|
||||
UNIQUE INDEX `apps_tenantId_slug_key`(`tenantId`, `slug`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `app_pages` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`appId` VARCHAR(191) NOT NULL,
|
||||
`slug` VARCHAR(191) NOT NULL,
|
||||
`label` VARCHAR(191) NOT NULL,
|
||||
`type` VARCHAR(191) NOT NULL,
|
||||
`objectApiName` VARCHAR(191) NULL,
|
||||
`objectId` VARCHAR(191) NULL,
|
||||
`config` JSON NULL,
|
||||
`sortOrder` INTEGER NOT NULL DEFAULT 0,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `app_pages_appId_idx`(`appId`),
|
||||
INDEX `app_pages_objectId_idx`(`objectId`),
|
||||
UNIQUE INDEX `app_pages_appId_slug_key`(`appId`, `slug`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `users` ADD CONSTRAINT `users_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `roles` ADD CONSTRAINT `roles_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `permissions` ADD CONSTRAINT `permissions_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `user_roles` ADD CONSTRAINT `user_roles_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `user_roles` ADD CONSTRAINT `user_roles_roleId_fkey` FOREIGN KEY (`roleId`) REFERENCES `roles`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `role_permissions` ADD CONSTRAINT `role_permissions_roleId_fkey` FOREIGN KEY (`roleId`) REFERENCES `roles`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `role_permissions` ADD CONSTRAINT `role_permissions_permissionId_fkey` FOREIGN KEY (`permissionId`) REFERENCES `permissions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `object_definitions` ADD CONSTRAINT `object_definitions_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `field_definitions` ADD CONSTRAINT `field_definitions_objectId_fkey` FOREIGN KEY (`objectId`) REFERENCES `object_definitions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `accounts` ADD CONSTRAINT `accounts_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `accounts` ADD CONSTRAINT `accounts_ownerId_fkey` FOREIGN KEY (`ownerId`) REFERENCES `users`(`id`) ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `apps` ADD CONSTRAINT `apps_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `app_pages` ADD CONSTRAINT `app_pages_appId_fkey` FOREIGN KEY (`appId`) REFERENCES `apps`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `app_pages` ADD CONSTRAINT `app_pages_objectId_fkey` FOREIGN KEY (`objectId`) REFERENCES `object_definitions`(`id`) ON UPDATE CASCADE;
|
||||
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "mysql"
|
||||
227
backend/prisma/schema.prisma
Normal file
227
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,227 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// Multi-tenancy
|
||||
model Tenant {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
slug String @unique
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
users User[]
|
||||
objectDefinitions ObjectDefinition[]
|
||||
accounts Account[]
|
||||
apps App[]
|
||||
roles Role[]
|
||||
permissions Permission[]
|
||||
|
||||
@@map("tenants")
|
||||
}
|
||||
|
||||
// User & Auth
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
email String
|
||||
password String
|
||||
firstName String?
|
||||
lastName String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
userRoles UserRole[]
|
||||
accounts Account[]
|
||||
|
||||
@@unique([tenantId, email])
|
||||
@@index([tenantId])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// RBAC - Spatie-like
|
||||
model Role {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
name String
|
||||
guardName String @default("api")
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
userRoles UserRole[]
|
||||
rolePermissions RolePermission[]
|
||||
|
||||
@@unique([tenantId, name, guardName])
|
||||
@@index([tenantId])
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
name String
|
||||
guardName String @default("api")
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
rolePermissions RolePermission[]
|
||||
|
||||
@@unique([tenantId, name, guardName])
|
||||
@@index([tenantId])
|
||||
@@map("permissions")
|
||||
}
|
||||
|
||||
model UserRole {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
roleId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, roleId])
|
||||
@@index([userId])
|
||||
@@index([roleId])
|
||||
@@map("user_roles")
|
||||
}
|
||||
|
||||
model RolePermission {
|
||||
id String @id @default(uuid())
|
||||
roleId String
|
||||
permissionId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([roleId, permissionId])
|
||||
@@index([roleId])
|
||||
@@index([permissionId])
|
||||
@@map("role_permissions")
|
||||
}
|
||||
|
||||
// Object Definition (Metadata)
|
||||
model ObjectDefinition {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
apiName String
|
||||
label String
|
||||
pluralLabel String?
|
||||
description String? @db.Text
|
||||
isSystem Boolean @default(false)
|
||||
tableName String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
fields FieldDefinition[]
|
||||
pages AppPage[]
|
||||
|
||||
@@unique([tenantId, apiName])
|
||||
@@index([tenantId])
|
||||
@@map("object_definitions")
|
||||
}
|
||||
|
||||
model FieldDefinition {
|
||||
id String @id @default(uuid())
|
||||
objectId String
|
||||
apiName String
|
||||
label String
|
||||
type String // text, number, boolean, date, datetime, lookup, picklist, etc.
|
||||
description String? @db.Text
|
||||
isRequired Boolean @default(false)
|
||||
isUnique Boolean @default(false)
|
||||
isReadonly Boolean @default(false)
|
||||
isLookup Boolean @default(false)
|
||||
referenceTo String? // objectApiName for lookup fields
|
||||
defaultValue String?
|
||||
options Json? // for picklist fields
|
||||
validationRules Json? // custom validation rules
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
object ObjectDefinition @relation(fields: [objectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([objectId, apiName])
|
||||
@@index([objectId])
|
||||
@@map("field_definitions")
|
||||
}
|
||||
|
||||
// Example static object: Account
|
||||
model Account {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
name String
|
||||
status String @default("active")
|
||||
ownerId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
owner User @relation(fields: [ownerId], references: [id])
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([ownerId])
|
||||
@@map("accounts")
|
||||
}
|
||||
|
||||
// Application Builder
|
||||
model App {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
slug String
|
||||
label String
|
||||
description String? @db.Text
|
||||
icon String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
pages AppPage[]
|
||||
|
||||
@@unique([tenantId, slug])
|
||||
@@index([tenantId])
|
||||
@@map("apps")
|
||||
}
|
||||
|
||||
model AppPage {
|
||||
id String @id @default(uuid())
|
||||
appId String
|
||||
slug String
|
||||
label String
|
||||
type String // list, board, dashboard, form, detail
|
||||
objectApiName String?
|
||||
objectId String?
|
||||
config Json? // page-specific configuration
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
object ObjectDefinition? @relation(fields: [objectId], references: [id])
|
||||
|
||||
@@unique([appId, slug])
|
||||
@@index([appId])
|
||||
@@index([objectId])
|
||||
@@map("app_pages")
|
||||
}
|
||||
11
backend/src/app-builder/app-builder.module.ts
Normal file
11
backend/src/app-builder/app-builder.module.ts
Normal 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 {}
|
||||
242
backend/src/app-builder/app-builder.service.ts
Normal file
242
backend/src/app-builder/app-builder.service.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
45
backend/src/app-builder/runtime-app.controller.ts
Normal file
45
backend/src/app-builder/runtime-app.controller.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
69
backend/src/app-builder/setup-app.controller.ts
Normal file
69
backend/src/app-builder/setup-app.controller.ts
Normal 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
23
backend/src/app.module.ts
Normal 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 {}
|
||||
82
backend/src/auth/auth.controller.ts
Normal file
82
backend/src/auth/auth.controller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
24
backend/src/auth/auth.module.ts
Normal file
24
backend/src/auth/auth.module.ts
Normal 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 {}
|
||||
92
backend/src/auth/auth.service.ts
Normal file
92
backend/src/auth/auth.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
8
backend/src/auth/current-user.decorator.ts
Normal file
8
backend/src/auth/current-user.decorator.ts
Normal 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;
|
||||
},
|
||||
);
|
||||
5
backend/src/auth/jwt-auth.guard.ts
Normal file
5
backend/src/auth/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
30
backend/src/auth/jwt.strategy.ts
Normal file
30
backend/src/auth/jwt.strategy.ts
Normal 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
39
backend/src/main.ts
Normal 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();
|
||||
11
backend/src/object/object.module.ts
Normal file
11
backend/src/object/object.module.ts
Normal 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 {}
|
||||
191
backend/src/object/object.service.ts
Normal file
191
backend/src/object/object.service.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
98
backend/src/object/runtime-object.controller.ts
Normal file
98
backend/src/object/runtime-object.controller.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
51
backend/src/object/setup-object.controller.ts
Normal file
51
backend/src/object/setup-object.controller.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
9
backend/src/prisma/prisma.module.ts
Normal file
9
backend/src/prisma/prisma.module.ts
Normal 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 {}
|
||||
16
backend/src/prisma/prisma.service.ts
Normal file
16
backend/src/prisma/prisma.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
35
backend/src/rbac/permissions.guard.ts
Normal file
35
backend/src/rbac/permissions.guard.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
8
backend/src/rbac/rbac.decorator.ts
Normal file
8
backend/src/rbac/rbac.decorator.ts
Normal 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);
|
||||
8
backend/src/rbac/rbac.module.ts
Normal file
8
backend/src/rbac/rbac.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RbacService } from './rbac.service';
|
||||
|
||||
@Module({
|
||||
providers: [RbacService],
|
||||
exports: [RbacService],
|
||||
})
|
||||
export class RbacModule {}
|
||||
103
backend/src/rbac/rbac.service.ts
Normal file
103
backend/src/rbac/rbac.service.ts
Normal 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,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
33
backend/src/rbac/roles.guard.ts
Normal file
33
backend/src/rbac/roles.guard.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
8
backend/src/tenant/tenant.decorator.ts
Normal file
8
backend/src/tenant/tenant.decorator.ts
Normal 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;
|
||||
},
|
||||
);
|
||||
16
backend/src/tenant/tenant.middleware.ts
Normal file
16
backend/src/tenant/tenant.middleware.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
9
backend/src/tenant/tenant.module.ts
Normal file
9
backend/src/tenant/tenant.module.ts
Normal 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('*');
|
||||
}
|
||||
}
|
||||
22
backend/tsconfig.json
Normal file
22
backend/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user