Commit 67c259bf authored by ThinhNC's avatar ThinhNC

feat(auth): support dual login with email or phone number and role population

parent 1277fb20
...@@ -80,6 +80,7 @@ File này chỉ lưu sự thật và quyết định dài hạn giúp các phiê ...@@ -80,6 +80,7 @@ File này chỉ lưu sự thật và quyết định dài hạn giúp các phiê
- QUY TẮC BẮT BUỘC: Tất cả tên quyền (Permission names) PHẢI được định nghĩa tập trung trong `src/common/constants/permission.constant.ts` (ở cả BE và FE), TUYỆT ĐỐI KHÔNG hardcode chuỗi string permission rải rác trong code. - QUY TẮC BẮT BUỘC: Tất cả tên quyền (Permission names) PHẢI được định nghĩa tập trung trong `src/common/constants/permission.constant.ts` (ở cả BE và FE), TUYỆT ĐỐI KHÔNG hardcode chuỗi string permission rải rác trong code.
- Mọi route endpoint nghiệp vụ ở backend bắt buộc được bảo vệ bằng middleware `requirePermission(PERMISSIONS.*)`. - Mọi route endpoint nghiệp vụ ở backend bắt buộc được bảo vệ bằng middleware `requirePermission(PERMISSIONS.*)`.
- Các vai trò hệ thống mặc định/bất biến (Bootstrap & System protection) được định nghĩa tập trung qua `SYSTEM_ROLES` trong `src/common/constants/system-role.constant.ts` (ví dụ `SYSTEM_ROLES.USER` cho vai trò đăng ký mặc định, `SYSTEM_ROLES.ADMIN` cho vai trò quản trị bất biến), không dùng `SYSTEM_ROLES` để kiểm tra phân quyền. - Các vai trò hệ thống mặc định/bất biến (Bootstrap & System protection) được định nghĩa tập trung qua `SYSTEM_ROLES` trong `src/common/constants/system-role.constant.ts` (ví dụ `SYSTEM_ROLES.USER` cho vai trò đăng ký mặc định, `SYSTEM_ROLES.ADMIN` cho vai trò quản trị bất biến), không dùng `SYSTEM_ROLES` để kiểm tra phân quyền.
- Endpoint đăng nhập `POST /api/v1/auth/login` hỗ trợ linh hoạt cả email và số điện thoại thông qua trường `email` hoặc `account`, tự động chuẩn hóa định dạng số điện thoại Việt Nam và truy vấn role đi kèm.
## Trạng thái đã biết ## Trạng thái đã biết
......
export interface LoginDto { export interface LoginDto {
email: string; email: string;
account?: string;
password: string; password: string;
} }
......
...@@ -161,6 +161,7 @@ export class AuthRepository { ...@@ -161,6 +161,7 @@ export class AuthRepository {
async findByPhone(phoneNumber: string) { async findByPhone(phoneNumber: string) {
return prisma.user.findFirst({ return prisma.user.findFirst({
where: { phoneNumber, deletedAt: null }, where: { phoneNumber, deletedAt: null },
include: { role: true },
}); });
} }
......
...@@ -24,8 +24,19 @@ export class AuthService { ...@@ -24,8 +24,19 @@ export class AuthService {
private readonly uploadService = new UploadService(); private readonly uploadService = new UploadService();
async login(data: LoginDto, metadata?: { userAgent?: string; ipAddress?: string }): Promise<LoginResponseDto> { async login(data: LoginDto, metadata?: { userAgent?: string; ipAddress?: string }): Promise<LoginResponseDto> {
const { email, password } = data; const rawIdentifier = (data.account || data.email || '').trim();
const user = await this.repository.findByEmail(email); const { password } = data;
let user;
if (rawIdentifier.includes('@')) {
user = await this.repository.findByEmail(rawIdentifier.toLowerCase());
} else {
const normalizedPhone = rawIdentifier.replace(/^\+84/, '0').replace(/^84/, '0');
user = await this.repository.findByPhone(normalizedPhone);
if (!user) {
user = await this.repository.findByEmail(rawIdentifier.toLowerCase());
}
}
if (!user) { if (!user) {
throw new AppError('Invalid credentials', 401, ERROR_CODE.INVALID_CREDENTIALS); throw new AppError('Invalid credentials', 401, ERROR_CODE.INVALID_CREDENTIALS);
......
import { z } from 'zod'; import { z } from 'zod';
import { validateUrl } from '../../common/helpers/url.helper'; import { validateUrl } from '../../common/helpers/url.helper';
export const loginSchema = z.object({ export const loginSchema = z
email: z .object({
.string() email: z.string().optional(),
.min(1, 'Email is required') account: z.string().optional(),
.email('Invalid email format') password: z.string().min(1, 'Password is required'),
.transform((val) => val.trim().toLowerCase()), })
password: z.string().min(1, 'Password is required'), .refine((data) => Boolean((data.email && data.email.trim().length > 0) || (data.account && data.account.trim().length > 0)), {
}); message: 'Email or account is required',
path: ['email'],
})
.transform((data) => ({
email: (data.email || data.account)!.trim(),
password: data.password,
}));
export const zaloLoginSchema = z export const zaloLoginSchema = z
.object({ .object({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment