This commit is contained in:
Milo Schwartz 2024-10-02 23:39:14 -04:00
commit 7dbf4307e7
No known key found for this signature in database
9 changed files with 198 additions and 60 deletions

View file

@ -30,26 +30,20 @@ async function insertDummyData() {
.get(); .get();
// Insert dummy users // Insert dummy users
await db.insert(users).values([ // await db.insert(users).values([
{ // {
orgId: org1.orgId, // email: "john@fossorial.com",
name: "John Doe", // groups: "admin,developer",
email: "john@fossorial.com", // },
groups: "admin,developer", // {
}, // email: "jane@fossorial.com",
{ // groups: "developer",
orgId: org1.orgId, // },
name: "Jane Smith", // {
email: "jane@fossorial.com", // email: "bob@fosrl.io",
groups: "developer", // groups: "admin",
}, // },
{ // ]);
orgId: org2.orgId,
name: "Bob Johnson",
email: "bob@fosrl.io",
groups: "admin",
},
]);
// Insert dummy exit nodes // Insert dummy exit nodes
const exitNode1 = db const exitNode1 = db
@ -107,6 +101,7 @@ async function insertDummyData() {
.values({ .values({
resourceId: `web.${site1.subdomain}.${org1.domain}`, resourceId: `web.${site1.subdomain}.${org1.domain}`,
siteId: site1.siteId, siteId: site1.siteId,
orgId: site1.orgId,
name: "Web Server", name: "Web Server",
subdomain: "web", subdomain: "web",
}) })
@ -118,6 +113,7 @@ async function insertDummyData() {
.values({ .values({
resourceId: `web2.${site1.subdomain}.${org1.domain}`, resourceId: `web2.${site1.subdomain}.${org1.domain}`,
siteId: site1.siteId, siteId: site1.siteId,
orgId: site1.orgId,
name: "Web Server 2", name: "Web Server 2",
subdomain: "web2", subdomain: "web2",
}) })
@ -129,6 +125,7 @@ async function insertDummyData() {
.values({ .values({
resourceId: `db.${site2.subdomain}.${org2.domain}`, resourceId: `db.${site2.subdomain}.${org2.domain}`,
siteId: site2.siteId, siteId: site2.siteId,
orgId: site2.orgId,
name: "Database", name: "Database",
subdomain: "db", subdomain: "db",
}) })

View file

@ -31,6 +31,9 @@ export const resources = sqliteTable("resources", {
siteId: integer("siteId").references(() => sites.siteId, { siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
orgId: integer("orgId").references(() => orgs.orgId, {
onDelete: "cascade",
}),
name: text("name").notNull(), name: text("name").notNull(),
subdomain: text("subdomain"), subdomain: text("subdomain"),
}); });

View file

@ -17,32 +17,34 @@ unauthenticated.get("/", (_, res) => {
// Authenticated Root routes // Authenticated Root routes
export const authenticated = Router(); export const authenticated = Router();
authenticated.put("/site", site.createSite);
authenticated.get("/site/:siteId", site.getSite);
authenticated.get("/sites", site.listSites);
authenticated.post("/site/:siteId", site.updateSite);
authenticated.delete("/site/:siteId", site.deleteSite);
authenticated.put("/org", org.createOrg); authenticated.put("/org", org.createOrg);
authenticated.get("/org/:orgId", org.getOrg);
authenticated.get("/orgs", org.listOrgs); authenticated.get("/orgs", org.listOrgs);
authenticated.get("/org/:orgId", org.getOrg);
authenticated.post("/org/:orgId", org.updateOrg); authenticated.post("/org/:orgId", org.updateOrg);
authenticated.delete("/org/:orgId", org.deleteOrg); authenticated.delete("/org/:orgId", org.deleteOrg);
authenticated.put("/resource", resource.createResource); 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/:siteId/resource", resource.createResource);
authenticated.get("/site/:siteId/resources", resource.listResources);
authenticated.get("/org/:orgId/resources", resource.listResources);
authenticated.get("/resource/:resourceId", resource.getResource); authenticated.get("/resource/:resourceId", resource.getResource);
authenticated.get("/resources", resource.listResources);
authenticated.post("/resource/:resourceId", resource.updateResource); authenticated.post("/resource/:resourceId", resource.updateResource);
authenticated.delete("/resource/:resourceId", resource.deleteResource); authenticated.delete("/resource/:resourceId", resource.deleteResource);
authenticated.put("/target", target.createTarget); authenticated.put("/resource/:resourceId/target", target.createTarget);
authenticated.get("/resource/:resourceId/targets", target.listTargets);
authenticated.get("/target/:targetId", target.getTarget); authenticated.get("/target/:targetId", target.getTarget);
authenticated.get("/targets", target.listTargets);
authenticated.post("/target/:targetId", target.updateTarget); authenticated.post("/target/:targetId", target.updateTarget);
authenticated.delete("/target/:targetId", target.deleteTarget); authenticated.delete("/target/:targetId", target.deleteTarget);
authenticated.get("/user/:userId", user.getUser);
authenticated.get("/users", user.listUsers); authenticated.get("/users", user.listUsers);
// authenticated.get("/org/:orgId/users", user.???); // TODO: Implement this
authenticated.get("/user/:userId", user.getUser);
authenticated.delete("/user/:userId", user.deleteUser); authenticated.delete("/user/:userId", user.deleteUser);
// Auth routes // Auth routes

View file

@ -6,9 +6,13 @@ import response from "@server/utils/response";
import HttpCode from '@server/types/HttpCode'; import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
const createResourceParamsSchema = z.object({
siteId: z.number().int().positive(),
orgId: z.number().int().positive(),
});
// Define Zod schema for request body validation // Define Zod schema for request body validation
const createResourceSchema = z.object({ const createResourceSchema = z.object({
siteId: z.number().int().positive(),
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
subdomain: z.string().min(1).max(255).optional(), subdomain: z.string().min(1).max(255).optional(),
}); });
@ -26,7 +30,20 @@ export async function createResource(req: Request, res: Response, next: NextFunc
); );
} }
const { siteId, name, subdomain } = parsedBody.data; const { name, subdomain } = parsedBody.data;
// Validate request params
const parsedParams = createResourceParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
}
const { siteId, orgId } = parsedParams.data;
// Generate a unique resourceId // Generate a unique resourceId
const resourceId = "subdomain" // TODO: create the subdomain here const resourceId = "subdomain" // TODO: create the subdomain here
@ -35,6 +52,7 @@ export async function createResource(req: Request, res: Response, next: NextFunc
const newResource = await db.insert(resources).values({ const newResource = await db.insert(resources).values({
resourceId, resourceId,
siteId, siteId,
orgId,
name, name,
subdomain, subdomain,
}).returning(); }).returning();

View file

@ -7,10 +7,16 @@ import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
import { sql, eq } from 'drizzle-orm'; import { sql, eq } from 'drizzle-orm';
const listResourcesParamsSchema = z.object({
siteId: z.coerce.number().int().positive().optional(),
orgId: z.coerce.number().int().positive().optional(),
}).refine(data => !!data.siteId !== !!data.orgId, {
message: "Either siteId or orgId must be provided, but not both",
});
const listResourcesSchema = z.object({ const listResourcesSchema = z.object({
limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)), limit: z.coerce.number().int().positive().default(10),
offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)), offset: z.coerce.number().int().nonnegative().default(0),
siteId: z.string().optional().transform(Number).pipe(z.number().int().positive()),
}); });
export async function listResources(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listResources(req: Request, res: Response, next: NextFunction): Promise<any> {
@ -24,10 +30,20 @@ export async function listResources(req: Request, res: Response, next: NextFunct
) )
); );
} }
const { limit, offset } = parsedQuery.data;
const { limit, offset, siteId } = parsedQuery.data; const parsedParams = listResourcesParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
}
const { siteId, orgId } = parsedParams.data;
let baseQuery = db let baseQuery: any = db
.select({ .select({
resourceId: resources.resourceId, resourceId: resources.resourceId,
name: resources.name, name: resources.name,
@ -37,11 +53,14 @@ export async function listResources(req: Request, res: Response, next: NextFunct
.from(resources) .from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId)); .leftJoin(sites, eq(resources.siteId, sites.siteId));
let countQuery = db.select({ count: sql<number>`cast(count(*) as integer)` }).from(resources); let countQuery: any = db.select({ count: sql<number>`cast(count(*) as integer)` }).from(resources);
if (siteId) { if (siteId) {
baseQuery = baseQuery.where(eq(resources.siteId, siteId)); baseQuery = baseQuery.where(eq(resources.siteId, siteId));
countQuery = countQuery.where(eq(resources.siteId, siteId)); countQuery = countQuery.where(eq(resources.siteId, siteId));
} else if (orgId) {
baseQuery = baseQuery.where(eq(resources.orgId, orgId));
countQuery = countQuery.where(eq(resources.orgId, orgId));
} }
const resourcesList = await baseQuery.limit(limit).offset(offset); const resourcesList = await baseQuery.limit(limit).offset(offset);

View file

@ -1,19 +1,70 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { db } from '@server/db';
import { sites } from '@server/db/schema';
import response from "@server/utils/response"; import response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors';
// define zod type here const createSiteParamsSchema = z.object({
orgId: z.number().int().positive(),
});
export async function createSite( // Define Zod schema for request body validation
req: Request, const createSiteSchema = z.object({
res: Response, name: z.string().min(1).max(255),
next: NextFunction, subdomain: z.string().min(1).max(255).optional(),
): Promise<any> { pubKey: z.string().optional(),
return response<null>(res, { subnet: z.string().optional(),
data: null, });
export async function createSite(req: Request, res: Response, next: NextFunction): Promise<any> {
try {
// Validate request body
const parsedBody = createSiteSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ')
)
);
}
const { name, subdomain, pubKey, subnet } = parsedBody.data;
// Validate request params
const parsedParams = createSiteParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
}
const { orgId } = parsedParams.data;
// Create new site in the database
const newSite = await db.insert(sites).values({
orgId,
name,
subdomain,
pubKey,
subnet,
}).returning();
return res.status(HttpCode.CREATED).send(
response(res, {
data: newSite[0],
success: true, success: true,
error: false, error: false,
message: "Logged in successfully", message: "Site created successfully",
status: HttpCode.OK, status: HttpCode.CREATED,
}); })
);
} catch (error) {
next(error);
}
} }

