Next.js를 위한 기성 로그인 페이지
8개의 작은 파일은 Aurabase 이메일의 링크가 가리키는 페이지를 포함하여 Next.js 앱에 완전한 로그인 흐름을 제공합니다.
1. 고객 및 대리인
이 페이지는 쿠키 세션과 함께 앱 라우터를 사용합니다( Next.js SSR참조). NEXT_PUBLIC_AURABASE_URL 및 NEXT_PUBLIC_AURABASE_ANON_KEY를 설정한 다음 서버 클라이언트와 프록시를 추가합니다. Next.js 15에서 파일 이름은 middleware.ts, 함수 이름은 middleware입니다.
import { cookies } from 'next/headers'
import { createServerClient } from '@aurabase/aurabase-js/ssr'
export async function serverClient() {
const store = await cookies()
return createServerClient(process.env.NEXT_PUBLIC_AURABASE_URL!, process.env.NEXT_PUBLIC_AURABASE_ANON_KEY!, {
cookies: {
getAll: () => store.getAll(),
// 서버 구성 요소는 쿠키를 쓸 수 없습니다. 대신 아래 프록시가 세션을 새로 고칩니다.
setAll(list) {
try {
list.forEach(({ name, value, options }) => store.set(name, value, options))
} catch {}
},
},
})
}
import { NextResponse, type NextRequest } from 'next/server'
import { createServerClient } from '@aurabase/aurabase-js/ssr'
const PROTECTED = ['/dashboard']
export async function proxy(request: NextRequest) {
const response = NextResponse.next({ request })
const aura = createServerClient(process.env.NEXT_PUBLIC_AURABASE_URL!, process.env.NEXT_PUBLIC_AURABASE_ANON_KEY!, {
cookies: {
getAll: () => request.cookies.getAll(),
setAll(list) {
list.forEach(({ name, value, options }) => {
request.cookies.set(name, value)
response.cookies.set(name, value, options)
})
},
},
})
// getUser()는 토큰을 새로 고치고 사용자가 누구인지 서버에 묻습니다. getSession()으로 승인하지 마십시오.
const { data: user } = await aura.auth.getUser()
if (!user && PROTECTED.some((p) => request.nextUrl.pathname.startsWith(p))) {
return NextResponse.redirect(new URL('/login', request.url))
}
return response
}
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }
2. 로그인 및 회원가입
'use server'
import { redirect } from 'next/navigation'
import { serverClient } from '@/lib/aurabase/server'
export async function login(formData: FormData) {
const aura = await serverClient()
const { data, error } = await aura.auth.signInWithPassword({
email: String(formData.get('email')),
password: String(formData.get('password')),
})
if (error) redirect(`/login?error=${encodeURIComponent(error.code ?? 'ERROR')}`)
// MFA가 활성화된 사용자는 세션 대신 챌린지를 받습니다. 여기에서 코드를 요청하세요.
if (data && 'mfa_required' in data) redirect(`/login?error=MFA_REQUIRED`)
redirect('/dashboard')
}
export async function sendMagicLink(formData: FormData) {
const aura = await serverClient()
await aura.auth.signInWithMagicLink({ email: String(formData.get('email')) })
redirect('/login?sent=1')
}
import Link from 'next/link'
import { login, sendMagicLink } from './actions'
export default async function LoginPage({ searchParams }: { searchParams: Promise<{ error?: string; sent?: string }> }) {
const { error, sent } = await searchParams
return (
<main>
<h1>Sign in</h1>
{error && <p role="alert">Sign-in failed: {error}</p>}
{sent && <p>If this address has an account, a sign-in link was sent.</p>}
<form action={login}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
<button type="submit">Sign in</button>
</form>
<form action={sendMagicLink}>
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Email me a sign-in link</button>
</form>
<p>
<Link href="/signup">Create an account</Link> · <Link href="/forgot-password">Forgot your password?</Link>
</p>
</main>
)
}
'use server'
import { redirect } from 'next/navigation'
import { serverClient } from '@/lib/aurabase/server'
export async function signup(formData: FormData) {
const aura = await serverClient()
const { data, error } = await aura.auth.signUp({
email: String(formData.get('email')),
password: String(formData.get('password')),
})
if (error) redirect(`/signup?error=${encodeURIComponent(error.message)}`)
// 프로젝트에 이메일 확인이 필요한 경우 아직 세션이 없습니다. `data.session`은 null입니다.
redirect(data?.session ? '/dashboard' : '/signup?check=1')
}
import Link from 'next/link'
import { signup } from './actions'
export default async function SignupPage({ searchParams }: { searchParams: Promise<{ error?: string; check?: string }> }) {
const { error, check } = await searchParams
return (
<main>
<h1>Create an account</h1>
{error && <p role="alert">{error}</p>}
{check && <p>Check your inbox: confirm your email address to finish.</p>}
<form action={signup}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required minLength={8} />
<button type="submit">Sign up</button>
</form>
<p>
<Link href="/login">Already have an account?</Link>
</p>
</main>
)
}
3. 비밀번호 재설정
재설정 이메일은 <Site URL>/reset-password?token=…에 연결됩니다. Studio › 인증 › 구성에서 사이트 URL을 설정합니다.
'use server'
import { redirect } from 'next/navigation'
import { serverClient } from '@/lib/aurabase/server'
export async function requestReset(formData: FormData) {
const aura = await serverClient()
await aura.auth.resetPasswordForEmail(String(formData.get('email')))
// 주소에 계정이 있는지 여부(계정 열거 없음)와 관계없이 동일한 답변입니다.
redirect('/forgot-password?sent=1')
}
import { requestReset } from './actions'
export default async function ForgotPasswordPage({ searchParams }: { searchParams: Promise<{ sent?: string }> }) {
const { sent } = await searchParams
return (
<main>
<h1>Reset your password</h1>
{sent && <p>If this address has an account, a reset link was sent.</p>}
<form action={requestReset}>
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Send the link</button>
</form>
</main>
)
}
'use server'
import { redirect } from 'next/navigation'
import { serverClient } from '@/lib/aurabase/server'
export async function resetPassword(formData: FormData) {
const token = String(formData.get('token'))
const aura = await serverClient()
const { error } = await aura.auth.resetPassword(token, String(formData.get('password')))
if (error) redirect(`/reset-password?token=${encodeURIComponent(token)}&error=${encodeURIComponent(error.message)}`)
// 재설정하면 사용자가 새 세션에 로그인됩니다. 이전 세션이 취소되었습니다.
redirect('/dashboard')
}
import { resetPassword } from './actions'
export default async function ResetPasswordPage({ searchParams }: { searchParams: Promise<{ token?: string; error?: string }> }) {
const { token, error } = await searchParams
if (!token) return <main><p role="alert">This link is incomplete. Request a new one.</p></main>
return (
<main>
<h1>Choose a new password</h1>
{error && <p role="alert">{error}</p>}
<form action={resetPassword}>
<input type="hidden" name="token" value={token} />
<input name="password" type="password" placeholder="New password" required minLength={8} />
<button type="submit">Save</button>
</form>
</main>
)
}
4. 이메일 및 매직 링크
확인 및 매직 링크 이메일은 <Site URL>/verify-email?token=… 및 <Site URL>/magic-link/verify?token=…에 연결됩니다. 경로 핸들러는 토큰을 사용한 다음 리디렉션합니다.
import { NextResponse, type NextRequest } from 'next/server'
import { serverClient } from '@/lib/aurabase/server'
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('token')
if (token) {
const aura = await serverClient()
const { error } = await aura.auth.verifyOtp({ type: 'signup', token })
if (!error) return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.redirect(new URL('/login?error=LINK_INVALID', request.url))
}
import { NextResponse, type NextRequest } from 'next/server'
import { serverClient } from '@/lib/aurabase/server'
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('token')
if (token) {
const aura = await serverClient()
const { error } = await aura.auth.verifyMagicLink(token)
if (!error) return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.redirect(new URL('/login?error=LINK_INVALID', request.url))
}
5. 보호된 페이지
'use server'
import { redirect } from 'next/navigation'
import { serverClient } from '@/lib/aurabase/server'
export async function logout() {
const aura = await serverClient()
await aura.auth.signOut()
redirect('/login')
}
import { redirect } from 'next/navigation'
import { serverClient } from '@/lib/aurabase/server'
import { logout } from './actions'
export default async function DashboardPage() {
const aura = await serverClient()
const { data: user } = await aura.auth.getUser()
if (!user) redirect('/login')
return (
<main>
<h1>Dashboard</h1>
<p>Signed in as {user.email}</p>
<form action={logout}>
<button type="submit">Sign out</button>
</form>
</main>
)
}
6. 로컬에서 사용해 보세요
로컬 스택에서는 이메일이 전송되는 대신 Mailpit에 의해 캡처됩니다. kubectl -n aurabase-dev port-forward svc/aurabase-mailpit 8025:8025로 받은 편지함을 연 다음 http://localhost:8025를 엽니다. 앱의 출처(예: http://localhost:3000/*)를 Studio › 인증 › 구성 › 리디렉션 URL에 추가하고 사이트 URL을 여기에 설정하세요.
proxy.ts는 PROTECTED에 나열된 경로에서만 리디렉션됩니다. 프록시는 그 자체로 인증 경계가 아니기 때문에 대시보드 페이지는 getUser()를 사용하여 사용자를 다시 확인합니다.