Skip to content

Commit bdb99ab

Browse files
authored
refactor(schemas): centralize regex constants (@byseif21, @fehmer) (#7710)
1 parent bdbfa9a commit bdb99ab

6 files changed

Lines changed: 96 additions & 53 deletions

File tree

frontend/src/ts/utils/misc.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { lastElementFromArray } from "./arrays";
22
import { Config } from "@monkeytype/schemas/configs";
33
import { Mode, Mode2, PersonalBests } from "@monkeytype/schemas/shared";
44
import { Result } from "@monkeytype/schemas/results";
5-
import { RankAndCount } from "@monkeytype/schemas/users";
5+
import { RankAndCount, UserNameSchema } from "@monkeytype/schemas/users";
66
import { roundTo2 } from "@monkeytype/util/numbers";
77
import { animate, AnimationParams } from "animejs";
88
import { ElementWithUtils } from "./dom";
@@ -147,11 +147,7 @@ export function escapeHTML<T extends string | null | undefined>(str: T): T {
147147

148148
export function isUsernameValid(name: string): boolean {
149149
if (name === null || name === undefined || name === "") return false;
150-
if (name.toLowerCase().includes("miodec")) return false;
151-
if (name.toLowerCase().includes("bitly")) return false;
152-
if (name.length > 14) return false;
153-
if (/^\..*/.test(name.toLowerCase())) return false;
154-
return /^[0-9a-zA-Z_.-]+$/.test(name);
150+
return UserNameSchema.safeParse(name).success;
155151
}
156152

157153
export function clearTimeouts(timeouts: (number | NodeJS.Timeout)[]): void {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, expect } from "vitest";
2+
import { nameWithSeparators, slug } from "../src/util";
3+
4+
describe("Schema Validation Tests", () => {
5+
describe("nameWithSeparators", () => {
6+
const schema = nameWithSeparators();
7+
8+
it("accepts valid names", () => {
9+
expect(schema.safeParse("valid_name").success).toBe(true);
10+
expect(schema.safeParse("valid-name").success).toBe(true);
11+
expect(schema.safeParse("valid123").success).toBe(true);
12+
expect(schema.safeParse("Valid_Name-Check").success).toBe(true);
13+
});
14+
15+
it("rejects leading/trailing separators", () => {
16+
expect(schema.safeParse("_invalid").success).toBe(false);
17+
expect(schema.safeParse("invalid-").success).toBe(false);
18+
});
19+
20+
it("rejects consecutive separators", () => {
21+
expect(schema.safeParse("inv__alid").success).toBe(false);
22+
expect(schema.safeParse("inv--alid").success).toBe(false);
23+
expect(schema.safeParse("inv-_alid").success).toBe(false);
24+
});
25+
26+
it("rejects dots", () => {
27+
expect(schema.safeParse("invalid.dot").success).toBe(false);
28+
expect(schema.safeParse(".invalid").success).toBe(false);
29+
});
30+
});
31+
32+
describe("slug", () => {
33+
const schema = slug();
34+
35+
it("accepts valid slugs", () => {
36+
expect(schema.safeParse("valid-slug.123_test").success).toBe(true);
37+
expect(schema.safeParse("valid.dots").success).toBe(true);
38+
expect(schema.safeParse("_leading_underscore_is_fine").success).toBe(
39+
true,
40+
);
41+
expect(schema.safeParse("-leading_hyphen_is_fine").success).toBe(true);
42+
expect(schema.safeParse("trailing_is_fine_in_slug_").success).toBe(true);
43+
});
44+
45+
it("rejects leading dots", () => {
46+
expect(schema.safeParse(".invalid").success).toBe(false);
47+
});
48+
49+
it("rejects invalid characters", () => {
50+
expect(schema.safeParse("invalid,comma").success).toBe(false);
51+
expect(schema.safeParse(",invalid").success).toBe(false);
52+
expect(schema.safeParse("invalid space").success).toBe(false);
53+
expect(schema.safeParse("invalid#hash").success).toBe(false);
54+
});
55+
});
56+
});

packages/schemas/src/ape-keys.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { z } from "zod";
2-
import { IdSchema } from "./util";
2+
import { IdSchema, slug } from "./util";
33

4-
export const ApeKeyNameSchema = z
5-
.string()
6-
.regex(/^[0-9a-zA-Z_.-]+$/)
7-
.max(20);
4+
export const ApeKeyNameSchema = slug().max(20);
85

96
export const ApeKeyUserDefinedSchema = z.object({
107
name: ApeKeyNameSchema,

packages/schemas/src/presets.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { z } from "zod";
2-
import { IdSchema, TagSchema } from "./util";
2+
import { IdSchema, nameWithSeparators, TagSchema } from "./util";
33
import {
44
ConfigGroupName,
55
ConfigGroupNameSchema,
66
PartialConfigSchema,
77
} from "./configs";
88

9-
export const PresetNameSchema = z
10-
.string()
11-
.regex(/^[0-9a-zA-Z_-]+$/)
12-
.max(16);
9+
export const PresetNameSchema = nameWithSeparators().max(16);
1310
export type PresetName = z.infer<typeof PresetNameSchema>;
1411

1512
export const PresetTypeSchema = z.enum(["full", "partial"]);

packages/schemas/src/users.ts

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z, ZodEffects, ZodOptional, ZodString } from "zod";
2-
import { IdSchema, StringNumberSchema } from "./util";
2+
import { IdSchema, nameWithSeparators, slug, StringNumberSchema } from "./util";
33
import { LanguageSchema } from "./languages";
44
import {
55
ModeSchema,
@@ -18,10 +18,7 @@ import { ConnectionSchema } from "./connections";
1818
const NoneFilterSchema = z.literal("none");
1919
export const ResultFiltersSchema = z.object({
2020
_id: IdSchema,
21-
name: z
22-
.string()
23-
.regex(/^[0-9a-zA-Z_.-]+$/)
24-
.max(16),
21+
name: slug().max(16),
2522
pb: z
2623
.object({
2724
no: z.boolean(),
@@ -72,11 +69,13 @@ export const UserStreakSchema = z
7269
})
7370
.strict();
7471
export type UserStreak = z.infer<typeof UserStreakSchema>;
72+
export const TagNameSchema = nameWithSeparators().max(16);
73+
export type TagName = z.infer<typeof TagNameSchema>;
7574

7675
export const UserTagSchema = z
7776
.object({
7877
_id: IdSchema,
79-
name: z.string(),
78+
name: TagNameSchema,
8079
personalBests: PersonalBestsSchema,
8180
})
8281
.strict();
@@ -90,19 +89,13 @@ function profileDetailsBase(
9089
.transform((value) => (value === null ? undefined : value));
9190
}
9291

93-
export const TwitterProfileSchema = profileDetailsBase(
94-
z
95-
.string()
96-
.max(20)
97-
.regex(/^[0-9a-zA-Z_.-]+$/),
98-
).or(z.literal(""));
92+
export const TwitterProfileSchema = profileDetailsBase(slug().max(20)).or(
93+
z.literal(""),
94+
);
9995

100-
export const GithubProfileSchema = profileDetailsBase(
101-
z
102-
.string()
103-
.max(39)
104-
.regex(/^[0-9a-zA-Z_.-]+$/),
105-
).or(z.literal(""));
96+
export const GithubProfileSchema = profileDetailsBase(slug().max(39)).or(
97+
z.literal(""),
98+
);
10699

107100
export const WebsiteSchema = profileDetailsBase(
108101
z.string().url().max(200).startsWith("https://"),
@@ -125,10 +118,7 @@ export const UserProfileDetailsSchema = z
125118
.strict();
126119
export type UserProfileDetails = z.infer<typeof UserProfileDetailsSchema>;
127120

128-
export const CustomThemeNameSchema = z
129-
.string()
130-
.regex(/^[0-9a-zA-Z_-]+$/)
131-
.max(16);
121+
export const CustomThemeNameSchema = nameWithSeparators().max(16);
132122
export type CustomThemeName = z.infer<typeof CustomThemeNameSchema>;
133123

134124
export const CustomThemeSchema = z
@@ -244,14 +234,7 @@ export type FavoriteQuotes = z.infer<typeof FavoriteQuotesSchema>;
244234
export const UserEmailSchema = z.string().email();
245235
export const UserNameSchema = doesNotContainDisallowedWords(
246236
"substring",
247-
z
248-
.string()
249-
.min(1)
250-
.max(16)
251-
.regex(
252-
/^[\da-zA-Z_-]+$/,
253-
"Can only contain lower/uppercase letters, underscore and minus.",
254-
),
237+
slug().min(1).max(16),
255238
);
256239

257240
export const UserSchema = z.object({
@@ -297,12 +280,6 @@ export type ResultFiltersGroup = keyof ResultFilters;
297280
export type ResultFiltersGroupItem<T extends ResultFiltersGroup> =
298281
keyof ResultFilters[T];
299282

300-
export const TagNameSchema = z
301-
.string()
302-
.regex(/^[0-9a-zA-Z_.-]+$/)
303-
.max(16);
304-
export type TagName = z.infer<typeof TagNameSchema>;
305-
306283
export const TypingStatsSchema = z.object({
307284
completedTests: z.number().int().nonnegative().optional(),
308285
startedTests: z.number().int().nonnegative().optional(),

packages/schemas/src/util.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,29 @@ export const StringNumberSchema = z
88
)
99
.or(z.number().transform(String));
1010
export type StringNumber = z.infer<typeof StringNumberSchema>;
11-
1211
export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/);
1312

13+
export const slug = (): ZodString =>
14+
z
15+
.string()
16+
.regex(
17+
/^[0-9a-zA-Z_.-]+$/,
18+
"Only letters, numbers, underscores, dots and hyphens allowed",
19+
)
20+
.regex(/^[^.].*$/, "Cannot start with a dot");
21+
22+
export const nameWithSeparators = (): ZodString =>
23+
z
24+
.string()
25+
.regex(
26+
/^[0-9a-zA-Z_-]+$/,
27+
"Only letters, numbers, underscores and hyphens allowed",
28+
)
29+
.regex(
30+
/^[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+)*$/,
31+
"Separators cannot be at the start or end, or appear multiple times in a row",
32+
);
33+
1434
export const IdSchema = token();
1535
export type Id = z.infer<typeof IdSchema>;
1636

0 commit comments

Comments
 (0)