mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 03:02:03 +00:00
fix(auth): restore JWT and align CRUD with backend routes
This commit is contained in:
parent
7be969951c
commit
4a67d6b16f
17 changed files with 284 additions and 219 deletions
|
|
@ -1,30 +1,15 @@
|
||||||
import { expect, test, type Page } from "@playwright/test";
|
import { expect, test, type Page } from "@playwright/test";
|
||||||
|
|
||||||
const TEST_USER = {
|
const TEST_USER = {
|
||||||
id: 1,
|
token: "test-jwt-token",
|
||||||
|
expiration: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
||||||
email: "user@seahavenind.com",
|
email: "user@seahavenind.com",
|
||||||
name: "Test User",
|
userRoles: "Admin",
|
||||||
|
phoneNumber: "5551234567",
|
||||||
|
fullname: "Test User",
|
||||||
|
id: "1",
|
||||||
};
|
};
|
||||||
|
|
||||||
async function mockSession(page: Page, isAuthenticated: () => boolean = () => false) {
|
|
||||||
await page.route("**/api/Authentication/GetCurrentUser", async (route) => {
|
|
||||||
if (isAuthenticated()) {
|
|
||||||
await route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: "application/json",
|
|
||||||
body: JSON.stringify({ data: TEST_USER }),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await route.fulfill({
|
|
||||||
status: 401,
|
|
||||||
contentType: "application/json",
|
|
||||||
body: JSON.stringify({ message: "Unauthorized" }),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mockDashboardStats(page: Page) {
|
async function mockDashboardStats(page: Page) {
|
||||||
await page.route("**/api/Dashboard/Stats", async (route) => {
|
await page.route("**/api/Dashboard/Stats", async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
|
|
@ -44,7 +29,9 @@ async function mockDashboardStats(page: Page) {
|
||||||
|
|
||||||
test.describe("Login page", () => {
|
test.describe("Login page", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await mockSession(page);
|
await page.addInitScript(() => {
|
||||||
|
localStorage.removeItem("auth");
|
||||||
|
});
|
||||||
await mockDashboardStats(page);
|
await mockDashboardStats(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -86,13 +73,7 @@ test.describe("Login page", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("logs in successfully and navigates to dashboard", async ({ page }) => {
|
test("logs in successfully and navigates to dashboard", async ({ page }) => {
|
||||||
let authenticated = false;
|
|
||||||
|
|
||||||
await page.unroute("**/api/Authentication/GetCurrentUser");
|
|
||||||
await mockSession(page, () => authenticated);
|
|
||||||
|
|
||||||
await page.route("**/api/Authentication/Login", async (route) => {
|
await page.route("**/api/Authentication/Login", async (route) => {
|
||||||
authenticated = true;
|
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
|
|
@ -109,5 +90,9 @@ test.describe("Login page", () => {
|
||||||
await expect(page).toHaveURL(/\/$/);
|
await expect(page).toHaveURL(/\/$/);
|
||||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
||||||
await expect(page.getByRole("heading", { name: "Welcome back" })).not.toBeVisible();
|
await expect(page.getByRole("heading", { name: "Welcome back" })).not.toBeVisible();
|
||||||
|
|
||||||
|
const storedAuth = await page.evaluate(() => localStorage.getItem("auth"));
|
||||||
|
expect(storedAuth).toContain(TEST_USER.token);
|
||||||
|
expect(storedAuth).toContain(TEST_USER.fullname);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
export const API_PATHS = {
|
export const API_PATHS = {
|
||||||
authentication: {
|
authentication: {
|
||||||
login: "Authentication/Login",
|
login: "Authentication/Login",
|
||||||
logout: "Authentication/Logout",
|
|
||||||
refresh: "Authentication/Refresh",
|
|
||||||
getCurrentUser: "Authentication/GetCurrentUser",
|
|
||||||
changePassword: "Authentication/ChangePassword",
|
changePassword: "Authentication/ChangePassword",
|
||||||
forgetPassword: "Authentication/ForgetPassword",
|
forgetPassword: "Authentication/ForgetPassword",
|
||||||
verificationCode: "Authentication/VerificationCode",
|
verificationCode: "Authentication/VerificationCode",
|
||||||
|
|
@ -15,6 +12,9 @@ export const API_PATHS = {
|
||||||
create: "WorkOrder/Create",
|
create: "WorkOrder/Create",
|
||||||
update: "WorkOrder/Update",
|
update: "WorkOrder/Update",
|
||||||
delete: "WorkOrder/DeleteWorkOrder",
|
delete: "WorkOrder/DeleteWorkOrder",
|
||||||
|
addWorkorder: "WorkOrder/AddWorkorder",
|
||||||
|
editWorkorder: "WorkOrder/EditWorkorder",
|
||||||
|
deleteWorkorder: "WorkOrder/DeleteWorkorder",
|
||||||
addCommentJson: "WorkOrder/AddCommentJson",
|
addCommentJson: "WorkOrder/AddCommentJson",
|
||||||
changeAssignment: "WorkOrder/ChangeAssignment",
|
changeAssignment: "WorkOrder/ChangeAssignment",
|
||||||
updateChecklistItem: "WorkOrder/UpdateChecklistItem",
|
updateChecklistItem: "WorkOrder/UpdateChecklistItem",
|
||||||
|
|
@ -54,6 +54,9 @@ export const API_PATHS = {
|
||||||
createEmployee: "Employee/CreateEmployee",
|
createEmployee: "Employee/CreateEmployee",
|
||||||
updateEmployee: "Employee/UpdateEmployee",
|
updateEmployee: "Employee/UpdateEmployee",
|
||||||
deleteEmployee: "Employee/DeleteEmployee",
|
deleteEmployee: "Employee/DeleteEmployee",
|
||||||
|
addEmployees: "Employee/AddEmployees",
|
||||||
|
editEmployees: "Employee/EditEmployees",
|
||||||
|
deleteEmployees: "Employee/DeleteEmployees",
|
||||||
dropdown: "Employee/Dropdown",
|
dropdown: "Employee/Dropdown",
|
||||||
},
|
},
|
||||||
contact: {
|
contact: {
|
||||||
|
|
@ -64,6 +67,7 @@ export const API_PATHS = {
|
||||||
delete: "Contact/Delete",
|
delete: "Contact/Delete",
|
||||||
addContacts: "Contact/AddContacts",
|
addContacts: "Contact/AddContacts",
|
||||||
editContacts: "Contact/EditContacts",
|
editContacts: "Contact/EditContacts",
|
||||||
|
deleteContacts: "Contact/DeleteContacts",
|
||||||
dropdown: "Contact/Dropdown",
|
dropdown: "Contact/Dropdown",
|
||||||
getLocations: "Contact/GetLocations",
|
getLocations: "Contact/GetLocations",
|
||||||
},
|
},
|
||||||
|
|
@ -85,6 +89,9 @@ export const API_PATHS = {
|
||||||
addAccount: "Account/AddAccount",
|
addAccount: "Account/AddAccount",
|
||||||
editAccount: "Account/EditAccount",
|
editAccount: "Account/EditAccount",
|
||||||
deleteAccount: "Account/DeleteAccount",
|
deleteAccount: "Account/DeleteAccount",
|
||||||
|
addAccounts: "Account/AddAccounts",
|
||||||
|
editAccounts: "Account/EditAccounts",
|
||||||
|
deleteAccounts: "Account/DeleteAccounts",
|
||||||
dropdown: "Account/Dropdown",
|
dropdown: "Account/Dropdown",
|
||||||
},
|
},
|
||||||
asset: {
|
asset: {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ import { API_PATHS } from "@/api/api-paths";
|
||||||
import { createApiErrorFromResponse, mapHttpStatusToMessage } from "@/api/api-error";
|
import { createApiErrorFromResponse, mapHttpStatusToMessage } from "@/api/api-error";
|
||||||
import { queryKeys } from "@/infra/query-key/query-key";
|
import { queryKeys } from "@/infra/query-key/query-key";
|
||||||
import queryClient from "@/lib/query/query-client";
|
import queryClient from "@/lib/query/query-client";
|
||||||
|
import { clearAuth, getAccessToken } from "@/lib/auth/auth-storage";
|
||||||
let isRefreshing = false;
|
|
||||||
let refreshPromise: Promise<boolean> | null = null;
|
|
||||||
|
|
||||||
function resolvePrefixUrl(url: string): string {
|
function resolvePrefixUrl(url: string): string {
|
||||||
const trimmed = url.trim() || "/api";
|
const trimmed = url.trim() || "/api";
|
||||||
|
|
@ -26,6 +24,7 @@ function resolvePrefixUrl(url: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSessionAndRedirectToLogin(): void {
|
function clearSessionAndRedirectToLogin(): void {
|
||||||
|
clearAuth();
|
||||||
queryClient.setQueryData(queryKeys.auth.session(), null);
|
queryClient.setQueryData(queryKeys.auth.session(), null);
|
||||||
|
|
||||||
if (typeof window !== "undefined" && window.location.pathname !== "/login") {
|
if (typeof window !== "undefined" && window.location.pathname !== "/login") {
|
||||||
|
|
@ -33,49 +32,25 @@ function clearSessionAndRedirectToLogin(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshSession(): Promise<boolean> {
|
function isAuthLoginRequest(request: Request): boolean {
|
||||||
if (isRefreshing && refreshPromise) {
|
return request.url.includes(API_PATHS.authentication.login);
|
||||||
return refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
isRefreshing = true;
|
|
||||||
refreshPromise = (async () => {
|
|
||||||
try {
|
|
||||||
const response = await api.post(API_PATHS.authentication.refresh, {
|
|
||||||
retry: { limit: 0 },
|
|
||||||
});
|
|
||||||
return response.ok;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isRefreshing = false;
|
|
||||||
refreshPromise = null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRefreshRequest(request: Request): boolean {
|
|
||||||
return request.url.includes(API_PATHS.authentication.refresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAuthSessionRequest(request: Request): boolean {
|
|
||||||
return (
|
|
||||||
request.url.includes(API_PATHS.authentication.getCurrentUser) ||
|
|
||||||
request.url.includes(API_PATHS.authentication.login) ||
|
|
||||||
request.url.includes(API_PATHS.authentication.logout)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api: KyInstance = ky.create({
|
export const api: KyInstance = ky.create({
|
||||||
prefixUrl: resolvePrefixUrl(env.apiUrl),
|
prefixUrl: resolvePrefixUrl(env.apiUrl),
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
|
beforeRequest: [
|
||||||
|
(request) => {
|
||||||
|
const token = getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
request.headers.set("Authorization", token);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
beforeError: [
|
beforeError: [
|
||||||
async (error: HTTPError) => {
|
async (error: HTTPError) => {
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
|
|
@ -92,22 +67,12 @@ export const api: KyInstance = ky.create({
|
||||||
],
|
],
|
||||||
afterResponse: [
|
afterResponse: [
|
||||||
async (request, _options, response) => {
|
async (request, _options, response) => {
|
||||||
if (response.status !== 401 || isRefreshRequest(request) || isAuthSessionRequest(request)) {
|
if (response.status !== 401 || isAuthLoginRequest(request)) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshed = await refreshSession();
|
clearSessionAndRedirectToLogin();
|
||||||
if (!refreshed) {
|
return response;
|
||||||
clearSessionAndRedirectToLogin();
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const retried = await ky(request, { credentials: "include" });
|
|
||||||
if (retried.status === 401) {
|
|
||||||
clearSessionAndRedirectToLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
return retried;
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
5
src/app/(protected)/calendar/_layout.tsx
Normal file
5
src/app/(protected)/calendar/_layout.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function CalendarLayout() {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,3 @@
|
||||||
import {
|
|
||||||
FaAddressBook,
|
|
||||||
FaBell,
|
|
||||||
FaCalendarAlt,
|
|
||||||
FaClipboardList,
|
|
||||||
FaCog,
|
|
||||||
FaCubes,
|
|
||||||
FaDollarSign,
|
|
||||||
FaFileInvoiceDollar,
|
|
||||||
FaMapMarkerAlt,
|
|
||||||
FaMoneyCheckAlt,
|
|
||||||
FaTachometerAlt,
|
|
||||||
FaTools,
|
|
||||||
FaTruck,
|
|
||||||
FaUserTie,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import type { IconType } from "react-icons";
|
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -26,60 +9,12 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import { navGroups, type NavItemDef } from "@/config/menu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const SIDEBAR_WIDTH = 220;
|
const SIDEBAR_WIDTH = 220;
|
||||||
const SIDEBAR_COLLAPSED_WIDTH = 56;
|
const SIDEBAR_COLLAPSED_WIDTH = 56;
|
||||||
|
|
||||||
type NavItemDef = {
|
|
||||||
label: string;
|
|
||||||
icon: IconType;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const navGroups: { title: string; items: NavItemDef[] }[] = [
|
|
||||||
{
|
|
||||||
title: "Overview",
|
|
||||||
items: [
|
|
||||||
{ label: "Dashboard", icon: FaTachometerAlt, path: "/" },
|
|
||||||
{ label: "Calendar", icon: FaCalendarAlt, path: "/calendar" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Operations",
|
|
||||||
items: [
|
|
||||||
{ label: "Work Orders", icon: FaTools, path: "/workorders" },
|
|
||||||
{ label: "Uplifts", icon: FaDollarSign, path: "/uplifts" },
|
|
||||||
{ label: "Follow Ups", icon: FaBell, path: "/followups" },
|
|
||||||
{ label: "PM Schedules", icon: FaClipboardList, path: "/pmschedules" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Vendors",
|
|
||||||
items: [
|
|
||||||
{ label: "Vendors", icon: FaTruck, path: "/vendors" },
|
|
||||||
{ label: "Vendor POs", icon: FaFileInvoiceDollar, path: "/vendor-pos" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Customers & Sites",
|
|
||||||
items: [
|
|
||||||
{ label: "Accounts", icon: FaMoneyCheckAlt, path: "/accounts" },
|
|
||||||
{ label: "Contacts", icon: FaAddressBook, path: "/contacts" },
|
|
||||||
{ label: "Locations", icon: FaMapMarkerAlt, path: "/locations" },
|
|
||||||
{ label: "Assets", icon: FaCubes, path: "/assets" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Internal",
|
|
||||||
items: [{ label: "Employees", icon: FaUserTie, path: "/employees" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "System",
|
|
||||||
items: [{ label: "Settings", icon: FaCog, path: "/settings/dropdowns" }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type NavItemProps = NavItemDef & {
|
type NavItemProps = NavItemDef & {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
end?: boolean;
|
end?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ export function AppTopbar({ onToggleNav }: AppTopbarProps) {
|
||||||
logout();
|
logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const userInitials = user?.name
|
const userInitials = user?.fullname
|
||||||
? user.name
|
? user.fullname
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((part) => part[0])
|
.map((part) => part[0])
|
||||||
.join("")
|
.join("")
|
||||||
|
|
@ -104,7 +104,7 @@ export function AppTopbar({ onToggleNav }: AppTopbarProps) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
title={user?.name || "User"}
|
title="Account"
|
||||||
onClick={(event) => setUserAnchor(event.currentTarget)}
|
onClick={(event) => setUserAnchor(event.currentTarget)}
|
||||||
sx={{
|
sx={{
|
||||||
color: "var(--color-header-text-muted)",
|
color: "var(--color-header-text-muted)",
|
||||||
|
|
@ -168,7 +168,6 @@ export function AppTopbar({ onToggleNav }: AppTopbarProps) {
|
||||||
{userInitials}
|
{userInitials}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="subtitle2">{user?.name || "User"}</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{user?.email || ""}
|
{user?.email || ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
|
||||||
67
src/config/menu.ts
Normal file
67
src/config/menu.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import {
|
||||||
|
FaAddressBook,
|
||||||
|
FaBell,
|
||||||
|
FaClipboardList,
|
||||||
|
FaCog,
|
||||||
|
FaCubes,
|
||||||
|
FaDollarSign,
|
||||||
|
FaFileInvoiceDollar,
|
||||||
|
FaMapMarkerAlt,
|
||||||
|
FaMoneyCheckAlt,
|
||||||
|
FaTachometerAlt,
|
||||||
|
FaTools,
|
||||||
|
FaTruck,
|
||||||
|
FaUserTie,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import type { IconType } from "react-icons";
|
||||||
|
|
||||||
|
export type NavItemDef = {
|
||||||
|
label: string;
|
||||||
|
icon: IconType;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavGroup = {
|
||||||
|
title: string;
|
||||||
|
items: NavItemDef[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navGroups: NavGroup[] = [
|
||||||
|
{
|
||||||
|
title: "Overview",
|
||||||
|
items: [{ label: "Dashboard", icon: FaTachometerAlt, path: "/" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Operations",
|
||||||
|
items: [
|
||||||
|
{ label: "Work Orders", icon: FaTools, path: "/workorders" },
|
||||||
|
{ label: "Uplifts", icon: FaDollarSign, path: "/uplifts" },
|
||||||
|
{ label: "Follow Ups", icon: FaBell, path: "/followups" },
|
||||||
|
{ label: "PM Schedules", icon: FaClipboardList, path: "/pmschedules" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Vendors",
|
||||||
|
items: [
|
||||||
|
{ label: "Vendors", icon: FaTruck, path: "/vendors" },
|
||||||
|
{ label: "Vendor POs", icon: FaFileInvoiceDollar, path: "/vendor-pos" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Customers & Sites",
|
||||||
|
items: [
|
||||||
|
{ label: "Accounts", icon: FaMoneyCheckAlt, path: "/accounts" },
|
||||||
|
{ label: "Contacts", icon: FaAddressBook, path: "/contacts" },
|
||||||
|
{ label: "Locations", icon: FaMapMarkerAlt, path: "/locations" },
|
||||||
|
{ label: "Assets", icon: FaCubes, path: "/assets" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Internal",
|
||||||
|
items: [{ label: "Employees", icon: FaUserTie, path: "/employees" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "System",
|
||||||
|
items: [{ label: "Settings", icon: FaCog, path: "/settings/dropdowns" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { API_PATHS } from "@/api/api-paths";
|
import { API_PATHS } from "@/api/api-paths";
|
||||||
import { apiDelete, apiGet, apiPost, apiPut } from "@/api/api";
|
import { apiGet, apiPost } from "@/api/api";
|
||||||
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
||||||
import {
|
import {
|
||||||
mapAccount,
|
mapAccount,
|
||||||
|
|
@ -38,24 +38,31 @@ export const accountsApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getById: async (id: string | number): Promise<Account> => {
|
getById: async (id: string | number): Promise<Account> => {
|
||||||
const data = await apiGet<unknown>(`${API_PATHS.rest.accounts}/${id}`);
|
const data = await apiGet<unknown>(API_PATHS.account.getById, {
|
||||||
|
searchParams: { id },
|
||||||
|
});
|
||||||
return mapAccount(handleApiResponse(data));
|
return mapAccount(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (payload: unknown): Promise<Account> => {
|
create: async (payload: unknown): Promise<Account> => {
|
||||||
const data = await apiPost<unknown>(API_PATHS.rest.accounts, mapAccountToBackend(payload));
|
const data = await apiPost<unknown>(
|
||||||
return mapAccount(handleApiResponse(data));
|
API_PATHS.account.addAccounts,
|
||||||
},
|
|
||||||
|
|
||||||
update: async (id: string | number, payload: unknown): Promise<Account> => {
|
|
||||||
const data = await apiPut<unknown>(
|
|
||||||
`${API_PATHS.rest.accounts}/${id}`,
|
|
||||||
mapAccountToBackend(payload),
|
mapAccountToBackend(payload),
|
||||||
);
|
);
|
||||||
return mapAccount(handleApiResponse(data));
|
return mapAccount(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
update: async (id: string | number, payload: unknown): Promise<Account> => {
|
||||||
|
const data = await apiPost<unknown>(
|
||||||
|
API_PATHS.account.editAccounts,
|
||||||
|
mapAccountToBackend({ ...(payload as object), id }),
|
||||||
|
);
|
||||||
|
return mapAccount(handleApiResponse(data));
|
||||||
|
},
|
||||||
|
|
||||||
delete: async (id: string | number): Promise<void> => {
|
delete: async (id: string | number): Promise<void> => {
|
||||||
await apiDelete<unknown>(`${API_PATHS.rest.accounts}/${id}`);
|
await apiGet<unknown>(API_PATHS.account.deleteAccounts, {
|
||||||
|
searchParams: { id },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* Auth API contract (cookie-based session):
|
* Auth API contract (JWT):
|
||||||
* - POST Authentication/Login — sets httpOnly session cookies
|
* - POST Authentication/Login — returns token + user fields; persisted in localStorage
|
||||||
* - GET Authentication/GetCurrentUser — returns active user from cookie
|
* - Session restored from stored login payload (no GetCurrentUser)
|
||||||
* - POST Authentication/Logout — clears session cookies
|
|
||||||
* - POST Authentication/Refresh — refreshes session cookie on 401
|
|
||||||
*/
|
*/
|
||||||
import { API_PATHS } from "@/api/api-paths";
|
import { API_PATHS } from "@/api/api-paths";
|
||||||
import { api, apiPost } from "@/api/api";
|
import { apiPost } from "@/api/api";
|
||||||
import { handleApiResponse } from "@/api/handle-api-response";
|
import { handleApiResponse } from "@/api/handle-api-response";
|
||||||
|
import { clearAuth, getAuthUser, isSessionValid, setAuthUser } from "@/lib/auth/auth-storage";
|
||||||
import type {
|
import type {
|
||||||
AuthUser,
|
AuthUser,
|
||||||
ChangePasswordPayload,
|
ChangePasswordPayload,
|
||||||
|
|
@ -17,29 +16,24 @@ import type {
|
||||||
|
|
||||||
export async function login(credentials: LoginCredentials): Promise<AuthUser> {
|
export async function login(credentials: LoginCredentials): Promise<AuthUser> {
|
||||||
const response = await apiPost<unknown>(API_PATHS.authentication.login, credentials);
|
const response = await apiPost<unknown>(API_PATHS.authentication.login, credentials);
|
||||||
return handleApiResponse<AuthUser>(response);
|
const user = handleApiResponse<AuthUser>(response);
|
||||||
|
setAuthUser(user);
|
||||||
|
return getAuthUser() ?? user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(): Promise<void> {
|
export async function logout(): Promise<void> {
|
||||||
await apiPost(API_PATHS.authentication.logout);
|
clearAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSession(): Promise<AuthUser | null> {
|
export async function getSession(): Promise<AuthUser | null> {
|
||||||
try {
|
const user = getAuthUser();
|
||||||
const response = await api.get(API_PATHS.authentication.getCurrentUser, {
|
|
||||||
throwHttpErrors: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!user || !isSessionValid(user)) {
|
||||||
return null;
|
clearAuth();
|
||||||
}
|
|
||||||
|
|
||||||
const payload = (await response.json()) as unknown;
|
|
||||||
const user = handleApiResponse<AuthUser | null>(payload);
|
|
||||||
return user ?? null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changePassword(data: ChangePasswordPayload): Promise<unknown> {
|
export async function changePassword(data: ChangePasswordPayload): Promise<unknown> {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
export interface AuthUser {
|
export interface AuthUser {
|
||||||
id?: string | number;
|
token: string;
|
||||||
|
expiration: string;
|
||||||
|
email: string;
|
||||||
|
userRoles: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
fullname: string;
|
||||||
|
id: string | number;
|
||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
|
||||||
username?: string;
|
|
||||||
role?: string;
|
|
||||||
roles?: string[];
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginCredentials {
|
export interface LoginCredentials {
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ export function useLogin(): UseMutationResult<AuthUser, Error, LoginCredentials>
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (credentials: LoginCredentials) => authApi.login(credentials),
|
mutationFn: (credentials: LoginCredentials) => authApi.login(credentials),
|
||||||
meta: { suppressErrorToast: true },
|
meta: { suppressErrorToast: true },
|
||||||
onSuccess: async () => {
|
onSuccess: (user) => {
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth.session() });
|
queryClient.setQueryData(queryKeys.auth.session(), user);
|
||||||
toast.success("Login successful!");
|
toast.success("Login successful!");
|
||||||
navigate("/");
|
navigate("/");
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { API_PATHS } from "@/api/api-paths";
|
import { API_PATHS } from "@/api/api-paths";
|
||||||
import { apiDelete, apiGet, apiPost, apiPut } from "@/api/api";
|
import { apiGet, apiPost } from "@/api/api";
|
||||||
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
||||||
import {
|
import {
|
||||||
mapContact,
|
mapContact,
|
||||||
|
|
@ -38,24 +38,31 @@ export const contactsApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getById: async (id: string | number): Promise<Contact> => {
|
getById: async (id: string | number): Promise<Contact> => {
|
||||||
const data = await apiGet<unknown>(`${API_PATHS.rest.contacts}/${id}`);
|
const data = await apiGet<unknown>(API_PATHS.contact.getById, {
|
||||||
|
searchParams: { id },
|
||||||
|
});
|
||||||
return mapContact(handleApiResponse(data));
|
return mapContact(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (payload: unknown): Promise<Contact> => {
|
create: async (payload: unknown): Promise<Contact> => {
|
||||||
const data = await apiPost<unknown>(API_PATHS.rest.contacts, mapContactToBackend(payload));
|
const data = await apiPost<unknown>(
|
||||||
return mapContact(handleApiResponse(data));
|
API_PATHS.contact.addContacts,
|
||||||
},
|
|
||||||
|
|
||||||
update: async (id: string | number, payload: unknown): Promise<Contact> => {
|
|
||||||
const data = await apiPut<unknown>(
|
|
||||||
`${API_PATHS.rest.contacts}/${id}`,
|
|
||||||
mapContactToBackend(payload),
|
mapContactToBackend(payload),
|
||||||
);
|
);
|
||||||
return mapContact(handleApiResponse(data));
|
return mapContact(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
update: async (id: string | number, payload: unknown): Promise<Contact> => {
|
||||||
|
const data = await apiPost<unknown>(
|
||||||
|
API_PATHS.contact.editContacts,
|
||||||
|
mapContactToBackend({ ...(payload as object), id }),
|
||||||
|
);
|
||||||
|
return mapContact(handleApiResponse(data));
|
||||||
|
},
|
||||||
|
|
||||||
delete: async (id: string | number): Promise<void> => {
|
delete: async (id: string | number): Promise<void> => {
|
||||||
await apiDelete<unknown>(`${API_PATHS.rest.contacts}/${id}`);
|
await apiGet<unknown>(API_PATHS.contact.deleteContacts, {
|
||||||
|
searchParams: { id },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { API_PATHS } from "@/api/api-paths";
|
import { API_PATHS } from "@/api/api-paths";
|
||||||
import { apiDelete, apiGet, apiPost, apiPut } from "@/api/api";
|
import { apiGet, apiPost } from "@/api/api";
|
||||||
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
||||||
import {
|
import {
|
||||||
mapEmployeeListItems,
|
mapEmployeeListItems,
|
||||||
|
|
@ -38,25 +38,32 @@ export const employeesApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getById: async (id: string | number): Promise<Employee | null> => {
|
getById: async (id: string | number): Promise<Employee | null> => {
|
||||||
const data = await apiGet<unknown>(`${API_PATHS.rest.employees}/${id}`);
|
const data = await apiGet<unknown>(API_PATHS.employee.getById, {
|
||||||
|
searchParams: { id },
|
||||||
|
});
|
||||||
return transformEmployee(handleApiResponse(data));
|
return transformEmployee(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (payload: Employee): Promise<Employee | null> => {
|
create: async (payload: Employee): Promise<Employee | null> => {
|
||||||
const data = await apiPost<unknown>(API_PATHS.rest.employees, transformToBackend(payload));
|
const data = await apiPost<unknown>(
|
||||||
|
API_PATHS.employee.addEmployees,
|
||||||
|
transformToBackend(payload),
|
||||||
|
);
|
||||||
return transformEmployee(handleApiResponse(data));
|
return transformEmployee(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (id: string | number, payload: Employee): Promise<Employee | null> => {
|
update: async (id: string | number, payload: Employee): Promise<Employee | null> => {
|
||||||
const data = await apiPut<unknown>(
|
const data = await apiPost<unknown>(
|
||||||
`${API_PATHS.rest.employees}/${id}`,
|
API_PATHS.employee.editEmployees,
|
||||||
transformToBackend({ ...payload, id }),
|
transformToBackend({ ...payload, id }),
|
||||||
);
|
);
|
||||||
return transformEmployee(handleApiResponse(data));
|
return transformEmployee(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (id: string | number): Promise<void> => {
|
delete: async (id: string | number): Promise<void> => {
|
||||||
await apiDelete<unknown>(`${API_PATHS.rest.employees}/${id}`);
|
await apiGet<unknown>(API_PATHS.employee.deleteEmployees, {
|
||||||
|
searchParams: { id },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getDropdown: () => apiGet<unknown>(API_PATHS.employee.dropdown),
|
getDropdown: () => apiGet<unknown>(API_PATHS.employee.dropdown),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { API_PATHS } from "@/api/api-paths";
|
import { API_PATHS } from "@/api/api-paths";
|
||||||
import { apiDelete, apiGet, apiPost, apiPut } from "@/api/api";
|
import { apiDelete, apiGet, apiPost } from "@/api/api";
|
||||||
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
||||||
import {
|
import {
|
||||||
mapDispatchComment,
|
mapDispatchComment,
|
||||||
|
|
@ -99,7 +99,7 @@ export const workOrdersApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getById: async (id: string | number): Promise<WorkOrderDetail> => {
|
getById: async (id: string | number): Promise<WorkOrderDetail> => {
|
||||||
const data = await apiGet<unknown>(`${API_PATHS.rest.workOrders}/${id}`);
|
const data = await apiGet<unknown>(API_PATHS.workOrder.getById, { searchParams: { id } });
|
||||||
return mapWorkOrderDetail(handleApiResponse(data));
|
return mapWorkOrderDetail(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -107,7 +107,7 @@ export const workOrdersApi = {
|
||||||
values: WorkOrderFormSchemaValues | WorkOrderFormValues,
|
values: WorkOrderFormSchemaValues | WorkOrderFormValues,
|
||||||
): Promise<WorkOrderDetail> => {
|
): Promise<WorkOrderDetail> => {
|
||||||
const payload = mapWorkOrderFormToCreatePayload(values);
|
const payload = mapWorkOrderFormToCreatePayload(values);
|
||||||
const data = await apiPost<unknown>(API_PATHS.rest.workOrders, payload);
|
const data = await apiPost<unknown>(API_PATHS.workOrder.addWorkorder, payload);
|
||||||
return mapWorkOrderDetail(handleApiResponse(data));
|
return mapWorkOrderDetail(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -116,12 +116,12 @@ export const workOrdersApi = {
|
||||||
values: WorkOrderFormSchemaValues | WorkOrderFormValues,
|
values: WorkOrderFormSchemaValues | WorkOrderFormValues,
|
||||||
): Promise<WorkOrderDetail> => {
|
): Promise<WorkOrderDetail> => {
|
||||||
const payload = mapWorkOrderFormToUpdatePayload(id, values);
|
const payload = mapWorkOrderFormToUpdatePayload(id, values);
|
||||||
const data = await apiPut<unknown>(`${API_PATHS.rest.workOrders}/${id}`, payload);
|
const data = await apiPost<unknown>(API_PATHS.workOrder.editWorkorder, payload);
|
||||||
return mapWorkOrderDetail(handleApiResponse(data));
|
return mapWorkOrderDetail(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (id: string | number): Promise<void> => {
|
delete: async (id: string | number): Promise<void> => {
|
||||||
await apiDelete<unknown>(`${API_PATHS.rest.workOrders}/${id}`);
|
await apiDelete<unknown>(API_PATHS.workOrder.deleteWorkorder, { searchParams: { id } });
|
||||||
},
|
},
|
||||||
|
|
||||||
addComment: async (
|
addComment: async (
|
||||||
|
|
|
||||||
68
src/lib/auth/auth-storage.ts
Normal file
68
src/lib/auth/auth-storage.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type { AuthUser } from "@/domain/auth/types/auth-user";
|
||||||
|
|
||||||
|
const AUTH_STORAGE_KEY = "auth";
|
||||||
|
|
||||||
|
function parseExpiration(expiration: string): number | null {
|
||||||
|
const asNumber = Number(expiration);
|
||||||
|
if (!Number.isNaN(asNumber) && asNumber > 0) {
|
||||||
|
return asNumber > 1e12 ? asNumber : asNumber * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Date.parse(expiration);
|
||||||
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAuthUser(user: AuthUser): void {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthUser(): AuthUser | null {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = localStorage.getItem(AUTH_STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as AuthUser;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccessToken(): string | null {
|
||||||
|
const user = getAuthUser();
|
||||||
|
if (!user?.token || typeof user.token !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Bearer ${user.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAuth(): void {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSessionValid(user: AuthUser | null): boolean {
|
||||||
|
if (!user?.token || !user.expiration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresAt = parseExpiration(user.expiration);
|
||||||
|
if (expiresAt === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Date.now() < expiresAt;
|
||||||
|
}
|
||||||
|
|
@ -5,12 +5,23 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import LoginPage from "@/app/(auth)/login";
|
import LoginPage from "@/app/(auth)/login";
|
||||||
import * as authApi from "@/domain/auth/api/auth-api";
|
import * as authApi from "@/domain/auth/api/auth-api";
|
||||||
|
import type { AuthUser } from "@/domain/auth/types/auth-user";
|
||||||
import { renderWithProviders } from "@/test/test-utils";
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
const { toastSuccessMock } = vi.hoisted(() => ({
|
const { toastSuccessMock } = vi.hoisted(() => ({
|
||||||
toastSuccessMock: vi.fn(),
|
toastSuccessMock: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const TEST_USER: AuthUser = {
|
||||||
|
token: "test-jwt-token",
|
||||||
|
expiration: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
||||||
|
email: "user@seahavenind.com",
|
||||||
|
userRoles: "Admin",
|
||||||
|
phoneNumber: "5551234567",
|
||||||
|
fullname: "Test User",
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
vi.mock("@/domain/auth/api/auth-api", () => ({
|
vi.mock("@/domain/auth/api/auth-api", () => ({
|
||||||
getSession: vi.fn(),
|
getSession: vi.fn(),
|
||||||
login: vi.fn(),
|
login: vi.fn(),
|
||||||
|
|
@ -61,10 +72,7 @@ describe("LoginPage integration", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("logs in successfully and navigates to dashboard", async () => {
|
it("logs in successfully and navigates to dashboard", async () => {
|
||||||
const userPayload = { id: 1, email: "user@seahavenind.com" };
|
vi.mocked(authApi.login).mockResolvedValue(TEST_USER);
|
||||||
|
|
||||||
vi.mocked(authApi.login).mockResolvedValue(userPayload);
|
|
||||||
vi.mocked(authApi.getSession).mockResolvedValue(userPayload);
|
|
||||||
|
|
||||||
renderWithProviders(<LoginRoutes />, { route: "/login" });
|
renderWithProviders(<LoginRoutes />, { route: "/login" });
|
||||||
|
|
||||||
|
|
@ -116,7 +124,7 @@ describe("LoginPage integration", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows loading state while login is pending", async () => {
|
it("shows loading state while login is pending", async () => {
|
||||||
let resolveLogin: (value: { id: number; email: string }) => void = () => {};
|
let resolveLogin: (value: AuthUser) => void = () => {};
|
||||||
|
|
||||||
vi.mocked(authApi.login).mockImplementation(
|
vi.mocked(authApi.login).mockImplementation(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -124,7 +132,6 @@ describe("LoginPage integration", () => {
|
||||||
resolveLogin = resolve;
|
resolveLogin = resolve;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
vi.mocked(authApi.getSession).mockResolvedValue({ id: 1, email: "user@seahavenind.com" });
|
|
||||||
|
|
||||||
renderWithProviders(<LoginRoutes />, { route: "/login" });
|
renderWithProviders(<LoginRoutes />, { route: "/login" });
|
||||||
|
|
||||||
|
|
@ -142,7 +149,7 @@ describe("LoginPage integration", () => {
|
||||||
expect(within(form!).getByRole("button", { name: /signing in/i })).toBeDisabled();
|
expect(within(form!).getByRole("button", { name: /signing in/i })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
resolveLogin({ id: 1, email: "user@seahavenind.com" });
|
resolveLogin(TEST_USER);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("dashboard")).toBeInTheDocument();
|
expect(screen.getByTestId("dashboard")).toBeInTheDocument();
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,20 @@ import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { GuestRoute } from "@/components/auth/guest-route";
|
import { GuestRoute } from "@/components/auth/guest-route";
|
||||||
import { ProtectedRoute } from "@/components/auth/protected-route";
|
import { ProtectedRoute } from "@/components/auth/protected-route";
|
||||||
|
import type { AuthUser } from "@/domain/auth/types/auth-user";
|
||||||
import { AuthContext, type AuthContextValue } from "@/providers/auth-context";
|
import { AuthContext, type AuthContextValue } from "@/providers/auth-context";
|
||||||
import { renderWithProviders } from "@/test/test-utils";
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
const mockAuthUser: AuthUser = {
|
||||||
|
token: "test-jwt-token",
|
||||||
|
expiration: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
||||||
|
email: "user@seahavenind.com",
|
||||||
|
userRoles: "Admin",
|
||||||
|
phoneNumber: "5551234567",
|
||||||
|
fullname: "Test User",
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
function createAuthContextValue(overrides: Partial<AuthContextValue> = {}): AuthContextValue {
|
function createAuthContextValue(overrides: Partial<AuthContextValue> = {}): AuthContextValue {
|
||||||
return {
|
return {
|
||||||
user: null,
|
user: null,
|
||||||
|
|
@ -51,7 +62,7 @@ describe("ProtectedRoute", () => {
|
||||||
</ProtectedRoute>,
|
</ProtectedRoute>,
|
||||||
createAuthContextValue({
|
createAuthContextValue({
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
user: { id: 1, email: "user@seahavenind.com" },
|
user: mockAuthUser,
|
||||||
}),
|
}),
|
||||||
"/workorders",
|
"/workorders",
|
||||||
);
|
);
|
||||||
|
|
@ -81,7 +92,7 @@ describe("GuestRoute", () => {
|
||||||
</GuestRoute>,
|
</GuestRoute>,
|
||||||
createAuthContextValue({
|
createAuthContextValue({
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
user: { id: 1, email: "user@seahavenind.com" },
|
user: mockAuthUser,
|
||||||
}),
|
}),
|
||||||
"/login",
|
"/login",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue