refactor contexts, format zod errors, and more refactoring

This commit is contained in:
Milo Schwartz 2024-11-03 13:57:51 -05:00
parent 2635443105
commit 2852d62258
No known key found for this signature in database
83 changed files with 2150 additions and 1264 deletions

View file

@ -120,7 +120,8 @@ if (!parsedConfig.success) {
throw new Error(`Invalid configuration file: ${errors}`); throw new Error(`Invalid configuration file: ${errors}`);
} }
process.env.SERVER_EXTERNAL_PORT = parsedConfig.data.server.external_port.toString(); process.env.SERVER_EXTERNAL_PORT =
parsedConfig.data.server.external_port.toString();
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
?.require_email_verification ?.require_email_verification
? "true" ? "true"

View file

@ -47,7 +47,7 @@ authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
authenticated.get("/org/:orgId/site/:niceId", verifyOrgAccess, site.getSite); authenticated.get("/org/:orgId/site/:niceId", verifyOrgAccess, site.getSite);
authenticated.get( authenticated.get(
"/org/:orgId/pickSiteDefaults", "/org/:orgId/pick-site-defaults",
verifyOrgAccess, verifyOrgAccess,
site.pickSiteDefaults site.pickSiteDefaults
); );
@ -118,12 +118,12 @@ authenticated.delete(
target.deleteTarget target.deleteTarget
); );
authenticated.put( // authenticated.put(
"/org/:orgId/role", // "/org/:orgId/role",
verifyOrgAccess, // verifyOrgAccess,
verifySuperuser, // verifySuperuser,
role.createRole // role.createRole
); // );
authenticated.get("/org/:orgId/roles", verifyOrgAccess, role.listRoles); authenticated.get("/org/:orgId/roles", verifyOrgAccess, role.listRoles);
authenticated.get( authenticated.get(
"/role/:roleId", "/role/:roleId",
@ -131,18 +131,18 @@ authenticated.get(
verifyUserInRole, verifyUserInRole,
role.getRole role.getRole
); );
authenticated.post( // authenticated.post(
"/role/:roleId", // "/role/:roleId",
verifyRoleAccess, // verifyRoleAccess,
verifySuperuser, // verifySuperuser,
role.updateRole // role.updateRole
); // );
authenticated.delete( // authenticated.delete(
"/role/:roleId", // "/role/:roleId",
verifyRoleAccess, // verifyRoleAccess,
verifySuperuser, // verifySuperuser,
role.deleteRole // role.deleteRole
); // );
authenticated.put( authenticated.put(
"/role/:roleId/site", "/role/:roleId/site",
@ -210,12 +210,6 @@ authenticated.delete(
verifyUserAccess, verifyUserAccess,
user.removeUserOrg user.removeUserOrg
); );
authenticated.put(
"/org/:orgId/user/:userId",
verifyOrgAccess,
verifyUserAccess,
user.addUserOrg
);
authenticated.put( authenticated.put(
"/user/:userId/site", "/user/:userId/site",

View file

@ -10,6 +10,7 @@ import logger from '@server/logger';
import config from "@server/config"; import config from "@server/config";
import { getUniqueExitNodeEndpointName } from '@server/db/names'; import { getUniqueExitNodeEndpointName } from '@server/db/names';
import { findNextAvailableCidr } from "@server/utils/ip"; import { findNextAvailableCidr } from "@server/utils/ip";
import { fromError } from 'zod-validation-error';
// Define Zod schema for request validation // Define Zod schema for request validation
const getConfigSchema = z.object({ const getConfigSchema = z.object({
publicKey: z.string(), publicKey: z.string(),
@ -33,7 +34,7 @@ export async function getConfig(req: Request, res: Response, next: NextFunction)
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }

View file

@ -1,32 +1,38 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { orgs } from '@server/db/schema'; import { orgs } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const getOrgSchema = z.object({ const getOrgSchema = z.object({
orgId: z.string() orgId: z.string(),
}); });
export async function checkId(req: Request, res: Response, next: NextFunction): Promise<any> { export async function checkId(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedQuery = getOrgSchema.safeParse(req.query); const parsedQuery = getOrgSchema.safeParse(req.query);
if (!parsedQuery.success) { if (!parsedQuery.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map(e => e.message).join(', ') fromError(parsedQuery.error).toString()
) )
); );
} }
const { orgId } = parsedQuery.data; const { orgId } = parsedQuery.data;
const org = await db.select() const org = await db
.select()
.from(orgs) .from(orgs)
.where(eq(orgs.orgId, orgId)) .where(eq(orgs.orgId, orgId))
.limit(1); .limit(1);
@ -50,6 +56,11 @@ export async function checkId(req: Request, res: Response, next: NextFunction):
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -10,6 +10,7 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
import logger from '@server/logger'; import logger from '@server/logger';
import { createSuperuserRole } from '@server/db/ensureActions'; import { createSuperuserRole } from '@server/db/ensureActions';
import config, { APP_PATH } from "@server/config"; import config, { APP_PATH } from "@server/config";
import { fromError } from 'zod-validation-error';
const createOrgSchema = z.object({ const createOrgSchema = z.object({
orgId: z.string(), orgId: z.string(),
@ -26,7 +27,7 @@ export async function createOrg(req: Request, res: Response, next: NextFunction)
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }

View file

@ -8,6 +8,7 @@ import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
import logger from '@server/logger'; import logger from '@server/logger';
import { fromError } from 'zod-validation-error';
const deleteOrgSchema = z.object({ const deleteOrgSchema = z.object({
orgId: z.string() orgId: z.string()
@ -20,7 +21,7 @@ export async function deleteOrg(req: Request, res: Response, next: NextFunction)
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }

View file

@ -8,6 +8,7 @@ import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
import logger from '@server/logger'; import logger from '@server/logger';
import { fromError } from 'zod-validation-error';
const updateOrgParamsSchema = z.object({ const updateOrgParamsSchema = z.object({
orgId: z.string() orgId: z.string()
@ -27,7 +28,7 @@ export async function updateOrg(req: Request, res: Response, next: NextFunction)
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -37,7 +38,7 @@ export async function updateOrg(req: Request, res: Response, next: NextFunction)
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }

View file

@ -9,6 +9,7 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
import logger from '@server/logger'; import logger from '@server/logger';
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
import stoi from '@server/utils/stoi'; import stoi from '@server/utils/stoi';
import { fromError } from 'zod-validation-error';
const createResourceParamsSchema = z.object({ const createResourceParamsSchema = z.object({
siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()), siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()),
@ -29,7 +30,7 @@ export async function createResource(req: Request, res: Response, next: NextFunc
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -42,7 +43,7 @@ export async function createResource(req: Request, res: Response, next: NextFunc
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }

View file

@ -1,20 +1,25 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources } from '@server/db/schema'; import { resources } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
// Define Zod schema for request parameters validation // Define Zod schema for request parameters validation
const deleteResourceSchema = z.object({ const deleteResourceSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function deleteResource(req: Request, res: Response, next: NextFunction): Promise<any> { export async function deleteResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
// Validate request parameters // Validate request parameters
const parsedParams = deleteResourceSchema.safeParse(req.params); const parsedParams = deleteResourceSchema.safeParse(req.params);
@ -22,7 +27,7 @@ export async function deleteResource(req: Request, res: Response, next: NextFunc
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -30,13 +35,22 @@ export async function deleteResource(req: Request, res: Response, next: NextFunc
const { resourceId } = parsedParams.data; const { resourceId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.deleteResource, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.deleteResource,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// Delete the resource from the database // Delete the resource from the database
const deletedResource = await db.delete(resources) const deletedResource = await db
.delete(resources)
.where(eq(resources.resourceId, resourceId)) .where(eq(resources.resourceId, resourceId))
.returning(); .returning();
@ -58,6 +72,11 @@ export async function deleteResource(req: Request, res: Response, next: NextFunc
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources } from '@server/db/schema'; import { resources } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
// Define Zod schema for request parameters validation // Define Zod schema for request parameters validation
const getResourceSchema = z.object({ const getResourceSchema = z.object({
@ -19,9 +20,13 @@ export type GetResourceResponse = {
siteId: number; siteId: number;
orgId: string; orgId: string;
name: string; name: string;
} };
export async function getResource(req: Request, res: Response, next: NextFunction): Promise<any> { export async function getResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
// Validate request parameters // Validate request parameters
const parsedParams = getResourceSchema.safeParse(req.params); const parsedParams = getResourceSchema.safeParse(req.params);
@ -29,7 +34,7 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -37,13 +42,22 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
const { resourceId } = parsedParams.data; const { resourceId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.getResource, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.getResource,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// Fetch the resource from the database // Fetch the resource from the database
const resource = await db.select() const resource = await db
.select()
.from(resources) .from(resources)
.where(eq(resources.resourceId, resourceId)) .where(eq(resources.resourceId, resourceId))
.limit(1); .limit(1);
@ -62,7 +76,7 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
resourceId: resource[0].resourceId, resourceId: resource[0].resourceId,
siteId: resource[0].siteId, siteId: resource[0].siteId,
orgId: resource[0].orgId, orgId: resource[0].orgId,
name: resource[0].name name: resource[0].name,
}, },
success: true, success: true,
error: false, error: false,
@ -71,6 +85,11 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
}); });
} catch (error) { } catch (error) {
throw error; throw error;
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleResources, roles } from '@server/db/schema'; import { roleResources, roles } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const listResourceRolesSchema = z.object({ const listResourceRolesSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function listResourceRoles(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listResourceRoles(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = listResourceRolesSchema.safeParse(req.params); const parsedParams = listResourceRolesSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -28,9 +33,17 @@ export async function listResourceRoles(req: Request, res: Response, next: NextF
const { resourceId } = parsedParams.data; const { resourceId } = parsedParams.data;
// Check if the user has permission to list resource roles // Check if the user has permission to list resource roles
const hasPermission = await checkUserActionPermission(ActionsEnum.listResourceRoles, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.listResourceRoles,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const resourceRolesList = await db const resourceRolesList = await db
@ -53,6 +66,11 @@ export async function listResourceRoles(req: Request, res: Response, next: NextF
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources } from '@server/db/schema'; import { resources } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
// Define Zod schema for request parameters validation // Define Zod schema for request parameters validation
const updateResourceParamsSchema = z.object({ const updateResourceParamsSchema = z.object({
@ -15,14 +16,20 @@ const updateResourceParamsSchema = z.object({
}); });
// Define Zod schema for request body validation // Define Zod schema for request body validation
const updateResourceBodySchema = z.object({ const updateResourceBodySchema = z
name: z.string().min(1).max(255).optional(), .object({
subdomain: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
}).refine(data => Object.keys(data).length > 0, { subdomain: z.string().min(1).max(255).optional(),
message: "At least one field must be provided for update" })
}); .refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});
export async function updateResource(req: Request, res: Response, next: NextFunction): Promise<any> { export async function updateResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
// Validate request parameters // Validate request parameters
const parsedParams = updateResourceParamsSchema.safeParse(req.params); const parsedParams = updateResourceParamsSchema.safeParse(req.params);
@ -30,7 +37,7 @@ export async function updateResource(req: Request, res: Response, next: NextFunc
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -41,7 +48,7 @@ export async function updateResource(req: Request, res: Response, next: NextFunc
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -50,13 +57,22 @@ export async function updateResource(req: Request, res: Response, next: NextFunc
const updateData = parsedBody.data; const updateData = parsedBody.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.updateResource, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.updateResource,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// Update the resource in the database // Update the resource in the database
const updatedResource = await db.update(resources) const updatedResource = await db
.update(resources)
.set(updateData) .set(updateData)
.where(eq(resources.resourceId, resourceId)) .where(eq(resources.resourceId, resourceId))
.returning(); .returning();
@ -79,6 +95,11 @@ export async function updateResource(req: Request, res: Response, next: NextFunc
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleActions, roles } from '@server/db/schema'; import { roleActions, roles } 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
import { fromError } from "zod-validation-error";
const addRoleActionParamSchema = z.object({ const addRoleActionParamSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -17,14 +18,18 @@ const addRoleActionSchema = z.object({
actionId: z.string(), actionId: z.string(),
}); });
export async function addRoleAction(req: Request, res: Response, next: NextFunction): Promise<any> { export async function addRoleAction(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = addRoleActionSchema.safeParse(req.body); const parsedBody = addRoleActionSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -36,7 +41,7 @@ export async function addRoleAction(req: Request, res: Response, next: NextFunct
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -44,22 +49,42 @@ export async function addRoleAction(req: Request, res: Response, next: NextFunct
const { roleId } = parsedParams.data; const { roleId } = parsedParams.data;
// Check if the user has permission to add role actions // Check if the user has permission to add role actions
const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleAction, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.addRoleAction,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// Get the orgId for the role // Get the orgId for the role
const role = await db.select({ orgId: roles.orgId }).from(roles).where(eq(roles.roleId, roleId)).limit(1); const role = await db
.select({ orgId: roles.orgId })
.from(roles)
.where(eq(roles.roleId, roleId))
.limit(1);
if (role.length === 0) { if (role.length === 0) {
return next(createHttpError(HttpCode.NOT_FOUND, `Role with ID ${roleId} not found`)); return next(
createHttpError(
HttpCode.NOT_FOUND,
`Role with ID ${roleId} not found`
)
);
} }
const newRoleAction = await db.insert(roleActions).values({ const newRoleAction = await db
roleId, .insert(roleActions)
actionId, .values({
orgId: role[0].orgId!, roleId,
}).returning(); actionId,
orgId: role[0].orgId!,
})
.returning();
return response(res, { return response(res, {
data: newRoleAction[0], data: newRoleAction[0],
@ -70,6 +95,11 @@ export async function addRoleAction(req: Request, res: Response, next: NextFunct
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,12 +1,13 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleResources } from '@server/db/schema'; import { roleResources } 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const addRoleResourceParamsSchema = z.object({ const addRoleResourceParamsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -16,14 +17,18 @@ const addRoleResourceSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function addRoleResource(req: Request, res: Response, next: NextFunction): Promise<any> { export async function addRoleResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = addRoleResourceSchema.safeParse(req.body); const parsedBody = addRoleResourceSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -35,7 +40,7 @@ export async function addRoleResource(req: Request, res: Response, next: NextFun
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -43,15 +48,26 @@ export async function addRoleResource(req: Request, res: Response, next: NextFun
const { roleId } = parsedParams.data; const { roleId } = parsedParams.data;
// Check if the user has permission to add role resources // Check if the user has permission to add role resources
const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleResource, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.addRoleResource,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const newRoleResource = await db.insert(roleResources).values({ const newRoleResource = await db
roleId, .insert(roleResources)
resourceId, .values({
}).returning(); roleId,
resourceId,
})
.returning();
return response(res, { return response(res, {
data: newRoleResource[0], data: newRoleResource[0],
@ -62,6 +78,11 @@ export async function addRoleResource(req: Request, res: Response, next: NextFun
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources, roleResources, roleSites } from '@server/db/schema'; import { resources, roleResources, roleSites } 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
import { fromError } from "zod-validation-error";
const addRoleSiteParamsSchema = z.object({ const addRoleSiteParamsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -17,14 +18,18 @@ const addRoleSiteSchema = z.object({
siteId: z.string().transform(Number).pipe(z.number().int().positive()), siteId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function addRoleSite(req: Request, res: Response, next: NextFunction): Promise<any> { export async function addRoleSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = addRoleSiteSchema.safeParse(req.body); const parsedBody = addRoleSiteSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -36,7 +41,7 @@ export async function addRoleSite(req: Request, res: Response, next: NextFunctio
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -44,17 +49,29 @@ export async function addRoleSite(req: Request, res: Response, next: NextFunctio
const { roleId } = parsedParams.data; const { roleId } = parsedParams.data;
// Check if the user has permission to add role sites // Check if the user has permission to add role sites
const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleSite, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.addRoleSite,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const newRoleSite = await db.insert(roleSites).values({ const newRoleSite = await db
roleId, .insert(roleSites)
siteId, .values({
}).returning(); roleId,
siteId,
})
.returning();
const siteResources = await db.select() const siteResources = await db
.select()
.from(resources) .from(resources)
.where(eq(resources.siteId, siteId)); .where(eq(resources.siteId, siteId));
@ -74,6 +91,11 @@ export async function addRoleSite(req: Request, res: Response, next: NextFunctio
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,15 +1,16 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roles } from '@server/db/schema'; import { roles } 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const createRoleParamsSchema = z.object({ const createRoleParamsSchema = z.object({
orgId: z.string() orgId: z.string(),
}); });
const createRoleSchema = z.object({ const createRoleSchema = z.object({
@ -17,14 +18,18 @@ const createRoleSchema = z.object({
description: z.string().optional(), description: z.string().optional(),
}); });
export async function createRole(req: Request, res: Response, next: NextFunction): Promise<any> { export async function createRole(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = createRoleSchema.safeParse(req.body); const parsedBody = createRoleSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -36,7 +41,7 @@ export async function createRole(req: Request, res: Response, next: NextFunction
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -44,15 +49,26 @@ export async function createRole(req: Request, res: Response, next: NextFunction
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
// Check if the user has permission to create roles // Check if the user has permission to create roles
const hasPermission = await checkUserActionPermission(ActionsEnum.createRole, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.createRole,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const newRole = await db.insert(roles).values({ const newRole = await db
...roleData, .insert(roles)
orgId, .values({
}).returning(); ...roleData,
orgId,
})
.returning();
return response(res, { return response(res, {
data: newRole[0], data: newRole[0],
@ -63,6 +79,11 @@ export async function createRole(req: Request, res: Response, next: NextFunction
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roles } from '@server/db/schema'; import { roles } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const deleteRoleSchema = z.object({ const deleteRoleSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()) roleId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function deleteRole(req: Request, res: Response, next: NextFunction): Promise<any> { export async function deleteRole(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = deleteRoleSchema.safeParse(req.params); const parsedParams = deleteRoleSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -28,15 +33,24 @@ export async function deleteRole(req: Request, res: Response, next: NextFunction
const { roleId } = parsedParams.data; const { roleId } = parsedParams.data;
// Check if the user has permission to delete roles // Check if the user has permission to delete roles
const hasPermission = await checkUserActionPermission(ActionsEnum.deleteRole, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.deleteRole,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const role = await db.select() const role = await db
.from(roles) .select()
.where(eq(roles.roleId, roleId)) .from(roles)
.limit(1); .where(eq(roles.roleId, roleId))
.limit(1);
if (role.length === 0) { if (role.length === 0) {
return next( return next(
@ -56,7 +70,8 @@ export async function deleteRole(req: Request, res: Response, next: NextFunction
); );
} }
const deletedRole = await db.delete(roles) const deletedRole = await db
.delete(roles)
.where(eq(roles.roleId, roleId)) .where(eq(roles.roleId, roleId))
.returning(); .returning();
@ -78,6 +93,11 @@ export async function deleteRole(req: Request, res: Response, next: NextFunction
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roles } from '@server/db/schema'; import { roles } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const getRoleSchema = z.object({ const getRoleSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()) roleId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function getRole(req: Request, res: Response, next: NextFunction): Promise<any> { export async function getRole(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = getRoleSchema.safeParse(req.params); const parsedParams = getRoleSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -28,12 +33,21 @@ export async function getRole(req: Request, res: Response, next: NextFunction):
const { roleId } = parsedParams.data; const { roleId } = parsedParams.data;
// Check if the user has permission to get roles // Check if the user has permission to get roles
const hasPermission = await checkUserActionPermission(ActionsEnum.getRole, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.getRole,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const role = await db.select() const role = await db
.select()
.from(roles) .from(roles)
.where(eq(roles.roleId, roleId)) .where(eq(roles.roleId, roleId))
.limit(1); .limit(1);
@ -56,6 +70,11 @@ export async function getRole(req: Request, res: Response, next: NextFunction):
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleActions, actions } from '@server/db/schema'; import { roleActions, actions } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const listRoleActionsSchema = z.object({ const listRoleActionsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function listRoleActions(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listRoleActions(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = listRoleActionsSchema.safeParse(req.params); const parsedParams = listRoleActionsSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -28,9 +33,17 @@ export async function listRoleActions(req: Request, res: Response, next: NextFun
const { roleId } = parsedParams.data; const { roleId } = parsedParams.data;
// Check if the user has permission to list role actions // Check if the user has permission to list role actions
const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleActions, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.listRoleActions,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const roleActionsList = await db const roleActionsList = await db
@ -43,7 +56,7 @@ export async function listRoleActions(req: Request, res: Response, next: NextFun
.innerJoin(actions, eq(roleActions.actionId, actions.actionId)) .innerJoin(actions, eq(roleActions.actionId, actions.actionId))
.where(eq(roleActions.roleId, roleId)); .where(eq(roleActions.roleId, roleId));
// TODO: Do we need to filter out what the user can see? // TODO: Do we need to filter out what the user can see?
return response(res, { return response(res, {
data: roleActionsList, data: roleActionsList,
@ -54,6 +67,11 @@ export async function listRoleActions(req: Request, res: Response, next: NextFun
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleResources, resources } from '@server/db/schema'; import { roleResources, resources } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const listRoleResourcesSchema = z.object({ const listRoleResourcesSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function listRoleResources(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listRoleResources(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = listRoleResourcesSchema.safeParse(req.params); const parsedParams = listRoleResourcesSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -28,9 +33,17 @@ export async function listRoleResources(req: Request, res: Response, next: NextF
const { roleId } = parsedParams.data; const { roleId } = parsedParams.data;
// Check if the user has permission to list role resources // Check if the user has permission to list role resources
const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleResources, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.listRoleResources,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const roleResourcesList = await db const roleResourcesList = await db
@ -40,10 +53,13 @@ export async function listRoleResources(req: Request, res: Response, next: NextF
subdomain: resources.subdomain, subdomain: resources.subdomain,
}) })
.from(roleResources) .from(roleResources)
.innerJoin(resources, eq(roleResources.resourceId, resources.resourceId)) .innerJoin(
resources,
eq(roleResources.resourceId, resources.resourceId)
)
.where(eq(roleResources.roleId, roleId)); .where(eq(roleResources.roleId, roleId));
// TODO: Do we need to filter out what the user can see? // TODO: Do we need to filter out what the user can see?
return response(res, { return response(res, {
data: roleResourcesList, data: roleResourcesList,
@ -54,6 +70,11 @@ export async function listRoleResources(req: Request, res: Response, next: NextF
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleSites, sites } from '@server/db/schema'; import { roleSites, sites } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const listRoleSitesSchema = z.object({ const listRoleSitesSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function listRoleSites(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listRoleSites(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = listRoleSitesSchema.safeParse(req.params); const parsedParams = listRoleSitesSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -28,9 +33,17 @@ export async function listRoleSites(req: Request, res: Response, next: NextFunct
const { roleId } = parsedParams.data; const { roleId } = parsedParams.data;
// Check if the user has permission to list role sites // Check if the user has permission to list role sites
const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleSites, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.listRoleSites,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const roleSitesList = await db const roleSitesList = await db
@ -42,7 +55,7 @@ export async function listRoleSites(req: Request, res: Response, next: NextFunct
.innerJoin(sites, eq(roleSites.siteId, sites.siteId)) .innerJoin(sites, eq(roleSites.siteId, sites.siteId))
.where(eq(roleSites.roleId, roleId)); .where(eq(roleSites.roleId, roleId));
// TODO: Do we need to filter out what the user can see? // TODO: Do we need to filter out what the user can see?
return response(res, { return response(res, {
data: roleSitesList, data: roleSitesList,
@ -53,6 +66,11 @@ export async function listRoleSites(req: Request, res: Response, next: NextFunct
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,32 +1,49 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roles, orgs } from '@server/db/schema'; import { roles, orgs } 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'; import createHttpError from "http-errors";
import { sql, eq } from 'drizzle-orm'; import { sql, eq } from "drizzle-orm";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const listRolesParamsSchema = z.object({ const listRolesParamsSchema = z.object({
orgId: z.string() orgId: z.string(),
}); });
const listRolesSchema = z.object({ const listRolesSchema = z.object({
limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)), limit: z
offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)), .string()
orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()), .optional()
.transform(Number)
.pipe(z.number().int().positive().default(10)),
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 listRoles(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listRoles(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedQuery = listRolesSchema.safeParse(req.query); const parsedQuery = listRolesSchema.safeParse(req.query);
if (!parsedQuery.success) { if (!parsedQuery.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map(e => e.message).join(', ') fromError(parsedQuery.error).toString()
) )
); );
} }
@ -38,7 +55,7 @@ export async function listRoles(req: Request, res: Response, next: NextFunction)
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -46,9 +63,17 @@ export async function listRoles(req: Request, res: Response, next: NextFunction)
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
// Check if the user has permission to list roles // Check if the user has permission to list roles
const hasPermission = await checkUserActionPermission(ActionsEnum.listRoles, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.listRoles,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
let baseQuery: any = db let baseQuery: any = db
@ -64,8 +89,10 @@ export async function listRoles(req: Request, res: Response, next: NextFunction)
.leftJoin(orgs, eq(roles.orgId, orgs.orgId)) .leftJoin(orgs, eq(roles.orgId, orgs.orgId))
.where(eq(roles.orgId, orgId)); .where(eq(roles.orgId, orgId));
let countQuery: any = db.select({ count: sql<number>`cast(count(*) as integer)` }).from(roles) let countQuery: any = db
.where(eq(roles.orgId, orgId)); .select({ count: sql<number>`cast(count(*) as integer)` })
.from(roles)
.where(eq(roles.orgId, orgId));
const rolesList = await baseQuery.limit(limit).offset(offset); const rolesList = await baseQuery.limit(limit).offset(offset);
const totalCountResult = await countQuery; const totalCountResult = await countQuery;
@ -87,6 +114,11 @@ export async function listRoles(req: Request, res: Response, next: NextFunction)
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleActions } from '@server/db/schema'; import { roleActions } from "@server/db/schema";
import { and, eq } from 'drizzle-orm'; import { and, eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const removeRoleActionParamsSchema = z.object({ const removeRoleActionParamsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -17,14 +18,18 @@ const removeRoleActionSchema = z.object({
actionId: z.string(), actionId: z.string(),
}); });
export async function removeRoleAction(req: Request, res: Response, next: NextFunction): Promise<any> { export async function removeRoleAction(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = removeRoleActionSchema.safeParse(req.params); const parsedParams = removeRoleActionSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -36,7 +41,7 @@ export async function removeRoleAction(req: Request, res: Response, next: NextFu
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -44,13 +49,27 @@ export async function removeRoleAction(req: Request, res: Response, next: NextFu
const { roleId } = parsedBody.data; const { roleId } = parsedBody.data;
// Check if the user has permission to remove role actions // Check if the user has permission to remove role actions
const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleAction, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.removeRoleAction,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const deletedRoleAction = await db.delete(roleActions) const deletedRoleAction = await db
.where(and(eq(roleActions.roleId, roleId), eq(roleActions.actionId, actionId))) .delete(roleActions)
.where(
and(
eq(roleActions.roleId, roleId),
eq(roleActions.actionId, actionId)
)
)
.returning(); .returning();
if (deletedRoleAction.length === 0) { if (deletedRoleAction.length === 0) {
@ -71,6 +90,11 @@ export async function removeRoleAction(req: Request, res: Response, next: NextFu
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleResources } from '@server/db/schema'; import { roleResources } from "@server/db/schema";
import { and, eq } from 'drizzle-orm'; import { and, eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const removeRoleResourceParamsSchema = z.object({ const removeRoleResourceParamsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -17,14 +18,18 @@ const removeRoleResourceSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function removeRoleResource(req: Request, res: Response, next: NextFunction): Promise<any> { export async function removeRoleResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = removeRoleResourceSchema.safeParse(req.params); const parsedParams = removeRoleResourceSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -36,7 +41,7 @@ export async function removeRoleResource(req: Request, res: Response, next: Next
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -44,13 +49,27 @@ export async function removeRoleResource(req: Request, res: Response, next: Next
const { roleId } = parsedBody.data; const { roleId } = parsedBody.data;
// Check if the user has permission to remove role resources // Check if the user has permission to remove role resources
const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleResource, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.removeRoleResource,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const deletedRoleResource = await db.delete(roleResources) const deletedRoleResource = await db
.where(and(eq(roleResources.roleId, roleId), eq(roleResources.resourceId, resourceId))) .delete(roleResources)
.where(
and(
eq(roleResources.roleId, roleId),
eq(roleResources.resourceId, resourceId)
)
)
.returning(); .returning();
if (deletedRoleResource.length === 0) { if (deletedRoleResource.length === 0) {
@ -71,6 +90,11 @@ export async function removeRoleResource(req: Request, res: Response, next: Next
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources, roleResources, roleSites } from '@server/db/schema'; import { resources, roleResources, roleSites } from "@server/db/schema";
import { and, eq } from 'drizzle-orm'; import { and, eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const removeRoleSiteParamsSchema = z.object({ const removeRoleSiteParamsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()), roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -17,14 +18,18 @@ const removeRoleSiteSchema = z.object({
siteId: z.string().transform(Number).pipe(z.number().int().positive()), siteId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function removeRoleSite(req: Request, res: Response, next: NextFunction): Promise<any> { export async function removeRoleSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = removeRoleSiteSchema.safeParse(req.params); const parsedParams = removeRoleSiteSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -36,7 +41,7 @@ export async function removeRoleSite(req: Request, res: Response, next: NextFunc
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -44,13 +49,24 @@ export async function removeRoleSite(req: Request, res: Response, next: NextFunc
const { roleId } = parsedBody.data; const { roleId } = parsedBody.data;
// Check if the user has permission to remove role sites // Check if the user has permission to remove role sites
const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleSite, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.removeRoleSite,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const deletedRoleSite = await db.delete(roleSites) const deletedRoleSite = await db
.where(and(eq(roleSites.roleId, roleId), eq(roleSites.siteId, siteId))) .delete(roleSites)
.where(
and(eq(roleSites.roleId, roleId), eq(roleSites.siteId, siteId))
)
.returning(); .returning();
if (deletedRoleSite.length === 0) { if (deletedRoleSite.length === 0) {
@ -62,13 +78,20 @@ export async function removeRoleSite(req: Request, res: Response, next: NextFunc
); );
} }
const siteResources = await db.select() const siteResources = await db
.from(resources) .select()
.where(eq(resources.siteId, siteId)); .from(resources)
.where(eq(resources.siteId, siteId));
for (const resource of siteResources) { for (const resource of siteResources) {
await db.delete(roleResources) await db
.where(and(eq(roleResources.roleId, roleId), eq(roleResources.resourceId, resource.resourceId))) .delete(roleResources)
.where(
and(
eq(roleResources.roleId, roleId),
eq(roleResources.resourceId, resource.resourceId)
)
)
.returning(); .returning();
} }
@ -81,6 +104,11 @@ export async function removeRoleSite(req: Request, res: Response, next: NextFunc
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,33 +1,40 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roles } from '@server/db/schema'; import { roles } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const updateRoleParamsSchema = z.object({ const updateRoleParamsSchema = z.object({
roleId: z.string().transform(Number).pipe(z.number().int().positive()) roleId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
const updateRoleBodySchema = z.object({ const updateRoleBodySchema = z
name: z.string().min(1).max(255).optional(), .object({
description: z.string().optional(), name: z.string().min(1).max(255).optional(),
}).refine(data => Object.keys(data).length > 0, { description: z.string().optional(),
message: "At least one field must be provided for update" })
}); .refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});
export async function updateRole(req: Request, res: Response, next: NextFunction): Promise<any> { export async function updateRole(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = updateRoleParamsSchema.safeParse(req.params); const parsedParams = updateRoleParamsSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -37,7 +44,7 @@ export async function updateRole(req: Request, res: Response, next: NextFunction
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -46,15 +53,24 @@ export async function updateRole(req: Request, res: Response, next: NextFunction
const updateData = parsedBody.data; const updateData = parsedBody.data;
// Check if the user has permission to update roles // Check if the user has permission to update roles
const hasPermission = await checkUserActionPermission(ActionsEnum.updateRole, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.updateRole,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const role = await db.select() const role = await db
.from(roles) .select()
.where(eq(roles.roleId, roleId)) .from(roles)
.limit(1); .where(eq(roles.roleId, roleId))
.limit(1);
if (role.length === 0) { if (role.length === 0) {
return next( return next(
@ -74,7 +90,8 @@ export async function updateRole(req: Request, res: Response, next: NextFunction
); );
} }
const updatedRole = await db.update(roles) const updatedRole = await db
.update(roles)
.set(updateData) .set(updateData)
.where(eq(roles.roleId, roleId)) .where(eq(roles.roleId, roleId))
.returning(); .returning();
@ -97,6 +114,11 @@ export async function updateRole(req: Request, res: Response, next: NextFunction
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,20 +1,25 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roles, userSites, sites, roleSites, exitNodes } from '@server/db/schema'; import {
roles,
userSites,
sites,
roleSites,
exitNodes,
} 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { eq, and } from 'drizzle-orm'; import { eq, and } from "drizzle-orm";
import { getUniqueSiteName } from '@server/db/names'; import { getUniqueSiteName } from "@server/db/names";
import { addPeer } from '../gerbil/peers'; import { addPeer } from "../gerbil/peers";
import { fromError } from "zod-validation-error";
const API_BASE_URL = "http://localhost:3000";
const createSiteParamsSchema = z.object({ const createSiteParamsSchema = z.object({
orgId: z.string() orgId: z.string(),
}); });
// Define Zod schema for request body validation // Define Zod schema for request body validation
@ -36,7 +41,11 @@ export type CreateSiteResponse = {
// subnet: string; // subnet: string;
}; };
export async function createSite(req: Request, res: Response, next: NextFunction): Promise<any> { export async function createSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
// Validate request body // Validate request body
const parsedBody = createSiteSchema.safeParse(req.body); const parsedBody = createSiteSchema.safeParse(req.body);
@ -44,7 +53,7 @@ export async function createSite(req: Request, res: Response, next: NextFunction
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -57,7 +66,7 @@ export async function createSite(req: Request, res: Response, next: NextFunction
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -65,38 +74,49 @@ export async function createSite(req: Request, res: Response, next: NextFunction
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.createSite, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.createSite,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission perform this action"
)
);
} }
if (!req.userOrgRoleId) { if (!req.userOrgRoleId) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have a role')); return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
} }
const niceId = await getUniqueSiteName(orgId); const niceId = await getUniqueSiteName(orgId);
// Create new site in the database // Create new site in the database
const [newSite] = await db.insert(sites).values({ const [newSite] = await db
orgId, .insert(sites)
exitNodeId, .values({
name, orgId,
niceId, exitNodeId,
pubKey, name,
subnet, niceId,
}).returning(); pubKey,
subnet,
})
.returning();
// find the superuser roleId and also add the resource to the superuser role // find the superuser roleId and also add the resource to the superuser role
const superuserRole = await db.select() const superuserRole = await db
.from(roles) .select()
.where(and(eq(roles.isSuperuserRole, true), eq(roles.orgId, orgId))) .from(roles)
.limit(1); .where(and(eq(roles.isSuperuserRole, true), eq(roles.orgId, orgId)))
.limit(1);
if (superuserRole.length === 0) { if (superuserRole.length === 0) {
return next( return next(
createHttpError( createHttpError(HttpCode.NOT_FOUND, `Superuser role not found`)
HttpCode.NOT_FOUND,
`Superuser role not found`
)
); );
} }
@ -135,6 +155,11 @@ export async function createSite(req: Request, res: Response, next: NextFunction
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,23 +1,28 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { sites } from '@server/db/schema'; import { sites } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { deletePeer } from '../gerbil/peers'; import { deletePeer } from "../gerbil/peers";
import { fromError } from "zod-validation-error";
const API_BASE_URL = "http://localhost:3000"; const API_BASE_URL = "http://localhost:3000";
// Define Zod schema for request parameters validation // Define Zod schema for request parameters validation
const deleteSiteSchema = z.object({ const deleteSiteSchema = z.object({
siteId: z.string().transform(Number).pipe(z.number().int().positive()) siteId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function deleteSite(req: Request, res: Response, next: NextFunction): Promise<any> { export async function deleteSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
// Validate request parameters // Validate request parameters
const parsedParams = deleteSiteSchema.safeParse(req.params); const parsedParams = deleteSiteSchema.safeParse(req.params);
@ -25,7 +30,7 @@ export async function deleteSite(req: Request, res: Response, next: NextFunction
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -33,13 +38,22 @@ export async function deleteSite(req: Request, res: Response, next: NextFunction
const { siteId } = parsedParams.data; const { siteId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.deleteSite, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.deleteSite,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// Delete the site from the database // Delete the site from the database
const [deletedSite] = await db.delete(sites) const [deletedSite] = await db
.delete(sites)
.where(eq(sites.siteId, siteId)) .where(eq(sites.siteId, siteId))
.returning(); .returning();
@ -63,26 +77,33 @@ export async function deleteSite(req: Request, res: Response, next: NextFunction
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }
async function removePeer(publicKey: string) { async function removePeer(publicKey: string) {
try { try {
const response = await fetch(`${API_BASE_URL}/peer?public_key=${encodeURIComponent(publicKey)}`, { const response = await fetch(
method: 'DELETE', `${API_BASE_URL}/peer?public_key=${encodeURIComponent(publicKey)}`,
}); {
method: "DELETE",
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
console.log('Peer removed successfully:', data.status); console.log("Peer removed successfully:", data.status);
return data; return data;
} catch (error: any) { } catch (error: any) {
console.error('Error removing peer:', error.message); console.error("Error removing peer:", error.message);
throw error; throw error;
} }
} }

View file

@ -1,18 +1,24 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { sites } from '@server/db/schema'; import { sites } from "@server/db/schema";
import { eq, and } from 'drizzle-orm'; import { eq, and } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import stoi from '@server/utils/stoi'; import stoi from "@server/utils/stoi";
import { fromError } from "zod-validation-error";
// Define Zod schema for request parameters validation // Define Zod schema for request parameters validation
const getSiteSchema = z.object({ const getSiteSchema = z.object({
siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()).optional(), siteId: z
.string()
.optional()
.transform(stoi)
.pipe(z.number().int().positive().optional())
.optional(),
niceId: z.string().optional(), niceId: z.string().optional(),
orgId: z.string().optional(), orgId: z.string().optional(),
}); });
@ -22,9 +28,13 @@ export type GetSiteResponse = {
name: string; name: string;
subdomain: string; subdomain: string;
subnet: string; subnet: string;
} };
export async function getSite(req: Request, res: Response, next: NextFunction): Promise<any> { export async function getSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
// Validate request parameters // Validate request parameters
const parsedParams = getSiteSchema.safeParse(req.params); const parsedParams = getSiteSchema.safeParse(req.params);
@ -32,7 +42,7 @@ export async function getSite(req: Request, res: Response, next: NextFunction):
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -40,27 +50,37 @@ export async function getSite(req: Request, res: Response, next: NextFunction):
const { siteId, niceId, orgId } = parsedParams.data; const { siteId, niceId, orgId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.updateSite, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.updateSite,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
let site; let site;
// Fetch the site from the database // Fetch the site from the database
if (siteId) { if (siteId) {
site = await db.select() site = await db
.select()
.from(sites) .from(sites)
.where(eq(sites.siteId, siteId)) .where(eq(sites.siteId, siteId))
.limit(1); .limit(1);
} else if (niceId && orgId) { } else if (niceId && orgId) {
site = await db.select() site = await db
.select()
.from(sites) .from(sites)
.where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId))) .where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId)))
.limit(1); .limit(1);
} }
if (!site) { if (!site) {
return next(createHttpError(HttpCode.NOT_FOUND, 'Site not found')); return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
} }
if (site.length === 0) { if (site.length === 0) {
@ -86,6 +106,11 @@ export async function getSite(req: Request, res: Response, next: NextFunction):
}); });
} catch (error) { } catch (error) {
logger.error("Error from getSite: ", error); logger.error("Error from getSite: ", error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { roleSites, roles } from '@server/db/schema'; import { roleSites, roles } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const listSiteRolesSchema = z.object({ const listSiteRolesSchema = z.object({
siteId: z.string().transform(Number).pipe(z.number().int().positive()), siteId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function listSiteRoles(req: Request, res: Response, next: NextFunction): Promise<any> { export async function listSiteRoles(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = listSiteRolesSchema.safeParse(req.params); const parsedParams = listSiteRolesSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -28,9 +33,17 @@ export async function listSiteRoles(req: Request, res: Response, next: NextFunct
const { siteId } = parsedParams.data; const { siteId } = parsedParams.data;
// Check if the user has permission to list site roles // Check if the user has permission to list site roles
const hasPermission = await checkUserActionPermission(ActionsEnum.listSiteRoles, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.listSiteRoles,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const siteRolesList = await db const siteRolesList = await db
@ -53,6 +66,11 @@ export async function listSiteRoles(req: Request, res: Response, next: NextFunct
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -18,26 +18,25 @@ export type PickSiteDefaultsResponse = {
listenPort: number; listenPort: number;
endpoint: string; endpoint: string;
subnet: string; subnet: string;
} };
export async function pickSiteDefaults( export async function pickSiteDefaults(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission( const hasPermission = await checkUserActionPermission(
ActionsEnum.createSite, ActionsEnum.createSite,
req, req
); );
if (!hasPermission) { if (!hasPermission) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"User does not have permission to perform this action", "User does not have permission to perform this action"
), )
); );
} }
@ -47,10 +46,7 @@ export async function pickSiteDefaults(
const nodes = await db.select().from(exitNodes); const nodes = await db.select().from(exitNodes);
if (nodes.length === 0) { if (nodes.length === 0) {
return next( return next(
createHttpError( createHttpError(HttpCode.NOT_FOUND, "No exit nodes available")
HttpCode.NOT_FOUND,
"No exit nodes available",
),
); );
} }
@ -59,9 +55,10 @@ export async function pickSiteDefaults(
// TODO: this probably can be optimized... // TODO: this probably can be optimized...
// list all of the sites on that exit node // list all of the sites on that exit node
const sitesQuery = await db.select({ const sitesQuery = await db
subnet: sites.subnet .select({
}) subnet: sites.subnet,
})
.from(sites) .from(sites)
.where(eq(sites.exitNodeId, exitNode.exitNodeId)); .where(eq(sites.exitNodeId, exitNode.exitNodeId));
@ -74,8 +71,8 @@ export async function pickSiteDefaults(
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
"No available subnets", "No available subnets"
), )
); );
} }
@ -97,11 +94,7 @@ export async function pickSiteDefaults(
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next( return next(
createHttpError( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred...",
),
); );
} }
} }

View file

@ -1,33 +1,40 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { sites } from '@server/db/schema'; import { sites } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
// Define Zod schema for request parameters validation // Define Zod schema for request parameters validation
const updateSiteParamsSchema = z.object({ const updateSiteParamsSchema = z.object({
siteId: z.string().transform(Number).pipe(z.number().int().positive()) siteId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
// Define Zod schema for request body validation // Define Zod schema for request body validation
const updateSiteBodySchema = z.object({ const updateSiteBodySchema = z
name: z.string().min(1).max(255).optional(), .object({
subdomain: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
pubKey: z.string().optional(), subdomain: z.string().min(1).max(255).optional(),
subnet: z.string().optional(), pubKey: z.string().optional(),
exitNode: z.number().int().positive().optional(), subnet: z.string().optional(),
megabytesIn: z.number().int().nonnegative().optional(), exitNode: z.number().int().positive().optional(),
megabytesOut: z.number().int().nonnegative().optional(), megabytesIn: z.number().int().nonnegative().optional(),
}).refine(data => Object.keys(data).length > 0, { megabytesOut: z.number().int().nonnegative().optional(),
message: "At least one field must be provided for update" })
}); .refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});
export async function updateSite(req: Request, res: Response, next: NextFunction): Promise<any> { export async function updateSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
// Validate request parameters // Validate request parameters
const parsedParams = updateSiteParamsSchema.safeParse(req.params); const parsedParams = updateSiteParamsSchema.safeParse(req.params);
@ -35,7 +42,7 @@ export async function updateSite(req: Request, res: Response, next: NextFunction
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -46,7 +53,7 @@ export async function updateSite(req: Request, res: Response, next: NextFunction
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -55,13 +62,22 @@ export async function updateSite(req: Request, res: Response, next: NextFunction
const updateData = parsedBody.data; const updateData = parsedBody.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.updateSite, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.updateSite,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// Update the site in the database // Update the site in the database
const updatedSite = await db.update(sites) const updatedSite = await db
.update(sites)
.set(updateData) .set(updateData)
.where(eq(sites.siteId, siteId)) .where(eq(sites.siteId, siteId))
.returning(); .returning();
@ -84,6 +100,11 @@ export async function updateSite(req: Request, res: Response, next: NextFunction
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,15 +1,16 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources, sites, targets } from '@server/db/schema'; import { resources, sites, targets } 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { addPeer } from '../gerbil/peers'; import { addPeer } from "../gerbil/peers";
import { eq, and } from 'drizzle-orm'; import { eq, and } from "drizzle-orm";
import { isIpInCidr } from '@server/utils/ip'; import { isIpInCidr } from "@server/utils/ip";
import { fromError } from "zod-validation-error";
const createTargetParamsSchema = z.object({ const createTargetParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.number().int().positive()), resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
@ -23,14 +24,18 @@ const createTargetSchema = z.object({
enabled: z.boolean().default(true), enabled: z.boolean().default(true),
}); });
export async function createTarget(req: Request, res: Response, next: NextFunction): Promise<any> { export async function createTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = createTargetSchema.safeParse(req.body); const parsedBody = createTargetSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -42,7 +47,7 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -50,15 +55,24 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
const { resourceId } = parsedParams.data; const { resourceId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.createTarget, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.createTarget,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// get the resource // get the resource
const [resource] = await db.select({ const [resource] = await db
siteId: resources.siteId, .select({
}) siteId: resources.siteId,
})
.from(resources) .from(resources)
.where(eq(resources.resourceId, resourceId)); .where(eq(resources.resourceId, resourceId));
@ -72,9 +86,10 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
} }
// TODO: is this all inefficient? // TODO: is this all inefficient?
// get the site // get the site
const [site] = await db.select() const [site] = await db
.select()
.from(sites) .from(sites)
.where(eq(sites.siteId, resource.siteId!)) .where(eq(sites.siteId, resource.siteId!))
.limit(1); .limit(1);
@ -98,10 +113,13 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
); );
} }
const newTarget = await db.insert(targets).values({ const newTarget = await db
resourceId, .insert(targets)
...targetData .values({
}).returning(); resourceId,
...targetData,
})
.returning();
// Fetch resources for this site // Fetch resources for this site
const resourcesRes = await db.query.resources.findMany({ const resourcesRes = await db.query.resources.findMany({
@ -109,16 +127,18 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
}); });
// Fetch targets for all resources of this site // Fetch targets for all resources of this site
const targetIps = await Promise.all(resourcesRes.map(async (resource) => { const targetIps = await Promise.all(
const targetsRes = await db.query.targets.findMany({ resourcesRes.map(async (resource) => {
where: eq(targets.resourceId, resource.resourceId), const targetsRes = await db.query.targets.findMany({
}); where: eq(targets.resourceId, resource.resourceId),
return targetsRes.map(target => `${target.ip}/32`); });
})); return targetsRes.map((target) => `${target.ip}/32`);
})
);
await addPeer(site.exitNodeId!, { await addPeer(site.exitNodeId!, {
publicKey: site.pubKey, publicKey: site.pubKey,
allowedIps: targetIps.flat() allowedIps: targetIps.flat(),
}); });
return response(res, { return response(res, {
@ -130,6 +150,11 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,27 +1,32 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources, sites, targets } from '@server/db/schema'; import { resources, sites, targets } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { addPeer } from '../gerbil/peers'; import { addPeer } from "../gerbil/peers";
import { fromError } from "zod-validation-error";
const deleteTargetSchema = z.object({ const deleteTargetSchema = z.object({
targetId: z.string().transform(Number).pipe(z.number().int().positive()) targetId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function deleteTarget(req: Request, res: Response, next: NextFunction): Promise<any> { export async function deleteTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = deleteTargetSchema.safeParse(req.params); const parsedParams = deleteTargetSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -29,13 +34,21 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
const { targetId } = parsedParams.data; const { targetId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.deleteTarget, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.deleteTarget,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const [deletedTarget] = await db
const [deletedTarget] = await db.delete(targets) .delete(targets)
.where(eq(targets.targetId, targetId)) .where(eq(targets.targetId, targetId))
.returning(); .returning();
@ -48,9 +61,10 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
); );
} }
// get the resource // get the resource
const [resource] = await db.select({ const [resource] = await db
siteId: resources.siteId, .select({
}) siteId: resources.siteId,
})
.from(resources) .from(resources)
.where(eq(resources.resourceId, deletedTarget.resourceId!)); .where(eq(resources.resourceId, deletedTarget.resourceId!));
@ -66,7 +80,8 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
// TODO: is this all inefficient? // TODO: is this all inefficient?
// get the site // get the site
const [site] = await db.select() const [site] = await db
.select()
.from(sites) .from(sites)
.where(eq(sites.siteId, resource.siteId!)) .where(eq(sites.siteId, resource.siteId!))
.limit(1); .limit(1);
@ -86,16 +101,18 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
}); });
// Fetch targets for all resources of this site // Fetch targets for all resources of this site
const targetIps = await Promise.all(resourcesRes.map(async (resource) => { const targetIps = await Promise.all(
const targetsRes = await db.query.targets.findMany({ resourcesRes.map(async (resource) => {
where: eq(targets.resourceId, resource.resourceId), const targetsRes = await db.query.targets.findMany({
}); where: eq(targets.resourceId, resource.resourceId),
return targetsRes.map(target => `${target.ip}/32`); });
})); return targetsRes.map((target) => `${target.ip}/32`);
})
);
await addPeer(site.exitNodeId!, { await addPeer(site.exitNodeId!, {
publicKey: site.pubKey, publicKey: site.pubKey,
allowedIps: targetIps.flat() allowedIps: targetIps.flat(),
}); });
return response(res, { return response(res, {
@ -107,6 +124,11 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { targets } from '@server/db/schema'; import { targets } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const getTargetSchema = z.object({ const getTargetSchema = z.object({
targetId: z.string().transform(Number).pipe(z.number().int().positive()) targetId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function getTarget(req: Request, res: Response, next: NextFunction): Promise<any> { export async function getTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = getTargetSchema.safeParse(req.params); const parsedParams = getTargetSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -28,12 +33,21 @@ export async function getTarget(req: Request, res: Response, next: NextFunction)
const { targetId } = parsedParams.data; const { targetId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.getTarget, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.getTarget,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const target = await db.select() const target = await db
.select()
.from(targets) .from(targets)
.where(eq(targets.targetId, targetId)) .where(eq(targets.targetId, targetId))
.limit(1); .limit(1);
@ -56,6 +70,11 @@ export async function getTarget(req: Request, res: Response, next: NextFunction)
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,36 +1,43 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { targets } from '@server/db/schema'; import { targets } from "@server/db/schema";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const updateTargetParamsSchema = z.object({ const updateTargetParamsSchema = z.object({
targetId: z.string().transform(Number).pipe(z.number().int().positive()) targetId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
const updateTargetBodySchema = z.object({ const updateTargetBodySchema = z
// ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete .object({
method: z.string().min(1).max(10).optional(), // ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete
port: z.number().int().min(1).max(65535).optional(), method: z.string().min(1).max(10).optional(),
protocol: z.string().optional(), port: z.number().int().min(1).max(65535).optional(),
enabled: z.boolean().optional(), protocol: z.string().optional(),
}).refine(data => Object.keys(data).length > 0, { enabled: z.boolean().optional(),
message: "At least one field must be provided for update" })
}); .refine((data) => Object.keys(data).length > 0, {
message: "At least one field must be provided for update",
});
export async function updateTarget(req: Request, res: Response, next: NextFunction): Promise<any> { export async function updateTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = updateTargetParamsSchema.safeParse(req.params); const parsedParams = updateTargetParamsSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -40,7 +47,7 @@ export async function updateTarget(req: Request, res: Response, next: NextFuncti
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -49,12 +56,21 @@ export async function updateTarget(req: Request, res: Response, next: NextFuncti
const updateData = parsedBody.data; const updateData = parsedBody.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.updateTarget, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.updateTarget,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const updatedTarget = await db.update(targets) const updatedTarget = await db
.update(targets)
.set(updateData) .set(updateData)
.where(eq(targets.targetId, targetId)) .where(eq(targets.targetId, targetId))
.returning(); .returning();
@ -77,6 +93,11 @@ export async function updateTarget(req: Request, res: Response, next: NextFuncti
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { userActions, users } from '@server/db/schema'; import { userActions, users } 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
import { fromError } from "zod-validation-error";
const addUserActionSchema = z.object({ const addUserActionSchema = z.object({
userId: z.string(), userId: z.string(),
@ -15,14 +16,18 @@ const addUserActionSchema = z.object({
orgId: z.string(), orgId: z.string(),
}); });
export async function addUserAction(req: Request, res: Response, next: NextFunction): Promise<any> { export async function addUserAction(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = addUserActionSchema.safeParse(req.body); const parsedBody = addUserActionSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -30,22 +35,42 @@ export async function addUserAction(req: Request, res: Response, next: NextFunct
const { userId, actionId, orgId } = parsedBody.data; const { userId, actionId, orgId } = parsedBody.data;
// Check if the user has permission to add user actions // Check if the user has permission to add user actions
const hasPermission = await checkUserActionPermission(ActionsEnum.addUserAction, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.addUserAction,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// Check if the user exists // Check if the user exists
const user = await db.select().from(users).where(eq(users.userId, userId)).limit(1); const user = await db
.select()
.from(users)
.where(eq(users.userId, userId))
.limit(1);
if (user.length === 0) { if (user.length === 0) {
return next(createHttpError(HttpCode.NOT_FOUND, `User with ID ${userId} not found`)); return next(
createHttpError(
HttpCode.NOT_FOUND,
`User with ID ${userId} not found`
)
);
} }
const newUserAction = await db.insert(userActions).values({ const newUserAction = await db
userId, .insert(userActions)
actionId, .values({
orgId, userId,
}).returning(); actionId,
orgId,
})
.returning();
return response(res, { return response(res, {
data: newUserAction[0], data: newUserAction[0],
@ -56,6 +81,11 @@ export async function addUserAction(req: Request, res: Response, next: NextFunct
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,87 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { db } from '@server/db';
import { userOrgs, users, roles } from '@server/db/schema';
import { and, eq } from 'drizzle-orm';
import response from "@server/utils/response";
import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors';
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
import logger from '@server/logger';
const addUserParamsSchema = z.object({
userId: z.string().uuid(),
orgId: z.string()
});
const addUserSchema = z.object({
roleId: z.number().int().positive(),
});
export async function addUserOrg(req: Request, res: Response, next: NextFunction): Promise<any> {
try {
const parsedParams = addUserParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ')
)
);
}
const { userId, orgId } = parsedParams.data;
const parsedBody = addUserSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ')
)
);
}
const { roleId } = parsedBody.data;
// Check if the user has permission to add users
const hasPermission = await checkUserActionPermission(ActionsEnum.addUser, req);
if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
}
// Check if the user exists
const user = await db.select().from(users).where(eq(users.userId, userId)).limit(1);
if (user.length === 0) {
return next(createHttpError(HttpCode.NOT_FOUND, `User with ID ${userId} not found`));
}
// Check if the user is already in the organization
const existingUserOrg = await db.select()
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.limit(1);
if (existingUserOrg.length > 0) {
return next(createHttpError(HttpCode.CONFLICT, 'User is already a member of this organization'));
}
// Add the user to the userOrgs table
const newUserOrg = await db.insert(userOrgs).values({
userId,
orgId,
roleId
}).returning();
return response(res, {
data: newUserOrg[0],
success: true,
error: false,
message: "User added to organization successfully",
status: HttpCode.CREATED,
});
} catch (error) {
logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
}
}

View file

@ -1,26 +1,31 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { userResources } from '@server/db/schema'; import { userResources } 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const addUserResourceSchema = z.object({ const addUserResourceSchema = z.object({
userId: z.string(), userId: z.string(),
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function addUserResource(req: Request, res: Response, next: NextFunction): Promise<any> { export async function addUserResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = addUserResourceSchema.safeParse(req.body); const parsedBody = addUserResourceSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -28,15 +33,26 @@ export async function addUserResource(req: Request, res: Response, next: NextFun
const { userId, resourceId } = parsedBody.data; const { userId, resourceId } = parsedBody.data;
// Check if the user has permission to add user resources // Check if the user has permission to add user resources
const hasPermission = await checkUserActionPermission(ActionsEnum.addUserResource, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.addUserResource,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const newUserResource = await db.insert(userResources).values({ const newUserResource = await db
userId, .insert(userResources)
resourceId, .values({
}).returning(); userId,
resourceId,
})
.returning();
return response(res, { return response(res, {
data: newUserResource[0], data: newUserResource[0],
@ -47,6 +63,11 @@ export async function addUserResource(req: Request, res: Response, next: NextFun
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,27 +1,32 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources, userResources, userSites } from '@server/db/schema'; import { resources, userResources, userSites } 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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { eq } from 'drizzle-orm'; import { eq } from "drizzle-orm";
import { fromError } from "zod-validation-error";
const addUserSiteSchema = z.object({ const addUserSiteSchema = z.object({
userId: z.string(), userId: z.string(),
siteId: z.string().transform(Number).pipe(z.number().int().positive()), siteId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function addUserSite(req: Request, res: Response, next: NextFunction): Promise<any> { export async function addUserSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = addUserSiteSchema.safeParse(req.body); const parsedBody = addUserSiteSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -29,18 +34,30 @@ export async function addUserSite(req: Request, res: Response, next: NextFunctio
const { userId, siteId } = parsedBody.data; const { userId, siteId } = parsedBody.data;
// Check if the user has permission to add user sites // Check if the user has permission to add user sites
const hasPermission = await checkUserActionPermission(ActionsEnum.addUserSite, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.addUserSite,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const newUserSite = await db.insert(userSites).values({ const newUserSite = await db
userId, .insert(userSites)
siteId, .values({
}).returning(); userId,
siteId,
})
.returning();
// Add all resources associated with the site to the user // Add all resources associated with the site to the user
const siteResources = await db.select() const siteResources = await db
.select()
.from(resources) .from(resources)
.where(eq(resources.siteId, siteId)); .where(eq(resources.siteId, siteId));
@ -60,6 +77,11 @@ export async function addUserSite(req: Request, res: Response, next: NextFunctio
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,6 +1,5 @@
export * from "./getUser"; export * from "./getUser";
export * from "./removeUserOrg"; export * from "./removeUserOrg";
export * from "./addUserOrg";
export * from "./listUsers"; export * from "./listUsers";
export * from "./setUserRole"; export * from "./setUserRole";
export * from "./inviteUser"; export * from "./inviteUser";

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { userActions } from '@server/db/schema'; import { userActions } from "@server/db/schema";
import { and, eq } from 'drizzle-orm'; import { and, eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const removeUserActionParamsSchema = z.object({ const removeUserActionParamsSchema = z.object({
userId: z.string(), userId: z.string(),
@ -15,17 +16,21 @@ const removeUserActionParamsSchema = z.object({
const removeUserActionSchema = z.object({ const removeUserActionSchema = z.object({
actionId: z.string(), actionId: z.string(),
orgId: z.string() orgId: z.string(),
}); });
export async function removeUserAction(req: Request, res: Response, next: NextFunction): Promise<any> { export async function removeUserAction(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = removeUserActionParamsSchema.safeParse(req.params); const parsedParams = removeUserActionParamsSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -37,7 +42,7 @@ export async function removeUserAction(req: Request, res: Response, next: NextFu
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -45,17 +50,28 @@ export async function removeUserAction(req: Request, res: Response, next: NextFu
const { actionId, orgId } = parsedBody.data; const { actionId, orgId } = parsedBody.data;
// Check if the user has permission to remove user actions // Check if the user has permission to remove user actions
const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserAction, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.removeUserAction,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const deletedUserAction = await db.delete(userActions) const deletedUserAction = await db
.where(and( .delete(userActions)
eq(userActions.userId, userId), .where(
eq(userActions.actionId, actionId), and(
eq(userActions.orgId, orgId) eq(userActions.userId, userId),
)) eq(userActions.actionId, actionId),
eq(userActions.orgId, orgId)
)
)
.returning(); .returning();
if (deletedUserAction.length === 0) { if (deletedUserAction.length === 0) {
@ -76,6 +92,11 @@ export async function removeUserAction(req: Request, res: Response, next: NextFu
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,27 +1,32 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { userOrgs, users } from '@server/db/schema'; import { userOrgs, users } from "@server/db/schema";
import { and, eq } from 'drizzle-orm'; import { and, eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const removeUserSchema = z.object({ const removeUserSchema = z.object({
userId: z.string().uuid(), userId: z.string().uuid(),
orgId: z.string() orgId: z.string(),
}); });
export async function removeUserOrg(req: Request, res: Response, next: NextFunction): Promise<any> { export async function removeUserOrg(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = removeUserSchema.safeParse(req.params); const parsedParams = removeUserSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -29,13 +34,22 @@ export async function removeUserOrg(req: Request, res: Response, next: NextFunct
const { userId, orgId } = parsedParams.data; const { userId, orgId } = parsedParams.data;
// Check if the user has permission to list sites // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(ActionsEnum.removeUser, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.removeUser,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// remove the user from the userOrgs table // remove the user from the userOrgs table
await db.delete(userOrgs) await db
.delete(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))); .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)));
return response(res, { return response(res, {
@ -47,6 +61,11 @@ export async function removeUserOrg(req: Request, res: Response, next: NextFunct
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,27 +1,32 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { userResources } from '@server/db/schema'; import { userResources } from "@server/db/schema";
import { and, eq } from 'drizzle-orm'; import { and, eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const removeUserResourceSchema = z.object({ const removeUserResourceSchema = z.object({
userId: z.string(), userId: z.string(),
resourceId: z.string().transform(Number).pipe(z.number().int().positive()) resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
}); });
export async function removeUserResource(req: Request, res: Response, next: NextFunction): Promise<any> { export async function removeUserResource(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = removeUserResourceSchema.safeParse(req.params); const parsedParams = removeUserResourceSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -29,13 +34,27 @@ export async function removeUserResource(req: Request, res: Response, next: Next
const { userId, resourceId } = parsedParams.data; const { userId, resourceId } = parsedParams.data;
// Check if the user has permission to remove user resources // Check if the user has permission to remove user resources
const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserResource, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.removeUserResource,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const deletedUserResource = await db.delete(userResources) const deletedUserResource = await db
.where(and(eq(userResources.userId, userId), eq(userResources.resourceId, resourceId))) .delete(userResources)
.where(
and(
eq(userResources.userId, userId),
eq(userResources.resourceId, resourceId)
)
)
.returning(); .returning();
if (deletedUserResource.length === 0) { if (deletedUserResource.length === 0) {
@ -56,6 +75,11 @@ export async function removeUserResource(req: Request, res: Response, next: Next
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,13 +1,14 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { resources, userResources, userSites } from '@server/db/schema'; import { resources, userResources, userSites } from "@server/db/schema";
import { and, eq } from 'drizzle-orm'; import { and, eq } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const removeUserSiteParamsSchema = z.object({ const removeUserSiteParamsSchema = z.object({
userId: z.string(), userId: z.string(),
@ -17,14 +18,18 @@ const removeUserSiteSchema = z.object({
siteId: z.number().int().positive(), siteId: z.number().int().positive(),
}); });
export async function removeUserSite(req: Request, res: Response, next: NextFunction): Promise<any> { export async function removeUserSite(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedParams = removeUserSiteParamsSchema.safeParse(req.params); const parsedParams = removeUserSiteParamsSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map(e => e.message).join(', ') fromError(parsedParams.error).toString()
) )
); );
} }
@ -36,7 +41,7 @@ export async function removeUserSite(req: Request, res: Response, next: NextFunc
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -44,13 +49,24 @@ export async function removeUserSite(req: Request, res: Response, next: NextFunc
const { siteId } = parsedBody.data; const { siteId } = parsedBody.data;
// Check if the user has permission to remove user sites // Check if the user has permission to remove user sites
const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserSite, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.removeUserSite,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
const deletedUserSite = await db.delete(userSites) const deletedUserSite = await db
.where(and(eq(userSites.userId, userId), eq(userSites.siteId, siteId))) .delete(userSites)
.where(
and(eq(userSites.userId, userId), eq(userSites.siteId, siteId))
)
.returning(); .returning();
if (deletedUserSite.length === 0) { if (deletedUserSite.length === 0) {
@ -62,13 +78,20 @@ export async function removeUserSite(req: Request, res: Response, next: NextFunc
); );
} }
const siteResources = await db.select() const siteResources = await db
.select()
.from(resources) .from(resources)
.where(eq(resources.siteId, siteId)); .where(eq(resources.siteId, siteId));
for (const resource of siteResources) { for (const resource of siteResources) {
await db.delete(userResources) await db
.where(and(eq(userResources.userId, userId), eq(userResources.resourceId, resource.resourceId))) .delete(userResources)
.where(
and(
eq(userResources.userId, userId),
eq(userResources.resourceId, resource.resourceId)
)
)
.returning(); .returning();
} }
@ -81,6 +104,11 @@ export async function removeUserSite(req: Request, res: Response, next: NextFunc
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -1,28 +1,33 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { z } from 'zod'; import { z } from "zod";
import { db } from '@server/db'; import { db } from "@server/db";
import { userOrgs, roles } from '@server/db/schema'; import { userOrgs, roles } from "@server/db/schema";
import { eq, and } from 'drizzle-orm'; import { eq, and } from "drizzle-orm";
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'; import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from '@server/logger'; import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const addUserRoleSchema = z.object({ const addUserRoleSchema = z.object({
userId: z.string(), userId: z.string(),
roleId: z.number().int().positive(), roleId: z.number().int().positive(),
orgId: z.string() orgId: z.string(),
}); });
export async function addUserRole(req: Request, res: Response, next: NextFunction): Promise<any> { export async function addUserRole(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try { try {
const parsedBody = addUserRoleSchema.safeParse(req.body); const parsedBody = addUserRoleSchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedBody.error.errors.map(e => e.message).join(', ') fromError(parsedBody.error).toString()
) )
); );
} }
@ -30,22 +35,37 @@ export async function addUserRole(req: Request, res: Response, next: NextFunctio
const { userId, roleId, orgId } = parsedBody.data; const { userId, roleId, orgId } = parsedBody.data;
// Check if the user has permission to add user roles // Check if the user has permission to add user roles
const hasPermission = await checkUserActionPermission(ActionsEnum.addUserRole, req); const hasPermission = await checkUserActionPermission(
ActionsEnum.addUserRole,
req
);
if (!hasPermission) { if (!hasPermission) {
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission to perform this action"
)
);
} }
// Check if the role exists and belongs to the specified org // Check if the role exists and belongs to the specified org
const roleExists = await db.select() const roleExists = await db
.select()
.from(roles) .from(roles)
.where(and(eq(roles.roleId, roleId), eq(roles.orgId, orgId))) .where(and(eq(roles.roleId, roleId), eq(roles.orgId, orgId)))
.limit(1); .limit(1);
if (roleExists.length === 0) { if (roleExists.length === 0) {
return next(createHttpError(HttpCode.NOT_FOUND, 'Role not found or does not belong to the specified organization')); return next(
createHttpError(
HttpCode.NOT_FOUND,
"Role not found or does not belong to the specified organization"
)
);
} }
const newUserRole = await db.update(userOrgs) const newUserRole = await db
.update(userOrgs)
.set({ roleId }) .set({ roleId })
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.returning(); .returning();
@ -59,6 +79,11 @@ export async function addUserRole(req: Request, res: Response, next: NextFunctio
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred..."
)
);
} }
} }

View file

@ -20,7 +20,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { useToast } from "@app/hooks/use-toast"; import { useToast } from "@app/hooks/useToast";
import { ListOrgsResponse } from "@server/routers/org"; import { ListOrgsResponse } from "@server/routers/org";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";

View file

@ -8,6 +8,9 @@ import { internal } from "@app/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { GetOrgResponse, ListOrgsResponse } from "@server/routers/org"; import { GetOrgResponse, ListOrgsResponse } from "@server/routers/org";
import { authCookieHeader } from "@app/api/cookies"; import { authCookieHeader } from "@app/api/cookies";
import { cache } from "react";
export const dynamic = "force-dynamic";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Settings - Pangolin`, title: `Settings - Pangolin`,
@ -47,7 +50,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const { children } = props; const { children } = props;
const user = await verifySession(); const getUser = cache(verifySession);
const user = await getUser();
if (!user) { if (!user) {
redirect("/auth/login"); redirect("/auth/login");
@ -56,20 +60,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const cookie = await authCookieHeader(); const cookie = await authCookieHeader();
try { try {
await internal.get<AxiosResponse<GetOrgResponse>>( const getOrg = cache(() =>
`/org/${params.orgId}`, internal.get<AxiosResponse<GetOrgResponse>>(
cookie `/org/${params.orgId}`,
cookie
)
); );
const org = await getOrg();
} catch { } catch {
redirect(`/`); redirect(`/`);
} }
let orgs: ListOrgsResponse["orgs"] = []; let orgs: ListOrgsResponse["orgs"] = [];
try { try {
const res = await internal.get<AxiosResponse<ListOrgsResponse>>( const getOrgs = cache(() =>
`/orgs`, internal.get<AxiosResponse<ListOrgsResponse>>(`/orgs`, cookie)
cookie
); );
const res = await getOrgs();
if (res && res.data.data.orgs) { if (res && res.data.data.orgs) {
orgs = res.data.data.orgs; orgs = res.data.data.orgs;
} }

View file

@ -8,7 +8,7 @@ import {
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/useToast";
import { Button} from "@/components/ui/button"; import { Button} from "@/components/ui/button";
import { import {
Form, Form,

View file

@ -36,6 +36,8 @@ import { AxiosResponse } from "axios";
import api from "@app/api"; import api from "@app/api";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { GetResourceResponse } from "@server/routers/resource";
import { useToast } from "@app/hooks/useToast";
const GeneralFormSchema = z.object({ const GeneralFormSchema = z.object({
name: z.string(), name: z.string(),
@ -49,6 +51,7 @@ export function GeneralForm() {
const orgId = params.orgId; const orgId = params.orgId;
const { resource, updateResource } = useResourceContext(); const { resource, updateResource } = useResourceContext();
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]); const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const { toast } = useToast();
const form = useForm<GeneralFormValues>({ const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema), resolver: zodResolver(GeneralFormSchema),
@ -72,7 +75,24 @@ export function GeneralForm() {
}, []); }, []);
async function onSubmit(data: GeneralFormValues) { async function onSubmit(data: GeneralFormValues) {
await updateResource({ name: data.name, siteId: data.siteId }); updateResource({ name: data.name, siteId: data.siteId });
await api
.post<AxiosResponse<GetResourceResponse>>(
`resource/${resource?.resourceId}`,
{
name: data.name,
siteId: data.siteId,
}
)
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update resource",
description:
e.response?.data?.message ||
"An error occurred while updating the resource",
});
});
} }
return ( return (

View file

@ -5,7 +5,7 @@ import { ChevronDownIcon } from "@radix-ui/react-icons";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/useToast";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { import {
Form, Form,
@ -78,7 +78,7 @@ export function CreateSiteForm() {
setKeypair(generatedKeypair); setKeypair(generatedKeypair);
setIsLoading(false); setIsLoading(false);
api.get(`/org/${orgId}/pickSiteDefaults`) api.get(`/org/${orgId}/pick-site-defaults`)
.catch((e) => { .catch((e) => {
toast({ toast({
title: "Error creating site...", title: "Error creating site...",

View file

@ -15,6 +15,8 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useSiteContext } from "@app/hooks/useSiteContext"; import { useSiteContext } from "@app/hooks/useSiteContext";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import api from "@app/api";
import { useToast } from "@app/hooks/useToast";
const GeneralFormSchema = z.object({ const GeneralFormSchema = z.object({
name: z.string(), name: z.string(),
@ -24,6 +26,7 @@ type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
export function GeneralForm() { export function GeneralForm() {
const { site, updateSite } = useSiteContext(); const { site, updateSite } = useSiteContext();
const { toast } = useToast();
const form = useForm<GeneralFormValues>({ const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema), resolver: zodResolver(GeneralFormSchema),
@ -34,7 +37,21 @@ export function GeneralForm() {
}); });
async function onSubmit(data: GeneralFormValues) { async function onSubmit(data: GeneralFormValues) {
await updateSite({ name: data.name }); updateSite({ name: data.name });
await api
.post(`/site/${site?.siteId}`, {
name: data.name,
})
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update site",
description:
e.message ||
"An error occurred while updating the site.",
});
});
} }
return ( return (

View file

@ -92,7 +92,7 @@ export const columns: ColumnDef<SiteRow>[] = [
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem> <DropdownMenuItem>
<Link <Link
href={`/${siteRow.orgId}/settings/sites/${siteRow.id}`} href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
> >
View settings View settings
</Link> </Link>

View file

@ -18,15 +18,30 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { useToast } from "@app/hooks/use-toast"; import { useToast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { InviteUserBody, InviteUserResponse } from "@server/routers/user"; import { InviteUserBody, InviteUserResponse } from "@server/routers/user";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { useParams } from "next/navigation";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
} from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext";
type InviteUserFormProps = {
open: boolean;
setOpen: (open: boolean) => void;
};
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email({ message: "Invalid email address" }), email: z.string().email({ message: "Invalid email address" }),
@ -34,9 +49,9 @@ const formSchema = z.object({
roleId: z.string(), roleId: z.string(),
}); });
export default function InviteUserForm() { export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
const { toast } = useToast(); const { toast } = useToast();
const { orgId } = useParams(); const { org } = useOrgContext();
const [inviteLink, setInviteLink] = useState<string | null>(null); const [inviteLink, setInviteLink] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -74,7 +89,7 @@ export default function InviteUserForm() {
const res = await api const res = await api
.post<AxiosResponse<InviteUserResponse>>( .post<AxiosResponse<InviteUserResponse>>(
`/org/${orgId}/create-invite`, `/org/${org?.org.orgId}/create-invite`,
{ {
email: values.email, email: values.email,
roleId: parseInt(values.roleId), roleId: parseInt(values.roleId),
@ -107,117 +122,151 @@ export default function InviteUserForm() {
return ( return (
<> <>
{!inviteLink && ( <Credenza open={open} onOpenChange={setOpen}>
<Form {...form}> <CredenzaContent>
<form <CredenzaHeader>
onSubmit={form.handleSubmit(onSubmit)} <CredenzaTitle>Invite User</CredenzaTitle>
className="space-y-4" <CredenzaDescription>
> Give new users access to your organization
<FormField </CredenzaDescription>
control={form.control} </CredenzaHeader>
name="email" <CredenzaBody>
render={({ field }) => ( {!inviteLink && (
<FormItem> <Form {...form}>
<FormLabel>Email</FormLabel> <form
<FormControl> onSubmit={form.handleSubmit(onSubmit)}
<Input className="space-y-4"
placeholder="Enter an email" id="invite-user-form"
{...field} >
/> <FormField
</FormControl> control={form.control}
<FormMessage /> name="email"
</FormItem> render={({ field }) => (
)} <FormItem>
/> <FormLabel>Email</FormLabel>
<FormField <FormControl>
control={form.control} <Input
name="roleId" placeholder="Enter an email"
render={({ field }) => ( {...field}
<FormItem> />
<FormLabel>Role</FormLabel> </FormControl>
<Select <FormMessage />
onValueChange={field.onChange} </FormItem>
defaultValue={field.value.toString()} )}
> />
<FormControl> <FormField
<SelectTrigger> control={form.control}
<SelectValue placeholder="Select role" /> name="roleId"
</SelectTrigger> render={({ field }) => (
</FormControl> <FormItem>
<SelectContent> <FormLabel>Role</FormLabel>
{roles.map((role) => ( <Select
<SelectItem onValueChange={
key={role.roleId} field.onChange
value={role.roleId.toString()} }
defaultValue={field.value.toString()}
> >
{role.name} <FormControl>
</SelectItem> <SelectTrigger>
))} <SelectValue placeholder="Select role" />
</SelectContent> </SelectTrigger>
</Select> </FormControl>
<FormMessage /> <SelectContent>
</FormItem> {roles.map((role) => (
)} <SelectItem
/> key={
<FormField role.roleId
control={form.control} }
name="validForHours" value={role.roleId.toString()}
render={({ field }) => ( >
<FormItem> {role.name}
<FormLabel>Valid For</FormLabel> </SelectItem>
<Select ))}
onValueChange={field.onChange} </SelectContent>
defaultValue={field.value.toString()} </Select>
> <FormMessage />
<FormControl> </FormItem>
<SelectTrigger> )}
<SelectValue placeholder="Select duration" /> />
</SelectTrigger> <FormField
</FormControl> control={form.control}
<SelectContent> name="validForHours"
{validFor.map((option) => ( render={({ field }) => (
<SelectItem <FormItem>
key={option.hours} <FormLabel>Valid For</FormLabel>
value={option.hours.toString()} <Select
onValueChange={
field.onChange
}
defaultValue={field.value.toString()}
> >
{option.name} <FormControl>
</SelectItem> <SelectTrigger>
))} <SelectValue placeholder="Select duration" />
</SelectContent> </SelectTrigger>
</Select> </FormControl>
<FormMessage /> <SelectContent>
</FormItem> {validFor.map(
)} (option) => (
/> <SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
)}
{inviteLink && (
<div className="max-w-md">
<p className="mb-4">
The user has been successfully invited. They
must access the link below to accept the
invitation.
</p>
<p className="mb-4">
The invite will expire in{" "}
<b>
{expiresInDays}{" "}
{expiresInDays === 1 ? "day" : "days"}
</b>
.
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/>
</div>
)}
</CredenzaBody>
<CredenzaFooter>
<Button <Button
type="submit" type="submit"
className="w-full" form="invite-user-form"
loading={loading} loading={loading}
disabled={inviteLink !== null} disabled={inviteLink !== null}
> >
Invite User Create Invitation
</Button> </Button>
</form> <CredenzaClose asChild>
</Form> <Button variant="outline">Close</Button>
)} </CredenzaClose>
</CredenzaFooter>
{inviteLink && ( </CredenzaContent>
<div className="max-w-md"> </Credenza>
<p className="mb-4">
The user has been successfully invited. They must access
the link below to accept the invitation.
</p>
<p className="mb-4">
The invite will expire in{" "}
<b>
{expiresInDays}{" "}
{expiresInDays === 1 ? "day" : "days"}
</b>
.
</p>
<CopyTextBox text={inviteLink} wrapText={false} />
</div>
)}
</> </>
); );
} }

View file

@ -10,16 +10,8 @@ import {
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { ArrowUpDown, MoreHorizontal } from "lucide-react"; import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { UsersDataTable } from "./UsersDataTable"; import { UsersDataTable } from "./UsersDataTable";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@app/components/ui/dialog";
import { useState } from "react"; import { useState } from "react";
import InviteUserForm from "./InviteUserForm"; import InviteUserForm from "./InviteUserForm";
import { Credenza, CredenzaTitle, CredenzaDescription, CredenzaHeader, CredenzaClose, CredenzaFooter, CredenzaContent, CredenzaBody } from "@app/components/Credenza";
export type UserRow = { export type UserRow = {
id: string; id: string;
@ -74,19 +66,10 @@ export default function UsersTable({ users }: UsersTableProps) {
return ( return (
<> <>
<Credenza open={isInviteModalOpen} onOpenChange={setIsInviteModalOpen}> <InviteUserForm
<CredenzaContent> open={isInviteModalOpen}
<CredenzaHeader> setOpen={setIsInviteModalOpen}
<CredenzaTitle>Invite User</CredenzaTitle> />
<CredenzaDescription>
Give new users access to your organization
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<InviteUserForm />
</CredenzaBody>
</CredenzaContent>
</Credenza>
<UsersDataTable <UsersDataTable
columns={columns} columns={columns}

View file

@ -3,6 +3,9 @@ import { authCookieHeader } from "@app/api/cookies";
import { ListUsersResponse } from "@server/routers/user"; import { ListUsersResponse } from "@server/routers/user";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import UsersTable, { UserRow } from "./components/UsersTable"; import UsersTable, { UserRow } from "./components/UsersTable";
import { GetOrgResponse } from "@server/routers/org";
import { cache } from "react";
import OrgProvider from "@app/providers/OrgProvider";
type UsersPageProps = { type UsersPageProps = {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@ -10,15 +13,36 @@ type UsersPageProps = {
export default async function UsersPage(props: UsersPageProps) { export default async function UsersPage(props: UsersPageProps) {
const params = await props.params; const params = await props.params;
let users: ListUsersResponse["users"] = []; let users: ListUsersResponse["users"] = [];
try { const res = await internal
const res = await internal.get<AxiosResponse<ListUsersResponse>>( .get<AxiosResponse<ListUsersResponse>>(
`/org/${params.orgId}/users`, `/org/${params.orgId}/users`,
await authCookieHeader() await authCookieHeader()
); )
.catch((e) => {
console.error(e);
});
if (res && res.status === 200) {
users = res.data.data.users; users = res.data.data.users;
} catch (e) { }
console.error("Error fetching users", e);
let org: GetOrgResponse | null = null;
const getOrg = cache(async () =>
internal
.get<AxiosResponse<GetOrgResponse>>(
`/org/${params.orgId}`,
await authCookieHeader()
)
.catch((e) => {
console.error(e);
})
);
const orgRes = await getOrg();
if (orgRes && orgRes.status === 200) {
org = orgRes.data.data;
} }
const userRows: UserRow[] = users.map((user) => { const userRows: UserRow[] = users.map((user) => {
@ -40,7 +64,9 @@ export default async function UsersPage(props: UsersPageProps) {
</p> </p>
</div> </div>
<UsersTable users={userRows} /> <OrgProvider org={org}>
<UsersTable users={userRows} />
</OrgProvider>
</> </>
); );
} }

View file

@ -2,12 +2,16 @@ import LoginForm from "@app/app/auth/login/LoginForm";
import { verifySession } from "@app/lib/auth/verifySession"; import { verifySession } from "@app/lib/auth/verifySession";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { cache } from "react";
export const dynamic = 'force-dynamic';
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const user = await verifySession(); const getUser = cache(verifySession);
const user = await getUser();
if (user) { if (user) {
redirect("/"); redirect("/");

View file

@ -2,12 +2,16 @@ import SignupForm from "@app/app/auth/signup/SignupForm";
import { verifySession } from "@app/lib/auth/verifySession"; import { verifySession } from "@app/lib/auth/verifySession";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { cache } from "react";
export const dynamic = 'force-dynamic';
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const user = await verifySession(); const getUser = cache(verifySession);
const user = await getUser();
if (user) { if (user) {
redirect("/"); redirect("/");

View file

@ -32,7 +32,7 @@ import { AxiosResponse } from "axios";
import { VerifyEmailResponse } from "@server/routers/auth"; import { VerifyEmailResponse } from "@server/routers/auth";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { Alert, AlertDescription } from "../../../components/ui/alert"; import { Alert, AlertDescription } from "../../../components/ui/alert";
import { useToast } from "@app/hooks/use-toast"; import { useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
const FormSchema = z.object({ const FormSchema = z.object({

View file

@ -1,6 +1,9 @@
import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm"; import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm";
import { verifySession } from "@app/lib/auth/verifySession"; import { verifySession } from "@app/lib/auth/verifySession";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { cache } from "react";
export const dynamic = "force-dynamic";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
@ -10,7 +13,8 @@ export default async function Page(props: {
} }
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const user = await verifySession(); const getUser = cache(verifySession);
const user = await getUser();
if (!user) { if (!user) {
redirect("/"); redirect("/");

View file

@ -7,12 +7,16 @@ import { AxiosResponse } from "axios";
import { ArrowUpRight } from "lucide-react"; import { ArrowUpRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { cache } from "react";
export const dynamic = "force-dynamic";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const params = await props.searchParams; // this is needed to prevent static optimization const params = await props.searchParams; // this is needed to prevent static optimization
const user = await verifySession(); const getUser = cache(verifySession);
const user = await getUser();
if (!user) { if (!user) {
redirect("/auth/login"); redirect("/auth/login");

View file

@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Command, Command,

View file

@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from "@/components/ui/button"
import { import {
Form, Form,

View file

@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { import {

View file

@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { import {

View file

@ -6,7 +6,7 @@ import { useFieldArray, useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {

View file

@ -5,7 +5,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import Link from "next/link"; import Link from "next/link";
import api from "@app/api"; import api from "@app/api";
import { toast } from "@app/hooks/use-toast"; import { toast } from "@app/hooks/useToast";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { import {
Card, Card,

View file

@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Command, Command,

View file

@ -1,169 +1,179 @@
"use client" "use client";
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod";
import { ChevronDownIcon } from "@radix-ui/react-icons" import { ChevronDownIcon } from "@radix-ui/react-icons";
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form";
import { z } from "zod" import { z } from "zod";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast";
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useSiteContext } from "@app/hooks/useSiteContext" import { useSiteContext } from "@app/hooks/useSiteContext";
const appearanceFormSchema = z.object({ const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark"], { theme: z.enum(["light", "dark"], {
required_error: "Please select a theme.", required_error: "Please select a theme.",
}), }),
font: z.enum(["inter", "manrope", "system"], { font: z.enum(["inter", "manrope", "system"], {
invalid_type_error: "Select a font", invalid_type_error: "Select a font",
required_error: "Please select a font.", required_error: "Please select a font.",
}), }),
}) });
type AppearanceFormValues = z.infer<typeof appearanceFormSchema> type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
// This can come from your database or API. // This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = { const defaultValues: Partial<AppearanceFormValues> = {
theme: "light", theme: "light",
} };
export function AppearanceForm() { export function AppearanceForm() {
const site = useSiteContext(); const site = useSiteContext();
console.log(site); console.log(site);
const form = useForm<AppearanceFormValues>({
resolver: zodResolver(appearanceFormSchema),
defaultValues,
})
function onSubmit(data: AppearanceFormValues) { const form = useForm<AppearanceFormValues>({
toast({ resolver: zodResolver(appearanceFormSchema),
title: "You submitted the following values:", defaultValues,
description: ( });
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return ( function onSubmit(data: AppearanceFormValues) {
<Form {...form}> toast({
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> title: "You submitted the following values:",
<FormField description: (
control={form.control} <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
name="font" <code className="text-white">
render={({ field }) => ( {JSON.stringify(data, null, 2)}
<FormItem> </code>
<FormLabel>Font</FormLabel> </pre>
<div className="relative w-max"> ),
<FormControl> });
<select }
className={cn(
buttonVariants({ variant: "outline" }), return (
"w-[200px] appearance-none font-normal" <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="font"
render={({ field }) => (
<FormItem>
<FormLabel>Font</FormLabel>
<div className="relative w-max">
<FormControl>
<select
className={cn(
buttonVariants({
variant: "outline",
}),
"w-[200px] appearance-none font-normal"
)}
{...field}
>
<option value="inter">Inter</option>
<option value="manrope">Manrope</option>
<option value="system">System</option>
</select>
</FormControl>
<ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
</div>
<FormDescription>
Set the font you want to use in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)} )}
{...field} />
> <FormField
<option value="inter">Inter</option> control={form.control}
<option value="manrope">Manrope</option> name="theme"
<option value="system">System</option> render={({ field }) => (
</select> <FormItem className="space-y-1">
</FormControl> <FormLabel>Theme</FormLabel>
<ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" /> <FormDescription>
</div> Select the theme for the dashboard.
<FormDescription> </FormDescription>
Set the font you want to use in the dashboard. <FormMessage />
</FormDescription> <RadioGroup
<FormMessage /> onValueChange={field.onChange}
</FormItem> defaultValue={field.value}
)} className="grid max-w-md grid-cols-2 gap-8 pt-2"
/> >
<FormField <FormItem>
control={form.control} <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
name="theme" <FormControl>
render={({ field }) => ( <RadioGroupItem
<FormItem className="space-y-1"> value="light"
<FormLabel>Theme</FormLabel> className="sr-only"
<FormDescription> />
Select the theme for the dashboard. </FormControl>
</FormDescription> <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
<FormMessage /> <div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
<RadioGroup <div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
onValueChange={field.onChange} <div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
defaultValue={field.value} <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
className="grid max-w-md grid-cols-2 gap-8 pt-2" </div>
> <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<FormItem> <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
<FormLabel className="[&:has([data-state=checked])>div]:border-primary"> <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
<FormControl> </div>
<RadioGroupItem value="light" className="sr-only" /> <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
</FormControl> <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
<div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent"> <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
<div className="space-y-2 rounded-sm bg-[#ecedef] p-2"> </div>
<div className="space-y-2 rounded-md bg-white p-2 shadow-sm"> </div>
<div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" /> </div>
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" /> <span className="block w-full p-2 text-center font-normal">
</div> Light
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm"> </span>
<div className="h-4 w-4 rounded-full bg-[#ecedef]" /> </FormLabel>
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" /> </FormItem>
</div> <FormItem>
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm"> <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
<div className="h-4 w-4 rounded-full bg-[#ecedef]" /> <FormControl>
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" /> <RadioGroupItem
</div> value="dark"
</div> className="sr-only"
</div> />
<span className="block w-full p-2 text-center font-normal"> </FormControl>
Light <div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
</span> <div className="space-y-2 rounded-sm bg-slate-950 p-2">
</FormLabel> <div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
</FormItem> <div className="h-2 w-[80px] rounded-lg bg-slate-400" />
<FormItem> <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
<FormLabel className="[&:has([data-state=checked])>div]:border-primary"> </div>
<FormControl> <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<RadioGroupItem value="dark" className="sr-only" /> <div className="h-4 w-4 rounded-full bg-slate-400" />
</FormControl> <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
<div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground"> </div>
<div className="space-y-2 rounded-sm bg-slate-950 p-2"> <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm"> <div className="h-4 w-4 rounded-full bg-slate-400" />
<div className="h-2 w-[80px] rounded-lg bg-slate-400" /> <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
<div className="h-2 w-[100px] rounded-lg bg-slate-400" /> </div>
</div> </div>
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm"> </div>
<div className="h-4 w-4 rounded-full bg-slate-400" /> <span className="block w-full p-2 text-center font-normal">
<div className="h-2 w-[100px] rounded-lg bg-slate-400" /> Dark
</div> </span>
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm"> </FormLabel>
<div className="h-4 w-4 rounded-full bg-slate-400" /> </FormItem>
<div className="h-2 w-[100px] rounded-lg bg-slate-400" /> </RadioGroup>
</div> </FormItem>
</div> )}
</div> />
<span className="block w-full p-2 text-center font-normal">
Dark
</span>
</FormLabel>
</FormItem>
</RadioGroup>
</FormItem>
)}
/>
<Button type="submit">Update preferences</Button> <Button type="submit">Update preferences</Button>
</form> </form>
</Form> </Form>
) );
} }

View file

@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { import {

View file

@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { import {

View file

@ -6,7 +6,7 @@ import { useFieldArray, useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {

View file

@ -1,6 +1,6 @@
"use client" "use client"
import { useToast } from "@/hooks/use-toast" import { useToast } from "@/hooks/useToast"
import { import {
Toast, Toast,
ToastClose, ToastClose,

View file

@ -0,0 +1,11 @@
import { GetOrgResponse } from "@server/routers/org";
import { createContext } from "react";
interface OrgContextType {
org: GetOrgResponse | null;
updateOrg: (updateOrg: Partial<GetOrgResponse>) => void;
}
const OrgContext = createContext<OrgContextType | undefined>(undefined);
export default OrgContext;

View file

@ -3,9 +3,11 @@ import { createContext } from "react";
interface ResourceContextType { interface ResourceContextType {
resource: GetResourceResponse | null; resource: GetResourceResponse | null;
updateResource: (updatedResource: Partial<GetResourceResponse>) => Promise<void>; updateResource: (updatedResource: Partial<GetResourceResponse>) => void;
} }
const ResourceContext = createContext<ResourceContextType | undefined>(undefined); const ResourceContext = createContext<ResourceContextType | undefined>(
undefined
);
export default ResourceContext; export default ResourceContext;

View file

@ -3,9 +3,9 @@ import { createContext } from "react";
interface SiteContextType { interface SiteContextType {
site: GetSiteResponse | null; site: GetSiteResponse | null;
updateSite: (updatedSite: Partial<GetSiteResponse>) => Promise<void>; updateSite: (updatedSite: Partial<GetSiteResponse>) => void;
} }
const SiteContext = createContext<SiteContextType | undefined>(undefined); const SiteContext = createContext<SiteContextType | undefined>(undefined);
export default SiteContext; export default SiteContext;

View file

@ -0,0 +1,10 @@
import OrgContext from "@app/contexts/orgContext";
import { useContext } from "react";
export function useOrgContext() {
const context = useContext(OrgContext);
if (context === undefined) {
throw new Error("useOrgContext must be used within a OrgProvider");
}
return context;
}

View file

@ -4,7 +4,9 @@ import { useContext } from "react";
export function useResourceContext() { export function useResourceContext() {
const context = useContext(ResourceContext); const context = useContext(ResourceContext);
if (context === undefined) { if (context === undefined) {
throw new Error('useResourceContext must be used within a ResourceProvider'); throw new Error(
"useResourceContext must be used within a ResourceProvider"
);
} }
return context; return context;
} }

View file

@ -14,4 +14,4 @@ export async function verifySession(): Promise<GetUserResponse | null> {
} catch { } catch {
return null; return null;
} }
} }

View file

@ -0,0 +1,39 @@
"use client";
import OrgContext from "@app/contexts/orgContext";
import { GetOrgResponse } from "@server/routers/org";
import { useState } from "react";
interface OrgProviderProps {
children: React.ReactNode;
org: GetOrgResponse | null;
}
export function OrgProvider({ children, org: serverOrg }: OrgProviderProps) {
const [org, setOrg] = useState<GetOrgResponse | null>(serverOrg);
const updateOrg = (updatedOrg: Partial<GetOrgResponse>) => {
if (!org) {
throw new Error("No org to update");
}
setOrg((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
...updatedOrg,
};
});
};
return (
<OrgContext.Provider value={{ org, updateOrg }}>
{children}
</OrgContext.Provider>
);
}
export default OrgProvider;

View file

@ -1,10 +1,7 @@
"use client"; "use client";
import api from "@app/api";
import ResourceContext from "@app/contexts/resourceContext"; import ResourceContext from "@app/contexts/resourceContext";
import { toast } from "@app/hooks/use-toast";
import { GetResourceResponse } from "@server/routers/resource/getResource"; import { GetResourceResponse } from "@server/routers/resource/getResource";
import { AxiosResponse } from "axios";
import { useState } from "react"; import { useState } from "react";
interface ResourceProviderProps { interface ResourceProviderProps {
@ -12,34 +9,36 @@ interface ResourceProviderProps {
resource: GetResourceResponse | null; resource: GetResourceResponse | null;
} }
export function ResourceProvider({ children, resource: serverResource }: ResourceProviderProps) { export function ResourceProvider({
const [resource, setResource] = useState<GetResourceResponse | null>(serverResource); children,
resource: serverResource,
}: ResourceProviderProps) {
const [resource, setResource] = useState<GetResourceResponse | null>(
serverResource
);
const updateResource = async (updatedResource: Partial<GetResourceResponse>) => { const updateResource = (updatedResource: Partial<GetResourceResponse>) => {
try { if (!resource) {
if (!resource) { throw new Error("No resource to update");
throw new Error("No resource to update"); }
setResource((prev) => {
if (!prev) {
return prev;
} }
const res = await api.post<AxiosResponse<GetResourceResponse>>( return {
`resource/${resource.resourceId}`, ...prev,
updatedResource, ...updatedResource,
); };
setResource(res.data.data); });
toast({
title: "Resource updated!",
});
} catch (error) {
console.error(error);
toast({
variant: "destructive",
title: "Error updating resource...",
})
}
}; };
return (
return <ResourceContext.Provider value={{ resource, updateResource }}>{children}</ResourceContext.Provider>; <ResourceContext.Provider value={{ resource, updateResource }}>
{children}
</ResourceContext.Provider>
);
} }
export default ResourceProvider; export default ResourceProvider;

View file

@ -1,10 +1,7 @@
"use client"; "use client";
import api from "@app/api";
import SiteContext from "@app/contexts/siteContext"; import SiteContext from "@app/contexts/siteContext";
import { toast } from "@app/hooks/use-toast";
import { GetSiteResponse } from "@server/routers/site/getSite"; import { GetSiteResponse } from "@server/routers/site/getSite";
import { AxiosResponse } from "axios";
import { useState } from "react"; import { useState } from "react";
interface SiteProviderProps { interface SiteProviderProps {
@ -12,34 +9,32 @@ interface SiteProviderProps {
site: GetSiteResponse | null; site: GetSiteResponse | null;
} }
export function SiteProvider({ children, site: serverSite }: SiteProviderProps) { export function SiteProvider({
children,
site: serverSite,
}: SiteProviderProps) {
const [site, setSite] = useState<GetSiteResponse | null>(serverSite); const [site, setSite] = useState<GetSiteResponse | null>(serverSite);
const updateSite = async (updatedSite: Partial<GetSiteResponse>) => { const updateSite = (updatedSite: Partial<GetSiteResponse>) => {
try { if (!site) {
if (!site) { throw new Error("No site to update");
throw new Error("No site to update");
}
const res = await api.post<AxiosResponse<GetSiteResponse>>(
`site/${site.siteId}`,
updatedSite,
);
setSite(res.data.data);
toast({
title: "Site updated!",
});
} catch (error) {
console.error(error);
toast({
variant: "destructive",
title: "Error updating site...",
})
} }
setSite((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
...updatedSite,
};
});
}; };
return (
return <SiteContext.Provider value={{ site, updateSite }}>{children}</SiteContext.Provider>; <SiteContext.Provider value={{ site, updateSite }}>
{children}
</SiteContext.Provider>
);
} }
export default SiteProvider; export default SiteProvider;