Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ yarn-error.log*
.mastra/

# Ignore preload script changes unless the script should be updated. Uncomment to update.
/public/onlook-preload-script.js
/public/onlook-preload-script.*
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Broader ignore pattern for preload script LGTM; ensure previously tracked files are untracked

Good call generalizing to onlook-preload-script.*. If any such files were already tracked, remove them from the index so the ignore takes effect.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking for tracked preload script files under apps/web/client/public..."
git ls-files -z | rg -z -nP '^apps/web/client/public/onlook-preload-script\.' || true
echo "If any paths appear above, untrack them with:"
echo "  git rm --cached <path>"

Length of output: 478


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Listing any tracked preload script files under apps/web/client/public:"
git ls-files 'apps/web/client/public/onlook-preload-script.*' || true

Length of output: 282


Remove tracked preload script so the new ignore pattern takes effect

The file apps/web/client/public/onlook-preload-script.js is still in the index, so your .gitignore won’t ignore future changes. Please untrack it:

• Tracked file:

  • apps/web/client/public/onlook-preload-script.js

Run:

git rm --cached apps/web/client/public/onlook-preload-script.js

and commit the removal.

🤖 Prompt for AI Agents
In apps/web/client/.gitignore around line 52 the new pattern
/public/onlook-preload-script.* will not take effect because
apps/web/client/public/onlook-preload-script.js is still tracked; untrack the
file by running git rm --cached apps/web/client/public/onlook-preload-script.js
(or use your GUI equivalent) and then commit the removal so future changes are
ignored.

4 changes: 1 addition & 3 deletions apps/web/client/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import path from 'node:path';
import './src/env';

