feat(frontend): sidebar migration started
This commit is contained in:
@@ -3,7 +3,8 @@ import { usernameClient } from "better-auth/client/plugins";
|
||||
import { create } from "zustand";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { redirect, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { api } from "./axiosAPI";
|
||||
|
||||
// ---- TYPES ----
|
||||
export type Session = typeof authClient.$Infer.Session | null;
|
||||
@@ -24,7 +25,7 @@ export type UserRoles = {
|
||||
|
||||
type UserRoleState = {
|
||||
userRoles: UserRoles[] | null;
|
||||
fetchRoles: (userId: string) => Promise<void>;
|
||||
fetchRoles: () => Promise<void>;
|
||||
clearRoles: () => void;
|
||||
};
|
||||
|
||||
@@ -37,14 +38,11 @@ export const useAuth = create<SessionState>((set) => ({
|
||||
|
||||
export const useUserRoles = create<UserRoleState>((set) => ({
|
||||
userRoles: null,
|
||||
fetchRoles: async (userId: string) => {
|
||||
fetchRoles: async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/${userId}/roles`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to fetch roles");
|
||||
const roles = await res.json();
|
||||
set({ userRoles: roles });
|
||||
const res = await api.get("/api/user/roles");
|
||||
const roles = res.data;
|
||||
set({ userRoles: roles.data });
|
||||
} catch (err) {
|
||||
console.error("Error fetching roles:", err);
|
||||
set({ userRoles: null });
|
||||
@@ -52,6 +50,59 @@ export const useUserRoles = create<UserRoleState>((set) => ({
|
||||
},
|
||||
clearRoles: () => set({ userRoles: null }),
|
||||
}));
|
||||
|
||||
export function userAccess(
|
||||
moduleName: string | null,
|
||||
roles: UserRoles["role"] | UserRoles["role"][]
|
||||
): boolean {
|
||||
const { userRoles } = useUserRoles();
|
||||
|
||||
if (!userRoles) return false;
|
||||
|
||||
const roleArray = Array.isArray(roles) ? roles : [roles];
|
||||
|
||||
return userRoles.some(
|
||||
(m) =>
|
||||
(moduleName ? m.module === moduleName : true) &&
|
||||
roleArray.includes(m.role)
|
||||
);
|
||||
}
|
||||
|
||||
export async function checkUserAccess({
|
||||
allowedRoles,
|
||||
moduleName,
|
||||
}: {
|
||||
allowedRoles: UserRoles["role"][];
|
||||
moduleName?: string;
|
||||
//location: { pathname: string; search: string };
|
||||
}) {
|
||||
try {
|
||||
// fetch roles from your API (credentials required)
|
||||
const res = await api.get("/api/user/roles", { withCredentials: true });
|
||||
const roles = res.data.data as UserRoles[];
|
||||
|
||||
const hasAccess = roles.some(
|
||||
(r) =>
|
||||
(moduleName ? r.module === moduleName : true) &&
|
||||
allowedRoles.includes(r.role)
|
||||
);
|
||||
|
||||
if (!hasAccess) {
|
||||
throw redirect({
|
||||
to: "/",
|
||||
search: { from: location.pathname + location.search },
|
||||
});
|
||||
}
|
||||
|
||||
// return roles so the route component can use them if needed
|
||||
return roles;
|
||||
} catch {
|
||||
throw redirect({
|
||||
to: "/login",
|
||||
search: { redirect: location.pathname + location.search },
|
||||
});
|
||||
}
|
||||
}
|
||||
// ---- BETTER AUTH CLIENT ----
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: `${window.location.origin}/lst/api/auth`,
|
||||
@@ -85,6 +136,9 @@ export async function signin(data: { username: string; password: string }) {
|
||||
|
||||
export const useLogout = () => {
|
||||
const { clearSession } = useAuth();
|
||||
const { clearRoles } = useUserRoles();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const router = useRouter();
|
||||
const logout = async () => {
|
||||
await authClient.signOut();
|
||||
@@ -92,7 +146,8 @@ export const useLogout = () => {
|
||||
router.invalidate();
|
||||
router.clearCache();
|
||||
clearSession();
|
||||
|
||||
clearRoles();
|
||||
navigate({ to: "/" });
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
||||
6
frontend/src/lib/axiosAPI.ts
Normal file
6
frontend/src/lib/axiosAPI.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: "/lst",
|
||||
withCredentials: true, // ✅ always send credentials (cookies)
|
||||
});
|
||||
@@ -13,12 +13,11 @@ export const CheckboxField = ({ label }: CheckboxFieldProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="m-2 p-2 flex flex-row">
|
||||
<div>
|
||||
<Label htmlFor="active">
|
||||
<span>{label}</span>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor={field.name}>
|
||||
<span>{label}</span>
|
||||
</Label>
|
||||
|
||||
<Checkbox
|
||||
id={field.name}
|
||||
checked={field.state.value}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { useSession } from "../authClient";
|
||||
import { useSession, useUserRoles } from "../authClient";
|
||||
|
||||
export function SessionGuard({ children }: { children: React.ReactNode }) {
|
||||
const { data: session, isLoading } = useSession();
|
||||
const { fetchRoles } = useUserRoles();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !session) {
|
||||
router.navigate({ to: "/" }); // redirect if not logged in
|
||||
// if (!isLoading && !session) {
|
||||
// router.navigate({ to: "/" }); // redirect if not logged in
|
||||
// }
|
||||
if (session) {
|
||||
fetchRoles();
|
||||
}
|
||||
}, [isLoading, session, router]);
|
||||
|
||||
if (isLoading) return <div>Checking session…</div>;
|
||||
if (isLoading) return <div>App Loading</div>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
67
frontend/src/lib/providers/theme-provider.tsx
Normal file
67
frontend/src/lib/providers/theme-provider.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {createContext, useContext, useEffect, useState} from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user