mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-10 05:54:58 +02:00
commit
6fba13c8d1
17 changed files with 1401 additions and 28 deletions
|
@ -51,13 +51,17 @@ export enum ActionsEnum {
|
|||
// removeUserAction = "removeUserAction",
|
||||
// removeUserSite = "removeUserSite",
|
||||
getOrgUser = "getOrgUser",
|
||||
"setResourcePassword" = "setResourcePassword",
|
||||
"setResourcePincode" = "setResourcePincode",
|
||||
"setResourceWhitelist" = "setResourceWhitelist",
|
||||
"getResourceWhitelist" = "getResourceWhitelist",
|
||||
"generateAccessToken" = "generateAccessToken",
|
||||
"deleteAcessToken" = "deleteAcessToken",
|
||||
"listAccessTokens" = "listAccessTokens"
|
||||
setResourcePassword = "setResourcePassword",
|
||||
setResourcePincode = "setResourcePincode",
|
||||
setResourceWhitelist = "setResourceWhitelist",
|
||||
getResourceWhitelist = "getResourceWhitelist",
|
||||
generateAccessToken = "generateAccessToken",
|
||||
deleteAcessToken = "deleteAcessToken",
|
||||
listAccessTokens = "listAccessTokens",
|
||||
createResourceRule = "createResourceRule",
|
||||
deleteResourceRule = "deleteResourceRule",
|
||||
listResourceRules = "listResourceRules",
|
||||
updateResourceRule = "updateResourceRule",
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
|
|
@ -54,7 +54,8 @@ export const resources = sqliteTable("resources", {
|
|||
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isBaseDomain: integer("isBaseDomain", { mode: "boolean" })
|
||||
isBaseDomain: integer("isBaseDomain", { mode: "boolean" }),
|
||||
applyRules: integer("applyRules", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
export const targets = sqliteTable("targets", {
|
||||
|
@ -371,6 +372,16 @@ export const versionMigrations = sqliteTable("versionMigrations", {
|
|||
executedAt: integer("executedAt").notNull()
|
||||
});
|
||||
|
||||
export const resourceRules = sqliteTable("resourceRules", {
|
||||
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
action: text("action").notNull(), // ACCEPT, DROP
|
||||
match: text("match").notNull(), // CIDR, PATH, IP
|
||||
value: text("value").notNull()
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
|
@ -403,3 +414,4 @@ export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
|||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
|
||||
export type ResourceRule = InferSelectModel<typeof resourceRules>;
|
|
@ -2,7 +2,7 @@ import path from "path";
|
|||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.0.0-beta.12";
|
||||
export const APP_VERSION = "1.0.0-beta.13";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { fromError } from "zod-validation-error";
|
|||
import { response } from "@server/lib/response";
|
||||
import db from "@server/db";
|
||||
import {
|
||||
resourceRules,
|
||||
ResourceAccessToken,
|
||||
ResourcePassword,
|
||||
resourcePassword,
|
||||
|
@ -14,7 +15,8 @@ import {
|
|||
resources,
|
||||
sessions,
|
||||
userOrgs,
|
||||
users
|
||||
users,
|
||||
ResourceRule
|
||||
} from "@server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
|
@ -28,6 +30,7 @@ import logger from "@server/logger";
|
|||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import NodeCache from "node-cache";
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { isIpInCidr } from "@server/lib/ip";
|
||||
|
||||
// We'll see if this speeds anything up
|
||||
const cache = new NodeCache({
|
||||
|
@ -79,6 +82,7 @@ export async function verifyResourceSession(
|
|||
host,
|
||||
originalRequestURL,
|
||||
requestIp,
|
||||
path,
|
||||
accessToken: token
|
||||
} = parsedBody.data;
|
||||
|
||||
|
@ -146,6 +150,25 @@ export async function verifyResourceSession(
|
|||
return allowed(res);
|
||||
}
|
||||
|
||||
// check the rules
|
||||
if (resource.applyRules) {
|
||||
const action = await checkRules(
|
||||
resource.resourceId,
|
||||
clientIp,
|
||||
path
|
||||
);
|
||||
|
||||
if (action == "ACCEPT") {
|
||||
logger.debug("Resource allowed by rule");
|
||||
return allowed(res);
|
||||
} else if (action == "DROP") {
|
||||
logger.debug("Resource denied by rule");
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
// otherwise its undefined and we pass
|
||||
}
|
||||
|
||||
const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||
|
||||
// check for access token
|
||||
|
@ -438,3 +461,93 @@ async function isUserAllowedToAccessResource(
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkRules(
|
||||
resourceId: number,
|
||||
clientIp: string | undefined,
|
||||
path: string | undefined
|
||||
): Promise<"ACCEPT" | "DROP" | undefined> {
|
||||
const ruleCacheKey = `rules:${resourceId}`;
|
||||
|
||||
let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey);
|
||||
|
||||
if (!rules) {
|
||||
rules = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId));
|
||||
|
||||
cache.set(ruleCacheKey, rules);
|
||||
}
|
||||
|
||||
if (rules.length === 0) {
|
||||
logger.debug("No rules found for resource", resourceId);
|
||||
return;
|
||||
}
|
||||
|
||||
let hasAcceptRule = false;
|
||||
|
||||
// First pass: look for DROP rules
|
||||
for (const rule of rules) {
|
||||
if (
|
||||
(clientIp &&
|
||||
rule.match == "CIDR" &&
|
||||
isIpInCidr(clientIp, rule.value) &&
|
||||
rule.action === "DROP") ||
|
||||
(clientIp &&
|
||||
rule.match == "IP" &&
|
||||
clientIp == rule.value &&
|
||||
rule.action === "DROP") ||
|
||||
(path &&
|
||||
rule.match == "PATH" &&
|
||||
urlGlobToRegex(rule.value).test(path) &&
|
||||
rule.action === "DROP")
|
||||
) {
|
||||
return "DROP";
|
||||
}
|
||||
// Track if we see any ACCEPT rules for the second pass
|
||||
if (rule.action === "ACCEPT") {
|
||||
hasAcceptRule = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: only check ACCEPT rules if we found one and didn't find a DROP
|
||||
if (hasAcceptRule) {
|
||||
for (const rule of rules) {
|
||||
if (rule.action !== "ACCEPT") continue;
|
||||
|
||||
if (
|
||||
(clientIp &&
|
||||
rule.match == "CIDR" &&
|
||||
isIpInCidr(clientIp, rule.value)) ||
|
||||
(clientIp &&
|
||||
rule.match == "IP" &&
|
||||
clientIp == rule.value) ||
|
||||
(path &&
|
||||
rule.match == "PATH" &&
|
||||
urlGlobToRegex(rule.value).test(path))
|
||||
) {
|
||||
return "ACCEPT";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function urlGlobToRegex(pattern: string): RegExp {
|
||||
// Trim any leading or trailing slashes
|
||||
pattern = pattern.replace(/^\/+|\/+$/g, "");
|
||||
|
||||
// Escape special regex characters except *
|
||||
const escapedPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
// Replace * with regex pattern for any valid URL segment characters
|
||||
const regexPattern = escapedPattern.replace(/\*/g, "[a-zA-Z0-9_-]+");
|
||||
|
||||
// Create the final pattern that:
|
||||
// 1. Optionally matches leading slash
|
||||
// 2. Matches the pattern
|
||||
// 3. Optionally matches trailing slash
|
||||
return new RegExp(`^/?${regexPattern}/?$`);
|
||||
}
|
|
@ -186,6 +186,32 @@ authenticated.get(
|
|||
verifyUserHasAction(ActionsEnum.listTargets),
|
||||
target.listTargets
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/rule",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule
|
||||
);
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/rules",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.listResourceRules),
|
||||
resource.listResourceRules
|
||||
);
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule
|
||||
);
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteResourceRule),
|
||||
resource.deleteResourceRule
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/target/:targetId",
|
||||
verifyTargetAccess,
|
||||
|
@ -205,6 +231,7 @@ authenticated.delete(
|
|||
target.deleteTarget
|
||||
);
|
||||
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/role",
|
||||
verifyOrgAccess,
|
||||
|
|
101
server/routers/resource/createResourceRule.ts
Normal file
101
server/routers/resource/createResourceRule.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourceRules, resources } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const createResourceRuleSchema = z
|
||||
.object({
|
||||
action: z.enum(["ACCEPT", "DROP"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH"]),
|
||||
value: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
const createResourceRuleParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function createResourceRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = createResourceRuleSchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { action, match, value } = parsedBody.data;
|
||||
|
||||
const parsedParams = createResourceRuleParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
// Verify that the referenced resource exists
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Create the new resource rule
|
||||
const [newRule] = await db
|
||||
.insert(resourceRules)
|
||||
.values({
|
||||
resourceId,
|
||||
action,
|
||||
match,
|
||||
value
|
||||
})
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: newRule,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource rule created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
71
server/routers/resource/deleteResourceRule.ts
Normal file
71
server/routers/resource/deleteResourceRule.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourceRules, resources } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const deleteResourceRuleSchema = z
|
||||
.object({
|
||||
ruleId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function deleteResourceRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = deleteResourceRuleSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { ruleId } = parsedParams.data;
|
||||
|
||||
// Delete the rule and return the deleted record
|
||||
const [deletedRule] = await db
|
||||
.delete(resourceRules)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.returning();
|
||||
|
||||
if (!deletedRule) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource rule with ID ${ruleId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource rule deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,3 +18,7 @@ export * from "./authWithWhitelist";
|
|||
export * from "./authWithAccessToken";
|
||||
export * from "./transferResource";
|
||||
export * from "./getExchangeToken";
|
||||
export * from "./createResourceRule";
|
||||
export * from "./deleteResourceRule";
|
||||
export * from "./listResourceRules";
|
||||
export * from "./updateResourceRule";
|
132
server/routers/resource/listResourceRules.ts
Normal file
132
server/routers/resource/listResourceRules.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
import { db } from "@server/db";
|
||||
import { resourceRules, resources } from "@server/db/schema";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
|
||||
const listResourceRulesParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
})
|
||||
.strict();
|
||||
|
||||
const listResourceRulesSchema = z.object({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("1000")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
offset: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative())
|
||||
});
|
||||
|
||||
function queryResourceRules(resourceId: number) {
|
||||
let baseQuery = db
|
||||
.select({
|
||||
ruleId: resourceRules.ruleId,
|
||||
resourceId: resourceRules.resourceId,
|
||||
action: resourceRules.action,
|
||||
match: resourceRules.match,
|
||||
value: resourceRules.value
|
||||
})
|
||||
.from(resourceRules)
|
||||
.leftJoin(resources, eq(resourceRules.resourceId, resources.resourceId))
|
||||
.where(eq(resourceRules.resourceId, resourceId));
|
||||
|
||||
return baseQuery;
|
||||
}
|
||||
|
||||
export type ListResourceRulesResponse = {
|
||||
rules: Awaited<ReturnType<typeof queryResourceRules>>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
export async function listResourceRules(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = listResourceRulesSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = listResourceRulesParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
// Verify the resource exists
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const baseQuery = queryResourceRules(resourceId);
|
||||
|
||||
let countQuery = db
|
||||
.select({ count: sql<number>`cast(count(*) as integer)` })
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId));
|
||||
|
||||
const rulesList = await baseQuery.limit(limit).offset(offset);
|
||||
const totalCountResult = await countQuery;
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
return response<ListResourceRulesResponse>(res, {
|
||||
data: {
|
||||
rules: rulesList,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource rules retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,7 +29,8 @@ const updateResourceBodySchema = z
|
|||
blockAccess: z.boolean().optional(),
|
||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||
emailWhitelistEnabled: z.boolean().optional(),
|
||||
isBaseDomain: z.boolean().optional()
|
||||
isBaseDomain: z.boolean().optional(),
|
||||
applyRules: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
|
|
130
server/routers/resource/updateResourceRule.ts
Normal file
130
server/routers/resource/updateResourceRule.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourceRules, resources } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const updateResourceRuleParamsSchema = z
|
||||
.object({
|
||||
ruleId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
})
|
||||
.strict();
|
||||
|
||||
// Define Zod schema for request body validation
|
||||
const updateResourceRuleSchema = z
|
||||
.object({
|
||||
action: z.enum(["ACCEPT", "DROP"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH"]).optional(),
|
||||
value: z.string().min(1).optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
message: "At least one field must be provided for update"
|
||||
});
|
||||
|
||||
export async function updateResourceRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
// Validate path parameters
|
||||
const parsedParams = updateResourceRuleParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Validate request body
|
||||
const parsedBody = updateResourceRuleSchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { ruleId, resourceId } = parsedParams.data;
|
||||
const updateData = parsedBody.data;
|
||||
|
||||
// Verify that the resource exists
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the rule exists and belongs to the specified resource
|
||||
const [existingRule] = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingRule) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource rule with ID ${ruleId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existingRule.resourceId !== resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Resource rule ${ruleId} does not belong to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update the rule
|
||||
const [updatedRule] = await db
|
||||
.update(resourceRules)
|
||||
.set(updateData)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: updatedRule,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource rule updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import m5 from "./scripts/1.0.0-beta6";
|
|||
import m6 from "./scripts/1.0.0-beta9";
|
||||
import m7 from "./scripts/1.0.0-beta10";
|
||||
import m8 from "./scripts/1.0.0-beta12";
|
||||
import m13 from "./scripts/1.0.0-beta13";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
|
@ -27,7 +28,8 @@ const migrations = [
|
|||
{ version: "1.0.0-beta.6", run: m5 },
|
||||
{ version: "1.0.0-beta.9", run: m6 },
|
||||
{ version: "1.0.0-beta.10", run: m7 },
|
||||
{ version: "1.0.0-beta.12", run: m8 }
|
||||
{ version: "1.0.0-beta.12", run: m8 },
|
||||
{ version: "1.0.0-beta.13", run: m13 }
|
||||
// Add new migrations here as they are created
|
||||
] as const;
|
||||
|
||||
|
|
29
server/setup/scripts/1.0.0-beta13.ts
Normal file
29
server/setup/scripts/1.0.0-beta13.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import db from "@server/db";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
export default async function migration() {
|
||||
console.log("Running setup script 1.0.0-beta.13...");
|
||||
|
||||
try {
|
||||
db.transaction((trx) => {
|
||||
trx.run(sql`CREATE TABLE resourceRules (
|
||||
ruleId integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
resourceId integer NOT NULL,
|
||||
action text NOT NULL,
|
||||
match text NOT NULL,
|
||||
value text NOT NULL,
|
||||
FOREIGN KEY (resourceId) REFERENCES resources(resourceId) ON UPDATE no action ON DELETE cascade
|
||||
);`);
|
||||
trx.run(
|
||||
sql`ALTER TABLE resources ADD applyRules integer DEFAULT false NOT NULL;`
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`Added new table and column: resourceRules, applyRules`);
|
||||
} catch (e) {
|
||||
console.log("Unable to add new table and column: resourceRules, applyRules");
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log("Done.");
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue