How to Integrate Supabase Auth with Next.js (JavaScript Only)

Suzune HorikitaSuzune Horikita
12 min read

Step 1: Create useAuth hook

// src/hooks/useAuth.js
'use client'

import { createContext, useContext, useEffect, useState } from 'react'
import supabase from '@/libs/supabase/client'

const AuthContext = createContext({
  session: null,
  user: null,
  loading: true,
  signUp: async () => { },
  signIn: async () => { },
  signInWithGoogle: async () => { },
  signOut: async () => { },
  forgotPassword: async () => { }
});

export const AuthProvider = ({ children }) => {
  const [session, setSession] = useState(null);
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchSession = async () => {
      const { data: { session }, error } = await supabase.auth.getSession();
      if (error) {
        console.error('Error fetching session:', error);
      } else {
        setSession(session);
        setUser(session?.user ?? null);
      }
      setLoading(false);
    };

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (_event, session) => {
        setSession(session);
        setUser(session?.user ?? null);
        setLoading(false);
      }
    );

    fetchSession();

    return () => {
      subscription?.unsubscribe();
    };
  }, []);



  const signUp = async (email, password, username) => {
    // Check if username already exists in auth.users -> user_metadata.username
    const { data: usersWithSameUsername, error: fetchError } = await supabase
      .from('users_view') // You need to create this view in your Supabase DB
      .select('id')
      .eq('user_metadata->>username', username)

    if (fetchError) throw fetchError
    if (usersWithSameUsername.length > 0) {
      throw new Error('Username already taken')
    }

    // Sign up new user
    const { error: signupError } = await supabase.auth.signUp({
      email,
      password,
      options: {
        data: {
          username,
        },
      },
    })

    if (signupError) throw signupError
  }


  const signIn = async (email, password) => {
    const { error } = await supabase.auth.signInWithPassword({ email, password });
    if (error) throw new Error(error.message);
  };

  const signInWithGoogle = async () => {
    const { error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: { redirectTo: `${window.location.origin}/auth/callback` }
    });
    if (error) throw error;
  };

  const signOut = async () => {
    const { error } = await supabase.auth.signOut();
    if (error) throw error;
    window.location.href = '/';
  };


  const forgotPassword = async (email) => {
    const { error } = await supabase.auth.resetPasswordForEmail(email, {
      redirectTo: `${window.location.origin}/auth/reset-password`,
    });
    if (error) throw error;
  };


  return (
    <AuthContext.Provider
      value={{
        session,
        user,
        loading,
        signUp,
        signIn,
        signInWithGoogle,
        signOut,
        forgotPassword
      }}
    >
      {!loading && children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within an AuthProvider');
  return context;
};

Step 2: Provide AuthProvider to the layout.js and create supabase client

import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { AuthProvider } from '@/hooks/useAuth'
import Navbar from "@/components/Navbar";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <AuthProvider>

          {children}
       </AuthProvider>

      </body>
    </html>
  );
}
// src/libs/supabase/client.js
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, {
  auth: {
    persistSession: true,
    autoRefreshToken: true,
    detectSessionInUrl: true,
  },
});

export default supabase;

// make sure in main_directory/.env.local
// NEXT_PUBLIC_SUPABASE_URL=https://your_supabase_url.supabase.co
// NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key

//  refer: https://supabase.com/dashboard/project/settings/api

Step 3 : Create authentication page for signup/login

//app/auth/page.jsx
'use client';

import { useEffect, useState } from 'react';

import { useRouter, useSearchParams } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import dynamic from 'next/dynamic';

const LoginForm = dynamic(() => import('@/components/authentication/LoginForm'), { ssr: false });
const SignUpForm = dynamic(() => import('@/components/authentication/SignUpForm'), { ssr: false });

