feat(auth): finally better auth working as i wanted it to

This commit is contained in:
2025-09-22 22:40:44 -05:00
parent 4ab43d91b9
commit 8f1375ab7b
50 changed files with 7939 additions and 5909 deletions

View File

@@ -78,8 +78,9 @@ const main = async () => {
app.use(express.json()); app.use(express.json());
const allowedOrigins = [ const allowedOrigins = [
"http://localhost:5173", // dev "http://localhost:5173", // lstV2 dev
"http://localhost:4200", "http://localhost:5500", // lst dev
"http://localhost:4200", // express
env.BETTER_AUTH_URL, // prod env.BETTER_AUTH_URL, // prod
]; ];

View File

@@ -0,0 +1,75 @@
import { db } from "../../../pkg/db/db.js";
import { userRoles } from "../../../pkg/db/schema/user_roles.js";
import { createLogger } from "../../../pkg/logger/logger.js";
import { tryCatch } from "../../../pkg/utils/tryCatch.js";
export const systemAdminRole = async (userId: string) => {
const log = createLogger({
module: "admin",
subModule: "systemAdminSetup",
});
const systemAdminRoles = [
{
userId: userId,
module: "users",
role: "systemAdmin",
},
{
userId: userId,
module: "system",
role: "systemAdmin",
},
{
userId: userId,
module: "ocp",
role: "systemAdmin",
},
{
userId: userId,
module: "siloAdjustments",
role: "systemAdmin",
},
{
userId: userId,
module: "demandManagement",
role: "systemAdmin",
},
{
userId: userId,
module: "logistics",
role: "systemAdmin",
},
{
userId: userId,
module: "production",
role: "systemAdmin",
},
{
userId: userId,
module: "quality",
role: "systemAdmin",
},
{
userId: userId,
module: "eom",
role: "systemAdmin",
},
{
userId: userId,
module: "forklifts",
role: "systemAdmin",
},
];
const { data, error } = await tryCatch(
db.insert(userRoles).values(systemAdminRoles).onConflictDoNothing()
);
if (error) {
log.error(
{ stack: { error: error } },
"There was an error creating the system admin roles"
);
}
log.info({ data }, "New system admin roles created");
};

View File

@@ -0,0 +1,19 @@
import type { Express, Request, Response } from "express";
import { requireAuth } from "../../pkg/middleware/authMiddleware.js";
//admin routes
import users from "./routes/getUserRoles.js";
import grantRoles from "./routes/grantRole.js";
export const setupAdminRoutes = (app: Express, basePath: string) => {
app.use(
basePath + "/api/admin/users",
requireAuth("user", ["systemAdmin"]), // will pass bc system admin but this is just telling us we need this
users
);
app.use(
basePath + "/api/admin",
requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this
grantRoles
);
};

View File

@@ -0,0 +1,52 @@
import { Router } from "express";
import type { Request, Response } from "express";
import { tryCatch } from "../../../pkg/utils/tryCatch.js";
import { db } from "../../../pkg/db/db.js";
import { user } from "../../../pkg/db/schema/auth-schema.js";
import { userRoles } from "../../../pkg/db/schema/user_roles.js";
const router = Router();
router.post("/", async (req: Request, res: Response) => {
// should get all users
const { data: users, error: userError } = await tryCatch(
db.select().from(user)
);
if (userError) {
return res.status(500).json({
success: false,
message: "Failed to get users",
error: userError,
});
}
// should get all roles
const { data: userRole, error: userRoleError } = await tryCatch(
db.select().from(userRoles)
);
if (userRoleError) {
return res.status(500).json({
success: false,
message: "Failed to get userRoless",
error: userRoleError,
});
}
// add the roles and return
const usersWithRoles = users.map((user) => {
const roles = userRole
.filter((ur) => ur.userId === user.id)
.map((ur) => ({ module: ur.module, role: ur.role }));
return { ...user, roles };
});
return res
.status(200)
.json({ success: true, message: "User data", data: usersWithRoles });
});
export default router;

View File

@@ -0,0 +1,74 @@
import { Router } from "express";
import type { Request, Response } from "express";
import { tryCatch } from "../../../pkg/utils/tryCatch.js";
import { db } from "../../../pkg/db/db.js";
import z from "zod";
import { userRoles } from "../../../pkg/db/schema/user_roles.js";
import { createLogger } from "../../../pkg/logger/logger.js";
const roleSchema = z.object({
module: z.enum([
"users",
"system",
"ocp",
"siloAdjustments",
"demandManagement",
"logistics",
"production",
"quality",
"eom",
"forklifts",
]),
role: z.enum(["admin", "manager", "supervisor", "test,", "viewer"]),
});
const router = Router();
router.post("/:userId/grant", async (req: Request, res: Response) => {
const log = createLogger({
module: "admin",
subModule: "grantRoles",
});
const userId = req.params.userId;
console.log(userId);
try {
const validated = roleSchema.parse(req.body);
const data = await db
.insert(userRoles)
.values({
userId,
module: validated.module,
role: validated.role,
})
.onConflictDoUpdate({
target: [userRoles.userId, userRoles.module],
set: { module: validated.module, role: validated.role },
});
log.info(
{},
`Module: ${validated.module}, Role: ${validated.role} as was just granted to userID: ${userId}`
);
return res.status(200).json({
success: true,
message: `Module: ${validated.module}, Role: ${validated.role} as was just granted`,
data,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
return res.status(400).json({
error: "Validation failed",
details: flattened,
});
}
return res.status(400).json({
success: false,
message: "Invalid input please try again.",
});
}
});
export default router;

View File

@@ -1,11 +1,12 @@
import { Router } from "express"; import { Router, type Request, type Response } from "express";
import { auth } from "../../../pkg/auth/auth.js"; import { auth } from "../../../pkg/auth/auth.js";
import { fromNodeHeaders } from "better-auth/node"; import { fromNodeHeaders } from "better-auth/node";
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
const router = Router(); const router = Router();
// GET /health // GET /health
router.get("/", async (req, res) => { router.get("/", async (req: Request, res: Response) => {
const session = await auth.api.getSession({ const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers), headers: fromNodeHeaders(req.headers),
}); });

View File

@@ -2,6 +2,11 @@ import { Router } from "express";
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import z from "zod"; import z from "zod";
import { auth } from "../../../pkg/auth/auth.js"; import { auth } from "../../../pkg/auth/auth.js";
import { db } from "../../../pkg/db/db.js";
import { count } from "drizzle-orm";
import { user } from "../../../pkg/db/schema/auth-schema.js";
import { APIError, betterAuth } from "better-auth";
import { systemAdminRole } from "../../admin/controller/systemAdminRole.js";
const router = Router(); const router = Router();
@@ -18,6 +23,9 @@ const registerSchema = z.object({
}); });
router.post("/", async (req: Request, res: Response) => { router.post("/", async (req: Request, res: Response) => {
// check if we are the first user so we can add as system admin to all modules
const totalUsers = await db.select({ count: count() }).from(user);
console.log(totalUsers[0].count);
try { try {
// Parse + validate incoming JSON against Zod schema // Parse + validate incoming JSON against Zod schema
const validated = registerSchema.parse(req.body); const validated = registerSchema.parse(req.body);
@@ -27,6 +35,9 @@ router.post("/", async (req: Request, res: Response) => {
body: validated, body: validated,
}); });
if (totalUsers[0].count === 0) {
systemAdminRole(user.user.id);
}
return res.status(201).json(user); return res.status(201).json(user);
} catch (err) { } catch (err) {
if (err instanceof z.ZodError) { if (err instanceof z.ZodError) {
@@ -36,8 +47,14 @@ router.post("/", async (req: Request, res: Response) => {
details: flattened, details: flattened,
}); });
} }
console.error(err);
return res.status(500).json({ error: "Internal server error" }); if (err instanceof APIError) {
return res.status(400).json({
success: false,
message: err.message,
error: err.status,
});
}
} }
}); });

