Add verify middleware

This commit is contained in:
Owen Schwartz 2024-10-03 22:31:20 -04:00
parent e89ee4042a
commit a8f944fc78
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
17 changed files with 1230 additions and 40 deletions

1
server/db/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
names.json

View file

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

View file

@ -73,6 +73,8 @@ declare global {
namespace Express {
interface Request {
user?: User;
userOrgRole?: string;
userOrgs?: number[];
}
}
}

View 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'));
}
}

View file

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

View 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'));
});
}

View 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'));
});
}

View 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'));
});
}

View 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'));
});
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,4 +5,5 @@ import { Session } from "lucia";
export interface AuthenticatedRequest extends Request {
user: User;
session: Session;
userOrgRole?: string;
}