Skip to content

Commit

Permalink
Merge branch 'main' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
chronark authored Oct 12, 2024
2 parents 5205126 + 87031f9 commit bab20b1
Show file tree
Hide file tree
Showing 13 changed files with 423 additions and 294 deletions.
25 changes: 20 additions & 5 deletions apps/api/src/pkg/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,31 @@ export async function insertGenericAuditLogs(

const { cache, logger, db } = c.get("services");

const auditLogsInserts = [];
const auditLogTargetInserts = [];

for (const log of arr) {
const cacheKey = [log.workspaceId, log.bucket].join(":");
let { val: bucket, err } = await cache.auditLogBucketByWorkspaceIdAndName.swr(
cacheKey,
async () => {
const bucket = await (tx ?? db.primary).query.auditLogBucket.findFirst({
// do not use the transaction here, otherwise we may run into race conditions
// https://github.com/unkeyed/unkey/pull/2278
const bucket = await db.readonly.query.auditLogBucket.findFirst({
where: (table, { eq, and }) =>
and(eq(table.workspaceId, log.workspaceId), eq(table.name, log.bucket)),
});

if (!bucket) {
return undefined;
}

return {
id: bucket.id,
};
},
);

if (err) {
logger.error("Could not find audit log bucket for workspace", {
workspaceId: log.workspaceId,
Expand All @@ -68,7 +76,9 @@ export async function insertGenericAuditLogs(

if (!bucket) {
const bucketId = newId("auditLogBucket");
await (tx ?? db.primary).insert(schema.auditLogBucket).values({
// do not use the transaction here, otherwise we may run into race conditions
// https://github.com/unkeyed/unkey/pull/2278
await db.primary.insert(schema.auditLogBucket).values({
id: bucketId,
workspaceId: log.workspaceId,
name: log.bucket,
Expand All @@ -79,7 +89,7 @@ export async function insertGenericAuditLogs(
}

const auditLogId = newId("auditLog");
await (tx ?? db.primary).insert(schema.auditLog).values({
auditLogsInserts.push({
id: auditLogId,
workspaceId: log.workspaceId,
bucketId: bucket.id,
Expand All @@ -96,8 +106,9 @@ export async function insertGenericAuditLogs(
actorName: log.actor.name,
actorMeta: log.actor.meta,
});
await (tx ?? db.primary).insert(schema.auditLogTarget).values(
log.resources.map((r) => ({

auditLogTargetInserts.push(
...log.resources.map((r) => ({
workspaceId: log.workspaceId,
bucketId: bucket.id,
auditLogId,
Expand All @@ -109,4 +120,8 @@ export async function insertGenericAuditLogs(
})),
);
}

await (tx ?? db.primary).insert(schema.auditLog).values(auditLogsInserts);

await (tx ?? db.primary).insert(schema.auditLogTarget).values(auditLogTargetInserts);
}
2 changes: 1 addition & 1 deletion apps/api/src/pkg/keys/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ export class KeyService {
const ratelimits: {
[name: string | "default"]: Required<RatelimitRequest>;
} = {};
if ("default" in data.ratelimits) {
if ("default" in data.ratelimits && typeof req.ratelimits === "undefined") {
ratelimits.default = {
identity: data.key.id,
name: data.ratelimits.default.name,
Expand Down
40 changes: 37 additions & 3 deletions apps/api/src/routes/v1_identities_createIdentity.error.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, expect, test } from "vitest";

import type { ErrorResponse } from "@/pkg/errors";
import { schema } from "@unkey/db";
import { newId } from "@unkey/id";
import { IntegrationHarness } from "src/pkg/testutil/integration-harness";

import { describe, expect, test } from "vitest";
import type {
V1IdentitiesCreateIdentityRequest,
V1IdentitiesCreateIdentityResponse,
Expand Down Expand Up @@ -38,3 +39,36 @@ describe.each([
});
});
});
describe("when identity exists already", () => {
test("should return correct code and message", async (t) => {
const h = await IntegrationHarness.init(t);
const { key: rootKey } = await h.createRootKey(["*"]);

const externalId = newId("test");
await h.db.primary.insert(schema.identities).values({
id: newId("test"),
workspaceId: h.resources.userWorkspace.id,
externalId,
});

const res = await h.post<V1IdentitiesCreateIdentityRequest, ErrorResponse>({
url: "/v1/identities.createIdentity",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${rootKey}`,
},
body: {
externalId: externalId,
},
});

expect(res.status).toEqual(412);
expect(res.body).toMatchObject({
error: {
code: "PRECONDITION_FAILED",
docs: "https://unkey.dev/docs/api-reference/errors/code/PRECONDITION_FAILED",
message: "Duplicate identity",
},
});
});
});
162 changes: 87 additions & 75 deletions apps/api/src/routes/v1_identities_createIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ const route = createRoute({
This usually comes from your authentication provider and could be a userId, organisationId or even an email.
It does not matter what you use, as long as it uniquely identifies something in your application.
`,
\`externalId\`s are unique across your workspace and therefore a \`PRECONDITION_FAILED\` error is returned when you try to create duplicates.
`,
example: "user_123",
}),
meta: z
Expand Down Expand Up @@ -128,80 +130,36 @@ export const registerV1IdentitiesCreateIdentity = (app: App) =>
environment: "default",
meta: req.meta,
};
await db.primary.transaction(async (tx) => {
await tx
.insert(schema.identities)
.values(identity)
.catch((e) => {
if (e instanceof DatabaseError && e.body.message.includes("desc = Duplicate entry")) {
throw new UnkeyApiError({
code: "PRECONDITION_FAILED",
message: "Duplicate identity ",
});
}
});
await db.primary
.transaction(async (tx) => {
await tx
.insert(schema.identities)
.values(identity)
.catch((e) => {
if (e instanceof DatabaseError && e.body.message.includes("Duplicate entry")) {
throw new UnkeyApiError({
code: "PRECONDITION_FAILED",
message: "Duplicate identity",
});
}
});

const ratelimits = req.ratelimits
? req.ratelimits.map((r) => ({
id: newId("ratelimit"),
identityId: identity.id,
workspaceId: auth.authorizedWorkspaceId,
name: r.name,
limit: r.limit,
duration: r.duration,
}))
: [];

if (ratelimits.length > 0) {
await tx.insert(schema.ratelimits).values(ratelimits);
}

await insertUnkeyAuditLog(c, tx, [
{
workspaceId: authorizedWorkspaceId,
event: "identity.create",
actor: {
type: "key",
id: rootKeyId,
},
description: `Created ${identity.id}`,
resources: [
{
type: "identity",
id: identity.id,
},
],
const ratelimits = req.ratelimits
? req.ratelimits.map((r) => ({
id: newId("ratelimit"),
identityId: identity.id,
workspaceId: auth.authorizedWorkspaceId,
name: r.name,
limit: r.limit,
duration: r.duration,
}))
: [];

context: {
location: c.get("location"),
userAgent: c.get("userAgent"),
},
},
...ratelimits.map((r) => ({
workspaceId: authorizedWorkspaceId,
event: "ratelimit.create" as const,
actor: {
type: "key" as const,
id: rootKeyId,
},
description: `Created ${r.id}`,
resources: [
{
type: "identity" as const,
id: identity.id,
},
{
type: "ratelimit" as const,
id: r.id,
},
],
if (ratelimits.length > 0) {
await tx.insert(schema.ratelimits).values(ratelimits);
}

context: { location: c.get("location"), userAgent: c.get("userAgent") },
})),
]);

c.executionCtx.waitUntil(
analytics.ingestUnkeyAuditLogsTinybird([
await insertUnkeyAuditLog(c, tx, [
{
workspaceId: authorizedWorkspaceId,
event: "identity.create",
Expand Down Expand Up @@ -243,10 +201,64 @@ export const registerV1IdentitiesCreateIdentity = (app: App) =>

context: { location: c.get("location"), userAgent: c.get("userAgent") },
})),
]),
);
});
]);

c.executionCtx.waitUntil(
analytics.ingestUnkeyAuditLogsTinybird([
{
workspaceId: authorizedWorkspaceId,
event: "identity.create",
actor: {
type: "key",
id: rootKeyId,
},
description: `Created ${identity.id}`,
resources: [
{
type: "identity",
id: identity.id,
},
],

context: {
location: c.get("location"),
userAgent: c.get("userAgent"),
},
},
...ratelimits.map((r) => ({
workspaceId: authorizedWorkspaceId,
event: "ratelimit.create" as const,
actor: {
type: "key" as const,
id: rootKeyId,
},
description: `Created ${r.id}`,
resources: [
{
type: "identity" as const,
id: identity.id,
},
{
type: "ratelimit" as const,
id: r.id,
},
],

context: { location: c.get("location"), userAgent: c.get("userAgent") },
})),
]),
);
})
.catch((e) => {
if (e instanceof UnkeyApiError) {
throw e;
}

throw new UnkeyApiError({
code: "INTERNAL_SERVER_ERROR",
message: "unable to store identity and ratelimits in the database",
});
});
return c.json({
identityId: identity.id,
});
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/libraries/ts/sdk/keys/verifications.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The number of requests that exceeded the usage limit.
<RequestExample>

```ts
const { result, error } = await unkey.keys.get({ keyId: "key_123" });
const { result, error } = await unkey.keys.getVerifications({ keyId: "key_123" });
```


Expand Down
27 changes: 15 additions & 12 deletions apps/www/app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,21 @@ export default async function Blog(props: Props) {
<div>
<TopRightShiningLight />
</div>
<div className="w-full px-0 mx-0 rounded-3xl">
<Link href={`${posts[0].url}`} key={posts[0].url}>
<BlogHero
tags={posts[0].tags}
imageUrl={posts[0].image ?? "/images/blog-images/defaultBlog.png"}
title={posts[0].title}
subTitle={posts[0].description}
author={authors[posts[0].author]}
publishDate={posts[0].date}
/>
</Link>
</div>

{posts.length > 0 ? (
<div className="w-full px-0 mx-0 rounded-3xl">
<Link href={`${posts[0].url}`} key={posts[0].url}>
<BlogHero
tags={posts[0].tags}
imageUrl={posts[0].image ?? "/images/blog-images/defaultBlog.png"}
title={posts[0].title}
subTitle={posts[0].description}
author={authors[posts[0].author]}
publishDate={posts[0].date}
/>
</Link>
</div>
) : null}
<BlogGrid posts={blogGridPosts} searchParams={props.searchParams} />
<CTA />
</div>
Expand Down
13 changes: 13 additions & 0 deletions apps/www/app/templates/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const frameworks = [
"Axum",
"Actix",
"Rocket",
"Oak",
] as const;
export type Framework = StrArrayToUnion<typeof frameworks>;
// id -> label
Expand Down Expand Up @@ -52,6 +53,18 @@ export type Template = {
};

export const templates: Record<string, Template> = {
"deno-oak-ratelimit": {
title: "Ratelimiting your Oak API",
description: "Simple deno API with Oak and ratelimiting with Unkey",
authors: ["Devansh-Baghel"],
repository: "https://github.com/Devansh-Baghel/deno-unkey-ratelimit-starter",

image: "/images/templates/deno-oak-ratelimit.png",
readmeUrl:
"https://raw.githubusercontent.com/Devansh-Baghel/deno-unkey-ratelimit-starter/refs/heads/main/README.md",
language: "Typescript",
framework: "Oak",
},
"rust-rocket": {
title: "Secure your Rust Rocket API with Unkey",
description: "Generative AI REST API built with Rust and Rocket web framework with call quotas",
Expand Down
Loading

0 comments on commit bab20b1

Please sign in to comment.