Skip to content

Commit

Permalink
feat: Adding refill to remaining keys (#682)
Browse files Browse the repository at this point in the history
* updated schema for db, updated update key

* Changed to Enum for refillIncrement

* Converted to Enum values

* Working Web App and API

* Started on replenish docs

* update ignore

* pre commit to merge pull main

* Merge origin main

* Fixed type errors in keys api

* Prep for Trigger

* Finished Replenisment Cron Imp

* feat(key replenish): added key replenishment daily and monthly

Key replenishment added with changes to db schema, docs, api routes, and cron added

key-299, key-300, key-301, key-302, key-303, key-304

* changed refill names and fixed wording in openAPI

* Fixed refill on web app

* wip

* work in progress PR changes

* Wording and trpc integration with replenish

* chore: merge main

* fix: issues

* fix: issues

* chore: remove log

* Requested changes made

* format

---------

Co-authored-by: chronark <[email protected]>
  • Loading branch information
MichaelUnkey and chronark authored Dec 22, 2023
1 parent 96086c1 commit b3de29d
Show file tree
Hide file tree
Showing 27 changed files with 745 additions and 109 deletions.
4 changes: 0 additions & 4 deletions .github/workflows/build_apps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ jobs:
env:
DRIZZLE_DATABASE_URL: 'mysql://unkey:password@localhost:3306/unkey'


- name: Install
uses: ./.github/actions/install

- name: Build
run: pnpm turbo run build --filter=${{matrix.app}}
env:
Expand Down
5 changes: 5 additions & 0 deletions .infisical.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"workspaceId": "6532bc32a4732d50aa605d13",
"defaultEnvironment": "",
"gitBranchToEnvironmentMapping": null
}
2 changes: 1 addition & 1 deletion apps/api/.infisical.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"workspaceId": "6532bc32a4732d50aa605d13",
"defaultEnvironment": "",
"defaultEnvironment": "dev",
"gitBranchToEnvironmentMapping": null
}
25 changes: 20 additions & 5 deletions apps/api/src/routes/legacy_apis_listKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,29 @@ export const registerLegacyApisListKeys = (app: App) =>
app.openapi(route, async (c) => {
const authorization = c.req.header("authorization")?.replace("Bearer ", "");
if (!authorization) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "key required",
});
}
const rootKey = await keyService.verifyKey(c, { key: authorization });
if (rootKey.error) {
throw new UnkeyApiError({ code: "INTERNAL_SERVER_ERROR", message: rootKey.error.message });
throw new UnkeyApiError({
code: "INTERNAL_SERVER_ERROR",
message: rootKey.error.message,
});
}
if (!rootKey.value.valid) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "the root key is not valid" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "the root key is not valid",
});
}
if (!rootKey.value.isRootKey) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "root key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "root key required",
});
}

const apiId = c.req.param("apiId");
Expand All @@ -89,7 +101,10 @@ export const registerLegacyApisListKeys = (app: App) =>
});

if (!api || api.workspaceId !== rootKey.value.authorizedWorkspaceId) {
throw new UnkeyApiError({ code: "NOT_FOUND", message: `api ${apiId} not found` });
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `api ${apiId} not found`,
});
}

if (!api.keyAuthId) {
Expand Down
30 changes: 24 additions & 6 deletions apps/api/src/routes/legacy_keys_createKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,29 @@ export const registerLegacyKeysCreate = (app: App) =>
app.openapi(route, async (c) => {
const authorization = c.req.header("authorization")?.replace("Bearer ", "");
if (!authorization) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "key required",
});
}
const rootKey = await keyService.verifyKey(c, { key: authorization });
if (rootKey.error) {
throw new UnkeyApiError({ code: "INTERNAL_SERVER_ERROR", message: rootKey.error.message });
throw new UnkeyApiError({
code: "INTERNAL_SERVER_ERROR",
message: rootKey.error.message,
});
}
if (!rootKey.value.valid) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "the root key is not valid" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "the root key is not valid",
});
}
if (!rootKey.value.isRootKey) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "root key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "root key required",
});
}

const req = c.req.valid("json");
Expand All @@ -187,7 +199,10 @@ export const registerLegacyKeysCreate = (app: App) =>
});

if (!api || api.workspaceId !== rootKey.value.authorizedWorkspaceId) {
throw new UnkeyApiError({ code: "NOT_FOUND", message: `api ${req.apiId} not found` });
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `api ${req.apiId} not found`,
});
}

