Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/auth/LoginPage.tsx
2026-07-04 07:17:05 +00:00

209 lines
5.8 KiB
TypeScript

import { type FormEvent, useState } from "react";
import {
Alert,
Box,
Button,
Center,
Group,
Image,
PasswordInput,
PinInput,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { AlertCircle, ArrowLeft } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "@/auth/useAuth";
import AuthShell from "@/components/auth/AuthShell";
import { extractApiError } from "@/utils/result";
/** Normalise Ethiopian local phone (09…/07…) to E.164; pass email through unchanged. */
const normaliseIdentifier = (raw: string): string => {
const v = raw.trim();
const digits = v.replace(/\D/g, "");
if (digits.length >= 9 && (v.startsWith("0") || v.startsWith("+251"))) {
const local = digits.startsWith("251")
? digits.slice(3)
: digits.replace(/^0/, "");
return `+251${local}`;
}
return v.toLowerCase();
};
const EDR_LOGO = "/assets/logo.svg";
const LoginPage = () => {
const navigate = useNavigate();
const { login, verifyMfa } = useAuth();
const [identifier, setIdentifier] = useState("");
const [password, setPassword] = useState("");
const [otp, setOtp] = useState("");
const [needsMfa, setNeedsMfa] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [normalizedIdentifier, setNormalizedIdentifier] = useState("");
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmitting(true);
setError(null);
try {
const normalized = normaliseIdentifier(identifier);
setNormalizedIdentifier(normalized);
const result = await login({ email: normalized, password });
if (result.mfaRequired) {
setNeedsMfa(true);
return;
}
// navigate("/dashboard/overview", { replace: true });
} catch (err) {
setError(extractApiError(err).message);
} finally {
setSubmitting(false);
}
};
const handleVerifyMfa = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmitting(true);
setError(null);
try {
await verifyMfa({ email: normalizedIdentifier, otp: otp.trim() });
navigate("/dashboard/overview", { replace: true });
} catch (err) {
setError(extractApiError(err).message);
} finally {
setSubmitting(false);
}
};
const loginForm = (
<Box component="form" onSubmit={handleSubmit}>
<Center mb={{ base: "md", sm: "lg" }}>
<Image src={EDR_LOGO} alt="EDR Freight" h={{ base: 36, sm: 44 }} w="auto" fit="contain" />
</Center>
<Stack gap={6} mb={{ base: "md", sm: "lg" }} ta="center">
<Title order={1} fz={{ base: "xl", sm: "26px" }} fw={700} lh={1.2}>
Welcome back!
</Title>
<Text size="sm" c="dimmed">
Log in to access the freight backoffice & explore all logistics
resources.
</Text>
</Stack>
<Stack gap="md">
<TextInput
label="Email or Phone"
placeholder="name@company.com or 09XXXXXXXX"
autoComplete="username"
required
disabled={submitting}
value={identifier}
onChange={(event) => setIdentifier(event.target.value)}
/>
<PasswordInput
label="Password"
placeholder="Enter your password"
required
disabled={submitting}
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
{error ? (
<Alert color="red" variant="light" icon={<AlertCircle size={18} />}>
{error}
</Alert>
) : null}
<Button type="submit" color="edr-green" fullWidth loading={submitting}>
Sign In
</Button>
</Stack>
</Box>
);
const mfaForm = (
<Box component="form" onSubmit={handleVerifyMfa}>
<Center mb={{ base: "md", sm: "lg" }}>
<Image src={EDR_LOGO} alt="EDR Freight" h={{ base: 36, sm: 44 }} w="auto" fit="contain" />
</Center>
<Stack gap={6} mb={{ base: "md", sm: "lg" }} ta="center">
<Title order={1} fz={{ base: "xl", sm: "26px" }} fw={700} lh={1.2}>
Multi-factor verification
</Title>
<Text size="sm" c="dimmed">
We sent a verification code to{" "}
<Text component="span" fw={500} c="var(--mantine-color-text)">
{normalizedIdentifier}
</Text>
. Enter it below to complete sign in.
</Text>
</Stack>
<Stack gap="md">
<Stack gap={6} align="center">
<Text size="sm" fw={500} c="edr-text">
Verification code
</Text>
<PinInput
length={6}
type="number"
oneTimeCode
value={otp}
placeholder="0"
disabled={submitting}
styles={{ input: { textAlign: "center" } }}
onChange={setOtp}
/>
</Stack>
{error ? (
<Alert color="red" variant="light" icon={<AlertCircle size={18} />}>
{error}
</Alert>
) : null}
<Group grow>
<Button
type="button"
variant="default"
leftSection={<ArrowLeft size={14} />}
disabled={submitting}
onClick={() => {
setNeedsMfa(false);
setOtp("");
setError(null);
}}
>
Back
</Button>
<Button
type="submit"
color="edr-green"
loading={submitting}
disabled={otp.trim().length !== 6}
>
Verify
</Button>
</Group>
</Stack>
</Box>
);
return <AuthShell>{!needsMfa ? loginForm : mfaForm}</AuthShell>;
};
export default LoginPage;