View File

@@ -1,8 +1,9 @@
import type { Express, Request, Response } from "express"; import type { Express, Request, Response } from "express";
import me from "./me.js"; import me from "./me.js";
import register from "./register.js"; import register from "./register.js";
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
export const setupAuthRoutes = (app: Express, basePath: string) => { export const setupAuthRoutes = (app: Express, basePath: string) => {
app.use(basePath + "/api/me", me); app.use(basePath + "/api/user/me", requireAuth(), me);
app.use(basePath + "/api/auth/register", register); app.use(basePath + "/api/user/register", register);
}; };

View File

@@ -2,6 +2,7 @@ import type { Express, Request, Response } from "express";
import healthRoutes from "./routes/healthRoutes.js"; import healthRoutes from "./routes/healthRoutes.js";
import { setupAuthRoutes } from "../auth/routes/routes.js"; import { setupAuthRoutes } from "../auth/routes/routes.js";
import { setupAdminRoutes } from "../admin/routes.js";
export const setupRoutes = (app: Express, basePath: string) => { export const setupRoutes = (app: Express, basePath: string) => {
// Root / health check // Root / health check
@@ -9,6 +10,7 @@ export const setupRoutes = (app: Express, basePath: string) => {
// all routes // all routes
setupAuthRoutes(app, basePath); setupAuthRoutes(app, basePath);
setupAdminRoutes(app, basePath);
// always try to go to the app weather we are in dev or in production. // always try to go to the app weather we are in dev or in production.
app.get(basePath + "/", (req: Request, res: Response) => { app.get(basePath + "/", (req: Request, res: Response) => {

View File

@@ -20,20 +20,26 @@ export const auth = betterAuth({
provider: "pg", provider: "pg",
schema, schema,
}), }),
trustedOrigins: [
"*.alpla.net",
"http://localhost:5173",
"http://localhost:5500",
],
appName: "lst", appName: "lst",
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
minPasswordLength: 8, // optional config minPasswordLength: 8, // optional config
}, },
plugins: [ plugins: [
jwt({ jwt: { expirationTime: "1h" } }), //jwt({ jwt: { expirationTime: "1h" } }),
apiKey(), apiKey(),
admin(), admin(),
username(), username(),
], ],
session: { session: {
expiresIn: 60 * 60, expiresIn: 60 * 60,
updateAge: 60 * 1, updateAge: 60 * 5,
freshAge: 60 * 2,
cookieCache: { cookieCache: {
enabled: true, enabled: true,
maxAge: 5 * 60, // Cache duration in seconds maxAge: 5 * 60, // Cache duration in seconds

View File

@@ -1,8 +1,8 @@
import { pgTable, text, uuid, uniqueIndex } from "drizzle-orm/pg-core"; import { pgTable, text, uuid, uniqueIndex } from "drizzle-orm/pg-core";
import { user } from "./auth-schema.js"; import { user } from "./auth-schema.js";
export const userRole = pgTable( export const userRoles = pgTable(
"user_role", "user_roles",
{ {
userRoleId: uuid("user_role_id").defaultRandom().primaryKey(), userRoleId: uuid("user_role_id").defaultRandom().primaryKey(),
userId: text("user_id") userId: text("user_id")
@@ -15,3 +15,4 @@ export const userRole = pgTable(
uniqueIndex("unique_user_module").on(table.userId, table.module), uniqueIndex("unique_user_module").on(table.userId, table.module),
] ]
); );
export type UserRole = typeof userRoles.$inferSelect;

View File

@@ -0,0 +1,90 @@
import type { Request, Response, NextFunction } from "express";
import { auth } from "../auth/auth.js";
import { userRoles, type UserRole } from "../db/schema/user_roles.js";
import { db } from "../db/db.js";
import { eq } from "drizzle-orm";
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email?: string;
roles: Record<string, string[]>;
};
}
}
}
function toWebHeaders(nodeHeaders: Request["headers"]): Headers {
const h = new Headers();
for (const [key, value] of Object.entries(nodeHeaders)) {
if (Array.isArray(value)) {
value.forEach((v) => h.append(key, v));
} else if (value !== undefined) {
h.set(key, value);
}
}
return h;
}
export const requireAuth = (moduleName?: string, requiredRoles?: string[]) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const headers = toWebHeaders(req.headers);
// Get session
const session = await auth.api.getSession({
headers,
query: { disableCookieCache: true },
});
if (!session) {
return res.status(401).json({ error: "No active session" });
}
const userId = session.user.id;
// Get roles
const roles = await db
.select()
.from(userRoles)
.where(eq(userRoles.userId, userId));
// Organize roles by module
const rolesByModule: Record<string, string[]> = {};
for (const r of roles) {
if (!rolesByModule[r.module]) rolesByModule[r.module] = [];
rolesByModule[r.module].push(r.role);
}
req.user = {
id: userId,
email: session.user.email,
roles: rolesByModule,
};
// SystemAdmin override
const hasSystemAdmin = Object.values(rolesByModule)
.flat()
.includes("systemAdmin");
// Role check (skip if systemAdmin)
if (requiredRoles?.length && !hasSystemAdmin) {
const moduleRoles = moduleName
? rolesByModule[moduleName] ?? []
: Object.values(rolesByModule).flat();
const hasAccess = moduleRoles.some((role) =>
requiredRoles.includes(role)
);
if (!hasAccess) {
return res.status(403).json({ error: "Forbidden" });
}
}
next();
} catch (err) {
console.error("Auth middleware error:", err);
res.status(500).json({ error: "Auth check failed" });
}
};
};

12080
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,50 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-checkbox": "^1.3.3",
"@tailwindcss/vite": "^4.1.13", "@radix-ui/react-label": "^2.1.7",
"@tanstack/react-query": "^5.89.0", "@radix-ui/react-select": "^2.2.6",
"@tanstack/react-router": "^1.131.36", "@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-router-devtools": "^1.131.36", "@tailwindcss/vite": "^4.1.13",
"better-auth": "^1.3.11", "@tanstack/react-form": "^1.23.0",
"class-variance-authority": "^0.7.1", "@tanstack/react-query": "^5.89.0",
"clsx": "^2.1.1", "@tanstack/react-router": "^1.131.36",
"lucide-react": "^0.542.0", "@tanstack/react-router-devtools": "^1.131.36",
"react": "^19.1.1", "axios": "^1.12.2",
"react-dom": "^19.1.1", "better-auth": "^1.3.11",
"tailwind-merge": "^3.3.1", "class-variance-authority": "^0.7.1",
"tailwindcss": "^4.1.13" "clsx": "^2.1.1",
}, "lucide-react": "^0.542.0",
"devDependencies": { "react": "^19.1.1",
"@eslint/js": "^9.33.0", "react-dom": "^19.1.1",
"@tanstack/router-plugin": "^1.131.36", "sonner": "^2.0.7",
"@types/node": "^24.3.1", "tailwind-merge": "^3.3.1",
"@types/react": "^19.1.10", "tailwindcss": "^4.1.13",
"@types/react-dom": "^19.1.7", "zustand": "^5.0.8"
"@vitejs/plugin-react-swc": "^4.0.0", },
"eslint": "^9.33.0", "devDependencies": {
"eslint-plugin-react-hooks": "^5.2.0", "@eslint/js": "^9.33.0",
"eslint-plugin-react-refresh": "^0.4.20", "@tanstack/router-plugin": "^1.131.36",
"globals": "^16.3.0", "@types/node": "^24.3.1",
"tw-animate-css": "^1.3.8", "@types/react": "^19.1.10",
"typescript": "~5.8.3", "@types/react-dom": "^19.1.7",
"typescript-eslint": "^8.39.1", "@vitejs/plugin-react-swc": "^4.0.0",
"vite": "^7.1.2" "eslint": "^9.33.0",
} "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"tw-animate-css": "^1.3.8",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
} }

