mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-29 23:34:56 -05:00
Stricter validation for custom CSS vars
This commit is contained in:
parent
22e954cb9e
commit
aaf865e8d0
|
|
@ -1,21 +1,11 @@
|
|||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CUSTOM_CSS_VAR_COLORS } from "~/constants";
|
||||
import { Button } from "./Button";
|
||||
import { Label } from "./Label";
|
||||
|
||||
const CUSTOM_COLORS = [
|
||||
"bg",
|
||||
"bg-darker",
|
||||
"bg-lighter",
|
||||
"text",
|
||||
"text-lighter",
|
||||
"theme",
|
||||
"theme-secondary",
|
||||
"chat",
|
||||
] as const;
|
||||
|
||||
type CustomColorsRecord = Partial<
|
||||
Record<(typeof CUSTOM_COLORS)[number], string>
|
||||
Record<(typeof CUSTOM_CSS_VAR_COLORS)[number], string>
|
||||
>;
|
||||
|
||||
export function CustomizedColorsInput({
|
||||
|
|
@ -33,7 +23,7 @@ export function CustomizedColorsInput({
|
|||
<Label>{t("custom.colors.title")}</Label>
|
||||
<input type="hidden" name="css" value={JSON.stringify(colors)} />
|
||||
<div className="colors__grid">
|
||||
{CUSTOM_COLORS.map((cssVar) => {
|
||||
{CUSTOM_CSS_VAR_COLORS.map((cssVar) => {
|
||||
return (
|
||||
<React.Fragment key={cssVar}>
|
||||
<div>{t(`custom.colors.${cssVar}`)}</div>
|
||||
|
|
|
|||
|
|
@ -134,3 +134,14 @@ export const PATCHES = [
|
|||
// date: "2023-08-30",
|
||||
// },
|
||||
];
|
||||
|
||||
export const CUSTOM_CSS_VAR_COLORS = [
|
||||
"bg",
|
||||
"bg-darker",
|
||||
"bg-lighter",
|
||||
"text",
|
||||
"text-lighter",
|
||||
"theme",
|
||||
"theme-secondary",
|
||||
"chat",
|
||||
] as const;
|
||||
|
|
|
|||
71
app/features/team/actions/t.$customUrl.edit.server.test.ts
Normal file
71
app/features/team/actions/t.$customUrl.edit.server.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { dbInsertUsers, dbReset, wrappedAction } from "~/utils/Test";
|
||||
import { action as teamIndexPageAction } from "../actions/t.server";
|
||||
import type { editTeamSchema } from "../team-schemas.server";
|
||||
import type { createTeamSchema } from "../team-schemas.server";
|
||||
import { action as _editTeamProfileAction } from "./t.$customUrl.edit.server";
|
||||
|
||||
const createTeamAction = wrappedAction<typeof createTeamSchema>({
|
||||
action: teamIndexPageAction,
|
||||
});
|
||||
|
||||
const editTeamProfileAction = wrappedAction<typeof editTeamSchema>({
|
||||
action: _editTeamProfileAction,
|
||||
});
|
||||
|
||||
const DEFAULT_FIELDS = {
|
||||
_action: "EDIT",
|
||||
name: "Team 1",
|
||||
bio: "",
|
||||
bsky: "",
|
||||
} as const;
|
||||
|
||||
describe("team page editing", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers();
|
||||
await createTeamAction({ name: "Team 1" }, { user: "regular" });
|
||||
});
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
it("adds valid custom css vars", async () => {
|
||||
const response = await editTeamProfileAction(
|
||||
{
|
||||
css: JSON.stringify({ bg: "#fff" }),
|
||||
...DEFAULT_FIELDS,
|
||||
},
|
||||
{ user: "regular", params: { customUrl: "team-1" } },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(302);
|
||||
});
|
||||
|
||||
it("prevents adding custom css var of unknown property", async () => {
|
||||
await expect(
|
||||
editTeamProfileAction(
|
||||
{
|
||||
css: JSON.stringify({
|
||||
"backdrop-filter": "#fff",
|
||||
}),
|
||||
...DEFAULT_FIELDS,
|
||||
},
|
||||
{ user: "regular", params: { customUrl: "team-1" } },
|
||||
),
|
||||
).rejects.toThrow("status code: 400");
|
||||
});
|
||||
|
||||
it("prevents adding custom css var of unknown value", async () => {
|
||||
await expect(
|
||||
editTeamProfileAction(
|
||||
{
|
||||
css: JSON.stringify({
|
||||
bg: "url(https://sendou.ink/u?q=1&_data=features%2Fuser-search%2Froutes%2Fu)",
|
||||
}),
|
||||
...DEFAULT_FIELDS,
|
||||
},
|
||||
{ user: "regular", params: { customUrl: "team-1" } },
|
||||
),
|
||||
).rejects.toThrow("status code: 400");
|
||||
});
|
||||
});
|
||||
|
|
@ -12,6 +12,10 @@ const editTeamAction = wrappedAction<typeof editTeamSchema>({
|
|||
action: _editTeamAction,
|
||||
});
|
||||
|
||||
const DEFAULT_FIELDS = {
|
||||
bio: null,
|
||||
} as any;
|
||||
|
||||
describe("team creation", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers();
|
||||
|
|
@ -28,9 +32,7 @@ describe("team creation", () => {
|
|||
{
|
||||
_action: "EDIT",
|
||||
name: "Team 2",
|
||||
bio: null,
|
||||
bsky: null,
|
||||
css: null,
|
||||
...DEFAULT_FIELDS,
|
||||
},
|
||||
{ user: "regular", params: { customUrl: "team-1" } },
|
||||
);
|
||||
|
|
@ -46,9 +48,7 @@ describe("team creation", () => {
|
|||
{
|
||||
_action: "EDIT",
|
||||
name: "𝓢𝓲𝓵",
|
||||
bio: null,
|
||||
bsky: null,
|
||||
css: null,
|
||||
...DEFAULT_FIELDS,
|
||||
},
|
||||
{ user: "regular", params: { customUrl: "team-1" } },
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { _action, falsyToNull, id, jsonParseable } from "~/utils/zod";
|
||||
import { _action, customCssVarObject, falsyToNull, id } from "~/utils/zod";
|
||||
import { TEAM, TEAM_MEMBER_ROLES } from "./team-constants";
|
||||
|
||||
export const teamParamsSchema = z.object({ customUrl: z.string() });
|
||||
|
|
@ -32,7 +32,7 @@ export const editTeamSchema = z.union([
|
|||
falsyToNull,
|
||||
z.string().max(TEAM.BSKY_MAX_LENGTH).nullable(),
|
||||
),
|
||||
css: z.preprocess(falsyToNull, z.string().refine(jsonParseable).nullable()),
|
||||
css: customCssVarObject,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
|||
79
app/features/user-page/routes/u.$identifier.edit.test.ts
Normal file
79
app/features/user-page/routes/u.$identifier.edit.test.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists";
|
||||
import { dbInsertUsers, dbReset, wrappedAction } from "~/utils/Test";
|
||||
import {
|
||||
action as editUserProfileAction,
|
||||
type userEditActionSchema,
|
||||
} from "./u.$identifier.edit";
|
||||
|
||||
const action = wrappedAction<typeof userEditActionSchema>({
|
||||
action: editUserProfileAction,
|
||||
});
|
||||
|
||||
const DEFAULT_FIELDS = {
|
||||
battlefy: null,
|
||||
bio: null,
|
||||
commissionsOpen: 1,
|
||||
commissionText: null,
|
||||
country: "FI",
|
||||
customName: null,
|
||||
customUrl: null,
|
||||
favoriteBadgeId: null,
|
||||
inGameNameDiscriminator: null,
|
||||
inGameNameText: null,
|
||||
motionSens: null,
|
||||
showDiscordUniqueName: 1,
|
||||
stickSens: null,
|
||||
weapons: JSON.stringify([
|
||||
{ weaponSplId: 1 as MainWeaponId, isFavorite: 0 },
|
||||
]) as any,
|
||||
};
|
||||
|
||||
describe("user page editing", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers();
|
||||
});
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
it("adds valid custom css vars", async () => {
|
||||
const response = await action(
|
||||
{
|
||||
css: JSON.stringify({ bg: "#fff" }),
|
||||
...DEFAULT_FIELDS,
|
||||
},
|
||||
{ user: "regular", params: { identifier: "2" } },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(302);
|
||||
});
|
||||
|
||||
it("prevents adding custom css var of unknown property", async () => {
|
||||
const res = await action(
|
||||
{
|
||||
css: JSON.stringify({
|
||||
"backdrop-filter": "#fff",
|
||||
}),
|
||||
...DEFAULT_FIELDS,
|
||||
},
|
||||
{ user: "regular", params: { identifier: "2" } },
|
||||
);
|
||||
|
||||
expect(res.errors[0]).toBe("Invalid custom CSS var object");
|
||||
});
|
||||
|
||||
it("prevents adding custom css var of unknown value", async () => {
|
||||
const res = await action(
|
||||
{
|
||||
css: JSON.stringify({
|
||||
bg: "url(https://sendou.ink/u?q=1&_data=features%2Fuser-search%2Froutes%2Fu)",
|
||||
}),
|
||||
...DEFAULT_FIELDS,
|
||||
},
|
||||
{ user: "regular", params: { identifier: "2" } },
|
||||
);
|
||||
|
||||
expect(res.errors[0]).toBe("Invalid custom CSS var object");
|
||||
});
|
||||
});
|
||||
|
|
@ -42,10 +42,10 @@ import { FAQ_PAGE, isCustomUrl, userPage } from "~/utils/urls";
|
|||
import {
|
||||
actualNumber,
|
||||
checkboxValueToDbBoolean,
|
||||
customCssVarObject,
|
||||
dbBoolean,
|
||||
falsyToNull,
|
||||
id,
|
||||
jsonParseable,
|
||||
processMany,
|
||||
safeJSONParse,
|
||||
undefinedToNull,
|
||||
|
|
@ -56,7 +56,7 @@ import type { UserPageLoaderData } from "./u.$identifier";
|
|||
import "~/styles/u-edit.css";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
|
||||
const userEditActionSchema = z
|
||||
export const userEditActionSchema = z
|
||||
.object({
|
||||
country: z.preprocess(
|
||||
falsyToNull,
|
||||
|
|
@ -127,7 +127,7 @@ const userEditActionSchema = z
|
|||
.refine((val) => /^[0-9a-z]{4,5}$/.test(val))
|
||||
.nullable(),
|
||||
),
|
||||
css: z.preprocess(falsyToNull, z.string().refine(jsonParseable).nullable()),
|
||||
css: customCssVarObject,
|
||||
weapons: z.preprocess(
|
||||
safeJSONParse,
|
||||
z
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { ZodType } from "zod";
|
||||
import { z } from "zod";
|
||||
import { CUSTOM_CSS_VAR_COLORS } from "~/constants";
|
||||
import type { abilitiesShort } from "~/modules/in-game-lists";
|
||||
import { abilities, mainWeaponIds, stageIds } from "~/modules/in-game-lists";
|
||||
import { FRIEND_CODE_REGEXP } from "../features/sendouq/q-constants";
|
||||
|
|
@ -15,7 +16,8 @@ export const nonEmptyString = z.string().trim().min(1, {
|
|||
|
||||
export const dbBoolean = z.coerce.number().min(0).max(1).int();
|
||||
|
||||
export const hexCode = z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
||||
const hexCodeRegex = /^#(?:[0-9a-fA-F]{3}){1,2}$/; // https://stackoverflow.com/a/1636354
|
||||
export const hexCode = z.string().regex(hexCodeRegex);
|
||||
|
||||
const abilityNameToType = (val: string) =>
|
||||
abilities.find((ability) => ability.name === val)?.type;
|
||||
|
|
@ -262,3 +264,27 @@ export const dayMonthYear = z.object({
|
|||
});
|
||||
|
||||
export type DayMonthYear = z.infer<typeof dayMonthYear>;
|
||||
|
||||
export const customCssVarObject = z.preprocess(
|
||||
falsyToNull,
|
||||
z.string().nullable().refine(validSerializedCustomCssVarObject, {
|
||||
message: "Invalid custom CSS var object",
|
||||
}),
|
||||
);
|
||||
|
||||
function validSerializedCustomCssVarObject(value: unknown) {
|
||||
if (!value) return true;
|
||||
|
||||
try {
|
||||
const parsedValue = JSON.parse(value as string);
|
||||
|
||||
for (const [key, value] of Object.entries(parsedValue)) {
|
||||
if (!CUSTOM_CSS_VAR_COLORS.includes(key as any)) return false;
|
||||
if (!hexCodeRegex.test(value as string)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user