Authentication
Y3NKO uses Supabase Auth for authentication, providing multiple sign-in methods and session management.
Overview
Supabase Auth provides:
- Email/password authentication
- OAuth providers (Google)
- Magic links
- Session management
- User metadata storage
Authentication Flow
User clicks "Sign In"
│
▼
┌─────────────────────┐
│ Login Page │
│ - Email/Password │
│ - Google OAuth │
│ - Magic Link │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Supabase Auth │
│ - Validates creds │
│ - Creates session │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Callback Route │
│ - Exchange code │
│ - Set cookies │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Redirect to │
│ Dashboard/Home │
└─────────────────────┘Supabase Client Setup
Server Client
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
},
},
}
)
}Browser Client
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}Getting the Current User
In Server Components
import { createClient } from '@/lib/supabase/server'
export default async function ProfilePage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
return <div>Welcome, {user.email}</div>
}In Server Actions
"use server"
import { createClient } from '@/lib/supabase/server'
export async function updateProfile(data: ProfileData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
throw new Error('Unauthorized')
}
// Update profile...
}In Client Components
"use client"
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
export function UserMenu() {
const [user, setUser] = useState(null)
const supabase = createClient()
useEffect(() => {
supabase.auth.getUser().then(({ data: { user } }) => {
setUser(user)
})
}, [])
return user ? <ProfileDropdown user={user} /> : <SignInButton />
}Protected Routes
Using Middleware
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
const response = NextResponse.next()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
)
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// Protected routes
if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
return NextResponse.redirect(new URL('/login', request.url))
}
if (request.nextUrl.pathname.startsWith('/admin') && !user) {
return NextResponse.redirect(new URL('/login', request.url))
}
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*', '/host/:path*'],
}Role-Based Access
Users have roles stored in the database:
enum UserRole {
USER = 'USER',
HOST = 'HOST',
ADMIN = 'ADMIN',
SUPER_ADMIN = 'SUPER_ADMIN'
}Checking Roles
// lib/auth.ts
import { createClient } from '@/lib/supabase/server'
import { prisma } from '@/lib/db'
export async function getCurrentUser() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return null
return await prisma.user.findUnique({
where: { id: user.id },
include: { profile: true }
})
}
export async function requireRole(roles: UserRole[]) {
const user = await getCurrentUser()
if (!user || !roles.includes(user.role)) {
throw new Error('Forbidden')
}
return user
}Usage in Pages
// app/admin/page.tsx
import { requireRole } from '@/lib/auth'
export default async function AdminPage() {
const user = await requireRole(['ADMIN', 'SUPER_ADMIN'])
return <AdminDashboard user={user} />
}Sign Out
"use client"
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
export function SignOutButton() {
const router = useRouter()
const supabase = createClient()
async function handleSignOut() {
await supabase.auth.signOut()
router.push('/')
router.refresh()
}
return <button onClick={handleSignOut}>Sign Out</button>
}OAuth Configuration
Google OAuth
- Configure in Supabase Dashboard > Authentication > Providers
- Add redirect URLs:
http://localhost:3000/auth/callback(development)https://y3nko.travel/auth/callback(production)
Callback Route
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/dashboard'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(new URL(next, request.url))
}
}
return NextResponse.redirect(new URL('/auth/error', request.url))
}Supabase handles token refresh automatically. Sessions are stored in HTTP-only cookies for security.