mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 08:32:54 +00:00
Merge pull request #407 from Tria-plc/passenger/feat/iam
feat: ( iam ) add portal auth registration, forgot/change password, …
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import FaydaSetupWizard from '@/components/FaydaSetupWizard';
|
||||
|
||||
export default function FaydaSetupPage() {
|
||||
return <FaydaSetupWizard />;
|
||||
}
|
||||
104
apps/edr-passenger-web/portal/src/app/forgot-password/page.tsx
Normal file
104
apps/edr-passenger-web/portal/src/app/forgot-password/page.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Train, MailCheck, ArrowLeft } from 'lucide-react';
|
||||
import { iamAuthApi } from '@/lib/api/auth';
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await iamAuthApi.forgotPassword(email);
|
||||
setSent(true);
|
||||
} catch (err: any) {
|
||||
const msg = err.response?.data?.message || err.message || '';
|
||||
setError(
|
||||
msg === 'user_not_found'
|
||||
? 'No account found with that email address.'
|
||||
: msg || 'Failed to send the reset link. Please try again.'
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-[rgb(20_113_76)] from-10% via-transparent to-[rgb(20_113_76)] to-90% dark:from-gray-900 dark:to-gray-800 flex items-center justify-center py-12 px-4">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="w-12 h-12 bg-[rgb(20_113_76)] rounded-lg flex items-center justify-center">
|
||||
<Train className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Reset your password</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||||
Enter your email and we'll send a reset link to the phone number on your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
{sent ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 px-4 py-3 rounded">
|
||||
<MailCheck className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm">
|
||||
A password reset link has been sent via SMS. Open it to set a new password — the link expires in 30 minutes.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/login" className="btn-primary w-full flex items-center justify-center gap-2">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to sign in
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => { setEmail(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
placeholder="your@email.com"
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn-primary w-full" disabled={loading || !email}>
|
||||
{loading ? 'Sending...' : 'Send reset link'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<Link
|
||||
href="/login"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to sign in
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import { z } from 'zod';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { useState, Suspense } from 'react';
|
||||
import { Train } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Train, ShieldCheck } from 'lucide-react';
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
@@ -85,6 +86,14 @@ function LoginContent() {
|
||||
{errors.password && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
|
||||
)}
|
||||
<div className="flex justify-end mt-1">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-sm text-[rgb(20_113_76)] dark:text-emerald-400 hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn-primary w-full" disabled={loading}>
|
||||
@@ -92,7 +101,23 @@ function LoginContent() {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700 space-y-3">
|
||||
<div className="text-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Don't have an account? </span>
|
||||
<Link href="/register" className="text-sm font-medium text-[rgb(20_113_76)] dark:text-emerald-400 hover:underline">
|
||||
Create account
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
href="/fayda-setup"
|
||||
className="flex items-center justify-center gap-2 text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
Already verified with Fayda? Set up your password
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
onClick={() => router.push('/booking/search')}
|
||||
className="text-sm text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary-400"
|
||||
|
||||
176
apps/edr-passenger-web/portal/src/app/register/page.tsx
Normal file
176
apps/edr-passenger-web/portal/src/app/register/page.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
'use client';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { useState } from 'react';
|
||||
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'),
|
||||
phone: z.string().min(9, 'Phone number is required'),
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
type RegisterForm = z.infer<typeof registerSchema>;
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
const registerUser = useAuthStore((s) => s.register);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { register, handleSubmit, formState: { errors } } = useForm<RegisterForm>({
|
||||
resolver: zodResolver(registerSchema as any),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: RegisterForm) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await registerUser({
|
||||
fullName: data.fullName,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
password: data.password,
|
||||
confirmPassword: data.confirmPassword,
|
||||
});
|
||||
router.push('/booking/search');
|
||||
} catch (err: any) {
|
||||
if (err.response?.status === 409) {
|
||||
setError('An account with this email or phone number already exists.');
|
||||
} else {
|
||||
setError(err.response?.data?.message || 'Registration failed. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-[rgb(20_113_76)] from-10% via-transparent to-[rgb(20_113_76)] to-90% dark:from-gray-900 dark:to-gray-800 flex items-center justify-center py-12 px-4">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="w-12 h-12 bg-[rgb(20_113_76)] rounded-lg flex items-center justify-center">
|
||||
<Train className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Create account</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-2">Book faster and manage your trips</p>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Full name</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('fullName')}
|
||||
className="input-field"
|
||||
placeholder="e.g. Abebe Kebede"
|
||||
autoComplete="name"
|
||||
/>
|
||||
{errors.fullName && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.fullName.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register('email')}
|
||||
className="input-field"
|
||||
placeholder="your@email.com"
|
||||
autoComplete="email"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone number</label>
|
||||
<input
|
||||
type="tel"
|
||||
{...register('phone')}
|
||||
className="input-field"
|
||||
placeholder="+251912345678"
|
||||
autoComplete="tel"
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.phone.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
{...register('password')}
|
||||
className="input-field"
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Confirm password</label>
|
||||
<input
|
||||
type="password"
|
||||
{...register('confirmPassword')}
|
||||
className="input-field"
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn-primary w-full" disabled={loading}>
|
||||
{loading ? 'Creating account...' : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Link
|
||||
href="/fayda-setup"
|
||||
className="flex items-center justify-center gap-2 w-full btn-secondary"
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
Already verified with Fayda? Set up your password
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Already have an account? </span>
|
||||
<Link href="/login" className="text-sm font-medium text-[rgb(20_113_76)] hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
154
apps/edr-passenger-web/portal/src/app/reset-password/page.tsx
Normal file
154
apps/edr-passenger-web/portal/src/app/reset-password/page.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Train, CheckCircle, ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { iamAuthApi } from '@/lib/api/auth';
|
||||
|
||||
function ResetPasswordContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const email = searchParams.get('email') || '';
|
||||
const userId = searchParams.get('userId') || '';
|
||||
const verificationCode = searchParams.get('verificationCode') || '';
|
||||
const linkValid = Boolean(email && userId && verificationCode);
|
||||
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (newPassword.length < 6) {
|
||||
setError('Password must be at least 6 characters.');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
await iamAuthApi.resetPassword({ userId, email, verificationCode, newPassword, confirmPassword });
|
||||
setSuccess(true);
|
||||
setTimeout(() => router.push('/login'), 2000);
|
||||
} catch (err: any) {
|
||||
const msg = err.response?.data?.message || err.message || '';
|
||||
setError(msg || 'Failed to reset password. The link may have expired — request a new one.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-[rgb(20_113_76)] from-10% via-transparent to-[rgb(20_113_76)] to-90% dark:from-gray-900 dark:to-gray-800 flex items-center justify-center py-12 px-4">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="w-12 h-12 bg-[rgb(20_113_76)] rounded-lg flex items-center justify-center">
|
||||
<Train className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Set a new password</h1>
|
||||
{linkValid && !success && (
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||||
Choose a new password for <span className="font-medium">{email}</span>.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
{!linkValid ? (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
||||
This password reset link is invalid or incomplete. Request a new one from the sign-in page.
|
||||
</div>
|
||||
<Link href="/forgot-password" className="btn-primary w-full flex items-center justify-center">
|
||||
Request a new link
|
||||
</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex items-center justify-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to sign in
|
||||
</Link>
|
||||
</div>
|
||||
) : success ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 px-4 py-3 rounded">
|
||||
<CheckCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm">Password reset successfully. Redirecting to sign in…</p>
|
||||
</div>
|
||||
<Link href="/login" className="btn-primary w-full flex items-center justify-center gap-2">
|
||||
Go to sign in
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">New password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => { setNewPassword(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Confirm new password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => { setConfirmPassword(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn-primary w-full" disabled={loading || !newPassword || !confirmPassword}>
|
||||
{loading ? 'Resetting...' : 'Reset password'}
|
||||
</button>
|
||||
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex items-center justify-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to sign in
|
||||
</Link>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ResetPasswordContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
24
apps/edr-passenger-web/portal/src/app/set-password/page.tsx
Normal file
24
apps/edr-passenger-web/portal/src/app/set-password/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import FaydaSetupWizard from '@/components/FaydaSetupWizard';
|
||||
|
||||
// Landing page for the IAM's Fayda set-password SMS link:
|
||||
// ${FE_BASE_URL}/set-password?email=..&userId=..&verificationCode=..
|
||||
// The wizard starts at step 2 with the code prefilled; the user enters their
|
||||
// phone number (verify-and-login requires it) and a new password.
|
||||
function SetPasswordContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const verificationCode = searchParams.get('verificationCode') || '';
|
||||
|
||||
return <FaydaSetupWizard initialOtp={verificationCode} />;
|
||||
}
|
||||
|
||||
export default function SetPasswordPage() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<SetPasswordContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { Menu, X, Moon, Sun, HelpCircle } from "lucide-react";
|
||||
import { Menu, X, Moon, Sun, HelpCircle, KeyRound, LogOut, ChevronDown } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuthStore } from "@/lib/auth-store";
|
||||
import ChangePasswordModal from "@/components/ChangePasswordModal";
|
||||
|
||||
export default function AppHeader() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
const { user, isAuthenticated, initialize, logout } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
const isDarkMode = document.documentElement.classList.contains("dark");
|
||||
setIsDark(isDarkMode);
|
||||
}, []);
|
||||
initialize();
|
||||
}, [initialize]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const html = document.documentElement;
|
||||
@@ -84,6 +90,67 @@ export default function AppHeader() {
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Auth */}
|
||||
{isAuthenticated && user ? (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||
className="flex items-center gap-2 p-1.5 hover:bg-white hover:bg-opacity-10 rounded-lg transition-colors"
|
||||
>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white text-[rgb(20_113_76)] font-semibold text-sm">
|
||||
{user.fullName?.toUpperCase().charAt(0) || 'U'}
|
||||
</div>
|
||||
<ChevronDown className="hidden sm:block w-4 h-4 text-gray-100" />
|
||||
</button>
|
||||
|
||||
{showUserMenu && (
|
||||
<div className="absolute right-0 top-full mt-2 w-56 z-50 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-xl">
|
||||
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">{user.fullName}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{user.email}</p>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUserMenu(false);
|
||||
setShowChangePassword(true);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
Change Password
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUserMenu(false);
|
||||
logout();
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-[rgb(20_113_76)] dark:text-emerald-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Link
|
||||
href="/login"
|
||||
className="px-3 py-1.5 text-sm font-medium text-gray-100 hover:bg-white hover:bg-opacity-10 rounded-lg transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
className="px-3 py-1.5 text-sm font-medium bg-white text-[rgb(20_113_76)] hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
@@ -115,10 +182,33 @@ export default function AppHeader() {
|
||||
>
|
||||
Help
|
||||
</Link>
|
||||
{!isAuthenticated && (
|
||||
<>
|
||||
<Link
|
||||
href="/login"
|
||||
className="block px-4 py-2 text-gray-100 dark:text-gray-300 hover:bg-white hover:bg-opacity-10 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
className="block px-4 py-2 text-gray-100 dark:text-gray-300 hover:bg-white hover:bg-opacity-10 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChangePasswordModal
|
||||
isOpen={showChangePassword}
|
||||
onClose={() => setShowChangePassword(false)}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, CheckCircle } from 'lucide-react';
|
||||
import { iamAuthApi } from '@/lib/api/auth';
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleClose = () => {
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setError('');
|
||||
setSuccess(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (newPassword.length < 6) {
|
||||
setError('New password must be at least 6 characters.');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError('New passwords do not match.');
|
||||
return;
|
||||
}
|
||||
if (newPassword === currentPassword) {
|
||||
setError('New password must be different from the current password.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
await iamAuthApi.changePassword({
|
||||
oldPassword: currentPassword,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
});
|
||||
setSuccess(true);
|
||||
setTimeout(() => handleClose(), 1500);
|
||||
} catch (err: any) {
|
||||
const msg = err.response?.data?.message || '';
|
||||
if (err.response?.status === 401) {
|
||||
setError('Current password is incorrect.');
|
||||
} else if (msg === 'new_password_same_as_old') {
|
||||
setError('New password must be different from the current password.');
|
||||
} else if (msg === 'new_passwords_do_not_match') {
|
||||
setError('New passwords do not match.');
|
||||
} else {
|
||||
setError(msg || 'Failed to change password. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Portal to <body> with a high z-index: the modal is mounted inside the
|
||||
// sticky z-50 header, whose stacking context would otherwise let page
|
||||
// content (e.g. the trip search widget) render on top of it.
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[100] p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl max-w-md w-full">
|
||||
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-6 py-4 flex items-center justify-between rounded-t-2xl">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Change Password</h3>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{success && (
|
||||
<div className="flex items-start gap-3 bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 px-4 py-3 rounded">
|
||||
<CheckCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm">Password changed successfully.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">Current Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => { setCurrentPassword(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => { setNewPassword(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => { setConfirmPassword(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="btn-secondary flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary flex-1"
|
||||
disabled={loading || success || !currentPassword || !newPassword || !confirmPassword}
|
||||
>
|
||||
{loading ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Train, ShieldCheck, CheckCircle, Info, ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { iamAuthApi } from '@/lib/api/auth';
|
||||
|
||||
interface FaydaSetupWizardProps {
|
||||
// Prefilled OTP when landing from the SMS link (/set-password?verificationCode=...)
|
||||
initialOtp?: string;
|
||||
}
|
||||
|
||||
type Outcome = 'success' | 'hasPassword' | null;
|
||||
|
||||
export default function FaydaSetupWizard({ initialOtp }: FaydaSetupWizardProps) {
|
||||
const router = useRouter();
|
||||
const [step, setStep] = useState<1 | 2>(initialOtp ? 2 : 1);
|
||||
const [phone, setPhone] = useState('');
|
||||
const [otp, setOtp] = useState(initialOtp || '');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [outcome, setOutcome] = useState<Outcome>(null);
|
||||
|
||||
const handleRequestCode = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await iamAuthApi.faydaRequestPasswordSetup(phone);
|
||||
setStep(2);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to send the code. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (newPassword.length < 6) {
|
||||
setError('Password must be at least 6 characters.');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await iamAuthApi.faydaVerifyAndLogin({ phoneNumber: phone, otp });
|
||||
const data = (res.data as any)?.data ?? res.data;
|
||||
if (!data.requiresPassword) {
|
||||
setOutcome('hasPassword');
|
||||
return;
|
||||
}
|
||||
await iamAuthApi.setFaydaPassword(
|
||||
{ userId: data.iamUserId, newPassword, confirmPassword },
|
||||
data.token,
|
||||
);
|
||||
setOutcome('success');
|
||||
setTimeout(() => router.push('/login'), 2500);
|
||||
} catch (err: any) {
|
||||
const msg = err.response?.data?.message || err.message || '';
|
||||
setError(msg || 'Verification failed. The code may be wrong or expired.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-[rgb(20_113_76)] from-10% via-transparent to-[rgb(20_113_76)] to-90% dark:from-gray-900 dark:to-gray-800 flex items-center justify-center py-12 px-4">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="w-12 h-12 bg-[rgb(20_113_76)] rounded-lg flex items-center justify-center">
|
||||
<Train className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 flex items-center justify-center gap-2">
|
||||
<ShieldCheck className="w-7 h-7 text-[rgb(20_113_76)] dark:text-emerald-400" />
|
||||
Fayda account setup
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||||
Already verified with Fayda? Set a password to access your account online.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
{outcome === 'success' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 px-4 py-3 rounded">
|
||||
<CheckCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm">
|
||||
Your password has been set and your account is now active. Redirecting to sign in…
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/login" className="btn-primary w-full flex items-center justify-center gap-2">
|
||||
Go to sign in
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
) : outcome === 'hasPassword' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300 px-4 py-3 rounded">
|
||||
<Info className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm">
|
||||
This account already has a password. Sign in with your email or phone number,
|
||||
or use forgot password if you can't remember it.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/login" className="btn-primary w-full flex items-center justify-center">
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="flex items-center justify-center text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
) : step === 1 ? (
|
||||
<form onSubmit={handleRequestCode} className="space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone number</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => { setPhone(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
placeholder="+251912345678"
|
||||
autoComplete="tel"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
The phone number you used during Fayda verification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn-primary w-full" disabled={loading || !phone}>
|
||||
{loading ? 'Sending...' : 'Send verification code'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<form onSubmit={handleSetPassword} className="space-y-4">
|
||||
<div className="flex items-start gap-3 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300 px-4 py-3 rounded">
|
||||
<Info className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm">
|
||||
If this phone number is Fayda-verified, an SMS with a verification code has been sent.
|
||||
Enter it below with your new password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone number</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => { setPhone(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
placeholder="+251912345678"
|
||||
autoComplete="tel"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Verification code</label>
|
||||
<input
|
||||
type="text"
|
||||
value={otp}
|
||||
onChange={(e) => { setOtp(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
placeholder="6-character code from SMS"
|
||||
maxLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">New password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => { setNewPassword(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Confirm new password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => { setConfirmPassword(e.target.value); setError(''); }}
|
||||
className="input-field"
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary w-full"
|
||||
disabled={loading || !phone || !otp || !newPassword || !confirmPassword}
|
||||
>
|
||||
{loading ? 'Verifying...' : 'Verify & set password'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setStep(1); setError(''); setOtp(''); }}
|
||||
className="w-full text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
Didn't get a code? Send again
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{outcome === null && (
|
||||
<div className="mt-6 text-center">
|
||||
<Link
|
||||
href="/login"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to sign in
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
apps/edr-passenger-web/portal/src/lib/api/auth.ts
Normal file
51
apps/edr-passenger-web/portal/src/lib/api/auth.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
// IAM (/v1/auth/*) and Fayda (/auth/fayda/*) endpoints use raw axios instead of
|
||||
// apiClient: apiClient's response interceptor clears the token and redirects to
|
||||
// /login on any 401 for non-public URLs — but the IAM returns 401 when the
|
||||
// current password is wrong on change-password, and OTP failures must surface
|
||||
// as inline errors, not a logout.
|
||||
export const iamAuthApi = {
|
||||
forgotPassword: (email: string) =>
|
||||
axios.post(`${API_URL}/v1/auth/forgot-password`, { email }),
|
||||
|
||||
// Completes the forgot-password flow using the link sent via SMS:
|
||||
// ${FE_BASE_URL}/reset-password?email=..&userId=..&verificationCode=..
|
||||
resetPassword: (data: {
|
||||
userId: string;
|
||||
email: string;
|
||||
verificationCode: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}) => axios.patch(`${API_URL}/v1/auth/set-password`, data),
|
||||
|
||||
changePassword: (data: {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}) =>
|
||||
axios.patch(`${API_URL}/v1/auth/change-password`, data, {
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem('auth_token')}` },
|
||||
}),
|
||||
|
||||
faydaRequestPasswordSetup: (phoneNumber: string) =>
|
||||
axios.post(`${API_URL}/auth/fayda/request-password-setup`, { phoneNumber }),
|
||||
|
||||
faydaVerifyAndLogin: (data: { phoneNumber: string; otp: string }) =>
|
||||
axios.post<{
|
||||
success: boolean;
|
||||
data: { token: string; refreshToken: string; requiresPassword: boolean; iamUserId: string };
|
||||
}>(`${API_URL}/auth/fayda/verify-and-login`, data),
|
||||
|
||||
// Bearer token comes from faydaVerifyAndLogin's response, not localStorage —
|
||||
// the user is not logged into the portal at this point.
|
||||
setFaydaPassword: (
|
||||
data: { userId: string; newPassword: string; confirmPassword: string },
|
||||
token: string,
|
||||
) =>
|
||||
axios.patch(`${API_URL}/v1/auth/set-fayda-password`, data, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
};
|
||||
@@ -40,10 +40,11 @@ interface AuthState {
|
||||
}
|
||||
|
||||
interface RegisterData {
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
fullName: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
@@ -118,7 +119,16 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
},
|
||||
|
||||
register: async (data: RegisterData) => {
|
||||
const response: any = await apiClient.post('/auth/register', data);
|
||||
// Shape required by the passenger-api RegisterDto; username = email by convention.
|
||||
const payload = {
|
||||
email: data.email,
|
||||
username: data.email,
|
||||
phoneNumber: data.phone,
|
||||
name: { en: data.fullName, am: data.fullName },
|
||||
password: data.password,
|
||||
confirmPassword: data.confirmPassword,
|
||||
};
|
||||
const response: any = await apiClient.post('/auth/register', payload);
|
||||
const { token, user } = response.data || response;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
Reference in New Issue
Block a user