feat(frontend): finished login form with validation

This commit is contained in:
2025-02-21 09:16:07 -06:00
parent d939332499
commit 9719451580
25 changed files with 551 additions and 230 deletions

View File

@@ -11,28 +11,36 @@
},
"dependencies": {
"@antfu/ni": "^23.3.1",
"@hookform/resolvers": "^4.1.0",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.66.5",
"@tanstack/react-router": "^1.106.0",
"@tanstack/zod-form-adapter": "^0.42.1",
"@types/react-grid-layout": "^1.3.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"js-cookie": "^3.0.5",
"lucide-react": "^0.475.0",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-grid-layout": "^1.5.0",
"react-hook-form": "^7.54.2",
"shadcn": "^2.4.0-canary.6",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.6",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {

View File

@@ -0,0 +1,18 @@
import {ReactNode} from "react";
import {Card} from "../ui/card";
interface LstCardProps {
children?: ReactNode;
className?: string;
style?: React.CSSProperties;
}
export function LstCard({children, className = "", style = {}}: LstCardProps) {
return (
<div className="m-auto">
<Card className={`border-solid border-2 border-[#00659c] ${className}`} style={style}>
{children}
</Card>
</div>
);
}

View File

@@ -6,91 +6,17 @@ import {QualitySideBar} from "./side-components/quality";
import {ForkliftSideBar} from "./side-components/forklift";
import {EomSideBar} from "./side-components/eom";
import {AdminSideBar} from "./side-components/admin";
type Feature = string;
interface Module {
id: number;
name: string;
features: Feature[];
active: boolean;
}
interface RolePermissions {
[moduleName: string]: Feature[];
}
interface Roles {
[roleName: string]: RolePermissions;
}
interface User {
id: number;
username: string;
role: keyof Roles;
}
const modules: Module[] = [
{id: 1, name: "production", active: true, features: ["view", "edit", "approve"]},
{id: 2, name: "logistics", active: true, features: ["view", "assign", "track"]},
{id: 3, name: "quality", active: false, features: ["view", "audit", "approve"]},
{id: 4, name: "forklift", active: true, features: ["view", "request", "operate"]},
{id: 5, name: "admin", active: true, features: ["view", "manage_users", "view_logs", "settings"]},
];
const rolePermissions: Roles = {
admin: {
production: ["view", "edit", "approve"],
logistics: ["view", "assign", "track"],
quality: ["view", "audit", "approve"],
forklift: ["view", "request", "operate"],
admin: ["view", "manage_users", "view_logs", "settings"],
},
manager: {
production: ["view", "edit"],
logistics: ["view", "assign"],
quality: ["view", "audit"],
forklift: ["view", "request"],
admin: ["view_logs"],
},
supervisor: {
production: ["view"],
logistics: ["view"],
quality: ["view"],
forklift: ["view", "request"],
admin: [],
},
user: {
production: ["view"],
logistics: [],
quality: [],
forklift: [],
admin: [],
},
};
// const users: User[] = [
// {id: 1, username: "admin", role: "admin"},
// {id: 2, username: "manager", role: "manager"},
// {id: 3, username: "supervisor", role: "supervisor"},
// {id: 4, username: "user", role: "user"},
// ];
function hasAccess(user: User, moduleName: string, feature: Feature): boolean {
return rolePermissions[user.role]?.[moduleName]?.includes(feature) || false;
}
function moduleActive(moduleName: string): boolean {
const module = modules.find((m) => m.name === moduleName);
return module ? module.active : false;
}
import {useSessionStore} from "../../lib/store/sessionStore";
import {hasAccess} from "../../utils/userAccess";
import {moduleActive} from "../../utils/moduleActive";
export function AppSidebar() {
const user = {id: 4, username: "admin", role: "admin"};
const {user} = useSessionStore();
return (
<Sidebar collapsible="icon">
<SidebarContent>
<Header />
{hasAccess(user, "production", "view") && moduleActive("production") && <ProductionSideBar />}
{moduleActive("production") && <ProductionSideBar />}
{hasAccess(user, "logistics", "view") && moduleActive("logistics") && <LogisticsSideBar />}
{hasAccess(user, "forklift", "view") && moduleActive("forklift") && <ForkliftSideBar />}
{hasAccess(user, "eom", "view") && moduleActive("admin") && <EomSideBar />}

View File

@@ -13,7 +13,7 @@ export function Header() {
<div className="flex flex-col gap-0.5 leading-none">
<span className="font-semibold">Logistics Support Tool</span>
<span className="">v1.0.0</span>
<span className="">v2.0.0</span>
</div>
</div>
</SidebarMenuButton>

View File

@@ -1,51 +1,36 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils"
import {cn} from "../../lib/utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
function Avatar({className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
)
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
function AvatarImage({className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
function AvatarFallback({className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
className={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
{...props}
/>
)
);
}
export { Avatar, AvatarImage, AvatarFallback }
export {Avatar, AvatarImage, AvatarFallback};

View File

@@ -0,0 +1,68 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn("flex flex-col gap-1.5 px-6 pt-6", className)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 pb-6", className)}
{...props}
/>
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,30 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -1,19 +1,21 @@
import * as React from "react";
import * as React from "react"
import {cn} from "../../lib/utils";
import { cn } from "@/lib/utils"
function Input({className, type, ...props}: React.ComponentProps<"input">) {
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4",
"border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
);
)
}
export {Input};
export { Input }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,27 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground font-medium",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground font-medium",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@@ -1,16 +1,14 @@
import {useRouter} from "@tanstack/react-router";
import {useSessionStore} from "../store/sessionStore";
import {useQueryClient} from "@tanstack/react-query";
export const useLogout = () => {
const {clearSession} = useSessionStore();
const router = useRouter();
const queryClient = useQueryClient();
const logout = async () => {
router.invalidate();
router.clearCache();
await clearSession();
queryClient.invalidateQueries();
clearSession();
window.location.reload();
};

View File

@@ -9,28 +9,31 @@ type User = {
export type SessionState = {
user: User | null;
token: string | null;
setSession: (user: SessionState["user"], token: string) => void;
setSession: (user: User | null, token: string | null) => void;
clearSession: () => void;
};
export const useSessionStore = create<SessionState>((set) => {
// Initialize from localStorage
const storedUser = localStorage.getItem("user");
// Initialize token from localStorage, but user remains in memory only
const storedToken = localStorage.getItem("auth_token");
return {
user: storedUser ? JSON.parse(storedUser) : null,
user: null, // User is NOT stored in localStorage
token: storedToken || null,
setSession: (user, token) => {
if (token) {
localStorage.setItem("auth_token", token);
//localStorage.setItem("user", JSON.stringify(user));
} else {
localStorage.removeItem("auth_token");
}
console.log("Setting session:", {user, token});
set({user, token});
},
clearSession: () => {
localStorage.removeItem("auth_token");
//localStorage.removeItem("user");
set({user: null, token: null});
},
};

View File

@@ -1,13 +1,14 @@
import {StrictMode} from "react";
import ReactDOM from "react-dom/client";
import "./styles.css";
import {SessionProvider} from "./components/providers/Providers.tsx";
import {RouterProvider, createRouter} from "@tanstack/react-router";
// Import the generated route tree
import {routeTree} from "./routeTree.gen";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
// Create a client
const queryClient = new QueryClient();
// Create a new router instance
const router = createRouter({routeTree});
@@ -24,9 +25,9 @@ if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<SessionProvider>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</SessionProvider>
</QueryClientProvider>
</StrictMode>
);
}

View File

@@ -14,6 +14,7 @@ import { Route as rootRoute } from './routes/__root'
import { Route as LoginImport } from './routes/login'
import { Route as AboutImport } from './routes/about'
import { Route as AuthImport } from './routes/_auth'
import { Route as AdminImport } from './routes/_admin'
import { Route as IndexImport } from './routes/index'
import { Route as OcpIndexImport } from './routes/ocp/index'
import { Route as OcpLotsImport } from './routes/ocp/lots'
@@ -38,6 +39,11 @@ const AuthRoute = AuthImport.update({
getParentRoute: () => rootRoute,
} as any)
const AdminRoute = AdminImport.update({
id: '/_admin',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
@@ -73,6 +79,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/_admin': {
id: '/_admin'
path: ''
fullPath: ''
preLoaderRoute: typeof AdminImport
parentRoute: typeof rootRoute
}
'/_auth': {
id: '/_auth'
path: ''
@@ -153,6 +166,7 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/_admin': typeof AdminRoute
'/_auth': typeof AuthRouteWithChildren
'/about': typeof AboutRoute
'/login': typeof LoginRoute
@@ -169,6 +183,7 @@ export interface FileRouteTypes {
id:
| '__root__'
| '/'
| '/_admin'
| '/_auth'
| '/about'
| '/login'
@@ -180,6 +195,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminRoute: typeof AdminRoute
AuthRoute: typeof AuthRouteWithChildren
AboutRoute: typeof AboutRoute
LoginRoute: typeof LoginRoute
@@ -189,6 +205,7 @@ export interface RootRouteChildren {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminRoute: AdminRoute,
AuthRoute: AuthRouteWithChildren,
AboutRoute: AboutRoute,
LoginRoute: LoginRoute,
@@ -207,6 +224,7 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/",
"/_admin",
"/_auth",
"/about",
"/login",
@@ -217,6 +235,9 @@ export const routeTree = rootRoute
"/": {
"filePath": "index.tsx"
},
"/_admin": {
"filePath": "_admin.tsx"
},
"/_auth": {
"filePath": "_auth.tsx",
"children": [

View File

@@ -1,4 +1,4 @@
import {createRootRoute, Outlet} from "@tanstack/react-router";
import {createRootRoute, Link, Outlet} from "@tanstack/react-router";
import {TanStackRouterDevtools} from "@tanstack/router-devtools";
import Cookies from "js-cookie";
import {SidebarProvider} from "../components/ui/sidebar";
@@ -14,14 +14,24 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../components/ui/dropdown-menu";
import {SessionProvider} from "../components/providers/Providers";
import {Toaster} from "sonner";
import {Button} from "../components/ui/button";
import {useLogout} from "../lib/hooks/useLogout";
import {useSession} from "../lib/hooks/useSession";
import {useSessionStore} from "../lib/store/sessionStore";
// same as the layout
export const Route = createRootRoute({
component: () => {
const sidebarState = Cookies.get("sidebar_state") === "true";
const {session} = useSession();
const {user} = useSessionStore();
const logout = useLogout();
return (
<>
<SessionProvider>
<ThemeProvider>
<nav className="flex justify-end">
<div className="m-2 flex flex-row">
@@ -31,6 +41,7 @@ export const Route = createRootRoute({
<div className="m-1">
<ModeToggle />
</div>
{session ? (
<div className="m-1">
<DropdownMenu>
<DropdownMenuTrigger>
@@ -40,22 +51,37 @@ export const Route = createRootRoute({
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuLabel>Hello {user?.username}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
<hr className="solid"></hr>
<DropdownMenuItem>
<div className="m-auto mt-3">
<Button onClick={() => logout()} variant="ghost">
Logout
</Button>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
<>
<Link to="/login">Login</Link>
</>
)}
</div>
</nav>
<SidebarProvider defaultOpen={sidebarState}>
<AppSidebar />
<Toaster expand={true} richColors closeButton />
<Outlet />
</SidebarProvider>
</ThemeProvider>
</SessionProvider>
<TanStackRouterDevtools position="bottom-right" />
</>

View File

@@ -0,0 +1,13 @@
import {createFileRoute, redirect} from "@tanstack/react-router";
// src/routes/_authenticated.tsx
export const Route = createFileRoute("/_admin")({
beforeLoad: async () => {
const auth = localStorage.getItem("auth_token");
if (!auth) {
throw redirect({
to: "/login",
});
}
},
});

View File

@@ -6,7 +6,7 @@ export const Route = createFileRoute("/_auth")({
const auth = localStorage.getItem("auth_token");
if (!auth) {
throw redirect({
to: "/",
to: "/login",
});
}
},

View File

@@ -1,38 +1,143 @@
import {createFileRoute, useRouter} from "@tanstack/react-router";
import {isAuthenticated, signIn, signOut} from "../utils/auth";
import {createFileRoute, redirect, useRouter} from "@tanstack/react-router";
import {useForm, Controller} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod";
import {LstCard} from "../components/extendedUI/LstCard";
import {Button} from "../components/ui/button";
import {Input} from "../components/ui/input";
import {CardHeader} from "../components/ui/card";
import {Label} from "../components/ui/label";
import {toast} from "sonner";
import {useSessionStore} from "../lib/store/sessionStore";
import {Checkbox} from "../components/ui/checkbox";
import {z} from "zod";
export const Route = createFileRoute("/login")({
component: RouteComponent,
beforeLoad: () => {
const isLoggedIn = localStorage.getItem("auth_token");
if (isLoggedIn) {
throw redirect({
to: "/",
});
}
},
});
const FormSchema = z.object({
username: z.string().min(1, "You must enter a valid username"),
password: z.string().min(4, "You must enter a valid password"),
rememberMe: z.boolean(),
});
function RouteComponent() {
const {setSession} = useSessionStore();
const rememeberMe = localStorage.getItem("rememberMe") === "true";
const username = localStorage.getItem("username") || "";
const router = useRouter();
const {
register,
handleSubmit,
control,
formState: {errors},
} = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
username: username || "",
password: "",
rememberMe: rememeberMe,
},
});
const onSubmitLogin = async (value: z.infer<typeof FormSchema>) => {
// Do something with form data
// first update the rememberMe incase it was selected
if (value.rememberMe) {
localStorage.setItem("rememberMe", value.rememberMe.toString());
localStorage.setItem("username", value.username);
} else {
localStorage.removeItem("rememberMe");
localStorage.removeItem("username");
}
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({username: value.username, password: value.password}),
});
// if (!response.ok) {
// throw new Error("Invalid credentials");
// }
const data = await response.json();
// Store token in localStorage
// localStorage.setItem("auth_token", data.data.token);
setSession(data.data.user, data.data.token);
toast.success(`You are logged in as ${data.data.user.username}`);
router.navigate({to: "/"});
} catch (err) {
toast.error("Invalid credentials");
}
};
return (
<div className="ml-[25%]">
<LstCard className="p-3 w-96">
<CardHeader>
<div>
<p className="text-2xl">Login to LST</p>
</div>
</CardHeader>
<hr className="rounded"></hr>
<form onSubmit={handleSubmit(onSubmitLogin)}>
<div>
<Label>Username</Label>
<Input
placeholder="smith001"
{...register("username")}
className={errors.username ? "border-red-500" : ""}
aria-invalid={!!errors.username}
/>
{errors.username && <p className="text-red-500 text-sm mt-1">{errors.username.message}</p>}
</div>
<div>
<h2>Ligin</h2>
{isAuthenticated() ? (
<>
<p>Hello User!</p>
<Button
onClick={async () => {
signOut();
router.invalidate();
}}
>
signOut
</Button>
<Label htmlFor={"password"}>Password</Label>
<Input
type="password"
{...register("password")}
className={errors.password ? "border-red-500" : ""}
aria-invalid={!!errors.password}
/>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>}
</>
) : (
<Button
onClick={async () => {
signIn();
router.invalidate();
}}
>
Sign in
</Button>
</div>
<div className="flex justify-between pt-2">
<div className="flex">
<Controller
render={({field}) => (
<Checkbox id="remember" checked={field.value} onCheckedChange={field.onChange} />
)}
control={control}
name="rememberMe"
defaultValue={rememeberMe}
/>
<Label htmlFor="remember" className="pl-2">
remember me
</Label>
</div>
<div className="flex justify-end">
<Button type="submit">Submit</Button>
</div>
</div>
</form>
</LstCard>
</div>
);
}

View File

@@ -0,0 +1,20 @@
type Feature = string;
interface Module {
id: number;
name: string;
features: Feature[];
active: boolean;
}
const modules: Module[] = [
{id: 1, name: "production", active: true, features: ["view", "edit", "approve"]},
{id: 2, name: "logistics", active: true, features: ["view", "assign", "track"]},
{id: 3, name: "quality", active: false, features: ["view", "audit", "approve"]},
{id: 4, name: "forklift", active: true, features: ["view", "request", "operate"]},
{id: 5, name: "admin", active: true, features: ["view", "manage_users", "view_logs", "settings"]},
];
export function moduleActive(moduleName: string): boolean {
const module = modules.find((m) => m.name === moduleName);
return module ? module.active : false;
}

View File

@@ -0,0 +1,52 @@
interface User {
id: number;
username: string;
role: keyof Roles;
}
interface Roles {
[roleName: string]: RolePermissions;
}
interface RolePermissions {
[moduleName: string]: Feature[];
}
type Feature = string;
const rolePermissions: Roles = {
admin: {
production: ["view", "manage", "update", "admin"],
logistics: ["view", "manage", "update", "admin"],
quality: ["view", "request", "manage", "update", "admin"],
forklift: ["view", "manage", "update", "admin"],
admin: ["view", "view_logs", "manage", "update", "admin"],
},
manager: {
production: ["view", "manage"],
logistics: ["view", "manage"],
quality: ["view", "manage"],
forklift: ["view", "manage"],
admin: ["view_logs"],
},
supervisor: {
production: ["view", "update"],
logistics: ["view", "update"],
quality: ["view", "update"],
forklift: ["view"],
admin: [],
},
user: {
production: ["view"],
logistics: ["view"],
quality: ["view"],
forklift: [],
admin: [],
},
};
// user will need access to the module.
// users role will determine there visual access
export function hasAccess(user: User | null, moduleName: string, feature: Feature): boolean {
return user?.role ? rolePermissions[user.role]?.[moduleName]?.includes(feature) || false : false;
}

View File

@@ -4,18 +4,18 @@
"description": "",
"main": "index.ts",
"scripts": {
"dev": "dotenvx run -- bun run -r --parallel --aggregate-output dev",
"dev:all": "concurrently -n 'server,frontend' -c '#007755,#2f6da3' 'cd server && bun run dev' 'cd frontend && bun run dev'",
"dev": "concurrently -n 'server,frontend' -c '#007755,#2f6da3' 'cd server && bun run dev' 'cd frontend && bun run dev'",
"dev:server": "bun --env-file .env --watch server/index.ts",
"dev:ocme": "bun --env-file .env --watch ocme/index.ts",
"dev:frontend": "cd frontend && bunx --bun vite",
"build:server": "cd apps/server && bun build index.js --outdir ../../dist/server",
"build:ocme": "rimraf dist/ocme && cd apps/ocme && bun build index.js --outdir ../../dist/ocme",
"build:front": "cd frontend && rimraf frontend/dist && bun run build",
"start": "cd server && bun run --env-file ../.env ./index.js",
"start": "bun --env-file .env server/index.js",
"commit": "cz",
"clean": "rimraf dist/server",
"deploy": "standard-version --conventional-commits"
"deploy": "standard-version --conventional-commits",
"ui:add": "cd frontend && bun shadcn add "
},
"keywords": [],
"author": "",

View File

@@ -10,7 +10,6 @@ import scalar from "./route/scalar";
// services
import {ocmeService} from "./services/ocme/ocmeServer";
console.log(process.env.JWT_SECRET);
const app = new OpenAPIHono();
app.use("*", logger());
@@ -52,8 +51,8 @@ routes.forEach((route) => {
// return c.json({success: true, message: "is authenticated"});
// });
app.get("*", serveStatic({root: "../frontend/dist"}));
app.get("*", serveStatic({path: "../frontend/dist/index.html"}));
app.get("*", serveStatic({root: "./frontend/dist"}));
app.get("*", serveStatic({path: "./frontend/dist/index.html"}));
export default app;

View File

@@ -63,6 +63,8 @@ const route = createRoute({
app.openapi(route, async (c) => {
let body: {username: string; password: string};
console.log(`Trying to login`);
try {
body = await c.req.json();
} catch (error) {

View File

@@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "index.ts"]
}

View File

@@ -18,6 +18,8 @@
"allowJs": true,
"types": [
"bun-types" // add Bun global
]
}
],
"outDir": "dist"
},
"include": ["./server/src", "./server/index.ts"]
}