Skip to content

feat: implement authentication for convex functions and relevant route.#34

Merged
millecodex merged 1 commit into
devfrom
hotfix/security
Jul 1, 2026
Merged

feat: implement authentication for convex functions and relevant route.#34
millecodex merged 1 commit into
devfrom
hotfix/security

Conversation

@Ronaldo93

@Ronaldo93 Ronaldo93 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Sign-up now supports invitation codes during email registration.
    • Course and lesson actions now follow clearer access rules for signed-in users and admins.
  • Bug Fixes

    • Fixed lesson progress tracking to use the current signed-in account automatically.
    • Updated lesson status updates to work without extra user identifiers.
    • Added stronger validation for invalid, expired, or overused invitation codes.
  • Security

    • Restricted course creation, lesson management, and invitation-code management to authorized admins.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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 tokenIdentifier, and adds/updates tests validating access control and authenticated flows.

Changes

Auth/admin gating and invitation code enforcement

Layer / File(s) Summary
Auth/admin wrapper builders
web/convex/functions.ts
Adds authenticatedQuery/authenticatedMutation (require auth user, require userProfiles for non-admins) and adminQuery/adminMutation (require role === "admin"), plus zAuthenticatedQuery, zAuthenticatedMutation, zAdminQuery, zAdminMutation Zod-validated exports.
Course and lesson endpoint re-wiring
web/convex/courses.ts, web/convex/lessons.ts
Course CRUD, listing, question, and progress endpoints switch to the new authenticated/admin wrappers; getLessonProgress/setLessonStatus drop tokenIdentifier args in favor of ctx.user._id; lesson CRUD endpoints move to zAuthenticatedQuery/zAdminMutation.
Invitation code gating and sign-up enforcement
web/convex/invitationCodes.ts, web/convex/auth.ts, web/convex/init.ts
Admin invitation-code endpoints switch to adminQuery/adminMutation; a new hooks.before middleware on /sign-up/email validates the invitation code against invitationCodes and rejects missing/invalid/expired codes; admin maintenance mutations in init.ts become internalMutation.
Frontend call-site updates
web/src/components/auth/forms/SignUpForm.tsx, web/src/components/home/CourseList.tsx, web/src/routes/_authenticated.course.tsx
Sign-up now sends the invitation code; getLessonProgress and setLessonStatus calls drop tokenIdentifier, relying on server-side auth context.
Authenticated/security test coverage
web/test/courses.test.ts, web/test/security.test.ts
courses.test.ts adds a createMockAdmin helper and rewires CRUD tests to an authenticated client; a new security.test.ts suite verifies unauthenticated, student, and admin access control for courses and invitation codes.

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
Loading
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
Loading

Possibly related PRs

  • RMIT-BDSL/ExemplAI#5: Synchronizes userProfiles.tokenIdentifier, which the new authenticatedQuery/adminQuery profile lookups depend on for correct user resolution.
  • RMIT-BDSL/ExemplAI#22: Touches the same web/convex/auth.ts createAuthOptions configuration and related lesson-progress APIs.
  • RMIT-BDSL/ExemplAI#31: Touches web/convex/init.ts admin-maintenance endpoints (removeAllAdminAccounts, listBetterAuthData) later re-declared as internalMutation in this PR.

Suggested reviewers: millecodex

Poem

A rabbit hopped through gates anew,
Checking codes and roles it knew,
"No admin badge? No course for you!"
Tokens dropped, but trust stays true,
Tests now guard the burrow's crew. 🐰🔐
Hop, hop, secure — the warren's through!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding authentication enforcement to Convex functions and the related route.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hotfix/security

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot requested a review from millecodex July 1, 2026 08:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
web/src/components/auth/forms/SignUpForm.tsx (1)

45-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid casting the whole signup payload to any.

Keep the new code field, 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 win

Add a sign-up hook test for invitation-code enforcement.

These tests manually seed Better Auth users, so they bypass the /sign-up/email before hook. Please add coverage that calls the sign-up flow without a code and with an invalid/expired/over-quota code, then asserts the expected BAD_REQUEST behavior. This directly covers the PR’s invitation-code sign-up objective and the hook in web/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

📥 Commits

Reviewing files that changed from the base of the PR and between 9943461 and 1d31dc4.

📒 Files selected for processing (11)
  • web/convex/auth.ts
  • web/convex/courses.ts
  • web/convex/functions.ts
  • web/convex/init.ts
  • web/convex/invitationCodes.ts
  • web/convex/lessons.ts
  • web/src/components/auth/forms/SignUpForm.tsx
  • web/src/components/home/CourseList.tsx
  • web/src/routes/_authenticated.course.tsx
  • web/test/courses.test.ts
  • web/test/security.test.ts
💤 Files with no reviewable changes (1)
  • web/src/routes/_authenticated.course.tsx

Comment thread web/convex/auth.ts
Comment on lines +69 to +76
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.",
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread web/convex/auth.ts
Comment on lines +78 to +96
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.",
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.

Comment thread web/test/courses.test.ts
Comment on lines +159 to +160
const makeCourse = async (client: ReturnType<typeof setup>["withIdentity"]) =>
client.mutation(api.courses.createCourse, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.ts

Repository: 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 || true

Repository: 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:


🌐 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:


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.

@millecodex millecodex merged commit eee48fd into dev Jul 1, 2026
1 of 2 checks passed
@Ronaldo93 Ronaldo93 deleted the hotfix/security branch July 2, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants