From b320bceee6bae96d557ee89fc7522f1fb0013585 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Thu, 5 Dec 2024 17:24:16 -0500 Subject: [PATCH 01/12] removal of refillInterval --- apps/api/src/pkg/key_migration/handler.ts | 1 - apps/api/src/routes/schema.ts | 8 +------- apps/api/src/routes/v1_apis_listKeys.ts | 5 ++--- internal/db/src/schema/keys.ts | 3 +-- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/apps/api/src/pkg/key_migration/handler.ts b/apps/api/src/pkg/key_migration/handler.ts index 452b30a4ad..3ebe525f14 100644 --- a/apps/api/src/pkg/key_migration/handler.ts +++ b/apps/api/src/pkg/key_migration/handler.ts @@ -80,7 +80,6 @@ export async function migrateKey( createdAt: new Date(), createdAtM: Date.now(), expires: message.expires ? new Date(message.expires) : null, - refillInterval: message.refill?.interval, refillAmount: message.refill?.amount, refillDay: message.refill?.refillDay, enabled: message.enabled, diff --git a/apps/api/src/routes/schema.ts b/apps/api/src/routes/schema.ts index a9d4d4b14b..b06b46a3f2 100644 --- a/apps/api/src/routes/schema.ts +++ b/apps/api/src/routes/schema.ts @@ -58,18 +58,13 @@ export const keySchema = z }), refill: z .object({ - interval: z.enum(["daily", "monthly"]).openapi({ - description: - "Determines the rate at which verifications will be refilled. When 'daily' is set for 'interval' 'refillDay' will be set to null.", - example: "daily", - }), amount: z.number().int().openapi({ description: "Resets `remaining` to this value every interval.", example: 100, }), refillDay: z.number().min(1).max(31).default(1).nullable().openapi({ description: - "The day verifications will refill each month, when interval is set to 'monthly'. Value is not zero-indexed making 1 the first day of the month. If left blank it will default to the first day of the month. When 'daily' is set for 'interval' 'refillDay' will be set to null.", + "The amount will refill on the day of the month specified on `refillDay`. If `refillDay` beyond the last day in the month, it will refill on the last day of the month. If left empty, it will refill daily.", example: 15, }), lastRefillAt: z.number().int().optional().openapi({ @@ -82,7 +77,6 @@ export const keySchema = z description: "Unkey allows you to refill remaining verifications on a key on a regular interval.", example: { - interval: "monthly", amount: 10, refillDay: 10, }, diff --git a/apps/api/src/routes/v1_apis_listKeys.ts b/apps/api/src/routes/v1_apis_listKeys.ts index 2e1c80a1d3..46bdc0571b 100644 --- a/apps/api/src/routes/v1_apis_listKeys.ts +++ b/apps/api/src/routes/v1_apis_listKeys.ts @@ -337,11 +337,10 @@ export const registerV1ApisListKeys = (app: App) => : undefined, remaining: k.remaining ?? undefined, refill: - k.refillInterval && k.refillAmount && k.lastRefillAt + k.refillAmount && k.lastRefillAt ? { - interval: k.refillInterval, amount: k.refillAmount, - refillDay: k.refillInterval === "monthly" && k.refillDay ? k.refillDay : null, + refillDay: k.refillDay ? k.refillDay : null, lastRefillAt: k.lastRefillAt?.getTime(), } : undefined, diff --git a/internal/db/src/schema/keys.ts b/internal/db/src/schema/keys.ts index 81360f3d9a..cff1c7edb4 100644 --- a/internal/db/src/schema/keys.ts +++ b/internal/db/src/schema/keys.ts @@ -5,7 +5,6 @@ import { datetime, index, int, - mysqlEnum, mysqlTable, text, tinyint, @@ -63,7 +62,7 @@ export const keys = mysqlTable( /** * You can refill uses to keys at a desired interval */ - refillInterval: mysqlEnum("refill_interval", ["daily", "monthly"]), + refillDay: tinyint("refill_day"), refillAmount: int("refill_amount"), lastRefillAt: datetime("last_refill_at", { fsp: 3 }), From c38f7e0c2e196e85b54af2809ab920536bc8b122 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Thu, 5 Dec 2024 18:43:12 -0500 Subject: [PATCH 02/12] Eliminated refs to refillInterval --- apps/api/src/routes/v1_apis_listKeys.ts | 2 +- apps/api/src/routes/v1_keys_createKey.ts | 15 +++------ apps/api/src/routes/v1_keys_getKey.ts | 16 +++++----- .../routes/v1_keys_updateKey.happy.test.ts | 1 - apps/api/src/routes/v1_keys_updateKey.ts | 2 -- .../v1_migrations_createKey.happy.test.ts | 2 -- .../api/src/routes/v1_migrations_createKey.ts | 31 ++++++++----------- .../[keyId]/settings/update-key-remaining.tsx | 4 +-- .../dashboard/api-key-table/index.tsx | 10 +++--- .../lib/trpc/routers/key/createRootKey.ts | 1 - .../lib/trpc/routers/key/updateRemaining.ts | 10 ++---- apps/dashboard/lib/zod-helper.ts | 1 - 12 files changed, 35 insertions(+), 60 deletions(-) diff --git a/apps/api/src/routes/v1_apis_listKeys.ts b/apps/api/src/routes/v1_apis_listKeys.ts index 46bdc0571b..d54ce7ebc3 100644 --- a/apps/api/src/routes/v1_apis_listKeys.ts +++ b/apps/api/src/routes/v1_apis_listKeys.ts @@ -340,7 +340,7 @@ export const registerV1ApisListKeys = (app: App) => k.refillAmount && k.lastRefillAt ? { amount: k.refillAmount, - refillDay: k.refillDay ? k.refillDay : null, + refillDay: k.refillDay ?? null, lastRefillAt: k.lastRefillAt?.getTime(), } : undefined, diff --git a/apps/api/src/routes/v1_keys_createKey.ts b/apps/api/src/routes/v1_keys_createKey.ts index 814cda8eb8..0233fbee5a 100644 --- a/apps/api/src/routes/v1_keys_createKey.ts +++ b/apps/api/src/routes/v1_keys_createKey.ts @@ -109,9 +109,6 @@ When validating a key, we will return this back to you, so you can clearly ident }), refill: z .object({ - interval: z.enum(["daily", "monthly"]).openapi({ - description: "Unkey will automatically refill verifications at the set interval.", - }), amount: z.number().int().min(1).positive().openapi({ description: "The number of verifications to refill for each occurrence is determined individually for each key.", @@ -131,7 +128,6 @@ When validating a key, we will return this back to you, so you can clearly ident description: "Unkey enables you to refill verifications for each key at regular intervals.", example: { - interval: "monthly", amount: 100, refillDay: 15, }, @@ -312,16 +308,16 @@ export const registerV1KeysCreateKey = (app: App) => message: "remaining must be greater than 0.", }); } - if ((req.remaining === null || req.remaining === undefined) && req.refill?.interval) { + if (req.remaining === null || req.remaining === undefined) { throw new UnkeyApiError({ code: "BAD_REQUEST", message: "remaining must be set if you are using refill.", }); } - if (req.refill?.refillDay && req.refill.interval === "daily") { + if (req.refill && !req.refill.amount) { throw new UnkeyApiError({ code: "BAD_REQUEST", - message: "when interval is set to 'daily', 'refillDay' must be null.", + message: "refill.amount must be set if you are using refill.", }); } /** @@ -372,10 +368,9 @@ export const registerV1KeysCreateKey = (app: App) => ratelimitLimit: req.ratelimit?.limit ?? req.ratelimit?.refillRate, ratelimitDuration: req.ratelimit?.duration ?? req.ratelimit?.refillInterval, remaining: req.remaining, - refillInterval: req.refill?.interval, - refillDay: req.refill?.interval === "daily" ? null : req?.refill?.refillDay ?? 1, + refillDay: req?.refill?.refillDay ?? null, refillAmount: req.refill?.amount, - lastRefillAt: req.refill?.interval ? new Date() : null, + lastRefillAt: null, deletedAt: null, enabled: req.enabled, environment: req.environment ?? null, diff --git a/apps/api/src/routes/v1_keys_getKey.ts b/apps/api/src/routes/v1_keys_getKey.ts index 524b5b215c..6ce98f31ae 100644 --- a/apps/api/src/routes/v1_keys_getKey.ts +++ b/apps/api/src/routes/v1_keys_getKey.ts @@ -150,15 +150,13 @@ export const registerV1KeysGetKey = (app: App) => updatedAt: key.updatedAtM ?? undefined, expires: key.expires?.getTime() ?? undefined, remaining: key.remaining ?? undefined, - refill: - key.refillInterval && key.refillAmount - ? { - interval: key.refillInterval, - amount: key.refillAmount, - refillDay: key.refillInterval === "monthly" ? key.refillDay : null, - lastRefillAt: key.lastRefillAt?.getTime(), - } - : undefined, + refill: key.refillAmount + ? { + amount: key.refillAmount, + refillDay: key.refillDay ?? null, + lastRefillAt: key.lastRefillAt?.getTime(), + } + : undefined, ratelimit: key.ratelimitAsync !== null && key.ratelimitLimit !== null && key.ratelimitDuration !== null ? { diff --git a/apps/api/src/routes/v1_keys_updateKey.happy.test.ts b/apps/api/src/routes/v1_keys_updateKey.happy.test.ts index 9387711286..e9583c146e 100644 --- a/apps/api/src/routes/v1_keys_updateKey.happy.test.ts +++ b/apps/api/src/routes/v1_keys_updateKey.happy.test.ts @@ -1054,7 +1054,6 @@ describe("When refillDay is omitted.", () => { expect(found).toBeDefined(); expect(found?.remaining).toEqual(10); expect(found?.refillAmount).toEqual(130); - expect(found?.refillInterval).toEqual("monthly"); expect(found?.refillDay).toEqual(1); }); }); diff --git a/apps/api/src/routes/v1_keys_updateKey.ts b/apps/api/src/routes/v1_keys_updateKey.ts index d12c521896..302d3e194e 100644 --- a/apps/api/src/routes/v1_keys_updateKey.ts +++ b/apps/api/src/routes/v1_keys_updateKey.ts @@ -389,12 +389,10 @@ export const registerV1KeysUpdate = (app: App) => if (typeof req.refill !== "undefined") { if (req.refill === null) { - changes.refillInterval = null; changes.refillAmount = null; changes.refillDay = null; changes.lastRefillAt = null; } else { - changes.refillInterval = req.refill.interval; changes.refillAmount = req.refill.amount; changes.refillDay = req.refill.refillDay ?? 1; } diff --git a/apps/api/src/routes/v1_migrations_createKey.happy.test.ts b/apps/api/src/routes/v1_migrations_createKey.happy.test.ts index 6e633b90a3..433c2e1242 100644 --- a/apps/api/src/routes/v1_migrations_createKey.happy.test.ts +++ b/apps/api/src/routes/v1_migrations_createKey.happy.test.ts @@ -520,7 +520,6 @@ describe("Should default to first day of month if none provided", () => { enabled: true, remaining: 10, refill: { - interval: "monthly", amount: 100, refillDay: undefined, }, @@ -536,7 +535,6 @@ describe("Should default to first day of month if none provided", () => { expect(found).toBeDefined(); expect(found?.remaining).toEqual(10); expect(found?.refillAmount).toEqual(100); - expect(found?.refillInterval).toEqual("monthly"); expect(found?.refillDay).toEqual(1); expect(found?.hash).toEqual(hash); }); diff --git a/apps/api/src/routes/v1_migrations_createKey.ts b/apps/api/src/routes/v1_migrations_createKey.ts index a9fd772504..072d636af8 100644 --- a/apps/api/src/routes/v1_migrations_createKey.ts +++ b/apps/api/src/routes/v1_migrations_createKey.ts @@ -123,25 +123,26 @@ When validating a key, we will return this back to you, so you can clearly ident }), refill: z .object({ - interval: z.enum(["daily", "monthly"]).openapi({ - description: - "Unkey will automatically refill verifications at the set interval.", - }), amount: z.number().int().min(1).positive().openapi({ description: "The number of verifications to refill for each occurrence is determined individually for each key.", }), - refillDay: z.number().min(1).max(31).optional().openapi({ - description: - "The day verifications will refill each month, when interval is set to 'monthly'", - }), + refillDay: z + .number() + .min(1) + .max(31) + .optional() + .openapi({ + description: `The day of the month, when we will refill the remaining verifications. To refill on the 15th of each month, set 'refillDay': 15. + If the day does not exist, for example you specified the 30th and it's february, we will refill them on the last day of the month instead.`, + }), }) .optional() .openapi({ description: "Unkey enables you to refill verifications for each key at regular intervals.", example: { - interval: "daily", + refillDay: 15, amount: 100, }, }), @@ -378,7 +379,7 @@ export const registerV1MigrationsCreateKeys = (app: App) => }); } - if ((key.remaining === null || key.remaining === undefined) && key.refill?.interval) { + if (key.remaining === null || key.remaining === undefined) { throw new UnkeyApiError({ code: "BAD_REQUEST", message: "remaining must be set if you are using refill.", @@ -391,12 +392,7 @@ export const registerV1MigrationsCreateKeys = (app: App) => message: "provide either `hash` or `plaintext`", }); } - if (key.refill?.refillDay && key.refill.interval === "daily") { - throw new UnkeyApiError({ - code: "BAD_REQUEST", - message: "when interval is set to 'daily', 'refillDay' must be null.", - }); - } + /** * Set up an api for production */ @@ -420,8 +416,7 @@ export const registerV1MigrationsCreateKeys = (app: App) => ratelimitLimit: key.ratelimit?.limit ?? key.ratelimit?.refillRate ?? null, ratelimitDuration: key.ratelimit?.refillInterval ?? key.ratelimit?.refillInterval ?? null, remaining: key.remaining ?? null, - refillInterval: key.refill?.interval ?? null, - refillDay: key.refill?.interval === "daily" ? null : key?.refill?.refillDay ?? 1, + refillDay: key?.refill?.refillDay ?? 1, refillAmount: key.refill?.amount ?? null, deletedAt: null, enabled: key.enabled ?? true, diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx index beee505036..04aa23c8aa 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx @@ -90,10 +90,8 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { limitEnabled: apiKey.remaining ? true : false, remaining: apiKey.remaining ? apiKey.remaining : undefined, refill: { - interval: apiKey.refillInterval === null ? "none" : apiKey.refillInterval, amount: apiKey.refillAmount ? apiKey.refillAmount : undefined, - refillDay: - apiKey.refillInterval === "monthly" && apiKey.refillDay ? apiKey.refillDay : undefined, + refillDay: apiKey.refillDay ?? undefined, }, }, }); diff --git a/apps/dashboard/components/dashboard/api-key-table/index.tsx b/apps/dashboard/components/dashboard/api-key-table/index.tsx index 27720c5579..c19021fbff 100644 --- a/apps/dashboard/components/dashboard/api-key-table/index.tsx +++ b/apps/dashboard/components/dashboard/api-key-table/index.tsx @@ -51,7 +51,7 @@ type Column = { ratelimitRefillRate: number | null; ratelimitRefillInterval: number | null; remaining: number | null; - refillInterval: string | null; + refillDay: string | null; refillAmount: number | null; }; @@ -181,12 +181,12 @@ export const ApiKeyTable: React.FC = ({ data }) => { ), }, { - accessorKey: "refillInterval", - header: "Refill Rate", + accessorKey: "refillDay", + header: "Refill Day", cell: ({ row }) => - row.original.refillInterval && row.original.refillAmount && row.original.remaining ? ( + row.original.refillDay && row.original.refillAmount && row.original.remaining ? (
- {row.original.refillInterval} + {row.original.refillDay ?? "Daily"}
) : ( diff --git a/apps/dashboard/lib/trpc/routers/key/createRootKey.ts b/apps/dashboard/lib/trpc/routers/key/createRootKey.ts index 45fa061841..35562275eb 100644 --- a/apps/dashboard/lib/trpc/routers/key/createRootKey.ts +++ b/apps/dashboard/lib/trpc/routers/key/createRootKey.ts @@ -91,7 +91,6 @@ export const createRootKey = t.procedure expires: null, createdAt: new Date(), remaining: null, - refillInterval: null, refillAmount: null, refillDay: null, lastRefillAt: null, diff --git a/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts b/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts index 835ece954c..1cae208696 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts @@ -37,7 +37,7 @@ export const updateKeyRemaining = t.procedure workspace: true, }, }); - const isMonthlyInterval = input.refill?.interval === "monthly"; + if (!key || key.workspace.tenantId !== ctx.tenant.id) { throw new TRPCError({ message: @@ -49,13 +49,9 @@ export const updateKeyRemaining = t.procedure .update(schema.keys) .set({ remaining: input.remaining ?? null, - refillInterval: - input.refill?.interval === "none" || input.refill?.interval === undefined - ? null - : input.refill?.interval, - refillDay: isMonthlyInterval ? input.refill?.refillDay : null, + refillDay: input.refill?.refillDay ?? null, refillAmount: input.refill?.amount ?? null, - lastRefillAt: input.refill?.interval ? new Date() : null, + lastRefillAt: new Date(), }) .where(eq(schema.keys.id, key.id)) .catch((_err) => { diff --git a/apps/dashboard/lib/zod-helper.ts b/apps/dashboard/lib/zod-helper.ts index 7970b612ec..71d327220d 100644 --- a/apps/dashboard/lib/zod-helper.ts +++ b/apps/dashboard/lib/zod-helper.ts @@ -31,7 +31,6 @@ export const formSchema = z.object({ .positive({ message: "Please enter a positive number" }), refill: z .object({ - interval: z.enum(["none", "daily", "monthly"]), amount: z.coerce .number({ errorMap: (issue, { defaultError }) => ({ From eeea5673d73edeaf7c8e3eced0b519700200d022 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Thu, 5 Dec 2024 19:39:18 -0500 Subject: [PATCH 03/12] More fixes for types and tests using refillInterval --- apps/api/src/pkg/key_migration/message.ts | 3 +- .../routes/v1_keys_createKey.error.test.ts | 32 --------- .../routes/v1_keys_createKey.happy.test.ts | 31 --------- .../v1_migrations_createKey.error.test.ts | 36 ---------- .../[keyId]/settings/update-key-remaining.tsx | 68 +++++-------------- apps/dashboard/lib/trpc/routers/key/create.ts | 4 +- .../lib/trpc/routers/key/updateRemaining.ts | 7 +- internal/db/src/schema/key_migrations.ts | 3 +- 8 files changed, 22 insertions(+), 162 deletions(-) diff --git a/apps/api/src/pkg/key_migration/message.ts b/apps/api/src/pkg/key_migration/message.ts index 91a9e39ccc..05f6809bc7 100644 --- a/apps/api/src/pkg/key_migration/message.ts +++ b/apps/api/src/pkg/key_migration/message.ts @@ -4,7 +4,6 @@ export type MessageBody = { keyAuthId: string; rootKeyId: string; prefix?: string; - name?: string; hash: string; start?: string; @@ -14,7 +13,7 @@ export type MessageBody = { permissions?: string[]; expires?: number; remaining?: number; - refill?: { interval: "daily" | "monthly"; amount: number; refillDay?: number }; + refill?: { amount: number; refillDay?: number }; ratelimit?: { async: boolean; limit: number; duration: number }; enabled: boolean; environment?: string; diff --git a/apps/api/src/routes/v1_keys_createKey.error.test.ts b/apps/api/src/routes/v1_keys_createKey.error.test.ts index f9a197f956..abbe35cc74 100644 --- a/apps/api/src/routes/v1_keys_createKey.error.test.ts +++ b/apps/api/src/routes/v1_keys_createKey.error.test.ts @@ -118,35 +118,3 @@ test("when key recovery is not enabled", async (t) => { }, }); }); - -test("reject invalid refill config when daily interval has non-null refillDay", async (t) => { - const h = await IntegrationHarness.init(t); - - const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]); - - const res = await h.post({ - url: "/v1/keys.createKey", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${root.key}`, - }, - body: { - byteLength: 16, - apiId: h.resources.userApi.id, - remaining: 10, - refill: { - amount: 100, - refillDay: 4, - interval: "daily", - }, - }, - }); - expect(res.status).toEqual(400); - expect(res.body).toMatchObject({ - error: { - code: "BAD_REQUEST", - docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST", - message: "when interval is set to 'daily', 'refillDay' must be null.", - }, - }); -}); diff --git a/apps/api/src/routes/v1_keys_createKey.happy.test.ts b/apps/api/src/routes/v1_keys_createKey.happy.test.ts index d8a8be632d..8b02cec8e5 100644 --- a/apps/api/src/routes/v1_keys_createKey.happy.test.ts +++ b/apps/api/src/routes/v1_keys_createKey.happy.test.ts @@ -467,35 +467,4 @@ describe("with externalId", () => { expect(key!.identity!.id).toEqual(identity.id); }); }); - describe("Should default first day of month if none provided", () => { - test("should provide default value", async (t) => { - const h = await IntegrationHarness.init(t); - const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]); - - const res = await h.post({ - url: "/v1/keys.createKey", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${root.key}`, - }, - body: { - apiId: h.resources.userApi.id, - remaining: 10, - refill: { - interval: "monthly", - amount: 20, - refillDay: undefined, - }, - }, - }); - - expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200); - - const key = await h.db.primary.query.keys.findFirst({ - where: (table, { eq }) => eq(table.id, res.body.keyId), - }); - expect(key).toBeDefined(); - expect(key!.refillDay).toEqual(1); - }); - }); }); diff --git a/apps/api/src/routes/v1_migrations_createKey.error.test.ts b/apps/api/src/routes/v1_migrations_createKey.error.test.ts index 14562b30e1..45523eaec5 100644 --- a/apps/api/src/routes/v1_migrations_createKey.error.test.ts +++ b/apps/api/src/routes/v1_migrations_createKey.error.test.ts @@ -112,39 +112,3 @@ test("reject invalid ratelimit config", async (t) => { expect(res.status).toEqual(400); expect(res.body.error.code).toEqual("BAD_REQUEST"); }); -test("reject invalid refill config when daily interval has non-null refillDay", async (t) => { - const h = await IntegrationHarness.init(t); - const { key } = await h.createRootKey(["*"]); - - const res = await h.post({ - url: "/v1/migrations.createKeys", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${key}`, - }, - body: [ - { - start: "x", - hash: { - value: "x", - variant: "sha256_base64", - }, - apiId: h.resources.userApi.id, - remaining: 10, - refill: { - amount: 100, - refillDay: 4, - interval: "daily", - }, - }, - ], - }); - expect(res.status).toEqual(400); - expect(res.body).toMatchObject({ - error: { - code: "BAD_REQUEST", - docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST", - message: "when interval is set to 'daily', 'refillDay' must be null.", - }, - }); -}); diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx index 04aa23c8aa..c3102a1efe 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx @@ -20,13 +20,6 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { toast } from "@/components/ui/toaster"; import { trpc } from "@/lib/trpc/client"; @@ -42,7 +35,6 @@ const formSchema = z.object({ remaining: z.coerce.number().positive({ message: "Please enter a positive number" }).optional(), refill: z .object({ - interval: z.enum(["none", "daily", "monthly"]), amount: z.coerce .number() .int() @@ -72,9 +64,8 @@ type Props = { id: string; workspaceId: string; remaining: number | null; - refillInterval: "daily" | "monthly" | null; refillAmount: number | null; - refillDay: number | null; + refillDay: number | null | undefined; }; }; @@ -99,7 +90,7 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { // set them to undefined so the form resets properly. form.resetField("remaining", undefined); form.resetField("refill.amount", undefined); - form.resetField("refill.interval", { defaultValue: "none" }); + form.resetField("refill.refillDay", undefined); form.resetField("refill", undefined); }; const updateRemaining = trpc.key.update.remaining.useMutation({ @@ -114,13 +105,13 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { }); async function onSubmit(values: z.infer) { - if (values.refill?.interval !== "none" && !values.refill?.amount) { + if (!values.refill?.amount) { form.setError("refill.amount", { message: "Please enter the number of uses per interval", }); return; } - if (values.refill.interval !== "none" && values.remaining === undefined) { + if (values.remaining === undefined) { form.setError("remaining", { message: "Please enter a value" }); return; } @@ -128,9 +119,6 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { delete values.refill; delete values.remaining; } - if (values.refill?.interval === "none") { - delete values.refill; - } await updateRemaining.mutateAsync(values); } @@ -174,37 +162,11 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { )} /> - - ( - - Refill Rate - - - - )} - /> = ({ apiKey }) => { className="w-full" type="number" {...field} - value={form.watch("refill.interval") === "none" ? undefined : field.value} + value={form.getValues("limitEnabled") ? field.value : undefined} /> @@ -229,22 +191,28 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { /> ( - Day of the month to refill uses + Day of the month to refill uses. - Enter the day to refill monthly. - + + Enter the day to refill monthly or leave blank for daily refill + + )} /> diff --git a/apps/dashboard/lib/trpc/routers/key/create.ts b/apps/dashboard/lib/trpc/routers/key/create.ts index 93fb4c659f..ae49780878 100644 --- a/apps/dashboard/lib/trpc/routers/key/create.ts +++ b/apps/dashboard/lib/trpc/routers/key/create.ts @@ -18,7 +18,6 @@ export const createKey = t.procedure remaining: z.number().int().positive().optional(), refill: z .object({ - interval: z.enum(["daily", "monthly"]), amount: z.coerce.number().int().min(1), refillDay: z.number().int().min(1).max(31).optional(), }) @@ -102,10 +101,9 @@ export const createKey = t.procedure ratelimitLimit: input.ratelimit?.limit, ratelimitDuration: input.ratelimit?.duration, remaining: input.remaining, - refillInterval: input.refill?.interval ?? null, refillDay: input.refill?.refillDay ?? null, refillAmount: input.refill?.amount ?? null, - lastRefillAt: input.refill?.interval ? new Date() : null, + lastRefillAt: input.refill?.amount ? new Date() : null, deletedAt: null, enabled: input.enabled, environment: input.environment, diff --git a/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts b/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts index 1cae208696..456243dd52 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts @@ -12,7 +12,6 @@ export const updateKeyRemaining = t.procedure remaining: z.number().int().positive().optional(), refill: z .object({ - interval: z.enum(["daily", "monthly", "none"]), amount: z.number().int().min(1).optional(), refillDay: z.number().int().min(1).max(31).optional(), }) @@ -24,10 +23,6 @@ export const updateKeyRemaining = t.procedure input.remaining = undefined; input.refill = undefined; } - if (input.refill?.interval === "none") { - input.refill = undefined; - } - await db .transaction(async (tx) => { const key = await tx.query.keys.findFirst({ @@ -70,7 +65,7 @@ export const updateKeyRemaining = t.procedure event: "key.update", description: input.limitEnabled ? `Changed remaining for ${key.id} to remaining=${input.remaining}, refill=${ - input.refill ? `${input.refill.amount}@${input.refill.interval}` : "none" + input.refill ? `${input.refill.amount}@${input.refill.refillDay}` : "none" }` : `Disabled limit for ${key.id}`, resources: [ diff --git a/internal/db/src/schema/key_migrations.ts b/internal/db/src/schema/key_migrations.ts index 0c3470435b..c276315120 100644 --- a/internal/db/src/schema/key_migrations.ts +++ b/internal/db/src/schema/key_migrations.ts @@ -20,7 +20,6 @@ export const keyMigrationErrors = mysqlTable("key_migration_errors", { keyAuthId: string; rootKeyId: string; prefix?: string; - name?: string; hash: string; start?: string; @@ -30,7 +29,7 @@ export const keyMigrationErrors = mysqlTable("key_migration_errors", { permissions?: string[]; expires?: number; remaining?: number; - refill?: { interval: "daily" | "monthly"; amount: number; refillDay?: number | undefined }; + refill?: { amount: number; refillDay?: number | undefined }; ratelimit?: { async: boolean; limit: number; duration: number }; enabled: boolean; environment?: string; From da2f8f78bb6d4f37e6f1f065a5d4480f38c3145a Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Mon, 9 Dec 2024 14:10:48 -0500 Subject: [PATCH 04/12] migration changes --- apps/api/src/routes/schema.ts | 6 ++++ internal/db/src/schema/keys.ts | 3 +- tools/migrate/refill-migrate.ts | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tools/migrate/refill-migrate.ts diff --git a/apps/api/src/routes/schema.ts b/apps/api/src/routes/schema.ts index b06b46a3f2..f52f265a51 100644 --- a/apps/api/src/routes/schema.ts +++ b/apps/api/src/routes/schema.ts @@ -58,6 +58,11 @@ export const keySchema = z }), refill: z .object({ + interval: z.enum(["daily", "monthly"]).openapi({ + description: + "Determines the rate at which verifications will be refilled. When 'daily' is set for 'interval' 'refillDay' will be set to null.", + example: "daily", + }), amount: z.number().int().openapi({ description: "Resets `remaining` to this value every interval.", example: 100, @@ -77,6 +82,7 @@ export const keySchema = z description: "Unkey allows you to refill remaining verifications on a key on a regular interval.", example: { + interval: "monthly", amount: 10, refillDay: 10, }, diff --git a/internal/db/src/schema/keys.ts b/internal/db/src/schema/keys.ts index cff1c7edb4..81360f3d9a 100644 --- a/internal/db/src/schema/keys.ts +++ b/internal/db/src/schema/keys.ts @@ -5,6 +5,7 @@ import { datetime, index, int, + mysqlEnum, mysqlTable, text, tinyint, @@ -62,7 +63,7 @@ export const keys = mysqlTable( /** * You can refill uses to keys at a desired interval */ - + refillInterval: mysqlEnum("refill_interval", ["daily", "monthly"]), refillDay: tinyint("refill_day"), refillAmount: int("refill_amount"), lastRefillAt: datetime("last_refill_at", { fsp: 3 }), diff --git a/tools/migrate/refill-migrate.ts b/tools/migrate/refill-migrate.ts new file mode 100644 index 0000000000..f15ba090d0 --- /dev/null +++ b/tools/migrate/refill-migrate.ts @@ -0,0 +1,54 @@ +import { eq, mysqlDrizzle, schema } from "@unkey/db"; +import mysql from "mysql2/promise"; + +async function main() { + const conn = await mysql.createConnection( + `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}`, + ); + + await conn.ping(); + const db = mysqlDrizzle(conn, { schema, mode: "default" }); + + let cursor = ""; + let keyChanges = 0; + do { + const keys = await db.query.keys.findMany({ + where: (table, { isNotNull, gt, and }) => + and( + gt(table.id, cursor), + isNotNull(table.refillInterval), + isNotNull(table.refillAmount), + isNotNull(table.remaining), + ), + limit: 1000, + orderBy: (table, { asc }) => asc(table.id), + }); + + cursor = keys.at(-1)?.id ?? ""; + console.info({ cursor, keys: keys.length }); + + for (const key of keys) { + if (key.refillInterval === "monthly") { + if (key.refillDay === null) { + key.refillDay = 1; + } + key.refillInterval = null; + } + if (key.refillInterval === "daily") { + key.refillDay = null; + key.refillInterval = null; + } + const changed = await db + .update(schema.keys) + .set({ refillDay: key.refillDay, refillInterval: key.refillInterval }) + .where(eq(schema.keys.id, key.id)); + if (changed) { + keyChanges++; + } + } + } while (cursor); + await conn.end(); + console.info("Migration completed. Keys Changed", keyChanges); +} + +main(); From 130f749ea06779c83c856648106b586c2ab689e6 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Mon, 9 Dec 2024 14:12:31 -0500 Subject: [PATCH 05/12] reverted change --- apps/api/src/routes/v1_keys_createKey.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/api/src/routes/v1_keys_createKey.ts b/apps/api/src/routes/v1_keys_createKey.ts index 924c381237..0a272ca2b9 100644 --- a/apps/api/src/routes/v1_keys_createKey.ts +++ b/apps/api/src/routes/v1_keys_createKey.ts @@ -110,6 +110,9 @@ When validating a key, we will return this back to you, so you can clearly ident }), refill: z .object({ + interval: z.enum(["daily", "monthly"]).openapi({ + description: "Unkey will automatically refill verifications at the set interval.", + }), amount: z.number().int().min(1).positive().openapi({ description: "The number of verifications to refill for each occurrence is determined individually for each key.", From 84b392904f8cc6e1495ad238e4981ce280b16b24 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Mon, 16 Dec 2024 08:21:26 -0500 Subject: [PATCH 06/12] save before pull main --- apps/api/src/routes/schema.ts | 2 +- apps/api/src/routes/v1_keys_createKey.ts | 3 +- apps/api/src/routes/v1_keys_getKey.ts | 1 + .../api/src/routes/v1_migrations_createKey.ts | 11 +++++++ .../[apiId]/keys/[keyAuthId]/new/client.tsx | 32 ------------------- 5 files changed, 15 insertions(+), 34 deletions(-) diff --git a/apps/api/src/routes/schema.ts b/apps/api/src/routes/schema.ts index f52f265a51..1c86a004cb 100644 --- a/apps/api/src/routes/schema.ts +++ b/apps/api/src/routes/schema.ts @@ -58,7 +58,7 @@ export const keySchema = z }), refill: z .object({ - interval: z.enum(["daily", "monthly"]).openapi({ + interval: z.enum(["daily", "monthly"]).optional().openapi({ description: "Determines the rate at which verifications will be refilled. When 'daily' is set for 'interval' 'refillDay' will be set to null.", example: "daily", diff --git a/apps/api/src/routes/v1_keys_createKey.ts b/apps/api/src/routes/v1_keys_createKey.ts index 0a272ca2b9..2c37186a59 100644 --- a/apps/api/src/routes/v1_keys_createKey.ts +++ b/apps/api/src/routes/v1_keys_createKey.ts @@ -110,7 +110,7 @@ When validating a key, we will return this back to you, so you can clearly ident }), refill: z .object({ - interval: z.enum(["daily", "monthly"]).openapi({ + interval: z.enum(["daily", "monthly"]).optional().openapi({ description: "Unkey will automatically refill verifications at the set interval.", }), amount: z.number().int().min(1).positive().openapi({ @@ -372,6 +372,7 @@ export const registerV1KeysCreateKey = (app: App) => ratelimitLimit: req.ratelimit?.limit ?? req.ratelimit?.refillRate, ratelimitDuration: req.ratelimit?.duration ?? req.ratelimit?.refillInterval, remaining: req.remaining, + refillInterval: req.refill?.interval ?? null, refillDay: req?.refill?.refillDay ?? null, refillAmount: req.refill?.amount, lastRefillAt: null, diff --git a/apps/api/src/routes/v1_keys_getKey.ts b/apps/api/src/routes/v1_keys_getKey.ts index 6ce98f31ae..c9509b0bc5 100644 --- a/apps/api/src/routes/v1_keys_getKey.ts +++ b/apps/api/src/routes/v1_keys_getKey.ts @@ -152,6 +152,7 @@ export const registerV1KeysGetKey = (app: App) => remaining: key.remaining ?? undefined, refill: key.refillAmount ? { + interval: key.refillInterval ?? undefined, amount: key.refillAmount, refillDay: key.refillDay ?? null, lastRefillAt: key.lastRefillAt?.getTime(), diff --git a/apps/api/src/routes/v1_migrations_createKey.ts b/apps/api/src/routes/v1_migrations_createKey.ts index 072d636af8..54e1658f21 100644 --- a/apps/api/src/routes/v1_migrations_createKey.ts +++ b/apps/api/src/routes/v1_migrations_createKey.ts @@ -123,6 +123,10 @@ When validating a key, we will return this back to you, so you can clearly ident }), refill: z .object({ + interval: z.enum(["monthly", "daily"]).optional().openapi({ + description: + "The interval at which we will refill the remaining verifications.", + }), amount: z.number().int().min(1).positive().openapi({ description: "The number of verifications to refill for each occurrence is determined individually for each key.", @@ -399,6 +403,12 @@ export const registerV1MigrationsCreateKeys = (app: App) => const hash = key.plaintext ? await sha256(key.plaintext) : key.hash!.value; + if (key.refill?.interval === "monthly" && key.refill?.refillDay === undefined) { + key.refill.refillDay = 1; + } + if (key.refill?.interval === "daily" && key.refill?.refillDay !== undefined) { + key.refill.refillDay = undefined; + } keys.push({ id: key.keyId, keyAuthId: api.keyAuthId!, @@ -416,6 +426,7 @@ export const registerV1MigrationsCreateKeys = (app: App) => ratelimitLimit: key.ratelimit?.limit ?? key.ratelimit?.refillRate ?? null, ratelimitDuration: key.ratelimit?.refillInterval ?? key.ratelimit?.refillInterval ?? null, remaining: key.remaining ?? null, + refillInterval: null, refillDay: key?.refill?.refillDay ?? 1, refillAmount: key.refill?.amount ?? null, deletedAt: null, diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx index ebcecf9150..987067427e 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx @@ -17,13 +17,6 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Button } from "@unkey/ui"; import { Separator } from "@/components/ui/separator"; @@ -493,31 +486,6 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro )} /> - ( - - Refill Rate - - - Interval key will be refilled. - - - )} - /> Date: Mon, 16 Dec 2024 10:05:01 -0500 Subject: [PATCH 07/12] fixed checks for correct data in refill --- apps/api/src/routes/v1_keys_createKey.ts | 25 +++++++++++-------- .../api/src/routes/v1_migrations_createKey.ts | 18 +++++++++---- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/apps/api/src/routes/v1_keys_createKey.ts b/apps/api/src/routes/v1_keys_createKey.ts index 2c37186a59..e3c859d75b 100644 --- a/apps/api/src/routes/v1_keys_createKey.ts +++ b/apps/api/src/routes/v1_keys_createKey.ts @@ -312,18 +312,21 @@ export const registerV1KeysCreateKey = (app: App) => message: "remaining must be greater than 0.", }); } - if (req.remaining === null || req.remaining === undefined) { - throw new UnkeyApiError({ - code: "BAD_REQUEST", - message: "remaining must be set if you are using refill.", - }); - } - if (req.refill && !req.refill.amount) { - throw new UnkeyApiError({ - code: "BAD_REQUEST", - message: "refill.amount must be set if you are using refill.", - }); + if (req.refill) { + if (req.remaining === null || req.remaining === undefined) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "remaining must be set if you are using refill.", + }); + } + if (!req.refill.amount) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "refill.amount must be set if you are using refill.", + }); + } } + /** * Set up an api for production */ diff --git a/apps/api/src/routes/v1_migrations_createKey.ts b/apps/api/src/routes/v1_migrations_createKey.ts index 54e1658f21..a79a848a6b 100644 --- a/apps/api/src/routes/v1_migrations_createKey.ts +++ b/apps/api/src/routes/v1_migrations_createKey.ts @@ -383,11 +383,19 @@ export const registerV1MigrationsCreateKeys = (app: App) => }); } - if (key.remaining === null || key.remaining === undefined) { - throw new UnkeyApiError({ - code: "BAD_REQUEST", - message: "remaining must be set if you are using refill.", - }); + if (key.refill) { + if (key.remaining === null || key.remaining === undefined) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "remaining must be set if you are using refill.", + }); + } + if (!key.refill.amount) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "refill.amount must be set if you are using refill.", + }); + } } if (!key.hash && !key.plaintext) { From 624bb92c3ceab30d9e8aa05b2d5347141a59b876 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Mon, 16 Dec 2024 15:02:22 -0500 Subject: [PATCH 08/12] partial Fixes --- .../[apiId]/keys/[keyAuthId]/new/client.tsx | 18 +++++------------- .../[apiId]/keys/[keyAuthId]/new/validation.ts | 4 ++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx index 987067427e..316462481d 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx @@ -91,13 +91,10 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro if (!values.ratelimitEnabled) { delete values.ratelimit; } - const refill = values.limit?.refill; - if (refill?.interval === "daily") { - refill?.refillDay === undefined; - } - if (refill?.interval === "monthly" && !refill.refillDay) { - refill.refillDay = 1; + if (!values.limit?.refill?.amount) { + delete values.limit?.refill; } + await key.mutateAsync({ keyAuthId, @@ -106,7 +103,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro expires: values.expires?.getTime() ?? undefined, ownerId: values.ownerId ?? undefined, remaining: values.limit?.remaining ?? undefined, - refill: refill, + refill: values.limit?.refill?.amount !== undefined ? { amount: values.limit.refill.amount, refillDay: values.limit.refill.refillDay } : undefined, enabled: true, }); @@ -136,7 +133,6 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro const resetLimited = () => { form.resetField("limit.refill.amount", undefined); - form.resetField("limit.refill.interval", undefined); form.resetField("limit.refill", undefined); form.resetField("limit.remaining", undefined); form.resetField("limit", undefined); @@ -513,10 +509,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro ( @@ -563,7 +556,6 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro ( diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/validation.ts b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/validation.ts index 69d47f70ca..f036148dd7 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/validation.ts +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/validation.ts @@ -51,7 +51,6 @@ export const formSchema = z.object({ .positive({ message: "Please enter a positive number" }), refill: z .object({ - interval: z.enum(["daily", "monthly"]).default("monthly"), amount: z.coerce .number({ errorMap: (issue, { defaultError }) => ({ @@ -63,7 +62,8 @@ export const formSchema = z.object({ }) .int() .min(1) - .positive(), + .positive() + .optional(), refillDay: z.coerce .number({ errorMap: (issue, { defaultError }) => ({ From a55c17d764f27d1994cb06114d401dd1befbb5de Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Mon, 16 Dec 2024 16:32:05 -0500 Subject: [PATCH 09/12] changed new and update key form --- .../[keyId]/settings/update-key-remaining.tsx | 34 ++++------- .../[apiId]/keys/[keyAuthId]/new/client.tsx | 8 ++- .../keys/[keyAuthId]/new/validation.ts | 58 +++++++++++-------- 3 files changed, 49 insertions(+), 51 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx index 2fc173a239..f6906b6967 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-remaining.tsx @@ -35,26 +35,14 @@ const formSchema = z.object({ remaining: z.coerce.number().positive({ message: "Please enter a positive number" }).optional(), refill: z .object({ - amount: z.coerce - .number() - .int() - .min(1, { - message: "Please enter the number of uses per interval", - }) - .positive() - .optional(), - refillDay: z.coerce - .number({ - errorMap: (issue, { defaultError }) => ({ - message: - issue.code === "invalid_type" - ? "Refill day must be an integer between 1 and 31" - : defaultError, - }), - }) - .int() - .min(1) - .max(31) + amount: z + .literal("") + .transform(() => undefined) + .or(z.coerce.number().int().positive()), + refillDay: z + .literal("") + .transform(() => undefined) + .or(z.coerce.number().int().max(31).positive()) .optional(), }) .optional(), @@ -105,9 +93,9 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { }); async function onSubmit(values: z.infer) { - if (!values.refill?.amount) { + if (!values.refill?.amount && values.refill?.refillDay) { form.setError("refill.amount", { - message: "Please enter the number of uses per interval", + message: "Please enter the number of uses per interval or remove the refill day", }); return; } @@ -185,7 +173,7 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { Enter the number of uses to refill per interval. - + )} /> diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx index 316462481d..4ad9495363 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx @@ -94,7 +94,6 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro if (!values.limit?.refill?.amount) { delete values.limit?.refill; } - await key.mutateAsync({ keyAuthId, @@ -103,7 +102,10 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro expires: values.expires?.getTime() ?? undefined, ownerId: values.ownerId ?? undefined, remaining: values.limit?.remaining ?? undefined, - refill: values.limit?.refill?.amount !== undefined ? { amount: values.limit.refill.amount, refillDay: values.limit.refill.refillDay } : undefined, + refill: + values.limit?.refill?.amount !== undefined + ? { amount: values.limit.refill.amount, refillDay: values.limit.refill.refillDay } + : undefined, enabled: true, }); @@ -509,7 +511,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro ( diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/validation.ts b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/validation.ts index f036148dd7..d3465a981b 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/validation.ts +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/validation.ts @@ -51,31 +51,39 @@ export const formSchema = z.object({ .positive({ message: "Please enter a positive number" }), refill: z .object({ - amount: z.coerce - .number({ - errorMap: (issue, { defaultError }) => ({ - message: - issue.code === "invalid_type" - ? "Refill amount must be greater than 0 and a integer" - : defaultError, - }), - }) - .int() - .min(1) - .positive() - .optional(), - refillDay: z.coerce - .number({ - errorMap: (issue, { defaultError }) => ({ - message: - issue.code === "invalid_type" - ? "Refill day must be an integer between 1 and 31" - : defaultError, - }), - }) - .int() - .min(1) - .max(31) + amount: z + .literal("") + .transform(() => undefined) + .or( + z.coerce + .number({ + errorMap: (issue, { defaultError }) => ({ + message: + issue.code === "invalid_type" + ? "Refill amount must be greater than 0 and a integer" + : defaultError, + }), + }) + .int() + .positive(), + ), + refillDay: z + .literal("") + .transform(() => undefined) + .or( + z.coerce + .number({ + errorMap: (issue, { defaultError }) => ({ + message: + issue.code === "invalid_type" + ? "Refill day must be an integer between 1 and 31" + : defaultError, + }), + }) + .int() + .max(31) + .positive(), + ) .optional(), }) .optional(), From 932b4b618d560ad6d51d6a68c3a3409e4cced31a Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Tue, 17 Dec 2024 12:27:27 -0500 Subject: [PATCH 10/12] revert api changes --- apps/api/src/pkg/key_migration/handler.ts | 1 + apps/api/src/pkg/key_migration/message.ts | 2 +- apps/api/src/routes/v1_apis_listKeys.ts | 1 + .../routes/v1_keys_createKey.error.test.ts | 32 +++++++++++++++++ .../routes/v1_keys_createKey.happy.test.ts | 31 ++++++++++++++++ apps/api/src/routes/v1_keys_createKey.ts | 9 ++++- apps/api/src/routes/v1_keys_getKey.ts | 1 - .../routes/v1_keys_updateKey.happy.test.ts | 1 + apps/api/src/routes/v1_keys_updateKey.ts | 2 ++ .../v1_migrations_createKey.error.test.ts | 36 +++++++++++++++++++ .../v1_migrations_createKey.happy.test.ts | 2 ++ .../api/src/routes/v1_migrations_createKey.ts | 30 ++++++++-------- apps/dashboard/lib/trpc/routers/key/create.ts | 1 + .../lib/trpc/routers/key/createRootKey.ts | 1 + .../lib/trpc/routers/key/updateRemaining.ts | 3 +- internal/db/src/schema/key_migrations.ts | 2 +- 16 files changed, 134 insertions(+), 21 deletions(-) diff --git a/apps/api/src/pkg/key_migration/handler.ts b/apps/api/src/pkg/key_migration/handler.ts index 3ebe525f14..452b30a4ad 100644 --- a/apps/api/src/pkg/key_migration/handler.ts +++ b/apps/api/src/pkg/key_migration/handler.ts @@ -80,6 +80,7 @@ export async function migrateKey( createdAt: new Date(), createdAtM: Date.now(), expires: message.expires ? new Date(message.expires) : null, + refillInterval: message.refill?.interval, refillAmount: message.refill?.amount, refillDay: message.refill?.refillDay, enabled: message.enabled, diff --git a/apps/api/src/pkg/key_migration/message.ts b/apps/api/src/pkg/key_migration/message.ts index 05f6809bc7..032a619a76 100644 --- a/apps/api/src/pkg/key_migration/message.ts +++ b/apps/api/src/pkg/key_migration/message.ts @@ -13,7 +13,7 @@ export type MessageBody = { permissions?: string[]; expires?: number; remaining?: number; - refill?: { amount: number; refillDay?: number }; + refill?: { interval: "daily" | "monthly"; amount: number; refillDay?: number }; ratelimit?: { async: boolean; limit: number; duration: number }; enabled: boolean; environment?: string; diff --git a/apps/api/src/routes/v1_apis_listKeys.ts b/apps/api/src/routes/v1_apis_listKeys.ts index 5b24ce3f30..c10ceb8e58 100644 --- a/apps/api/src/routes/v1_apis_listKeys.ts +++ b/apps/api/src/routes/v1_apis_listKeys.ts @@ -320,6 +320,7 @@ export const registerV1ApisListKeys = (app: App) => refill: k.refillAmount && k.lastRefillAt ? { + interval: k.refillInterval ?? undefined, amount: k.refillAmount, refillDay: k.refillDay ?? null, lastRefillAt: k.lastRefillAt?.getTime(), diff --git a/apps/api/src/routes/v1_keys_createKey.error.test.ts b/apps/api/src/routes/v1_keys_createKey.error.test.ts index abbe35cc74..99e7b27341 100644 --- a/apps/api/src/routes/v1_keys_createKey.error.test.ts +++ b/apps/api/src/routes/v1_keys_createKey.error.test.ts @@ -118,3 +118,35 @@ test("when key recovery is not enabled", async (t) => { }, }); }); + +test("reject invalid refill config when daily interval has non-null refillDay", async (t) => { + const h = await IntegrationHarness.init(t); + + const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]); + + const res = await h.post({ + url: "/v1/keys.createKey", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + byteLength: 16, + apiId: h.resources.userApi.id, + remaining: 10, + refill: { + amount: 100, + refillDay: 4, + interval: "daily", + }, + }, + }); + expect(res.status).toEqual(400); + expect(res.body).toMatchObject({ + error: { + code: "BAD_REQUEST", + docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST", + message: "When interval is set to 'daily', 'refillDay' must be null.", + }, + }); +}); diff --git a/apps/api/src/routes/v1_keys_createKey.happy.test.ts b/apps/api/src/routes/v1_keys_createKey.happy.test.ts index 8b02cec8e5..d8a8be632d 100644 --- a/apps/api/src/routes/v1_keys_createKey.happy.test.ts +++ b/apps/api/src/routes/v1_keys_createKey.happy.test.ts @@ -467,4 +467,35 @@ describe("with externalId", () => { expect(key!.identity!.id).toEqual(identity.id); }); }); + describe("Should default first day of month if none provided", () => { + test("should provide default value", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]); + + const res = await h.post({ + url: "/v1/keys.createKey", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + apiId: h.resources.userApi.id, + remaining: 10, + refill: { + interval: "monthly", + amount: 20, + refillDay: undefined, + }, + }, + }); + + expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200); + + const key = await h.db.primary.query.keys.findFirst({ + where: (table, { eq }) => eq(table.id, res.body.keyId), + }); + expect(key).toBeDefined(); + expect(key!.refillDay).toEqual(1); + }); + }); }); diff --git a/apps/api/src/routes/v1_keys_createKey.ts b/apps/api/src/routes/v1_keys_createKey.ts index e3c859d75b..2bd06ce7d0 100644 --- a/apps/api/src/routes/v1_keys_createKey.ts +++ b/apps/api/src/routes/v1_keys_createKey.ts @@ -132,6 +132,7 @@ When validating a key, we will return this back to you, so you can clearly ident description: "Unkey enables you to refill verifications for each key at regular intervals.", example: { + interval: "monthly", amount: 100, refillDay: 15, }, @@ -325,6 +326,12 @@ export const registerV1KeysCreateKey = (app: App) => message: "refill.amount must be set if you are using refill.", }); } + if (req.refill.interval === "daily" && req.refill.refillDay) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "When interval is set to 'daily', 'refillDay' must be null.", + }); + } } /** @@ -376,7 +383,7 @@ export const registerV1KeysCreateKey = (app: App) => ratelimitDuration: req.ratelimit?.duration ?? req.ratelimit?.refillInterval, remaining: req.remaining, refillInterval: req.refill?.interval ?? null, - refillDay: req?.refill?.refillDay ?? null, + refillDay: req.refill?.interval === "daily" ? null : req?.refill?.refillDay ?? 1, refillAmount: req.refill?.amount, lastRefillAt: null, deletedAt: null, diff --git a/apps/api/src/routes/v1_keys_getKey.ts b/apps/api/src/routes/v1_keys_getKey.ts index c9509b0bc5..77c98cc88e 100644 --- a/apps/api/src/routes/v1_keys_getKey.ts +++ b/apps/api/src/routes/v1_keys_getKey.ts @@ -162,7 +162,6 @@ export const registerV1KeysGetKey = (app: App) => key.ratelimitAsync !== null && key.ratelimitLimit !== null && key.ratelimitDuration !== null ? { async: key.ratelimitAsync, - type: key.ratelimitAsync ? "fast" : ("consistent" as any), limit: key.ratelimitLimit, duration: key.ratelimitDuration, diff --git a/apps/api/src/routes/v1_keys_updateKey.happy.test.ts b/apps/api/src/routes/v1_keys_updateKey.happy.test.ts index e9583c146e..9387711286 100644 --- a/apps/api/src/routes/v1_keys_updateKey.happy.test.ts +++ b/apps/api/src/routes/v1_keys_updateKey.happy.test.ts @@ -1054,6 +1054,7 @@ describe("When refillDay is omitted.", () => { expect(found).toBeDefined(); expect(found?.remaining).toEqual(10); expect(found?.refillAmount).toEqual(130); + expect(found?.refillInterval).toEqual("monthly"); expect(found?.refillDay).toEqual(1); }); }); diff --git a/apps/api/src/routes/v1_keys_updateKey.ts b/apps/api/src/routes/v1_keys_updateKey.ts index 302d3e194e..d12c521896 100644 --- a/apps/api/src/routes/v1_keys_updateKey.ts +++ b/apps/api/src/routes/v1_keys_updateKey.ts @@ -389,10 +389,12 @@ export const registerV1KeysUpdate = (app: App) => if (typeof req.refill !== "undefined") { if (req.refill === null) { + changes.refillInterval = null; changes.refillAmount = null; changes.refillDay = null; changes.lastRefillAt = null; } else { + changes.refillInterval = req.refill.interval; changes.refillAmount = req.refill.amount; changes.refillDay = req.refill.refillDay ?? 1; } diff --git a/apps/api/src/routes/v1_migrations_createKey.error.test.ts b/apps/api/src/routes/v1_migrations_createKey.error.test.ts index 45523eaec5..14562b30e1 100644 --- a/apps/api/src/routes/v1_migrations_createKey.error.test.ts +++ b/apps/api/src/routes/v1_migrations_createKey.error.test.ts @@ -112,3 +112,39 @@ test("reject invalid ratelimit config", async (t) => { expect(res.status).toEqual(400); expect(res.body.error.code).toEqual("BAD_REQUEST"); }); +test("reject invalid refill config when daily interval has non-null refillDay", async (t) => { + const h = await IntegrationHarness.init(t); + const { key } = await h.createRootKey(["*"]); + + const res = await h.post({ + url: "/v1/migrations.createKeys", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${key}`, + }, + body: [ + { + start: "x", + hash: { + value: "x", + variant: "sha256_base64", + }, + apiId: h.resources.userApi.id, + remaining: 10, + refill: { + amount: 100, + refillDay: 4, + interval: "daily", + }, + }, + ], + }); + expect(res.status).toEqual(400); + expect(res.body).toMatchObject({ + error: { + code: "BAD_REQUEST", + docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST", + message: "when interval is set to 'daily', 'refillDay' must be null.", + }, + }); +}); diff --git a/apps/api/src/routes/v1_migrations_createKey.happy.test.ts b/apps/api/src/routes/v1_migrations_createKey.happy.test.ts index 433c2e1242..6e633b90a3 100644 --- a/apps/api/src/routes/v1_migrations_createKey.happy.test.ts +++ b/apps/api/src/routes/v1_migrations_createKey.happy.test.ts @@ -520,6 +520,7 @@ describe("Should default to first day of month if none provided", () => { enabled: true, remaining: 10, refill: { + interval: "monthly", amount: 100, refillDay: undefined, }, @@ -535,6 +536,7 @@ describe("Should default to first day of month if none provided", () => { expect(found).toBeDefined(); expect(found?.remaining).toEqual(10); expect(found?.refillAmount).toEqual(100); + expect(found?.refillInterval).toEqual("monthly"); expect(found?.refillDay).toEqual(1); expect(found?.hash).toEqual(hash); }); diff --git a/apps/api/src/routes/v1_migrations_createKey.ts b/apps/api/src/routes/v1_migrations_createKey.ts index a79a848a6b..f5e691bad1 100644 --- a/apps/api/src/routes/v1_migrations_createKey.ts +++ b/apps/api/src/routes/v1_migrations_createKey.ts @@ -146,6 +146,7 @@ When validating a key, we will return this back to you, so you can clearly ident description: "Unkey enables you to refill verifications for each key at regular intervals.", example: { + interval: "monthly", refillDay: 15, amount: 100, }, @@ -383,19 +384,11 @@ export const registerV1MigrationsCreateKeys = (app: App) => }); } - if (key.refill) { - if (key.remaining === null || key.remaining === undefined) { - throw new UnkeyApiError({ - code: "BAD_REQUEST", - message: "remaining must be set if you are using refill.", - }); - } - if (!key.refill.amount) { - throw new UnkeyApiError({ - code: "BAD_REQUEST", - message: "refill.amount must be set if you are using refill.", - }); - } + if ((key.remaining === null || key.remaining === undefined) && key.refill?.interval) { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "remaining must be set if you are using refill.", + }); } if (!key.hash && !key.plaintext) { @@ -404,7 +397,12 @@ export const registerV1MigrationsCreateKeys = (app: App) => message: "provide either `hash` or `plaintext`", }); } - + if (key.refill?.refillDay && key.refill.interval === "daily") { + throw new UnkeyApiError({ + code: "BAD_REQUEST", + message: "when interval is set to 'daily', 'refillDay' must be null.", + }); + } /** * Set up an api for production */ @@ -434,8 +432,8 @@ export const registerV1MigrationsCreateKeys = (app: App) => ratelimitLimit: key.ratelimit?.limit ?? key.ratelimit?.refillRate ?? null, ratelimitDuration: key.ratelimit?.refillInterval ?? key.ratelimit?.refillInterval ?? null, remaining: key.remaining ?? null, - refillInterval: null, - refillDay: key?.refill?.refillDay ?? 1, + refillInterval: key.refill?.interval ?? null, + refillDay: key.refill?.interval === "daily" ? null : key?.refill?.refillDay ?? 1, refillAmount: key.refill?.amount ?? null, deletedAt: null, enabled: key.enabled ?? true, diff --git a/apps/dashboard/lib/trpc/routers/key/create.ts b/apps/dashboard/lib/trpc/routers/key/create.ts index ae49780878..2af2ec4dca 100644 --- a/apps/dashboard/lib/trpc/routers/key/create.ts +++ b/apps/dashboard/lib/trpc/routers/key/create.ts @@ -101,6 +101,7 @@ export const createKey = t.procedure ratelimitLimit: input.ratelimit?.limit, ratelimitDuration: input.ratelimit?.duration, remaining: input.remaining, + refillInterval: input.refill?.refillDay ? "monthly" : "daily", refillDay: input.refill?.refillDay ?? null, refillAmount: input.refill?.amount ?? null, lastRefillAt: input.refill?.amount ? new Date() : null, diff --git a/apps/dashboard/lib/trpc/routers/key/createRootKey.ts b/apps/dashboard/lib/trpc/routers/key/createRootKey.ts index 35562275eb..45fa061841 100644 --- a/apps/dashboard/lib/trpc/routers/key/createRootKey.ts +++ b/apps/dashboard/lib/trpc/routers/key/createRootKey.ts @@ -91,6 +91,7 @@ export const createRootKey = t.procedure expires: null, createdAt: new Date(), remaining: null, + refillInterval: null, refillAmount: null, refillDay: null, lastRefillAt: null, diff --git a/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts b/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts index 456243dd52..e438c109b0 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts @@ -43,10 +43,11 @@ export const updateKeyRemaining = t.procedure await tx .update(schema.keys) .set({ + refillInterval: input.refill?.refillDay ? "monthly" : "daily", remaining: input.remaining ?? null, refillDay: input.refill?.refillDay ?? null, refillAmount: input.refill?.amount ?? null, - lastRefillAt: new Date(), + lastRefillAt: input.refill?.amount ? new Date() : null, }) .where(eq(schema.keys.id, key.id)) .catch((_err) => { diff --git a/internal/db/src/schema/key_migrations.ts b/internal/db/src/schema/key_migrations.ts index c276315120..1198fb998e 100644 --- a/internal/db/src/schema/key_migrations.ts +++ b/internal/db/src/schema/key_migrations.ts @@ -29,7 +29,7 @@ export const keyMigrationErrors = mysqlTable("key_migration_errors", { permissions?: string[]; expires?: number; remaining?: number; - refill?: { amount: number; refillDay?: number | undefined }; + refill?: { interval: "daily" | "monthly"; amount: number; refillDay?: number | undefined }; ratelimit?: { async: boolean; limit: number; duration: number }; enabled: boolean; environment?: string; From 775fc5401e07c3dddb8e06f6c94592560e7ee1db Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Tue, 17 Dec 2024 14:17:12 -0500 Subject: [PATCH 11/12] mod refill migration --- tools/migrate/refill-migrate.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/migrate/refill-migrate.ts b/tools/migrate/refill-migrate.ts index f15ba090d0..2a33c715b9 100644 --- a/tools/migrate/refill-migrate.ts +++ b/tools/migrate/refill-migrate.ts @@ -32,11 +32,9 @@ async function main() { if (key.refillDay === null) { key.refillDay = 1; } - key.refillInterval = null; } if (key.refillInterval === "daily") { key.refillDay = null; - key.refillInterval = null; } const changed = await db .update(schema.keys) From 16df3fc55e05405e25eb6f8d4d5408563aa7a233 Mon Sep 17 00:00:00 2001 From: Michael Silva Date: Tue, 17 Dec 2024 15:16:00 -0500 Subject: [PATCH 12/12] Refined migration --- tools/migrate/refill-migrate.ts | 37 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/tools/migrate/refill-migrate.ts b/tools/migrate/refill-migrate.ts index 2a33c715b9..c01541d5ca 100644 --- a/tools/migrate/refill-migrate.ts +++ b/tools/migrate/refill-migrate.ts @@ -1,4 +1,4 @@ -import { eq, mysqlDrizzle, schema } from "@unkey/db"; +import { eq, isNull, mysqlDrizzle, schema } from "@unkey/db"; import mysql from "mysql2/promise"; async function main() { @@ -13,12 +13,15 @@ async function main() { let keyChanges = 0; do { const keys = await db.query.keys.findMany({ - where: (table, { isNotNull, gt, and }) => + where: (table, { isNotNull, gt, and, or }) => and( gt(table.id, cursor), - isNotNull(table.refillInterval), isNotNull(table.refillAmount), isNotNull(table.remaining), + or( + and(eq(table.refillInterval, "monthly"), isNull(table.refillDay)), + and(eq(table.refillInterval, "daily"), isNotNull(table.refillDay)), + ), ), limit: 1000, orderBy: (table, { asc }) => asc(table.id), @@ -28,20 +31,22 @@ async function main() { console.info({ cursor, keys: keys.length }); for (const key of keys) { - if (key.refillInterval === "monthly") { - if (key.refillDay === null) { - key.refillDay = 1; + if (key.refillInterval === "monthly" && key.refillDay === null) { + const changed = await db + .update(schema.keys) + .set({ refillDay: 1 }) + .where(eq(schema.keys.id, key.id)); + if (changed) { + keyChanges++; + } + } else if (key.refillInterval === "daily" && key.refillDay !== null) { + const changed = await db + .update(schema.keys) + .set({ refillDay: null }) + .where(eq(schema.keys.id, key.id)); + if (changed) { + keyChanges++; } - } - if (key.refillInterval === "daily") { - key.refillDay = null; - } - const changed = await db - .update(schema.keys) - .set({ refillDay: key.refillDay, refillInterval: key.refillInterval }) - .where(eq(schema.keys.id, key.id)); - if (changed) { - keyChanges++; } } } while (cursor);