Skip to content

Commit

Permalink
PR changes
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelUnkey committed Nov 14, 2024
1 parent 27abeca commit cb2ca1e
Show file tree
Hide file tree
Showing 15 changed files with 250 additions and 133 deletions.
2 changes: 1 addition & 1 deletion apps/api/src/pkg/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ export class Analytics {
public get getVerificationsDaily() {
return this.clickhouse.verifications.perDay;
}
}
}
5 changes: 2 additions & 3 deletions apps/api/src/routes/v1_ratelimit_deleteOverride.error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test("Missing Namespace", async (t) => {
id: namespaceId,
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
name: "namespace",
name: newId("test"),
};
await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace);

Expand All @@ -34,15 +34,14 @@ test("Missing Namespace", async (t) => {
async: false,
});

const root = await h.createRootKey(["*", "ratelimit.*.delete_override"]);
const root = await h.createRootKey(["ratelimit.*.delete_override"]);
const res = await h.post<V1RatelimitDeleteOverrideRequest, V1RatelimitDeleteOverrideResponse>({
url: "/v1/ratelimit.deleteOverride",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
namespaceId: undefined,
identifier,
},
});
Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/routes/v1_ratelimit_deleteOverride.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect, test } from "vitest";
import { randomUUID } from "node:crypto";
import { IntegrationHarness } from "src/pkg/testutil/integration-harness";

