mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-02 17:14:55 +02:00
Add verify middleware
This commit is contained in:
parent
e89ee4042a
commit
a8f944fc78
17 changed files with 1230 additions and 40 deletions
1
server/db/.gitignore
vendored
Normal file
1
server/db/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
names.json
|
|
@ -89,6 +89,16 @@ export const sessions = sqliteTable("session", {
|
|||
expiresAt: integer("expiresAt").notNull(),
|
||||
});
|
||||
|
||||
export const userOrgs = sqliteTable("userOrgs", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
orgId: integer("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId),
|
||||
role: text("role").notNull(), // e.g., 'admin', 'member', etc.
|
||||
});
|
||||
|
||||
// Define the model types for type inference
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
|
|
|
@ -73,6 +73,8 @@ declare global {
|
|||
namespace Express {
|
||||
interface Request {
|
||||
user?: User;
|
||||
userOrgRole?: string;
|
||||
userOrgs?: number[];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
33
server/routers/auth/getUserOrgs.ts
Normal file
33
server/routers/auth/getUserOrgs.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { db } from '@server/db';
|
||||
import { userOrgs, orgs } from '@server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import createHttpError from 'http-errors';
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
|
||||
export async function getUserOrgs(req: Request, res: Response, next: NextFunction) {
|
||||
const userId = req.user?.id; // Assuming you have user information in the request
|
||||
|
||||
if (!userId) {
|
||||
return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
|
||||
}
|
||||
|
||||
try {
|
||||
const userOrganizations = await db.select({
|
||||
orgId: userOrgs.orgId,
|
||||
role: userOrgs.role,
|
||||
})
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.userId, userId));
|
||||
|
||||
req.userOrgs = userOrganizations.map(org => org.orgId);
|
||||
// req.userOrgRoles = userOrganizations.reduce((acc, org) => {
|
||||
// acc[org.orgId] = org.role;
|
||||
// return acc;
|
||||
// }, {} as Record<number, string>);
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error retrieving user organizations'));
|
||||
}
|
||||
}
|
|
@ -4,3 +4,8 @@ export * from "./logout";
|
|||
export * from "./verifyTotp";
|
||||
export * from "./requestTotpSecret";
|
||||
export * from "./disable2fa";
|
||||
export * from "./verifyOrgAccess";
|
||||
export * from "./getUserOrgs";
|
||||
export * from "./verifySiteAccess";
|
||||
export * from "./verifyResourceAccess";
|
||||
export * from "./verifyTargetAccess";
|
36
server/routers/auth/verifyOrgAccess.ts
Normal file
36
server/routers/auth/verifyOrgAccess.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { db } from '@server/db';
|
||||
import { userOrgs } from '@server/db/schema';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import createHttpError from 'http-errors';
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
import { AuthenticatedRequest } from '@server/types/Auth';
|
||||
|
||||
export function verifyOrgAccess(req: Request, res: Response, next: NextFunction) {
|
||||
const userId = req.user.id; // Assuming you have user information in the request
|
||||
const orgId = parseInt(req.params.orgId);
|
||||
|
||||
if (!userId) {
|
||||
return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
|
||||
}
|
||||
|
||||
if (isNaN(orgId)) {
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid organization ID'));
|
||||
}
|
||||
|
||||
db.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.then((result) => {
|
||||
if (result.length === 0) {
|
||||
next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
|
||||
} else {
|
||||
// User has access, attach the user's role to the request for potential future use
|
||||
req.userOrgRole = result[0].role;
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying organization access'));
|
||||
});
|
||||
}
|
54
server/routers/auth/verifyResourceAccess.ts
Normal file
54
server/routers/auth/verifyResourceAccess.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { db } from '@server/db';
|
||||
import { resources, userOrgs } from '@server/db/schema';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import createHttpError from 'http-errors';
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
|
||||
export async function verifyResourceAccess(req: Request, res: Response, next: NextFunction) {
|
||||
const userId = req.user!.id; // Assuming you have user information in the request
|
||||
const resourceId = req.params.resourceId;
|
||||
|
||||
if (!userId) {
|
||||
return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
|
||||
}
|
||||
|
||||
const resource = await db.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (resource.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource[0].orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`resource with ID ${resourceId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
db.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, resource[0].orgId)))
|
||||
.then((result) => {
|
||||
if (result.length === 0) {
|
||||
next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
|
||||
} else {
|
||||
// User has access, attach the user's role to the request for potential future use
|
||||
req.userOrgRole = result[0].role;
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying organization access'));
|
||||
});
|
||||
}
|
58
server/routers/auth/verifySiteAccess.ts
Normal file
58
server/routers/auth/verifySiteAccess.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { db } from '@server/db';
|
||||
import { sites, userOrgs } from '@server/db/schema';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import createHttpError from 'http-errors';
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
|
||||
export async function verifySiteAccess(req: Request, res: Response, next: NextFunction) {
|
||||
const userId = req.user!.id; // Assuming you have user information in the request
|
||||
const siteId = parseInt(req.params.siteId);
|
||||
|
||||
if (!userId) {
|
||||
return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
|
||||
}
|
||||
|
||||
if (isNaN(siteId)) {
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid organization ID'));
|
||||
}
|
||||
|
||||
const site = await db.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (site.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with ID ${siteId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!site[0].orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`Site with ID ${siteId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
db.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, site[0].orgId)))
|
||||
.then((result) => {
|
||||
if (result.length === 0) {
|
||||
next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
|
||||
} else {
|
||||
// User has access, attach the user's role to the request for potential future use
|
||||
req.userOrgRole = result[0].role;
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying organization access'));
|
||||
});
|
||||
}
|
83
server/routers/auth/verifyTargetAccess.ts
Normal file
83
server/routers/auth/verifyTargetAccess.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { db } from '@server/db';
|
||||
import { resources, targets, userOrgs } from '@server/db/schema';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import createHttpError from 'http-errors';
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
|
||||
export async function verifyTargetAccess(req: Request, res: Response, next: NextFunction) {
|
||||
const userId = req.user!.id; // Assuming you have user information in the request
|
||||
const targetId = parseInt(req.params.targetId);
|
||||
|
||||
if (!userId) {
|
||||
return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
|
||||
}
|
||||
|
||||
if (isNaN(targetId)) {
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid organization ID'));
|
||||
}
|
||||
|
||||
const target = await db.select()
|
||||
.from(targets)
|
||||
.where(eq(targets.targetId, targetId))
|
||||
.limit(1);
|
||||
|
||||
if (target.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`target with ID ${targetId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const resourceId = target[0].resourceId;
|
||||
|
||||
if (resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`target with ID ${targetId} does not have a resource ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const resource = await db.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId!))
|
||||
.limit(1);
|
||||
|
||||
if (resource.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource[0].orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
`resource with ID ${resourceId} does not have an organization ID`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
db.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, resource[0].orgId)))
|
||||
.then((result) => {
|
||||
if (result.length === 0) {
|
||||
next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
|
||||
} else {
|
||||
// User has access, attach the user's role to the request for potential future use
|
||||
req.userOrgRole = result[0].role;
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying organization access'));
|
||||
});
|
||||
}
|
|
@ -7,6 +7,7 @@ import * as user from "./user";
|
|||
import * as auth from "./auth";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { verifySessionMiddleware } from "@server/middlewares";
|
||||
import { verifyOrgAccess, getUserOrgs, verifySiteAccess, verifyResourceAccess, verifyTargetAccess } from "./auth";
|
||||
|
||||
// Root routes
|
||||
export const unauthenticated = Router();
|
||||
|
@ -19,30 +20,30 @@ unauthenticated.get("/", (_, res) => {
|
|||
export const authenticated = Router();
|
||||
authenticated.use(verifySessionMiddleware);
|
||||
|
||||
authenticated.put("/org", org.createOrg);
|
||||
authenticated.get("/orgs", org.listOrgs);
|
||||
authenticated.get("/org/:orgId", org.getOrg);
|
||||
authenticated.post("/org/:orgId", org.updateOrg);
|
||||
authenticated.delete("/org/:orgId", org.deleteOrg);
|
||||
authenticated.put("/org", getUserOrgs, org.createOrg);
|
||||
authenticated.get("/orgs", getUserOrgs, org.listOrgs); // TODO we need to check the orgs here
|
||||
authenticated.get("/org/:orgId", verifyOrgAccess, org.getOrg);
|
||||
authenticated.post("/org/:orgId", verifyOrgAccess, org.updateOrg);
|
||||
authenticated.delete("/org/:orgId", verifyOrgAccess, org.deleteOrg);
|
||||
|
||||
authenticated.put("/org/:orgId/site", site.createSite);
|
||||
authenticated.get("/org/:orgId/sites", site.listSites);
|
||||
authenticated.get("/site/:siteId", site.getSite);
|
||||
authenticated.post("/site/:siteId", site.updateSite);
|
||||
authenticated.delete("/site/:siteId", site.deleteSite);
|
||||
authenticated.put("/org/:orgId/site", verifyOrgAccess, site.createSite);
|
||||
authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
|
||||
authenticated.get("/site/:siteId", verifySiteAccess, site.getSite);
|
||||
authenticated.post("/site/:siteId", verifySiteAccess, site.updateSite);
|
||||
authenticated.delete("/site/:siteId", verifySiteAccess, site.deleteSite);
|
||||
|
||||
authenticated.put("/org/:orgId/site/:siteId/resource", resource.createResource);
|
||||
authenticated.put("/org/:orgId/site/:siteId/resource", verifyOrgAccess, resource.createResource);
|
||||
authenticated.get("/site/:siteId/resources", resource.listResources);
|
||||
authenticated.get("/org/:orgId/resources", resource.listResources);
|
||||
authenticated.get("/resource/:resourceId", resource.getResource);
|
||||
authenticated.post("/resource/:resourceId", resource.updateResource);
|
||||
authenticated.delete("/resource/:resourceId", resource.deleteResource);
|
||||
authenticated.get("/org/:orgId/resources", verifyOrgAccess, resource.listResources);
|
||||
authenticated.get("/resource/:resourceId", verifyResourceAccess, resource.getResource);
|
||||
authenticated.post("/resource/:resourceId", verifyResourceAccess, resource.updateResource);
|
||||
authenticated.delete("/resource/:resourceId", verifyResourceAccess, resource.deleteResource);
|
||||
|
||||
authenticated.put("/resource/:resourceId/target", target.createTarget);
|
||||
authenticated.get("/resource/:resourceId/targets", target.listTargets);
|
||||
authenticated.get("/target/:targetId", target.getTarget);
|
||||
authenticated.post("/target/:targetId", target.updateTarget);
|
||||
authenticated.delete("/target/:targetId", target.deleteTarget);
|
||||
authenticated.put("/resource/:resourceId/target", verifyResourceAccess, target.createTarget);
|
||||
authenticated.get("/resource/:resourceId/targets", verifyResourceAccess, target.listTargets);
|
||||
authenticated.get("/target/:targetId", verifyTargetAccess, target.getTarget);
|
||||
authenticated.post("/target/:targetId", verifyTargetAccess, target.updateTarget);
|
||||
authenticated.delete("/target/:targetId", verifyTargetAccess, target.deleteTarget);
|
||||
|
||||
authenticated.get("/users", user.listUsers);
|
||||
// authenticated.get("/org/:orgId/users", user.???); // TODO: Implement this
|
||||
|
|
|
@ -11,6 +11,8 @@ const createOrgSchema = z.object({
|
|||
domain: z.string().min(1).max(255),
|
||||
});
|
||||
|
||||
const MAX_ORGS = 5;
|
||||
|
||||
export async function createOrg(req: Request, res: Response, next: NextFunction): Promise<any> {
|
||||
try {
|
||||
const parsedBody = createOrgSchema.safeParse(req.body);
|
||||
|
@ -23,6 +25,16 @@ export async function createOrg(req: Request, res: Response, next: NextFunction)
|
|||
);
|
||||
}
|
||||
|
||||
const userOrgIds = req.userOrgs;
|
||||
if (userOrgIds && userOrgIds.length > MAX_ORGS) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Maximum number of organizations reached.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { name, domain } = parsedBody.data;
|
||||
|
||||
const newOrg = await db.insert(orgs).values({
|
||||
|
|
|
@ -5,7 +5,7 @@ import { orgs } from '@server/db/schema';
|
|||
import response from "@server/utils/response";
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
import createHttpError from 'http-errors';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { sql, inArray } from 'drizzle-orm';
|
||||
|
||||
const listOrgsSchema = z.object({
|
||||
limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)),
|
||||
|
@ -26,15 +26,45 @@ export async function listOrgs(req: Request, res: Response, next: NextFunction):
|
|||
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
// Use the userOrgs passed from the middleware
|
||||
const userOrgIds = req.userOrgs;
|
||||
|
||||
if (!userOrgIds || userOrgIds.length === 0) {
|
||||
return res.status(HttpCode.OK).send(
|
||||
response(res, {
|
||||
data: {
|
||||
organizations: [],
|
||||
pagination: {
|
||||
total: 0,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "No organizations found for the user",
|
||||
status: HttpCode.OK,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const organizations = await db.select()
|
||||
.from(orgs)
|
||||
.where(inArray(orgs.orgId, userOrgIds))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
const totalCountResult = await db.select({ count: sql<number>`cast(count(*) as integer)` })
|
||||
.from(orgs);
|
||||
.from(orgs)
|
||||
.where(inArray(orgs.orgId, userOrgIds));
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
// // Add the user's role for each organization
|
||||
// const organizationsWithRoles = organizations.map(org => ({
|
||||
// ...org,
|
||||
// userRole: req.userOrgRoles[org.orgId],
|
||||
// }));
|
||||
|
||||
return res.status(HttpCode.OK).send(
|
||||
response(res, {
|
||||
data: {
|
||||
|
|
|
@ -5,6 +5,9 @@ import { sites } from '@server/db/schema';
|
|||
import response from "@server/utils/response";
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
import createHttpError from 'http-errors';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const API_BASE_URL = "http://localhost:3000";
|
||||
|
||||
const createSiteParamsSchema = z.object({
|
||||
orgId: z.number().int().positive(),
|
||||
|
@ -67,4 +70,29 @@ export async function createSite(req: Request, res: Response, next: NextFunction
|
|||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function addPeer(peer: string) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/peer`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(peer),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: any = await response.json();
|
||||
console.log('Peer added successfully:', data.status);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error('Error adding peer:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ import response from "@server/utils/response";
|
|||
import HttpCode from '@server/types/HttpCode';
|
||||
import createHttpError from 'http-errors';
|
||||
|
||||
|
||||
const API_BASE_URL = "http://localhost:3000";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const deleteSiteSchema = z.object({
|
||||
siteId: z.string().transform(Number).pipe(z.number().int().positive())
|
||||
|
@ -54,3 +57,23 @@ export async function deleteSite(req: Request, res: Response, next: NextFunction
|
|||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function removePeer(publicKey: string) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/peer?public_key=${encodeURIComponent(publicKey)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Peer removed successfully:', data.status);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error('Error removing peer:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,4 +5,5 @@ import { Session } from "lucia";
|
|||
export interface AuthenticatedRequest extends Request {
|
||||
user: User;
|
||||
session: Session;
|
||||
userOrgRole?: string;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue