Conversation
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)
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
📝 WalkthroughWalkthroughReplaced 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
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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests
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.
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. Comment |
|
Thank you for following the naming conventions for pull request titles! 🙏 |
There was a problem hiding this comment.
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 violationsDuplicate 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 contextNOT_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 charactersRegex 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 inputTrim 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 catchesInline
.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 stringWith
if (!orgId && !role), callers can receiveorgIdasundefinedwhile the type promisesstring(unsafe; pages query by orgId).Prefer guaranteeing
orgIdhere, 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 castComment mentions “getAuth” but you now use
getAuthOrRedirect. Also the cast on return can be avoided if the function guarantees non-nullorgId.- const user = await auth.getUser(userId); // getAuth will redirect if there's no userId + const user = await auth.getUser(userId); // getAuthOrRedirect ensures userId existsapps/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_ENVwithprocess.env.VERCEL_ENV(or extend your Zod schema inapps/dashboard/lib/env.tsto includeVERCEL_TARGET_ENVif you need custom environments) (vercel.com)- No change needed for Stripe env naming:
stripeEnv()correctly expectsSTRIPE_SECRET_KEY, noUNKEY_prefix requiredapps/dashboard/app/integrations/vercel/callback/page.tsx (6)
48-51: Scope integration lookup to the authenticated workspace to prevent IDORQuerying 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 paramsconfigurationId 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
codeand a safenextpath.
109-116: Do not expose Vercel access tokens to the clientPassing 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 usersRendering 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 redirectsReturn 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 flagDead comments create confusion, especially in a deprecated file.
apps/dashboard/lib/auth/utils.ts (1)
38-43: Allow sign-out without requiring authenticationCalling 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 filtergetAuthOrRedirect 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 deletionCurrently 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.
📒 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.tsxapps/dashboard/app/(app)/apis/[apiId]/actions.tsapps/dashboard/app/(app)/audit/page.tsxapps/dashboard/lib/trpc/routers/org/revokeInvitation.tsapps/dashboard/app/(app)/settings/team/members.tsxapps/dashboard/app/(app)/settings/team/client.tsxapps/dashboard/lib/trpc/routers/rbac.tsapps/dashboard/app/(app)/settings/general/update-workspace-name.tsxapps/dashboard/app/(app)/logs/page.tsxapps/dashboard/app/(app)/gateway-new/page.tsxapps/dashboard/app/(app)/layout.tsxapps/dashboard/app/(app)/settings/team/invitations.tsxapps/dashboard/app/(app)/settings/team/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsxapps/dashboard/app/(app)/page.tsxapps/dashboard/lib/trpc/trpc.tsapps/dashboard/lib/trpc/routers/workspace/changeName.tsapps/dashboard/app/(app)/identities/page.tsxapps/dashboard/lib/trpc/routers/org/updateMembership.tsapps/dashboard/app/(app)/identities/[identityId]/page.tsxapps/dashboard/app/(app)/settings/vercel/page.tsxapps/dashboard/app/(app)/settings/billing/page.tsxapps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.tsapps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsxapps/dashboard/app/(app)/projects/page.tsxapps/dashboard/app/integrations/vercel/callback/page.tsxapps/dashboard/lib/trpc/routers/org/getOrg.tsapps/dashboard/lib/trpc/routers/org/inviteMember.tsapps/dashboard/lib/auth/utils.tsapps/dashboard/lib/trpc/routers/org/getInvitationList.tsapps/dashboard/lib/auth.tsapps/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.tsxapps/dashboard/app/(app)/apis/[apiId]/actions.tsapps/dashboard/app/(app)/audit/page.tsxapps/dashboard/lib/trpc/routers/org/revokeInvitation.tsapps/dashboard/app/(app)/settings/team/members.tsxapps/dashboard/app/(app)/settings/team/client.tsxapps/dashboard/lib/trpc/routers/rbac.tsapps/dashboard/app/(app)/settings/general/update-workspace-name.tsxapps/dashboard/app/(app)/logs/page.tsxapps/dashboard/app/(app)/gateway-new/page.tsxapps/dashboard/app/(app)/layout.tsxapps/dashboard/app/(app)/settings/team/invitations.tsxapps/dashboard/app/(app)/settings/team/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsxapps/dashboard/app/(app)/page.tsxapps/dashboard/lib/trpc/trpc.tsapps/dashboard/lib/trpc/routers/workspace/changeName.tsapps/dashboard/app/(app)/identities/page.tsxapps/dashboard/lib/trpc/routers/org/updateMembership.tsapps/dashboard/app/(app)/identities/[identityId]/page.tsxapps/dashboard/app/(app)/settings/vercel/page.tsxapps/dashboard/app/(app)/settings/billing/page.tsxapps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.tsapps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsxapps/dashboard/app/(app)/projects/page.tsxapps/dashboard/app/integrations/vercel/callback/page.tsxapps/dashboard/lib/trpc/routers/org/getOrg.tsapps/dashboard/lib/trpc/routers/org/inviteMember.tsapps/dashboard/lib/auth/utils.tsapps/dashboard/lib/trpc/routers/org/getInvitationList.tsapps/dashboard/lib/auth.tsapps/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.tsapps/dashboard/lib/trpc/routers/org/revokeInvitation.tsapps/dashboard/lib/trpc/routers/rbac.tsapps/dashboard/lib/trpc/trpc.tsapps/dashboard/lib/trpc/routers/workspace/changeName.tsapps/dashboard/lib/trpc/routers/org/updateMembership.tsapps/dashboard/lib/trpc/routers/org/getOrganizationMemberList.tsapps/dashboard/lib/trpc/routers/org/getOrg.tsapps/dashboard/lib/trpc/routers/org/inviteMember.tsapps/dashboard/lib/auth/utils.tsapps/dashboard/lib/trpc/routers/org/getInvitationList.tsapps/dashboard/lib/auth.tsapps/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.tsxapps/dashboard/app/(app)/apis/[apiId]/actions.tsapps/dashboard/app/(app)/audit/page.tsxapps/dashboard/app/(app)/logs/page.tsxapps/dashboard/app/(app)/gateway-new/page.tsxapps/dashboard/app/(app)/layout.tsxapps/dashboard/app/(app)/settings/team/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsxapps/dashboard/app/(app)/page.tsxapps/dashboard/app/(app)/identities/page.tsxapps/dashboard/app/(app)/identities/[identityId]/page.tsxapps/dashboard/app/(app)/settings/vercel/page.tsxapps/dashboard/app/(app)/settings/billing/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsxapps/dashboard/app/(app)/projects/page.tsxapps/dashboard/app/integrations/vercel/callback/page.tsxapps/dashboard/lib/auth/utils.tsapps/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.tsapps/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 inupsertPermissions. This helper is called by the root‐key flows inapps/dashboard/lib/trpc/routers/key/createRootKey.ts(≈lines 139–143) and.../updateRootKeyPermissions.ts(≈lines 72–76) to write into the system workspace; stripping out theUNKEY_WORKSPACE_IDbranch 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/newwhenworkspaceis falsy (which covers a missingorgId), so adding an extraif (!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 standardYour guidelines require UNKEY__VARNAME.
stripeEnv()exposesSTRIPE_SECRET_KEY. Confirm these are backed byUNKEY_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 afterconst { orgId } = await getAuthOrRedirect();to handle missingorgId(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 importsSearch confirmed no
import { getAuth } from "@/lib/auth"in any.ts/.tsxfiles; all existinggetAuthcalls 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 semanticsSwitching 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 neededgetAuthOrRedirect 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 helperConsistent 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 LGTMImporting 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.
- 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")
The role schema is a string by default but we should validate it to make sure it fits the enum.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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 editsThe 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 500The 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 requireWorkspaceAdd 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 revocationCurrently 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.
📒 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.tsxapps/dashboard/app/(app)/gateway-new/page.tsxapps/dashboard/app/(app)/settings/billing/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsxapps/dashboard/app/(app)/settings/vercel/page.tsxapps/dashboard/app/(app)/settings/team/page.tsxapps/dashboard/app/(app)/settings/general/page.tsxapps/dashboard/lib/auth/utils.tsapps/dashboard/lib/auth.tsapps/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.tsxapps/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.tsxapps/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.tsapps/dashboard/lib/trpc/routers/org/updateMembership.tsapps/dashboard/lib/trpc/routers/org/inviteMember.tsapps/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 enumConfining to explicit roles blocks invalid values.
7-9: Add requireWorkspace before requireOrgIdrequireOrgId 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 allowstenant.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 addedNon-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.
There was a problem hiding this comment.
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 callingauthProvider.updateMembership, fetch all members viaauthProvider.getOrganizationMemberList(ctx.orgId), filter forrole === 'admin', and if there’s only one admin whosemembershipIdequalsinput.membershipId, throw aTRPCError({ 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.
📒 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.tsxapps/dashboard/app/(app)/identities/[identityId]/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsxapps/dashboard/app/(app)/projects/page.tsxapps/dashboard/lib/trpc/routers/org/updateMembership.tsapps/dashboard/app/success/page.tsxapps/dashboard/app/(app)/identities/page.tsxapps/dashboard/app/(app)/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsxapps/dashboard/app/(app)/settings/general/page.tsxapps/dashboard/app/(app)/settings/team/page.tsxapps/dashboard/app/(app)/gateway-new/page.tsxapps/dashboard/app/(app)/settings/team/role-switcher.tsxapps/dashboard/app/(app)/settings/vercel/page.tsxapps/dashboard/app/(app)/audit/page.tsxapps/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.tsxapps/dashboard/app/(app)/identities/[identityId]/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsxapps/dashboard/app/(app)/projects/page.tsxapps/dashboard/lib/trpc/routers/org/updateMembership.tsapps/dashboard/app/success/page.tsxapps/dashboard/app/(app)/identities/page.tsxapps/dashboard/app/(app)/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsxapps/dashboard/app/(app)/settings/general/page.tsxapps/dashboard/app/(app)/settings/team/page.tsxapps/dashboard/app/(app)/gateway-new/page.tsxapps/dashboard/app/(app)/settings/team/role-switcher.tsxapps/dashboard/app/(app)/settings/vercel/page.tsxapps/dashboard/app/(app)/audit/page.tsxapps/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.tsxapps/dashboard/app/(app)/identities/[identityId]/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsxapps/dashboard/app/(app)/projects/page.tsxapps/dashboard/app/(app)/identities/page.tsxapps/dashboard/app/(app)/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsxapps/dashboard/app/(app)/settings/general/page.tsxapps/dashboard/app/(app)/settings/team/page.tsxapps/dashboard/app/(app)/gateway-new/page.tsxapps/dashboard/app/(app)/settings/vercel/page.tsxapps/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.tsxapps/dashboard/app/(app)/identities/[identityId]/page.tsxapps/dashboard/app/(app)/settings/billing/stripe/checkout/page.tsxapps/dashboard/app/(app)/projects/page.tsxapps/dashboard/app/(app)/settings/general/page.tsxapps/dashboard/app/(app)/settings/team/page.tsxapps/dashboard/app/(app)/gateway-new/page.tsxapps/dashboard/app/(app)/settings/vercel/page.tsxapps/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.tsapps/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.tsapps/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.tsapps/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 immediateif (!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.
There was a problem hiding this comment.
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.
📒 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.tsxapps/dashboard/app/(app)/page.tsxapps/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.tsxapps/dashboard/app/(app)/page.tsxapps/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.tsxapps/dashboard/app/(app)/page.tsxapps/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.tsxapps/dashboard/app/(app)/page.tsxapps/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.
apps/dashboard/app/(app)/settings/billing/stripe/portal/page.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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
Useprocess.env.APP_URLfor self-hosted deployments before falling back to Vercel logic;process.env.VERCEL_TARGET_ENVis the correct Vercel variable for custom environments (bothproduction,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
📒 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.tsxapps/dashboard/app/(app)/identities/page.tsxapps/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.tsxapps/dashboard/app/(app)/identities/page.tsxapps/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.tsxapps/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 concatenatesorgId—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.
| export const removeMembership = t.procedure | ||
| .use(requireUser) | ||
| .use(requireOrgAdmin) | ||
| .use(requireOrgId) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 requireOrgAdmin → requireOrgId while three other procedures (inviteMember, updateMembership, revokeInvitation) follow the standard pattern requireOrgId → requireOrgAdmin
How to reproduce:
grep -r "requireOrgAdmin.*requireOrgId\|requireOrgId.*requireOrgAdmin" apps/dashboard/lib/trpc/routers/org/Result: Mixed patterns found - 2 files use requireOrgAdmin → requireOrgId, 3 files use requireOrgId → requireOrgAdmin
Expected: All organization procedures should follow the consistent pattern requireOrgId → requireOrgAdmin for logical flow (establish context before permission check) and code maintainability
There was a problem hiding this comment.
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 ofas GetAuthResultConstruct and return an explicit object to avoid an unsound cast. Verified no stale
getAuthimports 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
📒 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
|
This will need to be revisited after slug routing goes in because it will be 100% conflicting. |
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
getAuthtogetAuthOrRedirectfor better clarity and intentgetAuthimplementations where appropriate2. Organization Security Middleware
requireOrgIdmiddleware to centralize organization access validationorgIdparametersgetInvitationListgetOrginviteMemberremoveMembershiprevokeInvitationupdateMembership3. Workspace Security Fixes
changeNameendpointworkspaceIdfrom input schema to prevent arbitrary workspace accessctx.workspace.id) instead of user input4. Enhanced RBAC Permission Management
upsertPermissionto follow tRPC procedure patterns5. Team Settings UI Improvements
orgIdparameters🛡️ Security Benefits
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
How should this be tested?
Checklist
Required
pnpm buildpnpm fmtconsole.logsgit pull origin mainAppreciated
Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Chores