🚀 Feature: Implement Global Super Admin access & bypass tenant filtering

This commit is contained in:
Hamza-Ayed
2026-04-22 22:47:53 +03:00
parent 944c82730d
commit 2f238e19c2
9 changed files with 128 additions and 84 deletions

View File

@@ -27,43 +27,43 @@ export class CompaniesController {
constructor(private companiesService: CompaniesService) {} constructor(private companiesService: CompaniesService) {}
@Post() @Post()
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT) @Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
async create(@CurrentUser() user: any, @Body() dto: any) { async create(@CurrentUser() user: any, @Body() dto: any) {
return this.companiesService.create(user.tenantId, dto); return this.companiesService.create(user, dto);
} }
@Get() @Get()
async findAll(@CurrentUser() user: any) { async findAll(@CurrentUser() user: any) {
return this.companiesService.findAll(user.tenantId); return this.companiesService.findAll(user);
} }
@Get(':id') @Get(':id')
async findOne(@CurrentUser() user: any, @Param('id') id: string) { async findOne(@CurrentUser() user: any, @Param('id') id: string) {
return this.companiesService.findOne(user.tenantId, id); return this.companiesService.findOne(user, id);
} }
@Patch(':id') @Patch(':id')
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT) @Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
async update( async update(
@CurrentUser() user: any, @CurrentUser() user: any,
@Param('id') id: string, @Param('id') id: string,
@Body() dto: any, @Body() dto: any,
) { ) {
return this.companiesService.update(user.tenantId, id, dto); return this.companiesService.update(user, id, dto);
} }
/** /**
* ربط بيانات جو فوترة المشفرة * ربط بيانات جو فوترة المشفرة
*/ */
@Put(':id/jofotara') @Put(':id/jofotara')
@Roles(UserRole.ADMIN) @Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async setJoFotara( async setJoFotara(
@CurrentUser() user: any, @CurrentUser() user: any,
@Param('id') id: string, @Param('id') id: string,
@Body() dto: { clientId: string; secretKey: string }, @Body() dto: { clientId: string; secretKey: string },
) { ) {
return this.companiesService.setJoFotaraCredentials( return this.companiesService.setJoFotaraCredentials(
user.tenantId, user,
id, id,
dto.clientId, dto.clientId,
dto.secretKey, dto.secretKey,

View File

@@ -14,6 +14,7 @@ import {
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { Company } from './entities/company.entity'; import { Company } from './entities/company.entity';
import { UserRole } from '../users/enums/role.enum';
import { EncryptionService } from '../../services/encryption/encryption.service'; import { EncryptionService } from '../../services/encryption/encryption.service';
import { SubscriptionsService } from '../subscriptions/subscription.service'; import { SubscriptionsService } from '../subscriptions/subscription.service';
@@ -30,7 +31,8 @@ export class CompaniesService {
/** /**
* إنشاء شركة جديدة مع التحقق من حدود الاشتراك * إنشاء شركة جديدة مع التحقق من حدود الاشتراك
*/ */
async create(tenantId: string, dto: any): Promise<Company> { async create(user: any, dto: any): Promise<Company> {
const tenantId = user.tenantId;
// 1. Check subscription limits // 1. Check subscription limits
const canCreate = await this.subscriptionsService.checkCompanyLimit(tenantId); const canCreate = await this.subscriptionsService.checkCompanyLimit(tenantId);
if (!canCreate) { if (!canCreate) {
@@ -48,9 +50,12 @@ export class CompaniesService {
/** /**
* قائمة الشركات التابعة للمكتب * قائمة الشركات التابعة للمكتب
*/ */
async findAll(tenantId: string): Promise<Company[]> { async findAll(user: any): Promise<Company[]> {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? { is_active: true } : { tenant_id: user.tenantId, is_active: true };
return this.companyRepository.find({ return this.companyRepository.find({
where: { tenant_id: tenantId, is_active: true }, where: filter,
order: { created_at: 'DESC' }, order: { created_at: 'DESC' },
}); });
} }
@@ -58,9 +63,12 @@ export class CompaniesService {
/** /**
* تفاصيل شركة محددة * تفاصيل شركة محددة
*/ */
async findOne(tenantId: string, id: string): Promise<Company> { async findOne(user: any, id: string): Promise<Company> {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? { id } : { id, tenant_id: user.tenantId };
const company = await this.companyRepository.findOne({ const company = await this.companyRepository.findOne({
where: { id, tenant_id: tenantId }, where: filter,
}); });
if (!company) throw new NotFoundException('Company not found'); if (!company) throw new NotFoundException('Company not found');
return company; return company;
@@ -69,8 +77,8 @@ export class CompaniesService {
/** /**
* تحديث بيانات شركة * تحديث بيانات شركة
*/ */
async update(tenantId: string, id: string, dto: any): Promise<Company> { async update(user: any, id: string, dto: any): Promise<Company> {
const company = await this.findOne(tenantId, id); const company = await this.findOne(user, id);
Object.assign(company, dto); Object.assign(company, dto);
return this.companyRepository.save(company); return this.companyRepository.save(company);
} }
@@ -79,12 +87,12 @@ export class CompaniesService {
* حفظ مفاتيح جو فوترة (مُشفرة) * حفظ مفاتيح جو فوترة (مُشفرة)
*/ */
async setJoFotaraCredentials( async setJoFotaraCredentials(
tenantId: string, user: any,
id: string, id: string,
clientId: string, clientId: string,
secretKey: string, secretKey: string,
): Promise<void> { ): Promise<void> {
const company = await this.findOne(tenantId, id); const company = await this.findOne(user, id);
company.jofotara_client_id_encrypted = this.encryptionService.encrypt(clientId); company.jofotara_client_id_encrypted = this.encryptionService.encrypt(clientId);
company.jofotara_secret_key_encrypted = this.encryptionService.encrypt(secretKey); company.jofotara_secret_key_encrypted = this.encryptionService.encrypt(secretKey);
@@ -95,9 +103,12 @@ export class CompaniesService {
/** /**
* الحصول على المفاتيح (مفكوك تشفيرها) — للاستخدام الداخلي فقط * الحصول على المفاتيح (مفكوك تشفيرها) — للاستخدام الداخلي فقط
*/ */
async getDecryptedCredentials(tenantId: string, id: string) { async getDecryptedCredentials(user: any, id: string) {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? { id } : { id, tenant_id: user.tenantId };
const company = await this.companyRepository.findOne({ const company = await this.companyRepository.findOne({
where: { id, tenant_id: tenantId }, where: filter,
select: ['jofotara_client_id_encrypted', 'jofotara_secret_key_encrypted'], select: ['jofotara_client_id_encrypted', 'jofotara_secret_key_encrypted'],
}); });

View File

@@ -10,16 +10,16 @@ export class DashboardController {
@Get('stats') @Get('stats')
async getStats(@CurrentUser() user: any) { async getStats(@CurrentUser() user: any) {
return this.dashboardService.getStats(user.tenantId); return this.dashboardService.getStats(user);
} }
@Get('multi-entity') @Get('multi-entity')
async getMultiEntityStats(@CurrentUser() user: any) { async getMultiEntityStats(@CurrentUser() user: any) {
return this.dashboardService.getMultiEntityStats(user.tenantId); return this.dashboardService.getMultiEntityStats(user);
} }
@Get('risk-invoices') @Get('risk-invoices')
async getRiskInvoices(@CurrentUser() user: any) { async getRiskInvoices(@CurrentUser() user: any) {
return this.dashboardService.getRiskInvoices(user.tenantId); return this.dashboardService.getRiskInvoices(user);
} }
} }

View File

@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm'; import { Repository, Between } from 'typeorm';
import { Invoice, InvoiceStatus } from '../invoices/entities/invoice.entity'; import { Invoice, InvoiceStatus } from '../invoices/entities/invoice.entity';
import { Company } from '../companies/entities/company.entity'; import { Company } from '../companies/entities/company.entity';
import { UserRole } from '../users/enums/role.enum';
@Injectable() @Injectable()
export class DashboardService { export class DashboardService {
@@ -13,23 +14,29 @@ export class DashboardService {
private companyRepository: Repository<Company>, private companyRepository: Repository<Company>,
) {} ) {}
async getStats(tenantId: string) { async getStats(user: any) {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? {} : { tenant_id: user.tenantId };
const totalInvoices = await this.invoiceRepository.count({ const totalInvoices = await this.invoiceRepository.count({
where: { tenant_id: tenantId }, where: filter,
}); });
const approvedInvoices = await this.invoiceRepository.count({ const approvedInvoices = await this.invoiceRepository.count({
where: { tenant_id: tenantId, status: InvoiceStatus.APPROVED }, where: { ...filter, status: InvoiceStatus.APPROVED },
}); });
// Using QueryBuilder for better control // Using QueryBuilder for better control
const statuses = await this.invoiceRepository const query = this.invoiceRepository
.createQueryBuilder('invoice') .createQueryBuilder('invoice')
.select('status') .select('status')
.addSelect('COUNT(*)', 'count') .addSelect('COUNT(*)', 'count');
.where('invoice.tenant_id = :tenantId', { tenantId })
.groupBy('status') if (!isSuperAdmin) {
.getRawMany(); query.where('invoice.tenant_id = :tenantId', { tenantId: user.tenantId });
}
const statuses = await query.groupBy('status').getRawMany();
const statusMap = statuses.reduce((acc, curr) => { const statusMap = statuses.reduce((acc, curr) => {
acc[curr.status] = parseInt(curr.count); acc[curr.status] = parseInt(curr.count);
@@ -37,12 +44,12 @@ export class DashboardService {
}, {}); }, {});
const companiesCount = await this.companyRepository.count({ const companiesCount = await this.companyRepository.count({
where: { tenant_id: tenantId }, where: filter,
}); });
// Calculate total tax (mock logic for now, should sum up tax fields) // Calculate total tax (mock logic for now, should sum up tax fields)
const invoices = await this.invoiceRepository.find({ const invoices = await this.invoiceRepository.find({
where: { tenant_id: tenantId, status: InvoiceStatus.APPROVED }, where: { ...filter, status: InvoiceStatus.APPROVED },
select: ['tax_amount'], select: ['tax_amount'],
}); });
@@ -50,7 +57,7 @@ export class DashboardService {
// Get recent activities (last 5 invoices) // Get recent activities (last 5 invoices)
const recentInvoices = await this.invoiceRepository.find({ const recentInvoices = await this.invoiceRepository.find({
where: { tenant_id: tenantId }, where: filter,
order: { created_at: 'DESC' }, order: { created_at: 'DESC' },
take: 5, take: 5,
relations: ['company'], relations: ['company'],
@@ -81,9 +88,12 @@ export class DashboardService {
* جلب إحصائيات مجمعة لكل الشركات (Elite Accountant View) * جلب إحصائيات مجمعة لكل الشركات (Elite Accountant View)
* Get summarized stats for all companies under a tenant * Get summarized stats for all companies under a tenant
*/ */
async getMultiEntityStats(tenantId: string) { async getMultiEntityStats(user: any) {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? {} : { tenant_id: user.tenantId };
const companies = await this.companyRepository.find({ const companies = await this.companyRepository.find({
where: { tenant_id: tenantId }, where: filter,
}); });
const companyStats = await Promise.all( const companyStats = await Promise.all(
@@ -133,11 +143,14 @@ export class DashboardService {
/** /**
* جلب الفواتير التي بها مخاطر (فشل التحقق أو مرفوضة) عبر كافة الشركات * جلب الفواتير التي بها مخاطر (فشل التحقق أو مرفوضة) عبر كافة الشركات
*/ */
async getRiskInvoices(tenantId: string) { async getRiskInvoices(user: any) {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? {} : { tenant_id: user.tenantId };
return this.invoiceRepository.find({ return this.invoiceRepository.find({
where: [ where: [
{ tenant_id: tenantId, status: InvoiceStatus.VALIDATION_FAILED }, { ...filter, status: InvoiceStatus.VALIDATION_FAILED },
{ tenant_id: tenantId, status: InvoiceStatus.REJECTED }, { ...filter, status: InvoiceStatus.REJECTED },
], ],
relations: ['company'], relations: ['company'],
order: { updated_at: 'DESC' }, order: { updated_at: 'DESC' },

View File

@@ -40,7 +40,7 @@ export class InvoicesController {
*/ */
@Get() @Get()
async findAllByTenant(@CurrentUser() user: any) { async findAllByTenant(@CurrentUser() user: any) {
return this.invoicesService.findAllByTenant(user.tenantId); return this.invoicesService.findAllByTenant(user);
} }
/** /**
@@ -48,14 +48,14 @@ export class InvoicesController {
* Upload an invoice file for a specific company * Upload an invoice file for a specific company
*/ */
@Post('upload/:companyId') @Post('upload/:companyId')
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT) @Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
async upload( async upload(
@CurrentUser() user: any, @CurrentUser() user: any,
@Param('companyId', ParseUUIDPipe) companyId: string, @Param('companyId', ParseUUIDPipe) companyId: string,
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
) { ) {
return this.invoicesService.upload(user.tenantId, companyId, file); return this.invoicesService.upload(user, companyId, file);
} }
/** /**
@@ -67,7 +67,7 @@ export class InvoicesController {
@CurrentUser() user: any, @CurrentUser() user: any,
@Param('companyId', ParseUUIDPipe) companyId: string, @Param('companyId', ParseUUIDPipe) companyId: string,
) { ) {
return this.invoicesService.findAll(user.tenantId, companyId); return this.invoicesService.findAll(user, companyId);
} }
/** /**
@@ -79,7 +79,7 @@ export class InvoicesController {
@CurrentUser() user: any, @CurrentUser() user: any,
@Param('id', ParseUUIDPipe) id: string, @Param('id', ParseUUIDPipe) id: string,
) { ) {
return this.invoicesService.findOne(user.tenantId, id); return this.invoicesService.findOne(user, id);
} }
/** /**
@@ -90,9 +90,9 @@ export class InvoicesController {
* Submit an invoice to the official JoFotara portal * Submit an invoice to the official JoFotara portal
*/ */
@Post(':id/submit') @Post(':id/submit')
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT) @Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
async submit(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) { async submit(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) {
return this.invoicesService.submitToJoFotara(user.tenantId, id); return this.invoicesService.submitToJoFotara(user, id);
} }
/** /**
@@ -100,9 +100,9 @@ export class InvoicesController {
* Permanently delete an invoice * Permanently delete an invoice
*/ */
@Post(':id/delete') // Using POST for delete to match frontend request style or use standard DELETE @Post(':id/delete') // Using POST for delete to match frontend request style or use standard DELETE
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT) @Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
async remove(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) { async remove(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) {
return this.invoicesService.remove(user.tenantId, id); return this.invoicesService.remove(user, id);
} }
/** /**
@@ -115,11 +115,11 @@ export class InvoicesController {
@Param('id', ParseUUIDPipe) id: string, @Param('id', ParseUUIDPipe) id: string,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
) { ) {
const streamableFile = await this.invoicesService.getFile(user.tenantId, id); const streamableFile = await this.invoicesService.getFile(user, id);
// We need to determine the content type to ensure it opens inline in the browser (iframe) // We need to determine the content type to ensure it opens inline in the browser (iframe)
// We can fetch the invoice details first to get the extension. // We can fetch the invoice details first to get the extension.
const invoice = await this.invoicesService.findOne(user.tenantId, id); const invoice = await this.invoicesService.findOne(user, id);
let mimeType = 'application/pdf'; // Default fallback let mimeType = 'application/pdf'; // Default fallback
if (invoice.original_file_path) { if (invoice.original_file_path) {
const ext = invoice.original_file_path.split('.').pop()?.toLowerCase(); const ext = invoice.original_file_path.split('.').pop()?.toLowerCase();

View File

@@ -25,6 +25,7 @@ import * as path from 'path';
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull'; import { Queue } from 'bull';
import { Invoice, InvoiceStatus } from './entities/invoice.entity'; import { Invoice, InvoiceStatus } from './entities/invoice.entity';
import { UserRole } from '../users/enums/role.enum';
import { LocalStorageService } from '../../services/storage/local-storage.service'; import { LocalStorageService } from '../../services/storage/local-storage.service';
import { SubscriptionsService } from '../subscriptions/subscription.service'; import { SubscriptionsService } from '../subscriptions/subscription.service';
import { UBLGeneratorService } from './ubl-generator.service'; import { UBLGeneratorService } from './ubl-generator.service';
@@ -52,10 +53,11 @@ export class InvoicesService {
* Upload a new invoice and initiate extraction/validation * Upload a new invoice and initiate extraction/validation
*/ */
async upload( async upload(
tenantId: string, user: any,
companyId: string, companyId: string,
file: Express.Multer.File, file: Express.Multer.File,
): Promise<Invoice> { ): Promise<Invoice> {
const tenantId = user.tenantId;
const canUpload = await this.subscriptionsService.checkInvoiceLimit(tenantId); const canUpload = await this.subscriptionsService.checkInvoiceLimit(tenantId);
if (!canUpload) { if (!canUpload) {
throw new ForbiddenException('Monthly invoice limit reached for your current plan'); throw new ForbiddenException('Monthly invoice limit reached for your current plan');
@@ -95,9 +97,12 @@ export class InvoicesService {
* قائمة الفواتير لشركة محددة * قائمة الفواتير لشركة محددة
* Find all invoices for a specific company * Find all invoices for a specific company
*/ */
async findAll(tenantId: string, companyId: string): Promise<Invoice[]> { async findAll(user: any, companyId: string): Promise<Invoice[]> {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? { company_id: companyId } : { tenant_id: user.tenantId, company_id: companyId };
return this.invoiceRepository.find({ return this.invoiceRepository.find({
where: { tenant_id: tenantId, company_id: companyId }, where: filter,
order: { created_at: 'DESC' }, order: { created_at: 'DESC' },
}); });
} }
@@ -106,9 +111,12 @@ export class InvoicesService {
* قائمة جميع الفواتير للمكتب (المستأجر) * قائمة جميع الفواتير للمكتب (المستأجر)
* Find all invoices for the entire tenant (accounting office) * Find all invoices for the entire tenant (accounting office)
*/ */
async findAllByTenant(tenantId: string): Promise<Invoice[]> { async findAllByTenant(user: any): Promise<Invoice[]> {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? {} : { tenant_id: user.tenantId };
return this.invoiceRepository.find({ return this.invoiceRepository.find({
where: { tenant_id: tenantId }, where: filter,
relations: ['company'], relations: ['company'],
order: { created_at: 'DESC' }, order: { created_at: 'DESC' },
}); });
@@ -118,9 +126,12 @@ export class InvoicesService {
* تفاصيل فاتورة محددة مع بنودها * تفاصيل فاتورة محددة مع بنودها
* Get detailed information for a specific invoice including its lines * Get detailed information for a specific invoice including its lines
*/ */
async findOne(tenantId: string, id: string): Promise<Invoice> { async findOne(user: any, id: string): Promise<Invoice> {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? { id } : { id, tenant_id: user.tenantId };
const invoice = await this.invoiceRepository.findOne({ const invoice = await this.invoiceRepository.findOne({
where: { id, tenant_id: tenantId }, where: filter,
relations: ['lines'], relations: ['lines'],
}); });
if (!invoice) throw new NotFoundException('Invoice not found'); if (!invoice) throw new NotFoundException('Invoice not found');
@@ -131,8 +142,8 @@ export class InvoicesService {
* تحديث بيانات الفاتورة يدوياً * تحديث بيانات الفاتورة يدوياً
* Manually update invoice data (only if not already submitted) * Manually update invoice data (only if not already submitted)
*/ */
async update(tenantId: string, id: string, updateData: any): Promise<Invoice> { async update(user: any, id: string, updateData: any): Promise<Invoice> {
const invoice = await this.findOne(tenantId, id); const invoice = await this.findOne(user, id);
if (invoice.status === InvoiceStatus.APPROVED || invoice.status === InvoiceStatus.SUBMITTING) { if (invoice.status === InvoiceStatus.APPROVED || invoice.status === InvoiceStatus.SUBMITTING) {
throw new ForbiddenException('Cannot edit already approved or submitted invoice'); throw new ForbiddenException('Cannot edit already approved or submitted invoice');
@@ -146,15 +157,15 @@ export class InvoicesService {
* إرسال الفاتورة إلى بوابة جو فوترة الحكومية (ISTD) * إرسال الفاتورة إلى بوابة جو فوترة الحكومية (ISTD)
* Submit the validated invoice to the official JoFotara portal * Submit the validated invoice to the official JoFotara portal
*/ */
async submitToJoFotara(tenantId: string, id: string): Promise<any> { async submitToJoFotara(user: any, id: string): Promise<any> {
const invoice = await this.findOne(tenantId, id); const invoice = await this.findOne(user, id);
if (invoice.status !== InvoiceStatus.VALIDATED && invoice.status !== InvoiceStatus.VALIDATION_FAILED && invoice.status !== InvoiceStatus.EXTRACTED) { if (invoice.status !== InvoiceStatus.VALIDATED && invoice.status !== InvoiceStatus.VALIDATION_FAILED && invoice.status !== InvoiceStatus.EXTRACTED) {
throw new ForbiddenException('Invoice must be validated or extracted before submission'); throw new ForbiddenException('Invoice must be validated or extracted before submission');
} }
const company = await this.companiesService.findOne(tenantId, invoice.company_id); const company = await this.companiesService.findOne(user, invoice.company_id);
const credentials = await this.companiesService.getDecryptedCredentials(tenantId, invoice.company_id); const credentials = await this.companiesService.getDecryptedCredentials(user, invoice.company_id);
const xml = this.ublGenerator.generateXML(invoice, company); const xml = this.ublGenerator.generateXML(invoice, company);
@@ -182,8 +193,8 @@ export class InvoicesService {
* حذف الفاتورة والملف التابع لها نهائياً * حذف الفاتورة والملف التابع لها نهائياً
* Permanently delete an invoice and its associated file from storage * Permanently delete an invoice and its associated file from storage
*/ */
async remove(tenantId: string, id: string): Promise<void> { async remove(user: any, id: string): Promise<void> {
const invoice = await this.findOne(tenantId, id); const invoice = await this.findOne(user, id);
// 1. Delete file if exists // 1. Delete file if exists
if (invoice.original_file_path) { if (invoice.original_file_path) {
@@ -198,8 +209,8 @@ export class InvoicesService {
* الحصول على الملف كـ Stream لتحميله أو عرضه * الحصول على الملف كـ Stream لتحميله أو عرضه
* Get the original invoice file as a streamable file for download/view * Get the original invoice file as a streamable file for download/view
*/ */
async getFile(tenantId: string, id: string): Promise<StreamableFile> { async getFile(user: any, id: string): Promise<StreamableFile> {
const invoice = await this.findOne(tenantId, id); const invoice = await this.findOne(user, id);
if (!invoice.original_file_path) { if (!invoice.original_file_path) {
throw new NotFoundException('Invoice file not found'); throw new NotFoundException('Invoice file not found');
} }

View File

@@ -9,4 +9,5 @@ export enum UserRole {
ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها لكل الشركات) ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها لكل الشركات)
CLIENT = 'client', // عميل (قادر على رفع وإدارة فواتير شركته فقط) CLIENT = 'client', // عميل (قادر على رفع وإدارة فواتير شركته فقط)
VIEWER = 'viewer', // مشاهد (قادر على الاطلاع فقط) VIEWER = 'viewer', // مشاهد (قادر على الاطلاع فقط)
SUPER_ADMIN = 'super_admin', // مدير النظام العام (قادر على رؤية كل شيء)
} }

View File

@@ -26,25 +26,25 @@ export class UsersController {
constructor(private usersService: UsersService) {} constructor(private usersService: UsersService) {}
@Post() @Post()
@Roles(UserRole.ADMIN) @Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async create(@CurrentUser() user: any, @Body() dto: any) { async create(@CurrentUser() user: any, @Body() dto: any) {
return this.usersService.create(user.tenantId, dto); return this.usersService.create(user, dto);
} }
@Get() @Get()
async findAll(@CurrentUser() user: any) { async findAll(@CurrentUser() user: any) {
return this.usersService.findAll(user.tenantId); return this.usersService.findAll(user);
} }
@Get(':id') @Get(':id')
async findOne(@CurrentUser() user: any, @Param('id') id: string) { async findOne(@CurrentUser() user: any, @Param('id') id: string) {
return this.usersService.findOne(user.tenantId, id); return this.usersService.findOne(user, id);
} }
@Delete(':id') @Delete(':id')
@Roles(UserRole.ADMIN) @Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async remove(@CurrentUser() user: any, @Param('id') id: string) { async remove(@CurrentUser() user: any, @Param('id') id: string) {
return this.usersService.remove(user.tenantId, id, user.id); return this.usersService.remove(user, id, user.id);
} }
@Post('profile') @Post('profile')

View File

@@ -9,6 +9,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { User } from './entities/user.entity'; import { User } from './entities/user.entity';
import { UserRole } from './enums/role.enum';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
@@ -20,7 +21,8 @@ export class UsersService {
/** /**
* إضافة مستخدم لمكتب محاسبة * إضافة مستخدم لمكتب محاسبة
*/ */
async create(tenantId: string, dto: any): Promise<User> { async create(user: any, dto: any): Promise<User> {
const tenantId = user.tenantId;
const normalizedEmail = dto.email?.trim().toLowerCase(); const normalizedEmail = dto.email?.trim().toLowerCase();
const existing = await this.userRepository.findOne({ const existing = await this.userRepository.findOne({
where: { email: normalizedEmail, tenant_id: tenantId }, where: { email: normalizedEmail, tenant_id: tenantId },
@@ -32,22 +34,25 @@ export class UsersService {
const passwordHash = await bcrypt.hash(dto.password, 12); const passwordHash = await bcrypt.hash(dto.password, 12);
const user = this.userRepository.create({ const newUser = this.userRepository.create({
...dto, ...dto,
email: normalizedEmail, email: normalizedEmail,
password_hash: passwordHash, password_hash: passwordHash,
tenant_id: tenantId, tenant_id: tenantId,
} as Partial<User>); } as Partial<User>);
return this.userRepository.save(user); return this.userRepository.save(newUser);
} }
/** /**
* قائمة مستخدمي المكتب * قائمة مستخدمي المكتب
*/ */
async findAll(tenantId: string): Promise<User[]> { async findAll(user: any): Promise<User[]> {
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
const filter = isSuperAdmin ? { is_active: true } : { tenant_id: user.tenantId, is_active: true };
return this.userRepository.find({ return this.userRepository.find({
where: { tenant_id: tenantId, is_active: true }, where: filter,
order: { created_at: 'ASC' }, order: { created_at: 'ASC' },
}); });
} }
@@ -55,22 +60,25 @@ export class UsersService {
/** /**
* تفاصيل مستخدم * تفاصيل مستخدم
*/ */
async findOne(tenantId: string, id: string): Promise<User> { async findOne(user: any, id: string): Promise<User> {
const user = await this.userRepository.findOne({ const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
where: { id, tenant_id: tenantId }, const filter = isSuperAdmin ? { id } : { id, tenant_id: user.tenantId };
const foundUser = await this.userRepository.findOne({
where: filter,
}); });
if (!user) throw new NotFoundException('User not found'); if (!foundUser) throw new NotFoundException('User not found');
return user; return foundUser;
} }
/** /**
* تعطيل مستخدم * تعطيل مستخدم
*/ */
async remove(tenantId: string, id: string, currentUserId: string): Promise<void> { async remove(user: any, id: string, currentUserId: string): Promise<void> {
if (id === currentUserId) { if (id === currentUserId) {
throw new ConflictException('لا يمكنك تعطيل حسابك الشخصي'); throw new ConflictException('لا يمكنك تعطيل حسابك الشخصي');
} }
const user = await this.findOne(tenantId, id); await this.findOne(user, id);
await this.userRepository.update(id, { is_active: false }); await this.userRepository.update(id, { is_active: false });
} }