Skip to content

Commit

Permalink
feat: centralize validation schemas (#2669)
Browse files Browse the repository at this point in the history
* feat: centralize validation schemas

* Fix code scanning alert no. 12: Overly permissive regular expression range

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
  • Loading branch information
chronark and github-advanced-security[bot] authored Dec 2, 2024
1 parent ae32930 commit 66c4cd5
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 295 deletions.
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@unkey/metrics": "workspace:^",
"@unkey/rbac": "workspace:^",
"@unkey/schema": "workspace:^",
"@unkey/validation": "workspace:^",
"@unkey/worker-logging": "workspace:^",
"hono": "^4.6.3",
"superjson": "^2.2.1",
Expand Down
19 changes: 7 additions & 12 deletions apps/api/src/routes/v1_permissions_createPermission.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { App } from "@/pkg/hono/app";
import { createRoute, z } from "@hono/zod-openapi";

import { validation } from "@unkey/validation";

import { insertUnkeyAuditLog } from "@/pkg/audit";
import { rootKeyAuth } from "@/pkg/auth/root_key";
import { openApiErrorResponses } from "@/pkg/errors";
Expand All @@ -20,18 +22,11 @@ const route = createRoute({
content: {
"application/json": {
schema: z.object({
name: z
.string()
.min(3)
.regex(/^[a-zA-Z0-9_:\-\.\*]+$/, {
message:
"Must be at least 3 characters long and only contain alphanumeric, colons, periods, dashes and underscores",
})
.openapi({
description: "The unique name of your permission.",
example: "record.write",
}),
description: z.string().optional().openapi({
name: validation.identifier.openapi({
description: "The unique name of your permission.",
example: "record.write",
}),
description: validation.description.optional().openapi({
description:
"Explain what this permission does. This is just for your team, your users will not see this.",
example: "record.write can create new dns records for our domains.",
Expand Down
18 changes: 6 additions & 12 deletions apps/api/src/routes/v1_permissions_createRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { openApiErrorResponses } from "@/pkg/errors";
import { schema } from "@unkey/db";
import { newId } from "@unkey/id";
import { buildUnkeyQuery } from "@unkey/rbac";
import { validation } from "@unkey/validation";

const route = createRoute({
tags: ["permissions"],
Expand All @@ -20,18 +21,11 @@ const route = createRoute({
content: {
"application/json": {
schema: z.object({
name: z
.string()
.min(3)
.regex(/^[a-zA-Z0-9_:\-\.\*]+$/, {
message:
"Must be at least 3 characters long and only contain alphanumeric, colons, periods, dashes and underscores",
})
.openapi({
description: "The unique name of your role.",
example: "dns.records.manager",
}),
description: z.string().optional().openapi({
name: validation.name.openapi({
description: "The unique name of your role.",
example: "dns.records.manager",
}),
description: validation.description.optional().openapi({
description:
"Explain what this role does. This is just for your team, your users will not see this.",
example: "dns.records.manager can read and write dns records for our domains.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,18 @@ import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { DialogTrigger } from "@radix-ui/react-dialog";
import { validation } from "@unkey/validation";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

type Props = {
trigger: React.ReactNode;
};

const formSchema = z.object({
name: z
.string()
.min(3)
.regex(/^[a-zA-Z0-9_:\-\.\*]+$/, {
message:
"Must be at least 3 characters long and only contain alphanumeric, colons, periods, dashes and underscores",
}),

description: z.string().optional(),
name: validation.name,
description: validation.description.optional(),
});

export const CreateNewPermission: React.FC<Props> = ({ trigger }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { DialogTrigger } from "@radix-ui/react-dialog";
import type { Role } from "@unkey/db";
import { validation } from "@unkey/validation";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
Expand All @@ -38,14 +39,8 @@ type Props = {
};

const formSchema = z.object({
name: z
.string()
.min(3)
.regex(/^[a-zA-Z0-9_:\-\.\*]+$/, {
message:
"Must be at least 3 characters long and only contain alphanumeric, colons, periods, dashes and underscores",
}),
description: z.string().optional(),
name: validation.name,
description: validation.description.optional(),
});

export const UpdateRole: React.FC<Props> = ({ trigger, role }) => {
Expand Down
12 changes: 3 additions & 9 deletions apps/dashboard/app/(app)/authorization/roles/create-new-role.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,20 @@ import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { DialogTrigger } from "@radix-ui/react-dialog";
import type { Permission } from "@unkey/db";
import { validation } from "@unkey/validation";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

type Props = {
trigger: React.ReactNode;
permissions?: Permission[];
};

const formSchema = z.object({
name: z
.string()
.min(3)
.regex(/^[a-zA-Z0-9_:\-\.\*]+$/, {
message:
"Must be at least 3 characters long and only contain alphanumeric, colons, periods, dashes and underscores",
}),
name: validation.name,

description: z.string().optional(),
description: validation.description.optional(),
permissionOptions: z
.array(
z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,14 @@ import { toast } from "@/components/ui/toaster";
import { tags } from "@/lib/cache";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { validation } from "@unkey/validation";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { z } from "zod";
const formSchema = z.object({
name: z
.string()
.trim()
.min(3)
.max(50)
.regex(/^[a-zA-Z0-9_\-\.]+$/, {
message:
"Name must be 3-50 characters long and can only contain letters, numbers, underscores, hyphens, and periods.",
}),
namespaceId: z.string(),
workspaceId: z.string(),
name: validation.name,
namespaceId: validation.unkeyId,
workspaceId: validation.unkeyId,
});

type Props = {
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@unkey/rbac": "workspace:^",
"@unkey/resend": "workspace:^",
"@unkey/schema": "workspace:^",
"@unkey/validation": "workspace:^",
"@unkey/vault": "workspace:^",
"@unkey/vercel": "workspace:^",
"@upstash/ratelimit": "^2.0.1",
Expand Down
3 changes: 3 additions & 0 deletions internal/validation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
`@unkey/validation` contains various shared schema or validation utils.

For example user-chosen identifiers.
18 changes: 18 additions & 0 deletions internal/validation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@unkey/validation",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"types": "src/index.ts",
"keywords": [],
"author": "Andreas Thomas",
"license": "AGPL-3.0",
"devDependencies": {
"@types/node": "^20.14.9",
"@unkey/tsconfig": "workspace:*",
"typescript": "^5.5.3"
},
"dependencies": {
"zod": "^3.23.5"
}
}
46 changes: 46 additions & 0 deletions internal/validation/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { z } from "zod";

/*
* An identifier is any string the user gives us to be used as a lookup key.
* It must be URL safe and fit into our database (varchar 256)
*/
const identifier = z
.string()
.min(3)
.max(256)
.regex(
/^[a-zA-Z0-9_\.:\-]*$/,
"Only alphanumeric, underscores, periods, colons and hyphens are allowed",
);

/**
* A name is a user given human-readable string.
*
* It must not be used in URLs.
*
* @example the name of a key
*/
const name = z.string().min(3).max(256);

/**
* A description is a user given human-readable string.
*
* It must not be used in URLs.
*
* @example The description of a permission
*/
const description = z.string().min(3).max(256);

const unkeyId = z
.string()
.regex(
/^[a-z]{3,4}_[a-zA-Z0-9]{8,}$/,
"Unkey IDs must include a prefix, separated by an underscore: key_abcdefg123",
);

export const validation = {
identifier,
name,
description,
unkeyId,
};
8 changes: 8 additions & 0 deletions internal/validation/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@unkey/tsconfig/base.json",
"exclude": ["dist"],
"compilerOptions": {
"outDir": "dist"
}
}
Loading

0 comments on commit 66c4cd5

Please sign in to comment.