View file

@ -7,10 +7,13 @@ import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
import { sql, eq } from 'drizzle-orm'; import { sql, eq } from 'drizzle-orm';
const listSitesParamsSchema = z.object({
orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()),
});
const listSitesSchema = z.object({ const listSitesSchema = z.object({
limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)), limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)),
offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)), offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)),
orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()),
}); });
export async function listSites(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listSites(req: Request, res: Response, next: NextFunction): Promise<any> {
@ -25,7 +28,19 @@ export async function listSites(req: Request, res: Response, next: NextFunction)
); );
} }
const { limit, offset, orgId } = parsedQuery.data; const { limit, offset } = parsedQuery.data;
const parsedParams = listSitesParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
}
const { orgId } = parsedParams.data;
let baseQuery: any = db let baseQuery: any = db
.select({ .select({

View file

@ -6,8 +6,11 @@ import response from "@server/utils/response";
import HttpCode from '@server/types/HttpCode'; import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
const createTargetSchema = z.object({ const createTargetParamsSchema = z.object({
resourceId: z.string().uuid(), resourceId: z.string().uuid(),
});
const createTargetSchema = z.object({
ip: z.string().ip(), ip: z.string().ip(),
method: z.string().min(1).max(10), method: z.string().min(1).max(10),
port: z.number().int().min(1).max(65535), port: z.number().int().min(1).max(65535),
@ -29,7 +32,22 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
const targetData = parsedBody.data; const targetData = parsedBody.data;
const newTarget = await db.insert(targets).values(targetData).returning(); const parsedParams = createTargetParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
}
const { resourceId } = parsedParams.data;
const newTarget = await db.insert(targets).values({
resourceId,
...targetData
}).returning();
return res.status(HttpCode.CREATED).send( return res.status(HttpCode.CREATED).send(
response(res, { response(res, {

View file

@ -7,10 +7,13 @@ import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
import { sql, eq } from 'drizzle-orm'; import { sql, eq } from 'drizzle-orm';
const listTargetsParamsSchema = z.object({
resourceId: z.string().optional()
});
const listTargetsSchema = z.object({ const listTargetsSchema = z.object({
limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)), limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)),
offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)), offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)),
resourceId: z.string().optional(),
}); });
export async function listTargets(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listTargets(req: Request, res: Response, next: NextFunction): Promise<any> {
@ -25,7 +28,19 @@ export async function listTargets(req: Request, res: Response, next: NextFunctio
); );
} }
const { limit, offset, resourceId } = parsedQuery.data; const { limit, offset } = parsedQuery.data;
const parsedParams = listTargetsParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
}
const { resourceId } = parsedParams.data;
let baseQuery: any = db let baseQuery: any = db
.select({ .select({