const nextConfig: NextConfig = {
devIndicators: {
buildActivity: false,
},
devIndicators: false,
eslint: {
ignoreDuringBuilds: true,
},
Expand Down
2 changes: 0 additions & 2 deletions apps/web/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
"clsx": "^2.1.1",
"culori": "^4.0.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"embla-carousel-react": "^8.6.0",
"flexsearch": "^0.8.160",
"freestyle-sandboxes": "^0.0.78",
Expand Down Expand Up @@ -99,7 +98,6 @@
"superjson": "^2.2.1",
"tailwind-merge": "^3.2.0",
"tw-animate-css": "^1.2.5",
"url-join": "^5.0.0",
"use-resize-observer": "^9.1.0",
"uuid": "^11.1.0",
"webfontloader": "^1.6.28",
Expand Down
12 changes: 9 additions & 3 deletions apps/web/client/src/app/_components/login-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { useAuthContext } from '../auth/auth-context';

export const GithubLoginButton = ({
className,
returnUrl,
}: {
className?: string;
returnUrl: string | null;
}) => {
const t = useTranslations();
const { lastSignInMethod, handleLogin, signingInMethod } = useAuthContext();
Expand All @@ -26,7 +28,7 @@ export const GithubLoginButton = ({
? 'bg-teal-100 dark:bg-teal-950 border-teal-300 dark:border-teal-700 text-teal-900 dark:text-teal-100 text-small hover:bg-teal-200/50 dark:hover:bg-teal-800 hover:border-teal-500/70 dark:hover:border-teal-500'
: 'bg-background-onlook',
)}
onClick={() => handleLogin(SignInMethod.GITHUB)}
onClick={() => handleLogin(SignInMethod.GITHUB, returnUrl)}
disabled={!!signingInMethod}
>
{isSigningIn ? (
Expand All @@ -45,8 +47,10 @@ export const GithubLoginButton = ({

export const GoogleLoginButton = ({
className,
returnUrl,
}: {
className?: string;
returnUrl: string | null;
}) => {
const t = useTranslations();
const { lastSignInMethod, handleLogin, signingInMethod } = useAuthContext();
Expand All @@ -63,7 +67,7 @@ export const GoogleLoginButton = ({
? 'bg-teal-100 dark:bg-teal-950 border-teal-300 dark:border-teal-700 text-teal-900 dark:text-teal-100 text-small hover:bg-teal-200/50 dark:hover:bg-teal-800 hover:border-teal-500/70 dark:hover:border-teal-500'
: 'bg-background-onlook',
)}
onClick={() => handleLogin(SignInMethod.GOOGLE)}
onClick={() => handleLogin(SignInMethod.GOOGLE, returnUrl)}
disabled={!!signingInMethod}
>
{isSigningIn ? (
Expand All @@ -82,8 +86,10 @@ export const GoogleLoginButton = ({

export const DevLoginButton = ({
className,
returnUrl,
}: {
className?: string;
returnUrl: string | null;
}) => {
const t = useTranslations();
const { handleDevLogin, signingInMethod } = useAuthContext();
Expand All @@ -93,7 +99,7 @@ export const DevLoginButton = ({
<Button
variant="outline"
className="w-full text-active text-small"
onClick={handleDevLogin}
onClick={() => handleDevLogin(returnUrl)}
disabled={!!signingInMethod}
>
{isSigningIn ? (
Expand Down
12 changes: 6 additions & 6 deletions apps/web/client/src/app/auth/auth-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ interface AuthContextType {
lastSignInMethod: SignInMethod | null;
isAuthModalOpen: boolean;
setIsAuthModalOpen: (open: boolean) => void;
handleLogin: (method: SignInMethod.GITHUB | SignInMethod.GOOGLE) => void;
handleDevLogin: () => void;
handleLogin: (method: SignInMethod.GITHUB | SignInMethod.GOOGLE, returnUrl: string | null) => void;
handleDevLogin: (returnUrl: string | null) => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);
Expand All @@ -30,19 +30,19 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
});
}, []);

const handleLogin = async (method: SignInMethod.GITHUB | SignInMethod.GOOGLE) => {
const handleLogin = async (method: SignInMethod.GITHUB | SignInMethod.GOOGLE, returnUrl: string | null) => {
setSigningInMethod(method);
await login(method);
await login(method, returnUrl);

localforage.setItem(LAST_SIGN_IN_METHOD_KEY, method);
setTimeout(() => {
setSigningInMethod(null);
}, 5000);
};

const handleDevLogin = async () => {
const handleDevLogin = async (returnUrl: string | null) => {
setSigningInMethod(SignInMethod.DEV);
await devLogin();
await devLogin(returnUrl);
setTimeout(() => {
setSigningInMethod(null);
}, 5000);
Expand Down
10 changes: 4 additions & 6 deletions apps/web/client/src/app/auth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { api } from '~/trpc/server';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
const returnUrl = searchParams.get('returnUrl');

if (code) {
const supabase = await createClient();
const { error, data } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
const forwardedHost = request.headers.get('x-forwarded-host'); // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === 'development';
const user = await api.user.upsert({
id: data.user.id,
});
Expand All @@ -36,12 +36,10 @@ export async function GET(request: Request) {
});

// Redirect to the redirect page which will handle the return URL
if (isLocalEnv) {
return NextResponse.redirect(`${origin}/auth/redirect`);
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}/auth/redirect`);
if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${returnUrl || '/'}`);
} else {
return NextResponse.redirect(`${origin}/auth/redirect`);
return NextResponse.redirect(`${origin}${returnUrl || '/'}`);
}
}
console.error(`Error exchanging code for session: ${error}`);
Expand Down
29 changes: 29 additions & 0 deletions apps/web/client/src/app/invitation/[id]/_components/auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client';
import { Routes } from '@/utils/constants';
import { Button } from '@onlook/ui/button';
import { Icons } from '@onlook/ui/icons/index';
import { redirect, usePathname, useSearchParams } from 'next/navigation';

export const HandleAuth = () => {
const pathname = usePathname();
const searchParams = useSearchParams();

const handleLogin = () => {
const currentUrl = `${pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
redirect(`${Routes.LOGIN}?returnUrl=${currentUrl}`);
}

return (
<div className="flex justify-center items-center h-screen">
<div className="flex flex-col items-center justify-center gap-4">
<div className="text-2xl">You must be logged in to accept this invitation</div>
<Button variant="outline"
onClick={handleLogin}
>
<Icons.OnlookLogo className="size-4" />
Login or Signup
</Button>
</div>
</div>
)
}
53 changes: 45 additions & 8 deletions apps/web/client/src/app/invitation/[id]/_components/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@ import { useRouter, useSearchParams } from 'next/navigation';
export function Main({ invitationId }: { invitationId: string }) {
const router = useRouter();
const token = useSearchParams().get('token');
const { data: invitation, isLoading: loadingInvitation } = api.invitation.get.useQuery({
const { data: invitation, isLoading: loadingInvitation, error: getInvitationError } = api.invitation.get.useQuery({
id: invitationId,
});
const acceptInvitationMutation = api.invitation.accept.useMutation({

const { mutate: acceptInvitation, isPending: isAcceptingInvitation, error: acceptInvitationError } = api.invitation.accept.useMutation({
onSuccess: () => {
router.push(Routes.PROJECTS);
if (invitation?.projectId) {
router.push(`${Routes.PROJECTS}/${invitation.projectId}`);
} else {
router.push(Routes.PROJECTS);
}
},
});

const error = getInvitationError || acceptInvitationError;

if (loadingInvitation) {
return (
<div className="flex justify-center w-full h-full">
Expand All @@ -34,12 +41,42 @@ export function Main({ invitationId }: { invitationId: string }) {
);
}

if (error) {
return (
<div className="flex flex-row w-full">
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="flex items-center gap-4">
<Icons.ExclamationTriangle className="h-6 w-6" />
<div className="text-2xl">Error accepting invitation</div>
</div>
<div className="text-md">
{error.message}
</div>
<div className="flex justify-center">
<Button
type="button"
onClick={() => {
router.push(Routes.PROJECTS);
}}
>
<Icons.ArrowLeft className="h-4 w-4" />
Back to home
</Button>
</div>
</div>
</div>
);
}

if (!invitation || !token) {
return (
<div className="flex flex-row w-full">
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="text-xl text-foreground-secondary">Invitation not found</div>
<div className="text-md text-foreground-tertiary">
<div className="flex items-center gap-4">
<Icons.ExclamationTriangle className="h-6 w-6" />
<div className="text-xl">Invitation not found</div>
</div>
<div className="text-md">
The invitation you are looking for does not exist or has expired.
</div>
<div className="flex justify-center">
Expand Down Expand Up @@ -69,14 +106,14 @@ export function Main({ invitationId }: { invitationId: string }) {
<Button
type="button"
onClick={() => {
acceptInvitationMutation.mutate({
acceptInvitation({
id: invitationId,
token: invitation.token,
});
}}
disabled={acceptInvitationMutation.isPending}
disabled={isAcceptingInvitation}
>
Join Project
Accept Invitation
</Button>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions apps/web/client/src/app/invitation/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { type Metadata } from 'next';
import { HandleAuth } from './_components/auth';
import { createClient } from '@/utils/supabase/server';

export const metadata: Metadata = {
title: 'Onlook',
description: 'Onlook – Invitation',
};

export default async function Layout({ children }: Readonly<{ children: React.ReactNode }>) {
const supabase = await createClient();
const {
data: { session },
} = await supabase.auth.getSession();

if (!session) {
return <HandleAuth />;
}
return (
<div className="w-screen h-screen flex flex-col items-center justify-center">
{children}
Expand Down
1 change: 0 additions & 1 deletion apps/web/client/src/app/invitation/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ import { Main } from './_components/main';

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const id = (await params).id;

return <Main invitationId={id} />;
}
12 changes: 6 additions & 6 deletions apps/web/client/src/app/login/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SignInMethod } from '@onlook/models';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';

export async function login(provider: SignInMethod.GITHUB | SignInMethod.GOOGLE) {
export async function login(provider: SignInMethod.GITHUB | SignInMethod.GOOGLE, returnUrl: string | null) {
const supabase = await createClient();
const origin = (await headers()).get('origin');

Expand All @@ -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.

}

// Start OAuth flow
// Note: User object will be created in the auth callback route if it doesn't exist
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${origin}/auth/callback`,
redirectTo: `${origin}/auth/callback${returnUrl ? `?returnUrl=${returnUrl}` : ''}`,
},
});

Expand All @@ -34,7 +34,7 @@ export async function login(provider: SignInMethod.GITHUB | SignInMethod.GOOGLE)
redirect(data.url);
}

export async function devLogin() {
export async function devLogin(returnUrl: string | null) {
if (process.env.NODE_ENV !== 'development') {
throw new Error('Dev login is only available in development mode');
}
Expand All @@ -44,7 +44,7 @@ export async function devLogin() {
const { data: { session } } = await supabase.auth.getSession();

if (session) {
redirect('/');
redirect(returnUrl || '/');
}

const { data, error } = await supabase.auth.signInWithPassword({
Expand All @@ -56,5 +56,5 @@ export async function devLogin() {
console.error('Error signing in with password:', error);
throw new Error('Error signing in with password');
}
redirect('/');
redirect(returnUrl || '/');
}
10 changes: 5 additions & 5 deletions apps/web/client/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { Icons } from '@onlook/ui/icons';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { DevLoginButton, GithubLoginButton, GoogleLoginButton } from '../_components/login-button';
import { useAuthContext } from '../auth/auth-context';

export default function LoginPage() {
const isDev = process.env.NODE_ENV === 'development';
const t = useTranslations();
const { handleDevLogin } = useAuthContext();
const backgroundUrl = useGetBackground('login');
const returnUrl = useSearchParams().get('returnUrl');

return (
<div className="flex h-screen w-screen" >
Expand All @@ -34,10 +34,10 @@ export default function LoginPage() {
</p>
</div>
<div className="space-x-2 flex flex-row">
<GithubLoginButton />
<GoogleLoginButton />
<GithubLoginButton returnUrl={returnUrl} />
<GoogleLoginButton returnUrl={returnUrl} />
</div>
{isDev && <DevLoginButton />}
{isDev && <DevLoginButton returnUrl={returnUrl} />}
<p className="text-small text-foreground-onlook">
{t(transKeys.welcome.terms.agreement)}{' '}
<Link
Expand Down
Loading