Skip to content

Commit

Permalink
fix: wrong advanced filter formatting handling
Browse files Browse the repository at this point in the history
  • Loading branch information
DonKoko committed Dec 9, 2024
1 parent 32ab92f commit adea8f2
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function FilterOperatorDisplay({
}

/** Maps the FilterOperator to a user friendly name */
const operatorsMap: Record<FilterOperator, string[]> = {
export const operatorsMap: Record<FilterOperator, string[]> = {
is: ["=", "is"],
isNot: ["≠", "Is not"],
contains: ["∋", "Contains"],
Expand Down
9 changes: 6 additions & 3 deletions app/modules/asset/data.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,17 +219,20 @@ export async function advancedModeLoader({
filters,
serializedCookie: filtersCookie,
redirectNeeded,
} = await getAdvancedFiltersFromRequest(request, organizationId);
} = await getAdvancedFiltersFromRequest(request, organizationId, settings);

const currentFilterParams = new URLSearchParams(filters || "");
const searchParams = filters
? currentFilterParams
: getCurrentSearchParams(request);
const paramsValues = getParamsValues(searchParams);
const { teamMemberIds } = paramsValues;
if (filters && redirectNeeded) {

if (redirectNeeded) {
const cookieParams = new URLSearchParams(filters);
return redirect(`/assets?${cookieParams.toString()}`);
return redirect(`/assets?${cookieParams.toString()}`, {
headers: filtersCookie ? [setCookie(filtersCookie)] : undefined,
});
}

/** Query tierLimit, assets & Asset index settings */
Expand Down
61 changes: 61 additions & 0 deletions app/modules/asset/utils.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Asset, AssetStatus, Location, Prisma } from "@prisma/client";
import { z } from "zod";
import { filterOperatorSchema } from "~/components/assets/assets-index/advanced-filters/schema";
import { getParamsValues } from "~/utils/list";
import type { Column } from "../asset-index-settings/helpers";

export function getLocationUpdateNoteContent({
currentLocation,
Expand Down Expand Up @@ -144,3 +146,62 @@ export function getAssetsWhereInput({

return where;
}

/**
* Schema for validating advanced filter parameter format
* Validates the 'operator:value' format and ensures operator is valid
*/
export const advancedFilterFormatSchema = z.string().refine(
(value) => {
const parts = value.split(":");
if (parts.length !== 2) return false;

const [operator] = parts;
return filterOperatorSchema.safeParse(operator).success;
},
{
message: "Filter must be in format 'operator:value' with valid operator",
}
);

/**
* Validates if a filter value matches the expected advanced filter format
* Uses Zod schema for strict type validation
* @param value - The filter value to validate
* @returns boolean indicating if the value matches advanced filter format
*/
function isValidAdvancedFilterFormat(value: string): boolean {
return advancedFilterFormatSchema.safeParse(value).success;
}

/**
* Validates and sanitizes URL parameters for advanced index mode
* Removes any parameters that don't match the expected advanced filter format
* @param searchParams - The URL search parameters to validate
* @param columns - The configured columns for advanced index
* @returns Validated and sanitized search parameters
*/
export function validateAdvancedFilterParams(
searchParams: URLSearchParams,
columns: Column[]
): URLSearchParams {
const validatedParams = new URLSearchParams();
const columnNames = columns.map((col) => col.name);

// Iterate through all parameters
searchParams.forEach((value, key) => {
// Preserve non-filter params (pagination, sorting, etc)
if (!columnNames.includes(key as any)) {
validatedParams.append(key, value);
return;
}

// Validate filter format for column parameters
if (isValidAdvancedFilterFormat(value)) {
validatedParams.append(key, value);
}
// Invalid format - parameter will be dropped
});

return validatedParams;
}
92 changes: 73 additions & 19 deletions app/utils/cookies.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { AssetIndexSettings } from "@prisma/client";
import { createCookie } from "@remix-run/node"; // or cloudflare/deno

import type { Cookie } from "@remix-run/node";
import { cleanParamsForCookie } from "~/hooks/search-params";
import { advancedFilterFormatSchema } from "~/modules/asset/utils.server";
import type { Column } from "~/modules/asset-index-settings/helpers";
import { getCurrentSearchParams } from "./http.server";

// find cookie by name from request headers
Expand Down Expand Up @@ -141,36 +144,87 @@ export const createAdvancedAssetFilterCookie = (orgId: string) =>
maxAge: 60 * 60 * 24 * 365, // 1 year
});

/**
* Gets and validates advanced filters from request parameters
* Ensures URL parameters match the expected advanced filter format
* @param request - The incoming request
* @param organizationId - The organization ID for the request
* @param settings - The asset index settings containing column configuration
* @returns Object containing filters, serialized cookie, and redirect status
*/
export async function getAdvancedFiltersFromRequest(
request: Request,
organizationId: string
) {
organizationId: string,
settings: AssetIndexSettings
): Promise<{
filters: string | undefined;
serializedCookie: string | undefined;
redirectNeeded: boolean;
}> {
let filters = getCurrentSearchParams(request).toString();
const cookieHeader = request.headers.get("Cookie");
const advancedAssetFilterCookie =
createAdvancedAssetFilterCookie(organizationId);

const assetFilterCookie = createAdvancedAssetFilterCookie(organizationId);
if (filters) {
// Clean filters before storing in cookie
const cleanedFilters = cleanParamsForCookie(filters);
// Only serialize to cookie if we have filters after cleaning
const serializedCookie = cleanedFilters
? await assetFilterCookie.serialize(cleanedFilters)
: null;
const validatedParams = new URLSearchParams();
const columnNames = (settings.columns as Column[]).map((col) => col.name);

// Return original filters for URL but cleaned cookie
return { filters, serializedCookie };
} else if (cookieHeader) {
// Use existing cookie filter but clean it
filters = (await assetFilterCookie.parse(cookieHeader)) || {};
const cleanedFilters = cleanParamsForCookie(filters);
new URLSearchParams(filters).forEach((value, key) => {
if (!columnNames.includes(key as any)) {
validatedParams.append(key, value);
return;
}

if (advancedFilterFormatSchema.safeParse(value).success) {
validatedParams.append(key, value);
}
});

const validatedParamsString = validatedParams.toString();
const cleanedFilters = cleanParamsForCookie(validatedParamsString);

// Only redirect if we have filters after cleaning
return {
filters: cleanedFilters,
redirectNeeded: !!cleanedFilters,
filters: validatedParamsString,
serializedCookie: cleanedFilters
? await advancedAssetFilterCookie.serialize(cleanedFilters)
: undefined,
redirectNeeded: validatedParamsString !== filters,
};
} else if (cookieHeader) {
filters = (await advancedAssetFilterCookie.parse(cookieHeader)) || "";

if (filters) {
const validatedParams = new URLSearchParams();
const columnNames = (settings.columns as Column[]).map((col) => col.name);

new URLSearchParams(filters).forEach((value, key) => {
if (!columnNames.includes(key as any)) {
validatedParams.append(key, value);
return;
}

if (advancedFilterFormatSchema.safeParse(value).success) {
validatedParams.append(key, value);
}
});

const validatedParamsString = validatedParams.toString();
const cleanedFilters = cleanParamsForCookie(validatedParamsString);

return {
filters: cleanedFilters || undefined,
serializedCookie: undefined,
redirectNeeded: !!cleanedFilters,
};
}
}
return { filters };

return {
filters: "",
serializedCookie: undefined,
redirectNeeded: false,
};
}

/** HIDE PWA INSTALL PROMPT COOKIE */
Expand Down
3 changes: 2 additions & 1 deletion app/utils/csv.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ export async function exportAssetsFromIndexToCsv({
/** Parse filters */
const { filters } = await getAdvancedFiltersFromRequest(
request,
organizationId
organizationId,
settings
);

/** Make an array of the ids and check if we have to take all */
Expand Down

0 comments on commit adea8f2

Please sign in to comment.