diff --git a/app/auth/confirm/route.ts b/app/auth/confirm/route.ts new file mode 100644 index 0000000..b5b922f --- /dev/null +++ b/app/auth/confirm/route.ts @@ -0,0 +1,46 @@ +import { createServerClient, type CookieOptions } from '@supabase/ssr' +import { type EmailOtpType } from '@supabase/supabase-js' +import { cookies } from 'next/headers' +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(request: NextRequest) { + const {searchParams} = new URL(request.url) + const token_hash = searchParams.get('token_hash') + const type = searchParams.get('type') as EmailOtpType | null + const next = searchParams.get('next') ?? '/' + const redirectTo = request.nextUrl.clone() + redirectTo.pathname = next + + if (token_hash && type) { + const cookieStore = cookies() + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + get(name: string) { + return cookieStore.get(name)?.value + }, + set(name: string, value: string, options: CookieOptions) { + cookieStore.set({ name, value, ...options }) + }, + remove(name: string, options: CookieOptions) { + cookieStore.delete({ name, ...options }) + }, + }, + } + ) + + const { error } = await supabase.auth.verifyOtp({ + type, + token_hash, + }) + if (!error) { + return NextResponse.redirect(redirectTo) + } + } + + // return the user to an error page with some instructions + redirectTo.pathname = '/auth/auth-code-error' + return NextResponse.redirect(redirectTo) +} \ No newline at end of file diff --git a/app/lib/db.ts b/app/lib/db.ts index 923158f..8a841e4 100644 --- a/app/lib/db.ts +++ b/app/lib/db.ts @@ -1,3 +1,4 @@ +import {z} from 'zod' import { TrendingData, RegularData } from '@/app/lib/definitions' import postgres from 'postgres' diff --git a/app/lib/definitions.ts b/app/lib/definitions.ts index 1421056..33369b1 100644 --- a/app/lib/definitions.ts +++ b/app/lib/definitions.ts @@ -1,47 +1,61 @@ -export type Selection = { - id: string - title: string - category: string - year: number - rating: string - isTrending: boolean - isBookmarked: boolean -} +import { z } from 'zod' -export type TrendingThumbs = { - id: string - selectionId: string - small: string - large: string -} +const User = z.object({ + id: z.string(), + email: z.string().email({ message: "Invalid email address" }), + password: z.string().min(7, { message: 'Must be 7 or more characters long' }), +}) -export type RegularThumbs = { - id: string - selectionId: string - small: string - medium: string - large: string -} +const Selection = z.object({ + id: z.string(), + title: z.string(), + category: z.string(), + year: z.number(), + rating: z.string(), + isTrending: z.boolean(), + isBookmarked: z.boolean(), +}) -export type TrendingData = { - id: string - title: string - rating: string - year: number - category: string - is_bookmarked: boolean - large: string - small: string -} +const TrendingThumbs = z.object({ + id: z.string(), + selectionId: z.string(), + small: z.string(), + large: z.string(), +}) -export type RegularData = { - id: string - title: string - rating: string - year: number - category: string - is_bookmarked: boolean - large: string - small: string - medium: string -} \ No newline at end of file +const RegularThumbs = z.object({ + id: z.string(), + selectionId: z.string(), + small: z.string(), + medium: z.string(), + large: z.string(), +}) + +const TrendingData = z.object({ + id: z.string(), + title: z.string(), + rating: z.string(), + year: z.number(), + category: z.string(), + is_bookmarked: z.boolean(), + large: z.string(), + small: z.string(), +}) + +const RegularData = z.object({ + id: z.string(), + title: z.string(), + rating: z.string(), + year: z.number(), + category: z.string(), + is_bookmarked: z.boolean(), + large: z.string(), + small: z.string(), + medium: z.string(), +}) + +export type Selection = z.infer +export type TrendingThumbs = z.infer +export type RegularThumbs = z.infer +export type TrendingData = z.infer +export type RegularData = z.infer diff --git a/app/signup/routeTWO.tsx b/app/signup/routeTWO.tsx new file mode 100644 index 0000000..dce35e9 --- /dev/null +++ b/app/signup/routeTWO.tsx @@ -0,0 +1,27 @@ +'use server' +import { redirect } from 'next/navigation' +import { headers, cookies } from 'next/headers' +import { createClient } from '@/utils/supabase/server' + + +const signUpWithEmail = async ({ email, password }: {email: string, password: string}) => { + const origin = headers().get('origin') + const cookieStore = cookies() + const supabase = createClient(cookieStore) + + const { error } = await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${origin}/auth/callback`, + }, + }) + + if (error) { + return redirect('/login?message=Could not authenticate user') + } + + return redirect('/login?message=Check email to continue sign in process') +} + +export { signUpWithEmail } diff --git a/app/ui/components/Auth.tsx b/app/ui/components/Auth.tsx new file mode 100644 index 0000000..3f041e2 --- /dev/null +++ b/app/ui/components/Auth.tsx @@ -0,0 +1,68 @@ +'use client' +import Link from "next/link" +import Image from "next/image" +import { createClient } from '@/utils/supabase/client' +import { useState } from "react" + +const Auth = () => { + const [user, setUser] = useState('') + const supabase = createClient() + + const subscription = supabase.auth.onAuthStateChange(async (event, session) => { + console.log(event, session) + + if (event === 'INITIAL_SESSION') { + // handle initial session + const { data, error } = await supabase.auth.getSession() + } else if (event === 'SIGNED_IN') { + // handle sign in event + // set user + const { data, error } = await supabase.auth.getUser() + if(!error?.status) { + setUser(data.user?.email || '') + } + } else if (event === 'SIGNED_OUT') { + // handle sign out event + // unset user + setUser('') + } else if (event === 'PASSWORD_RECOVERY') { + // handle password recovery event + // supabase.auth.resetPasswordForEmail() + } else if (event === 'TOKEN_REFRESHED') { + // handle token refreshed event + } else if (event === 'USER_UPDATED') { + // handle user updated event + } +}) + +// call unsubscribe to remove the callback +// subscription.unsubscribe() + + if(user === ''){ + return ( +
+ + Login + +
+ ) + } + return ( +
+ supabase.auth.signOut()} + alt="icon" + width={80} + height={80} + /> +
+ ) +} + +export default Auth \ No newline at end of file diff --git a/app/ui/components/Navbar.tsx b/app/ui/components/Navbar.tsx index 4cf43a8..313b1c8 100644 --- a/app/ui/components/Navbar.tsx +++ b/app/ui/components/Navbar.tsx @@ -4,12 +4,18 @@ import HomeIcon from "@/app/ui/components/HomeIcon"; import TvIcon from "@/app/ui/components/TvIcon"; import BookmarkIcon from "@/app/ui/components/BookmarkIcon"; import MovieIcon from "@/app/ui/components/MovieIcon"; +import Auth from "@/app/ui/components/Auth"; + const Navbar = () => { + + return (
- icon + + icon +
@@ -26,16 +32,10 @@ const Navbar = () => {
- icon +
- ); + ) }; export default Navbar; diff --git a/app/ui/login/Login.tsx b/app/ui/login/Login.tsx index 3260df4..29958b2 100644 --- a/app/ui/login/Login.tsx +++ b/app/ui/login/Login.tsx @@ -3,14 +3,36 @@ import Image from "next/image"; import Link from "next/link"; import {useRouter} from "next/navigation"; -import {useState, FormEvent, ChangeEvent} from "react"; +import { z } from 'zod' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from "react-hook-form"; +import { createClient } from '@/utils/supabase/client' + + +const FormFieldsSchema = z + .object({ + email: z.string().email(), + password: z + .string() + .min(7, { message: 'Must be at least 7 characters long' }), + }) + +type FormFields = z.infer const Login = () => { - const [email, setEmail] = useState(""); - const [passwordOne, setPasswordOne] = useState(""); const router = useRouter(); - const [error, setError] = useState(null); + const { register, handleSubmit, formState: { errors }, +} = useForm({resolver: zodResolver(FormFieldsSchema)}) + +const supabase = createClient() +const loginWithEmail = async ({email, password}: {email: string, password: string}) => { + const res = await supabase.auth.signInWithPassword({ + email, + password + }) + if (res.data.user?.email) router.push('/') +} return (
@@ -26,25 +48,23 @@ const Login = () => {

Login

-
+ loginWithEmail({email: d.email, password: d.password}))}> setEmail(event.target.value)} required /> + {errors.email?.message} setPasswordOne(event.target.value)} + {...register('password')} required /> + {errors.password?.message}
- ); -}; + ) +} -export default Signup; +export default Signup diff --git a/package-lock.json b/package-lock.json index 2419a65..8698563 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "mango-entertainment", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.3.3", "@supabase/ssr": "^0.0.10", "@supabase/supabase-js": "^2.39.0", "clsx": "^2.0.0", @@ -15,7 +16,9 @@ "pg": "^8.11.3", "postgres": "^3.4.3", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-hook-form": "^7.49.2", + "zod": "^3.22.4" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7", @@ -123,6 +126,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.3.tgz", + "integrity": "sha512-bOMxKkSD3zWcS11TKoUQ8O0ZqKslFohvUsPKSrdCHiuEuMjRo/u3cq9YRJD/+xtNGYup++XD2LkjhegP5XENiw==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -4206,6 +4217,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.49.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz", + "integrity": "sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5325,6 +5352,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 02554b8..75cb2e7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"" }, "dependencies": { + "@hookform/resolvers": "^3.3.3", "@supabase/ssr": "^0.0.10", "@supabase/supabase-js": "^2.39.0", "clsx": "^2.0.0", @@ -18,7 +19,9 @@ "pg": "^8.11.3", "postgres": "^3.4.3", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-hook-form": "^7.49.2", + "zod": "^3.22.4" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7",