Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/ts/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Config } from "@monkeytype/schemas/configs";
import { Mode, Mode2, PersonalBests } from "@monkeytype/schemas/shared";
import { Result } from "@monkeytype/schemas/results";
import { RankAndCount } from "@monkeytype/schemas/users";
import { SLUG_REGEX } from "@monkeytype/schemas/util";
import { roundTo2 } from "@monkeytype/util/numbers";
import { animate, AnimationParams } from "animejs";
import { ElementWithUtils } from "./dom";
Expand Down Expand Up @@ -151,7 +152,7 @@ export function isUsernameValid(name: string): boolean {
if (name.toLowerCase().includes("bitly")) return false;
if (name.length > 14) return false;
if (/^\..*/.test(name.toLowerCase())) return false;
return /^[0-9a-zA-Z_.-]+$/.test(name);
return SLUG_REGEX.test(name);
}

export function clearTimeouts(timeouts: (number | NodeJS.Timeout)[]): void {
Expand Down
38 changes: 38 additions & 0 deletions packages/schemas/__tests__/util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, expect } from "vitest";
import { nameWithUnderscores, slug } from "../src/util";

describe("Schema Validation Tests", () => {
describe("nameWithUnderscores", () => {
const schema = nameWithUnderscores();

it("accepts valid names", () => {
expect(schema.safeParse("valid_name").success).toBe(true);
expect(schema.safeParse("valid123").success).toBe(true);
expect(schema.safeParse("Valid_Name_Check").success).toBe(true);
});

it("rejects leading/trailing underscores", () => {
expect(schema.safeParse("_invalid").success).toBe(false);
expect(schema.safeParse("invalid_").success).toBe(false);
});

it("rejects consecutive underscores", () => {
expect(schema.safeParse("inv__alid").success).toBe(false);
});

it("rejects non-underscore separators", () => {
expect(schema.safeParse("invalid-name").success).toBe(false);
});
});

describe("slug", () => {
const schema = slug();

it("accepts valid slugs", () => {
expect(schema.safeParse("valid-slug.123_test").success).toBe(true);
expect(schema.safeParse("valid.dots").success).toBe(true);
expect(schema.safeParse("_leading_is_fine_in_slug").success).toBe(true);
expect(schema.safeParse("trailing_is_fine_in_slug_").success).toBe(true);
});
});
});
7 changes: 2 additions & 5 deletions packages/schemas/src/ape-keys.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { z } from "zod";
import { IdSchema } from "./util";
import { IdSchema, slug } from "./util";

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

