Skip to content

Commit 3661e6f

Browse files
committedDec 26, 2024
feat(login): add login functionality
1 parent 58ed1f8 commit 3661e6f

File tree

10 files changed

+522
-105
lines changed

10 files changed

+522
-105
lines changed
 

‎.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
2+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key

‎app/login/components/login-form.tsx

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
import { zodResolver } from "@hookform/resolvers/zod"
6+
import { useForm } from "react-hook-form"
7+
import * as z from "zod"
8+
import { Button } from "@/components/ui/button"
9+
import {
10+
Form,
11+
FormControl,
12+
FormField,
13+
FormItem,
14+
FormLabel,
15+
FormMessage,
16+
} from "@/components/ui/form"
17+
import { Input } from "@/components/ui/input"
18+
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
19+
import { Loader2 } from 'lucide-react'
20+
import {createClient} from "@/utils/supabase/client";
21+
22+
const formSchema = z.object({
23+
email: z.string().email({
24+
message: "Please enter a valid email address.",
25+
}),
26+
password: z.string().min(6, {
27+
message: "Password must be at least 6 characters.",
28+
}),
29+
})
30+
31+
export function LoginForm() {
32+
const supabase = createClient()
33+
34+
const [isLoading, setIsLoading] = useState(false)
35+
const [error, setError] = useState<string | null>(null)
36+
const router = useRouter()
37+
38+
const form = useForm<z.infer<typeof formSchema>>({
39+
resolver: zodResolver(formSchema),
40+
defaultValues: {
41+
email: "",
42+
password: "",
43+
},
44+
})
45+
46+
async function onSubmit(values: z.infer<typeof formSchema>) {
47+
setIsLoading(true)
48+
setError(null)
49+
50+
const { error } = await supabase.auth.signInWithPassword({
51+
email: values.email,
52+
password: values.password,
53+
})
54+
55+
if (error) {
56+
setError(error.message)
57+
setIsLoading(false)
58+
} else {
59+
router.push('/') // Redirect to dashboard on successful login
60+
}
61+
}
62+
63+
return (
64+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
65+
<Card className="w-[350px]">
66+
<CardHeader>
67+
<CardTitle>Login</CardTitle>
68+
</CardHeader>
69+
<CardContent>
70+
<Form {...form}>
71+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
72+
<FormField
73+
control={form.control}
74+
name="email"
75+
render={({ field }) => (
76+
<FormItem>
77+
<FormLabel>Email</FormLabel>
78+
<FormControl>
79+
<Input placeholder="Enter your email" {...field} />
80+
</FormControl>
81+
<FormMessage />
82+
</FormItem>
83+
)}
84+
/>
85+
<FormField
86+
control={form.control}
87+
name="password"
88+
render={({ field }) => (
89+
<FormItem>
90+
<FormLabel>Password</FormLabel>
91+
<FormControl>
92+
<Input type="password" placeholder="Enter your password" {...field} />
93+
</FormControl>
94+
<FormMessage />
95+
</FormItem>
96+
)}
97+
/>
98+
{error && <p className="text-red-500 text-sm">{error}</p>}
99+
<Button type="submit" className="w-full" disabled={isLoading}>
100+
{isLoading ? (
101+
<>
102+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
103+
Logging in...
104+
</>
105+
) : (
106+
'Login'
107+
)}
108+
</Button>
109+
</form>
110+
</Form>
111+
</CardContent>
112+
<CardFooter>
113+
<p className="text-sm text-gray-500">
114+
Demo Credentials:<br />
115+
Email: data@table.org <br />
116+
Password: serverside
117+
</p>
118+
</CardFooter>
119+
</Card>
120+
</div>
121+
)
122+
}
123+

‎app/login/page.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {LoginForm} from "@/app/login/components/login-form";
2+
3+
export default function LoginPage() {
4+
return <LoginForm />
5+
}

‎app/page.tsx