export default function AuthPage() {
  const { user, loading } = useAuth();
  const router = useRouter();
  const searchParams = useSearchParams();

  const initialMode = searchParams.get('status') === 'signup' ? 'signup' : 'login';
  const [authMode, setAuthMode] = useState(initialMode);

  useEffect(() => {
    if (!loading && user) {
      router.push('/');
    }
  }, [user, loading, router]);

  const handleModeChange = (mode) => {
    setAuthMode(mode);
    const newUrl = `/auth?status=${mode}`;
    router.push(newUrl);
  };

  return (
    <div className="min-h-screen flex flex-col justify-center items-center px-4 bg-white">
      <div className="bg-white rounded-xl shadow-md p-6 w-full max-w-md">
        <h2 className="text-center text-2xl font-bold mb-4">
          {authMode === 'login' ? 'Login to Your App Name' : 'Sign Up for Your App Name'}
        </h2>

        <div className="flex justify-center space-x-4 mb-6">
          <button
            onClick={() => handleModeChange('login')}
            className={`px-4 py-2 rounded-md text-sm font-medium ${authMode === 'login' ? 'bg-orange-500 text-white' : 'bg-gray-200 text-gray-700'
              }`}
          >
            Login
          </button>
          <button
            onClick={() => handleModeChange('signup')}
            className={`px-4 py-2 rounded-md text-sm font-medium ${authMode === 'signup' ? 'bg-orange-500 text-white' : 'bg-gray-200 text-gray-700'
              }`}
          >
            Sign Up
          </button>
        </div>

        {/* Login or Signup Form */}
        <div>
          {authMode === 'login' ? (
            <>
              <LoginForm />


            </>
          ) : (
            <SignUpForm />
          )}
        </div>
      </div>
    </div>
  );
}

Step 4: Create Authentication sub components

Create Account Component

// src/components/authentication/SignUpForm.js
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { Mail, Lock, User, Loader2, Eye, EyeOff } from 'lucide-react'
import { useAuth } from '@/hooks/useAuth'


