feat: implement authentication for convex functions and relevant route.#34
Conversation
📝 WalkthroughWalkthroughThis PR introduces authenticated and admin-scoped Convex query/mutation wrappers, applies them across course, lesson, and invitation-code endpoints, enforces invitation-code validation during Better Auth sign-up, updates internal admin maintenance mutations, adjusts frontend call sites to drop ChangesAuth/admin gating and invitation code enforcement
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant SignUpForm
participant BetterAuth
participant AuthMiddleware
participant invitationCodesTable
SignUpForm->>BetterAuth: signUp.email(code, email, password)
BetterAuth->>AuthMiddleware: hooks.before /sign-up/email
AuthMiddleware->>invitationCodesTable: query by_code(code)
invitationCodesTable-->>AuthMiddleware: record or none
AuthMiddleware-->>BetterAuth: throw BAD_REQUEST or continue
BetterAuth-->>SignUpForm: success or error
sequenceDiagram
participant Client
participant ConvexEndpoint
participant authComponent
participant userProfilesTable
Client->>ConvexEndpoint: call authenticated/admin function
ConvexEndpoint->>authComponent: safeGetAuthUser()
authComponent-->>ConvexEndpoint: user or none
ConvexEndpoint->>userProfilesTable: lookup by tokenIdentifier (non-admin)
userProfilesTable-->>ConvexEndpoint: profile or none
ConvexEndpoint-->>Client: proceed or throw Unauthenticated/Unauthorized
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
web/src/components/auth/forms/SignUpForm.tsx (1)
45-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid casting the whole signup payload to
any.Keep the new
codefield, but type it narrowly so regressions in the Better Auth signup payload are still caught.Proposed type-safe payload
- const signUpRes = await authClient.signUp.email({ + const signUpPayload = { email: value.email, password: value.password, name: value.name, code: value.code, // Pass code to Better Auth signup body - } as any); + } satisfies Parameters<typeof authClient.signUp.email>[0] & { + code: string; + }; + + const signUpRes = await authClient.signUp.email(signUpPayload);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/auth/forms/SignUpForm.tsx` around lines 45 - 46, The signup payload in SignUpForm should not be cast to any, as that bypasses type checking for the Better Auth signup body. Update the signup call near the code field handling to keep code included while typing the payload narrowly, using the existing signup payload types or a small explicit type extension so mismatches in the rest of the fields are still caught.web/test/security.test.ts (1)
1-188: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAdd a sign-up hook test for invitation-code enforcement.
These tests manually seed Better Auth users, so they bypass the
/sign-up/emailbeforehook. Please add coverage that calls the sign-up flow without a code and with an invalid/expired/over-quota code, then asserts the expectedBAD_REQUESTbehavior. This directly covers the PR’s invitation-code sign-up objective and the hook inweb/convex/auth.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/test/security.test.ts` around lines 1 - 188, Add a focused sign-up hook test that exercises the `/sign-up/email` flow instead of seeding Better Auth data directly, since `createMockUser` bypasses the `before` hook in `web/convex/auth.ts`. In `security.test.ts`, add cases for missing invitation code and for invalid, expired, and over-quota codes, then assert each call fails with the expected BAD_REQUEST behavior. Use the existing `convexTest` setup and reference the auth sign-up hook path through the sign-up flow so the new coverage directly validates invitation-code enforcement.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/convex/auth.ts`:
- Around line 78-96: The invitation check in auth.ts only validates the code and
never consumes it, so update the successful sign-up path to increment usesCount,
record whoUsed, and create the required userProfiles.by_token entry after signup
completes. Make the invite consumption and gated profile creation happen
atomically with the user/profile lifecycle, and reference the invitationCodes
and userProfiles logic near the existing invitation lookup and validation flow.
- Around line 69-76: The /sign-up/email guard in auth.api.signUpEmail is
blocking internal admin bootstrap because createAdminUser from auth.ts/init.ts
calls it without an invitation code. Update the sign-up hook to allow a trusted
internal bootstrap path for admin provisioning, or route createAdminUser through
a separate admin-only path so the student invitation code check is skipped for
that flow. Use the existing signUpEmail and createAdminUser symbols to keep the
fix scoped correctly.
In `@web/test/courses.test.ts`:
- Around line 159-160: The authenticated test client in makeCourse is typed as
the withIdentity method instead of the client it returns, so client.mutation is
checked against the wrong shape. Update the type annotation to use the return
type of setup.withIdentity, for example by switching to
ReturnType<ReturnType<typeof setup>["withIdentity"]> or a shared alias, so the
client variable matches the actual authenticated client object.
---
Nitpick comments:
In `@web/src/components/auth/forms/SignUpForm.tsx`:
- Around line 45-46: The signup payload in SignUpForm should not be cast to any,
as that bypasses type checking for the Better Auth signup body. Update the
signup call near the code field handling to keep code included while typing the
payload narrowly, using the existing signup payload types or a small explicit
type extension so mismatches in the rest of the fields are still caught.
In `@web/test/security.test.ts`:
- Around line 1-188: Add a focused sign-up hook test that exercises the
`/sign-up/email` flow instead of seeding Better Auth data directly, since
`createMockUser` bypasses the `before` hook in `web/convex/auth.ts`. In
`security.test.ts`, add cases for missing invitation code and for invalid,
expired, and over-quota codes, then assert each call fails with the expected
BAD_REQUEST behavior. Use the existing `convexTest` setup and reference the auth
sign-up hook path through the sign-up flow so the new coverage directly
validates invitation-code enforcement.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 827fa747-db0b-4e7f-947d-7cee51d44c4d
📒 Files selected for processing (11)
web/convex/auth.tsweb/convex/courses.tsweb/convex/functions.tsweb/convex/init.tsweb/convex/invitationCodes.tsweb/convex/lessons.tsweb/src/components/auth/forms/SignUpForm.tsxweb/src/components/home/CourseList.tsxweb/src/routes/_authenticated.course.tsxweb/test/courses.test.tsweb/test/security.test.ts
💤 Files with no reviewable changes (1)
- web/src/routes/_authenticated.course.tsx
| if (apiCtx.path === "/sign-up/email") { | ||
| const body = apiCtx.body as any; | ||
| const code = body?.code; | ||
| if (!code) { | ||
| throw new APIError("BAD_REQUEST", { | ||
| message: "Invitation code is required.", | ||
| }); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve the internal admin bootstrap path.
web/convex/init.ts still calls auth.api.signUpEmail without code, so this hook rejects createAdminUser before the admin can be created. Add a trusted internal bootstrap path or update admin provisioning so it does not hit the student invitation requirement.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/convex/auth.ts` around lines 69 - 76, The /sign-up/email guard in
auth.api.signUpEmail is blocking internal admin bootstrap because
createAdminUser from auth.ts/init.ts calls it without an invitation code. Update
the sign-up hook to allow a trusted internal bootstrap path for admin
provisioning, or route createAdminUser through a separate admin-only path so the
student invitation code check is skipped for that flow. Use the existing
signUpEmail and createAdminUser symbols to keep the fix scoped correctly.
| const invitation = await ctx.db | ||
| .query("invitationCodes") | ||
| .withIndex("by_code", (q) => q.eq("code", code)) | ||
| .unique(); | ||
|
|
||
| if (!invitation || !invitation.isValid || invitation.usesCount >= invitation.quantity) { | ||
| throw new APIError("BAD_REQUEST", { | ||
| message: "Invalid or expired invitation code.", | ||
| }); | ||
| } | ||
|
|
||
| if (invitation.expiryDate) { | ||
| const expiry = new Date(invitation.expiryDate); | ||
| if (!isNaN(expiry.getTime()) && expiry.getTime() < Date.now()) { | ||
| throw new APIError("BAD_REQUEST", { | ||
| message: "Invitation code has expired.", | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Consume the invitation and create the gated profile after sign-up succeeds.
This hook only checks the code. It never increments usesCount, records whoUsed, or creates the userProfiles.by_token row now required by authenticatedQuery/authenticatedMutation, so valid users can be locked out and single-use codes can be reused. Move consumption/profile creation into a successful sign-up path and make the invite update atomic with the user/profile lifecycle.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/convex/auth.ts` around lines 78 - 96, The invitation check in auth.ts
only validates the code and never consumes it, so update the successful sign-up
path to increment usesCount, record whoUsed, and create the required
userProfiles.by_token entry after signup completes. Make the invite consumption
and gated profile creation happen atomically with the user/profile lifecycle,
and reference the invitationCodes and userProfiles logic near the existing
invitation lookup and validation flow.
| const makeCourse = async (client: ReturnType<typeof setup>["withIdentity"]) => | ||
| client.mutation(api.courses.createCourse, { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the declared typecheck command and this helper annotation without running project code.
rg -n '"typecheck"|tsc --noEmit' package.json .github 2>/dev/null || true
rg -n 'ReturnType<typeof setup>\["withIdentity"\]|makeCourse' web/test/courses.test.tsRepository: RMIT-BDSL/ExemplAI
Length of output: 634
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== web/test/courses.test.ts (around lines 140-175) =="
sed -n '140,175p' web/test/courses.test.ts | cat -n
echo
echo "== locate setup definition and withIdentity usage =="
rg -n "function setup|const setup|withIdentity|ReturnType<.*setup" web -g '!**/node_modules/**'
echo
echo "== show setup file candidates =="
fd -a "setup.ts" web || true
fd -a "setup.*" web/test web || trueRepository: RMIT-BDSL/ExemplAI
Length of output: 2660
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== web/test/courses.test.ts top section =="
sed -n '1,70p' web/test/courses.test.ts | cat -n
echo
echo "== search for convexTest / withIdentity typing in repo =="
rg -n "withIdentity\(|convexTest\(|createMockAdmin|AuthenticatedTestClient|mutation\(" web -g '!**/node_modules/**' | sed -n '1,220p'
echo
echo "== find type declarations for convex test helpers if present =="
rg -n "withIdentity" . -g '!**/node_modules/**' -g '!**/.git/**' | sed -n '1,220p'Repository: RMIT-BDSL/ExemplAI
Length of output: 7795
🌐 Web query:
convex-test withIdentity return type mutation query TestConvexClient TypeScript
💡 Result:
In the context of the convex-test library for testing Convex functions in TypeScript, withIdentity is a method used to simulate an authenticated user for subsequent query and mutation calls [1][2]. When you call t.withIdentity(identityAttributes), it returns a new client-like object (often referred to as an authenticated test client) that wraps your original test helper (t). This new object shares the same API as the primary test helper, specifically providing.query and.mutation methods, but it ensures that any Convex functions executed through this client will see the provided identity when calling context.auth.getUserIdentity [1][2]. Key points regarding its use: 1. Usage: You create an authenticated client by passing an object containing user attributes (such as name, issuer, subject, or tokenIdentifier) to withIdentity [1][3][2]. 2. Automatic Generation: If you call withIdentity() without arguments, Convex will automatically generate default values for issuer, subject, and tokenIdentifier [1][2]. 3. Execution: Once you have the authenticated client (e.g., const asSarah = t.withIdentity({ name: "Sarah" })), you can call your API functions normally [1][2]: await asSarah.mutation(api.tasks.create, { text: "Add tests" }); const result = await asSarah.query(api.tasks.list); This mechanism allows you to unit test authentication-dependent logic, such as ensuring that functions only return data accessible to a specific user or that mutations are protected against unauthorized access [1][2][4]. Note that this library is distinct from the production ConvexClient (used in standard application code), which requires setAuth for authentication and is intended for use with a running Convex backend, rather than the isolated testing environment provided by convex-test [5][2][6].
Citations:
- 1: https://docs.convex.dev/testing/convex-test.md
- 2: https://docs.convex.dev/testing/convex-test
- 3: No clear way to interface with convex-auth get-convex/convex-test#32
- 4: https://github.com/get-convex/convex-helpers/blob/68afe3a0/packages/convex-helpers/server/customFunctions.test.ts
- 5: https://get-convex-convex-backend.mintlify.app/client/typescript-sdk
- 6: https://www.mintlify.com/get-convex/convex-backend/client/typescript-sdk
🌐 Web query:
convex-test withIdentity return type mutation query TypeScript
💡 Result:
In the convex-test library, the withIdentity method creates a new, identity-aware version of your test helper, allowing you to invoke mutations and queries as a specific user [1][2]. When you call t.withIdentity({... }), it returns an object (the identity-aware test helper) that provides the same interface as your primary test helper t, including the.mutation and.query methods [1][2]. These methods automatically handle the return types of your Convex functions via TypeScript's inference, just as they do in your standard application code [3]. Usage Pattern: 1. Initialize: const t = convexTest(schema, modules); [1][2] 2. Impersonate: const asUser = t.withIdentity({ name: "User Name" }); [1][2] 3. Execute: const result = await asUser.mutation(api.myModule.myMutation, { arg: "value" }); [1][2] The return type of the mutation or query invoked via this helper will automatically match the return type defined in your Convex function [3]. TypeScript preserves this type information because the.mutation and.query methods use the FunctionReference provided by the api object, which carries the return type signature [1][2][3]. Note that while this method simulates authentication, it is a mock environment [4]. If your functions rely on complex authentication libraries (like @convex-dev/auth), you may need to ensure your identity object includes the expected fields (e.g., tokenIdentifier) or mock the auth module if you encounter issues with identity propagation in complex test scenarios [5][4].
Citations:
- 1: https://docs.convex.dev/testing/convex-test.md
- 2: https://docs.convex.dev/testing/convex-test
- 3: https://www.convex.dev/typescript/core-concepts/functions-methods/typescript-return-type
- 4: No clear way to interface with convex-auth get-convex/convex-test#32
- 5: Implement getUserIdentity syscall and fix authentication context propagation in nested contexts get-convex/convex-test#50
Fix the authenticated test client type
ReturnType<typeof setup>["withIdentity"] resolves to the withIdentity method, not the client it returns, so client.mutation(...) is typed against the wrong shape. Use ReturnType<ReturnType<typeof setup>["withIdentity"]> (or an alias for it) here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/test/courses.test.ts` around lines 159 - 160, The authenticated test
client in makeCourse is typed as the withIdentity method instead of the client
it returns, so client.mutation is checked against the wrong shape. Update the
type annotation to use the return type of setup.withIdentity, for example by
switching to ReturnType<ReturnType<typeof setup>["withIdentity"]> or a shared
alias, so the client variable matches the actual authenticated client object.
Summary by CodeRabbit
New Features
Bug Fixes
Security