mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
refactor: standardize import quotes and improve cookie management in auth storage
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Anchor,
|
Anchor,
|
||||||
@@ -11,29 +11,35 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
IconDeviceMobile,
|
IconDeviceMobile,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconMail,
|
IconMail,
|
||||||
} from '@tabler/icons-react';
|
} from "@tabler/icons-react";
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from "react-router-dom";
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from "react-redux";
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from "@ema-platform/api";
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from "@ema-platform/ui";
|
||||||
import { AuthShell } from '../components/AuthShell';
|
import { AuthShell } from "../components/AuthShell";
|
||||||
import { loginSuccess, setUser, setCurrentProfile } from '../store/auth.slice';
|
import { loginSuccess, setUser, setCurrentProfile } from "../store/auth.slice";
|
||||||
import type { LoginPayload, AuthUser, CurrentProfile } from '../types/auth.types';
|
import type {
|
||||||
import { useAuthConfig } from '../AuthConfig';
|
LoginPayload,
|
||||||
import { authStorage } from '../utils/auth-storage';
|
AuthUser,
|
||||||
|
CurrentProfile,
|
||||||
|
} from "../types/auth.types";
|
||||||
|
import { useAuthConfig } from "../AuthConfig";
|
||||||
|
import { authStorage } from "../utils/auth-storage";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
email: z.string().email({ message: 'Enter a valid email' }),
|
email: z.string().email({ message: "Enter a valid email" }),
|
||||||
password: z.string().min(5, { message: 'Password must be at least 6 characters' }),
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(5, { message: "Password must be at least 6 characters" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
type FormValues = z.infer<typeof schema>;
|
||||||
@@ -48,7 +54,10 @@ export function LoginPage() {
|
|||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
const [loginTrigger] = useApiMutation<LoginPayload>();
|
const [loginTrigger] = useApiMutation<LoginPayload>();
|
||||||
const [meTrigger] = useApiMutation<AuthUser>();
|
const [meTrigger] = useApiMutation<AuthUser>();
|
||||||
const [profileCheckTrigger] = useApiMutation<{ total: number; items: CurrentProfile[] }>();
|
const [profileCheckTrigger] = useApiMutation<{
|
||||||
|
total: number;
|
||||||
|
items: CurrentProfile[];
|
||||||
|
}>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -62,15 +71,15 @@ export function LoginPage() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await loginTrigger({
|
const data = await loginTrigger({
|
||||||
url: '/auth/login',
|
url: "/auth/login",
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: values,
|
body: values,
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
dispatch(loginSuccess(data));
|
dispatch(loginSuccess(data));
|
||||||
|
|
||||||
const me = await meTrigger({
|
const me = await meTrigger({
|
||||||
url: '/auth/me',
|
url: "/auth/me",
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
dispatch(setUser(me));
|
dispatch(setUser(me));
|
||||||
|
|
||||||
@@ -79,7 +88,7 @@ export function LoginPage() {
|
|||||||
const q = `w=user_id:=:${me.id}&i=user,address,profession`;
|
const q = `w=user_id:=:${me.id}&i=user,address,profession`;
|
||||||
const result = await profileCheckTrigger({
|
const result = await profileCheckTrigger({
|
||||||
url: `/profiles?q=${encodeURIComponent(q)}`,
|
url: `/profiles?q=${encodeURIComponent(q)}`,
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
if (result.total > 0 && result.items.length > 0) {
|
if (result.total > 0 && result.items.length > 0) {
|
||||||
const profile = result.items[0];
|
const profile = result.items[0];
|
||||||
@@ -92,7 +101,7 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!me.isPhoneNumberVerified) {
|
if (!me.isPhoneNumberVerified) {
|
||||||
navigate('/otp-verify', {
|
navigate("/otp-verify", {
|
||||||
state: {
|
state: {
|
||||||
email: me.email,
|
email: me.email,
|
||||||
phoneNumber: me.phoneNumber,
|
phoneNumber: me.phoneNumber,
|
||||||
@@ -103,7 +112,7 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasProfile) {
|
if (!hasProfile) {
|
||||||
navigate('/profile-setup');
|
navigate("/profile-setup");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,10 +120,11 @@ export function LoginPage() {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(err as { data?: { message?: string } })?.data?.message ??
|
||||||
(err instanceof Error ? err.message : 'Something went wrong');
|
(err instanceof Error ? err.message : "Something went wrong");
|
||||||
setServerError(msg);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
|
localStorage.setItem("rememberMe", String(rememberMe));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -132,7 +142,12 @@ export function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{serverError && (
|
{serverError && (
|
||||||
<Alert variant="light" color="red" withCloseButton onClose={() => setServerError(null)}>
|
<Alert
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
withCloseButton
|
||||||
|
onClose={() => setServerError(null)}
|
||||||
|
>
|
||||||
{serverError}
|
{serverError}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -145,7 +160,7 @@ export function LoginPage() {
|
|||||||
size="md"
|
size="md"
|
||||||
leftSection={<IconMail size={18} />}
|
leftSection={<IconMail size={18} />}
|
||||||
error={errors.email?.message}
|
error={errors.email?.message}
|
||||||
{...register('email')}
|
{...register("email")}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label="Password"
|
||||||
@@ -153,7 +168,7 @@ export function LoginPage() {
|
|||||||
size="md"
|
size="md"
|
||||||
leftSection={<IconLock size={18} />}
|
leftSection={<IconLock size={18} />}
|
||||||
error={errors.password?.message}
|
error={errors.password?.message}
|
||||||
{...register('password')}
|
{...register("password")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
@@ -194,14 +209,14 @@ export function LoginPage() {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="md"
|
size="md"
|
||||||
leftSection={<IconDeviceMobile size={18} />}
|
leftSection={<IconDeviceMobile size={18} />}
|
||||||
onClick={() => notify.info('Phone sign-in is coming soon.')}
|
onClick={() => notify.info("Phone sign-in is coming soon.")}
|
||||||
>
|
>
|
||||||
Sign in with phone number
|
Sign in with phone number
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{enableSignup && (
|
{enableSignup && (
|
||||||
<Text ta="center" size="sm" c="dimmed">
|
<Text ta="center" size="sm" c="dimmed">
|
||||||
Don't have an account?{' '}
|
Don't have an account?{" "}
|
||||||
<Anchor component={Link} to="/signup" fw={700}>
|
<Anchor component={Link} to="/signup" fw={700}>
|
||||||
Create one
|
Create one
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
|
||||||
import type { AuthState, AuthUser, CurrentProfile, LoginPayload } from '../types/auth.types';
|
import type {
|
||||||
import { authStorage, clearAllCookies } from '../utils/auth-storage';
|
AuthState,
|
||||||
|
AuthUser,
|
||||||
|
CurrentProfile,
|
||||||
|
LoginPayload,
|
||||||
|
} from "../types/auth.types";
|
||||||
|
import { authStorage } from "../utils/auth-storage";
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
user: null,
|
user: null,
|
||||||
@@ -10,7 +15,7 @@ const initialState: AuthState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const authSlice = createSlice({
|
const authSlice = createSlice({
|
||||||
name: 'auth',
|
name: "auth",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
loginSuccess(state, action: PayloadAction<LoginPayload>) {
|
loginSuccess(state, action: PayloadAction<LoginPayload>) {
|
||||||
@@ -37,7 +42,7 @@ const authSlice = createSlice({
|
|||||||
state.isAuthenticated = false;
|
state.isAuthenticated = false;
|
||||||
state.currentProfile = null;
|
state.currentProfile = null;
|
||||||
authStorage.clear();
|
authStorage.clear();
|
||||||
clearAllCookies()
|
authStorage.clear();
|
||||||
},
|
},
|
||||||
hydrateAuth(state) {
|
hydrateAuth(state) {
|
||||||
const token = authStorage.getToken();
|
const token = authStorage.getToken();
|
||||||
@@ -55,5 +60,12 @@ const authSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { loginSuccess, setUser, setCurrentProfile, clearCurrentProfile, logout, hydrateAuth } = authSlice.actions;
|
export const {
|
||||||
|
loginSuccess,
|
||||||
|
setUser,
|
||||||
|
setCurrentProfile,
|
||||||
|
clearCurrentProfile,
|
||||||
|
logout,
|
||||||
|
hydrateAuth,
|
||||||
|
} = authSlice.actions;
|
||||||
export const authReducer = authSlice.reducer;
|
export const authReducer = authSlice.reducer;
|
||||||
|
|||||||
@@ -1,35 +1,45 @@
|
|||||||
import Cookies from 'js-cookie';
|
import Cookies from "js-cookie";
|
||||||
|
const rememberMe = localStorage.getItem("rememberMe") === "true";
|
||||||
const COOKIE_EXPIRES_DAYS = 7; // ponytail: cookie lifetime knob; JWT expiry + refresh govern real auth
|
const COOKIE_EXPIRES_DAYS = rememberMe ? 15 : 1;
|
||||||
|
|
||||||
interface Backend {
|
interface Backend {
|
||||||
get(k: string): string | null;
|
get(k: string): string | null;
|
||||||
set(k: string, v: string): void;
|
set(k: string, v: string): void;
|
||||||
remove(k: string): void;
|
remove(k: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localBackend: Backend = {
|
const localStore: Backend = {
|
||||||
get: (k) => localStorage.getItem(k),
|
get: (k) => localStorage.getItem(k),
|
||||||
set: (k, v) => localStorage.setItem(k, v),
|
set: (k, v) => localStorage.setItem(k, v),
|
||||||
remove: (k) => localStorage.removeItem(k),
|
remove: (k) => localStorage.removeItem(k),
|
||||||
};
|
};
|
||||||
|
|
||||||
const cookieBackend: Backend = {
|
const cookieStore: Backend = {
|
||||||
get: (k) => Cookies.get(k) ?? null,
|
get: (k) => Cookies.get(k) ?? null,
|
||||||
// ponytail: 4KB/cookie cap — auth-user/current-profile JSON must stay under it
|
|
||||||
set: (k, v) =>
|
set: (k, v) =>
|
||||||
Cookies.set(k, v, { path: '/', secure: true, sameSite: 'strict', expires: COOKIE_EXPIRES_DAYS }),
|
Cookies.set(k, v, {
|
||||||
remove: (k) => Cookies.remove(k, { path: '/' }),
|
path: "/",
|
||||||
|
secure: true,
|
||||||
|
sameSite: "strict",
|
||||||
|
expires: COOKIE_EXPIRES_DAYS,
|
||||||
|
}),
|
||||||
|
remove: (k) => Cookies.remove(k, { path: "/" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUTH_KEY_NAMES = ['auth-token', 'refresh-token', 'auth-user', 'profile-id', 'current-profile'];
|
const AUTH_KEY_NAMES = [
|
||||||
|
"auth-token",
|
||||||
|
"refresh-token",
|
||||||
|
"auth-user",
|
||||||
|
"profile-id",
|
||||||
|
"current-profile",
|
||||||
|
];
|
||||||
|
|
||||||
let _prefix = 'ema-auth';
|
let _prefix = "ema-auth";
|
||||||
let backend: Backend = localBackend;
|
let backend: Backend = localStore;
|
||||||
|
|
||||||
export function configureAuthStorage(prefix: string, useCookies = false) {
|
export function configureAuthStorage(prefix: string, useCookies = false) {
|
||||||
_prefix = prefix;
|
_prefix = prefix;
|
||||||
backend = useCookies ? cookieBackend : localBackend;
|
backend = useCookies ? cookieStore : localStore;
|
||||||
if (useCookies) {
|
if (useCookies) {
|
||||||
// one-time cleanup: drop pre-migration localStorage leftovers so tokens live in cookies only
|
// one-time cleanup: drop pre-migration localStorage leftovers so tokens live in cookies only
|
||||||
AUTH_KEY_NAMES.forEach((k) => localStorage.removeItem(`${prefix}-${k}`));
|
AUTH_KEY_NAMES.forEach((k) => localStorage.removeItem(`${prefix}-${k}`));
|
||||||
@@ -41,41 +51,40 @@ function key(k: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const authStorage = {
|
export const authStorage = {
|
||||||
getToken: () => backend.get(key('auth-token')) ?? undefined,
|
getToken: () => backend.get(key("auth-token")) ?? undefined,
|
||||||
setToken: (token: string) => backend.set(key('auth-token'), token),
|
setToken: (token: string) => backend.set(key("auth-token"), token),
|
||||||
getRefreshToken: () => backend.get(key('refresh-token')) ?? undefined,
|
getRefreshToken: () => backend.get(key("refresh-token")) ?? undefined,
|
||||||
setRefreshToken: (t: string) => backend.set(key('refresh-token'), t),
|
setRefreshToken: (t: string) => backend.set(key("refresh-token"), t),
|
||||||
getUser: <T = unknown>(): T | null => {
|
getUser: <T = unknown>(): T | null => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(backend.get(key('auth-user')) ?? 'null') as T | null;
|
return JSON.parse(backend.get(key("auth-user")) ?? "null") as T | null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setUser: <T>(u: T) => backend.set(key('auth-user'), JSON.stringify(u)),
|
setUser: <T>(u: T) => backend.set(key("auth-user"), JSON.stringify(u)),
|
||||||
getProfileId: () => backend.get(key('profile-id')) ?? undefined,
|
getProfileId: () => backend.get(key("profile-id")) ?? undefined,
|
||||||
setProfileId: (id: string) => backend.set(key('profile-id'), id),
|
setProfileId: (id: string) => backend.set(key("profile-id"), id),
|
||||||
getProfile: <T = unknown>(): T | null => {
|
getProfile: <T = unknown>(): T | null => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(backend.get(key('current-profile')) ?? 'null') as T | null;
|
return JSON.parse(
|
||||||
|
backend.get(key("current-profile")) ?? "null",
|
||||||
|
) as T | null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setProfile: <T>(p: T) => backend.set(key('current-profile'), JSON.stringify(p)),
|
setProfile: <T>(p: T) =>
|
||||||
removeProfile: () => backend.remove(key('current-profile')),
|
backend.set(key("current-profile"), JSON.stringify(p)),
|
||||||
|
removeProfile: () => backend.remove(key("current-profile")),
|
||||||
clear: () => {
|
clear: () => {
|
||||||
[key('auth-token'), key('refresh-token'), key('auth-user'), key('profile-id'), key('current-profile')].forEach((k) =>
|
[
|
||||||
backend.remove(k),
|
key("auth-token"),
|
||||||
);
|
key("refresh-token"),
|
||||||
Cookies.remove('auth-token', { path: '/' });
|
key("auth-user"),
|
||||||
|
key("profile-id"),
|
||||||
|
key("current-profile"),
|
||||||
|
].forEach((k) => backend.remove(k));
|
||||||
|
Cookies.remove("auth-token", { path: "/" });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearAllCookies = () => {
|
|
||||||
const cookies = Cookies.get();
|
|
||||||
Object.keys(cookies).forEach((name) => {
|
|
||||||
Cookies.remove(name);
|
|
||||||
Cookies.remove(name, { path: '/' });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user