diff --git a/.changeset/late-colts-shout.md b/.changeset/late-colts-shout.md new file mode 100644 index 0000000000..a8d9104b4e --- /dev/null +++ b/.changeset/late-colts-shout.md @@ -0,0 +1,5 @@ +--- +"api": minor +--- + +add /v1/keys.whoami route diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0792c3fe5b..c4c781371b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ -* @perkinsjr @chronark +* @perkinsjr @chronark @mcstepp @MichaelUnkey /apps/www @perkinsjr @chronark @MichaelUnkey @mcstepp /apps/dashboard @perkinsjr @chronark @mcstepp diff --git a/apps/api/src/pkg/hono/app.ts b/apps/api/src/pkg/hono/app.ts index 5d040694bd..060dab8a4e 100644 --- a/apps/api/src/pkg/hono/app.ts +++ b/apps/api/src/pkg/hono/app.ts @@ -29,7 +29,7 @@ export function newApp() { app.doc("/openapi.json", { openapi: "3.0.0", info: { - title: "Unkey Api", + title: "Unkey API", version: "1.0.0", }, diff --git a/apps/api/src/routes/v1_keys_ami.ts b/apps/api/src/routes/v1_keys_ami.ts new file mode 100644 index 0000000000..a32b4172d4 --- /dev/null +++ b/apps/api/src/routes/v1_keys_ami.ts @@ -0,0 +1,174 @@ +import type { App } from "@/pkg/hono/app"; +import { createRoute, z } from "@hono/zod-openapi"; + +import { rootKeyAuth } from "@/pkg/auth/root_key"; +import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; +import { sha256 } from "@unkey/hash"; +import { buildUnkeyQuery } from "@unkey/rbac"; + +const route = createRoute({ + tags: ["keys"], + operationId: "whoami", + method: "post", + path: "/v1/keys.whoami", + security: [{ bearerAuth: [] }], + request: { + body: { + required: true, + content: { + "application/json": { + schema: z.object({ + key: z.string().min(1).openapi({ + description: "The actual key to fetch", + example: "sk_123", + }), + }), + }, + }, + }, + }, + + responses: { + 200: { + description: "The configuration for a single key", + content: { + "application/json": { + schema: z.object({ + id: z.string().openapi({ + description: "The ID of the key", + example: "key_123", + }), + name: z.string().optional().openapi({ + description: "The name of the key", + example: "API Key 1", + }), + remaining: z.number().int().optional().openapi({ + description: "The remaining number of requests for the key", + example: 1000, + }), + identity: z + .object({ + id: z.string().openapi({ + description: "The identity ID associated with the key", + example: "id_123", + }), + externalId: z.string().openapi({ + description: "The external identity ID associated with the key", + example: "ext123", + }), + }) + .optional() + .openapi({ + description: "The identity object associated with the key", + }), + meta: z + .record(z.unknown()) + .optional() + .openapi({ + description: "Metadata associated with the key", + example: { role: "admin", plan: "premium" }, + }), + createdAt: z.number().int().openapi({ + description: "The timestamp in milliseconds when the key was created", + example: 1620000000000, + }), + enabled: z.boolean().openapi({ + description: "Whether the key is enabled", + example: true, + }), + environment: z.string().optional().openapi({ + description: "The environment the key is associated with", + example: "production", + }), + }), + }, + }, + }, + ...openApiErrorResponses, + }, +}); + +export type Route = typeof route; +export type V1KeysWhoAmIRequest = z.infer< + (typeof route.request.body.content)["application/json"]["schema"] +>; +export type V1KeysWhoAmIResponse = z.infer< + (typeof route.responses)[200]["content"]["application/json"]["schema"] +>; + +export const registerV1KeysWhoAmI = (app: App) => + app.openapi(route, async (c) => { + const { key: secret } = c.req.valid("json"); + const { cache, db } = c.get("services"); + const hash = await sha256(secret); + const { val: data, err } = await cache.keyByHash.swr(hash, async () => { + const dbRes = await db.readonly.query.keys.findFirst({ + where: (table, { eq, and, isNull }) => and(eq(table.hash, hash), isNull(table.deletedAt)), + with: { + keyAuth: { + with: { + api: true, + }, + }, + identity: true, + }, + }); + + if (!dbRes) { + return null; + } + + return { + key: { + ...dbRes, + }, + api: dbRes.keyAuth.api, + identity: dbRes.identity, + } as any; // this was necessary so that we don't need to return the workspace and other types defined in keyByHash + }); + + if (err) { + throw new UnkeyApiError({ + code: "INTERNAL_SERVER_ERROR", + message: `unable to load key: ${err.message}`, + }); + } + if (!data) { + throw new UnkeyApiError({ + code: "NOT_FOUND", + message: "Key not found", + }); + } + const { api, key } = data; + const auth = await rootKeyAuth( + c, + buildUnkeyQuery(({ or }) => or("*", "api.*.read_key", `api.${api.id}.read_key`)), + ); + + if (key.workspaceId !== auth.authorizedWorkspaceId) { + throw new UnkeyApiError({ + code: "NOT_FOUND", + message: "Key not found", + }); + } + let meta = key.meta ? JSON.parse(key.meta) : undefined; + if (!meta || Object.keys(meta).length === 0) { + meta = undefined; + } + + return c.json({ + id: key.id, + name: key.name ?? undefined, + remaining: key.remaining ?? undefined, + identity: data.identity + ? { + id: data.identity.id, + externalId: data.identity.externalId, + } + : undefined, + meta: meta, + createdAt: key.createdAt.getTime(), + enabled: key.enabled, + environment: key.environment ?? undefined, + }); + }); diff --git a/apps/api/src/routes/v1_keys_whoami.error.test.ts b/apps/api/src/routes/v1_keys_whoami.error.test.ts new file mode 100644 index 0000000000..b592e964ee --- /dev/null +++ b/apps/api/src/routes/v1_keys_whoami.error.test.ts @@ -0,0 +1,34 @@ +import { newId } from "@unkey/id"; +import { KeyV1 } from "@unkey/keys"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; +import { expect, test } from "vitest"; + +import type { V1KeysWhoAmIRequest, V1KeysWhoAmIResponse } from "./v1_keys_whoami"; + +test("when the key does not exist", async (t) => { + const h = await IntegrationHarness.init(t); + const apiId = newId("api"); + const key = new KeyV1({ prefix: "test", byteLength: 16 }).toString(); + + const root = await h.createRootKey([`api.${apiId}.read_key`]); + + const res = await h.post({ + url: "/v1/keys.whoami", + headers: { + Authorization: `Bearer ${root.key}`, + "Content-Type": "application/json", + }, + body: { + key: key, + }, + }); + + expect(res.status).toEqual(404); + expect(res.body).toMatchObject({ + error: { + code: "NOT_FOUND", + docs: "https://unkey.dev/docs/api-reference/errors/code/NOT_FOUND", + message: "Key not found", + }, + }); +}); diff --git a/apps/api/src/routes/v1_keys_whoami.happy.test.ts b/apps/api/src/routes/v1_keys_whoami.happy.test.ts new file mode 100644 index 0000000000..b38c1c1247 --- /dev/null +++ b/apps/api/src/routes/v1_keys_whoami.happy.test.ts @@ -0,0 +1,86 @@ +import { schema } from "@unkey/db"; +import { sha256 } from "@unkey/hash"; +import { newId } from "@unkey/id"; +import { KeyV1 } from "@unkey/keys"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; + +import { randomUUID } from "node:crypto"; +import { expect, test } from "vitest"; +import type { V1KeysWhoAmIRequest, V1KeysWhoAmIResponse } from "./v1_keys_whoami"; + +test("returns 200", async (t) => { + const h = await IntegrationHarness.init(t); + const root = await h.createRootKey(["api.*.read_key"]); + + const key = new KeyV1({ byteLength: 16 }).toString(); + const hash = await sha256(key); + const meta = JSON.stringify({ hello: "world" }); + + const keySchema = { + id: newId("test"), + keyAuthId: h.resources.userKeyAuth.id, + workspaceId: h.resources.userWorkspace.id, + start: "test", + name: "test", + remaining: 100, + enabled: true, + environment: "test", + hash: hash, + meta: meta, + createdAt: new Date(), + }; + await h.db.primary.insert(schema.keys).values(keySchema); + + const res = await h.post({ + url: "/v1/keys.whoami", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + key: key, + }, + }); + expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200); + + expect(res.body.id).toEqual(keySchema.id); + expect(res.body.name).toEqual(keySchema.name); + expect(res.body.remaining).toEqual(keySchema.remaining); + expect(res.body.name).toEqual(keySchema.name); + expect(res.body.meta).toEqual(JSON.parse(keySchema.meta)); + expect(res.body.enabled).toEqual(keySchema.enabled); + expect(res.body.environment).toEqual(keySchema.environment); +}); + +test("returns identity", async (t) => { + const h = await IntegrationHarness.init(t); + + const identity = { + id: newId("identity"), + externalId: randomUUID(), + workspaceId: h.resources.userWorkspace.id, + }; + await h.db.primary.insert(schema.identities).values(identity); + + const { key } = await h.createKey({ identityId: identity.id }); + const root = await h.createRootKey([ + `api.${h.resources.userApi.id}.read_api`, + `api.${h.resources.userApi.id}.read_key`, + ]); + + const res = await h.post({ + url: "/v1/keys.whoami", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + key: key, + }, + }); + + expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200); + expect(res.body.identity).toBeDefined(); + expect(res.body.identity!.id).toEqual(identity.id); + expect(res.body.identity!.externalId).toEqual(identity.externalId); +}); diff --git a/apps/api/src/routes/v1_keys_whoami.security.test.ts b/apps/api/src/routes/v1_keys_whoami.security.test.ts new file mode 100644 index 0000000000..7725c70a14 --- /dev/null +++ b/apps/api/src/routes/v1_keys_whoami.security.test.ts @@ -0,0 +1,76 @@ +import { randomUUID } from "node:crypto"; +import { runCommonRouteTests } from "@/pkg/testutil/common-tests"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; + +import { describe, expect, test } from "vitest"; +import type { V1KeysWhoAmIRequest, V1KeysWhoAmIResponse } from "./v1_keys_whoami"; + +runCommonRouteTests({ + prepareRequest: async (h) => { + const { key } = await h.createKey(); + return { + method: "POST", + url: "/v1/keys.whoami", + headers: { + "Content-Type": "application/json", + }, + body: { + key: key, + }, + }; + }, +}); + +describe("correct permissions", () => { + describe.each([ + { name: "legacy", roles: ["*"] }, + { name: "legacy and more", roles: ["*", randomUUID()] }, + { name: "wildcard api", roles: ["api.*.read_key", "api.*.read_api"] }, + { + name: "wildcard mixed", + roles: ["api.*.read_key", (apiId: string) => `api.${apiId}.read_api`], + }, + { + name: "wildcard mixed 2", + roles: ["api.*.read_api", (apiId: string) => `api.${apiId}.read_key`], + }, + { name: "wildcard and more", roles: ["api.*.read_key", "api.*.read_api", randomUUID()] }, + { + name: "specific apiId", + roles: [ + (apiId: string) => `api.${apiId}.read_key`, + (apiId: string) => `api.${apiId}.read_api`, + ], + }, + { + name: "specific apiId and more", + roles: [ + (apiId: string) => `api.${apiId}.read_key`, + (apiId: string) => `api.${apiId}.read_api`, + randomUUID(), + ], + }, + ])("$name", ({ roles }) => { + test("returns 200", async (t) => { + const h = await IntegrationHarness.init(t); + const { key } = await h.createKey(); + + const root = await h.createRootKey( + roles.map((role) => (typeof role === "string" ? role : role(h.resources.userApi.id))), + ); + + const res = await h.post({ + url: "/v1/keys.whoami", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${root.key}`, + }, + body: { + key: key, + }, + }); + + expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toEqual(200); + }); + }); +}); diff --git a/apps/api/src/routes/v1_keys_whoami.ts b/apps/api/src/routes/v1_keys_whoami.ts new file mode 100644 index 0000000000..a32b4172d4 --- /dev/null +++ b/apps/api/src/routes/v1_keys_whoami.ts @@ -0,0 +1,174 @@ +import type { App } from "@/pkg/hono/app"; +import { createRoute, z } from "@hono/zod-openapi"; + +import { rootKeyAuth } from "@/pkg/auth/root_key"; +import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; +import { sha256 } from "@unkey/hash"; +import { buildUnkeyQuery } from "@unkey/rbac"; + +const route = createRoute({ + tags: ["keys"], + operationId: "whoami", + method: "post", + path: "/v1/keys.whoami", + security: [{ bearerAuth: [] }], + request: { + body: { + required: true, + content: { + "application/json": { + schema: z.object({ + key: z.string().min(1).openapi({ + description: "The actual key to fetch", + example: "sk_123", + }), + }), + }, + }, + }, + }, + + responses: { + 200: { + description: "The configuration for a single key", + content: { + "application/json": { + schema: z.object({ + id: z.string().openapi({ + description: "The ID of the key", + example: "key_123", + }), + name: z.string().optional().openapi({ + description: "The name of the key", + example: "API Key 1", + }), + remaining: z.number().int().optional().openapi({ + description: "The remaining number of requests for the key", + example: 1000, + }), + identity: z + .object({ + id: z.string().openapi({ + description: "The identity ID associated with the key", + example: "id_123", + }), + externalId: z.string().openapi({ + description: "The external identity ID associated with the key", + example: "ext123", + }), + }) + .optional() + .openapi({ + description: "The identity object associated with the key", + }), + meta: z + .record(z.unknown()) + .optional() + .openapi({ + description: "Metadata associated with the key", + example: { role: "admin", plan: "premium" }, + }), + createdAt: z.number().int().openapi({ + description: "The timestamp in milliseconds when the key was created", + example: 1620000000000, + }), + enabled: z.boolean().openapi({ + description: "Whether the key is enabled", + example: true, + }), + environment: z.string().optional().openapi({ + description: "The environment the key is associated with", + example: "production", + }), + }), + }, + }, + }, + ...openApiErrorResponses, + }, +}); + +export type Route = typeof route; +export type V1KeysWhoAmIRequest = z.infer< + (typeof route.request.body.content)["application/json"]["schema"] +>; +export type V1KeysWhoAmIResponse = z.infer< + (typeof route.responses)[200]["content"]["application/json"]["schema"] +>; + +export const registerV1KeysWhoAmI = (app: App) => + app.openapi(route, async (c) => { + const { key: secret } = c.req.valid("json"); + const { cache, db } = c.get("services"); + const hash = await sha256(secret); + const { val: data, err } = await cache.keyByHash.swr(hash, async () => { + const dbRes = await db.readonly.query.keys.findFirst({ + where: (table, { eq, and, isNull }) => and(eq(table.hash, hash), isNull(table.deletedAt)), + with: { + keyAuth: { + with: { + api: true, + }, + }, + identity: true, + }, + }); + + if (!dbRes) { + return null; + } + + return { + key: { + ...dbRes, + }, + api: dbRes.keyAuth.api, + identity: dbRes.identity, + } as any; // this was necessary so that we don't need to return the workspace and other types defined in keyByHash + }); + + if (err) { + throw new UnkeyApiError({ + code: "INTERNAL_SERVER_ERROR", + message: `unable to load key: ${err.message}`, + }); + } + if (!data) { + throw new UnkeyApiError({ + code: "NOT_FOUND", + message: "Key not found", + }); + } + const { api, key } = data; + const auth = await rootKeyAuth( + c, + buildUnkeyQuery(({ or }) => or("*", "api.*.read_key", `api.${api.id}.read_key`)), + ); + + if (key.workspaceId !== auth.authorizedWorkspaceId) { + throw new UnkeyApiError({ + code: "NOT_FOUND", + message: "Key not found", + }); + } + let meta = key.meta ? JSON.parse(key.meta) : undefined; + if (!meta || Object.keys(meta).length === 0) { + meta = undefined; + } + + return c.json({ + id: key.id, + name: key.name ?? undefined, + remaining: key.remaining ?? undefined, + identity: data.identity + ? { + id: data.identity.id, + externalId: data.identity.externalId, + } + : undefined, + meta: meta, + createdAt: key.createdAt.getTime(), + enabled: key.enabled, + environment: key.environment ?? undefined, + }); + }); diff --git a/apps/api/src/worker.ts b/apps/api/src/worker.ts index 6a15a707f6..913cc4bfc5 100644 --- a/apps/api/src/worker.ts +++ b/apps/api/src/worker.ts @@ -13,6 +13,7 @@ import { registerV1KeysGetVerifications } from "./routes/v1_keys_getVerification import { registerV1KeysUpdate } from "./routes/v1_keys_updateKey"; import { registerV1KeysUpdateRemaining } from "./routes/v1_keys_updateRemaining"; import { registerV1KeysVerifyKey } from "./routes/v1_keys_verifyKey"; +import { registerV1KeysWhoAmI } from "./routes/v1_keys_whoami"; import { registerV1Liveness } from "./routes/v1_liveness"; import { registerV1RatelimitLimit } from "./routes/v1_ratelimit_limit"; @@ -67,6 +68,7 @@ registerV1Liveness(app); // keys registerV1KeysGetKey(app); +registerV1KeysWhoAmI(app); registerV1KeysDeleteKey(app); registerV1KeysCreateKey(app); registerV1KeysVerifyKey(app); diff --git a/apps/billing/package.json b/apps/billing/package.json index 46f615dd7f..7d3ad7babf 100644 --- a/apps/billing/package.json +++ b/apps/billing/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@types/node": "^20.14.9", - "@types/react": "^18.2.79", + "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "drizzle-kit": "^0.24.2", "typescript": "^5.5.3" diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-enabled.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-enabled.tsx index 24c1dcfe61..39a8f55211 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-enabled.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-enabled.tsx @@ -56,7 +56,7 @@ export const UpdateKeyEnabled: React.FC = ({ apiKey }) => { }); async function onSubmit(values: z.infer) { - updateEnabled.mutateAsync(values); + await updateEnabled.mutateAsync(values); } return ( diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-expiration.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-expiration.tsx index 6e4a51f63e..23bb38c8ff 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-expiration.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-expiration.tsx @@ -79,7 +79,7 @@ export const UpdateKeyExpiration: React.FC = ({ apiKey }) => { }); async function onSubmit(values: z.infer) { - changeExpiration.mutateAsync(values); + await changeExpiration.mutateAsync(values); } return ( diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-name.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-name.tsx index 7981ec5b86..188e5b72ad 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-name.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-name.tsx @@ -61,7 +61,7 @@ export const UpdateKeyName: React.FC = ({ apiKey }) => { }); async function onSubmit(values: z.infer) { - updateName.mutateAsync(values); + await updateName.mutateAsync(values); } return (
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-owner-id.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-owner-id.tsx index bc471b242f..fcf3a5211c 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-owner-id.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-owner-id.tsx @@ -62,7 +62,7 @@ export const UpdateKeyOwnerId: React.FC = ({ apiKey }) => { }); async function onSubmit(values: z.infer) { - updateOwnerId.mutateAsync(values); + await updateOwnerId.mutateAsync(values); } return ( diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-ratelimit.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-ratelimit.tsx index 720816f161..a74b2cad4e 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-ratelimit.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/update-key-ratelimit.tsx @@ -94,7 +94,7 @@ export const UpdateKeyRatelimit: React.FC = ({ apiKey }) => { }, }); async function onSubmit(values: z.infer) { - updateRatelimit.mutateAsync(values); + await updateRatelimit.mutateAsync(values); } return ( 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 5030b63087..0a9d978f65 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 @@ -117,7 +117,7 @@ export const UpdateKeyRemaining: React.FC = ({ apiKey }) => { if (values.refill?.interval === "none") { delete values.refill; } - updateRemaining.mutateAsync(values); + await updateRemaining.mutateAsync(values); } return ( diff --git a/apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx index 6faa740610..fc16fc94b8 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx @@ -63,7 +63,7 @@ export default async function SettingsPage(props: Props) { - Api ID + API ID This is your api id. It's used in some API calls. diff --git a/apps/dashboard/app/(app)/apis/[apiId]/settings/update-api-name.tsx b/apps/dashboard/app/(app)/apis/[apiId]/settings/update-api-name.tsx index e5680471dd..3f6132f766 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/settings/update-api-name.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/settings/update-api-name.tsx @@ -63,9 +63,9 @@ export const UpdateApiName: React.FC = ({ api }) => { - Api Name + API Name - Api names are not customer facing. Choose a name that makes it easy to recognize for + API names are not customer facing. Choose a name that makes it easy to recognize for you. diff --git a/apps/dashboard/app/(app)/apis/[apiId]/settings/update-ip-whitelist.tsx b/apps/dashboard/app/(app)/apis/[apiId]/settings/update-ip-whitelist.tsx index 4f1ca6437e..06fe1b3f71 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/settings/update-ip-whitelist.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/settings/update-ip-whitelist.tsx @@ -65,7 +65,7 @@ export const UpdateIpWhitelist: React.FC = ({ api, workspace }) => { }); async function onSubmit(values: z.infer) { - updateIps.mutateAsync(values); + await updateIps.mutateAsync(values); } return ( diff --git a/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/delete-permission.tsx b/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/delete-permission.tsx index 691c130acd..1b1de8c149 100644 --- a/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/delete-permission.tsx +++ b/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/delete-permission.tsx @@ -67,7 +67,7 @@ export const DeletePermission: React.FC = ({ trigger, permission }) => { return ( setOpen(o)}> - {trigger} + {trigger} Delete Permission diff --git a/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/page.tsx b/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/page.tsx index 10de75c985..8c9a2eb8ae 100644 --- a/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/page.tsx +++ b/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/page.tsx @@ -84,10 +84,12 @@ export default async function RolesPage(props: Props) { className="w-40 font-mono font-medium ph-no-capture" > - - {permission.name} -
- + +
+ {permission.name} +
+ +
{shouldShowTooltip && ( diff --git a/apps/dashboard/app/(app)/authorization/permissions/create-new-permission.tsx b/apps/dashboard/app/(app)/authorization/permissions/create-new-permission.tsx index 95865b7f51..6a66b45eef 100644 --- a/apps/dashboard/app/(app)/authorization/permissions/create-new-permission.tsx +++ b/apps/dashboard/app/(app)/authorization/permissions/create-new-permission.tsx @@ -75,7 +75,7 @@ export const CreateNewPermission: React.FC = ({ trigger }) => { return ( - {trigger} + {trigger} Create a new permission diff --git a/apps/dashboard/app/(app)/authorization/roles/[roleId]/delete-role.tsx b/apps/dashboard/app/(app)/authorization/roles/[roleId]/delete-role.tsx index a82b265810..a61741a3c4 100644 --- a/apps/dashboard/app/(app)/authorization/roles/[roleId]/delete-role.tsx +++ b/apps/dashboard/app/(app)/authorization/roles/[roleId]/delete-role.tsx @@ -68,7 +68,7 @@ export const DeleteRole: React.FC = ({ trigger, role }) => { return ( setOpen(o)}> - {trigger} + {trigger} Delete Role diff --git a/apps/dashboard/app/(app)/authorization/roles/[roleId]/update-role.tsx b/apps/dashboard/app/(app)/authorization/roles/[roleId]/update-role.tsx index 2ea3a2a6bf..d7c3363023 100644 --- a/apps/dashboard/app/(app)/authorization/roles/[roleId]/update-role.tsx +++ b/apps/dashboard/app/(app)/authorization/roles/[roleId]/update-role.tsx @@ -74,7 +74,7 @@ export const UpdateRole: React.FC = ({ trigger, role }) => { return ( - {trigger} + {trigger} Update Role diff --git a/apps/dashboard/app/(app)/authorization/roles/create-new-role.tsx b/apps/dashboard/app/(app)/authorization/roles/create-new-role.tsx index c9840ee2db..b65f6e8e80 100644 --- a/apps/dashboard/app/(app)/authorization/roles/create-new-role.tsx +++ b/apps/dashboard/app/(app)/authorization/roles/create-new-role.tsx @@ -95,7 +95,7 @@ export const CreateNewRole: React.FC = ({ trigger, permissions }) => { return ( - {trigger} + {trigger} Create a new role diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx index 10920125ef..b614b9e65f 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx @@ -228,11 +228,11 @@ export default async function RatelimitNamespacePage(props: { No usage Ratelimit something or change the range - - {snippet} - - + + {snippet} + + )} @@ -272,11 +272,11 @@ export default async function RatelimitNamespacePage(props: { No usage Ratelimit something or change the range - - {snippet} - - + + {snippet} + + )}
diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/settings/update-namespace-name.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/settings/update-namespace-name.tsx index abbdddc3b1..b77f2a6af0 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/settings/update-namespace-name.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/settings/update-namespace-name.tsx @@ -52,7 +52,7 @@ export const UpdateNamespaceName: React.FC = ({ namespace }) => { if (values.name === namespace.name || !values.name) { return toast.error("Please provide a valid name before saving."); } - updateName.mutateAsync(values); + await updateName.mutateAsync(values); } return ( diff --git a/apps/dashboard/app/(app)/ratelimits/sparkline.tsx b/apps/dashboard/app/(app)/ratelimits/sparkline.tsx index 6d9b156c4b..16cd8ed067 100644 --- a/apps/dashboard/app/(app)/ratelimits/sparkline.tsx +++ b/apps/dashboard/app/(app)/ratelimits/sparkline.tsx @@ -12,7 +12,7 @@ type Props = { }[]; }; -export const Sparkline: React.FC = async ({ data }) => { +export const Sparkline: React.FC = ({ data }) => { const data2 = data.map((d) => ({ date: new Date(d.time), values: d.values, diff --git a/apps/dashboard/app/(app)/semantic-cache/[gatewayId]/analytics/page.tsx b/apps/dashboard/app/(app)/semantic-cache/[gatewayId]/analytics/page.tsx index 4c3983a10b..c078d5c31a 100644 --- a/apps/dashboard/app/(app)/semantic-cache/[gatewayId]/analytics/page.tsx +++ b/apps/dashboard/app/(app)/semantic-cache/[gatewayId]/analytics/page.tsx @@ -6,11 +6,7 @@ import { Code } from "@/components/ui/code"; import { Separator } from "@/components/ui/separator"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; -import { - getAllSemanticCacheLogs, - getSemanticCachesDaily, - getSemanticCachesHourly, -} from "@/lib/tinybird"; +import { getSemanticCachesDaily, getSemanticCachesHourly } from "@/lib/tinybird"; import { BarChart } from "lucide-react"; import ms from "ms"; import { redirect } from "next/navigation"; @@ -220,11 +216,11 @@ export default async function SemanticCacheAnalyticsPage(props: { No usage Use the snippet below to start using the semantic cache. - - {snippet} - - + + {snippet} + + )} diff --git a/apps/dashboard/app/(app)/settings/billing/plans/button.tsx b/apps/dashboard/app/(app)/settings/billing/plans/button.tsx index 8b10427ec1..e173ed1562 100644 --- a/apps/dashboard/app/(app)/settings/billing/plans/button.tsx +++ b/apps/dashboard/app/(app)/settings/billing/plans/button.tsx @@ -60,7 +60,7 @@ export const ChangePlanButton: React.FC = ({ workspace, newPlan, label }) const isSamePlan = workspace.plan === newPlan; return ( - +