View File

@@ -0,0 +1,29 @@
import { Link } from "@tanstack/react-router";
import { useAuth, useLogout } from "../../lib/authClient";
import { Button } from "../ui/button";
export default function Nav() {
const { session } = useAuth();
const logout = useLogout();
return (
<nav>
{session?.session ? (
<>
<Button
onClick={() => {
logout();
}}
>
Sign Out
</Button>
</>
) : (
<>
<Button>
<Link to="/login">Login</Link>
</Button>
</>
)}
</nav>
);
}

View File

@@ -0,0 +1,92 @@
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 py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-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 CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
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 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
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 dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary 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-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 transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input 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 }

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(
"flex items-center gap-2 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,23 @@
import type { 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 (
<Card
className={`border-solid border-1 border-[#00659c] ${className}`}
style={style}
>
{children}
</Card>
);
}

View File

@@ -0,0 +1,183 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -1,11 +1,139 @@
import { createAuthClient } from "better-auth/client"; import { createAuthClient } from "better-auth/client";
import { usernameClient } from "better-auth/client/plugins"; import { usernameClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({ import { create } from "zustand";
baseURL: `${window.location.origin}/lst/api/auth`, // 👈 This is fine import { useQuery, useQueryClient } from "@tanstack/react-query";
callbacks: { import { useEffect } from "react";
onUpdate: (session: any) => console.log("Session updated", session), import { useRouter } from "@tanstack/react-router";
onSignIn: (session: any) => console.log("Signed in!", session),
onSignOut: () => console.log("Signed out!"), // ---- TYPES ----
export type Session = typeof authClient.$Infer.Session | null;
// Zustand store type
type SessionState = {
session: Session;
setSession: (session: Session) => void;
clearSession: () => void;
};
export type UserRoles = {
userRoleId: string;
userId: string;
module: string;
role: "systemAdmin" | "admin" | "manager" | "user" | "viewer";
};
type UserRoleState = {
userRoles: UserRoles[] | null;
fetchRoles: (userId: string) => Promise<void>;
clearRoles: () => void;
};
// ---- ZUSTAND STORE ----
export const useAuth = create<SessionState>((set) => ({
session: null,
setSession: (session) => set({ session }),
clearSession: () => set({ session: null }),
}));
export const useUserRoles = create<UserRoleState>((set) => ({
userRoles: null,
fetchRoles: async (userId: string) => {
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 });
} catch (err) {
console.error("Error fetching roles:", err);
set({ userRoles: null });
}
}, },
clearRoles: () => set({ userRoles: null }),
}));
// ---- BETTER AUTH CLIENT ----
export const authClient = createAuthClient({
baseURL: `${window.location.origin}/lst/api/auth`,
plugins: [usernameClient()], plugins: [usernameClient()],
callbacks: {
callbacks: {
onUpdate: (res: any) => {
// res has strong type
// res.data is `Session | null`
useAuth.getState().setSession(res?.data ?? null);
},
onSignIn: (res: any) => {
console.log("Setting session to ", res?.data);
useAuth.getState().setSession(res?.data ?? null);
},
onSignOut: () => {
useAuth.getState().clearSession();
},
},
},
}); });
// ---- AUTH API HELPERS ----
export async function signin(data: { username: string; password: string }) {
const res = await authClient.signIn.username(data);
if (res.error) throw res.error;
await authClient.getSession();
return res.data;
}
export const useLogout = () => {
const { clearSession } = useAuth();
const router = useRouter();
const logout = async () => {
await authClient.signOut();
router.invalidate();
router.clearCache();
clearSession();
window.location.reload();
};
return logout;
};
export async function getSession() {
const res = await authClient.getSession({
query: { disableCookieCache: true },
});
if (res.error) return null;
return res.data;
}
// ---- REACT QUERY INTEGRATION ----
export function useSession() {
const { setSession, clearSession } = useAuth();
const qc = useQueryClient();
const query = useQuery({
queryKey: ["session"],
queryFn: getSession,
refetchInterval: 60_000,
refetchOnWindowFocus: true,
});
//console.log("Auth Check", query.data);
// react to data change
useEffect(() => {
if (query.data !== undefined) {
setSession(query.data);
}
}, [query.data, setSession]);
// react to error
useEffect(() => {
if (query.error) {
clearSession();
qc.removeQueries({ queryKey: ["session"] });
}
}, [query.error, qc, clearSession]);
return query;
}

