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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
import { Router } from "express";
import { Router, type Request, type Response } from "express";
import { auth } from "../../../pkg/auth/auth.js";
import { 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),
});

View File

@@ -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,
});
}
}
});

View File

@@ -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);
};

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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;

View File

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