Skip to content

Commit 3bd9ce7

Browse files
committed
feat(convex): authenticate and rate limit for tasks.add
+ general enhancements
1 parent a6bb874 commit 3bd9ce7

10 files changed

Lines changed: 231 additions & 10 deletions

File tree

apps/backend-convex/convex/_generated/api.d.ts

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
* @module
99
*/
1010

11+
import type * as authInfo from "../authInfo.js";
12+
import type * as crons from "../crons.js";
13+
import type * as tasks from "../tasks.js";
14+
1115
import type {
1216
ApiFromModules,
1317
FilterApi,
1418
FunctionReference,
1519
} from "convex/server";
16-
import type * as authInfo from "../authInfo.js";
17-
import type * as tasks from "../tasks.js";
1820

1921
/**
2022
* A utility for referencing Convex functions in your app's API.
@@ -26,13 +28,163 @@ import type * as tasks from "../tasks.js";
2628
*/
2729
declare const fullApi: ApiFromModules<{
2830
authInfo: typeof authInfo;
31+
crons: typeof crons;
2932
tasks: typeof tasks;
3033
}>;
34+
declare const fullApiWithMounts: typeof fullApi;
35+
3136
export declare const api: FilterApi<
32-
typeof fullApi,
37+
typeof fullApiWithMounts,
3338
FunctionReference<any, "public">
3439
>;
3540
export declare const internal: FilterApi<
36-
typeof fullApi,
41+
typeof fullApiWithMounts,
3742
FunctionReference<any, "internal">
3843
>;
44+
45+
export declare const components: {
46+
rateLimiter: {
47+
lib: {
48+
checkRateLimit: FunctionReference<
49+
"query",
50+
"internal",
51+
{
52+
config:
53+
| {
54+
capacity?: number;
55+
kind: "token bucket";
56+
maxReserved?: number;
57+
period: number;
58+
rate: number;
59+
shards?: number;
60+
}
61+
| {
62+
capacity?: number;
63+
kind: "fixed window";
64+
maxReserved?: number;
65+
period: number;
66+
rate: number;
67+
shards?: number;
68+
start?: number;
69+
};
70+
count?: number;
71+
key?: string;
72+
name: string;
73+
reserve?: boolean;
74+
throws?: boolean;
75+
},
76+
{ ok: true; retryAfter?: number } | { ok: false; retryAfter: number }
77+
>;
78+
clearAll: FunctionReference<
79+
"mutation",
80+
"internal",
81+
{ before?: number },
82+
null
83+
>;
84+
rateLimit: FunctionReference<
85+
"mutation",
86+
"internal",
87+
{
88+
config:
89+
| {
90+
capacity?: number;
91+
kind: "token bucket";
92+
maxReserved?: number;
93+
period: number;
94+
rate: number;
95+
shards?: number;
96+
}
97+
| {
98+
capacity?: number;
99+
kind: "fixed window";
100+
maxReserved?: number;
101+
period: number;
102+
rate: number;
103+
shards?: number;
104+
start?: number;
105+
};
106+
count?: number;
107+
key?: string;
108+
name: string;
109+
reserve?: boolean;
110+
throws?: boolean;
111+
},
112+
{ ok: true; retryAfter?: number } | { ok: false; retryAfter: number }
113+
>;
114+
resetRateLimit: FunctionReference<
115+
"mutation",
116+
"internal",
117+
{ key?: string; name: string },
118+
null
119+
>;
120+
};
121+
public: {
122+
checkRateLimit: FunctionReference<
123+
"query",
124+
"internal",
125+
{
126+
config:
127+
| {
128+
capacity?: number;
129+
kind: "token bucket";
130+
maxReserved?: number;
131+
period: number;
132+
rate: number;
133+
shards?: number;
134+
}
135+
| {
136+
capacity?: number;
137+
kind: "fixed window";
138+
maxReserved?: number;
139+
period: number;
140+
rate: number;
141+
shards?: number;
142+
start?: number;
143+
};
144+
count?: number;
145+
key?: string;
146+
name: string;
147+
reserve?: boolean;
148+
throws?: boolean;
149+
},
150+
{ ok: true; retryAfter?: number } | { ok: false; retryAfter: number }
151+
>;
152+
rateLimit: FunctionReference<
153+
"mutation",
154+
"internal",
155+
{
156+
config:
157+
| {
158+
capacity?: number;
159+
kind: "token bucket";
160+
maxReserved?: number;
161+
period: number;
162+
rate: number;
163+
shards?: number;
164+
}
165+
| {
166+
capacity?: number;
167+
kind: "fixed window";
168+
maxReserved?: number;
169+
period: number;
170+
rate: number;
171+
shards?: number;
172+
start?: number;
173+
};
174+
count?: number;
175+
key?: string;
176+
name: string;
177+
reserve?: boolean;
178+
throws?: boolean;
179+
},
180+
{ ok: true; retryAfter?: number } | { ok: false; retryAfter: number }
181+
>;
182+
resetRateLimit: FunctionReference<
183+
"mutation",
184+
"internal",
185+
{ key?: string; name: string },
186+
null
187+
>;
188+
};
189+
};
190+
};

apps/backend-convex/convex/_generated/api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @module
99
*/
1010

11-
import { anyApi } from "convex/server";
11+
import { anyApi, componentsGeneric } from "convex/server";
1212

1313
/**
1414
* A utility for referencing Convex functions in your app's API.
@@ -20,3 +20,4 @@ import { anyApi } from "convex/server";
2020
*/
2121
export const api = anyApi;
2222
export const internal = anyApi;
23+
export const components = componentsGeneric();

