Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow-developer-to-set-a-custom-refill-time-when-using-the #2114

Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
6ed290f
refill day working on dashboard
MichaelUnkey Sep 18, 2024
17e48c1
First draft refill route changes
MichaelUnkey Sep 19, 2024
b14c926
fmt
MichaelUnkey Sep 19, 2024
8472e31
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Sep 19, 2024
8d07b05
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 19, 2024
0904817
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Sep 19, 2024
463f07d
working create
MichaelUnkey Sep 23, 2024
ac4ccaf
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 23, 2024
2dce62d
Docs
MichaelUnkey Sep 23, 2024
2a76eb6
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Sep 23, 2024
c35d5d3
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 23, 2024
f2647e1
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Sep 23, 2024
85d2da3
reqeusted changes part one
MichaelUnkey Sep 25, 2024
ae98c89
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 25, 2024
29558b5
tests added
MichaelUnkey Sep 26, 2024
616d9a0
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Sep 26, 2024
71c668c
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 26, 2024
da8b292
Fixed errors made changes from comments
MichaelUnkey Sep 26, 2024
cb283dc
missed file
MichaelUnkey Sep 26, 2024
48635cd
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 26, 2024
6476ba5
store changes for review not complete
MichaelUnkey Sep 27, 2024
e17617b
test changes
MichaelUnkey Sep 27, 2024
eb4df2e
test Fixes
MichaelUnkey Sep 27, 2024
d553e56
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 27, 2024
666c6a3
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 27, 2024
7446529
api test fix
MichaelUnkey Sep 27, 2024
0997a90
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Sep 27, 2024
13529b1
wording
MichaelUnkey Sep 29, 2024
1058fb2
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 30, 2024
a0847e9
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Sep 30, 2024
159869a
added permissions
MichaelUnkey Sep 30, 2024
f8a5480
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Sep 30, 2024
a46d8ea
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 30, 2024
97886d0
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 4, 2024
cfd7c49
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 4, 2024
3834fd7
Git comment changes
MichaelUnkey Oct 5, 2024
fdf8079
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 5, 2024
566bcb7
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 8, 2024
2b5c328
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Oct 8, 2024
5979d61
build issues
MichaelUnkey Oct 8, 2024
dc98949
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Oct 8, 2024
6e9dae9
planetscale v change
MichaelUnkey Oct 8, 2024
2ed73d8
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 8, 2024
5299ca7
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Oct 8, 2024
962324e
Fixed bucket cache audit logs
MichaelUnkey Oct 9, 2024
2685fe4
merge
MichaelUnkey Oct 9, 2024
315b16d
remove whitespace
MichaelUnkey Oct 10, 2024
c8c5907
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 10, 2024
9870f06
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 10, 2024
01427ee
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 15, 2024
061b995
revert to working
MichaelUnkey Oct 15, 2024
a00e9c2
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 15, 2024
7205bee
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2024
587f57e
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Oct 15, 2024
1e49cdd
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Oct 15, 2024
e895a13
Fix type
MichaelUnkey Oct 15, 2024
5c2c9bb
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 15, 2024
d93ce6d
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2024
a97b2b2
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Oct 16, 2024
1c70160
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Oct 16, 2024
a5ff863
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Oct 18, 2024
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
1 change: 1 addition & 0 deletions apps/api/src/pkg/key_migration/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export async function migrateKey(
expires: message.expires ? new Date(message.expires) : null,
refillInterval: message.refill?.interval,
refillAmount: message.refill?.amount,
refillDay: message.refill?.refillDay,
enabled: message.enabled,
remaining: message.remaining,
ratelimitAsync: message.ratelimit?.async,
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/pkg/key_migration/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type MessageBody = {
permissions?: string[];
expires?: number;
remaining?: number;
refill?: { interval: "daily" | "monthly"; amount: number };
refill?: { interval: "daily" | "monthly"; amount: number; refillDay?: number };
ratelimit?: { async: boolean; limit: number; duration: number };
enabled: boolean;
environment?: string;
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/routes/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export const keySchema = z
description: "Resets `remaining` to this value every interval.",
example: 100,
}),
refillDay: z.number().min(1).max(31).default(1).nullable().openapi({
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
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.",
}),
lastRefillAt: z.number().int().optional().openapi({
description: "The unix timestamp in miliseconds when the key was last refilled.",
example: 100,
Expand All @@ -76,10 +80,12 @@ export const keySchema = z
description:
"Unkey allows you to refill remaining verifications on a key on a regular interval.",
example: {
interval: "daily",
interval: "monthly",
amount: 10,
refillDay: 10,
},
}),

