Skip to content

Commit

Permalink
Merge branch 'main' into harshbhat/encrypted-meta
Browse files Browse the repository at this point in the history
  • Loading branch information
harshsbhat authored Sep 18, 2024
2 parents 0e9c23d + c9ef6df commit 616beb4
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 6 deletions.
6 changes: 6 additions & 0 deletions apps/api/src/pkg/keys/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,13 @@ export class KeyService {
duration: data.key.ratelimitDuration,
};
}

for (const r of req.ratelimits ?? []) {
if (r.name === "default" && "default" in ratelimits) {
// it's already added above
continue;
}

if (typeof r.limit !== "undefined" && typeof r.duration !== "undefined") {
ratelimits[r.name] = {
identity: data.identity?.id ?? data.key.id,
Expand Down
38 changes: 38 additions & 0 deletions apps/api/src/routes/v1_keys_verifyKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,44 @@ describe("with ratelimit override", () => {
});
});

describe("with default ratelimit", () => {
test("uses the on-key defined settings", { timeout: 20000 }, async (t) => {
const h = await IntegrationHarness.init(t);
const key = new KeyV1({ prefix: "test", byteLength: 16 }).toString();
await h.db.primary.insert(schema.keys).values({
id: newId("test"),
keyAuthId: h.resources.userKeyAuth.id,
hash: await sha256(key),
start: key.slice(0, 8),
workspaceId: h.resources.userWorkspace.id,
createdAt: new Date(),
ratelimitLimit: 10,
ratelimitDuration: 60_000,
ratelimitAsync: false,
});

const res = await h.post<V1KeysVerifyKeyRequest, V1KeysVerifyKeyResponse>({
url: "/v1/keys.verifyKey",
headers: {
"Content-Type": "application/json",
},
body: {
key,
apiId: h.resources.userApi.id,
ratelimits: [
{
name: "default",
},
],
},
});
expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);
expect(res.body.valid).toBe(true);
expect(res.body.ratelimit).toBeDefined();
expect(res.body.ratelimit!.limit).toEqual(10);
});
});

