From 448e64da027e0ce5170d935fc3b2f867f15a439f Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Thu, 2 Jul 2026 09:38:30 +0300 Subject: [PATCH 1/2] feat: ( iam ) add Station Master and Ticket Officer roles with positions --- .../src/seed/edr-passenger-org.seeder.ts | 83 ++++++++++++++++--- .../src/seed/edr-passenger.seed.ts | 16 +++- .../seed/passenger-permissions.registry.ts | 31 +++++-- 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/apps/edr-passenger-api/src/seed/edr-passenger-org.seeder.ts b/apps/edr-passenger-api/src/seed/edr-passenger-org.seeder.ts index 1bade05ae..5cc4e8878 100644 --- a/apps/edr-passenger-api/src/seed/edr-passenger-org.seeder.ts +++ b/apps/edr-passenger-api/src/seed/edr-passenger-org.seeder.ts @@ -42,6 +42,7 @@ export class EdrPassengerOrgSeeder { await this.ensureRoles(manager, EDR_PASSENGER_ROLES); await this.ensureRolePermissions(manager, EDR_PASSENGER_ROLES); await this.ensureSuperAdminPermissions(manager); + await this.ensurePositions(manager, organization.id); }); this.logger.log(`Ensured EDR passenger organization seed for '${EDR_ORG_KEY}'`); @@ -122,21 +123,51 @@ export class EdrPassengerOrgSeeder { const roleByKey = new Map(roles.map((r) => [r.key, r])); const permByKey = new Map(permissions.map((p) => [p.key, p])); - const links = seedRoles.flatMap((seedRole) => { + let totalUpserted = 0; + let totalPruned = 0; + + for (const seedRole of seedRoles) { const role = roleByKey.get(seedRole.key); if (!role) throw new Error(`missing_role:${seedRole.key}`); - return seedRole.permissionKeys.map((key) => { - const perm = permByKey.get(key); - if (!perm) throw new Error(`missing_permission:${key}`); - return { roleId: role.id, permissionId: perm.id }; - }); - }); + const desiredPermissionIds = new Set( + seedRole.permissionKeys.map((key) => { + const perm = permByKey.get(key); + if (!perm) throw new Error(`missing_permission:${key}`); + return perm.id as string; + }), + ); - await manager.getRepository(RolePermission).upsert(links, { - conflictPaths: { roleId: true, permissionId: true }, - }); - this.logger.log(`Ensured ${links.length} passenger role-permission links`); + // Remove links that are no longer in this role's preset + const existing = await manager.getRepository(RolePermission).find({ + where: { roleId: role.id as string }, + select: { permissionId: true }, + }); + const toRemove = existing + .map((rp) => rp.permissionId as string) + .filter((permId) => !desiredPermissionIds.has(permId)); + + if (toRemove.length > 0) { + await manager.getRepository(RolePermission).delete( + toRemove.map((permissionId) => ({ roleId: role.id as string, permissionId })), + ); + totalPruned += toRemove.length; + } + + // Upsert the full desired set + const links = [...desiredPermissionIds].map((permissionId) => ({ + roleId: role.id as string, + permissionId, + })); + await manager.getRepository(RolePermission).upsert(links, { + conflictPaths: { roleId: true, permissionId: true }, + }); + totalUpserted += links.length; + } + + this.logger.log( + `Synced passenger role-permission links: ${totalUpserted} upserted, ${totalPruned} pruned`, + ); } private async ensureSuperAdminPermissions(manager: EntityManager) { @@ -163,4 +194,34 @@ export class EdrPassengerOrgSeeder { ); this.logger.log(`Ensured ${permissions.length} passenger permissions on super_admin`); } + + private async ensurePositions(manager: EntityManager, organizationId: string) { + const positions = [ + { key: 'edr_passenger_director', name: { en: 'Director', am: 'ዳይሬክተር' }, rank: 1 }, + { key: 'edr_passenger_finance_manager', name: { en: 'Finance Manager', am: 'የፋይናንስ ሥራ አስኪያጅ' }, rank: 2 }, + { key: 'edr_passenger_finance_officer', name: { en: 'Finance Officer', am: 'የፋይናንስ ኦፊሰር' }, rank: 3 }, + { key: 'edr_passenger_team_leader', name: { en: 'Team Leader', am: 'ቡድን መሪ' }, rank: 4 }, + { key: 'edr_passenger_station_master', name: { en: 'Station Master', am: 'ጣቢያ ሃላፊ' }, rank: 5 }, + { key: 'edr_passenger_station_supervisor', name: { en: 'Station Supervisor', am: 'ጣቢያ ተቆጣጣሪ' }, rank: 6 }, + { key: 'edr_passenger_ticket_officer', name: { en: 'Passenger Ticket Officer', am: 'የተሳፋሪ ቲኬት ኦፊሰር' }, rank: 7 }, + { key: 'edr_passenger_operational_staff', name: { en: 'Operational Staff', am: 'ስራ ሰራተኛ' }, rank: 8 }, + ]; + + let created = 0; + for (const pos of positions) { + const existing = await manager.query>( + `SELECT id FROM iam.positions WHERE key = $1 AND organization_id = $2 LIMIT 1`, + [pos.key, organizationId], + ); + if (existing.length === 0) { + await manager.query( + `INSERT INTO iam.positions (id, name, key, rank, organization_id, unit_id, position_type_id, created_at, updated_at) + VALUES (gen_random_uuid(), $1::jsonb, $2, $3, $4::uuid, NULL, NULL, NOW(), NOW())`, + [JSON.stringify(pos.name), pos.key, pos.rank, organizationId], + ); + created++; + } + } + this.logger.log(`Ensured ${positions.length} passenger positions (${created} newly created)`); + } } diff --git a/apps/edr-passenger-api/src/seed/edr-passenger.seed.ts b/apps/edr-passenger-api/src/seed/edr-passenger.seed.ts index cd178139a..578cf66ff 100644 --- a/apps/edr-passenger-api/src/seed/edr-passenger.seed.ts +++ b/apps/edr-passenger-api/src/seed/edr-passenger.seed.ts @@ -30,15 +30,25 @@ export const EDR_PASSENGER_ROLES: PassengerSeedRole[] = [ permissionKeys: [...ROLE_PERMISSION_PRESETS.backofficeAdmin], }, { - key: 'edr_passenger_backoffice_staff', - name: { en: 'EDR Passenger Backoffice Staff' }, - permissionKeys: [...ROLE_PERMISSION_PRESETS.backofficeStaff], + key: 'edr_passenger_station_master', + name: { en: 'EDR Passenger Station Master' }, + permissionKeys: [...ROLE_PERMISSION_PRESETS.stationMaster], + }, + { + key: 'edr_passenger_ticket_officer', + name: { en: 'EDR Passenger Ticket Officer' }, + permissionKeys: [...ROLE_PERMISSION_PRESETS.ticketOfficer], }, { key: 'edr_passenger_agent', name: { en: 'EDR Passenger Agent' }, permissionKeys: [...ROLE_PERMISSION_PRESETS.agent], }, + { + key: 'edr_passenger_backoffice_staff', + name: { en: 'EDR Passenger Backoffice Staff' }, + permissionKeys: [...ROLE_PERMISSION_PRESETS.backofficeStaff], + }, { key: 'edr_passenger_finance', name: { en: 'EDR Passenger Finance' }, diff --git a/apps/edr-passenger-api/src/seed/passenger-permissions.registry.ts b/apps/edr-passenger-api/src/seed/passenger-permissions.registry.ts index d6a804b3a..bab7787b9 100644 --- a/apps/edr-passenger-api/src/seed/passenger-permissions.registry.ts +++ b/apps/edr-passenger-api/src/seed/passenger-permissions.registry.ts @@ -87,29 +87,42 @@ export const PASSENGER_PERMS = { export const ROLE_PERMISSION_PRESETS = { backofficeAdmin: [...PASSENGER_PERMISSION_KEYS], - backofficeStaff: [ + stationMaster: [ PASSENGER_PERMS.bookings.view, PASSENGER_PERMS.bookings.manage, - PASSENGER_PERMS.bookings.cancel, - PASSENGER_PERMS.passengers.view, - PASSENGER_PERMS.passengers.manage, PASSENGER_PERMS.tickets.view, PASSENGER_PERMS.tickets.manage, - PASSENGER_PERMS.payments.viewAll, - PASSENGER_PERMS.reports.view, - PASSENGER_PERMS.dashboard.view, - PASSENGER_PERMS.notifications.send, + PASSENGER_PERMS.passengers.view, PASSENGER_PERMS.agents.view, - PASSENGER_PERMS.fraud.view, PASSENGER_PERMS.audit.view, + PASSENGER_PERMS.notifications.send, + PASSENGER_PERMS.dashboard.view, + ], + + ticketOfficer: [ + PASSENGER_PERMS.tickets.view, + PASSENGER_PERMS.tickets.manage, + PASSENGER_PERMS.bookings.view, + PASSENGER_PERMS.passengers.view, + PASSENGER_PERMS.dashboard.view, ], agent: [ PASSENGER_PERMS.bookings.view, PASSENGER_PERMS.bookings.manage, + PASSENGER_PERMS.bookings.cancel, PASSENGER_PERMS.passengers.view, PASSENGER_PERMS.tickets.view, + PASSENGER_PERMS.tickets.manage, PASSENGER_PERMS.payments.refund, + PASSENGER_PERMS.dashboard.view, + ], + + backofficeStaff: [ + PASSENGER_PERMS.bookings.view, + PASSENGER_PERMS.passengers.view, + PASSENGER_PERMS.tickets.view, + PASSENGER_PERMS.dashboard.view, ], finance: [ From d6938df66cb403f0bd50ecba5090502c81972dae Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Thu, 2 Jul 2026 10:27:08 +0300 Subject: [PATCH 2/2] feat: ( iam ) add forgot and change password to passenger backoffice --- apps/edr-passenger-api/.env.example | 19 ++ .../backoffice/src/app/login/page.tsx | 154 +++++++++++- .../src/app/reset-password/page.tsx | 228 ++++++++++++++++++ .../components/layout/ChangePasswordModal.tsx | 141 +++++++++++ .../src/components/layout/Header.tsx | 19 +- .../backoffice/src/lib/api/auth.ts | 26 ++ .../backoffice/src/middleware.ts | 2 +- 7 files changed, 585 insertions(+), 4 deletions(-) create mode 100644 apps/edr-passenger-web/backoffice/src/app/reset-password/page.tsx create mode 100644 apps/edr-passenger-web/backoffice/src/components/layout/ChangePasswordModal.tsx create mode 100644 apps/edr-passenger-web/backoffice/src/lib/api/auth.ts diff --git a/apps/edr-passenger-api/.env.example b/apps/edr-passenger-api/.env.example index 328023c28..aaed09659 100644 --- a/apps/edr-passenger-api/.env.example +++ b/apps/edr-passenger-api/.env.example @@ -43,6 +43,13 @@ JWT_ACCESS_TOKEN_EXPIRES=1h JWT_REFRESH_TOKEN_SECRET= JWT_REFRESH_TOKEN_EXPIRES=7d +# @tria-plc IAM forgot-password flow — the reset link sent via SMS is +# ${FE_BASE_URL}/reset-password?email=..&userId=..&verificationCode=.. +# Point at the backoffice web app. Without it the link starts with "undefined/". +FE_BASE_URL=http://localhost:5184 +# OTP/reset-link TTL in minutes (IAM default: 30) +INVITATION_EXPIRY_DATE=30 + # SendGrid SENDGRID_API_KEY= SENDGRID_FROM_EMAIL=noreply@edr-platform.com @@ -151,7 +158,10 @@ FAYDA_TOKEN_ENDPOINT= FAYDA_USERINFO_ENDPOINT= # Base64 of the RSA private JWK (JSON). Secret — never commit a real value. FAYDA_PRIVATE_KEY_BASE64= +# OAuth redirect_uri passed to eSignet for MOBILE clients (the app calls /complete directly). FAYDA_REDIRECT_URI= +# OAuth redirect_uri passed to eSignet for WEB clients. Defaults to FAYDA_REDIRECT_URI when unset. +FAYDA_WEB_REDIRECT_URI= # Optional (defaults shown) FAYDA_SCOPE=openid profile email FAYDA_ACR_VALUES=mosip:idp:acr:generated-code @@ -160,6 +170,15 @@ FAYDA_SESSION_TTL_MINUTES=10 GITHUB_PACKAGE_TOKEN= +# --- Seeding ----------------------------------------------------------------- +# Set both to true on first run (or when resetting) to seed org, roles, and +# default backoffice staff users. Safe to leave true — all operations are idempotent. +# Login endpoint for backoffice users: POST /v1/auth/login +SEED_EDR_PASSENGER_ORG=false +SEED_PASSENGER_STAFF=false +# Plain-text password set on seeded staff accounts. Defaults to '12345678' if unset. +DEFAULT_PASSWORD=Admin@1234 + # --- Notification broker (RabbitMQ) ----------------------------------------------------------------- # Set RABBITMQ_ENABLED=false to skip connection entirely (dev without a local broker). RABBITMQ_ENABLED=false diff --git a/apps/edr-passenger-web/backoffice/src/app/login/page.tsx b/apps/edr-passenger-web/backoffice/src/app/login/page.tsx index b2c90d275..f104db3da 100644 --- a/apps/edr-passenger-web/backoffice/src/app/login/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/login/page.tsx @@ -5,9 +5,10 @@ import { useRouter } from 'next/navigation'; import { useAuthStore } from '@/lib/auth-store'; import { useTheme } from '@/lib/theme-store'; import { - Eye, EyeOff, Sun, Moon, ArrowRight, Loader2, - TicketCheck, Users, TrendingUp, ShieldCheck, + Eye, EyeOff, Sun, Moon, ArrowRight, ArrowLeft, Loader2, + TicketCheck, Users, TrendingUp, ShieldCheck, MailCheck, } from 'lucide-react'; +import { iamAuthApi } from '@/lib/api/auth'; const EDR_GREEN = 'rgb(20, 113, 76)'; @@ -28,6 +29,13 @@ export default function LoginPage() { const [emailFocused, setEmailFocused] = useState(false); const [passwordFocused, setPasswordFocused] = useState(false); + const [view, setView] = useState<'login' | 'forgot'>('login'); + const [forgotEmail, setForgotEmail] = useState(''); + const [forgotLoading, setForgotLoading] = useState(false); + const [forgotError, setForgotError] = useState(''); + const [forgotSent, setForgotSent] = useState(false); + const [forgotFocused, setForgotFocused] = useState(false); + const router = useRouter(); const { login } = useAuthStore(); const { isDark, toggleTheme } = useTheme(); @@ -53,6 +61,31 @@ export default function LoginPage() { } }; + const handleForgotSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setForgotLoading(true); + setForgotError(''); + try { + await iamAuthApi.forgotPassword(forgotEmail); + setForgotSent(true); + } catch (err: any) { + const msg = err.response?.data?.message || err.message || ''; + setForgotError( + msg === 'user_not_found' + ? 'No account found with that email address.' + : msg || 'Failed to send the reset link. Please try again.' + ); + } finally { + setForgotLoading(false); + } + }; + + const backToLogin = () => { + setView('login'); + setForgotError(''); + setForgotSent(false); + }; + if (!isMounted) return null; return ( @@ -94,6 +127,8 @@ export default function LoginPage() { {/* Form area */}
+ {view === 'login' ? ( + <> {/* Heading */}
@@ -171,6 +206,15 @@ export default function LoginPage() { {showPassword ? : }
+
+ +
{/* Submit */} @@ -206,6 +250,112 @@ export default function LoginPage() {

+ + ) : ( + <> + + {/* Heading */} +
+

+ Reset your password +

+

+ Enter your email address and we'll send a reset link to the phone number on your account. +

+
+ + {forgotSent ? ( +
+
+ +

+ A password reset link has been sent via SMS. Open it to set a new password — the link expires in 30 minutes. +

+
+ +
+ ) : ( + <> + {/* Error */} + {forgotError && (
+
+
+ ! +
+

{forgotError}

+
+ )} + +
+ {/* Email field */} +
+ +
+ { setForgotEmail(e.target.value); setForgotError(''); }} + onFocus={() => setForgotFocused(true)} + onBlur={() => setForgotFocused(false)} + className="w-full px-4 py-3 rounded-xl bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-600 text-sm focus:outline-none" + placeholder="name@edr.com" + required + autoComplete="email" + /> +
+
+ + {/* Submit */} + +
+ + + + )} + + )} diff --git a/apps/edr-passenger-web/backoffice/src/app/reset-password/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reset-password/page.tsx new file mode 100644 index 000000000..62e81322c --- /dev/null +++ b/apps/edr-passenger-web/backoffice/src/app/reset-password/page.tsx @@ -0,0 +1,228 @@ +'use client'; + +import { Suspense, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import Link from 'next/link'; +import { Eye, EyeOff, ArrowRight, ArrowLeft, Loader2, CheckCircle2 } from 'lucide-react'; +import { iamAuthApi } from '@/lib/api/auth'; + +function ResetPasswordForm() { + 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 [showPassword, setShowPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + const [newFocused, setNewFocused] = useState(false); + const [confirmFocused, setConfirmFocused] = 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 from the sign-in page.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + {/* Logo */} +
+
+ + + + + + +
+
+
ETHIO-DJIBOUTI
+
Railway
+
+
+ + {!linkValid ? ( +
+

+ Invalid reset link +

+

+ This password reset link is invalid or incomplete. Request a new one from the sign-in page. +

+ + + Back to sign in + +
+ ) : success ? ( +
+
+ +

+ Password reset successfully. Redirecting to sign in… +

+
+ + Go to sign in + + +
+ ) : ( + <> + {/* Heading */} +
+

+ Set a new password +

+

+ Choose a new password for {email}. +

+
+ + {/* Error */} + {error && ( +
+
+ ! +
+

{error}

+
+ )} + +
+ {/* New password */} +
+ +
+ { setNewPassword(e.target.value); setError(''); }} + onFocus={() => setNewFocused(true)} + onBlur={() => setNewFocused(false)} + className="w-full px-4 py-3 pr-11 rounded-xl bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-600 text-sm focus:outline-none" + placeholder="••••••••••" + required + minLength={6} + autoComplete="new-password" + /> + +
+
+ + {/* Confirm password */} +
+ +
+ { setConfirmPassword(e.target.value); setError(''); }} + onFocus={() => setConfirmFocused(true)} + onBlur={() => setConfirmFocused(false)} + className="w-full px-4 py-3 rounded-xl bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-600 text-sm focus:outline-none" + placeholder="••••••••••" + required + minLength={6} + autoComplete="new-password" + /> +
+
+ + {/* Submit */} + +
+ + + + Back to sign in + + + )} +
+
+ ); +} + +export default function ResetPasswordPage() { + return ( + + + + ); +} diff --git a/apps/edr-passenger-web/backoffice/src/components/layout/ChangePasswordModal.tsx b/apps/edr-passenger-web/backoffice/src/components/layout/ChangePasswordModal.tsx new file mode 100644 index 000000000..5641643fd --- /dev/null +++ b/apps/edr-passenger-web/backoffice/src/components/layout/ChangePasswordModal.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { CheckCircle2 } from 'lucide-react'; +import Modal from '@/components/ui/Modal'; +import ActionButton from '@/components/ui/ActionButton'; +import { iamAuthApi } from '@/lib/api/auth'; + +interface ChangePasswordModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) { + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + + const changePasswordMutation = useMutation({ + mutationFn: () => iamAuthApi.changePassword({ oldPassword, newPassword, confirmPassword }), + onSuccess: () => { + setSuccess(true); + setTimeout(() => handleClose(), 1500); + }, + onError: (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.'); + } + }, + }); + + const handleClose = () => { + setOldPassword(''); + setNewPassword(''); + setConfirmPassword(''); + setError(''); + setSuccess(false); + onClose(); + }; + + const handleSubmit = () => { + 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 === oldPassword) { + setError('New password must be different from the current password.'); + return; + } + changePasswordMutation.mutate(); + }; + + return ( + +
{ e.preventDefault(); handleSubmit(); }} + className="space-y-4" + > + {success && ( +
+ +

Password changed successfully.

+
+ )} + + {error && ( +
+

{error}

+
+ )} + +
+ + { setOldPassword(e.target.value); setError(''); }} + placeholder="Enter current password" + autoComplete="current-password" + required + /> +
+
+ + { setNewPassword(e.target.value); setError(''); }} + placeholder="Enter new password" + autoComplete="new-password" + minLength={6} + required + /> +
+
+ + { setConfirmPassword(e.target.value); setError(''); }} + placeholder="Re-enter new password" + autoComplete="new-password" + minLength={6} + required + /> +
+ +
+ + Cancel + + + Change Password + +
+
+
+ ); +} diff --git a/apps/edr-passenger-web/backoffice/src/components/layout/Header.tsx b/apps/edr-passenger-web/backoffice/src/components/layout/Header.tsx index 0e87eec43..f4547c69e 100644 --- a/apps/edr-passenger-web/backoffice/src/components/layout/Header.tsx +++ b/apps/edr-passenger-web/backoffice/src/components/layout/Header.tsx @@ -1,16 +1,18 @@ 'use client'; -import { Bell, LogOut, Moon, Sun, ChevronDown, HelpCircle } from 'lucide-react'; +import { Bell, LogOut, Moon, Sun, ChevronDown, HelpCircle, KeyRound } from 'lucide-react'; import { useAuthStore } from '@/lib/auth-store'; import { useTheme } from '@/lib/theme-store'; import { useState } from 'react'; import Link from 'next/link'; +import ChangePasswordModal from '@/components/layout/ChangePasswordModal'; export default function Header() { const { user, logout } = useAuthStore(); const { isDark, toggleTheme } = useTheme(); const [showUserMenu, setShowUserMenu] = useState(false); const [showNotifications, setShowNotifications] = useState(false); + const [showChangePassword, setShowChangePassword] = useState(false); return (
@@ -108,6 +110,16 @@ export default function Header() { > Settings +
); } diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/auth.ts b/apps/edr-passenger-web/backoffice/src/lib/api/auth.ts new file mode 100644 index 000000000..1fe1eccbb --- /dev/null +++ b/apps/edr-passenger-web/backoffice/src/lib/api/auth.ts @@ -0,0 +1,26 @@ +import axios from 'axios'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; + + +export const iamAuthApi = { + forgotPassword: (email: string) => + axios.post(`${API_URL}/v1/auth/forgot-password`, { email }), + + 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')}` }, + }), +}; diff --git a/apps/edr-passenger-web/backoffice/src/middleware.ts b/apps/edr-passenger-web/backoffice/src/middleware.ts index 0b437d112..7e8a1545d 100644 --- a/apps/edr-passenger-web/backoffice/src/middleware.ts +++ b/apps/edr-passenger-web/backoffice/src/middleware.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -const PUBLIC_PATHS = ['/login']; +const PUBLIC_PATHS = ['/login', '/reset-password']; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl;