From 8321c697ec9afc1912a9991a60b35e7f7787a3df Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Fri, 10 Jul 2026 13:38:53 +0300 Subject: [PATCH] feat( auth ): make email optional in signup/login --- .../src/modules/auth/auth.dto.ts | 19 +++++++--- .../modules/auth/passenger-auth.service.ts | 7 +++- .../portal/src/app/login/page.tsx | 38 +++++++++++++------ .../portal/src/app/register/page.tsx | 17 +++++++-- 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts index d0a691b0f..d7d80df94 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts @@ -1,4 +1,4 @@ -import { IsEmail, IsString, ValidateNested } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; @@ -13,8 +13,13 @@ export class NameDto { } export class RegisterDto { - @ApiProperty({ example: 'kelemu@email.com' }) - @IsEmail() + // Accepts an email OR a phone number. When a passenger signs up without an email, + // the portal passes the phone number here (and as `username`) — the IAM only requires + // a non-empty string, so a phone value is a valid account identifier. Kept as + // @IsString/@IsNotEmpty (not @IsEmail) so that phone-as-email passes the ValidationPipe. + @ApiProperty({ example: 'kelemu@email.com', description: 'Email or, when the user has no email, their phone number' }) + @IsString() + @IsNotEmpty() email: string; @ApiProperty({ example: 'kelemu.ketsela' }) @@ -42,8 +47,12 @@ export class ResendRegistrationCodeDto { } export class LoginDto { - @ApiProperty({ example: 'kelemu@email.com' }) - @IsEmail() + // Accepts an email OR a phone number in the same field. Passengers who registered + // without an email log in with their phone number, which the IAM matches. Kept as + // @IsString/@IsNotEmpty (not @IsEmail) so a phone value passes the ValidationPipe. + @ApiProperty({ example: 'kelemu@email.com', description: 'Email or phone number' }) + @IsString() + @IsNotEmpty() email: string; @ApiProperty({ example: 'password123', format: 'password' }) diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index 2d20b4503..9c962477b 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -184,8 +184,11 @@ export class PassengerAuthService { const { token, refreshToken } = iamResult as { token: string; refreshToken: string }; + // `dto.email` may hold an email OR a phone number (passengers without an email log in + // with their phone). Match on either so the post-auth lookup works regardless of which + // identifier was used. const iamRows = await this.dataSource.query( - `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE email = $1 LIMIT 1`, + `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE email = $1 OR phone_number = $1 LIMIT 1`, [dto.email], ); const iamUser = iamRows[0]; @@ -210,7 +213,7 @@ export class PassengerAuthService { return { token, refreshToken, - user: { id: iamUser.id, iamUserId: iamUser.id, email: dto.email, passengerId: passenger.id }, + user: { id: iamUser.id, iamUserId: iamUser.id, email: iamUser.email, passengerId: passenger.id }, }; } diff --git a/apps/edr-passenger-web/portal/src/app/login/page.tsx b/apps/edr-passenger-web/portal/src/app/login/page.tsx index 9453c39e2..97a595244 100644 --- a/apps/edr-passenger-web/portal/src/app/login/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/login/page.tsx @@ -7,10 +7,13 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { useAuthStore } from '@/lib/auth-store'; import { useState, Suspense } from 'react'; import Link from 'next/link'; -import { Train, ShieldCheck } from 'lucide-react'; +import { Train, ShieldCheck, Eye, EyeOff } from 'lucide-react'; const loginSchema = z.object({ - email: z.string().email('Invalid email address'), + // Accepts either an email or a phone number. Passengers who registered without an + // email sign in with their phone number, which is sent in the same `email` field — + // the IAM matches on either identifier. + email: z.string().min(1, 'Phone or email is required'), password: z.string().min(6, 'Password must be at least 6 characters'), }); @@ -22,6 +25,7 @@ function LoginContent() { const login = useAuthStore((s) => s.login); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(loginSchema as any), @@ -63,12 +67,13 @@ function LoginContent() { )}
- + {errors.email && (

{errors.email.message}

@@ -77,12 +82,23 @@ function LoginContent() {
- +
+ + +
{errors.password && (

{errors.password.message}

)} diff --git a/apps/edr-passenger-web/portal/src/app/register/page.tsx b/apps/edr-passenger-web/portal/src/app/register/page.tsx index a39810be1..f020d0f31 100644 --- a/apps/edr-passenger-web/portal/src/app/register/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/register/page.tsx @@ -11,7 +11,13 @@ import { Train, ShieldCheck } from 'lucide-react'; const registerSchema = z.object({ fullName: z.string().min(2, 'Full name is required'), - email: z.string().email('Invalid email address'), + // Email is optional. If provided it must be a valid address; if left blank we fall back + // to the phone number as the account identifier (see onSubmit). + email: z + .string() + .email('Invalid email address') + .optional() + .or(z.literal('')), phone: z.string().min(9, 'Phone number is required'), }); @@ -31,9 +37,12 @@ export default function RegisterPage() { setLoading(true); setError(''); try { + // No email? Use the phone number as the account identifier. The IAM (and our + // relaxed RegisterDto) accept any non-empty string in the email field. + const email = data.email?.trim() ? data.email.trim() : data.phone; const result = await registerUser({ fullName: data.fullName, - email: data.email, + email, phone: data.phone, }); const params = new URLSearchParams({ @@ -89,7 +98,9 @@ export default function RegisterPage() {
- +