import { schema } from "@unkey/db";
import { isNull, schema } from "@unkey/db";
import { newId } from "@unkey/id";
import type {
V1RatelimitDeleteOverrideRequest,
Expand All @@ -20,7 +20,7 @@ test("deletes override", async (t) => {
id: namespaceId,
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
name: "namespace",
name: newId("test"),
};
await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace);

Expand All @@ -34,7 +34,7 @@ test("deletes override", async (t) => {
async: false,
});

const root = await h.createRootKey(["*", "ratelimit.*.delete_override"]);
const root = await h.createRootKey(["ratelimit.*.delete_override"]);
const res = await h.post<V1RatelimitDeleteOverrideRequest, V1RatelimitDeleteOverrideResponse>({
url: "/v1/ratelimit.deleteOverride",
headers: {
Expand All @@ -50,7 +50,7 @@ test("deletes override", async (t) => {
expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.ratelimitOverrides.findFirst({
where: (table, { eq }) => eq(table.id, overrideId),
where: (table, { eq, and }) => and(eq(table.id, overrideId), isNull(table.deletedAt)),
});
expect(found).toBeUndefined();
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ runCommonRouteTests<V1RatelimitDeleteOverrideRequest>({
id: namespaceId,
workspaceId: rh.resources.userWorkspace.id,
createdAt: new Date(),
name: "namespace",
name: newId("test"),
};
await rh.db.primary.insert(schema.ratelimitNamespaces).values(namespace);
await rh.db.primary.insert(schema.ratelimitOverrides).values({
Expand Down Expand Up @@ -57,7 +57,7 @@ describe("correct roles", () => {
id: namespaceId,
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
name: "namespace",
name: newId("test"),
};
await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace);
await h.db.primary.insert(schema.ratelimitOverrides).values({
Expand Down Expand Up @@ -92,7 +92,8 @@ describe("correct roles", () => {
).toEqual(200);

const found = await h.db.primary.query.ratelimitOverrides.findFirst({
where: (table, { eq }) => eq(table.id, overrideId),
where: (table, { eq, and, isNull }) =>
and(isNull(table.deletedAt), eq(table.id, overrideId)),
});
expect(found).toBeUndefined();
});
Expand All @@ -113,7 +114,7 @@ describe("incorrect roles", () => {
id: namespaceId,
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
name: "namespace",
name: newId("test"),
};
await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace);
await h.db.primary.insert(schema.ratelimitOverrides).values({
Expand Down
57 changes: 28 additions & 29 deletions apps/api/src/routes/v1_ratelimit_deleteOverride.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const route = createRoute({
"application/json": {
schema: z.object({
namespaceId: z.string().optional().openapi({
description: "The id of the namespace.",
description:
"The id of the namespace. Either namespaceId or namespaceName must be provided",
example: "rlns_1234",
}),
namespaceName: z.string().optional().openapi({
Expand All @@ -29,7 +30,7 @@ const route = createRoute({
}),
identifier: z.string().openapi({
description:
"Identifier of your user, this can be their userId, an email, an ip or anything else.",
"Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( * ) can be used to match multiple identifiers, More info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules",
example: "user_123",
}),
}),
Expand Down Expand Up @@ -71,45 +72,43 @@ export const registerV1RatelimitDeleteOverride = (app: App) =>
message: "You must provide a namespaceId or a namespaceName",
});
}
// Todo Add an option for namespaceName to be used instead of namespaceId
const { db } = c.get("services");

const authorizedWorkspaceId = auth.authorizedWorkspaceId;
const namespace = await db.primary.query.ratelimitNamespaces.findFirst({
where: (table, { eq, and }) =>
and(
eq(table.workspaceId, authorizedWorkspaceId),
namespaceId ? eq(table.id, namespaceId) : eq(table.name, namespaceName!),
),
with: {
overrides: {
where: and(eq(schema.ratelimitOverrides.identifier, identifier)),
},
},
});
if (!namespace) {
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `Namespace ${namespaceId ? namespaceId : namespaceName} not found`,
});
}

await db.primary.transaction(async (tx) => {
const override = await tx.query.ratelimitOverrides.findFirst({
where: and(
eq(schema.ratelimitOverrides.workspaceId, auth.authorizedWorkspaceId),
eq(schema.ratelimitOverrides.namespaceId, namespace.id),
eq(schema.ratelimitOverrides.identifier, identifier),
),
const namespace = await db.primary.query.ratelimitNamespaces.findFirst({
where: (table, { eq, and }) =>
and(
eq(table.workspaceId, authorizedWorkspaceId),
namespaceId ? eq(table.id, namespaceId) : eq(table.name, namespaceName!),
),
with: {
overrides: {
where: (table, { eq, and, isNull }) =>
and(isNull(table.deletedAt), eq(table.identifier, identifier)),
},
},
});

if (!namespace) {
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `Namespace ${namespaceId ? namespaceId : namespaceName} not found`,
});
}
const override = namespace.overrides[0];

if (!override) {
throw new UnkeyApiError({
code: "NOT_FOUND",
message: `Override ${identifier} in namespace ${namespaceId} not found`,
message: `Override with ${identifier} identifier not found`,
});
}
await tx.delete(schema.ratelimitOverrides);
await tx
.update(schema.ratelimitOverrides)
.set({ deletedAt: new Date() })
.where(and(eq(schema.ratelimitOverrides.id, override.id)));

await insertUnkeyAuditLog(c, tx, {
workspaceId: auth.authorizedWorkspaceId,
Expand Down
45 changes: 43 additions & 2 deletions apps/api/src/routes/v1_ratelimit_getOverride.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { IntegrationHarness } from "src/pkg/testutil/integration-harness";
import { expect, test } from "vitest";
import type { V1RatelimitGetOverrideResponse } from "./v1_ratelimit_getOverride";

test("return a single override", async (t) => {
test("return a single override using namespaceId", async (t) => {
const h = await IntegrationHarness.init(t);
const root = await h.createRootKey(["ratelimit.*.read_override"]);
const namespaceId = newId("test");
const namespaceName = "Test.Name";
const namespaceName = randomUUID();
const overrideId = newId("test");
const identifier = randomUUID();

Expand Down Expand Up @@ -46,3 +46,44 @@ test("return a single override", async (t) => {
expect(res.body.duration).toEqual(60_000);
expect(res.body.async).toEqual(false);
});

test("return a single override using namespaceName", async (t) => {
const h = await IntegrationHarness.init(t);
const root = await h.createRootKey(["ratelimit.*.read_override"]);
const namespaceId = newId("test");
const namespaceName = randomUUID();
const overrideId = newId("test");
const identifier = randomUUID();

// Namespace
const namespace = {
id: namespaceId,
name: namespaceName,
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
};
await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace);
await h.db.primary.insert(schema.ratelimitOverrides).values({
id: overrideId,
workspaceId: h.resources.userWorkspace.id,
namespaceId: namespaceId,
identifier: identifier,
limit: 1,
duration: 60_000,
async: false,
});

const res = await h.get<V1RatelimitGetOverrideResponse>({
url: `/v1/ratelimit.getOverride?namespaceName=${namespaceName}&identifier=${identifier}`,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
});
expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);
expect(res.body.id).toBe(overrideId);
expect(res.body.identifier).toEqual(identifier);
expect(res.body.limit).toEqual(1);
expect(res.body.duration).toEqual(60_000);
expect(res.body.async).toEqual(false);
});
6 changes: 3 additions & 3 deletions apps/api/src/routes/v1_ratelimit_getOverride.security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ runCommonRouteTests<V1RatelimitGetOverrideRequest>({
id: namespaceId,
workspaceId: rh.resources.userWorkspace.id,
createdAt: new Date(),
name: "namespace",
name: newId("test"),
};
await rh.db.primary.insert(schema.ratelimitNamespaces).values(namespace);
await rh.db.primary.insert(schema.ratelimitOverrides).values({
Expand Down Expand Up @@ -53,7 +53,7 @@ describe("correct roles", () => {
id: namespaceId,
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
name: "namespace",
name: randomUUID(),
};
await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace);
await h.db.primary.insert(schema.ratelimitOverrides).values({
Expand Down Expand Up @@ -95,7 +95,7 @@ describe("incorrect roles", () => {
id: namespaceId,
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
name: "namespace",
name: randomUUID(),
};
await h.db.primary.insert(schema.ratelimitNamespaces).values(namespace);
await h.db.primary.insert(schema.ratelimitOverrides).values({
Expand Down
47 changes: 18 additions & 29 deletions apps/api/src/routes/v1_ratelimit_getOverride.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { rootKeyAuth } from "@/pkg/auth/root_key";
import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors";
import type { App } from "@/pkg/hono/app";
import { createRoute, z } from "@hono/zod-openapi";
import { and, eq, schema } from "@unkey/db";

import { buildUnkeyQuery } from "@unkey/rbac";

const route = createRoute({
Expand All @@ -14,12 +14,13 @@ const route = createRoute({
request: {
query: z.object({
namespaceId: z.string().optional().openapi({
description: "The id of the namespace.",
description:
"The id of the namespace. Either namespaceId or namespaceName must be provided",
example: "rlns_1234",
}),
namespaceName: z.string().optional().openapi({
description:
"The name of the namespace. Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes.",
"Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules",
example: "email.outbound",
}),
identifier: z.string().openapi({
Expand Down Expand Up @@ -49,6 +50,7 @@ const route = createRoute({
});

export type Route = typeof route;

export type V1RatelimitGetOverrideResponse = z.infer<
(typeof route.responses)[200]["content"]["application/json"]["schema"]
>;
Expand All @@ -60,7 +62,7 @@ export const registerV1RatelimitGetOverride = (app: App) =>

const auth = await rootKeyAuth(
c,
buildUnkeyQuery(({ or }) => or("*", "ratelimit.*.read_override")),
buildUnkeyQuery(({ or }) => or("ratelimit.*.read_override")),
);

const authorizedWorkspaceId = auth.authorizedWorkspaceId;
Expand All @@ -76,43 +78,30 @@ export const registerV1RatelimitGetOverride = (app: App) =>
message: "You must provide a namespaceId or a namespaceName",
});
}

const namespace = await db.readonly.query.ratelimitNamespaces.findFirst({
where: (table, { and, eq }) =>
const result = await db.primary.query.ratelimitNamespaces.findFirst({
where: (table, { eq, and }) =>
and(
eq(table.workspaceId, authorizedWorkspaceId),
namespaceId ? eq(table.id, namespaceId) : eq(table.name, namespaceName!),
),
with: {
overrides: {
where: and(eq(schema.ratelimitOverrides.identifier, identifier)),
where: (table, { eq, and, isNull }) =>
and(isNull(table.deletedAt), eq(table.identifier, identifier)),
},
},
});

if (!namespace) {
throw new Error(`Namespace ${namespaceId ? namespaceId : namespaceName} not found`);
}
const override = await db.primary.query.ratelimitOverrides.findFirst({
where: (table, { eq, and }) =>
and(
eq(table.workspaceId, authorizedWorkspaceId),
eq(table.namespaceId, namespace.id),
eq(table.identifier, identifier),
),
});
if (!override) {
throw new UnkeyApiError({
code: "NOT_FOUND",
message: "Override not found",
});
if (!result) {
throw new UnkeyApiError({ code: "NOT_FOUND", message: "Namespace not found" });
}

const override = result?.overrides[0];
return c.json({
id: override.id,
identifier: override.identifier,
limit: override.limit,
duration: override.duration,
async: override.async ?? undefined,
id: override?.id,
identifier: override?.identifier,
limit: override?.limit,
duration: override?.duration,
async: override?.async,
});
});
Loading

0 comments on commit cb2ca1e

Please sign in to comment.