# Week 1: 認證系統實作指南 ## 目標 建立完整的用戶認證系統,包含註冊、登入、登出功能,並設置 Protected Routes。 ## Day 1-2: Supabase Auth 設置 ### 1. 安裝依賴 ```bash npm install @supabase/supabase-js @supabase/auth-helpers-nextjs npm install @supabase/auth-ui-react @supabase/auth-ui-shared ``` ### 2. 建立 Supabase Client ```typescript // 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! ) } ``` ### 3. 建立 Server Client ```typescript // lib/supabase/server.ts import { createServerClient, type CookieOptions } from '@supabase/ssr' import { cookies } from 'next/headers' export function createClient() { const cookieStore = cookies() return 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.set({ name, value: '', ...options }) }, }, } ) } ``` ## Day 2-3: 認證頁面實作 ### 1. 註冊頁面 ```typescript // app/(auth)/register/page.tsx 'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import { createClient } from '@/lib/supabase/client' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Alert, AlertDescription } from '@/components/ui/alert' import Link from 'next/link' export default function RegisterPage() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const router = useRouter() const supabase = createClient() const handleRegister = async (e: React.FormEvent) => { e.preventDefault() setError(null) if (password !== confirmPassword) { setError('密碼不匹配') return } if (password.length < 6) { setError('密碼至少需要 6 個字符') return } setLoading(true) const { data, error } = await supabase.auth.signUp({ email, password, options: { emailRedirectTo: `${window.location.origin}/auth/callback`, }, }) if (error) { setError(error.message) setLoading(false) return } router.push('/dashboard') } return (
建立帳號 開始你的美劇學習之旅
{error && ( {error} )}
setEmail(e.target.value)} required disabled={loading} />
setPassword(e.target.value)} required disabled={loading} />
setConfirmPassword(e.target.value)} required disabled={loading} />
已有帳號? 立即登入
) } ``` ### 2. 登入頁面 ```typescript // app/(auth)/login/page.tsx 'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import { createClient } from '@/lib/supabase/client' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Alert, AlertDescription } from '@/components/ui/alert' import { Separator } from '@/components/ui/separator' import Link from 'next/link' export default function LoginPage() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const router = useRouter() const supabase = createClient() const handleLogin = async (e: React.FormEvent) => { e.preventDefault() setError(null) setLoading(true) const { error } = await supabase.auth.signInWithPassword({ email, password, }) if (error) { setError(error.message) setLoading(false) return } router.push('/dashboard') router.refresh() } const handleGoogleLogin = async () => { setError(null) setLoading(true) const { error } = await supabase.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: `${window.location.origin}/auth/callback`, }, }) if (error) { setError(error.message) setLoading(false) } } return (
歡迎回來 登入繼續你的學習
{error && ( {error} )}
setEmail(e.target.value)} required disabled={loading} />
setPassword(e.target.value)} required disabled={loading} />
忘記密碼?
還沒有帳號? 立即註冊
) } ``` ## Day 4: Protected Routes 設置 ### 1. Middleware 設置 ```typescript // middleware.ts import { createServerClient, type CookieOptions } from '@supabase/ssr' import { NextResponse, type NextRequest } from 'next/server' export async function middleware(request: NextRequest) { let response = NextResponse.next({ request: { headers: request.headers, }, }) const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return request.cookies.get(name)?.value }, set(name: string, value: string, options: CookieOptions) { request.cookies.set({ name, value, ...options, }) response = NextResponse.next({ request: { headers: request.headers, }, }) response.cookies.set({ name, value, ...options, }) }, remove(name: string, options: CookieOptions) { request.cookies.set({ name, value: '', ...options, }) response = NextResponse.next({ request: { headers: request.headers, }, }) response.cookies.set({ name, value: '', ...options, }) }, }, } ) const { data: { user } } = await supabase.auth.getUser() // 保護需要認證的路由 if (!user && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)) } // 已登入用戶訪問認證頁面時重定向 if (user && ( request.nextUrl.pathname === '/login' || request.nextUrl.pathname === '/register' )) { return NextResponse.redirect(new URL('/dashboard', request.url)) } return response } export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ], } ``` ### 2. Auth Context Provider ```typescript // components/providers/auth-provider.tsx 'use client' import { createContext, useContext, useEffect, useState } from 'react' import { User } from '@supabase/supabase-js' import { createClient } from '@/lib/supabase/client' type AuthContextType = { user: User | null loading: boolean } const AuthContext = createContext({ user: null, loading: true, }) export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) const supabase = createClient() useEffect(() => { const getUser = async () => { const { data: { user } } = await supabase.auth.getUser() setUser(user) setLoading(false) } getUser() const { data: { subscription } } = supabase.auth.onAuthStateChange( (_event, session) => { setUser(session?.user ?? null) } ) return () => subscription.unsubscribe() }, [supabase]) return ( {children} ) } export const useAuth = () => { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within AuthProvider') } return context } ``` ## Day 5: 測試與優化 ### 測試清單 - [ ] 註冊流程完整測試 - [ ] Email 驗證流程 - [ ] 登入/登出功能 - [ ] Google OAuth 登入 - [ ] Protected routes 重定向 - [ ] Session 持久化 - [ ] 錯誤處理 ### 效能優化 1. 實施 loading states 2. 錯誤邊界處理 3. 表單驗證優化 4. Session 快取策略 ### 安全檢查 1. CSRF 保護 2. Rate limiting 3. 密碼強度要求 4. SQL injection 防護 ## 部署檢查清單 ### Vercel 環境變數 ```env NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= ``` ### Supabase 設置 1. Email 模板自定義 2. OAuth 提供者配置 3. URL 配置(Site URL, Redirect URLs) 4. RLS 政策啟用 ## 常見問題 ### 1. OAuth 回調錯誤 確保在 Supabase Dashboard 中設置正確的 Redirect URLs: - 開發環境:`http://localhost:3000/auth/callback` - 生產環境:`https://your-domain.com/auth/callback` ### 2. Session 不持久 檢查 middleware 和 cookie 設置是否正確。 ### 3. RLS 政策錯誤 確保所有表都有適當的 RLS 政策,並且已啟用。 ## 下一步 完成認證系統後,進入 [Week 2: AI 功能實作](./week2-ai.md)