export default function SignUpForm() {
  const [loading, setLoading] = useState(false)
  const [showPassword, setShowPassword] = useState(false)
  const { signUp } = useAuth()

  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm()

  const onSubmit = async (data) => {
  try {
    setLoading(true)
    await signUp(data.email, data.password, data.username)
    alert('Account created successfully')
    reset()
  } catch (error) {
    alert(error.message || 'Something went wrong')
  } finally {
    setLoading(false)
  }
}

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 font-sans max-w-md mx-auto">
      {/* Username */}
      <div className="space-y-2">
        <label htmlFor="username" className="text-sm font-medium">Username</label>
        <div className="relative">
          <User className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
          <input
            {...register('username', {
              required: 'Username is required',
              pattern: {
                value: /^[A-Za-z0-9_]{3,16}$/,
                message: '316 characters, letters, numbers, or underscores',
              },
            })}
            id="username"
            type="text"
            placeholder="Choose a username"
            className="w-full pl-10 pr-3 py-2 border rounded-md focus:outline-none focus:ring"
          />
        </div>
        {errors.username && <p className="text-sm text-red-500">{errors.username.message}</p>}
      </div>

      {/* Email */}
      <div className="space-y-2">
        <label htmlFor="email" className="text-sm font-medium">Email</label>
        <div className="relative">
          <Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
          <input
            {...register('email', {
              required: 'Email is required',
              pattern: {
                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                message: 'Invalid email address',
              },
            })}
            id="email"
            type="email"
            placeholder="Enter your email"
            className="w-full pl-10 pr-3 py-2 border rounded-md focus:outline-none focus:ring"
          />
        </div>
        {errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
      </div>

      {/* Password */}
      <div className="space-y-2">
        <label htmlFor="password" className="text-sm font-medium">Password</label>
        <div className="relative">
          <Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
          <input
            {...register('password', {
              required: 'Password is required',
              minLength: {
                value: 8,
                message: 'Password must be at least 8 characters',
              },
            })}
            id="password"
            type={showPassword ? 'text' : 'password'}
            placeholder="Create a password"
            className="w-full pl-10 pr-10 py-2 border rounded-md focus:outline-none focus:ring"
          />
          <button
            type="button"
            onClick={() => setShowPassword((prev) => !prev)}
            className="absolute right-3 top-2.5 text-gray-500"
          >
            {showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
          </button>
        </div>
        {errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
      </div>

      {/* Submit Button */}
      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-2 rounded-md flex items-center justify-center hover:bg-blue-700 transition"
      >
        {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
        Create Account
      </button>
    </form>
  )
}

// use this sql :
//  create or replace view users_view as
// select 
//  id, 
//  email, 
//  raw_user_meta_data as user_metadata
//  from auth.users;

Login Form Component

// src/components/authentication/LoginForm.js
'use client';

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Mail, Lock, Eye, EyeOff, Loader2 } from 'lucide-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import supabase from '@/libs/supabase/client';

const ForgotPasswordForm = dynamic(() => import('@/components/authentication/ForgotPasswordPage'), {
  ssr: false,
});

export default function LoginForm() {
  const [loading, setLoading] = useState(false);
  const [showPassword, setShowPassword] = useState(false);
  const [showForgot, setShowForgot] = useState(false);
  const router = useRouter();

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = async (data) => {
    try {
      setLoading(true);
      const { error } = await supabase.auth.signInWithPassword({
        email: data.email,
        password: data.password,
      });

      if (error) {
        alert('Login failed: ' + error.message);
      } else {
        alert('Login successful!');
        router.push('/myprofile');
      }
    } catch (err) {
      alert('Login Failed');
    } finally {
      setLoading(false);
    }
  };

  const signInWithGoogle = async () => {
    const { error } = await supabase.auth.signInWithOAuth({ provider: 'google' });
    if (!error) router.push('/myprofile');
    if (error) alert('Google Sign-in failed: ' + error.message);
  };

  return (
    <>
    <div  className='space-y-6 max-w-md mx-auto p-6 border rounded-md shadow-md bg-white'>
      <form
        onSubmit={handleSubmit(onSubmit)}
        className=""
      >
        <div className="space-y-1">
          <label htmlFor="email" className="block text-sm font-medium">Email</label>
          <div className="relative">
            <Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
            <input
              {...register('email', {
                required: 'Email is required',
                pattern: {
                  value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                  message: 'Invalid email address',
                },
              })}
              type="email"
              id="email"
              placeholder="Enter your email"
              className="w-full pl-10 pr-3 py-2 border rounded focus:outline-none focus:ring-2 text-black"
            />
          </div>
          {errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
        </div>

        <div className="space-y-1">
          <label htmlFor="password" className="block text-sm font-medium">Password</label>
          <div className="relative">
            <Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
            <input
              {...register('password', { required: 'Password is required' })}
              type={showPassword ? 'text' : 'password'}
              id="password"
              placeholder="Enter your password"
              className="w-full pl-10 pr-10 py-2 border rounded focus:outline-none focus:ring-2 text-black"
            />
            <button
              type="button"
              onClick={() => setShowPassword(!showPassword)}
              className="absolute right-3 top-2.5"
            >
              {showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
            </button>
          </div>
          {errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
        </div>

        <button
          type="submit"
          disabled={loading}
          className="w-full flex justify-center items-center gap-2 px-4 py-2 bg-blue-500 text-black rounded hover:bg-blue-600 transition"
        >
          {loading && <Loader2 className="h-4 w-4 animate-spin" />}
          Login
        </button>

        <div className="relative my-4">
          <div className="absolute inset-0 flex items-center">
            <span className="w-full border-t" />
          </div>
          <div className="relative flex justify-center text-sm">
            <span className="bg-white rounded-3xl px-2 text-gray-500">Or continue with</span>
          </div>
        </div>

        <button
          type="button"
          onClick={signInWithGoogle}
          disabled={loading}
          className="w-full flex items-center justify-center gap-2 border px-4 py-2 rounded-2xl"
        >
          <img
            src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg"
            alt="Google"
            className="h-5 w-5"
          />
          <span className="text-sm font-medium">Continue with Google</span>
        </button>


      </form>

 <div className="mt-4 text-center">
          <div
            type="button"
            onClick={() => setShowForgot((prev) => !prev)}
            className="text-sm text-blue-600 hover:underline"
          >
             <ForgotPasswordForm />
          </div>
        </div>
</div>


    </>
  );
}

Forgot Password Component

// src/components/authentication/ForgotPasswordPage.js

'use client';

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '@/hooks/useAuth';

export default function ForgotPasswordPage() {
  const [loading, setLoading] = useState(false);
  const [isOpen, setIsOpen] = useState(false);
  const [message, setMessage] = useState(null);

  const { forgotPassword } = useAuth(); 

  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm();

  const onSubmit = async (data) => {
    setLoading(true);
    setMessage(null);
    try {
       await forgotPassword(data.email);
      alert('✅ Password reset instructions sent!');
      setIsOpen(false);
      reset();
    } catch (err) {
      setMessage('❌ ' + (err.message || 'Something went wrong.'));
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="w-full">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="text-sm text-black bg-cyan-200 px-4 py-1 rounded-full hover:bg-cyan-300"
      >
        Forgot password?
      </button>

      <AnimatePresence>
        {isOpen && (
          <motion.div
            initial={{ opacity: 0, y: -5 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -5 }}
            transition={{ duration: 0.2 }}
            className="mt-2 p-4 bg-gray-50 rounded-md border border-gray-200 shadow-sm"
          >
            <form onSubmit={handleSubmit(onSubmit)} className="space-y-3">
              <div>
                <input
                  id="email"
                  type="email"
                  placeholder="Enter your email"
                  className="w-full px-3 py-2 border rounded-md text-sm"
                  {...register('email', {
                    required: 'Email is required',
                    pattern: {
                      value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                      message: 'Invalid email',
                    },
                  })}
                />
                {errors.email && (
                  <p className="text-xs text-red-500 mt-1">
                    {errors.email.message}
                  </p>
                )}
              </div>

              <div className="flex justify-end gap-2">
                <button
                  type="button"
                  onClick={() => {
                    setIsOpen(false);
                    reset();
                  }}
                  className="text-xs px-3 py-1 bg-gray-200 rounded-md"
                >
                  Cancel
                </button>
                <button
                  type="submit"
                  disabled={loading}
                  className="text-xs px-3 py-1 bg-blue-600 text-white rounded-md"
                >
                  {loading ? 'Sending...' : 'Send'}
                </button>
              </div>

              {message && (
                <p className="text-xs text-center mt-1">{message}</p>
              )}
            </form>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

Step 6: Create Reset Password Page

//    app/auth/reset-password/page.jsx
"use client"

import { useState, useEffect } from 'react'
import { Eye, EyeOff, Loader2 } from 'lucide-react'
import { useAuth } from '@/hooks/useAuth'
import supabase from '@/libs/supabase/client'

export default function ResetPasswordPage() {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const [password, setPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')
  const [showPassword, setShowPassword] = useState(false)
  const [showConfirm, setShowConfirm] = useState(false)
  const { resetPassword } = useAuth()

  useEffect(() => {
    supabase.auth.getUser().then(({ data, error }) => {
      if (error || !data.user) {
        setError('Invalid or expired reset link')
      } else {
        setError(null)
      }
      setLoading(false)
    })
  }, [])

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (password !== confirmPassword) {
      alert('Passwords do not match')
      return
    }

    try {
      await supabase.auth.updateUser({ password })
      alert({
        title: 'Password Updated',
        description: 'You can now log in',
      })
      window.location.href = '/auth'
    } catch (error) {
      alert({
        variant: 'destructive',
        title: 'Reset Failed',
        description: error.message,
      })
    }
  }

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <Loader2 className="h-12 w-12 animate-spin text-orange-500" />
      </div>
    )
  }

  if (error) {
    return (
      <div className="min-h-screen flex flex-col items-center justify-center p-4 text-center">
        <h1 className="text-2xl font-bold text-red-500 mb-4">Error</h1>
        <p className="text-gray-600">{error}</p>
        <button
          onClick={() => (window.location.href = '/auth')}
          className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
        >
          Request New Reset Link
        </button>
      </div>
    )
  }

  return (
    <div className="min-h-screen flex items-center justify-center p-4 bg-gray-100">
      <form onSubmit={handleSubmit} className="w-full max-w-md bg-white p-6 rounded shadow space-y-6">
        <div className="text-center">
          <h1 className="text-3xl font-bold">Reset Password</h1>
          <p className="text-gray-600">Enter your new password</p>
        </div>

        {[{
          label: 'New Password', id: 'password', value: password, onChange: setPassword, show: showPassword, toggle: setShowPassword
        }, {
          label: 'Confirm Password', id: 'confirm', value: confirmPassword, onChange: setConfirmPassword, show: showConfirm, toggle: setShowConfirm
        }].map(({ label, id, value, onChange, show, toggle }) => (
          <div key={id}>
            <label htmlFor={id} className="block font-medium mb-1">{label}</label>
            <div className="relative">
              <input
                id={id}
                type={show ? 'text' : 'password'}
                required
                minLength={6}
                value={value}
                onChange={(e) => onChange(e.target.value)}
                className="w-full border px-3 py-2 rounded pr-10"
              />
              <button
                type="button"
                onClick={() => toggle(!show)}
                className="absolute right-3 top-2.5 text-gray-500"
                aria-label="Toggle visibility"
              >
                {show ? <EyeOff size={18} /> : <Eye size={18} />}
              </button>
            </div>
          </div>
        ))}

        <button
          type="submit"
          className="w-full bg-blue-600 text-white py-2 rounded font-semibold"
        >
          Reset Password
        </button>
      </form>
    </div>
  )
}

Step 7 : Create Callback for Auth Flow

//    app/auth/callback/page.jsx

'use client'

import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import supabase  from '@/libs/supabase/client'
import { Loader2 } from 'lucide-react'

export default function AuthCallback() {
  const router = useRouter()

  useEffect(() => {
    const handleRedirect = async () => {
      const hash = window.location.hash
      const params = new URLSearchParams(hash.slice(1))
      const type = params.get('type')
      const access_token = params.get('access_token')

      if (type !== 'recovery' || !access_token) {
        router.push('/auth?error=Invalid or expired link')
        return
      }

      // Let Supabase auto-detect session from URL
      await supabase.auth.getSession()

      router.push('/auth/reset-password')
    }

    handleRedirect()
  }, [router])

  return (
    <div className="min-h-screen flex items-center justify-center bg-black">
      <Loader2 className="h-12 w-12 animate-spin text-orange-500" />
    </div>
  )
}

Step 8 : My Profile page

// app/myprofile/page.js

'use client'

import { useAuth } from '@/hooks/useAuth';
import  supabase  from '@/libs/supabase/client';
import Image from 'next/image';
import { useState } from 'react';

const ProfilePage = () => {
  const { user, signOut } = useAuth();

  const [editing, setEditing] = useState(false);
  const [newUsername, setNewUsername] = useState(user?.user_metadata?.username || '');
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState('');

  if (!user) return <p>Loading user data...</p>;

  const email = user.email;
  const avatar = user.user_metadata?.avatar_url || '/default-avatar.png';
  const createdAt = new Date(user.created_at).toLocaleDateString();

  const handleLogout = async () => {
    try {
      await signOut();
    } catch (err) {
      alert('Failed to logout!');
    }
  };

  const handleUsernameUpdate = async () => {
    setLoading(true);
    setMessage('');

    const { data: existing, error: checkErr } = await supabase
      .from('users_view')
      .select('id')
      .eq('user_metadata->>username', newUsername);

    if (checkErr) {
      setMessage('Error checking username');
      setLoading(false);
      return;
    }

    if (existing.length > 0 && newUsername !== user.user_metadata?.username) {
      setMessage('Username already taken.');
      setLoading(false);
      return;
    }

    const { error } = await supabase.auth.updateUser({
      data: { username: newUsername },
    });

    if (error) {
      setMessage('Failed to update username.');
    } else {
      setMessage('Username updated successfully!');
      setEditing(false);
    }

    setLoading(false);
  };

  return (
    <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100 p-6">
      <div className="bg-white rounded-2xl shadow-xl p-6 max-w-sm w-full text-center">
        <Image
          src={avatar}
          alt="Profile Picture"
          width={100}
          height={100}
          className="rounded-full mx-auto mb-4"
        />

        {editing ? (
          <div className="mb-3">
            <input
              type="text"
              value={newUsername}
              onChange={(e) => setNewUsername(e.target.value)}
              className="border px-3 py-1 rounded w-full"
              placeholder="Enter new username"
            />
            <div className="flex justify-center mt-2 gap-2">
              <button
                onClick={handleUsernameUpdate}
                className="bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600"
                disabled={loading}
              >
                {loading ? 'Updating...' : 'Save'}
              </button>
              <button
                onClick={() => {
                  setEditing(false);
                  setNewUsername(user.user_metadata?.username || '');
                  setMessage('');
                }}
                className="bg-gray-400 text-white px-3 py-1 rounded hover:bg-gray-500"
              >
                Cancel
              </button>
            </div>
          </div>
        ) : (
          <>
            <h2 className="text-2xl font-bold mb-1">{user.user_metadata?.username || 'No username'}</h2>
            <button
              onClick={() => setEditing(true)}
              className="text-sm text-blue-500 hover:underline mb-3"
            >
              Edit Username
            </button>
          </>
        )}

        {message && <p className="text-sm text-center text-gray-600 mb-2">{message}</p>}

        <p className="text-gray-600 mb-1">{email}</p>
        <p className="text-sm text-gray-500 mb-4">Member since: {createdAt}</p>

        <button
          onClick={handleLogout}
          className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition"
        >
          Logout
        </button>
      </div>
    </div>
  );
};

export default ProfilePage;

In this post, I walk you through a complete authentication flow in Next.js using JavaScript—including login, signup, forgot password, reset password, and Google provider integration. I’ve shared detailed implementation steps to help you build a secure and user-friendly auth system. Hope you find it useful and enjoyable!

0
Subscribe to my newsletter

Read articles from Suzune Horikita directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Suzune Horikita
Suzune Horikita