feat(auth): finally better auth working as i wanted it to
This commit is contained in:
@@ -78,8 +78,9 @@ const main = async () => {
|
||||
app.use(express.json());
|
||||
|
||||
const allowedOrigins = [
|
||||
"http://localhost:5173", // dev
|
||||
"http://localhost:4200",
|
||||
"http://localhost:5173", // lstV2 dev
|
||||
"http://localhost:5500", // lst dev
|
||||
"http://localhost:4200", // express
|
||||
env.BETTER_AUTH_URL, // prod
|
||||
];
|
||||
|
||||
|
||||
75
app/src/internal/admin/controller/systemAdminRole.ts
Normal file
75
app/src/internal/admin/controller/systemAdminRole.ts
Normal 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");
|
||||
};
|
||||
19
app/src/internal/admin/routes.ts
Normal file
19
app/src/internal/admin/routes.ts
Normal 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
|
||||
);
|
||||
};
|
||||
52
app/src/internal/admin/routes/getUserRoles.ts
Normal file
52
app/src/internal/admin/routes/getUserRoles.ts
Normal 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;
|
||||
74
app/src/internal/admin/routes/grantRole.ts
Normal file
74
app/src/internal/admin/routes/grantRole.ts
Normal 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;
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { auth } from "../../../pkg/auth/auth.js";
|
||||
import { fromNodeHeaders } from "better-auth/node";
|
||||
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /health
|
||||
router.get("/", async (req, res) => {
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(req.headers),
|
||||
});
|
||||
|
||||
@@ -2,6 +2,11 @@ import { Router } from "express";
|
||||
import type { Request, Response } from "express";
|
||||
import z from "zod";
|
||||
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();
|
||||
|
||||
@@ -18,6 +23,9 @@ const registerSchema = z.object({
|
||||
});
|
||||
|
||||
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 {
|
||||
// Parse + validate incoming JSON against Zod schema
|
||||
const validated = registerSchema.parse(req.body);
|
||||
@@ -27,6 +35,9 @@ router.post("/", async (req: Request, res: Response) => {
|
||||
body: validated,
|
||||
});
|
||||
|
||||
if (totalUsers[0].count === 0) {
|
||||
systemAdminRole(user.user.id);
|
||||
}
|
||||
return res.status(201).json(user);
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
@@ -36,8 +47,14 @@ router.post("/", async (req: Request, res: Response) => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Express, Request, Response } from "express";
|
||||
import me from "./me.js";
|
||||
import register from "./register.js";
|
||||
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
|
||||
|
||||
export const setupAuthRoutes = (app: Express, basePath: string) => {
|
||||
app.use(basePath + "/api/me", me);
|
||||
app.use(basePath + "/api/auth/register", register);
|
||||
app.use(basePath + "/api/user/me", requireAuth(), me);
|
||||
app.use(basePath + "/api/user/register", register);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Express, Request, Response } from "express";
|
||||
|
||||
import healthRoutes from "./routes/healthRoutes.js";
|
||||
import { setupAuthRoutes } from "../auth/routes/routes.js";
|
||||
import { setupAdminRoutes } from "../admin/routes.js";
|
||||
|
||||
export const setupRoutes = (app: Express, basePath: string) => {
|
||||
// Root / health check
|
||||
@@ -9,6 +10,7 @@ export const setupRoutes = (app: Express, basePath: string) => {
|
||||
|
||||
// all routes
|
||||
setupAuthRoutes(app, basePath);
|
||||
setupAdminRoutes(app, basePath);
|
||||
|
||||
// always try to go to the app weather we are in dev or in production.
|
||||
app.get(basePath + "/", (req: Request, res: Response) => {
|
||||
|
||||
@@ -20,20 +20,26 @@ export const auth = betterAuth({
|
||||
provider: "pg",
|
||||
schema,
|
||||
}),
|
||||
trustedOrigins: [
|
||||
"*.alpla.net",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:5500",
|
||||
],
|
||||
appName: "lst",
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
minPasswordLength: 8, // optional config
|
||||
},
|
||||
plugins: [
|
||||
jwt({ jwt: { expirationTime: "1h" } }),
|
||||
//jwt({ jwt: { expirationTime: "1h" } }),
|
||||
apiKey(),
|
||||
admin(),
|
||||
username(),
|
||||
],
|
||||
session: {
|
||||
expiresIn: 60 * 60,
|
||||
updateAge: 60 * 1,
|
||||
updateAge: 60 * 5,
|
||||
freshAge: 60 * 2,
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60, // Cache duration in seconds
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { pgTable, text, uuid, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
import { user } from "./auth-schema.js";
|
||||
|
||||
export const userRole = pgTable(
|
||||
"user_role",
|
||||
export const userRoles = pgTable(
|
||||
"user_roles",
|
||||
{
|
||||
userRoleId: uuid("user_role_id").defaultRandom().primaryKey(),
|
||||
userId: text("user_id")
|
||||
@@ -15,3 +15,4 @@ export const userRole = pgTable(
|
||||
uniqueIndex("unique_user_module").on(table.userId, table.module),
|
||||
]
|
||||
);
|
||||
export type UserRole = typeof userRoles.$inferSelect;
|
||||
|
||||
90
app/src/pkg/middleware/authMiddleware.ts
Normal file
90
app/src/pkg/middleware/authMiddleware.ts
Normal 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
12080
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,50 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"@tanstack/react-router": "^1.131.36",
|
||||
"@tanstack/react-router-devtools": "^1.131.36",
|
||||
"better-auth": "^1.3.11",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@tanstack/router-plugin": "^1.131.36",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react-swc": "^4.0.0",
|
||||
"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"
|
||||
}
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-form": "^1.23.0",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"@tanstack/react-router": "^1.131.36",
|
||||
"@tanstack/react-router-devtools": "^1.131.36",
|
||||
"axios": "^1.12.2",
|
||||
"better-auth": "^1.3.11",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@tanstack/router-plugin": "^1.131.36",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react-swc": "^4.0.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
29
frontend/src/components/navBar/Nav.tsx
Normal file
29
frontend/src/components/navBar/Nav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
92
frontend/src/components/ui/card.tsx
Normal file
92
frontend/src/components/ui/card.tsx
Normal 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,
|
||||
}
|
||||
30
frontend/src/components/ui/checkbox.tsx
Normal file
30
frontend/src/components/ui/checkbox.tsx
Normal 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 }
|
||||
21
frontend/src/components/ui/input.tsx
Normal file
21
frontend/src/components/ui/input.tsx
Normal 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 }
|
||||
22
frontend/src/components/ui/label.tsx
Normal file
22
frontend/src/components/ui/label.tsx
Normal 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 }
|
||||
23
frontend/src/components/ui/lstCard.tsx
Normal file
23
frontend/src/components/ui/lstCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
183
frontend/src/components/ui/select.tsx
Normal file
183
frontend/src/components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
@@ -1,11 +1,139 @@
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { usernameClient } from "better-auth/client/plugins";
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: `${window.location.origin}/lst/api/auth`, // 👈 This is fine
|
||||
callbacks: {
|
||||
onUpdate: (session: any) => console.log("Session updated", session),
|
||||
onSignIn: (session: any) => console.log("Signed in!", session),
|
||||
onSignOut: () => console.log("Signed out!"),
|
||||
import { create } from "zustand";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
// ---- 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()],
|
||||
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;
|
||||
}
|
||||
|
||||
34
frontend/src/lib/formStuff/components/CheckBox.tsx
Normal file
34
frontend/src/lib/formStuff/components/CheckBox.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
16
frontend/src/lib/formStuff/components/FieldErrors.tsx
Normal file
16
frontend/src/lib/formStuff/components/FieldErrors.tsx
Normal 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>
|
||||
));
|
||||
};
|
||||
28
frontend/src/lib/formStuff/components/InputField.tsx
Normal file
28
frontend/src/lib/formStuff/components/InputField.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
57
frontend/src/lib/formStuff/components/SelectField.tsx
Normal file
57
frontend/src/lib/formStuff/components/SelectField.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
24
frontend/src/lib/formStuff/components/SubmitButton.tsx
Normal file
24
frontend/src/lib/formStuff/components/SubmitButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
15
frontend/src/lib/formStuff/index.tsx
Normal file
15
frontend/src/lib/formStuff/index.tsx
Normal 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,
|
||||
});
|
||||
18
frontend/src/lib/providers/SessionProvider.tsx
Normal file
18
frontend/src/lib/providers/SessionProvider.tsx
Normal 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}</>;
|
||||
}
|
||||
17
frontend/src/lib/querys/session.ts
Normal file
17
frontend/src/lib/querys/session.ts
Normal 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;
|
||||
};
|
||||
@@ -3,15 +3,29 @@ import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
// Import the generated route tree
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
// 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({ routeTree, basepath: "/lst/app" });
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
basepath: "/lst/app",
|
||||
context: {
|
||||
queryClient: {} as QueryClient,
|
||||
//login: () => {},
|
||||
//logout: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
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(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
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 AdminSettingsRouteImport } from './routes/admin_/settings'
|
||||
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'
|
||||
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
||||
|
||||
const _adminRoute = _adminRouteImport.update({
|
||||
id: '/__admin',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminLayoutRouteRoute = AdminLayoutRouteRouteImport.update({
|
||||
id: '/_adminLayout',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminSettingsRoute = AdminSettingsRouteImport.update({
|
||||
id: '/admin_/settings',
|
||||
path: '/admin/settings',
|
||||
const authLoginRoute = authLoginRouteImport.update({
|
||||
id: '/(auth)/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} 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 {
|
||||
'/': typeof IndexRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/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
|
||||
'/login': typeof authLoginRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/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
|
||||
'/login': typeof authLoginRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/_adminLayout': typeof AdminLayoutRouteRouteWithChildren
|
||||
'/__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
|
||||
'/(auth)/login': typeof authLoginRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/admin/settings'
|
||||
| '/admin/servers'
|
||||
| '/admin/users'
|
||||
| '/admin'
|
||||
| '/admin/servers/$serverId'
|
||||
| '/admin/user/$userId'
|
||||
| '/admin/servers/$serverId/edit'
|
||||
fullPaths: '/' | '/login'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/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'
|
||||
to: '/' | '/login'
|
||||
id: '__root__' | '/' | '/(auth)/login'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AdminLayoutRouteRoute: typeof AdminLayoutRouteRouteWithChildren
|
||||
_adminRoute: typeof _adminRoute
|
||||
AdminSettingsRoute: typeof AdminSettingsRoute
|
||||
authLoginRoute: typeof authLoginRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
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: '/'
|
||||
path: '/'
|
||||
@@ -170,97 +58,19 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin_/settings': {
|
||||
id: '/admin_/settings'
|
||||
path: '/admin/settings'
|
||||
fullPath: '/admin/settings'
|
||||
preLoaderRoute: typeof AdminSettingsRouteImport
|
||||
'/(auth)/login': {
|
||||
id: '/(auth)/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof authLoginRouteImport
|
||||
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 = {
|
||||
IndexRoute: IndexRoute,
|
||||
AdminLayoutRouteRoute: AdminLayoutRouteRouteWithChildren,
|
||||
_adminRoute: _adminRoute,
|
||||
AdminSettingsRoute: AdminSettingsRoute,
|
||||
authLoginRoute: authLoginRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
75
frontend/src/routes/(auth)/-components/LoginForm.tsx
Normal file
75
frontend/src/routes/(auth)/-components/LoginForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
frontend/src/routes/(auth)/login.tsx
Normal file
30
frontend/src/routes/(auth)/login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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 type { QueryClient } from "@tanstack/react-query";
|
||||
import { Toaster } from "sonner";
|
||||
import { SessionGuard } from "../lib/providers/SessionProvider";
|
||||
import Nav from "../components/navBar/Nav";
|
||||
|
||||
const RootLayout = () => (
|
||||
<>
|
||||
<nav className="flex gap-1">
|
||||
<Link to="/">Home</Link>
|
||||
{/* <Link to="/admin">Admin</Link> */}
|
||||
</nav>
|
||||
<hr></hr>
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools />
|
||||
</>
|
||||
);
|
||||
interface RootRouteContext {
|
||||
queryClient: QueryClient;
|
||||
//user: User | null;
|
||||
//login: (user: User) => void;
|
||||
//logout: () => void;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { useAuth } from "../lib/authClient";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: Index,
|
||||
});
|
||||
|
||||
function Index() {
|
||||
const { session } = useAuth();
|
||||
return (
|
||||
<div>
|
||||
<div className="h-screen flex items-center justify-center">
|
||||
<Button className="h-96 w-96">
|
||||
<a href="/lst/d" target="_blank" className="text-4xl">
|
||||
LST-DOCS
|
||||
</a>
|
||||
</Button>
|
||||
<div className="h-screen flex flex-col items-center justify-center">
|
||||
<div>Welcome, {session ? session.user.username : "Guest"}</div>
|
||||
<div>
|
||||
<Button className="h-96 w-96">
|
||||
<a href="/lst/d" target="_blank" className="text-4xl">
|
||||
LST-DOCS
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
6
frontend/src/types/index.ts
Normal file
6
frontend/src/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type User = {
|
||||
id?: number;
|
||||
name?: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
@@ -35,6 +35,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5500,
|
||||
proxy: {
|
||||
"/lst/api": {
|
||||
target: `http://localhost:${Number(
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"database/testFiles/test-tiPostOrders.ts",
|
||||
"scripts/translateScript.js",
|
||||
"app/main.ts",
|
||||
"app/src/types"
|
||||
"types"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
Reference in New Issue
Block a user