ratelimit: z
.object({
async: z.boolean().openapi({
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v1_apis_listKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export const registerV1ApisListKeys = (app: App) =>
? {
interval: k.refillInterval,
amount: k.refillAmount,
refillDay: k.refillInterval === "monthly" ? k.refillDay : null,
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
lastRefillAt: k.lastRefillAt?.getTime(),
}
: undefined,
Expand Down
33 changes: 32 additions & 1 deletion apps/api/src/routes/v1_keys_createKey.error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { V1KeysCreateKeyRequest, V1KeysCreateKeyResponse } from "./v1_keys_
test("when the api does not exist", async (t) => {
const h = await IntegrationHarness.init(t);
const apiId = newId("api");

const root = await h.createRootKey([`api.${apiId}.create_key`]);
/* The code snippet is making a POST request to the "/v1/keys.createKey" endpoint with the specified headers. It is using the `h.post` method from the `Harness` instance to send the request. The generic types `<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>` specify the request payload and response types respectively. */

Expand Down Expand Up @@ -119,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<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
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.",
},
});
});
117 changes: 75 additions & 42 deletions apps/api/src/routes/v1_keys_createKey.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,48 +241,48 @@ describe("permissions", () => {
});
});

describe("with encryption", () => {
test("encrypts a key", async (t) => {
const h = await IntegrationHarness.init(t);

await h.db.primary
.update(schema.keyAuth)
.set({
storeEncryptedKeys: true,
})
.where(eq(schema.keyAuth.id, h.resources.userKeyAuth.id));

const root = await h.createRootKey([
`api.${h.resources.userApi.id}.create_key`,
`api.${h.resources.userApi.id}.encrypt_key`,
]);

const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
url: "/v1/keys.createKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
apiId: h.resources.userApi.id,
recoverable: true,
},
});

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),
with: {
encrypted: true,
},
});
expect(key).toBeDefined();
expect(key!.encrypted).toBeDefined();
expect(typeof key?.encrypted?.encrypted).toBe("string");
expect(typeof key?.encrypted?.encryptionKeyId).toBe("string");
});
});
// describe("with encryption", () => {
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
// test("encrypts a key", async (t) => {
// const h = await IntegrationHarness.init(t);

// await h.db.primary
// .update(schema.keyAuth)
// .set({
// storeEncryptedKeys: true,
// })
// .where(eq(schema.keyAuth.id, h.resources.userKeyAuth.id));

// const root = await h.createRootKey([
// `api.${h.resources.userApi.id}.create_key`,
// `api.${h.resources.userApi.id}.encrypt_key`,
// ]);

// const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
// url: "/v1/keys.createKey",
// headers: {
// "Content-Type": "application/json",
// Authorization: `Bearer ${root.key}`,
// },
// body: {
// apiId: h.resources.userApi.id,
// recoverable: true,
// },
// });

// 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),
// with: {
// encrypted: true,
// },
// });
// expect(key).toBeDefined();
// expect(key!.encrypted).toBeDefined();
// expect(typeof key?.encrypted?.encrypted).toBe("string");
// expect(typeof key?.encrypted?.encryptionKeyId).toBe("string");
// });
// });

