Skip to content

Conversation

Kitenite
Copy link
Contributor

@Kitenite Kitenite commented Aug 15, 2025

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Release
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes


Important

This PR adds access control and enhanced invitation management to projects, including new authentication flows, email templates, and session handling improvements.

  • Access Control:
    • Added access control to project pages in layout.tsx to restrict access based on user permissions.
    • Implemented HandleAuth component in invitation/[id]/_components/auth.tsx for login prompts.
  • Invitation Management:
    • Enhanced invitation flow with constructInvitationLink in invitation.ts and updated email templates in invite-user.tsx.
    • Added getWithoutToken query in invitation.ts to fetch invitation details without a token.
    • Updated InvitationRow in members/invitation-row.tsx to include copy-to-clipboard functionality.
  • Email and Constants:
    • Introduced SUPPORT_EMAIL in contact.ts and used it in email templates and notifications.
    • Updated sendInvitationEmail in invitation.ts to include inviter's name in the subject.
  • Session and URL Management:
    • Replaced localStorage with localforage for session management in multiple components.
    • Added sanitizeReturnUrl and getReturnUrlQueryParam in url/index.ts for secure URL handling.
  • Miscellaneous:
    • Removed unused dependencies dayjs and url-join from package.json.
    • Minor UI adjustments in features/page.tsx and hero/features-hero.tsx.

This description was created by Ellipsis for df07cef. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • Project and invitation pages now gate access and show clear "Access denied" or login prompts with an authentication redirect flow.
    • Added invitation copy-to-clipboard and explicit "Accept Invitation" flow; email invites now show inviter info.
  • Improvements

    • Post-login returns restored reliably across the app; drafts and return URLs now persist more robustly.
    • Improved invitation email content, clearer expiry/error messages, loading UIs, and member invite UX.
    • Centralized routes and minor dev UI tweaks (dev indicators disabled).

Copy link

vercel bot commented Aug 15, 2025

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

Project Deployment Preview Comments Updated (UTC)
web Ready Ready Preview Comment Aug 18, 2025 8:54pm
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
docs Skipped Skipped Aug 18, 2025 8:54pm

Copy link

supabase bot commented Aug 15, 2025

This pull request has been ignored for the connected project wowaemfasoptxrdjhilu because there are no changes detected in apps/backend/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link

coderabbitai bot commented Aug 15, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Warning

Rate limit exceeded

@Kitenite has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 9 minutes and 5 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between dc75913 and df07cef.

📒 Files selected for processing (3)
  • apps/web/client/src/app/project/[id]/_components/members/invitation-row.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/members/invite-member-input.tsx (4 hunks)
  • apps/web/client/src/app/project/[id]/_components/members/members-content.tsx (2 hunks)

Walkthrough

Implements returnUrl-based auth plumbing (persisted with localforage), adds project- and invitation-level access gating, revamps invitation APIs and email/template flows (including constructInvitationLink), updates DB invitation indexing and relations, migrates multiple localStorage usages to localforage, and adjusts related UI and routing constants.

Changes

Cohort / File(s) Summary
Auth returnUrl plumbing
apps/web/client/src/app/login/page.tsx, apps/web/client/src/app/login/actions.tsx, apps/web/client/src/app/auth/redirect/page.tsx, apps/web/client/src/app/auth/callback/route.ts, apps/web/client/src/components/ui/auth-redirect.tsx, apps/web/client/src/components/ui/avatar-dropdown/index.tsx, apps/web/client/src/app/projects/layout.tsx, apps/web/client/src/app/invitation/[id]/layout.tsx, apps/web/client/src/app/invitation/[id]/_components/auth.tsx
Adds returnUrl capture/persistence via LocalForageKeys and localforage, standardizes redirects to Routes.*, introduces client/server flows that store and consume returnUrl for post-login navigation, and gates invitation/project routes with session checks.
URL helpers & constants
apps/web/client/src/utils/url/index.ts, apps/web/client/src/utils/constants/index.ts
Adds getReturnUrlQueryParam and sanitizeReturnUrl helpers; adds Routes.AUTH_* constants and LocalForageKeys.RETURN_URL; asserts Git as const.
Project access gating (server + layout)
apps/web/client/src/app/project/[id]/layout.tsx, apps/web/client/src/server/api/routers/project/project.ts
New server-side project layout calls api.project.hasAccess; adds protected hasAccess tRPC endpoint returning boolean based on userProjects relation.
Invitation API and UI revamp
apps/web/client/src/server/api/routers/project/invitation.ts, apps/web/client/src/app/invitation/[id]/_components/main.tsx, apps/web/client/src/app/project/[id]/_components/members/invitation-row.tsx
Invitation router: adds getWithoutToken, includes inviter relation, switches to date-fns, granular validation errors, uses constructInvitationLink; UI: improved accept flow, error states, copy-invitation-link action, and navigate-to-project behavior.
DB schema: invitations & relations
packages/db/src/schema/project/invitation.ts, packages/db/src/schema/user/user.ts
Replaces unique constraint on (invitee_email, project_id) with an index; adds users→projectInvitations relation; removes explicit inviter relationName.
Email and template changes
packages/email/src/invitation.ts, packages/email/src/templates/invite-user.tsx
sendInvitationEmail now accepts inviteParams (inviteeEmail, invitedByEmail, invitedByName); adds constructInvitationLink; email from/to/subject and template props/styling updated.
Localforage migrations (hero & import flows)
apps/web/client/src/app/_components/hero/import.tsx, apps/web/client/src/app/_components/hero/start-blank.tsx, apps/web/client/src/app/_components/hero/create.tsx
Move saved return/draft persistence from localStorage to localforage; Create component wrapped with observer, async draft restore, and clears drafts after navigation.
Auth UI signatures & context
apps/web/client/src/app/_components/login-button.tsx, apps/web/client/src/app/auth/auth-context.tsx, apps/web/client/src/app/login/page.tsx
Login buttons now accept returnUrl props; Auth context handlers accept and persist returnUrl via LocalForageKeys; login page reads and forwards returnUrl to buttons.
Routing/UI tweaks and small UX updates
apps/web/client/src/app/_components/hero/features-hero.tsx, apps/web/client/src/app/features/page.tsx, apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx, apps/web/client/src/app/project/[id]/_components/members/members-content.tsx, .../members/invite-member-input.tsx, .../members/index.tsx
Centralizes navigation to Routes, wraps features page with providers/layout, changes avatar placement/size, enhances members loading UI, adds async invite creation loading state, and tweaks popover alignment.
GitHub OAuth & server redirects
apps/web/client/src/server/api/routers/github.ts, apps/web/client/src/app/auth/callback/route.ts
Use Routes.AUTH_CALLBACK / AUTH_REDIRECT for OAuth callback/redirect construction and standardize redirect URL construction using forwarded headers/origin.
Support email & constants usage
apps/web/client/src/app/project/[id]/_components/left-panel/help-dropdown/index.tsx, apps/web/client/src/components/store/editor/version/git.ts, packages/constants/src/contact.ts, packages/constants/src/index.ts
Introduces SUPPORT_EMAIL constant, re-exports it, and replaces hard-coded support addresses with the constant (including git config default).
Housekeeping / minor edits
apps/web/client/.gitignore, apps/web/client/next.config.ts, apps/web/client/package.json, apps/web/client/src/app/_components/landing-page/design-mockup/design-mockup-icons.tsx, apps/web/client/src/app/_components/auth-modal.tsx, apps/web/client/src/app/invitation/[id]/page.tsx, packages/models/src/project/invitation.ts, packages/models/src/project/index.ts
Broadened .gitignore pattern, changed devIndicators shape, removed dayjs/url-join deps, fixed SVG attribute names and IconProps typing, tiny formatting edits, removed legacy Invitation model and its re-export from project index.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Client as Next.js Client
  participant Supabase
  participant Store as localforage
  participant Router

  User->>Client: Visit protected page (project or invitation)
  Client->>Supabase: supabase.auth.getSession()
  alt No session
    Client->>Store: setItem(RETURN_URL, currentPath)
    Client->>Router: push(Routes.LOGIN + ?returnUrl)
  else Session exists
    Client-->>User: Render protected content
  end