+2-96
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,7 @@
1-
import Image from "next/image";
2-
31
export default function Home() {
42
return (
5-
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
6-
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
7-
<Image
8-
className="dark:invert"
9-
src="https://nextjs.org/icons/next.svg"
10-
alt="Next.js logo"
11-
width={180}
12-
height={38}
13-
priority
14-
/>
15-
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
16-
<li className="mb-2">
17-
Get started by editing{" "}
18-
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
19-
app/page.tsx
20-
</code>
21-
.
22-
</li>
23-
<li>Save and see your changes instantly.</li>
24-
</ol>
25-
26-
<div className="flex gap-4 items-center flex-col sm:flex-row">
27-
<a
28-
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
29-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
30-
target="_blank"
31-
rel="noopener noreferrer"
32-
>
33-
<Image
34-
className="dark:invert"
35-
src="https://nextjs.org/icons/vercel.svg"
36-
alt="Vercel logomark"
37-
width={20}
38-
height={20}
39-
/>
40-
Deploy now
41-
</a>
42-
<a
43-
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
44-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
45-
target="_blank"
46-
rel="noopener noreferrer"
47-
>
48-
Read our docs
49-
</a>
50-
</div>
51-
</main>
52-
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
53-
<a
54-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
55-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
56-
target="_blank"
57-
rel="noopener noreferrer"
58-
>
59-
<Image
60-
aria-hidden
61-
src="https://nextjs.org/icons/file.svg"
62-
alt="File icon"
63-
width={16}
64-
height={16}
65-
/>
66-
Learn
67-
</a>
68-
<a
69-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
70-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
71-
target="_blank"
72-
rel="noopener noreferrer"
73-
>
74-
<Image
75-
aria-hidden
76-
src="https://nextjs.org/icons/window.svg"
77-
alt="Window icon"
78-
width={16}
79-
height={16}
80-
/>
81-
Examples
82-
</a>
83-
<a
84-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
85-
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
86-
target="_blank"
87-
rel="noopener noreferrer"
88-
>
89-
<Image
90-
aria-hidden
91-
src="https://nextjs.org/icons/globe.svg"
92-
alt="Globe icon"
93-
width={16}
94-
height={16}
95-
/>
96-
Go to nextjs.org →
97-
</a>
98-
</footer>
3+
<div className="font-[family-name:var(--font-geist-sans)]">
4+
<p>hallo</p>
995
</div>
1006
);
1017
}

‎middleware.ts

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

‎package-lock.json

+263-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,21 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@hookform/resolvers": "^3.9.1",
13+
"@radix-ui/react-label": "^2.1.1",
14+
"@radix-ui/react-slot": "^1.1.1",
15+
"@supabase/ssr": "^0.5.2",
16+
"@supabase/supabase-js": "^2.47.10",
1217
"class-variance-authority": "^0.7.1",
1318
"clsx": "^2.1.1",
1419
"lucide-react": "^0.469.0",
1520
"next": "14.2.21",
1621
"react": "^18",
1722
"react-dom": "^18",
23+
"react-hook-form": "^7.54.2",
1824
"tailwind-merge": "^2.6.0",
19-
"tailwindcss-animate": "^1.0.7"
25+
"tailwindcss-animate": "^1.0.7",
26+
"zod": "^3.24.1"
2027
},
2128
"devDependencies": {
2229
"@types/node": "^20",

‎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+
}

‎utils/supabase/middleware.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { createServerClient } from '@supabase/ssr'
2+
import { NextResponse, type NextRequest } 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 }) => request.cookies.set(name, value))
19+
supabaseResponse = NextResponse.next({
20+
request,
21+
})
22+
cookiesToSet.forEach(({ name, value, options }) =>
23+
supabaseResponse.cookies.set(name, value, options)
24+
)
25+
},
26+
},
27+
}
28+
)
29+
30+
// IMPORTANT: Avoid writing any logic between createServerClient and
31+
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
32+
// issues with users being randomly logged out.
33+
34+
const {
35+
data: { user },
36+
} = await supabase.auth.getUser()
37+
38+
if (
39+
!user &&
40+
!request.nextUrl.pathname.startsWith('/login') &&
41+
!request.nextUrl.pathname.startsWith('/auth')
42+
) {
43+
// no user, potentially respond by redirecting the user to the login page
44+
const url = request.nextUrl.clone()
45+
url.pathname = '/login'
46+
return NextResponse.redirect(url)
47+
}
48+
49+
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
50+
// creating a new response object with NextResponse.next() make sure to:
51+
// 1. Pass the request in it, like so:
52+
// const myNewResponse = NextResponse.next({ request })
53+
// 2. Copy over the cookies, like so:
54+
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
55+
// 3. Change the myNewResponse object to fit your needs, but avoid changing
56+
// the cookies!
57+
// 4. Finally:
58+
// return myNewResponse
59+
// If this is not done, you may be causing the browser and server to go out
60+
// of sync and terminate the user's session prematurely!
61+
62+
return supabaseResponse
63+
}

‎utils/supabase/server.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { 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)
Please sign in to comment.