2026-03-06 09:05:31 -05:00

95 lines
3.3 KiB
TypeScript

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 });
}
}