Skip to content

Commit d42ef6e

Browse files
committed
setup
1 parent b015a80 commit d42ef6e

File tree

11 files changed

+337
-8
lines changed

11 files changed

+337
-8
lines changed

web/app/(api)/auth/callback/route.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from 'next/server';
2+
3+
// The client you created from the Server-Side Auth instructions
4+
import { createClient } from '@/utils/supabase/server';
5+
6+
export async function GET(request: Request) {
7+
const { searchParams, origin } = new URL(request.url);
8+
const code = searchParams.get('code');
9+
// if "next" is in param, use it as the redirect URL
10+
const next = searchParams.get('next') ?? '/';
11+
12+
if (code) {
13+
const supabase = await createClient();
14+
const { error } = await supabase.auth.exchangeCodeForSession(code);
15+
if (!error) {
16+
const forwardedHost = request.headers.get('x-forwarded-host'); // original origin before load balancer
17+
const isLocalEnv = process.env.NODE_ENV === 'development';
18+
if (isLocalEnv) {
19+
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
20+
return NextResponse.redirect(`${origin}${next}`);
21+
} else if (forwardedHost) {
22+
return NextResponse.redirect(`https://${forwardedHost}${next}`);
23+
} else {
24+
return NextResponse.redirect(`${origin}${next}`);
25+
}
26+
}
27+
}
28+
29+
// return the user to an error page with instructions
30+
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
31+
}

web/app/layout.tsx

+14-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import type { Metadata } from "next";
2-
import { Inter } from "next/font/google";
3-
import "./globals.css";
4-
import { Footer } from "@/components/Footer";
1+
import type { Metadata } from 'next';
2+
import { Inter } from 'next/font/google';
53

6-
const inter = Inter({ subsets: ["latin"] });
4+
import { Footer } from '@/components/Footer';
5+
import { Nav } from '@/components/Nav';
6+
7+
import './globals.css';
8+
9+
const inter = Inter({ subsets: ['latin'] });
710

811
export const metadata: Metadata = {
9-
title: "Fast Youtube Summary",
10-
description: "Summarize YouTube videos for free",
12+
title: 'Fast Youtube Summary',
13+
description: 'Summarize YouTube videos for free',
1114
};
1215