Loading
sequenceDiagram
  participant User
  participant Page as Invitation Page
  participant API as tRPC Invitation Router
  participant Email as Email Helper

  User->>Page: Click "Accept Invitation"
  Page->>API: acceptInvitation({ id, token })
  alt Valid & not expired
    API-->>Page: success (projectId?)
    Page->>Router: navigate(project or projects)
  else Error
    API-->>Page: Error (not found / email mismatch / expired)
    Page-->>User: Show error UI
  end
Loading
sequenceDiagram
  participant Layout as Project [id] Layout
  participant API as tRPC Project.hasAccess
  participant Child as Page Children

  Layout->>API: hasAccess({ projectId })
  alt Access granted
    API-->>Layout: true
    Layout-->>Child: Render children
  else Access denied
    API-->>Layout: false
    Layout-->>Layout: Render NoAccess fallback
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit hops through routes and keys,
Tucking returnUrl under mossy leaves.
Invitations stitched with tidy links, ✉️
Guards at the gate that gently think.
Localforage burrows, secrets kept —
Hops, redirects, and all things leapt. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/restrict-project-access

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (7)
apps/web/client/src/app/project/[id]/layout.tsx (5)

6-7: Fix Next.js layout prop types: params isn’t a Promise; drop await

Next.js app router passes params as a plain object. Typing it as Promise<...> and awaiting it works at runtime but is incorrect and weakens type inference.

Apply this diff:

