Compare commits

..

3 Commits

Author SHA1 Message Date
Mauricio Siu
2f9d5242d8 fix(ui): make interactive icons inside badges clickable again
The Tailwind v4 / shadcn badge update added [&>svg]:pointer-events-none
to the badge base class, which swallowed clicks on the X remove icons
rendered inside badges (watch paths in every git provider form), so
watch paths could no longer be deleted. Scope the rule to
non-interactive icons so svgs marked cursor-pointer receive clicks.

Fixes #4780
2026-07-14 11:45:07 -06:00
Mauricio Siu
7ba9818894 Merge pull request #4806 from tanaymishra/fix/validate-api-key-name-length
fix: validate API key name length to prevent opaque 500
2026-07-14 00:08:04 -06:00
tanaymishra
f577778667 fix: validate API key name length to prevent opaque 500
API key names longer than 32 characters were rejected by better-auth
with a 400 that surfaced as an opaque INTERNAL_SERVER_ERROR (the generic
"Failed to generate API key" toast). Add a shared name schema (min 1,
max 32, matching better-auth's default maximumNameLength) used by both
the tRPC input and the client form, and surface the limit on the Name
field so users see it before submitting.

Fixes #4798
2026-07-12 18:15:05 +05:30
5 changed files with 62 additions and 4 deletions

View File

@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys";
describe("apiKeyNameSchema", () => {
it("rejects an empty name", () => {
const result = apiKeyNameSchema.safeParse("");
expect(result.success).toBe(false);
});
it("accepts a name at the maximum length", () => {
const name = "a".repeat(API_KEY_NAME_MAX_LENGTH);
const result = apiKeyNameSchema.safeParse(name);
expect(result.success).toBe(true);
});
it("rejects a name over the maximum length instead of passing it to better-auth", () => {
const name = "a".repeat(API_KEY_NAME_MAX_LENGTH + 1);
const result = apiKeyNameSchema.safeParse(name);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toBe(
`Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`,
);
}
});
});

View File

@@ -32,10 +32,11 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1, "Name is required"), name: apiKeyNameSchema,
prefix: z.string().optional(), prefix: z.string().optional(),
expiresIn: z.number().nullable(), expiresIn: z.number().nullable(),
organizationId: z.string().min(1, "Organization is required"), organizationId: z.string().min(1, "Organization is required"),
@@ -159,8 +160,15 @@ export const AddApiKey = () => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="My API Key" {...field} /> <Input
placeholder="My API Key"
maxLength={API_KEY_NAME_MAX_LENGTH}
{...field}
/>
</FormControl> </FormControl>
<FormDescription>
Maximum {API_KEY_NAME_MAX_LENGTH} characters
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@@ -5,7 +5,7 @@ import type * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", "group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg:not(.cursor-pointer)]:pointer-events-none [&>svg]:size-3!",
{ {
variants: { variants: {
variant: { variant: {

View File

@@ -0,0 +1,23 @@
import { z } from "zod";
/**
* Maximum length allowed for an API key name.
*
* This mirrors the default `maximumNameLength` enforced by the
* `@better-auth/api-key` plugin. Names longer than this are rejected by
* better-auth with a 400, so we validate against it up front to surface a
* clear field-level error instead of an opaque 500.
*/
export const API_KEY_NAME_MAX_LENGTH = 32;
/**
* Shared validation for an API key name, used by both the tRPC input schema
* and the client form so the two can't drift.
*/
export const apiKeyNameSchema = z
.string()
.min(1, "Name is required")
.max(
API_KEY_NAME_MAX_LENGTH,
`Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`,
);

View File

@@ -35,6 +35,7 @@ import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt"; import * as bcrypt from "bcrypt";
import { and, asc, eq, gt, ne } from "drizzle-orm"; import { and, asc, eq, gt, ne } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { apiKeyNameSchema } from "@/lib/api-keys";
import { audit } from "@/server/api/utils/audit"; import { audit } from "@/server/api/utils/audit";
import { import {
adminProcedure, adminProcedure,
@@ -45,7 +46,7 @@ import {
} from "../trpc"; } from "../trpc";
const apiCreateApiKey = z.object({ const apiCreateApiKey = z.object({
name: z.string().min(1), name: apiKeyNameSchema,
prefix: z.string().optional(), prefix: z.string().optional(),
expiresIn: z.number().optional(), expiresIn: z.number().optional(),
metadata: z.object({ metadata: z.object({