test("creates a key with environment", async (t) => {
const h = await IntegrationHarness.init(t);
Expand Down Expand Up @@ -467,4 +467,37 @@ describe("with externalId", () => {
expect(key!.identity!.id).toEqual(identity.id);
});
});
describe("Should default last day of month if none provided", () => {
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
test("should provide default value", async (t) => {
const h = await IntegrationHarness.init(t);
const date = new Date();
const lastDate = date.getMonth() + 1;
const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]);

const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
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(lastDate);
});
});
});
26 changes: 25 additions & 1 deletion apps/api/src/routes/v1_keys_createKey.ts
chronark marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,27 @@ When validating a key, we will return this back to you, so you can clearly ident
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 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",
interval: "monthly",
amount: 100,
refillDay: 15,
},
}),

ratelimit: z
.object({
async: z
Expand Down Expand Up @@ -309,6 +320,12 @@ export const registerV1KeysCreateKey = (app: App) =>
message: "remaining must be set if you are using refill.",
});
}
if (req.refill?.refillDay && req.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
*/
Expand All @@ -326,6 +343,12 @@ export const registerV1KeysCreateKey = (app: App) =>
: Promise.resolve(null),
]);
const newKey = await retry(5, async (attempt) => {
const date = new Date();
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
let lastDayOfMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
if (req.refill?.refillDay && req?.refill?.refillDay <= lastDayOfMonth) {
lastDayOfMonth = req.refill.refillDay;
}

if (attempt > 1) {
logger.warn("retrying key creation", {
attempt,
Expand Down Expand Up @@ -357,6 +380,7 @@ export const registerV1KeysCreateKey = (app: App) =>
ratelimitDuration: req.ratelimit?.duration ?? req.ratelimit?.refillInterval,
remaining: req.remaining,
refillInterval: req.refill?.interval,
refillDay: req.refill?.interval === "monthly" ? lastDayOfMonth : null,
refillAmount: req.refill?.amount,
lastRefillAt: req.refill?.interval ? new Date() : null,
deletedAt: null,
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v1_keys_getKey.ts
chronark marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export const registerV1KeysGetKey = (app: App) =>
? {
interval: key.refillInterval,
amount: key.refillAmount,
refillDay: key.refillInterval === "monthly" ? key.refillDay : null,
lastRefillAt: key.lastRefillAt?.getTime(),
}
: undefined,
Expand Down
31 changes: 31 additions & 0 deletions apps/api/src/routes/v1_keys_updateKey.error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,34 @@ test("when the key does not exist", async (t) => {
},
});
});
test("reject invalid refill config", async (t) => {
const h = await IntegrationHarness.init(t);
const keyId = newId("test");
const root = await h.createRootKey([`api.${h.resources.userApi.id}.update_key`]);
/* The code snippet is making a POST request to the "/v1/keys.createKey" endpoint with the specified headers. It is using the `h.post` method from the `Harness` instance to send the request. The generic types `<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>` specify the request payload and response types respectively. */

const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
chronark marked this conversation as resolved.
Show resolved Hide resolved
url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId,
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.",
},
});
});
48 changes: 48 additions & 0 deletions apps/api/src/routes/v1_keys_updateKey.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1013,3 +1013,51 @@ test("update ratelimit should not disable it", async (t) => {
expect(verify.body.ratelimit!.limit).toBe(5);
expect(verify.body.ratelimit!.remaining).toBe(4);
});
describe("Should default last day of month if none provided", () => {
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
test("should provide default value", async (t) => {
const h = await IntegrationHarness.init(t);

const key = {
id: newId("test"),
keyAuthId: h.resources.userKeyAuth.id,
workspaceId: h.resources.userWorkspace.id,
start: "test",
name: "test",
hash: await sha256(new KeyV1({ byteLength: 16 }).toString()),

createdAt: new Date(),
};
await h.db.primary.insert(schema.keys).values(key);
const root = await h.createRootKey([`api.${h.resources.userApi.id}.update_key`]);
const date = new Date();
const lastDate = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId: key.id,
remaining: 10,
refill: {
interval: "monthly",
amount: 130,
refillDay: undefined,
},
enabled: true,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.id),
});
expect(found).toBeDefined();
expect(found?.remaining).toEqual(10);
expect(found?.refillAmount).toEqual(1030);
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
expect(found?.refillInterval).toEqual("monthly");
expect(found?.refillDay).toEqual(lastDate);
});
});
Loading
Loading