95 lines
3.3 KiB
TypeScript
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 });
|
|
}
|
|
}
|