describe("with ratelimit", () => {
describe("with valid key", () => {
test.skip(
Expand Down
9 changes: 3 additions & 6 deletions apps/dashboard/components/dashboard/command-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,16 @@ export function CommandMenu() {
const DiscordCommand: React.FC = () => {
const router = useRouter();
return (
<CommandItem onSelect={() => router.push("/discord")}>
<CommandItem onSelect={() => router.push("https://unkey.com/discord")}>
<svg
className="w-4 h-4 mr-2"
className="w-4 h-4 mr-2 fill-current"
viewBox="0 -28.5 256 256"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
>
<g>
<path
d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z"
fill-rule="nonzero"
/>
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" />
</g>
</svg>
<span>Go to Discord</span>
Expand Down
171 changes: 171 additions & 0 deletions apps/www/content/blog/learn-by-building.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
date: 2024-09-12
title: Learn by building
image: "/images/blog-images/covers/learn-by-building.png"
description: "What I learned developing a full application for the first time."
author: michael
tags: ["marketing"]
---




Last May I bought a [Bambu Labs A1](https://bambulab.com/en-us/a1). Mainly as a hobby and to make some parts for some home projects. While browsing for interesting creations from the community on [Makerworld](https://makerworld.com/en), I had the idea of creating myself a web app to store personal projects to reference and save notes and images of the progress.

## How I built my project

I am a relatively new developer, so planning larger projects like this is a new experience.

To build this project I needed:

- Authentication
- Database
- Web application
- Route protection

I went with:

- Auth.js
- sqlite db and Drizzle ORM
- NextJS
- Unkey ratelimit protection

I started with a basic database schema planning. I found that for me its always a good place to start. It allows me to get an idea of the data and how interaction with that data will happen.

### Setup

Next I gave myself a good starting point to quickly setup and hit the ground running. I found that the faster I can get something working the better for my ADHD. So I used the [T3 stack](https://create.t3.gg/) to give me a good head start.

```shell
pnpm create t3-app@latest
```

Options used:
<Image src="/images/blog-images/learn-by-building/createt3.png" alt="Using the playground" width="1920" height="1080"/>


### Build
Continuing that trend I added some basic [shadcn](https://ui.shadcn.com/) components to get started on the UI.
In just a short period of time I had a half decent looking app. But that was the easy part.

So UI being functional enough, I started digging into the api/server side of things. I set up the `Drizzle` schema and `tRPC` routes. Sure I may have needed the `tRPC` and `Drizzle` docs open the entire time. But hey, that is what they are for.
My first real hurdle was about now. As usual, I was starting to over think the schema and layout and whatever else. keeping on track with a larger project is a challenge for me.

When planning out the db schema I started adding more columns than needed. I also added references I would not need making it more complicated than it needed to be. I often have to stop myself from bouncing to another file if an idea pops into my head.
This was a good experience as it allowed me to think about self restraint and management. Just telling myself that things are fluid and can be changed later was very helpful. Nothing is perfect on the first draft so building things in a way that allows for changes later is important. For me being flexible is the way forward and not over thinking and getting stuck on a single task.

### Typescript

I have been working in a Typescript project for about a year now, but because a lot of the code was implemented when I got to Unkey. I often struggled with debugging errors.
On this project because I implemented code from start to finish, I got a lot more familiar with debugging typescript errors.

To make my life a bit easier, I used [zod](https://zod.dev/) to manage the tRPC routes.

```typescript
getProjectsByCategory: publicProcedure
.input(
z.object({
category: z.string().min(3),
}),
)
.query(async ({ ctx, input }) => {
const project = await ctx.db.query.projects.findMany({
where: eq(projects.category, input.category.toUpperCase()),
orderBy: (projects, { desc }) => [desc(projects.createdAt)],
limit: 50,
with: { steps: true },
});
return project;
}),
```

And on the form side a controlled form element with `zod` and `react-hook-form`

```typescript
const formSchema = z.object({
projectName: z.string().min(2).max(50),
category: z.string().min(2).max(50),
projectDescription: z
.string()
.min(10, { message: "Must be 10 or more characters long" })
.max(500, { message: "Must be less than 500 characters long" }),
projectImage: z
.instanceof(File)
.refine(
(file) => !ACCEPTED_IMAGE_TYPES.includes(file?.type),
"Only .jpg, .jpeg and .png formats are supported.",
)
.optional(),
});
```
Keeping my types in check made it easy to track down errors from human error. things like passing the wrong type to routes or incorrect variable names. Just makes less thing I need to worry about once setup so I can focus on the things that need to be done and not tracking down a typo or something.


### Ratelimit
If I ever want to launch this live I figured it would be a good idea to limit abuse on any of the secured routes. The choice was pretty easy being as I work for a company that has a `Ratelimit` sdk.

```shell
pnpm add @unkey/ratelimit

```

Unkey makes this incredible easy it take a couple of steps to implement. I used the docs as a reference point. [docs](https://www.unkey.com/docs/libraries/ts/ratelimit)

### Created ratelimit procedure

```typescript
export const rateLimitedProcedure = ({
limit,
duration,
}: {
limit: number;
duration: number;
}) =>
protectedProcedure.use(async (opts) => {
const unkey = new Ratelimit({
rootKey: env.UNKEY_ROOT_KEY,
namespace: `trpc_${opts.path}`,
limit: limit ?? 3,
duration: duration ? `${duration}s` : `${5}s`,
});

const ratelimit = await unkey.limit(opts.ctx.session.user.id);

if (!ratelimit.success) {
throw new TRPCError({
code: "TOO_MANY_REQUESTS",
message: JSON.stringify(ratelimit),
});
}

return opts.next({
ctx: {
...opts.ctx,
remaining: ratelimit.remaining,
},
});
});
```

And then used like this on any route you want to `ratelimit`

```typescript
create: rateLimitedProcedure({ limit: 3, duration: 5 })
.input(
z.object({
projectName: z.string().min(3),
projectDescription: z.string(),
category: z.string(),
projectImage: z.string().optional(),
}),
)
```

This is probably what took the longest. My experience is limited with `tRPC` routes and `ratelimiting`. I was stuck on this for a little while, as I have never really worked with tRPC and ratelimiting on my own. I tried to work through this, but needed to reach out to get help. Just like everyone else I hate bothering people but sometimes the best path forward to reaching out to someone else.

## Conclusion

In making this project I learned a hell of a lot. I now have a more solid understanding of client/server communications. How to debug and fix `Typescript` errors more effectively. When to ask for help from someone who has more experience, while docs will get you pretty far there is no substitute for another person to pair with. Big thanks to [James](https://x.com/james_r_perkins) and [Andreas](https://x.com/chronark_) for all the help over the last year. I would like to add more features to this in the future, but for now I have added the example into Unkey's templates page for anyone interested in checking out the code.

**Example**
[Unkey ratelimiting with TRPC + Drizzle](https://www.unkey.com/templates/unkey-trpc-ratelimit)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 616beb4

Please sign in to comment.