feat(auth): finally better auth working as i wanted it to
This commit is contained in:
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" });
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user