/** * ════════════════════════════════════════════════════════════ * مُصادَق (Musadaq) — Authentication Service * ════════════════════════════════════════════════════════════ */ import { Injectable, UnauthorizedException, ConflictException, InternalServerErrorException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { DataSource } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { User } from '../users/entities/user.entity'; import { Tenant, TenantStatus } from '../tenants/entities/tenant.entity'; import { UserRole } from '../users/enums/role.enum'; import { RegisterDto } from './dto/register.dto'; import { LoginDto } from './dto/login.dto'; import { Subscription, SubscriptionPlan, SubscriptionStatus } from '../subscriptions/entities/subscription.entity'; @Injectable() export class AuthService { constructor( private jwtService: JwtService, private configService: ConfigService, private dataSource: DataSource, ) { } /** * تسجيل مستخدم جديد (مدير مكتب) */ async register(dto: RegisterDto) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // 1. Check if user exists const existingUser = await queryRunner.manager.findOne(User, { where: { email: dto.email }, }); if (existingUser) { throw new ConflictException('Email already registered'); } // 2. Create Tenant (Accounting Office) const tenant = queryRunner.manager.create(Tenant, { name: dto.tenantName, email: dto.email, // Use same email for office contact initially phone: dto.phone, status: TenantStatus.TRIAL, trial_ends_at: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14 days trial }); const savedTenant = await queryRunner.manager.save(tenant); // 3. Create Default Subscription (Trial) const subscription = queryRunner.manager.create(Subscription, { tenant_id: savedTenant.id, plan: SubscriptionPlan.BASIC, max_companies: 3, // -1 means unlimited max_invoices_per_month: 200, price_jod: 15, // Basic price status: SubscriptionStatus.ACTIVE, current_period_start: new Date(), current_period_end: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), }); await queryRunner.manager.save(subscription); // 4. Create Admin User const passwordHash = await this.hashPassword(dto.password); const user = queryRunner.manager.create(User, { tenant_id: savedTenant.id, name: dto.name, email: dto.email, password_hash: passwordHash, role: UserRole.ADMIN, }); const savedUser = await queryRunner.manager.save(user); await queryRunner.commitTransaction(); return { userId: savedUser.id, tenantId: savedTenant.id, message: 'Registration successful', }; } catch (error) { await queryRunner.rollbackTransaction(); if (error instanceof ConflictException) throw error; throw new InternalServerErrorException('Registration failed'); } finally { await queryRunner.release(); } } /** * تسجيل دخول */ async login(dto: LoginDto) { const normalizedEmail = dto.email.trim().toLowerCase(); const user = await this.dataSource.getRepository(User).findOne({ where: { email: normalizedEmail, is_active: true }, select: ['id', 'email', 'password_hash', 'tenant_id', 'role', 'name'], }); if (!user || !(await this.comparePassword(dto.password, user.password_hash))) { throw new UnauthorizedException('Invalid credentials'); } // ── Self-Healing: Upgrade old trial accounts to unlimited companies ── try { await this.dataSource.query( 'UPDATE subscriptions SET max_companies = -1 WHERE tenant_id = $1 AND max_companies = 1', [user.tenant_id], ); } catch (e) { console.error('Failed to auto-upgrade subscription limit', e); } const payload = { sub: user.id, tenantId: user.tenant_id, role: user.role }; const accessToken = await this.jwtService.signAsync(payload); const refreshToken = await this.generateRefreshToken(payload); // Save refresh token hash await this.updateRefreshToken(user.id, refreshToken); return { user: { id: user.id, name: user.name, email: user.email, role: user.role, tenantId: user.tenant_id, }, accessToken, refreshToken, }; } /** * تجديد التوكن */ async refresh(userId: string, refreshToken: string) { const user = await this.dataSource.getRepository(User).findOne({ where: { id: userId, is_active: true }, select: ['id', 'tenant_id', 'role', 'refresh_token_hash'], }); if (!user || !user.refresh_token_hash) { throw new UnauthorizedException('Access Denied'); } const isMatch = await bcrypt.compare(refreshToken, user.refresh_token_hash); if (!isMatch) { throw new UnauthorizedException('Access Denied'); } const payload = { sub: user.id, tenantId: user.tenant_id, role: user.role }; const accessToken = await this.jwtService.signAsync(payload); const newRefreshToken = await this.generateRefreshToken(payload); await this.updateRefreshToken(user.id, newRefreshToken); return { accessToken, refreshToken: newRefreshToken, }; } /** * الحصول على بيانات المستخدم والاشتراك الحالي */ async getMe(userId: string) { const user = await this.dataSource.getRepository(User).findOne({ where: { id: userId }, relations: ['tenant'], }); if (!user) throw new UnauthorizedException(); return { user: { id: user.id, name: user.name, email: user.email, role: user.role, tenantId: user.tenant_id, }, tenant: user.tenant, }; } /** * تسجيل خروج */ async logout(userId: string) { await this.dataSource.getRepository(User).update(userId, { refresh_token_hash: undefined, }); return { message: 'Logged out' }; } // ── Helpers ───────────────────────────────────────────── private async hashPassword(password: string): Promise { return bcrypt.hash(password, 12); } private async comparePassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } private async generateRefreshToken(payload: any): Promise { return this.jwtService.signAsync(payload, { secret: this.configService.getOrThrow('JWT_REFRESH_SECRET'), expiresIn: this.configService.get('JWT_REFRESH_EXPIRY', '7d'), }); } private async updateRefreshToken(userId: string, refreshToken: string) { const hash = await bcrypt.hash(refreshToken, 10); await this.dataSource.getRepository(User).update(userId, { refresh_token_hash: hash, }); } }