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());
|
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
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
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 { 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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
1138
frontend/package-lock.json
generated
1138
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,19 +10,26 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@tanstack/react-form": "^1.23.0",
|
||||||
"@tanstack/react-query": "^5.89.0",
|
"@tanstack/react-query": "^5.89.0",
|
||||||
"@tanstack/react-router": "^1.131.36",
|
"@tanstack/react-router": "^1.131.36",
|
||||||
"@tanstack/react-router-devtools": "^1.131.36",
|
"@tanstack/react-router-devtools": "^1.131.36",
|
||||||
|
"axios": "^1.12.2",
|
||||||
"better-auth": "^1.3.11",
|
"better-auth": "^1.3.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.542.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.13"
|
"tailwindcss": "^4.1.13",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
|
|||||||
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 { 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;
|
||||||
|
}
|
||||||
|
|||||||
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 "./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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
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 { 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>
|
|
||||||
|
const RootLayout = () => {
|
||||||
|
//const { logout, login } = Route.useRouteContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SessionGuard>
|
||||||
|
<Nav />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
<Toaster expand={true} richColors closeButton />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</>
|
</SessionGuard>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Route = createRootRoute({ component: RootLayout });
|
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,14 +1,18 @@
|
|||||||
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">
|
||||||
|
<div>Welcome, {session ? session.user.username : "Guest"}</div>
|
||||||
|
<div>
|
||||||
<Button className="h-96 w-96">
|
<Button className="h-96 w-96">
|
||||||
<a href="/lst/d" target="_blank" className="text-4xl">
|
<a href="/lst/d" target="_blank" className="text-4xl">
|
||||||
LST-DOCS
|
LST-DOCS
|
||||||
@@ -16,5 +20,6 @@ function Index() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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: {
|
server: {
|
||||||
|
port: 5500,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/lst/api": {
|
"/lst/api": {
|
||||||
target: `http://localhost:${Number(
|
target: `http://localhost:${Number(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user