Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 2 additions & 6 deletions frontend/src/ts/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { lastElementFromArray } from "./arrays";
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 { RankAndCount, UserNameSchema } from "@monkeytype/schemas/users";
import { roundTo2 } from "@monkeytype/util/numbers";
import { animate, AnimationParams } from "animejs";
import { ElementWithUtils } from "./dom";
Expand Down Expand Up @@ -147,11 +147,7 @@ export function escapeHTML<T extends string | null | undefined>(str: T): T {

export function isUsernameValid(name: string): boolean {
if (name === null || name === undefined || name === "") return false;
if (name.toLowerCase().includes("miodec")) return false;
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 UserNameSchema.safeParse(name).success;
}

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

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

it("accepts valid names", () => {
expect(schema.safeParse("valid_name").success).toBe(true);
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 separators", () => {
expect(schema.safeParse("_invalid").success).toBe(false);
expect(schema.safeParse("invalid-").success).toBe(false);
});

it("rejects consecutive separators", () => {
expect(schema.safeParse("inv__alid").success).toBe(false);
expect(schema.safeParse("inv--alid").success).toBe(false);
expect(schema.safeParse("inv-_alid").success).toBe(false);
});

it("rejects dots", () => {
expect(schema.safeParse("invalid.dot").success).toBe(false);
expect(schema.safeParse(".invalid").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_underscore_is_fine").success).toBe(
true,
);
expect(schema.safeParse("-leading_hyphen_is_fine").success).toBe(true);
expect(schema.safeParse("trailing_is_fine_in_slug_").success).toBe(true);
});

it("rejects leading dots", () => {
expect(schema.safeParse(".invalid").success).toBe(false);
});

it("rejects invalid characters", () => {
expect(schema.safeParse("invalid,comma").success).toBe(false);
expect(schema.safeParse(",invalid").success).toBe(false);
expect(schema.safeParse("invalid space").success).toBe(false);
expect(schema.safeParse("invalid#hash").success).toBe(false);
});
});
});
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, nameWithSeparators, TagSchema } from "./util";
import {
ConfigGroupName,
ConfigGroupNameSchema,
PartialConfigSchema,
} from "./configs";

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

export const PresetTypeSchema = z.enum(["full", "partial"]);
Expand Down
49 changes: 13 additions & 36 deletions packages/schemas/src/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z, ZodEffects, ZodOptional, ZodString } from "zod";
import { IdSchema, StringNumberSchema } from "./util";
import { IdSchema, nameWithSeparators, slug, StringNumberSchema } from "./util";
import { LanguageSchema } from "./languages";
import {
ModeSchema,
Expand All @@ -18,10 +18,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: slug().max(16),
pb: z
.object({
no: z.boolean(),
Expand Down Expand Up @@ -72,11 +69,13 @@ export const UserStreakSchema = z
})
.strict();
export type UserStreak = z.infer<typeof UserStreakSchema>;
export const TagNameSchema = nameWithSeparators().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 +89,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 +118,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 = nameWithSeparators().max(16);
export type CustomThemeName = z.infer<typeof CustomThemeNameSchema>;

export const CustomThemeSchema = z
Expand Down Expand Up @@ -244,14 +234,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.",
),
slug().min(1).max(16),
);

export const UserSchema = z.object({
Expand Down Expand Up @@ -297,12 +280,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
22 changes: 21 additions & 1 deletion packages/schemas/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,29 @@ export const StringNumberSchema = z
)
.or(z.number().transform(String));
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",
)
.regex(/^[^.].*$/, "Cannot start with a dot");

export const nameWithSeparators = (): ZodString =>
z
.string()
.regex(
/^[0-9a-zA-Z_-]+$/,
"Only letters, numbers, underscores and hyphens allowed",
)
.regex(
/^[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+)*$/,
"Separators 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>;

Expand Down
Loading