View File

@@ -0,0 +1,34 @@
import { Label } from "@radix-ui/react-label";
import { useFieldContext } from "..";
import { FieldErrors } from "./FieldErrors";
import { Checkbox } from "../../../components/ui/checkbox";
type CheckboxFieldProps = {
label: string;
description?: string;
};
export const CheckboxField = ({ label }: CheckboxFieldProps) => {
const field = useFieldContext<boolean>();
return (
<div>
<div className="m-2 p-2 flex flex-row">
<div>
<Label htmlFor="active">
<span>{label}</span>
</Label>
</div>
<Checkbox
id={field.name}
checked={field.state.value}
onCheckedChange={(checked) => {
field.handleChange(checked === true);
}}
onBlur={field.handleBlur}
/>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -0,0 +1,16 @@
import type { AnyFieldMeta } from "@tanstack/react-form";
import { ZodError } from "zod";
type FieldErrorsProps = {
meta: AnyFieldMeta;
};
export const FieldErrors = ({ meta }: FieldErrorsProps) => {
if (!meta.isTouched) return null;
return meta.errors.map(({ message }: ZodError, index) => (
<p key={index} className="text-sm font-medium text-destructive">
{message}
</p>
));
};

View File

@@ -0,0 +1,28 @@
import { useFieldContext } from "..";
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { FieldErrors } from "./FieldErrors";
type InputFieldProps = {
label: string;
inputType: string;
required: boolean;
};
export const InputField = ({ label, inputType, required }: InputFieldProps) => {
const field = useFieldContext<any>();
return (
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<Input
id={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
type={inputType}
required={required}
/>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -0,0 +1,57 @@
import { useFieldContext } from "..";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../components/ui/select";
import { FieldErrors } from "./FieldErrors";
import { Label } from "../../../components/ui/label";
type SelectOption = {
value: string;
label: string;
};
type SelectFieldProps = {
label: string;
options: SelectOption[];
placeholder?: string;
};
export const SelectField = ({
label,
options,
placeholder,
}: SelectFieldProps) => {
const field = useFieldContext<string>();
return (
<div className="grid gap-3">
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<Select
value={field.state.value}
onValueChange={(value) => field.handleChange(value)}
>
<SelectTrigger
id={field.name}
onBlur={field.handleBlur}
className="w-[380px]"
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -0,0 +1,24 @@
import { useStore } from "@tanstack/react-form";
import { useFormContext } from "..";
import { Button } from "../../../components/ui/button";
type SubmitButtonProps = {
children: React.ReactNode;
};
export const SubmitButton = ({ children }: SubmitButtonProps) => {
const form = useFormContext();
const [isSubmitting] = useStore(form.store, (state) => [
state.isSubmitting,
state.canSubmit,
]);
return (
<div className="">
<Button type="submit" disabled={isSubmitting}>
{children}
</Button>
</div>
);
};

View File

@@ -0,0 +1,15 @@
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
import { SubmitButton } from "./components/SubmitButton";
import { InputField } from "./components/InputField";
import { SelectField } from "./components/SelectField";
import { CheckboxField } from "./components/CheckBox";
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts();
export const { useAppForm } = createFormHook({
fieldComponents: { InputField, SelectField, CheckboxField },
formComponents: { SubmitButton },
fieldContext,
formContext,
});

View File

@@ -0,0 +1,18 @@
import { useRouter } from "@tanstack/react-router";
import { useEffect } from "react";
import { useSession } from "../authClient";
export function SessionGuard({ children }: { children: React.ReactNode }) {
const { data: session, isLoading } = useSession();
const router = useRouter();
useEffect(() => {
if (!isLoading && !session) {
router.navigate({ to: "/" }); // redirect if not logged in
}
}, [isLoading, session, router]);
if (isLoading) return <div>Checking session</div>;
return <>{children}</>;
}

View File

@@ -0,0 +1,17 @@
import { queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getAuthSession() {
return queryOptions({
queryKey: ["session"],
queryFn: () => fetchSession(),
staleTime: 5000,
refetchOnWindowFocus: true,
});
}
const fetchSession = async () => {
const { data } = await axios.get("/lst/api/me");
return data;
};

View File

@@ -3,15 +3,29 @@ import ReactDOM from "react-dom/client";
import "./index.css"; import "./index.css";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Import the generated route tree // Import the generated route tree
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
// Create a client // Create a client
const queryClient = new QueryClient(); const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: 0,
refetchOnWindowFocus: false,
},
},
});
// Create a new router instance const router = createRouter({
const router = createRouter({ routeTree, basepath: "/lst/app" }); routeTree,
basepath: "/lst/app",
context: {
queryClient: {} as QueryClient,
//login: () => {},
//logout: () => {},
},
});
// Register the router instance for type safety // Register the router instance for type safety
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
@@ -20,10 +34,18 @@ declare module "@tanstack/react-router" {
} }
} }
const App = () => {
return (
<>
<RouterProvider router={router} context={{ queryClient }} />
</>
);
};
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RouterProvider router={router} /> <App />
</QueryClientProvider> </QueryClientProvider>
</StrictMode> </StrictMode>
); );