if (!api.keyAuthId) {
Expand All @@ -200,7 +215,10 @@ export const registerLegacyKeysCreate = (app: App) =>
/**
* Set up an api for production
*/
const key = new KeyV1({ byteLength: req.byteLength, prefix: req.prefix }).toString();
const key = new KeyV1({
byteLength: req.byteLength,
prefix: req.prefix,
}).toString();
const start = key.slice(0, (req.prefix?.length ?? 0) + 5);
const keyId = newId("key");
const hash = await sha256(key.toString());
Expand Down
34 changes: 26 additions & 8 deletions apps/api/src/routes/legacy_keys_getKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,38 @@ export const registerLegacyKeysGet = (app: App) =>
app.openapi(route, async (c) => {
const authorization = c.req.header("authorization")?.replace("Bearer ", "");
if (!authorization) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "key required",
});
}
const rootKey = await keyService.verifyKey(c, { key: authorization });
if (rootKey.error) {
throw new UnkeyApiError({ code: "INTERNAL_SERVER_ERROR", message: rootKey.error.message });
throw new UnkeyApiError({
code: "INTERNAL_SERVER_ERROR",
message: rootKey.error.message,
});
}
if (!rootKey.value.valid) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "the root key is not valid" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "the root key is not valid",
});
}
if (!rootKey.value.isRootKey) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "root key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "root key required",
});
}

const { keyId } = c.req.param();

if (!keyId) {
throw new UnkeyApiError({ code: "BAD_REQUEST", message: "no key id given" });
throw new UnkeyApiError({
code: "BAD_REQUEST",
message: "no key id given",
});
}

const data = await cache.withCache(c, "keyById", keyId, async () => {
Expand All @@ -80,7 +95,10 @@ export const registerLegacyKeysGet = (app: App) =>
});

if (!data || data.key.workspaceId !== rootKey.value.authorizedWorkspaceId) {
throw new UnkeyApiError({ code: "NOT_FOUND", message: `key ${keyId} not found` });
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `key ${keyId} not found`,
});
}

return c.json({
Expand All @@ -90,8 +108,8 @@ export const registerLegacyKeysGet = (app: App) =>
name: data.key.name ?? undefined,
start: data.key.start,
ownerId: data.key.ownerId ?? undefined,
meta: data.key.meta ?? undefined,
createdAt: data.key.createdAt.getTime() ?? undefined,
meta: data.key.meta ? JSON.parse(data.key.meta) : undefined,
createdAt: data.key.createdAt.getTime(),
forWorkspaceId: data.key.forWorkspaceId ?? undefined,
expiresAt: data.key.expires?.getTime() ?? undefined,
remaining: data.key.remaining ?? undefined,
Expand Down
25 changes: 20 additions & 5 deletions apps/api/src/routes/legacy_keys_updateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,31 @@ export const registerLegacyKeysUpdate = (app: App) =>
app.openapi(route, async (c) => {
const authorization = c.req.header("authorization")?.replace("Bearer ", "");
if (!authorization) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "key required",
});
}

// Get root key and check for API errors
const rootKey = await keyService.verifyKey(c, { key: authorization });
if (rootKey.error) {
throw new UnkeyApiError({ code: "INTERNAL_SERVER_ERROR", message: rootKey.error.message });
throw new UnkeyApiError({
code: "INTERNAL_SERVER_ERROR",
message: rootKey.error.message,
});
}
if (!rootKey.value.valid) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "the root key is not valid" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "the root key is not valid",
});
}
if (!rootKey.value.isRootKey) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "root key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "root key required",
});
}
const keyId = c.req.param("keyId");
const req = c.req.valid("json");
Expand All @@ -143,7 +155,10 @@ export const registerLegacyKeysUpdate = (app: App) =>
});

if (!key || key.workspaceId !== rootKey.value.authorizedWorkspaceId) {
throw new UnkeyApiError({ code: "NOT_FOUND", message: `key ${keyId} not found` });
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `key ${keyId} not found`,
});
}

