This commit is contained in:
Estifo77
2026-08-08 09:05:06 +03:00
parent 203d2e5026
commit a92580c7be

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState } from 'react';
import {
Alert,
Anchor,
@@ -11,56 +11,29 @@ import {
Text,
TextInput,
Title,
} from "@mantine/core";
} from '@mantine/core';
import {
IconArrowRight,
IconDeviceMobile,
IconLock,
IconMail,
} from "@tabler/icons-react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useNavigate, Link } from "react-router-dom";
import { useDispatch } from "react-redux";
import { useApiMutation } from "@ema-platform/api";
import { notify, useErrorHandler } from "@ema-platform/ui";
import { AuthShell } from "../components/AuthShell";
import { loginSuccess, setUser, setCurrentProfile } from "../store/auth.slice";
import type {
LoginPayload,
AuthUser,
CurrentProfile,
} from "../types/auth.types";
import { useAuthConfig } from "../AuthConfig";
import { authStorage } from "../utils/auth-storage";
const emailOrPhone = z
.string()
.trim()
.transform((value) => {
// Convert 09xxxxxxxx -> +2519xxxxxxxx
if (/^09\d{8}$/.test(value)) {
return `+251${value.substring(1)}`;
}
} from '@tabler/icons-react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNavigate, Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
import { loginSuccess, setUser, setCurrentProfile } from '../store/auth.slice';
import type { LoginPayload, AuthUser, CurrentProfile } from '../types/auth.types';
import { useAuthConfig } from '../AuthConfig';
import { authStorage } from '../utils/auth-storage';
return value;
})
.refine(
(value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const phoneRegex = /^\+2519\d{8}$/;
return emailRegex.test(value) || phoneRegex.test(value);
},
{
message: "Enter a valid email or phone number (+2519xxxxxxxx)",
},
);
const schema = z.object({
email: emailOrPhone,
password: z
.string()
.min(8, { message: "Password must be at least 8 characters" }),
email: z.string().email({ message: 'Enter a valid email' }),
password: z.string().min(5, { message: 'Password must be at least 6 characters' }),
});
type FormValues = z.infer<typeof schema>;
@@ -73,7 +46,9 @@ export function LoginPage() {
const [isLoading, setIsLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [serverError, setServerError] = useState<string | null>(null);
const { handleError } = useErrorHandler();
// MFA second step: set once /auth/login answers `mfaRequired` (US-IAM-006).
const [mfaEmail, setMfaEmail] = useState<string | null>(null);
const [mfaOtp, setMfaOtp] = useState('');
const [loginTrigger] = useApiMutation<LoginPayload>();
const [meTrigger] = useApiMutation<AuthUser>();
const [profileTrigger] = useApiMutation<{ profile: CurrentProfile | null }>();
@@ -90,8 +65,8 @@ export function LoginPage() {
setIsLoading(true);
try {
const data = await loginTrigger({
url: "/auth/login",
method: "POST",
url: '/auth/login',
method: 'POST',
body: values,
}).unwrap();
@@ -139,8 +114,8 @@ export function LoginPage() {
dispatch(loginSuccess(data));
const me = await meTrigger({
url: "/auth/me",
method: "GET",
url: '/auth/me',
method: 'GET',
}).unwrap();
dispatch(setUser(me));
@@ -151,8 +126,8 @@ export function LoginPage() {
// `useCurrentProfile` resolves it again on demand.
try {
const { profile } = await profileTrigger({
url: "/profiles/me",
method: "GET",
url: '/profiles/me',
method: 'GET',
}).unwrap();
if (profile) {
authStorage.setProfileId(profile.id);
@@ -162,23 +137,17 @@ export function LoginPage() {
// Offline or a 5xx — the portal still works; the resolver retries.
}
if (!me.isPhoneNumberVerified) {
navigate("/otp-verify", {
state: {
email: me.email,
phoneNumber: me.phoneNumber,
},
});
return;
}
navigate(loginRedirectPath);
} catch (err: unknown) {
setServerError(handleError(err));
} finally {
localStorage.setItem("rememberMe", String(rememberMe));
setIsLoading(false);
if (!me.isPhoneNumberVerified) {
navigate('/otp-verify', {
state: {
email: me.email,
phoneNumber: me.phoneNumber,
},
});
return;
}
navigate(loginRedirectPath);
};
return (
@@ -194,12 +163,7 @@ export function LoginPage() {
</div>
{serverError && (
<Alert
variant="light"
color="red"
withCloseButton
onClose={() => setServerError(null)}
>
<Alert variant="light" color="red" withCloseButton onClose={() => setServerError(null)}>
{serverError}
</Alert>
)}
@@ -248,7 +212,7 @@ export function LoginPage() {
size="md"
leftSection={<IconMail size={18} />}
error={errors.email?.message}
{...register("email")}
{...register('email')}
/>
<PasswordInput
label="Password"
@@ -256,7 +220,7 @@ export function LoginPage() {
size="md"
leftSection={<IconLock size={18} />}
error={errors.password?.message}
{...register("password")}
{...register('password')}
/>
<Group justify="space-between">
@@ -289,9 +253,23 @@ export function LoginPage() {
</Button>
</Stack>
</form>
)}
<Divider label="or" labelPosition="center" />
<Button
variant="default"
fullWidth
size="md"
leftSection={<IconDeviceMobile size={18} />}
onClick={() => notify.info('Phone sign-in is coming soon.')}
>
Sign in with phone number
</Button>
{enableSignup && (
<Text ta="center" size="sm" c="dimmed">
Don&apos;t have an account?{" "}
Don&apos;t have an account?{' '}
<Anchor component={Link} to="/signup" fw={700}>
Create one
</Anchor>
@@ -300,4 +278,4 @@ export function LoginPage() {
</Stack>
</AuthShell>
);
}
}