View File

@@ -9,160 +9,48 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as _adminRouteImport } from './routes/__admin'
import { Route as AdminLayoutRouteRouteImport } from './routes/_adminLayout/route'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AdminSettingsRouteImport } from './routes/admin_/settings' import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as AdminLayoutAdminIndexRouteImport } from './routes/_adminLayout/admin/index'
import { Route as AdminLayoutAdminUsersRouteImport } from './routes/_adminLayout/admin/users'
import { Route as AdminLayoutAdminServersRouteImport } from './routes/_adminLayout/admin/servers'
import { Route as AdminLayoutAdminUserUserIdRouteImport } from './routes/_adminLayout/admin/user/$userId'
import { Route as AdminLayoutAdminServersServerIdRouteImport } from './routes/_adminLayout/admin/servers/$serverId'
import { Route as AdminLayoutAdminServersServerIdEditRouteImport } from './routes/_adminLayout/admin/servers/$serverId_/edit'
const _adminRoute = _adminRouteImport.update({
id: '/__admin',
getParentRoute: () => rootRouteImport,
} as any)
const AdminLayoutRouteRoute = AdminLayoutRouteRouteImport.update({
id: '/_adminLayout',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AdminSettingsRoute = AdminSettingsRouteImport.update({ const authLoginRoute = authLoginRouteImport.update({
id: '/admin_/settings', id: '/(auth)/login',
path: '/admin/settings', path: '/login',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AdminLayoutAdminIndexRoute = AdminLayoutAdminIndexRouteImport.update({
id: '/admin/',
path: '/admin/',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const AdminLayoutAdminUsersRoute = AdminLayoutAdminUsersRouteImport.update({
id: '/admin/users',
path: '/admin/users',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const AdminLayoutAdminServersRoute = AdminLayoutAdminServersRouteImport.update({
id: '/admin/servers',
path: '/admin/servers',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const AdminLayoutAdminUserUserIdRoute =
AdminLayoutAdminUserUserIdRouteImport.update({
id: '/admin/user/$userId',
path: '/admin/user/$userId',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const AdminLayoutAdminServersServerIdRoute =
AdminLayoutAdminServersServerIdRouteImport.update({
id: '/$serverId',
path: '/$serverId',
getParentRoute: () => AdminLayoutAdminServersRoute,
} as any)
const AdminLayoutAdminServersServerIdEditRoute =
AdminLayoutAdminServersServerIdEditRouteImport.update({
id: '/$serverId_/edit',
path: '/$serverId/edit',
getParentRoute: () => AdminLayoutAdminServersRoute,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/admin/settings': typeof AdminSettingsRoute '/login': typeof authLoginRoute
'/admin/servers': typeof AdminLayoutAdminServersRouteWithChildren
'/admin/users': typeof AdminLayoutAdminUsersRoute
'/admin': typeof AdminLayoutAdminIndexRoute
'/admin/servers/$serverId': typeof AdminLayoutAdminServersServerIdRoute
'/admin/user/$userId': typeof AdminLayoutAdminUserUserIdRoute
'/admin/servers/$serverId/edit': typeof AdminLayoutAdminServersServerIdEditRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/admin/settings': typeof AdminSettingsRoute '/login': typeof authLoginRoute
'/admin/servers': typeof AdminLayoutAdminServersRouteWithChildren
'/admin/users': typeof AdminLayoutAdminUsersRoute
'/admin': typeof AdminLayoutAdminIndexRoute
'/admin/servers/$serverId': typeof AdminLayoutAdminServersServerIdRoute
'/admin/user/$userId': typeof AdminLayoutAdminUserUserIdRoute
'/admin/servers/$serverId/edit': typeof AdminLayoutAdminServersServerIdEditRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/_adminLayout': typeof AdminLayoutRouteRouteWithChildren '/(auth)/login': typeof authLoginRoute
'/__admin': typeof _adminRoute
'/admin_/settings': typeof AdminSettingsRoute
'/_adminLayout/admin/servers': typeof AdminLayoutAdminServersRouteWithChildren
'/_adminLayout/admin/users': typeof AdminLayoutAdminUsersRoute
'/_adminLayout/admin/': typeof AdminLayoutAdminIndexRoute
'/_adminLayout/admin/servers/$serverId': typeof AdminLayoutAdminServersServerIdRoute
'/_adminLayout/admin/user/$userId': typeof AdminLayoutAdminUserUserIdRoute
'/_adminLayout/admin/servers/$serverId_/edit': typeof AdminLayoutAdminServersServerIdEditRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths: '/' | '/login'
| '/'
| '/admin/settings'
| '/admin/servers'
| '/admin/users'
| '/admin'
| '/admin/servers/$serverId'
| '/admin/user/$userId'
| '/admin/servers/$serverId/edit'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to: '/' | '/login'
| '/' id: '__root__' | '/' | '/(auth)/login'
| '/admin/settings'
| '/admin/servers'
| '/admin/users'
| '/admin'
| '/admin/servers/$serverId'
| '/admin/user/$userId'
| '/admin/servers/$serverId/edit'
id:
| '__root__'
| '/'
| '/_adminLayout'
| '/__admin'
| '/admin_/settings'
| '/_adminLayout/admin/servers'
| '/_adminLayout/admin/users'
| '/_adminLayout/admin/'
| '/_adminLayout/admin/servers/$serverId'
| '/_adminLayout/admin/user/$userId'
| '/_adminLayout/admin/servers/$serverId_/edit'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AdminLayoutRouteRoute: typeof AdminLayoutRouteRouteWithChildren authLoginRoute: typeof authLoginRoute
_adminRoute: typeof _adminRoute
AdminSettingsRoute: typeof AdminSettingsRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/__admin': {
id: '/__admin'
path: ''
fullPath: ''
preLoaderRoute: typeof _adminRouteImport
parentRoute: typeof rootRouteImport
}
'/_adminLayout': {
id: '/_adminLayout'
path: ''
fullPath: ''
preLoaderRoute: typeof AdminLayoutRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@@ -170,97 +58,19 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/admin_/settings': { '/(auth)/login': {
id: '/admin_/settings' id: '/(auth)/login'
path: '/admin/settings' path: '/login'
fullPath: '/admin/settings' fullPath: '/login'
preLoaderRoute: typeof AdminSettingsRouteImport preLoaderRoute: typeof authLoginRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_adminLayout/admin/': {
id: '/_adminLayout/admin/'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AdminLayoutAdminIndexRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/_adminLayout/admin/users': {
id: '/_adminLayout/admin/users'
path: '/admin/users'
fullPath: '/admin/users'
preLoaderRoute: typeof AdminLayoutAdminUsersRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/_adminLayout/admin/servers': {
id: '/_adminLayout/admin/servers'
path: '/admin/servers'
fullPath: '/admin/servers'
preLoaderRoute: typeof AdminLayoutAdminServersRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/_adminLayout/admin/user/$userId': {
id: '/_adminLayout/admin/user/$userId'
path: '/admin/user/$userId'
fullPath: '/admin/user/$userId'
preLoaderRoute: typeof AdminLayoutAdminUserUserIdRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/_adminLayout/admin/servers/$serverId': {
id: '/_adminLayout/admin/servers/$serverId'
path: '/$serverId'
fullPath: '/admin/servers/$serverId'
preLoaderRoute: typeof AdminLayoutAdminServersServerIdRouteImport
parentRoute: typeof AdminLayoutAdminServersRoute
}
'/_adminLayout/admin/servers/$serverId_/edit': {
id: '/_adminLayout/admin/servers/$serverId_/edit'
path: '/$serverId/edit'
fullPath: '/admin/servers/$serverId/edit'
preLoaderRoute: typeof AdminLayoutAdminServersServerIdEditRouteImport
parentRoute: typeof AdminLayoutAdminServersRoute
}
} }
} }
interface AdminLayoutAdminServersRouteChildren {
AdminLayoutAdminServersServerIdRoute: typeof AdminLayoutAdminServersServerIdRoute
AdminLayoutAdminServersServerIdEditRoute: typeof AdminLayoutAdminServersServerIdEditRoute
}
const AdminLayoutAdminServersRouteChildren: AdminLayoutAdminServersRouteChildren =
{
AdminLayoutAdminServersServerIdRoute: AdminLayoutAdminServersServerIdRoute,
AdminLayoutAdminServersServerIdEditRoute:
AdminLayoutAdminServersServerIdEditRoute,
}
const AdminLayoutAdminServersRouteWithChildren =
AdminLayoutAdminServersRoute._addFileChildren(
AdminLayoutAdminServersRouteChildren,
)
interface AdminLayoutRouteRouteChildren {
AdminLayoutAdminServersRoute: typeof AdminLayoutAdminServersRouteWithChildren
AdminLayoutAdminUsersRoute: typeof AdminLayoutAdminUsersRoute
AdminLayoutAdminIndexRoute: typeof AdminLayoutAdminIndexRoute
AdminLayoutAdminUserUserIdRoute: typeof AdminLayoutAdminUserUserIdRoute
}
const AdminLayoutRouteRouteChildren: AdminLayoutRouteRouteChildren = {
AdminLayoutAdminServersRoute: AdminLayoutAdminServersRouteWithChildren,
AdminLayoutAdminUsersRoute: AdminLayoutAdminUsersRoute,
AdminLayoutAdminIndexRoute: AdminLayoutAdminIndexRoute,
AdminLayoutAdminUserUserIdRoute: AdminLayoutAdminUserUserIdRoute,
}
const AdminLayoutRouteRouteWithChildren =
AdminLayoutRouteRoute._addFileChildren(AdminLayoutRouteRouteChildren)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AdminLayoutRouteRoute: AdminLayoutRouteRouteWithChildren, authLoginRoute: authLoginRoute,
_adminRoute: _adminRoute,
AdminSettingsRoute: AdminSettingsRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View File

@@ -0,0 +1,75 @@
import { useRouter, useSearch } from "@tanstack/react-router";
import { getSession, signin, useAuth } from "../../../lib/authClient";
import { useAppForm } from "../../../lib/formStuff";
import { LstCard } from "../../../components/ui/lstCard";
import { toast } from "sonner";
export default function LoginForm() {
const router = useRouter();
const search = useSearch({ from: "/(auth)/login" });
const { setSession } = useAuth();
const form = useAppForm({
defaultValues: {
username: "",
password: "",
},
onSubmit: async ({ value }) => {
try {
await signin({
username: value.username,
password: value.password,
});
const session = await getSession();
setSession(session);
toast.success(
`Welcome back ${session?.user.name ? session?.user.name : session?.user.username} `
);
router.invalidate();
router.history.push(search.redirect ? search.redirect : "/");
} catch (error) {
console.log(error);
}
},
});
return (
<div className="ml-[25%]">
<LstCard className="p-3 w-96">
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField
name="username"
children={(field) => (
<field.InputField
label="Username"
inputType="string"
required={true}
/>
)}
/>
<form.AppField
name="password"
children={(field) => (
<field.InputField
label="Password"
inputType="password"
required={true}
/>
)}
/>
<div className="flex justify-end mt-2">
<form.AppForm>
<form.SubmitButton>Login</form.SubmitButton>
</form.AppForm>
</div>
</form>
</LstCard>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { z } from "zod";
import { authClient } from "../../lib/authClient";
import LoginForm from "./-components/LoginForm";
export const Route = createFileRoute("/(auth)/login")({
component: RouteComponent,
validateSearch: z.object({
redirect: z.string().optional(),
}),
beforeLoad: async () => {
const result = await authClient.getSession({
query: { disableCookieCache: true }, // force DB/Server lookup
});
//console.log("session check:", result.data);
if (result.data) {
throw redirect({ to: "/" });
}
},
});
function RouteComponent() {
return (
<div>
<LoginForm />
</div>
);
}

View File

@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/__admin')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(__admin)/__admin"!</div>
}

View File

@@ -1,16 +1,32 @@
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import type { QueryClient } from "@tanstack/react-query";
import { Toaster } from "sonner";
import { SessionGuard } from "../lib/providers/SessionProvider";
import Nav from "../components/navBar/Nav";
const RootLayout = () => ( interface RootRouteContext {
<> queryClient: QueryClient;
<nav className="flex gap-1"> //user: User | null;
<Link to="/">Home</Link> //login: (user: User) => void;
{/* <Link to="/admin">Admin</Link> */} //logout: () => void;
</nav> }
<hr></hr>
<Outlet />
<TanStackRouterDevtools />
</>
);
export const Route = createRootRoute({ component: RootLayout }); const RootLayout = () => {
//const { logout, login } = Route.useRouteContext();
return (
<div>
<SessionGuard>
<Nav />
<Outlet />
<Toaster expand={true} richColors closeButton />
<TanStackRouterDevtools />
</SessionGuard>
</div>
);
};
export const Route = createRootRouteWithContext<RootRouteContext>()({
component: RootLayout,
});

View File

@@ -1,23 +0,0 @@
import { Link } from "@tanstack/react-router";
export default function Nav() {
return (
<div>
<nav className="flex gap-1">
<Link
to="/admin"
className="[&.active]:font-bold"
activeOptions={{ exact: true }}
>
Admin
</Link>
<Link to="/admin/servers" className="[&.active]:font-bold">
Servers
</Link>
<Link to="/admin/users" className="[&.active]:font-bold">
Users
</Link>
</nav>
</div>
);
}

View File

@@ -1,8 +0,0 @@
import { getRouteApi } from "@tanstack/react-router";
export default function Server() {
const { useParams } = getRouteApi("/_adminLayout/admin/servers/$serverId");
const { serverId } = useParams();
return <div>server id {serverId}!</div>;
}

View File

@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_adminLayout/admin/')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/_adminLayout/admin/"!</div>
}

View File

@@ -1,27 +0,0 @@
import { createFileRoute, Link } from "@tanstack/react-router";
export const Route = createFileRoute("/_adminLayout/admin/servers")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div>
Hello "/admin/servers"! <br />
<Link
to="/admin/servers/$serverId"
params={{
serverId: "5",
}}
>
Server 5
</Link>
<Link
to="/admin/servers/$serverId"
params={(prev) => ({ ...prev, serverId: "10" })}
>
Server 5
</Link>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import Server from "../../-components/Server";
export const Route = createFileRoute("/_adminLayout/admin/servers/$serverId")({
component: RouteComponent,
});
function RouteComponent() {
return <Server />;
}

View File

@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_adminLayout/admin/servers/$serverId_/edit')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/_adminLayout/admin/_server/$edit"!</div>
}

View File

@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_adminLayout/admin/user/$userId')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/_adminLayout/admin/$userId"!</div>
}

View File

@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_adminLayout/admin/users')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/admin/users"!</div>
}

View File

@@ -1,16 +0,0 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import Nav from "./-components/Nav";
export const Route = createFileRoute("/_adminLayout")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div>
<Nav />
<hr />
<Outlet />
</div>
);
}

View File

@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/admin_/settings')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/admin_/settings"!</div>
}

View File

@@ -1,19 +1,24 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Button } from "../components/ui/button"; import { Button } from "../components/ui/button";
import { useAuth } from "../lib/authClient";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: Index, component: Index,
}); });
function Index() { function Index() {
const { session } = useAuth();
return ( return (
<div> <div>
<div className="h-screen flex items-center justify-center"> <div className="h-screen flex flex-col items-center justify-center">
<Button className="h-96 w-96"> <div>Welcome, {session ? session.user.username : "Guest"}</div>
<a href="/lst/d" target="_blank" className="text-4xl"> <div>
LST-DOCS <Button className="h-96 w-96">
</a> <a href="/lst/d" target="_blank" className="text-4xl">
</Button> LST-DOCS
</a>
</Button>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,6 @@
export type User = {
id?: number;
name?: string;
username: string;
password: string;
};

View File

@@ -35,6 +35,7 @@ export default defineConfig({
}, },
}, },
server: { server: {
port: 5500,
proxy: { proxy: {
"/lst/api": { "/lst/api": {
target: `http://localhost:${Number( target: `http://localhost:${Number(

View File

@@ -19,7 +19,7 @@
"database/testFiles/test-tiPostOrders.ts", "database/testFiles/test-tiPostOrders.ts",
"scripts/translateScript.js", "scripts/translateScript.js",
"app/main.ts", "app/main.ts",
"app/src/types" "types"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",