mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-14 08:56:39 +00:00
Merge branch 'main' of https://github.com/fosrl/pangolin
This commit is contained in:
@@ -35,8 +35,7 @@ export enum ActionsEnum {
|
||||
listUsers = "listUsers",
|
||||
listSiteRoles = "listSiteRoles",
|
||||
listResourceRoles = "listResourceRoles",
|
||||
addRoleSite = "addRoleSite",
|
||||
addRoleResource = "addRoleResource",
|
||||
setResourceRoles = "setResourceRoles",
|
||||
removeRoleResource = "removeRoleResource",
|
||||
removeRoleSite = "removeRoleSite",
|
||||
// addRoleAction = "addRoleAction",
|
||||
|
||||
@@ -25,7 +25,6 @@ export const sites = sqliteTable("sites", {
|
||||
|
||||
export const resources = sqliteTable("resources", {
|
||||
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
||||
fullDomain: text("fullDomain", { length: 2048 }),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -33,7 +32,7 @@ export const resources = sqliteTable("resources", {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
name: text("name").notNull(),
|
||||
subdomain: text("subdomain"),
|
||||
subdomain: text("subdomain").notNull(),
|
||||
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { roles, userOrgs } from "@server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const verifyRoleAccessSchema = z.object({
|
||||
roleIds: z.array(z.number().int().positive()).optional(),
|
||||
});
|
||||
|
||||
export async function verifyRoleAccess(
|
||||
req: Request,
|
||||
@@ -12,7 +18,7 @@ export async function verifyRoleAccess(
|
||||
next: NextFunction
|
||||
) {
|
||||
const userId = req.user?.userId;
|
||||
const roleId = parseInt(
|
||||
const singleRoleId = parseInt(
|
||||
req.params.roleId || req.body.roleId || req.query.roleId
|
||||
);
|
||||
|
||||
@@ -22,61 +28,61 @@ export async function verifyRoleAccess(
|
||||
);
|
||||
}
|
||||
|
||||
if (isNaN(roleId)) {
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid role ID"));
|
||||
const parsedBody = verifyRoleAccessSchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { roleIds } = parsedBody.data;
|
||||
const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
|
||||
|
||||
if (allRoleIds.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const role = await db
|
||||
const rolesData = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(eq(roles.roleId, roleId))
|
||||
.limit(1);
|
||||
.where(inArray(roles.roleId, allRoleIds));
|
||||
|
||||
if (role.length === 0) {
|
||||
if (rolesData.length !== allRoleIds.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Role with ID ${roleId} not found`
|
||||
"One or more roles not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
// Check user access to each role's organization
|
||||
for (const role of rolesData) {
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, role[0].orgId!)
|
||||
eq(userOrgs.orgId, role.orgId!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
req.userOrg = userOrgRole[0];
|
||||
}
|
||||
|
||||
if (!req.userOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
if (userOrgRole.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`User does not have access to organization for role ID ${role.roleId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (req.userOrg.orgId !== role[0].orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Role does not belong to the organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
req.userOrgRoleId = req.userOrg.roleId;
|
||||
req.userOrgId = req.userOrg.orgId;
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
logger.error("Error verifying role access:", error);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function verifySiteAccess(
|
||||
req: Request,
|
||||
@@ -28,6 +29,7 @@ export async function verifySiteAccess(
|
||||
}
|
||||
|
||||
if (isNaN(siteId)) {
|
||||
logger.debug(JSON.stringify(req.body));
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID"));
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function verifyUserAccess(
|
||||
req.userOrg = res[0];
|
||||
}
|
||||
|
||||
if (req.userOrg) {
|
||||
if (!req.userOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
verifyTargetAccess,
|
||||
verifyRoleAccess,
|
||||
verifyUserAccess,
|
||||
verifyUserInRole,
|
||||
} from "./auth";
|
||||
import { verifyUserHasAction } from "./auth/verifyUserHasAction";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
@@ -135,12 +136,13 @@ authenticated.post(
|
||||
); // maybe make this /invite/create instead
|
||||
authenticated.post("/invite/accept", user.acceptInvite);
|
||||
|
||||
// authenticated.get(
|
||||
// "/resource/:resourceId/roles",
|
||||
// verifyResourceAccess,
|
||||
// verifyUserHasAction(ActionsEnum.listResourceRoles),
|
||||
// resource.listResourceRoles
|
||||
// );
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/roles",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.listResourceRoles),
|
||||
resource.listResourceRoles
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId",
|
||||
verifyResourceAccess,
|
||||
@@ -251,20 +253,15 @@ authenticated.post(
|
||||
// verifyUserHasAction(ActionsEnum.listRoleSites),
|
||||
// role.listRoleSites
|
||||
// );
|
||||
// authenticated.put(
|
||||
// "/role/:roleId/resource",
|
||||
// verifyRoleAccess,
|
||||
// verifyUserInRole,
|
||||
// verifyUserHasAction(ActionsEnum.addRoleResource),
|
||||
// role.addRoleResource
|
||||
// );
|
||||
// authenticated.delete(
|
||||
// "/role/:roleId/resource",
|
||||
// verifyRoleAccess,
|
||||
// verifyUserInRole,
|
||||
// verifyUserHasAction(ActionsEnum.removeRoleResource),
|
||||
// role.removeRoleResource
|
||||
// );
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/roles",
|
||||
verifyResourceAccess,
|
||||
verifyRoleAccess,
|
||||
verifyUserHasAction(ActionsEnum.setResourceRoles),
|
||||
role.addRoleResource
|
||||
);
|
||||
|
||||
// authenticated.get(
|
||||
// "/role/:roleId/resources",
|
||||
// verifyRoleAccess,
|
||||
@@ -370,7 +367,7 @@ authRouter.use(
|
||||
authRouter.put("/signup", auth.signup);
|
||||
authRouter.post("/login", auth.login);
|
||||
authRouter.post("/logout", auth.logout);
|
||||
authRouter.post('/newt/get-token', getToken);
|
||||
authRouter.post("/newt/get-token", getToken);
|
||||
|
||||
authRouter.post("/2fa/enable", verifySessionUserMiddleware, auth.verifyTotp);
|
||||
authRouter.post(
|
||||
|
||||
@@ -12,11 +12,13 @@ import config from "@server/config";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { defaultRoleAllowedActions } from "../role";
|
||||
|
||||
const createOrgSchema = z.object({
|
||||
orgId: z.string(),
|
||||
name: z.string().min(1).max(255),
|
||||
// domain: z.string().min(1).max(255).optional(),
|
||||
});
|
||||
const createOrgSchema = z
|
||||
.object({
|
||||
orgId: z.string(),
|
||||
name: z.string().min(1).max(255),
|
||||
// domain: z.string().min(1).max(255).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const MAX_ORGS = 5;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ const updateOrgBodySchema = z
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
domain: z.string().min(1).max(255).optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
message: "At least one field must be provided for update",
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import createHttpError from "http-errors";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import stoi from "@server/utils/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||
|
||||
const createResourceParamsSchema = z.object({
|
||||
siteId: z
|
||||
@@ -25,10 +26,12 @@ const createResourceParamsSchema = z.object({
|
||||
orgId: z.string(),
|
||||
});
|
||||
|
||||
const createResourceSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
subdomain: z.string().min(1).max(255).optional(),
|
||||
});
|
||||
const createResourceSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255),
|
||||
subdomain: subdomainSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CreateResourceResponse = Resource;
|
||||
|
||||
@@ -85,12 +88,9 @@ export async function createResource(
|
||||
);
|
||||
}
|
||||
|
||||
const fullDomain = `${subdomain}.${org[0].domain}`;
|
||||
|
||||
const newResource = await db
|
||||
.insert(resources)
|
||||
.values({
|
||||
fullDomain,
|
||||
siteId,
|
||||
orgId,
|
||||
name,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources } from "@server/db/schema";
|
||||
import { Resource, resources } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -12,12 +12,7 @@ const getResourceSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
export type GetResourceResponse = {
|
||||
resourceId: number;
|
||||
siteId: number;
|
||||
orgId: string;
|
||||
name: string;
|
||||
};
|
||||
export type GetResourceResponse = Resource;
|
||||
|
||||
export async function getResource(
|
||||
req: Request,
|
||||
@@ -53,12 +48,7 @@ export async function getResource(
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
resourceId: resource[0].resourceId,
|
||||
siteId: resource[0].siteId,
|
||||
orgId: resource[0].orgId,
|
||||
name: resource[0].name,
|
||||
},
|
||||
data: resource[0],
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource retrieved successfully",
|
||||
|
||||
@@ -13,6 +13,23 @@ const listResourceRolesSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
async function query(resourceId: number) {
|
||||
return await db
|
||||
.select({
|
||||
roleId: roles.roleId,
|
||||
name: roles.name,
|
||||
description: roles.description,
|
||||
isAdmin: roles.isAdmin,
|
||||
})
|
||||
.from(roleResources)
|
||||
.innerJoin(roles, eq(roleResources.roleId, roles.roleId))
|
||||
.where(eq(roleResources.resourceId, resourceId));
|
||||
}
|
||||
|
||||
export type ListResourceRolesResponse = {
|
||||
roles: NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||
};
|
||||
|
||||
export async function listResourceRoles(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -31,19 +48,12 @@ export async function listResourceRoles(
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const resourceRolesList = await db
|
||||
.select({
|
||||
roleId: roles.roleId,
|
||||
name: roles.name,
|
||||
description: roles.description,
|
||||
isAdmin: roles.isAdmin,
|
||||
})
|
||||
.from(roleResources)
|
||||
.innerJoin(roles, eq(roleResources.roleId, roles.roleId))
|
||||
.where(eq(roleResources.resourceId, resourceId));
|
||||
const resourceRolesList = await query(resourceId);
|
||||
|
||||
return response(res, {
|
||||
data: resourceRolesList,
|
||||
return response<ListResourceRolesResponse>(res, {
|
||||
data: {
|
||||
roles: resourceRolesList,
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource roles retrieved successfully",
|
||||
|
||||
111
server/routers/resource/setResourceRoles.ts
Normal file
111
server/routers/resource/setResourceRoles.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roleResources, roles } from "@server/db/schema";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and, ne } from "drizzle-orm";
|
||||
|
||||
const setResourceRolesBodySchema = z.object({
|
||||
roleIds: z.array(z.number().int().positive()),
|
||||
});
|
||||
|
||||
const setResourceRolesParamsSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
export async function addRoleResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = setResourceRolesBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { roleIds } = parsedBody.data;
|
||||
|
||||
const parsedParams = setResourceRolesParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
// get this org's admin role
|
||||
const adminRole = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.name, "Admin"),
|
||||
eq(roles.orgId, req.userOrg!.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!adminRole.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Admin role not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (roleIds.includes(adminRole[0].roleId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Admin role cannot be assigned to resources"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.delete(roleResources).where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
ne(roleResources.roleId, adminRole[0].roleId) // delete all but the admin role
|
||||
)
|
||||
);
|
||||
|
||||
const newRoleResources = await Promise.all(
|
||||
roleIds.map((roleId) =>
|
||||
trx
|
||||
.insert(roleResources)
|
||||
.values({ roleId, resourceId })
|
||||
.returning()
|
||||
)
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Roles set for resource successfully",
|
||||
status: HttpCode.CREATED,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources } from "@server/db/schema";
|
||||
import { resources, sites } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||
|
||||
const updateResourceParamsSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
@@ -16,8 +17,11 @@ const updateResourceParamsSchema = z.object({
|
||||
const updateResourceBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
subdomain: z.string().min(1).max(255).optional(),
|
||||
subdomain: subdomainSchema.optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
// siteId: z.number(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
message: "At least one field must be provided for update",
|
||||
});
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roleResources } from "@server/db/schema";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const addRoleResourceParamsSchema = z.object({
|
||||
roleId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
const addRoleResourceSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
export async function addRoleResource(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = addRoleResourceSchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedBody.data;
|
||||
|
||||
const parsedParams = addRoleResourceParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { roleId } = parsedParams.data;
|
||||
|
||||
const newRoleResource = await db
|
||||
.insert(roleResources)
|
||||
.values({
|
||||
roleId,
|
||||
resourceId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: newRoleResource[0],
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource added to role successfully",
|
||||
status: HttpCode.CREATED,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,12 @@ const createRoleParamsSchema = z.object({
|
||||
orgId: z.string(),
|
||||
});
|
||||
|
||||
const createRoleSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
const createRoleSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const defaultRoleAllowedActions: ActionsEnum[] = [
|
||||
ActionsEnum.getOrg,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roles } from "@server/db/schema";
|
||||
import { roles, userOrgs } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -13,6 +13,10 @@ const deleteRoleSchema = z.object({
|
||||
roleId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
const deelteRoleBodySchema = z.object({
|
||||
roleId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
export async function deleteRole(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -29,7 +33,27 @@ export async function deleteRole(
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = deelteRoleBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { roleId } = parsedParams.data;
|
||||
const { roleId: newRoleId } = parsedBody.data;
|
||||
|
||||
if (roleId === newRoleId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Cannot delete a role and assign the same role`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const role = await db
|
||||
.select()
|
||||
@@ -55,20 +79,30 @@ export async function deleteRole(
|
||||
);
|
||||
}
|
||||
|
||||
const deletedRole = await db
|
||||
.delete(roles)
|
||||
.where(eq(roles.roleId, roleId))
|
||||
.returning();
|
||||
const newRole = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(eq(roles.roleId, newRoleId))
|
||||
.limit(1);
|
||||
|
||||
if (deletedRole.length === 0) {
|
||||
if (newRole.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Role with ID ${roleId} not found`
|
||||
`Role with ID ${newRoleId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// move all users from the userOrgs table with roleId to newRoleId
|
||||
await db
|
||||
.update(userOrgs)
|
||||
.set({ roleId: newRoleId })
|
||||
.where(eq(userOrgs.roleId, roleId));
|
||||
|
||||
// delete the old role
|
||||
await db.delete(roles).where(eq(roles.roleId, roleId));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from "./addRoleAction";
|
||||
export * from "./addRoleResource";
|
||||
export * from "../resource/setResourceRoles";
|
||||
export * from "./addRoleSite";
|
||||
export * from "./createRole";
|
||||
export * from "./deleteRole";
|
||||
|
||||
@@ -18,6 +18,7 @@ const updateRoleBodySchema = z
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
message: "At least one field must be provided for update",
|
||||
});
|
||||
|
||||
@@ -15,13 +15,15 @@ const createSiteParamsSchema = z.object({
|
||||
orgId: z.string(),
|
||||
});
|
||||
|
||||
const createSiteSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
exitNodeId: z.number().int().positive(),
|
||||
subdomain: z.string().min(1).max(255).optional(),
|
||||
pubKey: z.string().optional(),
|
||||
subnet: z.string(),
|
||||
});
|
||||
const createSiteSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255),
|
||||
exitNodeId: z.number().int().positive(),
|
||||
subdomain: z.string().min(1).max(255).optional(),
|
||||
pubKey: z.string().optional(),
|
||||
subnet: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CreateSiteResponse = {
|
||||
name: string;
|
||||
@@ -83,10 +85,7 @@ export async function createSite(
|
||||
};
|
||||
}
|
||||
|
||||
const [newSite] = await db
|
||||
.insert(sites)
|
||||
.values(payload)
|
||||
.returning();
|
||||
const [newSite] = await db.insert(sites).values(payload).returning();
|
||||
|
||||
const adminRole = await db
|
||||
.select()
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function listSites(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
parsedParams.error.errors.map((e) => e.message).join(", ")
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export async function pickSiteDefaults(
|
||||
status: HttpCode.OK,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
|
||||
@@ -23,6 +23,7 @@ const updateSiteBodySchema = z
|
||||
megabytesIn: z.number().int().nonnegative().optional(),
|
||||
megabytesOut: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
message: "At least one field must be provided for update",
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources, sites, targets } from "@server/db/schema";
|
||||
import { resources, sites, Target, targets } from "@server/db/schema";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -15,13 +15,17 @@ const createTargetParamsSchema = z.object({
|
||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
const createTargetSchema = z.object({
|
||||
ip: z.string().ip(),
|
||||
method: z.string().min(1).max(10),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
protocol: z.string().optional(),
|
||||
enabled: z.boolean().default(true),
|
||||
});
|
||||
const createTargetSchema = z
|
||||
.object({
|
||||
ip: z.string().ip(),
|
||||
method: z.string().min(1).max(10),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
protocol: z.string().optional(),
|
||||
enabled: z.boolean().default(true),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CreateTargetResponse = Target;
|
||||
|
||||
export async function createTarget(
|
||||
req: Request,
|
||||
@@ -102,6 +106,7 @@ export async function createTarget(
|
||||
.insert(targets)
|
||||
.values({
|
||||
resourceId,
|
||||
protocol: "tcp", // hard code for now
|
||||
...targetData,
|
||||
})
|
||||
.returning();
|
||||
@@ -126,7 +131,7 @@ export async function createTarget(
|
||||
allowedIps: targetIps.flat(),
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
return response<CreateTargetResponse>(res, {
|
||||
data: newTarget[0],
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -15,12 +15,12 @@ const updateTargetParamsSchema = z.object({
|
||||
|
||||
const updateTargetBodySchema = z
|
||||
.object({
|
||||
// ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete
|
||||
ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete
|
||||
method: z.string().min(1).max(10).optional(),
|
||||
port: z.number().int().min(1).max(65535).optional(),
|
||||
protocol: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
message: "At least one field must be provided for update",
|
||||
});
|
||||
|
||||
@@ -18,10 +18,15 @@ export async function traefikConfigProvider(
|
||||
schema.resources,
|
||||
eq(schema.targets.resourceId, schema.resources.resourceId)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.orgs,
|
||||
eq(schema.resources.orgId, schema.orgs.orgId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.targets.enabled, true),
|
||||
isNotNull(schema.resources.fullDomain)
|
||||
isNotNull(schema.resources.subdomain),
|
||||
isNotNull(schema.orgs.domain)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -60,15 +65,22 @@ export async function traefikConfigProvider(
|
||||
for (const item of all) {
|
||||
const target = item.targets;
|
||||
const resource = item.resources;
|
||||
const org = item.orgs;
|
||||
|
||||
const routerName = `${target.targetId}-router`;
|
||||
const serviceName = `${target.targetId}-service`;
|
||||
|
||||
if (!resource.fullDomain) {
|
||||
if (!resource || !resource.subdomain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const domainParts = resource.fullDomain.split(".");
|
||||
if (!org || !org.domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullDomain = `${resource.subdomain}.${org.domain}`;
|
||||
|
||||
const domainParts = fullDomain.split(".");
|
||||
let wildCard;
|
||||
if (domainParts.length <= 2) {
|
||||
wildCard = `*.${domainParts.join(".")}`;
|
||||
@@ -97,7 +109,7 @@ export async function traefikConfigProvider(
|
||||
],
|
||||
middlewares: resource.ssl ? [badgerMiddlewareName] : [],
|
||||
service: serviceName,
|
||||
rule: `Host(\`${resource.fullDomain}\`)`,
|
||||
rule: `Host(\`${fullDomain}\`)`,
|
||||
...(resource.ssl ? { tls } : {}),
|
||||
};
|
||||
|
||||
@@ -107,7 +119,7 @@ export async function traefikConfigProvider(
|
||||
entryPoints: [config.traefik.http_entrypoint],
|
||||
middlewares: [redirectMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: `Host(\`${resource.fullDomain}\`)`,
|
||||
rule: `Host(\`${fullDomain}\`)`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export async function acceptInvite(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser[0].email !== existingInvite[0].email) {
|
||||
if (req.user && req.user.email !== existingInvite[0].email) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
|
||||
@@ -8,10 +8,11 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import stoi from "@server/utils/stoi";
|
||||
|
||||
const addUserRoleParamsSchema = z.object({
|
||||
userId: z.string(),
|
||||
roleId: z.number().int().positive(),
|
||||
roleId: z.string().transform(stoi).pipe(z.number()),
|
||||
});
|
||||
|
||||
export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>;
|
||||
@@ -22,17 +23,17 @@ export async function addUserRole(
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = addUserRoleParamsSchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
const parsedParams = addUserRoleParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { userId, roleId } = parsedBody.data;
|
||||
const { userId, roleId } = parsedParams.data;
|
||||
|
||||
if (!req.userOrg) {
|
||||
return next(
|
||||
|
||||
9
server/schemas/subdomainSchema.ts
Normal file
9
server/schemas/subdomainSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const subdomainSchema = z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
|
||||
"Invalid subdomain format"
|
||||
)
|
||||
.min(1, "Subdomain must be at least 1 character long");
|
||||
Reference in New Issue
Block a user