1316
export default function RootLayout({
@@ -16,9 +19,12 @@ export default function RootLayout({
1619
children: React.ReactNode;
1720
}>) {
1821
return (
19-
<html lang="en">
22+
<html lang='en'>
2023
<body className={inter.className}>
24+
<Nav />
25+
<main className="min-h-[80vh]">
2126
{children}
27+
</main>
2228
<Footer />
2329
</body>
2430
</html>

web/app/login/page.tsx

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use client';
2+
3+
import Link from 'next/link';
4+
5+
import { GoogleIcon } from '@/components/icons/GoogleIcon';
6+
import { createClient } from '@/utils/supabase/client';
7+
8+
export default function Login() {
9+
const supabase = createClient();
10+
11+
// Google OAuth login handler
12+
const handleGoogleLogin = async () => {
13+
const { error } = await supabase.auth.signInWithOAuth({
14+
provider: 'google',
15+
options: {
16+
redirectTo: `${window.location.origin}/auth/callback`,
17+
},
18+
});
19+
if (error) {
20+
console.error('Google login failed:', error);
21+
}
22+
};
23+
24+
return (
25+
<section className='mx-auto mt-32 flex flex-col justify-between items-center'>
26+
<div className='w-full max-w-md border-2 border-black p-8'>
27+
<Link
28+
href='/'
29+
className='mb-8 flex items-center space-x-2 text-black no-underline hover:underline'
30+
>
31+
<svg
32+
xmlns='http://www.w3.org/2000/svg'
33+
width='24'
34+
height='24'
35+
viewBox='0 0 24 24'
36+
fill='none'
37+
stroke='currentColor'
38+
strokeWidth='2'
39+
strokeLinecap='round'
40+
strokeLinejoin='round'
41+
>
42+
<polyline points='15 18 9 12 15 6' />
43+
</svg>
44+
<span>Back</span>
45+
</Link>
46+
47+
<button
48+
type='button'
49+
onClick={handleGoogleLogin}
50+
className='mt-4 flex w-full items-center justify-center space-x-2 border border-black px-4 py-2'
51+
>
52+
<GoogleIcon className='h-6 w-6' />
53+
<span>Sign In with Google</span>
54+
</button>
55+
</div>
56+
</section>
57+
);
58+
}

web/components/Nav.tsx

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* v0 by Vercel.
3+
* @see https://v0.dev/t/xYHqD5MkVkT
4+
* Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app
5+
*/
6+
import Image from 'next/image';
7+
import Link from 'next/link';
8+
9+
import { Button } from '@/components/ui/button';
10+
import { createClient } from '@/utils/supabase/server';
11+
import AuthButton from './auth/AuthButton';
12+
13+
export const Nav = async () => {
14+
const supabase = await createClient();
15+
16+
const {
17+
data: { user },
18+
} = await supabase.auth.getUser();
19+
20+
console.log(user);
21+
return (
22+
<nav className='fixed inset-x-0 top-0 z-50 bg-white shadow-sm dark:bg-gray-950/90'>
23+
<div className='mx-auto w-full max-w-7xl px-4'>
24+
<div className='flex h-14 items-center justify-between'>
25+
<Link href='#' className='flex items-center' prefetch={false}>
26+
<Image
27+
src='/logo.png'
28+
alt='GitHub Logo'
29+
width={30}
30+
height={30}
31+
className='mx-auto'
32+
/>
33+
<span className='sr-only'>Acme Inc</span>
34+
</Link>
35+
<nav className='hidden gap-4 md:flex'>
36+
{/* <Link
37+
href='#'
38+
className='flex items-center text-sm font-medium transition-colors hover:underline'
39+
prefetch={false}
40+
>
41+
Home
42+
</Link> */}
43+
</nav>
44+
<div className='flex items-center gap-4'>
45+
<AuthButton />
46+
</div>
47+
</div>
48+
</div>
49+
</nav>
50+
);
51+
};

web/components/auth/AuthButton.tsx

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Link from 'next/link';
2+
import { redirect } from 'next/navigation';
3+
4+
import { Button } from '@/components/ui/button';
5+
import { createClient } from '@/utils/supabase/server';
6+
7+
export default async function AuthButton() {
8+
const supabase = await createClient();
9+
10+
const {
11+
data: { user },
12+
} = await supabase.auth.getUser();
13+
14+
const signOut = async () => {
15+
'use server';
16+
17+
const supabase = await createClient();
18+
await supabase.auth.signOut();
19+
return redirect('/login');
20+
};
21+
22+
return user ? (
23+
<div className='flex items-center gap-4'>
24+
<span className='hidden md:block'>Hey, {user.user_metadata.name}!</span>
25+
<form action={signOut}>
26+
<Button className='' variant='default'>
27+
Logout
28+
</Button>
29+
</form>
30+
</div>
31+
) : (
32+
<Button asChild>
33+
<Link
34+
href='/login'
35+
className='bg-btn-background hover:bg-btn-background-hover flex rounded-md px-4 py-2 no-underline'
36+
>
37+
Login
38+
</Link>
39+
</Button>
40+
);
41+
}

web/components/icons/GoogleIcon.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export function GoogleIcon(props: any) {
2+
return (
3+
<svg
4+
{...props}
5+
xmlns='http://www.w3.org/2000/svg'
6+
x='0px'
7+
y='0px'
8+
width='16'
9+
height='16'
10+
viewBox='0 0 48 48'
11+
>
12+
<path
13+
fill='#FFC107'
14+
d='M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z'
15+
></path>
16+
<path
17+
fill='#FF3D00'
18+
d='M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z'
19+
></path>
20+
<path
21+
fill='#4CAF50'
22+
d='M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z'
23+
></path>
24+
<path
25+
fill='#1976D2'
26+
d='M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z'
27+
></path>
28+
</svg>
29+
);
30+
}
31+

web/middleware.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type NextRequest } from 'next/server';
2+
3+
import { updateSession } from '@/utils/supabase/middleware';
4+
5+
export async function middleware(request: NextRequest) {
6+
return await updateSession(request);
7+
}
8+
9+
export const config = {
10+
matcher: [
11+
/*
12+
* Match all request paths except for the ones starting with:
13+
* - _next/static (static files)
14+
* - _next/image (image optimization files)
15+
* - favicon.ico (favicon file)
16+
* Feel free to modify this pattern to include more paths.
17+
*/
18+
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
19+
],
20+
};

web/public/logo.png

146 KB
Loading

web/utils/supabase/client.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createBrowserClient } from '@supabase/ssr'
2+
3+
export function createClient() {
4+
return createBrowserClient(
5+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
6+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7+
)
8+
}

web/utils/supabase/middleware.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createServerClient } from '@supabase/ssr';
2+
import { type NextRequest, NextResponse } from 'next/server';
3+
4+
export async function updateSession(request: NextRequest) {
5+
let supabaseResponse = NextResponse.next({
6+
request,
7+
});
8+
9+
const supabase = createServerClient(
10+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
11+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12+
{
13+
cookies: {
14+
getAll() {
15+
return request.cookies.getAll();
16+
},
17+
setAll(cookiesToSet) {
18+
cookiesToSet.forEach(({ name, value, options }) =>
19+
request.cookies.set(name, value)
20+
);
21+
supabaseResponse = NextResponse.next({
22+
request,
23+
});
24+
cookiesToSet.forEach(({ name, value, options }) =>
25+
supabaseResponse.cookies.set(name, value, options)
26+
);
27+
},
28+
},
29+
}
30+
);
31+
32+
// IMPORTANT: Avoid writing any logic between createServerClient and
33+
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
34+
// issues with users being randomly logged out.
35+
36+
// const {
37+
// data: { user },
38+
// } = await supabase.auth.getUser();
39+
40+
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
41+
// creating a new response object with NextResponse.next() make sure to:
42+
// 1. Pass the request in it, like so:
43+
// const myNewResponse = NextResponse.next({ request })
44+
// 2. Copy over the cookies, like so:
45+
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
46+
// 3. Change the myNewResponse object to fit your needs, but avoid changing
47+
// the cookies!
48+
// 4. Finally:
49+
// return myNewResponse
50+
// If this is not done, you may be causing the browser and server to go out
51+
// of sync and terminate the user's session prematurely!
52+
53+
return supabaseResponse;
54+
}

web/utils/supabase/server.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { type CookieOptions, createServerClient } from '@supabase/ssr';
2+
import { cookies } from 'next/headers';
3+
4+
export async function createClient() {
5+
const cookieStore = await cookies();
6+
7+
return createServerClient(
8+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
9+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
10+
{
11+
cookies: {
12+
getAll() {
13+
return cookieStore.getAll();
14+
},
15+
setAll(cookiesToSet) {
16+
try {
17+
cookiesToSet.forEach(({ name, value, options }) =>
18+
cookieStore.set(name, value, options)
19+
);
20+
} catch {
21+
// The `setAll` method was called from a Server Component.
22+
// This can be ignored if you have middleware refreshing
23+
// user sessions.
24+
}
25+
},
26+
},
27+
}
28+
);
29+
}

0 commit comments

Comments
 (0)