const authorizedWorkspaceId = rootKey.value.authorizedWorkspaceId;
Expand Down
13 changes: 8 additions & 5 deletions apps/api/src/routes/legacy_keys_verifyKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ A key could be invalid for a number of reasons, for example if it has expired, h
stripeCustomerId: "cus_1234",
},
}),
createdAt: z.number().openapi({
createdAt: z.number().optional().openapi({
description: "The unix timestamp in milliseconds when the key was created",
example: Date.now(),
}),
Expand Down Expand Up @@ -110,15 +110,15 @@ A key could be invalid for a number of reasons, for example if it has expired, h
example: 1000,
}),
code: z
.enum(["NOT_FOUND", "FORBIDDEN", "KEY_USAGE_EXCEEDED", "RATELIMITED"])
.enum(["NOT_FOUND", "FORBIDDEN", "USAGE_EXCEEDED", "RATE_LIMITED"])
.optional()
.openapi({
description: `If the key is invalid this field will be set to the reason why it is invalid.
Possible values are:
- NOT_FOUND: the key does not exist or has expired
- FORBIDDEN: the key is not allowed to access the api
- KEY_USAGE_EXCEEDED: the key has exceeded its request limit
- RATELIMITED: the key has been ratelimited,
- USAGE_EXCEEDED: the key has exceeded its request limit
- RATE_LIMITED: the key has been ratelimited,
`,
example: "NOT_FOUND",
}),
Expand All @@ -143,7 +143,10 @@ export const registerLegacyKeysVerifyKey = (app: App) =>

const { value, error } = await keyService.verifyKey(c, { key, apiId });
if (error) {
throw new UnkeyApiError({ code: "INTERNAL_SERVER_ERROR", message: error.message });
throw new UnkeyApiError({
code: "INTERNAL_SERVER_ERROR",
message: error.message,
});
}

if (!value.valid) {
Expand Down
26 changes: 25 additions & 1 deletion apps/api/src/routes/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const keySchema = z
stripeCustomerId: "cus_1234",
},
}),
createdAt: z.number().openapi({
createdAt: z.number().optional().openapi({
description: "The unix timestamp in milliseconds when the key was created",
example: Date.now(),
}),
Expand All @@ -56,6 +56,30 @@ export const keySchema = z
"The number of requests that can be made with this key before it becomes invalid. If this field is null or undefined, the key has no request limit.",
example: 1000,
}),
refill: z
.object({
interval: z.enum(["daily", "monthly"]).openapi({
description: "Determines the rate at which verifications will be refilled.",
example: "daily",
}),
amount: z.number().int().openapi({
description: "Resets `remaining` to this value every interval.",
example: 100,
}),
lastRefillAt: z.number().optional().openapi({
description: "The unix timestamp in miliseconds when the key was last refilled.",
example: 100,
}),
})
.optional()
.openapi({
description:
"Unkey allows you to refill remaining verifications on a key on a regular interval.",
example: {
interval: "daily",
amount: 10,
},
}),
ratelimit: z
.object({
type: z
Expand Down
33 changes: 28 additions & 5 deletions apps/api/src/routes/v1_apis_listKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,29 @@ export const registerV1ApisListKeys = (app: App) =>
app.openapi(route, async (c) => {
const authorization = c.req.header("authorization")?.replace("Bearer ", "");
if (!authorization) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "key required",
});
}
const rootKey = await keyService.verifyKey(c, { key: authorization });
if (rootKey.error) {
throw new UnkeyApiError({ code: "INTERNAL_SERVER_ERROR", message: rootKey.error.message });
throw new UnkeyApiError({
code: "INTERNAL_SERVER_ERROR",
message: rootKey.error.message,
});
}
if (!rootKey.value.valid) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "the root key is not valid" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "the root key is not valid",
});
}
if (!rootKey.value.isRootKey) {
throw new UnkeyApiError({ code: "UNAUTHORIZED", message: "root key required" });
throw new UnkeyApiError({
code: "UNAUTHORIZED",
message: "root key required",
});
}

const { apiId, limit, cursor, ownerId } = c.req.query();
Expand All @@ -85,7 +97,10 @@ export const registerV1ApisListKeys = (app: App) =>
});

if (!api || api.workspaceId !== rootKey.value.authorizedWorkspaceId) {
throw new UnkeyApiError({ code: "NOT_FOUND", message: `api ${apiId} not found` });
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `api ${apiId} not found`,
});
}

if (!api.keyAuthId) {
Expand Down Expand Up @@ -134,6 +149,14 @@ export const registerV1ApisListKeys = (app: App) =>
}
: undefined,
remaining: k.remaining ?? undefined,
refill:
k.refillInterval && k.refillAmount && k.lastRefillAt
? {
interval: k.refillInterval,
amount: k.refillAmount,
lastRefillAt: k.lastRefillAt?.getTime(),
}
: undefined,
})),
// @ts-ignore, mysql sucks
total: parseInt(total.at(0)?.count ?? "0"),
Expand Down
Loading

1 comment on commit b3de29d

@vercel
Copy link

@vercel vercel bot commented on b3de29d Dec 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

web – ./apps/web

unkey-web-lac.vercel.app
unkey.dev
web-git-main-unkey.vercel.app
www.unkey.dev
web-unkey.vercel.app

Please sign in to comment.