import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { DatabaseService } from '../../database/database.service'; @Injectable() export class AuthService { constructor( private readonly db: DatabaseService, private readonly jwt: JwtService, ) {} async register(dto: { email?: string; phone?: string; password: string; firstName: string; lastName: string; role: 'customer' | 'driver' | 'restaurant_owner'; }) { if (!dto.email && !dto.phone) { throw new ConflictException('Email or phone is required'); } if (dto.email) { const existing = await this.db.queryOne('SELECT id FROM users WHERE email = $1', [dto.email.toLowerCase()]); if (existing) throw new ConflictException('Email already registered'); } if (dto.phone && !dto.email) { const existing = await this.db.queryOne('SELECT id FROM users WHERE phone = $1', [dto.phone]); if (existing) throw new ConflictException('Phone already registered'); } const passwordHash = await bcrypt.hash(dto.password, 10); const email = dto.email ? dto.email.toLowerCase() : null; const user = await this.db.queryOne( `INSERT INTO users (email, password_hash, first_name, last_name, phone, role) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, email, first_name, last_name, role`, [email, passwordHash, dto.firstName, dto.lastName, dto.phone || null, dto.role], ); // Create role-specific record if (dto.role === 'driver') { const driverResult = await this.db.query( `INSERT INTO drivers (user_id) VALUES ($1) RETURNING id`, [user.id], ); const driverId = driverResult.rows[0].id; await this.db.query( `INSERT INTO driver_locations (driver_id, location) VALUES ($1, ST_GeographyFromText('POINT(0 0)'))`, [driverId], ); } return { user, token: this.signToken(user) }; } async login(emailOrPhone: string, password: string) { const identifier = emailOrPhone.toLowerCase().trim(); const user = await this.db.queryOne( `SELECT id, email, password_hash, first_name, last_name, role, is_active FROM users WHERE LOWER(email) = $1 OR phone = $1`, [identifier], ); if (!user) throw new UnauthorizedException('Invalid credentials'); if (!user.is_active) throw new UnauthorizedException('Account suspended'); const valid = await bcrypt.compare(password, user.password_hash); if (!valid) throw new UnauthorizedException('Invalid credentials'); const { password_hash, ...safeUser } = user; return { user: safeUser, token: this.signToken(user) }; } async validateToken(payload: { sub: string; role: string }) { return this.db.queryOne( `SELECT u.id, u.email, u.first_name, u.last_name, u.role, u.is_active, r.id AS "restaurantId", d.id AS "driverId" FROM users u LEFT JOIN restaurants r ON r.owner_id = u.id AND u.role = 'restaurant_owner' LEFT JOIN drivers d ON d.user_id = u.id AND u.role = 'driver' WHERE u.id = $1 AND u.is_active = TRUE`, [payload.sub], ); } private signToken(user: { id: string; role: string }) { return this.jwt.sign({ sub: user.id, role: user.role }); } }