export const ApeKeyUserDefinedSchema = z.object({
name: ApeKeyNameSchema,
Expand Down
7 changes: 2 additions & 5 deletions packages/schemas/src/presets.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { z } from "zod";
import { IdSchema, TagSchema } from "./util";
import { IdSchema, nameWithUnderscores, TagSchema } from "./util";
import {
ConfigGroupName,
ConfigGroupNameSchema,
PartialConfigSchema,
} from "./configs";

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

export const PresetTypeSchema = z.enum(["full", "partial"]);
Expand Down
54 changes: 18 additions & 36 deletions packages/schemas/src/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { z, ZodEffects, ZodOptional, ZodString } from "zod";
import { IdSchema, StringNumberSchema } from "./util";
import {
IdSchema,
nameWithUnderscores,
slug,
StringNumberSchema,
} from "./util";
import { LanguageSchema } from "./languages";
import {
ModeSchema,
Expand All @@ -18,10 +23,7 @@ import { ConnectionSchema } from "./connections";
const NoneFilterSchema = z.literal("none");
export const ResultFiltersSchema = z.object({
_id: IdSchema,
name: z
.string()
.regex(/^[0-9a-zA-Z_.-]+$/)
.max(16),
name: nameWithUnderscores().max(16),
pb: z
.object({
no: z.boolean(),
Expand Down Expand Up @@ -72,11 +74,13 @@ export const UserStreakSchema = z
})
.strict();
export type UserStreak = z.infer<typeof UserStreakSchema>;
export const TagNameSchema = nameWithUnderscores().max(16);
export type TagName = z.infer<typeof TagNameSchema>;

export const UserTagSchema = z
.object({
_id: IdSchema,
name: z.string(),
name: TagNameSchema,
personalBests: PersonalBestsSchema,
})
.strict();
Expand All @@ -90,19 +94,13 @@ function profileDetailsBase(
.transform((value) => (value === null ? undefined : value));
}

export const TwitterProfileSchema = profileDetailsBase(
z
.string()
.max(20)
.regex(/^[0-9a-zA-Z_.-]+$/),
).or(z.literal(""));
export const TwitterProfileSchema = profileDetailsBase(slug().max(20)).or(
z.literal(""),
);

export const GithubProfileSchema = profileDetailsBase(
z
.string()
.max(39)
.regex(/^[0-9a-zA-Z_.-]+$/),
).or(z.literal(""));
export const GithubProfileSchema = profileDetailsBase(slug().max(39)).or(
z.literal(""),
);

export const WebsiteSchema = profileDetailsBase(
z.string().url().max(200).startsWith("https://"),
Expand All @@ -125,10 +123,7 @@ export const UserProfileDetailsSchema = z
.strict();
export type UserProfileDetails = z.infer<typeof UserProfileDetailsSchema>;

export const CustomThemeNameSchema = z
.string()
.regex(/^[0-9a-zA-Z_-]+$/)
.max(16);
export const CustomThemeNameSchema = nameWithUnderscores().max(16);
export type CustomThemeName = z.infer<typeof CustomThemeNameSchema>;

export const CustomThemeSchema = z
Expand Down Expand Up @@ -244,14 +239,7 @@ export type FavoriteQuotes = z.infer<typeof FavoriteQuotesSchema>;
export const UserEmailSchema = z.string().email();
export const UserNameSchema = doesNotContainProfanity(
"substring",
z
.string()
.min(1)
.max(16)
.regex(
/^[\da-zA-Z_-]+$/,
"Can only contain lower/uppercase letters, underscore and minus.",
),
nameWithUnderscores().min(1).max(16),
);

export const UserSchema = z.object({
Expand Down Expand Up @@ -297,12 +285,6 @@ export type ResultFiltersGroup = keyof ResultFilters;
export type ResultFiltersGroupItem<T extends ResultFiltersGroup> =
keyof ResultFilters[T];

export const TagNameSchema = z
.string()
.regex(/^[0-9a-zA-Z_.-]+$/)
.max(16);
export type TagName = z.infer<typeof TagNameSchema>;

export const TypingStatsSchema = z.object({
completedTests: z.number().int().nonnegative().optional(),
startedTests: z.number().int().nonnegative().optional(),
Expand Down
19 changes: 18 additions & 1 deletion packages/schemas/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,27 @@ export type StringNumber = z.infer<typeof StringNumberSchema>;

export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/);

export const slug = (): ZodString =>
z
.string()
.regex(
/^[0-9a-zA-Z_.-]+$/,
"Only letters, numbers, underscores, dots and hyphens allowed",
);

export const nameWithUnderscores = (): ZodString =>
z
.string()
.regex(/^[0-9a-zA-Z_]+$/, "Only letters, numbers, and underscores allowed")
Comment thread
byseif21 marked this conversation as resolved.
Outdated
.regex(
/^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$/,
"Underscores cannot be at the start or end, or appear multiple times in a row",
);

export const IdSchema = token();
export type Id = z.infer<typeof IdSchema>;

export const TagSchema = token().max(50);
export const TagSchema = nameWithUnderscores().max(50);
Comment thread
byseif21 marked this conversation as resolved.
Outdated
export type Tag = z.infer<typeof TagSchema>;

export const NullableStringSchema = z
Expand Down
Loading