-export default async function Layout({ params, children }: Readonly<{ params: Promise<{ id: string }>, children: React.ReactNode }>) {
-    const projectId = (await params).id;
+export default async function Layout({ params, children }: Readonly<{ params: { id: string }; children: React.ReactNode }>) {
+    const { id: projectId } = params;

25-27: Use a native anchor for mailto: instead of next/link

Link is intended for client-side navigation of internal routes. For mailto: (and other external protocols), use <a>.

-                        <Link href={`mailto:${SUPPORT_EMAIL}`} className="text-primary underline">
-                            {SUPPORT_EMAIL}
-                        </Link>
+                        <a href={`mailto:${SUPPORT_EMAIL}`} className="text-primary underline">
+                            {SUPPORT_EMAIL}
+                        </a>

1-6: Consider forcing dynamic rendering to avoid caching auth-gated content

This layout gates children on a per-user check. If the api.project.hasAccess code path doesn’t touch cookies/headers in a way Next can detect automatically, the page might be statically cached. Consider marking this route segment dynamic.

 import { api } from "@/trpc/server";
 import { Routes } from "@/utils/constants";
 import { Icons } from "@onlook/ui/icons/index";
 import Link from "next/link";
 
+export const dynamic = 'force-dynamic';

If you already rely on middleware/session access that triggers dynamic rendering, this change may be unnecessary—please verify.


1-4: Optionally redirect unauthenticated users to Login

If protectedProcedure throws UNAUTHORIZED, this will bubble and render the error boundary. If you prefer a cleaner UX, catch and redirect to login; keep the NoAccess UI for authenticated users without membership.

 import { api } from "@/trpc/server";
 import { Routes } from "@/utils/constants";
 import { Icons } from "@onlook/ui/icons/index";
 import Link from "next/link";
+import { redirect } from "next/navigation";
@@
-    const hasAccess = await api.project.hasAccess({ projectId });
-    if (!hasAccess) {
-        return <NoAccess />;
-    }
+    try {
+        const hasAccess = await api.project.hasAccess({ projectId });
+        if (!hasAccess) {
+            return <NoAccess />;
+        }
+    } catch {
+        // Unauthenticated or server error: send to Login
+        redirect(Routes.LOGIN);
+    }

Also applies to: 8-13


16-17: Avoid hardcoding support email

Move [email protected] to a central config (env or constants) to avoid drift and ease changes.

apps/web/client/src/server/api/routers/project/project.ts (2)

36-49: Validate UUID and simplify to an existence check on userProjects

Current implementation loads the project with a nested relation to check membership. You can:

  • Validate the ID format early (DB uses UUID).
  • Query userProjects directly for existence. This avoids an unnecessary join, reduces payload, and relies on the composite PK to guarantee consistency.
-    hasAccess: protectedProcedure
-        .input(z.object({ projectId: z.string() }))
-        .query(async ({ ctx, input }) => {
-            const user = ctx.user;
-            const project = await ctx.db.query.projects.findFirst({
-                where: eq(projects.id, input.projectId),
-                with: {
-                    userProjects: {
-                        where: eq(userProjects.userId, user.id),
-                    },
-                },
-            });
-            return !!project && project.userProjects.length > 0;
-        }),
+    hasAccess: protectedProcedure
+        .input(z.object({ projectId: z.string().uuid() }))
+        .query(async ({ ctx, input }) => {
+            const userId = ctx.user.id;
+            const membership = await ctx.db.query.userProjects.findFirst({
+                where: and(
+                    eq(userProjects.projectId, input.projectId),
+                    eq(userProjects.userId, userId),
+                ),
+                columns: { projectId: true },
+            });
+            return !!membership;
+        }),

36-49: Add tests for access control paths

Recommend tests for:

  • returns true when user is a member (any role)
  • returns false when user is not a member
  • returns false for non-existent project
  • rejects unauthenticated access via protectedProcedure

I can scaffold tRPC integration tests with an in-memory DB or transaction sandbox if helpful.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 51ce76f and 619d8b3.

📒 Files selected for processing (2)
  • apps/web/client/src/app/project/[id]/layout.tsx (1 hunks)
  • apps/web/client/src/server/api/routers/project/project.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/web/client/src/app/project/[id]/layout.tsx (3)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
apps/web/client/src/utils/constants/index.ts (1)
  • Routes (1-20)
packages/ui/src/components/icons/index.tsx (1)
  • Icons (138-3503)
apps/web/client/src/server/api/routers/project/project.ts (3)
apps/web/client/src/server/api/trpc.ts (1)
  • protectedProcedure (130-149)
packages/db/src/schema/project/project.ts (1)
  • projects (12-30)
packages/db/src/schema/user/user-project.ts (1)
  • userProjects (10-23)

@Kitenite Kitenite marked this pull request as draft August 15, 2025 23:49
@vercel vercel bot temporarily deployed to Preview – docs August 16, 2025 01:40 Inactive
@vercel vercel bot temporarily deployed to Preview – docs August 17, 2025 23:31 Inactive
@@ -15,15 +15,15 @@ export async function login(provider: SignInMethod.GITHUB | SignInMethod.GOOGLE)
data: { session },
} = await supabase.auth.getSession();
if (session) {
redirect('/');
redirect(returnUrl || '/');
Copy link
Contributor

Choose a reason for hiding this comment

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

Validate the 'returnUrl' before using it in redirect calls to avoid open redirect vulnerabilities. Ensure it’s a relative/internal URL.

@vercel vercel bot temporarily deployed to Preview – docs August 18, 2025 06:06 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
apps/web/client/src/app/projects/layout.tsx (1)

19-21: Sanitize x-pathname to prevent open-redirects; also drop unnecessary await on headers()

Do not trust header-provided path blindly. Constrain to safe relative paths and avoid host/protocol injection. Also, headers() is synchronous in Next.js; awaiting it is unnecessary.

Apply this diff:

-        const headersList = await headers();
-        const pathname = headersList.get('x-pathname') || Routes.PROJECTS;
+        const headersList = headers();
+        const pathname = (() => {
+            const p = headersList.get('x-pathname');
+            return p && p.startsWith('/') && !p.startsWith('//') && !p.includes(':') ? p : Routes.PROJECTS;
+        })();
         redirect(`${Routes.LOGIN}?${getReturnUrlQueryParam(pathname)}`);
apps/web/client/src/app/invitation/[id]/_components/main.tsx (1)

31-36: Bug: returnUrl not URL-encoded; token query breaks and can be lost on redirect

Manually concatenating the unencoded current URL will split token into a top-level param on /login. Use the shared helper to encode.

Apply this diff:

     const handleLogin = async () => {
         const supabase = createClient();
         await supabase.auth.signOut();
         const currentUrl = `${pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
-        router.push(`${Routes.LOGIN}?${LocalForageKeys.RETURN_URL}=${currentUrl}`);
+        router.push(`${Routes.LOGIN}?${getReturnUrlQueryParam(currentUrl)}`);
     }
apps/web/client/src/server/api/routers/project/invitation.ts (2)

171-175: Ensure constructInvitationLink no longer depends on url-join (remove leftover dep)

The email helper still imports and uses url-join per the provided snippet. Replace with the standard URL API to avoid the dependency and edge-case bugs.

Proposed change in packages/email/src/invitation.ts (for that file):

export const constructInvitationLink = (
  publicUrl: string,
  invitationId: string,
  token: string,
) => {
  const url = new URL(publicUrl);
  url.pathname = `/invitation/${invitationId}`;
  url.searchParams.set('token', token);
  return url.toString();
};

Run this to locate any lingering url-join usage:

#!/bin/bash
set -euo pipefail

echo "Searching for 'url-join' imports/usages..."
rg -nP "url-join" -C2 || true

45-50: Do not expose the invitation token in the GET response

Leaking the token undermines secret-link semantics. Omit it from the response.

Apply this diff:

-        return {
-            ...invitation,
-            // @ts-expect-error - Drizzle is not typed correctly
-            inviter: toUser(invitation.inviter),
-        };
+        const { token: _omitToken, ...safeInvitation } = invitation as typeof invitation & { token?: string };
+        return {
+            ...safeInvitation,
+            // @ts-expect-error - Drizzle is not typed correctly
+            inviter: toUser(invitation.inviter),
+        };
🧹 Nitpick comments (4)
apps/web/client/src/utils/url/index.ts (1)

7-32: Add SSR guard to sanitizeReturnUrl and handle percent-encoded inputs

To ensure this runs safely in server contexts and avoids cases where the returnUrl is still percent-encoded, update the function as follows:

 export function sanitizeReturnUrl(returnUrl: string | null): string {
     // Default to home page if no return URL
     if (!returnUrl) {
         return Routes.HOME;
     }

+    // SSR guard: window isn’t available on the server
+    if (typeof window === 'undefined') {
+        // Only allow safe relative paths during SSR
+        if (returnUrl.startsWith('/') && !returnUrl.startsWith('//')) {
+            return returnUrl;
+        }
+        return Routes.HOME;
+    }
+
+    // Handle percent-encoded inputs (e.g. "%2Fpath")
+    try {
+        returnUrl = decodeURIComponent(returnUrl);
+    } catch {
+        // leave original if it’s not valid percent-encoding
+    }

     try {
         // If it's a relative path, it's safe
         if (returnUrl.startsWith('/') && !returnUrl.startsWith('//')) {
             return returnUrl;
         }

         // Parse as URL to check if it's same-origin
         const url = new URL(returnUrl, window.location.origin);

         // Only allow same-origin URLs
         if (url.origin === window.location.origin) {
             return url.pathname + url.search + url.hash;
         }
     } catch {
         // Invalid URL format, fall back to default
     }

     // Default to home page for any invalid or external URLs
     return Routes.HOME;
 }

• Preserve the original URL-parsing & same-origin logic.
• Decode percent-encoded paths before validation; this avoids inputs like /https%3A%2F%2F….
• On the server (SSR), only allow safe relative paths and fall back to HOME for everything else.

apps/web/client/src/app/invitation/[id]/_components/main.tsx (1)

16-16: Avoid duplicate useSearchParams() calls

Reuse the existing searchParams instance to read token.

Apply this diff:

-    const token = useSearchParams().get('token');
+    const token = searchParams.get('token');
apps/web/client/src/server/api/routers/project/invitation.ts (2)

73-78: Also strip token in getWithoutToken instead of overriding with null

Overwriting with null is okay but destructuring it out avoids accidental exposure if shapes change.

Apply this diff:

-        return {
-            ...invitation,
-            token: null,
-            // @ts-expect-error - Drizzle is not typed correctly
-            inviter: toUser(invitation.inviter),
-        };
+        const { token: _omitToken, ...safeInvitation } = invitation as typeof invitation & { token?: string };
+        return {
+            ...safeInvitation,
+            // @ts-expect-error - Drizzle is not typed correctly
+            inviter: toUser(invitation.inviter),
+        };

231-235: Minor: remove redundant truthiness check before delete

invitation is guaranteed truthy here; simplify the expiry delete branch.

Apply this diff:

-            if (isAfter(new Date(), invitation.expiresAt)) {
-                if (invitation) {
-                    await ctx.db
-                        .delete(projectInvitations)
-                        .where(eq(projectInvitations.id, invitation.id));
-                }
+            if (isAfter(new Date(), invitation.expiresAt)) {
+                await ctx.db
+                    .delete(projectInvitations)
+                    .where(eq(projectInvitations.id, invitation.id));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8dcefa9 and d62fc10.

📒 Files selected for processing (9)
  • apps/web/client/src/app/auth/redirect/page.tsx (1 hunks)
  • apps/web/client/src/app/invitation/[id]/_components/auth.tsx (1 hunks)
  • apps/web/client/src/app/invitation/[id]/_components/main.tsx (3 hunks)
  • apps/web/client/src/app/invitation/[id]/layout.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/members/invite-member-input.tsx (3 hunks)
  • apps/web/client/src/app/projects/layout.tsx (2 hunks)
  • apps/web/client/src/components/ui/avatar-dropdown/index.tsx (2 hunks)
  • apps/web/client/src/server/api/routers/project/invitation.ts (9 hunks)
  • apps/web/client/src/utils/url/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/client/src/components/ui/avatar-dropdown/index.tsx
  • apps/web/client/src/app/invitation/[id]/layout.tsx
  • apps/web/client/src/app/project/[id]/_components/members/invite-member-input.tsx
  • apps/web/client/src/app/auth/redirect/page.tsx
🧰 Additional context used
🧬 Code Graph Analysis (4)
apps/web/client/src/utils/url/index.ts (1)
apps/web/client/src/utils/constants/index.ts (2)
  • LocalForageKeys (42-44)
  • Routes (1-23)
apps/web/client/src/app/invitation/[id]/_components/main.tsx (3)
apps/web/server/src/router/trpc.ts (1)
  • router (12-12)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
apps/web/client/src/utils/constants/index.ts (2)
  • Routes (1-23)
  • LocalForageKeys (42-44)
apps/web/client/src/app/projects/layout.tsx (2)
apps/web/client/src/utils/constants/index.ts (1)
  • Routes (1-23)
apps/web/client/src/utils/url/index.ts (1)
  • getReturnUrlQueryParam (3-5)
apps/web/client/src/server/api/routers/project/invitation.ts (6)
packages/db/src/dto/user/user.ts (1)
  • toUser (18-30)
apps/web/client/src/server/api/trpc.ts (1)
  • protectedProcedure (130-149)
packages/db/src/schema/project/invitation.ts (1)
  • projectInvitations (7-27)
packages/db/src/schema/user/user.ts (1)
  • users (12-24)
packages/email/src/invitation.ts (2)
  • sendInvitationEmail (6-24)
  • constructInvitationLink (26-28)
apps/web/client/src/env.ts (1)
  • env (4-133)
🔇 Additional comments (5)
apps/web/client/src/app/projects/layout.tsx (1)

21-21: Sanitization Confirmed in Auth Redirect Flow

I’ve verified that all downstream uses of returnUrl are sanitized before navigation:

  • In apps/web/client/src/app/auth/redirect/page.tsx (lines 14–18), the code retrieves the stored returnUrl, passes it through sanitizeReturnUrl(...), and then calls router.replace(sanitizedUrl).
  • Both the project layout redirect (layout.tsx) and the invitation flow push the raw return URL into the login query, but it’s always processed through sanitizeReturnUrl in the redirect page before any client-side navigation.

No further changes are needed.

apps/web/client/src/app/invitation/[id]/_components/auth.tsx (1)

14-17: LGTM: returnUrl is correctly URL-encoded and centralized

Using getReturnUrlQueryParam ensures proper encoding (preserves token) and keeps behavior consistent across the app.

apps/web/client/src/utils/url/index.ts (1)

3-5: LGTM: query param helper encodes returnUrl

The helper returns a key=value pair with URL-encoded value as expected.

apps/web/client/src/app/invitation/[id]/_components/main.tsx (1)

54-86: Good error UX and recovery options

Clear error messaging with actions to go home or re-auth is solid. Once the login redirect encoding fix is applied, this flow should be reliable.

apps/web/client/src/server/api/routers/project/invitation.ts (1)

23-29: Enforce project-level row-level security on project_invitations

We didn’t find any RLS policies for project_invitations in your DB migrations—please verify that reads are scoped to project members only:

• apps/web/client/src/server/api/routers/project/invitation.ts
– Ensure the protectedProcedure.get endpoint only returns invitations for projects the user belongs to.
• DB migrations (e.g. packages/db/migrations/…)
– Confirm a CREATE POLICY exists on project_invitations for SELECT that restricts rows to the current project’s ID.
• Public invite flow (api.invitation.getWithoutToken)
– Verify it only returns minimal invite data when presented with a valid token and strips any sensitive fields.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/client/src/app/login/actions.tsx (1)

13-14: Harden origin handling; build redirectTo via URL and drop unnecessary await

  • The Origin header can be absent or spoofed; don’t trust it blindly. Prefer a known configured base URL, validate protocol, and construct redirectTo with URL to avoid double slashes.
  • headers() is synchronous; the await is unnecessary.

Proposed refactor:

-    const origin = (await headers()).get('origin') ?? env.NEXT_PUBLIC_SITE_URL;
-    const redirectTo = `${origin}${Routes.AUTH_CALLBACK}`;
+    const originHeader = headers().get('origin') ?? undefined;
+    const base = originHeader ?? env.NEXT_PUBLIC_SITE_URL;
+    let baseUrl: URL;
+    try {
+        baseUrl = new URL(base);
+    } catch {
+        throw new Error('Invalid base URL for OAuth redirect.');
+    }
+    if (!/^https?:$/.test(baseUrl.protocol)) {
+        throw new Error(`Invalid protocol for OAuth redirect: ${baseUrl.protocol}`);
+    }
+    // Use URL to safely join origin and path
+    const redirectTo = new URL(Routes.AUTH_CALLBACK, baseUrl.origin).toString();

Optional: In production, consider ignoring the Origin header and always using env.NEXT_PUBLIC_SITE_URL (or enforcing an allowlist) to mitigate spoofed origins from client-controlled requests.

🧹 Nitpick comments (5)
apps/web/client/src/utils/url/index.ts (2)

11-19: Harden input handling: trim and treat "null"/"undefined" strings as invalid

Minor robustness: user-agents or upstream code can pass " null " or "undefined" strings. Treat them as absent and fall back to HOME. Also operate on the trimmed value thereafter.

Apply this diff:

-    // Default to home page if no return URL
-    if (!returnUrl) {
+    // Default to home page if no return URL
+    if (!returnUrl) {
         return Routes.HOME;
     }
+    // Normalize common bogus values
+    const raw = returnUrl.trim();
+    if (!raw || raw === 'null' || raw === 'undefined') {
+        return Routes.HOME;
+    }
     try {
         // If it's a relative path, it's safe
-        if (returnUrl.startsWith('/') && !returnUrl.startsWith('//')) {
-            return returnUrl;
+        if (raw.startsWith('/') && !raw.startsWith('//')) {
+            return raw;
         }

And update subsequent references to use raw:

-        const url = new URL(returnUrl, currentOrigin);
+        const url = new URL(raw, currentOrigin);

3-5: Optional Refactor: Use URLSearchParams with prefix & sanitization

All current call sites prepend ?, so behavior won’t change, but adding a prefix option (and optional sanitization) makes this utility safer and more ergonomic:

Call sites found (all prepend ? today):

  • apps/web/client/src/app/projects/layout.tsx (line 21)
  • apps/web/client/src/components/ui/avatar-dropdown/index.tsx (line 37)
  • apps/web/client/src/app/invitation/[id]/_components/auth.tsx (line 16)

Suggested diff:

-export function getReturnUrlQueryParam(returnUrl: string | null): string {
-    return returnUrl ? `${LocalForageKeys.RETURN_URL}=${encodeURIComponent(returnUrl)}` : '';
-}
+export function getReturnUrlQueryParam(
+  returnUrl: string | null,
+  opts: { prefix?: '' | '?' | '&'; sanitize?: boolean; origin?: string } = {},
+): string {
+  if (!returnUrl) return '';
+  const prefix = opts.prefix ?? '';
+  const value = opts.sanitize
+    ? sanitizeReturnUrl(returnUrl, { origin: opts.origin })
+    : returnUrl;
+  const params = new URLSearchParams({ [LocalForageKeys.RETURN_URL]: value });
+  return `${prefix}${params.toString()}`;
+}

This preserves existing behavior (default prefix = '') while allowing callers to avoid manual ?/& handling and opt into URL sanitization.

packages/email/src/invitation.ts (1)

17-22: Minor: drop redundant await in return

return await inside an async function adds no value and slightly complicates stack traces.

Apply this diff:

-    return await client.emails.send({
+    return client.emails.send({
         from: 'Onlook <[email protected]>',
         to: inviteeEmail,
         subject: `Join ${invitedByName ?? invitedByEmail} on Onlook`,
         react: InviteUserEmail(inviteParams),
     });
apps/web/client/src/app/login/actions.tsx (2)

29-38: Use route constants for error redirects and guard against missing OAuth URL

  • Replace hard-coded "/error" with the existing Routes constant to keep routing consistent.
  • Add a defensive guard when data.url is missing and log the error for observability.
-            redirectTo,
+            redirectTo,
         },
     });
 
-    if (error) {
-        redirect('/error');
-    }
-
-    redirect(data.url);
+    if (error) {
+        console.error('OAuth sign-in failed:', error);
+        redirect(Routes.AUTH_CODE_ERROR);
+    }
+    if (!data?.url) {
+        console.error('No OAuth URL returned from Supabase.');
+        redirect(Routes.AUTH_CODE_ERROR);
+    }
+    redirect(data.url);

62-63: Post dev sign-in redirect to AUTH_REDIRECT is consistent

Looks good. Optional: consider capturing errors to telemetry (e.g., Sentry) similar to password sign-in error logging.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d62fc10 and d0a5714.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • apps/web/client/src/app/auth/auth-context.tsx (3 hunks)
  • apps/web/client/src/app/login/actions.tsx (4 hunks)
  • apps/web/client/src/app/project/[id]/_components/members/invite-member-input.tsx (3 hunks)
  • apps/web/client/src/utils/url/index.ts (1 hunks)
  • packages/email/src/invitation.ts (1 hunks)
  • packages/models/src/project/index.ts (0 hunks)
  • packages/models/src/project/invitation.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/models/src/project/invitation.ts
  • packages/models/src/project/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/client/src/app/project/[id]/_components/members/invite-member-input.tsx
  • apps/web/client/src/app/auth/auth-context.tsx
🧰 Additional context used
🧬 Code Graph Analysis (3)
packages/email/src/invitation.ts (1)
packages/email/src/templates/invite-user.tsx (1)
  • InviteUserEmail (23-81)
apps/web/client/src/utils/url/index.ts (1)
apps/web/client/src/utils/constants/index.ts (2)
  • LocalForageKeys (42-44)
  • Routes (1-23)
apps/web/client/src/app/login/actions.tsx (2)
apps/web/client/src/env.ts (1)
  • env (4-133)
apps/web/client/src/utils/constants/index.ts (1)
  • Routes (1-23)
🔇 Additional comments (7)
apps/web/client/src/utils/url/index.ts (2)

28-37: Same-origin enforcement and path-only return is solid

Using new URL(..., currentOrigin) and returning only pathname + search + hash prevents open redirects and strips origin. Good defensive posture.


21-26: sanitizeReturnUrl usage limited to client components (no SSR risk)

After searching the codebase (rg -nP -C3 '\bsanitizeReturnUrl\s*\('), the only invocation of sanitizeReturnUrl is in:

  • apps/web/client/src/app/auth/redirect/page.tsx

That file is a client component (it uses localforage and runs in the browser), so window.location.origin is always defined and the SSR fallback to Routes.HOME can’t occur. No changes needed.

packages/email/src/invitation.ts (2)

6-8: Solid refactor to structured inviteParams

Switching to [client, inviteParams, options] and destructuring props improves readability and aligns with the updated template props. Looks good.


25-29: Consider dropping the leading slash to preserve any base path

Using the WHATWG URL API here is spot-on. However, new URL('/invitation/…', publicUrl) will always reset the path to the domain root. If you ever host your site under a sub-path (e.g. https://example.com/app), that /app would be discarded. To retain any base path, remove the leading slash:

Files to update:

  • packages/email/src/invitation.ts

Apply this diff:

--- a/packages/email/src/invitation.ts
+++ b/packages/email/src/invitation.ts
@@ export const constructInvitationLink = (publicUrl: string, invitationId: string, token: string) => {
-    const url = new URL(`/invitation/${invitationId}`, publicUrl);
+    const url = new URL(`invitation/${invitationId}`, publicUrl);
     url.searchParams.set('token', token);
     return url.toString();
 };

Call sites (for reference):

  • apps/web/client/src/app/project/[id]/_components/members/invitation-row.tsx
  • apps/web/client/src/server/api/routers/project/invitation.ts

If your NEXT_PUBLIC_SITE_URL (or any other base-URL) never includes a path segment, this is purely optional. Otherwise, please apply the change.

apps/web/client/src/app/login/actions.tsx (3)

3-4: Imports of env and Routes are appropriate

Good move using centralized env validation and route constants to avoid magic strings.


21-22: Session short-circuit to AUTH_REDIRECT looks good

Keeps the flow consistent with the new post-login redirect handling.


50-51: Dev session short-circuit to AUTH_REDIRECT is consistent with the new flow

No issues. Guard against non-dev environments is correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
apps/web/client/src/app/login/actions.tsx (1)

13-14: Remove unnecessary await on headers() and harden redirectTo with URL + protocol validation

  • headers() is synchronous; awaiting it is unnecessary and may fail TS checks.
  • Build redirectTo with URL to avoid double slashes.
  • Validate base URL protocol (http/https only) and use a robust fallback.

This also addresses the earlier feedback about origin fallback and safe URL join.

Apply this diff:

-    const origin = (await headers()).get('origin') ?? env.NEXT_PUBLIC_SITE_URL;
-    const redirectTo = `${origin}${Routes.AUTH_CALLBACK}`;
+    const headerList = headers();
+    const originHeader = headerList.get('origin') ?? null;
+    const base = originHeader ?? env.NEXT_PUBLIC_SITE_URL;
+    const baseUrl = new URL(base);
+    if (!/^https?:$/.test(baseUrl.protocol)) {
+        throw new Error(`Invalid base URL protocol for OAuth redirect: ${baseUrl.protocol}`);
+    }
+    const redirectTo = new URL(Routes.AUTH_CALLBACK, baseUrl.origin).toString();

Optionally, consider logging when falling back to env.NEXT_PUBLIC_SITE_URL for observability.

To ensure there aren’t other instances of unnecessary await headers():

#!/bin/bash
rg -nP --type=ts --type=tsx -C2 'await\s+headers\s*\(\s*\)'
apps/web/client/src/app/auth/auth-context.tsx (2)

17-18: Type fix landed: async handlers now return Promise

This resolves the TS mismatch noted earlier.


36-42: Correct ordering: persist before invoking server action

Saving RETURN_URL and LAST_SIGN_IN_METHOD_KEY before calling login ensures values persist even when the Server Action redirects immediately. Nice.

apps/web/client/src/utils/url/index.ts (1)

7-38: Solid open-redirect mitigation with same-origin enforcement

sanitizeReturnUrl correctly:

  • treats single-slash relative paths as safe
  • rejects scheme-relative and cross-origin URLs
  • uses server-provided origin when available
    This addresses prior concerns about validating returnUrl before redirect.
🧹 Nitpick comments (5)
apps/web/client/src/app/login/actions.tsx (1)

33-38: Use an auth error route and guard against missing data.url

Redirecting to a generic '/error' loses context. Prefer the dedicated auth error route and handle the edge where data.url is unexpectedly empty.

Apply this diff:

-    if (error) {
-        redirect('/error');
-    }
-
-    redirect(data.url);
+    if (error) {
+        redirect(Routes.AUTH_CODE_ERROR);
+    }
+    if (!data?.url) {
+        redirect(Routes.AUTH_CODE_ERROR);
+    }
+    redirect(data.url);
apps/web/client/src/app/auth/auth-context.tsx (1)

48-53: Persist DEV as last sign-in method for consistency

Dev flow mirrors OAuth flow but doesn’t currently persist LAST_SIGN_IN_METHOD_KEY. Persisting SignInMethod.DEV keeps UX consistent (e.g., remembering last method).

Apply this diff:

     const handleDevLogin = async (returnUrl: string | null) => {
         setSigningInMethod(SignInMethod.DEV);
         if (returnUrl) {
             await localforage.setItem(LocalForageKeys.RETURN_URL, returnUrl);
         }
+        await localforage.setItem(LAST_SIGN_IN_METHOD_KEY, SignInMethod.DEV);
         await devLogin();
         setTimeout(() => {
             setSigningInMethod(null);
         }, 5000);
     }
apps/web/client/src/utils/url/index.ts (3)

3-5: Query param helper is fine; consider returning with leading '?'

Current return string is 'returnUrl=...'. Call sites need to prepend '?' or '&'. If most consumers need the leading '?', consider returning it here to reduce repetition.

Example alternative:

export function getReturnUrlQueryParam(returnUrl: string | null): string {
    return returnUrl ? `?${LocalForageKeys.RETURN_URL}=${encodeURIComponent(returnUrl)}` : '';
}

11-19: Minor hardening: trim input before checks

Trimming handles accidental whitespace and avoids false negatives in startsWith('/') and URL parsing.

Apply this diff:

-    // Default to home page if no return URL
-    if (!returnUrl) {
+    // Default to home page if no return URL
+    const raw = returnUrl?.trim();
+    if (!raw) {
         return Routes.HOME;
     }
     try {
         // If it's a relative path, it's safe
-        if (returnUrl.startsWith('/') && !returnUrl.startsWith('//')) {
-            return returnUrl;
+        if (raw.startsWith('/') && !raw.startsWith('//')) {
+            return raw;
         }
         // Resolve current origin from options or the browser (if available)
         const currentOrigin =
             opts.origin ?? (typeof window !== 'undefined' ? window.location.origin : undefined);
         // On the server (no origin), reject non-relative URLs
         if (!currentOrigin) {
             return Routes.HOME;
         }
         // Parse as URL to check if it's same-origin
-        const url = new URL(returnUrl, currentOrigin);
+        const url = new URL(raw, currentOrigin);

Also applies to: 21-32


7-38: Add focused unit tests for sanitizeReturnUrl

Given its security role, add tests covering:

  • '/', '/projects', '/projects?id=1#tab'
  • '//evil.com', 'http://evil.com', 'https://evil.com'
  • absolute same-origin vs different port
  • whitespace-wrapped values
  • server-side path with opts.origin provided vs missing

I can draft a small vitest/jest test suite for this helper. Want me to open a follow-up PR with tests?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d62fc10 and d0a5714.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • apps/web/client/src/app/auth/auth-context.tsx (3 hunks)
  • apps/web/client/src/app/login/actions.tsx (4 hunks)
  • apps/web/client/src/app/project/[id]/_components/members/invite-member-input.tsx (3 hunks)
  • apps/web/client/src/utils/url/index.ts (1 hunks)
  • packages/email/src/invitation.ts (1 hunks)
  • packages/models/src/project/index.ts (0 hunks)
  • packages/models/src/project/invitation.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/models/src/project/index.ts
  • packages/models/src/project/invitation.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/email/src/invitation.ts
  • apps/web/client/src/app/project/[id]/_components/members/invite-member-input.tsx
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/web/client/src/utils/url/index.ts (1)
apps/web/client/src/utils/constants/index.ts (2)
  • LocalForageKeys (42-44)
  • Routes (1-23)
apps/web/client/src/app/login/actions.tsx (2)
apps/web/client/src/env.ts (1)
  • env (4-133)
apps/web/client/src/utils/constants/index.ts (1)
  • Routes (1-23)
apps/web/client/src/app/auth/auth-context.tsx (2)
apps/web/client/src/utils/constants/index.ts (1)
  • LocalForageKeys (42-44)
apps/web/client/src/app/login/actions.tsx (1)
  • login (11-38)
🔇 Additional comments (6)
apps/web/client/src/app/login/actions.tsx (5)

3-4: Good move: centralized env and route constants

Importing env and Routes improves consistency and reduces hard-coded strings across auth flows.


21-21: Consistent redirect target

Redirecting existing sessions to Routes.AUTH_REDIRECT aligns with the new centralized flow.


29-31: LGTM: passing computed redirectTo to Supabase OAuth

Using the computed redirectTo keeps the OAuth callback aligned with the current origin/fallback.


50-50: Consistent redirect on existing dev session

Using Routes.AUTH_REDIRECT here keeps dev flow behavior aligned with the main flow.


62-62: Dev login completion redirect is consistent

Redirecting to Routes.AUTH_REDIRECT after successful dev login matches the centralized auth strategy.

apps/web/client/src/app/auth/auth-context.tsx (1)

28-34: Lazy-load last sign-in method

Fetching and setting lastSignInMethod asynchronously on mount is clean and avoids blocking render.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
apps/web/client/src/components/store/editor/version/git.ts (1)

65-72: Consider using a dedicated no-reply/automation email for commit authorship

Using SUPPORT_EMAIL for git author email can conflate commit authorship with the support channel (e.g., in blame/logs). If feasible, consider introducing a dedicated NO_REPLY_EMAIL or BOT_EMAIL for automated commits to clarify provenance.

apps/web/client/src/app/project/[id]/_components/left-panel/help-dropdown/index.tsx (1)

131-131: Prefer setting window.location for mailto instead of window.open

window.open with a mailto: can be blocked by popup blockers and target "_blank" is not meaningful here. Setting location is simpler and more reliable.

Apply this diff:

-                            onClick={() => window.open(`mailto:${SUPPORT_EMAIL}`, '_blank')}
+                            onClick={() => {
+                                window.location.href = `mailto:${SUPPORT_EMAIL}`;
+                            }}
apps/web/client/src/app/project/[id]/layout.tsx (3)

7-9: Type cleanup: params should not be a Promise; avoid unnecessary await

Next.js passes params as a plain object. Typing it as Promise adds noise and requires an unnecessary await.

Apply this diff:

-export default async function Layout({ params, children }: Readonly<{ params: Promise<{ id: string }>, children: React.ReactNode }>) {
-    const projectId = (await params).id;
+export default async function Layout({
+    params,
+    children,
+}: Readonly<{ params: { id: string }; children: React.ReactNode }>) {
+    const { id: projectId } = params;

9-12: Handle TRPC errors to avoid surfacing 500s on unauthorized/unauthenticated

If api.project.hasAccess throws (e.g., UNAUTHORIZED), the layout could error. Treat errors as “no access” or redirect, depending on your desired UX.

Apply this diff to fail closed without a 500:

-    const hasAccess = await api.project.hasAccess({ projectId });
-    if (!hasAccess) {
+    let hasAccess = false;
+    try {
+        hasAccess = await api.project.hasAccess({ projectId });
+    } catch {
+        hasAccess = false;
+    }
+    if (!hasAccess) {
         return <NoAccess />;
     }

If the intended behavior is to redirect unauthenticated users to login instead, say the word and I’ll adjust the snippet to use next/navigation’s redirect with your Routes.LOGIN.


24-29: Use a plain anchor for mailto instead of Next.js Link

Link is intended for client-side routing of internal pages; for mailto, a simple is more appropriate and avoids Next’s prefetch logic.

Apply this diff:

-                        <Link href={`mailto:${SUPPORT_EMAIL}`} className="text-primary underline">
-                            {SUPPORT_EMAIL}
-                        </Link>
+                        <a
+                            href={`mailto:${SUPPORT_EMAIL}`}
+                            className="text-primary underline"
+                        >
+                            {SUPPORT_EMAIL}
+                        </a>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d0a5714 and a0c9e13.

📒 Files selected for processing (8)
  • apps/web/client/src/app/auth/callback/route.ts (2 hunks)
  • apps/web/client/src/app/invitation/[id]/_components/main.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/left-panel/help-dropdown/index.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/layout.tsx (1 hunks)
  • apps/web/client/src/components/store/editor/version/git.ts (3 hunks)
  • packages/constants/src/contact.ts (1 hunks)
  • packages/constants/src/index.ts (1 hunks)
  • packages/email/src/invitation.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/constants/src/contact.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/client/src/app/auth/callback/route.ts
  • packages/email/src/invitation.ts
  • apps/web/client/src/app/invitation/[id]/_components/main.tsx
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/web/client/src/components/store/editor/version/git.ts (1)
packages/constants/src/contact.ts (1)
  • SUPPORT_EMAIL (1-1)
apps/web/client/src/app/project/[id]/layout.tsx (4)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
packages/constants/src/contact.ts (1)
  • SUPPORT_EMAIL (1-1)
apps/web/client/src/utils/constants/index.ts (1)
  • Routes (1-23)
packages/ui/src/components/icons/index.tsx (1)
  • Icons (138-3503)
apps/web/client/src/app/project/[id]/_components/left-panel/help-dropdown/index.tsx (1)
packages/constants/src/contact.ts (1)
  • SUPPORT_EMAIL (1-1)
🔇 Additional comments (4)
apps/web/client/src/components/store/editor/version/git.ts (2)

5-5: Good move: centralize git config email via SUPPORT_EMAIL

Replacing the hard-coded email with the shared SUPPORT_EMAIL constant improves maintainability and avoids drift.

Also applies to: 67-68


20-20: Format-only change

Constructor spacing/nit is fine; no action needed.

packages/constants/src/index.ts (1)

2-2: Barrel re-export looks correct

Re-exporting contact makes SUPPORT_EMAIL available app-wide via @onlook/constants. Consistent with other barrel exports.

apps/web/client/src/app/project/[id]/_components/left-panel/help-dropdown/index.tsx (1)

3-3: Centralized support email import is a win

Importing SUPPORT_EMAIL from @onlook/constants prevents hard-coding and keeps contact info consistent across the app.

@vercel vercel bot temporarily deployed to Preview – docs August 18, 2025 20:46 Inactive
@vercel vercel bot temporarily deployed to Preview – docs August 18, 2025 20:51 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/client/src/app/project/[id]/_components/members/invitation-row.tsx (1)

23-36: Fix clipboard error handling: writeText is async; current try/catch won’t catch rejections

navigator.clipboard.writeText returns a Promise. Without awaiting it, async failures won’t hit the catch block, and the UI will optimistically show “copied.” Also add a non-secure-context fallback.

Apply this diff:

-    const copyInvitationLink = () => {
-        try {
-            navigator.clipboard.writeText(constructInvitationLink(env.NEXT_PUBLIC_SITE_URL, invitation.id, invitation.token));
-            setIsCopied(true);
-            toast.success('Invitation link copied to clipboard');
-            setTimeout(() => {
-                setIsCopied(false);
-            }, 2000);
-        } catch (error) {
-            console.error('Failed to copy invitation link:', error);
-            toast.error('Failed to copy invitation link');
-            setIsCopied(false);
-        }
-    };
+    const copyInvitationLink = async () => {
+        try {
+            const link = constructInvitationLink(
+                env.NEXT_PUBLIC_SITE_URL,
+                invitation.id,
+                invitation.token
+            );
+            if (navigator.clipboard && window.isSecureContext) {
+                await navigator.clipboard.writeText(link);
+            } else {
+                // Fallback for HTTP/non-secure contexts and older browsers
+                const el = document.createElement('textarea');
+                el.value = link;
+                el.setAttribute('readonly', '');
+                el.style.position = 'fixed';
+                el.style.opacity = '0';
+                document.body.appendChild(el);
+                el.focus();
+                el.select();
+                document.execCommand('copy');
+                document.body.removeChild(el);
+            }
+            setIsCopied(true);
+            toast.success('Invitation link copied to clipboard');
+            setTimeout(() => {
+                setIsCopied(false);
+            }, 2000);
+        } catch (error) {
+            console.error('Failed to copy invitation link:', error);
+            toast.error('Failed to copy invitation link');
+            setIsCopied(false);
+        }
+    };
🧹 Nitpick comments (3)
apps/web/client/src/app/project/[id]/_components/members/invitation-row.tsx (3)

17-21: Add onError and user feedback for cancel mutation

Surface success/error to users and avoid silent failures.

-    const cancelInvitationMutation = api.invitation.delete.useMutation({
-        onSuccess: () => {
-            apiUtils.invitation.list.invalidate();
-        },
-    });
+    const cancelInvitationMutation = api.invitation.delete.useMutation({
+        onSuccess: () => {
+            toast.success('Invitation canceled');
+            apiUtils.invitation.list.invalidate();
+        },
+        onError: (err) => {
+            toast.error(err?.message ?? 'Failed to cancel invitation');
+        },
+    });

51-56: A11y: icon-only buttons need accessible names; also prevent accidental form submit

Add aria-labels to icon-only buttons. Set type="button" to prevent form submit if rendered inside a form. Optionally disable the cancel action while the mutation is pending.

-                        <Button
+                        <Button
+                            type="button"
                             variant="ghost"
                             size="icon"
+                            aria-label={isCopied ? 'Copied to clipboard' : 'Copy invitation link'}
                             onClick={copyInvitationLink}
                         >
...
-                        <Button
+                        <Button
+                            type="button"
                             variant="ghost"
                             size="icon"
+                            aria-label="Cancel invitation"
+                            disabled={cancelInvitationMutation.isPending}
                             onClick={() => {
                                 cancelInvitationMutation.mutate({ id: invitation.id });
                             }}
                         >

Also applies to: 65-72


28-31: Optional: clear timeout on unmount to avoid state updates after unmount

The setTimeout can resolve after unmount, causing a setState on an unmounted component. Track the timer and clear it on unmount.

-            setTimeout(() => {
-                setIsCopied(false);
-            }, 2000);
+            resetTimerRef.current = window.setTimeout(() => {
+                setIsCopied(false);
+            }, 2000);

Add the following outside the selected lines:

// 1) Update import
import { useEffect, useRef, useState } from 'react';

// 2) Inside component
const resetTimerRef = useRef<number | undefined>(undefined);

useEffect(() => {
  return () => {
    if (resetTimerRef.current) {
      clearTimeout(resetTimerRef.current);
    }
  };
}, []);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a0c9e13 and dc75913.

📒 Files selected for processing (1)
  • apps/web/client/src/app/project/[id]/_components/members/invitation-row.tsx (2 hunks)

@Kitenite Kitenite merged commit a91b9bc into main Aug 18, 2025
7 checks passed
@Kitenite Kitenite deleted the feat/restrict-project-access branch August 18, 2025 20:56
@Kitenite Kitenite restored the feat/restrict-project-access branch August 19, 2025 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant