Skip to content

chore: Auth changes#3898

Closed
perkinsjr wants to merge 16 commits intomainfrom
auth-rewrites
Closed

chore: Auth changes#3898
perkinsjr wants to merge 16 commits intomainfrom
auth-rewrites

Conversation

@perkinsjr
Copy link
Member

@perkinsjr perkinsjr commented Aug 31, 2025

What does this PR do?

This PR contains a comprehensive overhaul of authentication and authorization patterns across the Unkey dashboard, introducing critical security improvements and code quality enhancements.

🚀 Key Changes

1. Authentication Function Refactoring

  • Renamed getAuth to getAuthOrRedirect for better clarity and intent
  • Updated all imports across 17 files to use the new function name
  • Preserved separate getAuth implementations where appropriate
  • The new name clearly indicates the function's behavior of automatically redirecting unauthenticated users

2. Organization Security Middleware

  • Introduced requireOrgId middleware to centralize organization access validation
  • Eliminates code duplication across multiple tRPC procedures
  • Prevents privilege escalation attacks by removing client-controlled orgId parameters
  • Applied to all organization-related procedures:
    • getInvitationList
    • getOrg
    • inviteMember
    • removeMembership
    • revokeInvitation
    • updateMembership

3. Workspace Security Fixes

  • Fixed workspace hijacking vulnerability in the changeName endpoint
  • Removed workspaceId from input schema to prevent arbitrary workspace access
  • Now uses authenticated context (ctx.workspace.id) instead of user input
  • Maintains audit trail functionality while ensuring security

4. Enhanced RBAC Permission Management

  • Refactored upsertPermission to follow tRPC procedure patterns
  • Added workspace access validation for system/user workspaces
  • Improved input validation with regex patterns for permission names
  • Cleaned up error handling and code formatting

5. Team Settings UI Improvements

  • Simplified team settings components by removing redundant orgId parameters
  • Updated all client-side components to work with new secure endpoints
  • Improved type safety across team management interfaces

🛡️ Security Benefits

  1. Eliminates Privilege Escalation: Impossible for clients to specify arbitrary organization IDs
  2. Prevents Workspace Hijacking: Users can only modify their authenticated workspace
  3. Centralized Access Control: Single source of truth for organization validation
  4. Consistent Error Handling: Standardized security error responses

Fixes # (issue)

If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Make sure all functionality still works in the application.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

Summary by CodeRabbit

  • New Features

    • Dashboard now redirects users to sign-in or workspace setup when auth or workspace info is missing.
  • Refactor

    • Authentication unified across pages via a single auth helper with consistent redirect behavior.
    • Server now supplies organization/workspace context for team and org endpoints; clients no longer send org/workspace IDs.
  • Bug Fixes

    • Added workspace access validation to prevent cross-workspace actions.
  • Chores

    • Removed a legacy permission upsert API.

This commit introduces a centralized `requireOrgId` middleware to
eliminate
code duplication and improve security across all organization-related
tRPC
procedures.

### Changes Made

**New Middleware:**
- Added `requireOrgId` middleware in `lib/trpc/trpc.ts`
- Extracts and validates `orgId` from `ctx.workspace?.orgId`
- Provides consistent error handling with "Organization ID not found"
message
- Makes `ctx.orgId` available to procedures through context extension

**Updated Procedures:**
- `getInvitationList`: Removed input parameter, uses `requireOrgId`
middleware
- `getOrg`: Removed input parameter, uses `requireOrgId` middleware
- `inviteMember`: Removed `orgId` from input schema, uses middleware
- `removeMembership`: Removed `orgId` from input schema, uses middleware
- `revokeInvitation`: Removed `orgId` from input schema, uses middleware
- `updateMembership`: Removed `orgId` from input schema, uses middleware
- `getOrganizationMemberList`: Already using `ctx.tenant.id` correctly

**Client-side Updates:**
- All client components updated to remove orgId parameters from API
calls
- Team settings, invitations, members, and role management all secured

### Security Benefits

1. **Eliminates code duplication**: Single source of truth for orgId
validation
2. **Prevents privilege escalation**: Impossible for clients to specify
arbitrary orgIds
3. **Consistent error handling**: Standardized error messages and
behavior
4. **Type safety**: TypeScript guarantees `ctx.orgId` is available after
middleware
5. **Maintainability**: Changes to orgId handling only need to be made
in one place

### Implementation Details

The middleware follows the established pattern of other tRPC
middlewares:
- Validates required context (`ctx.workspace?.orgId`)
- Throws appropriate `TRPCError` if validation fails
- Extends context with validated `orgId` for use in procedures
- Applied in middleware chain: `requireUser` → `requireOrgAdmin` →
`requireOrgId`

This approach ensures all organization operations are strictly scoped to
the
authenticated user's organization without requiring repetitive
validation
code in each procedure.
- Remove workspaceId from input schema to prevent arbitrary workspace
access
- Use ctx.workspace.id from authenticated context instead of user input
- Ensures users can only modify their own authenticated workspace
- Maintains existing audit trail and functionality

Resolves unauthorized workspace name modification vulnerability
- Refactor upsertPermission to use tRPC procedure pattern
- Add workspace access validation for system/user workspaces
- Improve input validation with regex patterns for permission names
- Clean up code formatting and error handling
- Simplify team settings UI components
- Renamed getAuth function to getAuthWithRedirect in
apps/dashboard/lib/auth.ts
- Updated all imports from '@/lib/auth' across 17 files to use new
function name
- Preserved imports of getAuth from '@/lib/auth/get-auth' (different
implementation)
- New name better reflects function's behavior of automatically
redirecting unauthenticated users to sign-in page

Files updated:
- Function definition: lib/auth.ts
- Import updates: 16 page/component files + lib/auth/utils.ts
- Preserved: lib/auth/workos.ts, lib/trpc/context.ts (different getAuth)
@changeset-bot
Copy link

changeset-bot bot commented Aug 31, 2025

⚠️ No Changeset found

Latest commit: 71a7f3a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Aug 31, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
dashboard Ready Ready Preview Comment Sep 23, 2025 8:15am
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
engineering Ignored Ignored Preview Sep 23, 2025 8:15am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 31, 2025

📝 Walkthrough

Walkthrough

Replaced getAuth with getAuthOrRedirect across server pages and auth utilities (adds redirects for missing user/org); introduced requireOrgId middleware and moved orgId sourcing from client inputs into TRPC context; removed client-supplied workspace/org IDs from multiple RPCs and frontend calls; added RBAC workspace guard and deleted upsertPermission.

Changes

Cohort / File(s) Summary
Auth usage in pages
apps/dashboard/app/(app)/apis/[apiId]/actions.ts, apps/dashboard/app/(app)/audit/page.tsx, apps/dashboard/app/(app)/gateway-new/page.tsx, apps/dashboard/app/(app)/identities/[identityId]/page.tsx, apps/dashboard/app/(app)/identities/page.tsx, apps/dashboard/app/(app)/layout.tsx, apps/dashboard/app/(app)/logs/page.tsx, apps/dashboard/app/(app)/page.tsx, apps/dashboard/app/(app)/projects/page.tsx, apps/dashboard/app/(app)/settings/billing/page.tsx, apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx, apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx, apps/dashboard/app/(app)/settings/general/page.tsx, apps/dashboard/app/(app)/settings/team/page.tsx, apps/dashboard/app/(app)/settings/vercel/page.tsx, apps/dashboard/app/integrations/vercel/callback/page.tsx
Swapped getAuthgetAuthOrRedirect imports and calls; added redirect guards when orgId missing; several return redirect(...) changed to redirect(...).
Auth library & utils
apps/dashboard/lib/auth.ts, apps/dashboard/lib/auth/utils.ts
Exported GetAuthResult; renamed getAuthgetAuthOrRedirect and added redirects for missing user/org; getCurrentUser/requireAuth now use getAuthOrRedirect and return updated type.
TRPC core middleware
apps/dashboard/lib/trpc/trpc.ts
Added requireOrgId middleware (injects ctx.orgId or NOT_FOUND); simplified requireOrgAdmin to use ctx only and adjusted error handling and ordering.
TRPC org routers (context orgId)
apps/dashboard/lib/trpc/routers/org/getOrg.ts, .../getInvitationList.ts, .../getOrganizationMemberList.ts, .../inviteMember.ts, .../removeMembership.ts, .../revokeInvitation.ts, .../updateMembership.ts
Removed client-supplied orgId inputs; added/used requireOrgId middleware; handlers now read ctx.orgId; removed input-vs-context runtime checks; tightened role inputs to enums where applicable.
Team settings clients
apps/dashboard/app/(app)/settings/team/client.tsx, .../invitations.tsx, .../invite.tsx, .../members.tsx, .../role-switcher.tsx
Stopped passing orgId to queries/mutations; removed orgId from mutation payloads; role-switcher adds zod enum validation and uses { membershipId, role }.
Workspace name change
apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx, apps/dashboard/lib/trpc/routers/workspace/changeName.ts
Removed workspaceId from client form and RPC input; server updates use ctx.workspace.id.
RBAC adjustments
apps/dashboard/lib/trpc/routers/rbac.ts, (deleted) apps/dashboard/lib/trpc/routers/rbac/upsertPermission.ts
Added workspace-access validation using env().UNKEY_WORKSPACE_ID in upsert permissions; deleted upsertPermission.ts file (removed transactional upsert + audit logging helper).
Misc pages redirect change
apps/dashboard/app/success/page.tsx
Changed return redirect("/new") to redirect("/new") in workspace-not-found branch (control-flow effect).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Page as Server Page
  participant Auth as getAuthOrRedirect
  participant Router as Next.js Router

  User->>Page: GET page
  Page->>Auth: await getAuthOrRedirect()
  alt no userId
    Auth-->>Router: redirect /auth/sign-in
  else no orgId (and no role)
    Auth-->>Router: redirect /new
  else
    Auth-->>Page: { userId, orgId, role, impersonator }
    Page-->>User: Rendered response
  end
Loading
sequenceDiagram
  autonumber
  actor Client
  participant TRPC as t.procedure
  participant MW1 as requireUser
  participant MW2 as requireOrgId
  participant MW3 as requireOrgAdmin
  participant Handler as org route handler

  Client->>TRPC: Call org route (no orgId input)
  TRPC->>MW1: authenticate
  MW1-->>TRPC: ok
  TRPC->>MW2: inject ctx.orgId
  alt missing orgId
    MW2-->>Client: TRPC NOT_FOUND
  else
    MW2-->>TRPC: ctx.orgId set
    TRPC->>MW3: authorize admin using ctx.orgId
    alt not admin
      MW3-->>Client: TRPC FORBIDDEN
    else
      MW3-->>Handler: execute with ctx.orgId
      Handler-->>Client: result
    end
  end
Loading
sequenceDiagram
  autonumber
  actor Admin
  participant RBAC as rbac.upsertPermissions
  participant Env as env()
  participant DB as DB Tx

  Admin->>RBAC: upsertPermissions(workspaceId,...)
  RBAC->>Env: read UNKEY_WORKSPACE_ID
  RBAC->>RBAC: validate workspaceId ∈ {UNKEY_WORKSPACE_ID, ctx.workspace?.id}
  alt invalid workspace
    RBAC-->>Admin: TRPC FORBIDDEN "Access denied to workspace"
  else
    RBAC->>DB: transaction to upsert permissions
    DB-->>Admin: permissions
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • mcstepp
  • chronark
  • ogzhanolguncu

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive The PR description is detailed about the refactors, security rationale, and affected areas, and it marks the change type and checklist items, but it omits a specific issue reference and provides only a vague "How should this be tested?" entry without concrete reproduction steps, expected outcomes, or CI/test commands. Those omissions make it difficult for reviewers to verify the claimed test coverage and to validate the security fixes end-to-end. Because of the missing issue link and insufficient testing instructions the template compliance is incomplete and reviewer verification is limited. Please add or link the issue number this PR fixes (or explicitly state none) and expand the "How should this be tested?" section with concrete, step-by-step reproduction instructions, expected results, and any CI or local commands run (for example pnpm build and pnpm test). Also list specific scenarios to validate the security changes (e.g., attempts to pass client-controlled orgId, workspace rename attempts by non-owners, and unauthenticated access flows) and note any unit or integration tests added so reviewers can re-evaluate template compliance. After those details are provided the description should be rechecked and will likely be marked as passing.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title "chore: Auth changes" is concise, uses a conventional prefix, and correctly signals that the changes are related to authentication/authorization refactors in the PR; it aligns with the main changes (renaming getAuth to getAuthOrRedirect and adding requireOrgId middleware). It is somewhat broad and does not enumerate specific security fixes, but it is accurate and useful for quick history scanning. Overall the title sufficiently summarizes the primary intent of the changeset.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch auth-rewrites

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@vercel vercel bot temporarily deployed to Preview – engineering August 31, 2025 13:47 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard August 31, 2025 13:50 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard September 1, 2025 07:38 Inactive
@vercel vercel bot temporarily deployed to Preview – engineering September 1, 2025 11:51 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard September 1, 2025 11:53 Inactive
@perkinsjr perkinsjr marked this pull request as ready for review September 1, 2025 11:55
@github-actions
Copy link
Contributor

github-actions bot commented Sep 1, 2025

Thank you for following the naming conventions for pull request titles! 🙏

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (31)
apps/dashboard/lib/trpc/routers/rbac.ts (4)

107-116: Fix Drizzle where clause: use and(), not JS &&

Using JS && drops one predicate; use Drizzle's and() to compose conditions.

-      const key = await db.query.keys.findFirst({
-        where: eq(schema.keys.forWorkspaceId, workspace.id) && eq(schema.keys.id, input.rootKeyId),
+      const key = await db.query.keys.findFirst({
+        where: (table, { and, eq }) =>
+          and(eq(table.forWorkspaceId, workspace.id), eq(table.id, input.rootKeyId)),

817-874: Deduplicate slugs and make insert idempotent to avoid unique violations

Duplicate slugs produce duplicate inserts in one TX and may break on a unique (workspaceId, slug) index. Also handle races.

-    const existingPermissions = await tx.query.permissions.findMany({
-      where: (table, { inArray, and, eq }) =>
-        and(eq(table.workspaceId, workspaceId), inArray(table.slug, slugs)),
-    });
+    const uniqueSlugs = Array.from(new Set(slugs));
+    const existingPermissions = await tx.query.permissions.findMany({
+      where: (table, { inArray, and, eq }) =>
+        and(eq(table.workspaceId, workspaceId), inArray(table.slug, uniqueSlugs)),
+    });

-    const permissions = slugs.map((slug) => {
+    const permissions = uniqueSlugs.map((slug) => {
       const existingPermission = existingPermissions.find((p) => p.slug === slug);
       if (existingPermission) {
         return existingPermission;
       }
       const permission: Permission = {
         id: newId("permission"),
         workspaceId,
         name: slug,
         slug: slug,
         description: null,
         updatedAtM: null,
         createdAtM: Date.now(),
       };
       newPermissions.push(permission);
       auditLogs.push({
         workspaceId,
         actor: { type: "user", id: userId },
         event: "permission.create",
         description: `Created ${permission.id}`,
         resources: [
           {
             type: "permission",
             id: permission.id,
             name: permission.slug,
           },
         ],
         context: {
           location: ctx.audit.location,
           userAgent: ctx.audit.userAgent,
         },
       });
       return permission;
     });

-    if (newPermissions.length) {
-      await tx.insert(schema.permissions).values(newPermissions);
-    }
+    if (newPermissions.length) {
+      await tx
+        .insert(schema.permissions)
+        .values(newPermissions)
+        .onDuplicateKeyUpdate({ set: { updatedAtM: Date.now() } });
+    }

826-831: Use UNAUTHORIZED for missing user context

NOT_FOUND is misleading when userId is absent.

-    if (!userId) {
-      throw new TRPCError({
-        code: "NOT_FOUND",
-        message: "UserId not found",
-      });
-    }
+    if (!userId) {
+      throw new TRPCError({ code: "UNAUTHORIZED", message: "User not authenticated" });
+    }

12-18: Align validation message with allowed characters

Regex allows asterisks (*) but the error message doesn’t mention them.

-      "Must be at least 3 characters long and only contain alphanumeric, colons, periods, dashes and underscores",
+      "Must be at least 3 characters long and only contain alphanumeric characters, colons, periods, dashes, underscores, and asterisks (*)",
apps/dashboard/lib/trpc/routers/workspace/changeName.ts (2)

12-16: Normalize and validate name (trim + charset) to prevent bypass and garbage input

Trim spaces and restrict characters; otherwise " abc " passes min length and odd Unicode can slip in.

-      name: z
-        .string()
-        .min(3, "Workspace names must contain at least 3 characters")
-        .max(50, "Workspace names must contain less than 50 characters"),
+      name: z
+        .string()
+        .transform((s) => s.trim().replace(/\s+/g, " "))
+        .min(3, "Workspace names must contain at least 3 characters")
+        .max(50, "Workspace names must contain less than 50 characters")
+        .regex(/^[\p{L}\p{N}\s._-]+$/u, "Only letters, numbers, spaces, ., _, and - are allowed"),

19-33: Error handling swallows root cause and uses nested catches

Inline .catch() blocks make control-flow harder to reason about and drop DB error details (e.g., unique constraint → map to CONFLICT).

-    await db
-      .transaction(async (tx) => {
+    try {
+      await db.transaction(async (tx) => {
         await tx
           .update(schema.workspaces)
           .set({ name: input.name })
           .where(eq(schema.workspaces.id, ctx.workspace.id));
         await insertAuditLogs(tx, { /* ... */ });
-        await authClient.updateOrg({ id: ctx.tenant.id, name: input.name });
-      })
-      .catch((_err) => {
-        throw new TRPCError({
-          code: "INTERNAL_SERVER_ERROR",
-          message:
-            "We are unable to update the workspace name. Please try again or contact support@unkey.dev",
-        });
-      });
+      });
+      await authClient.updateOrg({ id: ctx.tenant.id, name: input.name });
+    } catch (err: any) {
+      // TODO: detect unique violation from driver and throw CONFLICT instead
+      throw new TRPCError({
+        code: "INTERNAL_SERVER_ERROR",
+        message:
+          "We are unable to update the workspace name. Please try again or contact support@unkey.dev",
+      });
+    }

Also applies to: 57-63

apps/dashboard/lib/auth.ts (2)

29-40: Type/logic mismatch: orgId may be undefined but GetAuthResult requires string

With if (!orgId && !role), callers can receive orgId as undefined while the type promises string (unsafe; pages query by orgId).

Prefer guaranteeing orgId here, or relax the type. Minimal fix:

-  if (!authResult.orgId && !authResult.role) {
+  if (!authResult.orgId) {
     redirect("/new");
   }

If you must allow routes without org context, introduce a separate helper (e.g., getUserOrRedirect) and keep this one org-required.


53-61: Nit: stale comment and cast

Comment mentions “getAuth” but you now use getAuthOrRedirect. Also the cast on return can be avoided if the function guarantees non-null orgId.

-  const user = await auth.getUser(userId); // getAuth will redirect if there's no userId
+  const user = await auth.getUser(userId); // getAuthOrRedirect ensures userId exists
apps/dashboard/app/(app)/settings/billing/page.tsx (1)

30-35: Stripe/env consistency and resiliency

  • Confirm env names follow UNKEY_* standard via stripeEnv().
  • Consider retry/backoff for Stripe reads to handle transient failures when listing products/fetching subscriptions.

Also applies to: 46-49, 127-135

apps/dashboard/app/(app)/apis/[apiId]/actions.ts (1)

26-30: Remove redundant orgId check; getAuthOrRedirect already enforces it.

Simplify guard to only validate apiId.

-  const { orgId } = await getAuthOrRedirect();
-  if (!apiId || !orgId) {
-    console.error("fetchApiLayoutDataFromDb: apiId or orgId is missing");
+  const { orgId } = await getAuthOrRedirect();
+  if (!apiId) {
+    console.error("fetchApiLayoutDataFromDb: apiId is missing");
     notFound();
   }
apps/dashboard/app/(app)/identities/page.tsx (1)

32-39: Fix deleted-workspace handling and incorrect redirect.

  • Query should exclude soft-deleted workspaces (isNull(table.deletedAtM)) for consistency with Results.
  • Redirecting unauthenticated users to sign-in is fine, but here auth has already succeeded; if no workspace, redirect to /new (matches rest of app).
-  const workspace = await db.query.workspaces.findFirst({
-    where: (table, { eq }) => eq(table.orgId, orgId),
-  });
+  const workspace = await db.query.workspaces.findFirst({
+    where: (table, { and, eq, isNull }) => and(eq(table.orgId, orgId), isNull(table.deletedAtM)),
+  });
@@
-  if (!workspace) {
-    return redirect("/auth/sign-in");
-  }
+  if (!workspace) {
+    return redirect("/new");
+  }
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (2)

55-60: Harden Stripe portal creation with error handling.

Handle API errors to avoid bubbling a 500 and provide a user-facing message.

-  const { url } = await stripe.billingPortal.sessions.create({
-    customer: ws.stripeCustomerId,
-    return_url: `${baseUrl}/success`,
-  });
-  return redirect(url);
+  try {
+    const { url } = await stripe.billingPortal.sessions.create({
+      customer: ws.stripeCustomerId,
+      return_url: `${baseUrl}/success`,
+    });
+    return redirect(url);
+  } catch (err) {
+    console.error("Stripe portal session creation failed", err);
+    return (
+      <Empty>
+        <Empty.Title>Unable to open Stripe portal</Empty.Title>
+        <Empty.Description>Please try again later or contact support.</Empty.Description>
+      </Empty>
+    );
+  }

36-41: Correct Vercel env var in base-URL logic

  • Replace process.env.VERCEL_TARGET_ENV with process.env.VERCEL_ENV (or extend your Zod schema in apps/dashboard/lib/env.ts to include VERCEL_TARGET_ENV if you need custom environments) (vercel.com)
  • No change needed for Stripe env naming: stripeEnv() correctly expects STRIPE_SECRET_KEY, no UNKEY_ prefix required
apps/dashboard/app/integrations/vercel/callback/page.tsx (6)

48-51: Scope integration lookup to the authenticated workspace to prevent IDOR

Querying by configurationId alone allows cross-workspace access if a user guesses/obtains another config ID. Constrain by workspaceId.

Apply:

-  let integration = await db.query.vercelIntegrations.findFirst({
-    where: eq(schema.vercelIntegrations.id, props.searchParams.configurationId),
-  });
+  let integration =
+    props.searchParams.configurationId
+      ? await db.query.vercelIntegrations.findFirst({
+          where: (table, { and, eq }) =>
+            and(
+              eq(table.id, props.searchParams.configurationId),
+              eq(table.workspaceId, workspace.id),
+            ),
+        })
+      : null;

14-21: Make configurationId optional and validate search params

configurationId isn’t guaranteed on first-time installs; current typing may mask runtime absence. Also validate inputs.

Apply:

 type Props = {
   searchParams: {
     code?: string;
     next?: string;
-    configurationId: string;
+    configurationId?: string;
   };
 };

Optionally, add zod validation inside Page() to enforce code and a safe next path.


109-116: Do not expose Vercel access tokens to the client

Passing accessToken to a Client component leaks credentials to the browser. Keep tokens server-side and have client call your API.

Apply:

   return (
     <Client
       projects={projects.val}
       apis={workspace.apis}
-      returnUrl={props.searchParams.next}
+      returnUrl={safeReturnUrl(props.searchParams.next)}
       integrationId={integration.id}
-      accessToken={integration.accessToken}
       vercelTeamId={integration.vercelTeamId}
     />
   );

Add helper in this file:

function safeReturnUrl(next?: string) {
  if (!next) return "/settings/vercel";
  try {
    const u = new URL(next, "http://localhost"); // base ignored for path checks
    if (u.origin === "http://localhost" && u.pathname.startsWith("/")) return u.pathname + u.search + u.hash;
  } catch {}
  return "/settings/vercel";
}

77-95: Avoid leaking internal error context to end users

Rendering raw error/context may expose sensitive data. Log server-side; show generic UI message.

Apply:

-  if (projects.err) {
-    return (
-      <Empty>
-        <Empty.Title>Error</Empty.Title>
-        <Empty.Description>
-          We couldn't load your projects from Vercel. Please try again or contact support.
-        </Empty.Description>
-        <Code className="text-left">
-          {JSON.stringify(
-            {
-              message: projects.err.message,
-              context: projects.err.context,
-            },
-            null,
-            2,
-          )}
-        </Code>
-      </Empty>
-    );
-  }
+  if (projects.err) {
+    console.error("Vercel projects error", {
+      message: projects.err.message,
+      context: projects.err.context,
+    });
+    return (
+      <Empty>
+        <Empty.Title>Error</Empty.Title>
+        <Empty.Description>
+          We couldn't load your projects from Vercel. Please try again or contact support.
+        </Empty.Description>
+      </Empty>
+    );
+  }

28-35: Replace placeholder messages with actionable redirects

Return consistent UX: redirect to an error/retry page instead of “no code/next”.

I can wire a fallback route like /settings/vercel/error?reason=missing_params; want a quick diff?


69-71: Remove commented redirects or guard with a feature flag

Dead comments create confusion, especially in a deprecated file.

apps/dashboard/lib/auth/utils.ts (1)

38-43: Allow sign-out without requiring authentication

Calling requireAuth here can redirect before deleting the cookie, breaking idempotent sign-out flows.

Apply:

 export async function signOut(): Promise<void> {
-  await requireAuth();
-  //const signOutUrl = await auth.getSignOutUrl();
   await deleteCookie(UNKEY_SESSION_COOKIE);
   redirect("/auth/sign-in");
 }
apps/dashboard/lib/trpc/trpc.ts (1)

64-83: Re-throw TRPCError in requireOrgAdmin and swap middleware order in org routers

  • In apps/dashboard/lib/trpc/trpc.ts, adjust requireOrgAdmin to re-throw existing TRPCError and only wrap unexpected errors. Apply:
-export const requireOrgAdmin = t.middleware(async ({ next, ctx }) => {
-  try {
-    const isAdmin = ctx.tenant?.role === "admin";
-
-    if (!isAdmin) {
-      throw new TRPCError({
-        code: "FORBIDDEN",
-        message: "This action requires admin privileges.",
-      });
-    }
-
-    return next();
-  } catch (error) {
-    throw new TRPCError({
-      code: "INTERNAL_SERVER_ERROR",
-      message: "Failed to verify admin privilege.",
-      cause: error,
-    });
-  }
-});
+export const requireOrgAdmin = t.middleware(async ({ next, ctx }) => {
+  try {
+    const isAdmin = ctx.tenant?.role === "admin";
+    if (!isAdmin) {
+      throw new TRPCError({
+        code: "FORBIDDEN",
+        message: "This action requires admin privileges.",
+      });
+    }
+    return next();
+  } catch (error) {
+    if (error instanceof TRPCError) throw error;
+    throw new TRPCError({
+      code: "INTERNAL_SERVER_ERROR",
+      message: "Failed to verify admin privilege.",
+    });
+  }
+});
  • In apps/dashboard/lib/trpc/routers/org/*.ts (e.g. inviteMember.ts, updateMembership.ts, revokeInvitation.ts, removeMembership.ts, getInvitationList.ts), swap the .use order from
    .use(requireUser).use(requireOrgAdmin).use(requireOrgId)
    to
    .use(requireUser).use(requireOrgId).use(requireOrgAdmin)
    so ctx.orgId is set before checking admin role.
apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx (3)

54-56: Fix toast copy: “namespace” → “workspace”.

User-facing text is inconsistent with the feature.

-      toast.error("Failed to update namespace name", {
+      toast.error("Failed to update workspace name", {
         description: err.message,
       });

93-95: Normalize comparisons and mirror schema limits in the input.

Whitespace-only edits can bypass the disabled check; also mirror max(50) in the control.

-            className="w-[21rem]"
+            id="workspaceName"
+            className="w-[21rem]"
             placeholder="Workspace Name"
             minLength={3}
+            maxLength={50}
             error={errors.workspaceName?.message}
             {...register("workspaceName")}
-              updateName.isLoading || isSubmitting || !isValid || watch("workspaceName") === name
+              updateName.isLoading ||
+              isSubmitting ||
+              !isValid ||
+              (watch("workspaceName")?.trim() ?? "") === name.trim()

Also applies to: 83-88


45-53: Use trimmed value when updating local state after success.

-      setName(watch("workspaceName"));
+      setName(watch("workspaceName")?.trim() ?? name);
apps/dashboard/app/(app)/settings/team/invitations.tsx (1)

76-89: Prevent double-submit and fix log message.

Disable the button while the mutation is in-flight and correct the console message.

-                <Button
+                <Button
                   variant="destructive"
-                  onClick={async () => {
+                  disabled={revokeInvitation.isLoading}
+                  loading={revokeInvitation.isLoading}
+                  onClick={async () => {
                     try {
                       await revokeInvitation.mutateAsync({
                         invitationId: invitation.id,
                       });
                     } catch (error) {
-                      console.error("Error removing member:", error);
+                      console.error("Error revoking invitation:", error);
                     }
                   }}
                 >
                   Revoke
                 </Button>
apps/dashboard/app/(app)/settings/team/client.tsx (1)

39-41: Derive membership from the fetched organization, not user.orgId.

Reduces coupling and avoids stale/missing user.orgId.

-  const currentOrgMembership = userMemberships?.find(
-    (membership) => membership.organization.id === user?.orgId,
-  );
+  const currentOrgMembership = userMemberships?.find(
+    (m) => m.organization.id === organization.id,
+  );
apps/dashboard/app/(app)/settings/team/members.tsx (1)

109-117: Disable action while removal is pending.

Prevents duplicate submissions and accidental double removals.

-                  trigger={(onClick) => <Button onClick={onClick}>Remove</Button>}
+                  trigger={(onClick) => (
+                    <Button onClick={onClick} disabled={removeMember.isLoading}>
+                      Remove
+                    </Button>
+                  )}
apps/dashboard/app/(app)/layout.tsx (1)

16-21: Guard against undefined orgId before DB filter

getAuthOrRedirect may return no orgId (e.g., role present but org not selected). Passing undefined into eq(table.orgId, orgId) can error in the query builder. Short-circuit to the creation flow first.

Apply:

 export default async function Layout({ children }: LayoutProps) {
   const { orgId, impersonator } = await getAuthOrRedirect();
 
+  if (!orgId) {
+    return redirect("/new");
+  }
+
   const isImpersonator = !!impersonator;
   const workspace = await db.query.workspaces.findFirst({
     where: (table, { and, eq, isNull }) => and(eq(table.orgId, orgId), isNull(table.deletedAtM)),
apps/dashboard/lib/trpc/routers/org/removeMembership.ts (1)

15-25: Enforce same-org membership before deletion

Currently any membershipId can be deleted without verifying it belongs to ctx.orgId. Add an org ownership check to prevent cross-org deletion.

-  .mutation(async ({ input }) => {
+  .mutation(async ({ ctx, input }) => {
     try {
+      const list = await authProvider.getOrganizationMemberList(ctx.orgId);
+      const belongsToOrg = list.data.some((m) => m.id === input.membershipId);
+      if (!belongsToOrg) {
+        throw new TRPCError({
+          code: "FORBIDDEN",
+          message: "Membership does not belong to this organization.",
+        });
+      }
       return await authProvider.removeMembership(input.membershipId);
     } catch (error) {
       throw new TRPCError({
         code: "INTERNAL_SERVER_ERROR",
         message: "Failed to remove membership",
         cause: error,
       });
     }
   });
apps/dashboard/lib/trpc/routers/org/updateMembership.ts (1)

16-21: Verify membership belongs to current org before updating.

Prevent cross-org updates by confirming membershipId’s org matches ctx.orgId (provider precheck or post-fetch).

apps/dashboard/lib/trpc/routers/org/getInvitationList.ts (1)

9-18: Align error messages with operation.

Console and TRPC messages mention “member list” but this fetches invitations.

-  console.error("Error retrieving organization member list:", error);
+  console.error("Error retrieving organization invitation list:", error);
 ...
-  message: "Failed to fetch organization member list",
+  message: "Failed to fetch organization invitation list",
♻️ Duplicate comments (5)
apps/dashboard/lib/trpc/routers/rbac.ts (1)

40-49: Stop re-querying workspace; use ctx.workspace.id directly (security + perf)

Multiple endpoints fetch the workspace by orgId; prefer scoping by ctx.workspace.id per prior guidance to prevent cross-workspace leakage and cut DB roundtrips.

Example for Lines 40-49:

-      const workspace = await db.query.workspaces.findFirst({
-        where: (table, { and, eq, isNull }) =>
-          and(eq(table.orgId, ctx.tenant.id), isNull(table.deletedAtM)),
-      });
-      if (!workspace) {
-        throw new TRPCError({ code: "NOT_FOUND", message: "workspace not found" });
-      }
+      const workspaceId = ctx.workspace.id;

Then use workspaceId in subsequent queries and audit logs.

Also applies to: 95-106, 179-197, 259-267, 312-323, 392-401, 424-434, 514-529, 565-573, 621-631, 678-686, 742-750

apps/dashboard/lib/trpc/routers/org/updateMembership.ts (1)

4-10: Same ctx merge bug impacts this route.

Without merging ctx in middlewares, ctx.workspace is lost and requireOrgId fails.

See fix diff provided in inviteMember.ts comment (trpc.ts).

apps/dashboard/lib/trpc/routers/org/getInvitationList.ts (1)

3-10: Ctx merge issue applies here too.

requireOrgId won’t find ctx.workspace unless middlewares merge ctx.

Refer to trpc.ts fix in inviteMember.ts comment.

apps/dashboard/lib/trpc/routers/org/getOrg.ts (1)

3-9: Ctx merge bug affects this route as well.

requireOrgId depends on ctx.workspace; ensure middlewares merge ctx.

See trpc.ts fix provided earlier.

apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts (1)

4-10: Ctx merge issue present here.

Same middleware ctx replacement will break requireOrgId.

Use the trpc.ts merge fix.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3b72d and f54b5ec.

📒 Files selected for processing (35)
  • apps/dashboard/app/(app)/apis/[apiId]/actions.ts (2 hunks)
  • apps/dashboard/app/(app)/audit/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/gateway-new/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/identities/page.tsx (3 hunks)
  • apps/dashboard/app/(app)/layout.tsx (2 hunks)
  • apps/dashboard/app/(app)/logs/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/billing/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/general/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/team/client.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/team/invitations.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/team/invite.tsx (0 hunks)
  • apps/dashboard/app/(app)/settings/team/members.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/team/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx (0 hunks)
  • apps/dashboard/app/(app)/settings/vercel/page.tsx (2 hunks)
  • apps/dashboard/app/integrations/vercel/callback/page.tsx (2 hunks)
  • apps/dashboard/lib/auth.ts (2 hunks)
  • apps/dashboard/lib/auth/utils.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/getInvitationList.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/getOrg.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/inviteMember.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/removeMembership.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/rbac.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/rbac/upsertPermission.ts (0 hunks)
  • apps/dashboard/lib/trpc/routers/workspace/changeName.ts (1 hunks)
  • apps/dashboard/lib/trpc/trpc.ts (1 hunks)
💤 Files with no reviewable changes (3)
  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx
  • apps/dashboard/lib/trpc/routers/rbac/upsertPermission.ts
  • apps/dashboard/app/(app)/settings/team/invite.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • apps/dashboard/app/(app)/settings/general/page.tsx
  • apps/dashboard/app/(app)/apis/[apiId]/actions.ts
  • apps/dashboard/app/(app)/audit/page.tsx
  • apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts
  • apps/dashboard/app/(app)/settings/team/members.tsx
  • apps/dashboard/app/(app)/settings/team/client.tsx
  • apps/dashboard/lib/trpc/routers/rbac.ts
  • apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx
  • apps/dashboard/app/(app)/logs/page.tsx
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/layout.tsx
  • apps/dashboard/app/(app)/settings/team/invitations.tsx
  • apps/dashboard/app/(app)/settings/team/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/lib/trpc/trpc.ts
  • apps/dashboard/lib/trpc/routers/workspace/changeName.ts
  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/settings/vercel/page.tsx
  • apps/dashboard/app/(app)/settings/billing/page.tsx
  • apps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.ts
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx
  • apps/dashboard/app/(app)/projects/page.tsx
  • apps/dashboard/app/integrations/vercel/callback/page.tsx
  • apps/dashboard/lib/trpc/routers/org/getOrg.ts
  • apps/dashboard/lib/trpc/routers/org/inviteMember.ts
  • apps/dashboard/lib/auth/utils.ts
  • apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
  • apps/dashboard/lib/auth.ts
  • apps/dashboard/lib/trpc/routers/org/removeMembership.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • apps/dashboard/app/(app)/settings/general/page.tsx
  • apps/dashboard/app/(app)/apis/[apiId]/actions.ts
  • apps/dashboard/app/(app)/audit/page.tsx
  • apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts
  • apps/dashboard/app/(app)/settings/team/members.tsx
  • apps/dashboard/app/(app)/settings/team/client.tsx
  • apps/dashboard/lib/trpc/routers/rbac.ts
  • apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx
  • apps/dashboard/app/(app)/logs/page.tsx
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/layout.tsx
  • apps/dashboard/app/(app)/settings/team/invitations.tsx
  • apps/dashboard/app/(app)/settings/team/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/lib/trpc/trpc.ts
  • apps/dashboard/lib/trpc/routers/workspace/changeName.ts
  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/settings/vercel/page.tsx
  • apps/dashboard/app/(app)/settings/billing/page.tsx
  • apps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.ts
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx
  • apps/dashboard/app/(app)/projects/page.tsx
  • apps/dashboard/app/integrations/vercel/callback/page.tsx
  • apps/dashboard/lib/trpc/routers/org/getOrg.ts
  • apps/dashboard/lib/trpc/routers/org/inviteMember.ts
  • apps/dashboard/lib/auth/utils.ts
  • apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
  • apps/dashboard/lib/auth.ts
  • apps/dashboard/lib/trpc/routers/org/removeMembership.ts
**/*.{env,js,ts,go}

📄 CodeRabbit inference engine (CLAUDE.md)

All environment variables must follow the format: UNKEY_<SERVICE_NAME>_VARNAME

Files:

  • apps/dashboard/app/(app)/apis/[apiId]/actions.ts
  • apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts
  • apps/dashboard/lib/trpc/routers/rbac.ts
  • apps/dashboard/lib/trpc/trpc.ts
  • apps/dashboard/lib/trpc/routers/workspace/changeName.ts
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.ts
  • apps/dashboard/lib/trpc/routers/org/getOrg.ts
  • apps/dashboard/lib/trpc/routers/org/inviteMember.ts
  • apps/dashboard/lib/auth/utils.ts
  • apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
  • apps/dashboard/lib/auth.ts
  • apps/dashboard/lib/trpc/routers/org/removeMembership.ts
🧠 Learnings (4)
📚 Learning: 2025-04-30T15:25:33.917Z
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)

Applied to files:

  • apps/dashboard/app/(app)/settings/general/page.tsx
  • apps/dashboard/app/(app)/apis/[apiId]/actions.ts
  • apps/dashboard/app/(app)/audit/page.tsx
  • apps/dashboard/app/(app)/logs/page.tsx
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/layout.tsx
  • apps/dashboard/app/(app)/settings/team/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/settings/vercel/page.tsx
  • apps/dashboard/app/(app)/settings/billing/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx
  • apps/dashboard/app/(app)/projects/page.tsx
  • apps/dashboard/app/integrations/vercel/callback/page.tsx
  • apps/dashboard/lib/auth/utils.ts
  • apps/dashboard/lib/auth.ts
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: When querying or updating namespaces in the Unkey dashboard, always scope the operations to the current workspace using `eq(table.workspaceId, ctx.workspace.id)` to prevent cross-workspace access.

Applied to files:

  • apps/dashboard/lib/trpc/routers/rbac.ts
  • apps/dashboard/lib/trpc/routers/workspace/changeName.ts
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/lib/trpc/routers/rbac.ts
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx
🧬 Code graph analysis (26)
apps/dashboard/app/(app)/settings/general/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/apis/[apiId]/actions.ts (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/audit/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts (1)
apps/dashboard/lib/trpc/trpc.ts (4)
  • t (8-8)
  • requireUser (10-21)
  • requireOrgAdmin (64-83)
  • requireOrgId (48-62)
apps/dashboard/lib/trpc/routers/rbac.ts (1)
apps/dashboard/lib/env.ts (1)
  • env (3-49)
apps/dashboard/app/(app)/logs/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/gateway-new/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/layout.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/settings/team/invitations.tsx (1)
apps/dashboard/lib/trpc/server.ts (1)
  • trpc (7-14)
apps/dashboard/app/(app)/settings/team/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/identities/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/lib/trpc/routers/org/updateMembership.ts (2)
apps/dashboard/lib/auth/workos.ts (1)
  • updateMembership (412-446)
apps/dashboard/lib/trpc/trpc.ts (4)
  • t (8-8)
  • requireUser (10-21)
  • requireOrgAdmin (64-83)
  • requireOrgId (48-62)
apps/dashboard/app/(app)/identities/[identityId]/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/settings/vercel/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/settings/billing/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.ts (2)
apps/dashboard/lib/auth/workos.ts (1)
  • getOrganizationMemberList (367-410)
apps/dashboard/lib/trpc/trpc.ts (2)
  • t (8-8)
  • requireUser (10-21)
apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/projects/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/integrations/vercel/callback/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/lib/trpc/routers/org/getOrg.ts (2)
apps/dashboard/lib/auth/workos.ts (1)
  • getOrg (253-264)
apps/dashboard/lib/trpc/trpc.ts (3)
  • t (8-8)
  • requireUser (10-21)
  • requireOrgId (48-62)
apps/dashboard/lib/trpc/routers/org/inviteMember.ts (2)
apps/dashboard/lib/auth/workos.ts (1)
  • inviteMember (461-487)
apps/dashboard/lib/trpc/trpc.ts (4)
  • t (8-8)
  • requireUser (10-21)
  • requireOrgAdmin (64-83)
  • requireOrgId (48-62)
apps/dashboard/lib/auth/utils.ts (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/lib/trpc/routers/org/getInvitationList.ts (2)
apps/dashboard/lib/auth/workos.ts (1)
  • getInvitationList (489-512)
apps/dashboard/lib/trpc/trpc.ts (4)
  • t (8-8)
  • requireUser (10-21)
  • requireOrgAdmin (64-83)
  • requireOrgId (48-62)
apps/dashboard/lib/trpc/routers/org/removeMembership.ts (2)
apps/dashboard/lib/auth/workos.ts (1)
  • removeMembership (448-458)
apps/dashboard/lib/trpc/trpc.ts (4)
  • t (8-8)
  • requireUser (10-21)
  • requireOrgAdmin (64-83)
  • requireOrgId (48-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
🔇 Additional comments (26)
apps/dashboard/lib/trpc/routers/rbac.ts (1)

806-815: Do not remove system workspace bypass globally in upsertPermissions. This helper is called by the root‐key flows in apps/dashboard/lib/trpc/routers/key/createRootKey.ts (≈lines 139–143) and .../updateRootKeyPermissions.ts (≈lines 72–76) to write into the system workspace; stripping out the UNKEY_WORKSPACE_ID branch will break those operations. Restrict any bypass removal to the RBAC use‐case or extract a dedicated method.

Likely an incorrect or invalid review comment.

apps/dashboard/app/(app)/settings/general/page.tsx (1)

1-1: Remove redundant orgId guard
The SettingsPage already redirects to /new when workspace is falsy (which covers a missing orgId), so adding an extra if (!orgId) check isn’t required.

Likely an incorrect or invalid review comment.

apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (1)

19-21: Verify env var naming matches repo standard

Your guidelines require UNKEY__VARNAME. stripeEnv() exposes STRIPE_SECRET_KEY. Confirm these are backed by UNKEY_STRIPE_* names or align them.

Also applies to: 31-35

apps/dashboard/app/(app)/settings/billing/page.tsx (1)

1-1: Require orgId before DB/Stripe work
Add a guard immediately after const { orgId } = await getAuthOrRedirect(); to handle missing orgId (e.g.

if (!orgId) return redirect("/new");

) in:

  • apps/dashboard/app/(app)/settings/billing/page.tsx (around lines 18–20)
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (around lines 11–13)
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (around lines 11–13)
apps/dashboard/app/(app)/apis/[apiId]/actions.ts (1)

1-1: Auth helper swap to getAuthOrRedirect looks correct.

apps/dashboard/app/(app)/identities/page.tsx (2)

4-4: Import switch to getAuthOrRedirect is fine.


69-69: Results auth usage is correct and consistent.

apps/dashboard/app/(app)/logs/page.tsx (2)

1-1: Auth change to getAuthOrRedirect looks good; downstream usage unchanged.

Also applies to: 11-11


17-19: Confirm UX: notFound vs redirect('/new').

Other pages redirect to /new when no workspace; this page 404s. If intentional, ignore; otherwise align for consistency.

apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (1)

1-1: Auth refactor LGTM.

Also applies to: 11-11

apps/dashboard/app/(app)/page.tsx (1)

1-1: Auth change and redirects look consistent with the rest of the app.

Also applies to: 8-8

apps/dashboard/app/(app)/settings/vercel/page.tsx (2)

9-9: Switch to getAuthOrRedirect looks correct.

Import path aligns with the new redirecting helper used across the dashboard.


9-9: No lingering getAuth imports

Search confirmed no import { getAuth } from "@/lib/auth" in any .ts/.tsx files; all existing getAuth calls reference the base helper (@/lib/auth/get-auth).

apps/dashboard/app/(app)/projects/page.tsx (1)

2-2: Import rename to getAuthOrRedirect is consistent.

apps/dashboard/app/(app)/gateway-new/page.tsx (1)

11-11: Auth helper swap LGTM.

apps/dashboard/app/(app)/identities/[identityId]/page.tsx (1)

13-13: Auth import update is correct.

apps/dashboard/app/(app)/audit/page.tsx (1)

7-7: Auth helper change looks good.

apps/dashboard/app/integrations/vercel/callback/page.tsx (2)

6-6: Correct auth import aligning with redirect semantics

Switching to getAuthOrRedirect from "@/lib/auth" is the right call for pages that should auto-redirect unauthenticated users.


36-36: Auth usage LGTM; no extra guard needed

getAuthOrRedirect already redirects on missing user/org in most cases; no additional checks required here.

apps/dashboard/lib/auth/utils.ts (1)

4-4: Auth import aligned with new helper

Consistent with the migration to getAuthOrRedirect.

apps/dashboard/app/(app)/settings/team/invitations.tsx (1)

24-24: LGTM on switching to ctx.orgId-backed query.

Query args removal aligns with requireOrgId middleware; cache key usage via utils.org.invitations.list.invalidate() is consistent.

apps/dashboard/app/(app)/settings/team/client.tsx (1)

31-35: LGTM on removing orgId input.

Using ctx.orgId-backed endpoint is correct; keep enabled: !!user.

apps/dashboard/app/(app)/settings/team/members.tsx (2)

27-28: LGTM on removing orgId argument.

Matches server-side ctx.orgId handling; invalidation key aligns.


26-36: Ensure server prevents removing the last admin.

UI disallows self-removal but not last-admin removal. Confirm backend enforces this invariant for org integrity.

apps/dashboard/app/(app)/layout.tsx (1)

4-4: Auth helper migration LGTM

Importing and using getAuthOrRedirect from "@/lib/auth" is aligned with the new redirect-on-miss behavior.

apps/dashboard/lib/trpc/routers/org/getOrg.ts (1)

8-11: LGTM: context-derived orgId usage.

Using ctx.orgId removes client-controlled orgId. Good hardening.

@perkinsjr perkinsjr marked this pull request as draft September 1, 2025 12:21
- Export GetAuthResult type from auth.ts for better type safety
- Simplify requireAuth() helper to use consistent return type
- Add orgId validation middleware to tRPC procedures
- Add null orgId checks and redirects to dashboard pages
- Update error code from BAD_REQUEST to NOT_FOUND for missing orgId
- Standardize organization member list handling across routers
- Fix typo in error message ("memberlist" → "member list")
@vercel vercel bot temporarily deployed to Preview – dashboard September 1, 2025 13:00 Inactive
The role schema is a string by default but we should validate it to make
sure it fits the enum.
@vercel vercel bot temporarily deployed to Preview – dashboard September 1, 2025 13:08 Inactive
@perkinsjr
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
apps/dashboard/lib/auth.ts (3)

7-15: Type guarantees don’t match runtime; orgId can be absent. Make it optional.
getAuthOrRedirect may return a session with role but no orgId, yet the type requires orgId. The cast “as GetAuthResult” hides this and can leak undefined downstream.

Apply this diff:

-export type GetAuthResult = {
-  userId: string;
-  orgId: string;
-  role: string;
-  impersonator?: {
-    email: string;
-    reason?: string | null;
-  };
-};
+export type GetAuthResult = {
+  userId: string;
+  orgId?: string;
+  role: string;
+  impersonator?: {
+    email: string;
+    reason?: string | null;
+  };
+};

29-40: Param name mismatch with JSDoc; also consider a helper to dedupe org guards.

  • Rename req → _req to match the docs.
  • Many pages now repeat “if (!orgId) redirect('/new')”. Consider a small helper wrapping getAuthOrRedirect for org-required pages to reduce duplication.

Apply this diff for the param rename:

-export async function getAuthOrRedirect(req?: NextRequest): Promise<GetAuthResult> {
-  const authResult = await getBaseAuth(req);
+export async function getAuthOrRedirect(_req?: NextRequest): Promise<GetAuthResult> {
+  const authResult = await getBaseAuth(_req);

Optional helper (add in this module or utils):

export async function getOrgOrRedirect() {
  const auth = await getAuthOrRedirect();
  if (!auth.orgId) redirect("/new");
  return auth as GetAuthResult & { orgId: string };
}

54-61: Centralize orgId validation in getAuthOrRedirect
In apps/dashboard/lib/auth.ts (lines 54–61):

  • Update the inline comment on auth.getUser:
    - const user = await auth.getUser(userId); // getAuth will redirect if there's no userId
    + const user = await auth.getUser(userId); // getAuthOrRedirect already ensures userId exists
  • Immediately after destructuring, enforce orgId presence:
    if (!orgId) redirect("/new");

This guarantees downstream code never receives an undefined orgId (so you can remove or simplify per-page guards).

apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx (2)

54-56: Fix user-facing copy (“namespace” → “workspace”).

-      toast.error("Failed to update namespace name", {
+      toast.error("Failed to update workspace name", {
         description: err.message,
       });

92-94: Prevent Save on whitespace-only edits.

Compare trimmed value to avoid enabling Save when only spaces changed.

-            disabled={
-              updateName.isLoading || isSubmitting || !isValid || watch("workspaceName") === name
-            }
+            disabled={
+              updateName.isLoading ||
+              isSubmitting ||
+              !isValid ||
+              watch("workspaceName")?.trim() === name
+            }
apps/dashboard/app/(app)/settings/team/role-switcher.tsx (1)

69-71: Self-change guard compares mismatched IDs.

member.id is a membershipId (used in mutation), but you compare against user.id (a userId). Use userMembership.id to correctly block self role changes.

-            disabled={(Boolean(user) && member.id === user?.id) || updateMember.isLoading}
+            disabled={(Boolean(userMembership) && member.id === userMembership.id) || updateMember.isLoading}
apps/dashboard/lib/trpc/routers/org/updateMembership.ts (1)

16-29: Scope enforcement: avoid cross-org membership edits

The mutation doesn’t use orgId, so updates could target a membership outside the current org unless the provider enforces scope. Pass ctx.orgId to the provider or pre-validate ownership.

Option A (preferred if provider supports scoping):

-.mutation(async ({ input }) => {
+.mutation(async ({ ctx, input }) => {
   try {
-    return await authProvider.updateMembership({
+    return await authProvider.updateMembership({
       membershipId: input.membershipId,
       role: input.role,
+      orgId: ctx.orgId,
     });

Option B (provider doesn’t accept orgId): fetch membership → assert membership.orgId === ctx.orgId → then update. I can draft this if you confirm the provider API.

apps/dashboard/lib/trpc/trpc.ts (1)

64-83: Bug: FORBIDDEN is being swallowed into 500

The catch wraps TRPCError(FORBIDDEN) into INTERNAL_SERVER_ERROR. Re-throw known TRPCErrors and only wrap unknowns.

 export const requireOrgAdmin = t.middleware(async ({ next, ctx }) => {
-  try {
+  try {
     const isAdmin = ctx.tenant?.role === "admin";
     if (!isAdmin) {
       throw new TRPCError({
         code: "FORBIDDEN",
         message: "This action requires admin privileges.",
       });
     }
     return next();
-  } catch (error) {
-    throw new TRPCError({
-      code: "INTERNAL_SERVER_ERROR",
-      message: "Failed to verify admin privilege.",
-      cause: error,
-    });
-  }
+  } catch (error) {
+    if (error instanceof TRPCError) {
+      throw error;
+    }
+    throw new TRPCError({
+      code: "INTERNAL_SERVER_ERROR",
+      message: "Failed to verify admin privilege.",
+      cause: error,
+    });
+  }
 });
♻️ Duplicate comments (10)
apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (1)

11-14: Added orgId guard — resolves prior suggestion.

apps/dashboard/app/(app)/identities/[identityId]/page.tsx (1)

29-32: Org guard added before access check — good.
Prevents undefined comparisons and aligns with onboarding flow.

apps/dashboard/app/(app)/gateway-new/page.tsx (1)

20-23: Defensive orgId check before upserting — good.
Avoids inserting with undefined orgId.

apps/dashboard/app/(app)/audit/page.tsx (1)

10-13: Guard resolves prior undefined-orgId risk; nit on return.

Matches the earlier suggestion; safe before getWorkspace. Consider removing return as redirect throws.

-  if (!orgId) {
-    return redirect("/new");
-  }
+  if (!orgId) {
+    redirect("/new");
+  }
apps/dashboard/app/(app)/settings/vercel/page.tsx (1)

29-32: Guard prevents eq(undefined) and aligns with prior review; nit on return.

Same pattern as other pages; you can omit return.

-  if (!orgId) {
-    return redirect("/new");
-  }
+  if (!orgId) {
+    redirect("/new");
+  }
apps/dashboard/app/(app)/settings/team/page.tsx (1)

10-13: Guard added correctly; resolves prior risk.

Using getAuthOrRedirect with an explicit orgId check + redirect prevents undefined org queries.

apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx (1)

27-29: Tighten max-length message.

Use “at most” for precision.

-      .max(50, {
-        message: "Workspace name must be less than 50 characters long",
-      }),
+      .max(50, { message: "Workspace name must be at most 50 characters long" }),
apps/dashboard/lib/trpc/routers/org/inviteMember.ts (1)

12-14: Harden input: validate and normalize email.

Reject invalid emails and normalize case/whitespace.

   .input(
     z.object({
-      email: z.string(),
+      email: z.string().trim().toLowerCase().email(),
       role: z.enum(["basic_member", "admin"]),
     }),
   )
apps/dashboard/lib/trpc/trpc.ts (1)

48-62: Document dependency on requireWorkspace

Add a brief note so future callers order middlewares correctly.

-export const requireOrgId = t.middleware(({ next, ctx }) => {
+// Requires requireWorkspace to run earlier; derives orgId from ctx.workspace.
+export const requireOrgId = t.middleware(({ next, ctx }) => {
   const orgId = ctx.workspace?.orgId;
   if (!orgId) {
     throw new TRPCError({
       code: "NOT_FOUND",
-      message: "Organization ID not found",
+      message: "Organization ID not found",
     });
   }
   return next({
     ctx: {
       orgId,
     },
   });
 });
apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts (1)

15-25: Enforce org scoping on revocation

Currently revokes by id only; must ensure the invitation belongs to ctx.orgId or pass orgId to the provider to prevent cross-org revocation.

Option A (provider accepts orgId):

-.mutation(async ({ input }) => {
+.mutation(async ({ ctx, input }) => {
   try {
-    return await authProvider.revokeOrgInvitation(input.invitationId);
+    return await authProvider.revokeOrgInvitation({
+      invitationId: input.invitationId,
+      orgId: ctx.orgId,
+    });

Option B (provider does not accept orgId): fetch invitation → compare invitation.orgId === ctx.orgId → revoke; otherwise throw FORBIDDEN. I can wire this up if you confirm available provider methods.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f54b5ec and 9647642.

📒 Files selected for processing (18)
  • apps/dashboard/app/(app)/audit/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/gateway-new/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx (3 hunks)
  • apps/dashboard/app/(app)/projects/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/billing/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/general/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/team/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/vercel/page.tsx (2 hunks)
  • apps/dashboard/lib/auth.ts (3 hunks)
  • apps/dashboard/lib/auth/utils.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/inviteMember.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts (1 hunks)
  • apps/dashboard/lib/trpc/trpc.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/lib/trpc/routers/org/removeMembership.ts:7-10
Timestamp: 2025-09-01T12:51:59.873Z
Learning: In the Unkey dashboard tRPC setup, the requireOrgId middleware preserves existing context (user, tenant, workspace) when adding orgId to the context, so concerns about context overwriting are unfounded when this middleware is used.
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx
📚 Learning: 2025-04-30T15:25:33.917Z
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)

Applied to files:

  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/settings/billing/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx
  • apps/dashboard/app/(app)/settings/vercel/page.tsx
  • apps/dashboard/app/(app)/settings/team/page.tsx
  • apps/dashboard/app/(app)/settings/general/page.tsx
  • apps/dashboard/lib/auth/utils.ts
  • apps/dashboard/lib/auth.ts
  • apps/dashboard/app/(app)/projects/page.tsx
📚 Learning: 2024-10-08T15:33:04.290Z
Learnt from: AkshayBandi027
PR: unkeyed/unkey#2215
File: apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx:28-29
Timestamp: 2024-10-08T15:33:04.290Z
Learning: In `authorization/roles/[roleId]/update-role.tsx`, the tag `role-${role.id}` is revalidated after updating a role to ensure that the caching mechanism is properly handled for roles.

Applied to files:

  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
📚 Learning: 2025-06-19T11:48:05.070Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3324
File: apps/dashboard/app/(app)/authorization/roles/components/table/components/actions/keys-table-action.popover.constants.tsx:17-18
Timestamp: 2025-06-19T11:48:05.070Z
Learning: In the authorization roles refactor, the RoleBasic type uses `roleId` as the property name for the role identifier, not `id`. This is consistent throughout the codebase in apps/dashboard/lib/trpc/routers/authorization/roles/query.ts.

Applied to files:

  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
📚 Learning: 2025-09-01T12:51:59.873Z
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/lib/trpc/routers/org/removeMembership.ts:7-10
Timestamp: 2025-09-01T12:51:59.873Z
Learning: In the Unkey dashboard tRPC setup, the requireOrgId middleware preserves existing context (user, tenant, workspace) when adding orgId to the context, so concerns about context overwriting are unfounded when this middleware is used.

Applied to files:

  • apps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.ts
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/lib/trpc/routers/org/inviteMember.ts
  • apps/dashboard/lib/trpc/trpc.ts
🔇 Additional comments (19)
apps/dashboard/app/(app)/settings/billing/page.tsx (2)

1-1: Switched to getAuthOrRedirect — consistent with new flow.
Import change is correct and aligns with central redirect behavior.


18-21: Early redirect on missing orgId prevents invalid queries.
Good guard before querying by orgId.

apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (1)

1-1: Import updated to getAuthOrRedirect — correct.

apps/dashboard/app/(app)/identities/[identityId]/page.tsx (2)

1-1: Added redirect import — needed for the new guard.


13-13: Switch to getAuthOrRedirect — consistent with the new auth flow.

apps/dashboard/app/(app)/gateway-new/page.tsx (1)

11-11: getAuthOrRedirect import — correct for centralized redirects.

apps/dashboard/app/(app)/projects/page.tsx (1)

2-4: Auth import swap looks correct.

Using getAuthOrRedirect and importing redirect/notFound aligns with the new flow.

apps/dashboard/app/(app)/audit/page.tsx (1)

7-8: Auth import change is consistent with the PR.

apps/dashboard/app/(app)/settings/vercel/page.tsx (1)

9-9: Imports wired correctly for new auth and control flow.

getAuthOrRedirect plus notFound/redirect is the intended setup.

Also applies to: 16-16

apps/dashboard/lib/auth/utils.ts (1)

4-4: Simplified requireAuth with correct typing — LGTM.

Returning Promise and delegating to getAuthOrRedirect removes redundant logic and standardizes types.

Also applies to: 10-12

apps/dashboard/app/(app)/settings/general/page.tsx (1)

1-1: Auth import migration is correct.

apps/dashboard/app/(app)/settings/general/update-workspace-name.tsx (1)

65-65: Good: server-context payload only.

Sending only { name } aligns with ctx.workspace.id usage server-side.

apps/dashboard/app/(app)/settings/team/role-switcher.tsx (1)

16-19: Solid: runtime role validation via zod.

Enum + safeParse blocks invalid roles before mutation.

Also applies to: 47-51, 56-56

apps/dashboard/lib/trpc/routers/org/inviteMember.ts (1)

6-9: Middleware order and ctx.orgId usage look correct.

requireOrgId before requireOrgAdmin ensures org context is available for admin checks; passing ctx.orgId to provider aligns with the new contract.

Also applies to: 21-22

apps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.ts (1)

5-11: Correctly deriving orgId from middleware.

Chain with requireOrgId and using ctx.orgId removes tenant/user ambiguity and matches the PR’s auth model.

Also applies to: 15-15

apps/dashboard/lib/trpc/routers/org/updateMembership.ts (2)

13-13: Good: role constrained to enum

Confining to explicit roles blocks invalid values.


7-9: Add requireWorkspace before requireOrgId

requireOrgId reads ctx.workspace?.orgId; guard the chain with requireWorkspace first.

 export const updateMembership = t.procedure
   .use(requireUser)
+  .use(requireWorkspace)
   .use(requireOrgId)
   .use(requireOrgAdmin)

And update imports:

-import { requireOrgAdmin, requireOrgId, requireUser, t } from "../../trpc";
+import { requireOrgAdmin, requireOrgId, requireUser, requireWorkspace, t } from "../../trpc";
⛔ Skipped due to learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/lib/trpc/routers/org/removeMembership.ts:7-10
Timestamp: 2025-09-01T12:51:59.873Z
Learning: In the Unkey dashboard tRPC setup, the requireOrgId middleware preserves existing context (user, tenant, workspace) when adding orgId to the context, so concerns about context overwriting are unfounded when this middleware is used.
apps/dashboard/lib/trpc/trpc.ts (1)

66-73: Include “owner” in the org-admin check if owners should have admin privileges
requireOrgAdmin currently only allows tenant.role === "admin", but new tenants default to role "owner", which this middleware will block. Confirm whether owners should be granted admin rights and, if so, update the check to include "owner".

apps/dashboard/lib/trpc/routers/org/revokeInvitation.ts (1)

11-13: Good: basic invitationId validation added

Non-empty constraint is a step up. Consider z.string().uuid() or .cuid2() if applicable.

Removed the unused return redirects.

Added some more saftey to the zod Schema to make sure it loads
correctly.
@vercel vercel bot temporarily deployed to Preview – dashboard September 1, 2025 16:48 Inactive
@perkinsjr perkinsjr marked this pull request as ready for review September 2, 2025 11:46
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (1)

59-59: Nit: drop unnecessary return before redirect for consistency.

-  return redirect(url);
+  redirect(url);
apps/dashboard/app/(app)/settings/vercel/page.tsx (1)

84-98: Avoid dumping raw error details to users.

JSON-stringifying the Vercel error may leak internals. Consider a user-friendly message and logging the full error server-side only.

-        <Empty.Description>
-          <Code className="text-left">{JSON.stringify(err, null, 2)}</Code>
-        </Empty.Description>
+        {process.env.NODE_ENV !== "production" ? (
+          <Empty.Description>
+            <Code className="text-left">{JSON.stringify(err, null, 2)}</Code>
+          </Empty.Description>
+        ) : null}
apps/dashboard/app/(app)/identities/page.tsx (2)

37-39: Redirect to setup (/new) instead of sign-in when no workspace.

User is already authenticated by getAuthOrRedirect; missing workspace should route to onboarding for consistency with other pages.

-  if (!workspace) {
-    return redirect("/auth/sign-in");
-  }
+  if (!workspace) {
+    redirect("/new");
+  }

69-75: Repeat the orgId guard in Results.

Same issue as in Page; guard before using orgId in the cached query/key.

-  const { orgId } = await getAuthOrRedirect();
+  const { orgId } = await getAuthOrRedirect();
+  if (!orgId) {
+    redirect("/new");
+  }
apps/dashboard/app/(app)/layout.tsx (1)

16-21: Add orgId guard before workspace query.

Prevents eq(table.orgId, orgId) with undefined; consistent with settings/vercel.

-  const { orgId, impersonator } = await getAuthOrRedirect();
+  const { orgId, impersonator } = await getAuthOrRedirect();
+  if (!orgId) {
+    redirect("/new");
+  }
apps/dashboard/app/(app)/settings/team/role-switcher.tsx (1)

70-71: Self-change guard compares mismatched IDs; fix to prevent self-demotion/edit.
member.id is a membershipId while user.id is a userId; this condition won’t block self-edits. Compare against userMembership.id instead.

Apply this diff:

-            disabled={(Boolean(user) && member.id === user?.id) || updateMember.isLoading}
+            disabled={(Boolean(userMembership) && member.id === userMembership.id) || updateMember.isLoading}
apps/dashboard/lib/trpc/routers/org/updateMembership.ts (1)

16-29: Prevent demoting the last organization admin
Before calling authProvider.updateMembership, fetch all members via authProvider.getOrganizationMemberList(ctx.orgId), filter for role === 'admin', and if there’s only one admin whose membershipId equals input.membershipId, throw a TRPCError({ code: 'FORBIDDEN', message: 'Cannot demote the last admin.' }).

-  .mutation(async ({ input }) => {
+  .mutation(async ({ ctx, input }) => {
+    // ensure we don’t remove the sole admin
+    const { memberships } = await authProvider.getOrganizationMemberList(ctx.orgId);
+    const admins = memberships.filter(m => m.role === 'admin');
+    if (admins.length === 1 && admins[0].membershipId === input.membershipId) {
+      throw new TRPCError({ code: 'FORBIDDEN', message: 'Cannot demote the last admin.' });
+    }
     try {
       return await authProvider.updateMembership({
         membershipId: input.membershipId,
         role: input.role,
       });
♻️ Duplicate comments (7)
apps/dashboard/app/(app)/identities/[identityId]/page.tsx (1)

1-1: Good: explicit orgId guard after getAuthOrRedirect (prevents role-only sessions from leaking into access check).

Import updates and the early redirect are correct and resolve the prior feedback.

Also applies to: 13-13, 29-32

apps/dashboard/app/(app)/gateway-new/page.tsx (1)

11-11: Good: added orgId guard before workspace upsert.

Prevents creating records with undefined orgId; aligns with the pattern used elsewhere.

Also applies to: 20-23

apps/dashboard/app/(app)/settings/team/page.tsx (1)

1-1: Good: migrate to getAuthOrRedirect and guard orgId before querying.

Redirect without return is correct since redirect() throws.

Also applies to: 3-3, 10-13

apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (1)

1-1: Good: consistent orgId guard and redirect flow.

Avoids querying with undefined orgId and uses redirect() correctly on missing workspace.

Also applies to: 11-14, 19-19

apps/dashboard/app/(app)/settings/vercel/page.tsx (1)

29-32: Hard guard for undefined orgId — resolved.

Good addition; prevents passing undefined into eq() on Line 34. Matches prior guidance and retrieved learnings about role-only sessions.

apps/dashboard/app/(app)/projects/page.tsx (1)

9-12: Good explicit orgId guard before the query.
Prevents eq(undefined) and matches the documented pattern of duplicating org guards per page context. This also covers role-only contexts not handled by getAuthOrRedirect.

apps/dashboard/app/(app)/settings/team/role-switcher.tsx (1)

59-59: Local state updated from validated value — LGTM.
Prevents drift; matches prior guidance.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9647642 and 9ba09f2.

📒 Files selected for processing (16)
  • apps/dashboard/app/(app)/audit/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/gateway-new/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx (3 hunks)
  • apps/dashboard/app/(app)/identities/page.tsx (4 hunks)
  • apps/dashboard/app/(app)/layout.tsx (3 hunks)
  • apps/dashboard/app/(app)/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/billing/page.tsx (3 hunks)
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/general/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/team/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/vercel/page.tsx (2 hunks)
  • apps/dashboard/app/success/page.tsx (1 hunks)
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • apps/dashboard/app/(app)/settings/billing/page.tsx
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx
  • apps/dashboard/app/(app)/projects/page.tsx
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/app/success/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/settings/general/page.tsx
  • apps/dashboard/app/(app)/settings/team/page.tsx
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx
  • apps/dashboard/app/(app)/settings/vercel/page.tsx
  • apps/dashboard/app/(app)/audit/page.tsx
  • apps/dashboard/app/(app)/layout.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • apps/dashboard/app/(app)/settings/billing/page.tsx
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx
  • apps/dashboard/app/(app)/projects/page.tsx
  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/app/success/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/settings/general/page.tsx
  • apps/dashboard/app/(app)/settings/team/page.tsx
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx
  • apps/dashboard/app/(app)/settings/vercel/page.tsx
  • apps/dashboard/app/(app)/audit/page.tsx
  • apps/dashboard/app/(app)/layout.tsx
**/*.{env,js,ts,go}

📄 CodeRabbit inference engine (CLAUDE.md)

All environment variables must follow the format: UNKEY_<SERVICE_NAME>_VARNAME

Files:

  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
🧠 Learnings (7)
📓 Common learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/app/(app)/settings/general/page.tsx:15-18
Timestamp: 2025-09-01T16:43:57.808Z
Learning: In the Unkey dashboard, orgId guards are intentionally duplicated across pages rather than extracted to helpers because each page needs different additional context (user details, DB connections, subscriptions, etc.). The orgId check serves as both authentication and handling edge cases where users sign up but don't have organization/workspace setup completed.
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/lib/trpc/routers/org/removeMembership.ts:7-10
Timestamp: 2025-09-01T12:51:59.873Z
Learning: In the Unkey dashboard tRPC setup, the requireOrgId middleware preserves existing context (user, tenant, workspace) when adding orgId to the context, so concerns about context overwriting are unfounded when this middleware is used.
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)
📚 Learning: 2025-04-30T15:25:33.917Z
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)

Applied to files:

  • apps/dashboard/app/(app)/settings/billing/page.tsx
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx
  • apps/dashboard/app/(app)/projects/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/settings/general/page.tsx
  • apps/dashboard/app/(app)/settings/team/page.tsx
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/settings/vercel/page.tsx
  • apps/dashboard/app/(app)/layout.tsx
📚 Learning: 2025-09-01T16:43:57.808Z
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/app/(app)/settings/general/page.tsx:15-18
Timestamp: 2025-09-01T16:43:57.808Z
Learning: In the Unkey dashboard, orgId guards are intentionally duplicated across pages rather than extracted to helpers because each page needs different additional context (user details, DB connections, subscriptions, etc.). The orgId check serves as both authentication and handling edge cases where users sign up but don't have organization/workspace setup completed.

Applied to files:

  • apps/dashboard/app/(app)/settings/billing/page.tsx
  • apps/dashboard/app/(app)/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx
  • apps/dashboard/app/(app)/projects/page.tsx
  • apps/dashboard/app/(app)/settings/general/page.tsx
  • apps/dashboard/app/(app)/settings/team/page.tsx
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/settings/vercel/page.tsx
  • apps/dashboard/app/(app)/audit/page.tsx
📚 Learning: 2025-09-01T12:51:59.873Z
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/lib/trpc/routers/org/removeMembership.ts:7-10
Timestamp: 2025-09-01T12:51:59.873Z
Learning: In the Unkey dashboard tRPC setup, the requireOrgId middleware preserves existing context (user, tenant, workspace) when adding orgId to the context, so concerns about context overwriting are unfounded when this middleware is used.

Applied to files:

  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/app/(app)/settings/general/page.tsx
📚 Learning: 2025-06-19T11:48:05.070Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3324
File: apps/dashboard/app/(app)/authorization/roles/components/table/components/actions/keys-table-action.popover.constants.tsx:17-18
Timestamp: 2025-06-19T11:48:05.070Z
Learning: In the authorization roles refactor, the RoleBasic type uses `roleId` as the property name for the role identifier, not `id`. This is consistent throughout the codebase in apps/dashboard/lib/trpc/routers/authorization/roles/query.ts.

Applied to files:

  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx
📚 Learning: 2024-10-08T15:33:04.290Z
Learnt from: AkshayBandi027
PR: unkeyed/unkey#2215
File: apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx:28-29
Timestamp: 2024-10-08T15:33:04.290Z
Learning: In `authorization/roles/[roleId]/update-role.tsx`, the tag `role-${role.id}` is revalidated after updating a role to ensure that the caching mechanism is properly handled for roles.

Applied to files:

  • apps/dashboard/lib/trpc/routers/org/updateMembership.ts
  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx
📚 Learning: 2025-08-04T07:44:39.438Z
Learnt from: CR
PR: unkeyed/unkey#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-04T07:44:39.438Z
Learning: Applies to **/*.{ts,tsx} : Use Zod for runtime validation in TypeScript projects

Applied to files:

  • apps/dashboard/app/(app)/settings/team/role-switcher.tsx
🧬 Code graph analysis (15)
apps/dashboard/app/(app)/settings/billing/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/identities/[identityId]/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/projects/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/lib/trpc/routers/org/updateMembership.ts (2)
apps/dashboard/lib/auth/workos.ts (1)
  • updateMembership (412-446)
apps/dashboard/lib/trpc/trpc.ts (4)
  • t (8-8)
  • requireUser (10-21)
  • requireOrgId (48-62)
  • requireOrgAdmin (64-83)
apps/dashboard/app/(app)/identities/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (2)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/lib/db.ts (1)
  • db (5-26)
apps/dashboard/app/(app)/settings/general/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/settings/team/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/gateway-new/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/settings/team/role-switcher.tsx (1)
internal/clickhouse/src/keys/keys.ts (1)
  • roleSchema (71-74)
apps/dashboard/app/(app)/settings/vercel/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/audit/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/layout.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
🔇 Additional comments (21)
apps/dashboard/app/success/page.tsx (1)

61-61: Nit resolved: use redirect() without return.

Correct for Next.js App Router; execution aborts via throw.

apps/dashboard/app/(app)/settings/billing/page.tsx (3)

1-1: Adopts getAuthOrRedirect correctly.

Import change aligns with the new redirecting auth helper.


18-21: Good: explicit orgId guard.

Prevents undefined orgId flowing into queries (role-only sessions).


29-31: Redirect without return is correct.

redirect() throws; dropping return is the right pattern.

apps/dashboard/app/(app)/settings/general/page.tsx (2)

15-18: Solid orgId guard.

Consistent with the intended per-page context checks.


23-23: Redirect without return — nice cleanup.

Matches Next.js redirect semantics.

apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (1)

17-17: Redirect without return is correct.

Consistent with redirect() throwing.

apps/dashboard/app/(app)/audit/page.tsx (1)

10-13: Nice hardening: orgId guard added.

Prevents undefined orgId reaching getWorkspace.

apps/dashboard/app/(app)/page.tsx (2)

13-13: Redirect without return is correct.

Matches Next.js redirect semantics.


15-15: Redirect without return is correct.

Consistent cleanup.

apps/dashboard/app/(app)/settings/vercel/page.tsx (1)

9-9: Import updates align with new auth flow.

Switch to getAuthOrRedirect and adding redirect import look correct and consistent with the PR-wide migration.

Also applies to: 16-16

apps/dashboard/app/(app)/identities/page.tsx (2)

101-103: LGTM: direct redirect without return.

Switching to redirect("/new") (without return) is correct; redirect throws and halts execution.


32-33: Drop generic orgId‐guard scan
OrgId guard logic is intentionally implemented per-page based on each page’s context (workspace existence checks, user setup flows, etc.), so a repo-wide “if (!orgId)” scan yields false positives; manually review each page’s flow or refine your script to also detect workspace-null checks rather than relying solely on an immediate if (!orgId).

Likely an incorrect or invalid review comment.

apps/dashboard/app/(app)/layout.tsx (2)

4-4: Auth import migration looks good.


29-31: LGTM: use redirect without return.

Correct Next.js pattern; redirect throws.

apps/dashboard/app/(app)/projects/page.tsx (1)

2-2: Imports look correct and consistent with the PR direction.
Using getAuthOrRedirect and importing redirect/notFound aligns with the new auth flow.

Also applies to: 4-4

apps/dashboard/app/(app)/settings/team/role-switcher.tsx (3)

16-19: Role schema addition is correct.
Constrained enum matches server-side validation.


47-51: Runtime validation on selection is solid.
Early return with a toast on invalid input is appropriate.


56-57: Passing the validated role to the mutation is correct.
Keeps client and server in sync.

apps/dashboard/lib/trpc/routers/org/updateMembership.ts (2)

6-9: Middleware order is correct.
User → OrgId → OrgAdmin ensures the admin check has org context.


11-15: Input schema tightened appropriately.
Enum for role and non-empty membershipId are good safeguards.

@vercel vercel bot temporarily deployed to Preview – dashboard September 2, 2025 12:48 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/dashboard/app/(app)/identities/page.tsx (3)

40-42: Fix redirect target when no workspace.
User is authenticated; send them to setup (/new), not /auth/sign-in.

Apply this diff:

-  if (!workspace) {
-    return redirect("/auth/sign-in");
-  }
+  if (!workspace) {
+    redirect("/new");
+  }

28-31: Clamp limit to a safe range.
Prevent accidental heavy queries via query params.

Apply this diff:

-  const limit = parseAsInteger
-    .withDefault(DEFAULT_LIMIT)
-    .parse(props.searchParams.limit ?? DEFAULT_LIMIT.toString());
+  const rawLimit = parseAsInteger
+    .withDefault(DEFAULT_LIMIT)
+    .parse(props.searchParams.limit ?? DEFAULT_LIMIT.toString());
+  const limit = Math.min(Math.max(rawLimit, 1), 250);

108-116: Avoid mutating cached data.
Reordering workspace.identities in place can leak mutations across uses in the same request.

Apply this diff:

-  if (props.search) {
+  let identities = [...workspace.identities];
+  if (props.search) {
     // If we have an exact match, we want to display it at the very top
-    const exactMatchIndex = workspace.identities.findIndex(
+    const exactMatchIndex = identities.findIndex(
       ({ id, externalId }) => props.search === id || props.search === externalId,
     );
     if (exactMatchIndex > 0) {
-      workspace.identities.unshift(workspace.identities.splice(exactMatchIndex, 1)[0]);
+      identities.unshift(identities.splice(exactMatchIndex, 1)[0]);
     }
   }
@@
-          {workspace.identities.map((identity) => {
+          {identities.map((identity) => {
@@
-      {workspace.identities.length === 0 ? (
+      {identities.length === 0 ? (

Also applies to: 130-151

♻️ Duplicate comments (2)
apps/dashboard/app/(app)/page.tsx (1)

8-12: Good: explicit orgId guard after getAuthOrRedirect.
Prevents eq(undefined, …) on role-only sessions and matches the “duplicate guard per page” convention.

apps/dashboard/app/(app)/identities/page.tsx (1)

32-35: Resolved: added orgId guard on the page component.
Prevents leaking undefined into the workspace query.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9ba09f2 and b89c37b.

📒 Files selected for processing (3)
  • apps/dashboard/app/(app)/identities/page.tsx (4 hunks)
  • apps/dashboard/app/(app)/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
🧠 Learnings (4)
📓 Common learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/app/(app)/settings/general/page.tsx:15-18
Timestamp: 2025-09-01T16:43:57.808Z
Learning: In the Unkey dashboard, orgId guards are intentionally duplicated across pages rather than extracted to helpers because each page needs different additional context (user details, DB connections, subscriptions, etc.). The orgId check serves as both authentication and handling edge cases where users sign up but don't have organization/workspace setup completed.
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/lib/trpc/routers/org/removeMembership.ts:7-10
Timestamp: 2025-09-01T12:51:59.873Z
Learning: In the Unkey dashboard tRPC setup, the requireOrgId middleware preserves existing context (user, tenant, workspace) when adding orgId to the context, so concerns about context overwriting are unfounded when this middleware is used.
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)
📚 Learning: 2025-09-01T16:43:57.808Z
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/app/(app)/settings/general/page.tsx:15-18
Timestamp: 2025-09-01T16:43:57.808Z
Learning: In the Unkey dashboard, orgId guards are intentionally duplicated across pages rather than extracted to helpers because each page needs different additional context (user details, DB connections, subscriptions, etc.). The orgId check serves as both authentication and handling edge cases where users sign up but don't have organization/workspace setup completed.

Applied to files:

  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
📚 Learning: 2025-04-30T15:25:33.917Z
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)

Applied to files:

  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/app/(app)/page.tsx
🧬 Code graph analysis (3)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/identities/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (2)

1-1: Correct helper import for page context.

Using getAuthOrRedirect from "@/lib/auth" is appropriate for server pages that should auto-redirect unauthenticated users.


11-11: Auth call migration looks good.

Swapping to await getAuthOrRedirect() aligns with the project-wide change and preserves redirect-on-missing-session behavior.

apps/dashboard/app/(app)/page.tsx (2)

1-1: Import rename to getAuthOrRedirect is correct.
Aligns with the new auth helper. No action needed.


17-20: Redirect usage is correct.
Using next/navigation’s redirect without return is idiomatic (throws).

apps/dashboard/app/(app)/identities/page.tsx (2)

4-4: Import migration to getAuthOrRedirect looks good.
Consistent with the repo-wide change.


104-106: LGTM: redirect to /new when workspace missing in Results.
Consistent with the setup flow.

@vercel vercel bot temporarily deployed to Preview – dashboard September 7, 2025 23:05 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (2)

59-63: Return the Stripe portal redirect and consider basic error handling.

Returning keeps style consistent; optionally wrap create() to show a user-friendly error if Stripe is down.

-  redirect(url);
+  return redirect(url);

40-45: Add APP_URL override and retain VERCEL_TARGET_ENV
Use process.env.APP_URL for self-hosted deployments before falling back to Vercel logic; process.env.VERCEL_TARGET_ENV is the correct Vercel variable for custom environments (both production, preview, development, or custom) (vercel.com)

-  const baseUrl = process.env.VERCEL
-    ? process.env.VERCEL_TARGET_ENV === "production"
-      ? "https://app.unkey.com"
-      : `https://${process.env.VERCEL_URL}`
-    : "http://localhost:3000";
+  const baseUrl =
+    process.env.APP_URL ??
+    (process.env.VERCEL
+      ? process.env.VERCEL_TARGET_ENV === "production"
+        ? "https://app.unkey.com"
+        : `https://${process.env.VERCEL_URL}`
+      : "http://localhost:3000");
apps/dashboard/app/(app)/identities/page.tsx (2)

40-42: Inconsistent redirect destination; likely should be onboarding (/new), not sign-in.

User is already authenticated here; redirecting to /auth/sign-in can cause loops. Align with other pages.

-  if (!workspace) {
-    return redirect("/auth/sign-in");
-  }
+  if (!workspace) {
+    return redirect("/new");
+  }

75-99: Optional: trim identity columns to what Row needs.

Reduce payload by selecting only id, externalId, meta in identities.

       with: {
         identities: {
+          columns: { id: true, externalId: true, meta: true },
           where: (table, { or, like }) =>
             or(like(table.externalId, `%${props.search}%`), like(table.id, `%${props.search}%`)),
♻️ Duplicate comments (3)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (2)

11-15: Good orgId guard; return the redirect for consistency and clearer control flow.

Next.js redirect() throws, but returning keeps style consistent across pages and avoids dead-code TS hints.

-  if (!orgId) {
-    redirect("/new");
-  }
+  if (!orgId) {
+    return redirect("/new");
+  }

21-21: Return the redirect for consistency.

-    redirect("/new");
+    return redirect("/new");
apps/dashboard/app/(app)/identities/page.tsx (1)

71-73: Add orgId guard inside Results to prevent eq(undefined, …) and bad cache keys.

Results can be used independently; guard like in Page. Also validates the cache key.

-async function Results(props: { search: string; limit: number }) {
-  const { orgId } = await getAuthOrRedirect();
+async function Results(props: { search: string; limit: number }) {
+  const { orgId } = await getAuthOrRedirect();
+  if (!orgId) {
+    return redirect("/new");
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b89c37b and 24060c4.

📒 Files selected for processing (3)
  • apps/dashboard/app/(app)/identities/page.tsx (5 hunks)
  • apps/dashboard/app/(app)/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/lib/trpc/routers/org/removeMembership.ts:7-10
Timestamp: 2025-09-01T12:51:59.904Z
Learning: In the Unkey dashboard tRPC setup, the requireOrgId middleware preserves existing context (user, tenant, workspace) when adding orgId to the context, so concerns about context overwriting are unfounded when this middleware is used.
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/app/(app)/settings/general/page.tsx:15-18
Timestamp: 2025-09-01T16:43:57.850Z
Learning: In the Unkey dashboard, orgId guards are intentionally duplicated across pages rather than extracted to helpers because each page needs different additional context (user details, DB connections, subscriptions, etc.). The orgId check serves as both authentication and handling edge cases where users sign up but don't have organization/workspace setup completed.
📚 Learning: 2025-04-30T15:25:33.917Z
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)

Applied to files:

  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/app/(app)/page.tsx
📚 Learning: 2025-09-01T16:43:57.850Z
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/app/(app)/settings/general/page.tsx:15-18
Timestamp: 2025-09-01T16:43:57.850Z
Learning: In the Unkey dashboard, orgId guards are intentionally duplicated across pages rather than extracted to helpers because each page needs different additional context (user details, DB connections, subscriptions, etc.). The orgId check serves as both authentication and handling edge cases where users sign up but don't have organization/workspace setup completed.

Applied to files:

  • apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/app/(app)/page.tsx
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/app/(app)/identities/page.tsx
  • apps/dashboard/app/(app)/page.tsx
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: When querying or updating namespaces in the Unkey dashboard, always scope the operations to the current workspace using `eq(table.workspaceId, ctx.workspace.id)` to prevent cross-workspace access.

Applied to files:

  • apps/dashboard/app/(app)/identities/page.tsx
📚 Learning: 2024-10-04T20:44:38.489Z
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.

Applied to files:

  • apps/dashboard/app/(app)/page.tsx
🧬 Code graph analysis (3)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/identities/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
apps/dashboard/app/(app)/page.tsx (1)
apps/dashboard/lib/auth.ts (1)
  • getAuthOrRedirect (29-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx (1)

1-1: Auth helper migration: good move.

Switch to getAuthOrRedirect aligns with the new redirecting auth contract.

apps/dashboard/app/(app)/page.tsx (2)

1-1: Auth helper migration: looks good.


15-16: Nice: minimal column projection.

apps/dashboard/app/(app)/identities/page.tsx (3)

4-4: Auth helper migration: approved.


99-100: Only one cache key concatenates orgId—and the existing guard ensures it’s never undefined


105-106: Return the redirect.

-    redirect("/new");
+    return redirect("/new");

Likely an incorrect or invalid review comment.

@vercel vercel bot temporarily deployed to Preview – engineering September 23, 2025 08:10 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard September 23, 2025 08:15 Inactive
export const removeMembership = t.procedure
.use(requireUser)
.use(requireOrgAdmin)
.use(requireOrgId)
Copy link

Choose a reason for hiding this comment

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

Middleware order is inconsistent - requireOrgAdmin comes before requireOrgId, which is the opposite pattern used in other similar procedures.

View Details
📝 Patch Details
diff --git a/apps/dashboard/lib/trpc/routers/org/getInvitationList.ts b/apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
index d58c52cfa..96e47e3ad 100644
--- a/apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
+++ b/apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
@@ -4,8 +4,8 @@ import { requireOrgAdmin, requireOrgId, requireUser, t } from "../../trpc";
 
 export const getInvitationList = t.procedure
   .use(requireUser)
-  .use(requireOrgAdmin)
   .use(requireOrgId)
+  .use(requireOrgAdmin)
   .query(async ({ ctx }) => {
     try {
       return await authProvider.getInvitationList(ctx.orgId);
diff --git a/apps/dashboard/lib/trpc/routers/org/removeMembership.ts b/apps/dashboard/lib/trpc/routers/org/removeMembership.ts
index bcfbde3ce..e3413adf6 100644
--- a/apps/dashboard/lib/trpc/routers/org/removeMembership.ts
+++ b/apps/dashboard/lib/trpc/routers/org/removeMembership.ts
@@ -5,8 +5,8 @@ import { requireOrgAdmin, requireOrgId, requireUser, t } from "../../trpc";
 
 export const removeMembership = t.procedure
   .use(requireUser)
-  .use(requireOrgAdmin)
   .use(requireOrgId)
+  .use(requireOrgAdmin)
   .input(
     z.object({
       membershipId: z.string(),

Analysis

Inconsistent middleware order in organization procedures

What fails: removeMembership.ts and getInvitationList.ts use middleware order requireUser → requireOrgAdmin → requireOrgId, while other organization admin procedures consistently use requireUser → requireOrgId → requireOrgAdmin

Pattern validation:

  • inviteMember.ts: requireUser → requireOrgId → requireOrgAdmin
  • updateMembership.ts: requireUser → requireOrgId → requireOrgAdmin
  • revokeInvitation.ts: requireUser → requireOrgId → requireOrgAdmin
  • removeMembership.ts: requireUser → requireOrgAdmin → requireOrgId ❌
  • getInvitationList.ts: requireUser → requireOrgAdmin → requireOrgId ❌

Expected: All organization admin procedures should follow the consistent pattern established by the majority (3/5 procedures) for code maintainability

Note: While both orders are functionally equivalent per tRPC middleware documentation, consistency improves code maintainability and reduces cognitive overhead for developers.

.use(requireOrgAdmin)
.input(z.string())
.query(async ({ ctx, input: orgId }) => {
.use(requireOrgId)
Copy link

Choose a reason for hiding this comment

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

Middleware order is inconsistent - requireOrgAdmin comes before requireOrgId, which is the opposite pattern used in other similar procedures and could cause context availability issues.

View Details
📝 Patch Details
diff --git a/apps/dashboard/lib/trpc/routers/org/getInvitationList.ts b/apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
index d58c52cfa..96e47e3ad 100644
--- a/apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
+++ b/apps/dashboard/lib/trpc/routers/org/getInvitationList.ts
@@ -4,8 +4,8 @@ import { requireOrgAdmin, requireOrgId, requireUser, t } from "../../trpc";
 
 export const getInvitationList = t.procedure
   .use(requireUser)
-  .use(requireOrgAdmin)
   .use(requireOrgId)
+  .use(requireOrgAdmin)
   .query(async ({ ctx }) => {
     try {
       return await authProvider.getInvitationList(ctx.orgId);
diff --git a/apps/dashboard/lib/trpc/routers/org/removeMembership.ts b/apps/dashboard/lib/trpc/routers/org/removeMembership.ts
index bcfbde3ce..e3413adf6 100644
--- a/apps/dashboard/lib/trpc/routers/org/removeMembership.ts
+++ b/apps/dashboard/lib/trpc/routers/org/removeMembership.ts
@@ -5,8 +5,8 @@ import { requireOrgAdmin, requireOrgId, requireUser, t } from "../../trpc";
 
 export const removeMembership = t.procedure
   .use(requireUser)
-  .use(requireOrgAdmin)
   .use(requireOrgId)
+  .use(requireOrgAdmin)
   .input(
     z.object({
       membershipId: z.string(),

Analysis

Inconsistent middleware order in organization procedures causes maintainability issues

What fails: Two organization procedures (getInvitationList and removeMembership) use inconsistent middleware order requireOrgAdminrequireOrgId while three other procedures (inviteMember, updateMembership, revokeInvitation) follow the standard pattern requireOrgIdrequireOrgAdmin

How to reproduce:

grep -r "requireOrgAdmin.*requireOrgId\|requireOrgId.*requireOrgAdmin" apps/dashboard/lib/trpc/routers/org/

Result: Mixed patterns found - 2 files use requireOrgAdminrequireOrgId, 3 files use requireOrgIdrequireOrgAdmin

Expected: All organization procedures should follow the consistent pattern requireOrgIdrequireOrgAdmin for logical flow (establish context before permission check) and code maintainability

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
apps/dashboard/lib/auth.ts (4)

7-15: Clarify GetAuthResult invariants (doc) to match runtime guarantees.

After redirects, userId is guaranteed; at least one of orgId or role is present, but not necessarily both. Make this explicit to avoid misuse down the line.

Apply this doc addition:

+/**
+ * Result of getAuthOrRedirect.
+ * Invariant: userId is always present; at least one of orgId or role is a non-empty string.
+ * orgId may be empty for system-level sessions.
+ */
 export type GetAuthResult = {
   userId: string;
   orgId: string;
   role: string;
   impersonator?: {
     email: string;
     reason?: string | null;
   };
 };

17-28: Fix stale JSDoc: param name/usage and return semantics.

The param is used and the function never returns “null values”; it redirects instead.

  /**
   * Validates the current user session and performs token refresh if needed.
@@
- * @param _req - Optional request object (not used but maintained for compatibility)
- * @returns Authentication result containing userId and orgId if authenticated, null values otherwise
+ * @param req - Optional NextRequest forwarded to the base get-auth implementation
+ * @returns Authentication result; redirects to sign-in when no user and to /new when neither orgId nor role is present
   * @throws Redirects to sign-in or organization/workspace creation pages if requirements aren't met
   */

54-61: Update stale inline comment.

The redirect guarantee now comes from getAuthOrRedirect.

-  const user = await auth.getUser(userId); // getAuth will redirect if there's no userId
+  const user = await auth.getUser(userId); // getAuthOrRedirect ensured userId is present

29-41: Return an explicit narrowed object instead of as GetAuthResult

Construct and return an explicit object to avoid an unsound cast. Verified no stale getAuth imports from "@/lib/auth" and no "use client" files import "@/lib/auth".

 export async function getAuthOrRedirect(req?: NextRequest): Promise<GetAuthResult> {
   const authResult = await getBaseAuth(req);
   if (!authResult.userId) {
     redirect("/auth/sign-in");
   }
 
   if (!authResult.orgId && !authResult.role) {
     redirect("/new");
   }
 
-  return authResult as GetAuthResult;
+  return {
+    userId: authResult.userId!,
+    orgId: authResult.orgId ?? "",
+    role: authResult.role ?? "",
+    impersonator: authResult.impersonator,
+  };
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24060c4 and 71a7f3a.

📒 Files selected for processing (3)
  • apps/dashboard/app/(app)/gateway-new/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/billing/page.tsx (3 hunks)
  • apps/dashboard/lib/auth.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/dashboard/app/(app)/gateway-new/page.tsx
  • apps/dashboard/app/(app)/settings/billing/page.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/app/(app)/settings/general/page.tsx:15-18
Timestamp: 2025-09-01T16:43:57.850Z
Learning: In the Unkey dashboard, orgId guards are intentionally duplicated across pages rather than extracted to helpers because each page needs different additional context (user details, DB connections, subscriptions, etc.). The orgId check serves as both authentication and handling edge cases where users sign up but don't have organization/workspace setup completed.
Learnt from: perkinsjr
PR: unkeyed/unkey#3898
File: apps/dashboard/lib/trpc/routers/org/removeMembership.ts:7-10
Timestamp: 2025-09-01T12:51:59.904Z
Learning: In the Unkey dashboard tRPC setup, the requireOrgId middleware preserves existing context (user, tenant, workspace) when adding orgId to the context, so concerns about context overwriting are unfounded when this middleware is used.
📚 Learning: 2025-04-30T15:25:33.917Z
Learnt from: mcstepp
PR: unkeyed/unkey#3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)

Applied to files:

  • apps/dashboard/lib/auth.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
  • GitHub Check: Vercel Agent Review

@perkinsjr perkinsjr closed this Sep 23, 2025
@perkinsjr
Copy link
Member Author

This will need to be revisited after slug routing goes in because it will be 100% conflicting.

@chronark chronark deleted the auth-rewrites branch November 5, 2025 12:44
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