🚀 Feature: Implement Global Super Admin access & bypass tenant filtering
This commit is contained in:
@@ -27,43 +27,43 @@ export class CompaniesController {
|
||||
constructor(private companiesService: CompaniesService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
|
||||
async create(@CurrentUser() user: any, @Body() dto: any) {
|
||||
return this.companiesService.create(user.tenantId, dto);
|
||||
return this.companiesService.create(user, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async findAll(@CurrentUser() user: any) {
|
||||
return this.companiesService.findAll(user.tenantId);
|
||||
return this.companiesService.findAll(user);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@CurrentUser() user: any, @Param('id') id: string) {
|
||||
return this.companiesService.findOne(user.tenantId, id);
|
||||
return this.companiesService.findOne(user, id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
|
||||
async update(
|
||||
@CurrentUser() user: any,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: any,
|
||||
) {
|
||||
return this.companiesService.update(user.tenantId, id, dto);
|
||||
return this.companiesService.update(user, id, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* ربط بيانات جو فوترة المشفرة
|
||||
*/
|
||||
@Put(':id/jofotara')
|
||||
@Roles(UserRole.ADMIN)
|
||||
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
|
||||
async setJoFotara(
|
||||
@CurrentUser() user: any,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: { clientId: string; secretKey: string },
|
||||
) {
|
||||
return this.companiesService.setJoFotaraCredentials(
|
||||
user.tenantId,
|
||||
user,
|
||||
id,
|
||||
dto.clientId,
|
||||
dto.secretKey,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Company } from './entities/company.entity';
|
||||
import { UserRole } from '../users/enums/role.enum';
|
||||
import { EncryptionService } from '../../services/encryption/encryption.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
|
||||
const canCreate = await this.subscriptionsService.checkCompanyLimit(tenantId);
|
||||
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({
|
||||
where: { tenant_id: tenantId, is_active: true },
|
||||
where: filter,
|
||||
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({
|
||||
where: { id, tenant_id: tenantId },
|
||||
where: filter,
|
||||
});
|
||||
if (!company) throw new NotFoundException('Company not found');
|
||||
return company;
|
||||
@@ -69,8 +77,8 @@ export class CompaniesService {
|
||||
/**
|
||||
* تحديث بيانات شركة
|
||||
*/
|
||||
async update(tenantId: string, id: string, dto: any): Promise<Company> {
|
||||
const company = await this.findOne(tenantId, id);
|
||||
async update(user: any, id: string, dto: any): Promise<Company> {
|
||||
const company = await this.findOne(user, id);
|
||||
Object.assign(company, dto);
|
||||
return this.companyRepository.save(company);
|
||||
}
|
||||
@@ -79,12 +87,12 @@ export class CompaniesService {
|
||||
* حفظ مفاتيح جو فوترة (مُشفرة)
|
||||
*/
|
||||
async setJoFotaraCredentials(
|
||||
tenantId: string,
|
||||
user: any,
|
||||
id: string,
|
||||
clientId: string,
|
||||
secretKey: string,
|
||||
): 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_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({
|
||||
where: { id, tenant_id: tenantId },
|
||||
where: filter,
|
||||
select: ['jofotara_client_id_encrypted', 'jofotara_secret_key_encrypted'],
|
||||
});
|
||||
|
||||
|
||||
@@ -10,16 +10,16 @@ export class DashboardController {
|
||||
|
||||
@Get('stats')
|
||||
async getStats(@CurrentUser() user: any) {
|
||||
return this.dashboardService.getStats(user.tenantId);
|
||||
return this.dashboardService.getStats(user);
|
||||
}
|
||||
|
||||
@Get('multi-entity')
|
||||
async getMultiEntityStats(@CurrentUser() user: any) {
|
||||
return this.dashboardService.getMultiEntityStats(user.tenantId);
|
||||
return this.dashboardService.getMultiEntityStats(user);
|
||||
}
|
||||
|
||||
@Get('risk-invoices')
|
||||
async getRiskInvoices(@CurrentUser() user: any) {
|
||||
return this.dashboardService.getRiskInvoices(user.tenantId);
|
||||
return this.dashboardService.getRiskInvoices(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Between } from 'typeorm';
|
||||
import { Invoice, InvoiceStatus } from '../invoices/entities/invoice.entity';
|
||||
import { Company } from '../companies/entities/company.entity';
|
||||
import { UserRole } from '../users/enums/role.enum';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
@@ -13,23 +14,29 @@ export class DashboardService {
|
||||
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({
|
||||
where: { tenant_id: tenantId },
|
||||
where: filter,
|
||||
});
|
||||
|
||||
const approvedInvoices = await this.invoiceRepository.count({
|
||||
where: { tenant_id: tenantId, status: InvoiceStatus.APPROVED },
|
||||
where: { ...filter, status: InvoiceStatus.APPROVED },
|
||||
});
|
||||
|
||||
// Using QueryBuilder for better control
|
||||
const statuses = await this.invoiceRepository
|
||||
const query = this.invoiceRepository
|
||||
.createQueryBuilder('invoice')
|
||||
.select('status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('invoice.tenant_id = :tenantId', { tenantId })
|
||||
.groupBy('status')
|
||||
.getRawMany();
|
||||
.addSelect('COUNT(*)', 'count');
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
query.where('invoice.tenant_id = :tenantId', { tenantId: user.tenantId });
|
||||
}
|
||||
|
||||
const statuses = await query.groupBy('status').getRawMany();
|
||||
|
||||
const statusMap = statuses.reduce((acc, curr) => {
|
||||
acc[curr.status] = parseInt(curr.count);
|
||||
@@ -37,12 +44,12 @@ export class DashboardService {
|
||||
}, {});
|
||||
|
||||
const companiesCount = await this.companyRepository.count({
|
||||
where: { tenant_id: tenantId },
|
||||
where: filter,
|
||||
});
|
||||
|
||||
// Calculate total tax (mock logic for now, should sum up tax fields)
|
||||
const invoices = await this.invoiceRepository.find({
|
||||
where: { tenant_id: tenantId, status: InvoiceStatus.APPROVED },
|
||||
where: { ...filter, status: InvoiceStatus.APPROVED },
|
||||
select: ['tax_amount'],
|
||||
});
|
||||
|
||||
@@ -50,7 +57,7 @@ export class DashboardService {
|
||||
|
||||
// Get recent activities (last 5 invoices)
|
||||
const recentInvoices = await this.invoiceRepository.find({
|
||||
where: { tenant_id: tenantId },
|
||||
where: filter,
|
||||
order: { created_at: 'DESC' },
|
||||
take: 5,
|
||||
relations: ['company'],
|
||||
@@ -81,9 +88,12 @@ export class DashboardService {
|
||||
* جلب إحصائيات مجمعة لكل الشركات (Elite Accountant View)
|
||||
* 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({
|
||||
where: { tenant_id: tenantId },
|
||||
where: filter,
|
||||
});
|
||||
|
||||
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({
|
||||
where: [
|
||||
{ tenant_id: tenantId, status: InvoiceStatus.VALIDATION_FAILED },
|
||||
{ tenant_id: tenantId, status: InvoiceStatus.REJECTED },
|
||||
{ ...filter, status: InvoiceStatus.VALIDATION_FAILED },
|
||||
{ ...filter, status: InvoiceStatus.REJECTED },
|
||||
],
|
||||
relations: ['company'],
|
||||
order: { updated_at: 'DESC' },
|
||||
|
||||
@@ -40,7 +40,7 @@ export class InvoicesController {
|
||||
*/
|
||||
@Get()
|
||||
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
|
||||
*/
|
||||
@Post('upload/:companyId')
|
||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async upload(
|
||||
@CurrentUser() user: any,
|
||||
@Param('companyId', ParseUUIDPipe) companyId: string,
|
||||
@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,
|
||||
@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,
|
||||
@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
|
||||
*/
|
||||
@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) {
|
||||
return this.invoicesService.submitToJoFotara(user.tenantId, id);
|
||||
return this.invoicesService.submitToJoFotara(user, id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,9 +100,9 @@ export class InvoicesController {
|
||||
* Permanently delete an invoice
|
||||
*/
|
||||
@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) {
|
||||
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,
|
||||
@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 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
|
||||
if (invoice.original_file_path) {
|
||||
const ext = invoice.original_file_path.split('.').pop()?.toLowerCase();
|
||||
|
||||
@@ -25,6 +25,7 @@ import * as path from 'path';
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Queue } from 'bull';
|
||||
import { Invoice, InvoiceStatus } from './entities/invoice.entity';
|
||||
import { UserRole } from '../users/enums/role.enum';
|
||||
import { LocalStorageService } from '../../services/storage/local-storage.service';
|
||||
import { SubscriptionsService } from '../subscriptions/subscription.service';
|
||||
import { UBLGeneratorService } from './ubl-generator.service';
|
||||
@@ -52,10 +53,11 @@ export class InvoicesService {
|
||||
* Upload a new invoice and initiate extraction/validation
|
||||
*/
|
||||
async upload(
|
||||
tenantId: string,
|
||||
user: any,
|
||||
companyId: string,
|
||||
file: Express.Multer.File,
|
||||
): Promise<Invoice> {
|
||||
const tenantId = user.tenantId;
|
||||
const canUpload = await this.subscriptionsService.checkInvoiceLimit(tenantId);
|
||||
if (!canUpload) {
|
||||
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
|
||||
*/
|
||||
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({
|
||||
where: { tenant_id: tenantId, company_id: companyId },
|
||||
where: filter,
|
||||
order: { created_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
@@ -106,9 +111,12 @@ export class InvoicesService {
|
||||
* قائمة جميع الفواتير للمكتب (المستأجر)
|
||||
* 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({
|
||||
where: { tenant_id: tenantId },
|
||||
where: filter,
|
||||
relations: ['company'],
|
||||
order: { created_at: 'DESC' },
|
||||
});
|
||||
@@ -118,9 +126,12 @@ export class InvoicesService {
|
||||
* تفاصيل فاتورة محددة مع بنودها
|
||||
* 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({
|
||||
where: { id, tenant_id: tenantId },
|
||||
where: filter,
|
||||
relations: ['lines'],
|
||||
});
|
||||
if (!invoice) throw new NotFoundException('Invoice not found');
|
||||
@@ -131,8 +142,8 @@ export class InvoicesService {
|
||||
* تحديث بيانات الفاتورة يدوياً
|
||||
* Manually update invoice data (only if not already submitted)
|
||||
*/
|
||||
async update(tenantId: string, id: string, updateData: any): Promise<Invoice> {
|
||||
const invoice = await this.findOne(tenantId, id);
|
||||
async update(user: any, id: string, updateData: any): Promise<Invoice> {
|
||||
const invoice = await this.findOne(user, id);
|
||||
|
||||
if (invoice.status === InvoiceStatus.APPROVED || invoice.status === InvoiceStatus.SUBMITTING) {
|
||||
throw new ForbiddenException('Cannot edit already approved or submitted invoice');
|
||||
@@ -146,15 +157,15 @@ export class InvoicesService {
|
||||
* إرسال الفاتورة إلى بوابة جو فوترة الحكومية (ISTD)
|
||||
* Submit the validated invoice to the official JoFotara portal
|
||||
*/
|
||||
async submitToJoFotara(tenantId: string, id: string): Promise<any> {
|
||||
const invoice = await this.findOne(tenantId, id);
|
||||
async submitToJoFotara(user: any, id: string): Promise<any> {
|
||||
const invoice = await this.findOne(user, id);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const company = await this.companiesService.findOne(tenantId, invoice.company_id);
|
||||
const credentials = await this.companiesService.getDecryptedCredentials(tenantId, invoice.company_id);
|
||||
const company = await this.companiesService.findOne(user, invoice.company_id);
|
||||
const credentials = await this.companiesService.getDecryptedCredentials(user, invoice.company_id);
|
||||
|
||||
const xml = this.ublGenerator.generateXML(invoice, company);
|
||||
|
||||
@@ -182,8 +193,8 @@ export class InvoicesService {
|
||||
* حذف الفاتورة والملف التابع لها نهائياً
|
||||
* Permanently delete an invoice and its associated file from storage
|
||||
*/
|
||||
async remove(tenantId: string, id: string): Promise<void> {
|
||||
const invoice = await this.findOne(tenantId, id);
|
||||
async remove(user: any, id: string): Promise<void> {
|
||||
const invoice = await this.findOne(user, id);
|
||||
|
||||
// 1. Delete file if exists
|
||||
if (invoice.original_file_path) {
|
||||
@@ -198,8 +209,8 @@ export class InvoicesService {
|
||||
* الحصول على الملف كـ Stream لتحميله أو عرضه
|
||||
* Get the original invoice file as a streamable file for download/view
|
||||
*/
|
||||
async getFile(tenantId: string, id: string): Promise<StreamableFile> {
|
||||
const invoice = await this.findOne(tenantId, id);
|
||||
async getFile(user: any, id: string): Promise<StreamableFile> {
|
||||
const invoice = await this.findOne(user, id);
|
||||
if (!invoice.original_file_path) {
|
||||
throw new NotFoundException('Invoice file not found');
|
||||
}
|
||||
|
||||
@@ -9,4 +9,5 @@ export enum UserRole {
|
||||
ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها لكل الشركات)
|
||||
CLIENT = 'client', // عميل (قادر على رفع وإدارة فواتير شركته فقط)
|
||||
VIEWER = 'viewer', // مشاهد (قادر على الاطلاع فقط)
|
||||
SUPER_ADMIN = 'super_admin', // مدير النظام العام (قادر على رؤية كل شيء)
|
||||
}
|
||||
|
||||
@@ -26,25 +26,25 @@ export class UsersController {
|
||||
constructor(private usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(UserRole.ADMIN)
|
||||
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
|
||||
async create(@CurrentUser() user: any, @Body() dto: any) {
|
||||
return this.usersService.create(user.tenantId, dto);
|
||||
return this.usersService.create(user, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async findAll(@CurrentUser() user: any) {
|
||||
return this.usersService.findAll(user.tenantId);
|
||||
return this.usersService.findAll(user);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@CurrentUser() user: any, @Param('id') id: string) {
|
||||
return this.usersService.findOne(user.tenantId, id);
|
||||
return this.usersService.findOne(user, id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles(UserRole.ADMIN)
|
||||
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
|
||||
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')
|
||||
|
||||
@@ -9,6 +9,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserRole } from './enums/role.enum';
|
||||
|
||||
@Injectable()
|
||||
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 existing = await this.userRepository.findOne({
|
||||
where: { email: normalizedEmail, tenant_id: tenantId },
|
||||
@@ -32,22 +34,25 @@ export class UsersService {
|
||||
|
||||
const passwordHash = await bcrypt.hash(dto.password, 12);
|
||||
|
||||
const user = this.userRepository.create({
|
||||
const newUser = this.userRepository.create({
|
||||
...dto,
|
||||
email: normalizedEmail,
|
||||
password_hash: passwordHash,
|
||||
tenant_id: tenantId,
|
||||
} 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({
|
||||
where: { tenant_id: tenantId, is_active: true },
|
||||
where: filter,
|
||||
order: { created_at: 'ASC' },
|
||||
});
|
||||
}
|
||||
@@ -55,22 +60,25 @@ export class UsersService {
|
||||
/**
|
||||
* تفاصيل مستخدم
|
||||
*/
|
||||
async findOne(tenantId: string, id: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id, tenant_id: tenantId },
|
||||
async findOne(user: any, id: string): Promise<User> {
|
||||
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||
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');
|
||||
return user;
|
||||
if (!foundUser) throw new NotFoundException('User not found');
|
||||
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) {
|
||||
throw new ConflictException('لا يمكنك تعطيل حسابك الشخصي');
|
||||
}
|
||||
const user = await this.findOne(tenantId, id);
|
||||
await this.findOne(user, id);
|
||||
await this.userRepository.update(id, { is_active: false });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user