apps/backend-convex/convex/_generated/server.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import {
1212
ActionBuilder,
13+
AnyComponents,
1314
HttpActionBuilder,
1415
MutationBuilder,
1516
QueryBuilder,
@@ -18,9 +19,15 @@ import {
1819
GenericQueryCtx,
1920
GenericDatabaseReader,
2021
GenericDatabaseWriter,
22+
FunctionReference,
2123
} from "convex/server";
2224
import type { DataModel } from "./dataModel.js";
2325

26+
type GenericCtx =
27+
| GenericActionCtx<DataModel>
28+
| GenericMutationCtx<DataModel>
29+
| GenericQueryCtx<DataModel>;
30+
2431
/**
2532
* Define a query in this Convex app's public API.
2633
*

apps/backend-convex/convex/_generated/server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
internalActionGeneric,
1717
internalMutationGeneric,
1818
internalQueryGeneric,
19+
componentsGeneric,
1920
} from "convex/server";
2021

2122
/**
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import rateLimiter from '@convex-dev/rate-limiter/convex.config'
2+
import { defineApp } from 'convex/server'
3+
4+
const app: ReturnType<typeof defineApp> = defineApp()
5+
app.use(rateLimiter)
6+
7+
export default app
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { cronJobs } from 'convex/server'
2+
import { internal } from './_generated/api'
3+
4+
const crons = cronJobs()
5+
6+
crons.interval(
7+
'clear tasks table',
8+
{ minutes: 10 }, // every 10 minutes
9+
internal.tasks.clearAll,
10+
)
11+
12+
export default crons

apps/backend-convex/convex/tasks.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import { MINUTE, RateLimiter } from '@convex-dev/rate-limiter'
12
import { v } from 'convex/values'
2-
import { mutation, query } from './_generated/server'
3+
import { components } from './_generated/api'
4+
import { internalMutation, mutation, query } from './_generated/server'
5+
6+
const rateLimiter = new RateLimiter(components.rateLimiter, {
7+
addTask: { kind: 'token bucket', rate: 10, period: MINUTE, capacity: 3 },
8+
})
39

410
export const get = query({
511
args: {},
@@ -13,6 +19,20 @@ export const add = mutation({
1319
text: v.string(),
1420
},
1521
handler: async (ctx, args) => {
22+
const userIdentity = await ctx.auth.getUserIdentity()
23+
if (userIdentity === null)
24+
throw new Error('Not authenticated')
25+
26+
await rateLimiter.limit(ctx, 'addTask', { key: userIdentity.subject, throws: true })
27+
1628
return await ctx.db.insert('tasks', { text: args.text })
1729
},
1830
})
31+
32+
export const clearAll = internalMutation({
33+
args: {},
34+
handler: async (ctx) => {
35+
const tasks = await ctx.db.query('tasks').collect()
36+
await Promise.all(tasks.map(task => ctx.db.delete(task._id)))
37+
},
38+
})

apps/backend-convex/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"_deploy": "convex deploy -y",
1717
"predev": "convex dev --once"
1818
},
19+
"dependencies": {
20+
"@convex-dev/rate-limiter": "^0.2.7"
21+
},
1922
"devDependencies": {
2023
"@local/common": "workspace:*",
2124
"@local/locales": "workspace:*",

apps/frontend/app/components/ConvexIntegrationTest.vue

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@ const taskInputRef = ref('')
1313
1414
async function addTask() {
1515
await mutateAddTask({ text: taskInputRef.value })
16-
.then(() => { taskInputRef.value = '' })
16+
.then((r) => {
17+
if ('error' in r && r.error)
18+
return toast.add({ severity: 'error', detail: r.error.message, life: 10000 })
19+
20+
taskInputRef.value = ''
21+
})
1722
}
1823
1924
const isFetchingTasks = ref(false)
2025
async function testConvexViaBackendTasksCTA() {
2126
isFetchingTasks.value = true
2227
2328
await hcParse($apiClient.api.dummy.convexTasks.$get())
24-
.then(r => toast.add({ detail: r.map(t => t.text).join('\n') }))
25-
.catch(e => toast.add({ severity: 'error', detail: e.message }))
29+
.then(r => toast.add({ detail: r.map(t => t.text).join('\n'), life: 5000 }))
30+
.catch(e => toast.add({ severity: 'error', detail: e.message, life: 10000 }))
2631
2732
isFetchingTasks.value = false
2833
}
@@ -33,7 +38,7 @@ async function testConvexViaBackendTasksCTA() {
3338
<div>
3439
{{ $t('components.convexIntegrationTest.configuredUrl') }}: <code class="rounded bg-gray-100 px-1 py-0.5 text-base dark:bg-gray-800">{{ convexClient.client.url }}</code>
3540
</div>
36-
<div>{{ $t('components.convexIntegrationTest.authInfo') }}: <pre class="max-w-full w-full overflow-x-auto rounded bg-black p-2 px-4 text-left text-xs text-white">{{ authInfo }}</pre></div>
41+
<div>{{ $t('components.convexIntegrationTest.authInfo') }}: <pre class="max-w-full w-full overflow-x-auto rounded bg-black p-2 px-4 text-left text-xs text-white">{{ authInfo ?? 'Not authenticated' }}</pre></div>
3742
</div>
3843
<div class="max-w-md w-full flex flex-col gap-5">
3944
<IftaLabel class="w-full">

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)