인증 커리큘럼으로

기술 문서 (프로토타입 버전)

Auth Gate

저장소의 docs/auth-gate-report.md 와 동일한 원문입니다. 아래에서 Markdown과 HTML 변환 결과를 각각 복사할 수 있습니다.

공개 문서 원문 (Markdown)

# SaaS Authentication Gate — Implementation Report

**Project:** saas-login-auth-checkout  
**Date:** 2026-03-12  
**Scope:** Production-grade auth protection (middleware, logout, session persistence, dashboard user display). OAuth logic unchanged.

---

## STEP 1–2 — Global Auth Middleware

**File:** `middleware.ts` (project root)

- **Protected routes:** `/dashboard`, `/pricing`, `/checkout` (and subpaths)
- **Public routes:** `/`, `/login`, `/auth/callback` (and subpaths) — not blocked
- **Logic:**
  1. Create Supabase server client with `createServerClient` from `@supabase/ssr`, using request cookies (`getAll`) and response cookies (`setAll`) for session refresh.
  2. Call `supabase.auth.getSession()`.
  3. If path is protected and there is no session → redirect to `/login`.
  4. Otherwise return `NextResponse.next()` (with optional refreshed cookies).
- **Matcher:** All routes except static assets (`_next/static`, `_next/image`, favicon, images).

**Note:** Next.js 16 shows a deprecation warning for the `middleware` file convention in favor of `proxy`. Current setup uses `middleware.ts` as requested and works as intended.

---

## STEP 3 — Logout Implementation

**File:** `lib/auth.ts`

- `signOut()` was already present; no change.
- Implementation: `await supabase.auth.signOut()`.

---

## STEP 4 — Logout Button

**File:** `app/dashboard/page.tsx`

- **Button:** `<button onClick={handleLogout}>Logout</button>` added in the dashboard header.
- **Behavior:** `handleLogout` calls `signOut()` then `router.replace("/login")`, so the user is sent to `/login` after logout.

---

## STEP 5 — Persistent Session

- **Client:** `lib/supabaseClient.ts` now uses `createBrowserClient` from `@supabase/ssr` instead of `createClient` from `@supabase/supabase-js`. Session is stored in **cookies** so that:
  - Middleware can read the same session via `createServerClient` and request cookies.
  - Session survives page reload and tab refresh.
- **Usage:** `supabase.auth.getSession()` (and `getUser()` in `lib/user.ts`) continue to be used; persistence is ensured by cookie-based storage.

---

## STEP 6 — User Profile Fetch

**File:** `lib/user.ts` (new)

- **Helper:** `getCurrentUser(): Promise<AuthUser | null>`
- **Implementation:** `const { data: { user } } = await supabase.auth.getUser()`
- **Return type:** `{ id: string; email: string | null; provider: string }` (provider from `user.app_metadata?.provider ?? 'email'`).

---

## STEP 7 — Dashboard Layout

**File:** `app/dashboard/page.tsx`

- **Data:** Uses `getCurrentUser()` instead of `getSession()` to display user info.
- **Display:**
  - **User email** in a small card (label “User email”, value or “(이메일 없음)”).
  - **User id** and **provider** in secondary text (e.g. “ID: … · Provider: google”).
  - **Logout** button in the top-right (next to “대시보드”).
- Unauthenticated users are still redirected to `/login` (client-side guard kept for consistency).

---

## STEP 8 — Security Verification (Test Cases)

| # | Scenario | Expected | Implementation |
|---|----------|----------|----------------|
| 1 | Logged-out user visits `/dashboard` | Redirect to `/login` | Middleware: protected path + no session → `NextResponse.redirect('/login')` |
| 2 | Logged-in user visits `/dashboard` | Access granted | Middleware: session present → `NextResponse.next()` |
| 3 | User clicks Logout | Session destroyed, redirect to `/login` | `signOut()` + `router.replace('/login')` |
| 4 | Page refresh while logged in | Session persists | Cookie-based client (`createBrowserClient`) + server client in middleware read same cookies |

**Additional:** Same behavior applies to `/pricing` and `/checkout` (protected by the same middleware). Public routes `/`, `/login`, `/auth/callback` are never blocked.

---

## STEP 9 — Final Checklist

| Item | Status |
|------|--------|
| Middleware protection | ✔ `middleware.ts` protects `/dashboard`, `/pricing`, `/checkout` |
| Logout implementation | ✔ `signOut()` in `lib/auth.ts` |
| Logout button | ✔ Dashboard “Logout” button → signOut + redirect to `/login` |
| Session persistence | ✔ Cookie-based client (`createBrowserClient`); reload keeps session |
| Dashboard user display | ✔ User email, id, provider + Logout button |

---

## Files Changed / Added

| File | Action |
|------|--------|
| `middleware.ts` | **Created** — Supabase session check, redirect for protected routes |
| `lib/supabaseClient.ts` | **Updated** — `createBrowserClient` from `@supabase/ssr` for cookie-based session |
| `lib/user.ts` | **Created** — `getCurrentUser()` (id, email, provider) |
| `app/dashboard/page.tsx` | **Updated** — User display (email, id, provider) + Logout button |
| `package.json` | **Updated** — Added `@supabase/ssr` (already present) |

**Not modified:** OAuth flow, `lib/auth.ts` sign-in helpers, `app/login/page.tsx`, `app/auth/callback/page.tsx`.

---

## Summary

- **Middleware** protects `/dashboard`, `/pricing`, `/checkout` and redirects unauthenticated users to `/login`.
- **Logout** is implemented and wired to a dashboard button with redirect to `/login`.
- **Session** is persisted in cookies and shared between client and middleware.
- **Dashboard** shows user email, id, provider and a Logout button.

The authentication gate is in place and ready for production use within the current stack (Next.js App Router, Supabase Auth, Google/Kakao OAuth).

공개 문서 변환 코드 (HTML)

<h1>SaaS Authentication Gate — Implementation Report</h1>
<p><strong>Project:</strong> saas-login-auth-checkout<br><strong>Date:</strong> 2026-03-12<br><strong>Scope:</strong> Production-grade auth protection (middleware, logout, session persistence, dashboard user display). OAuth logic unchanged.</p>
<hr>
<h2>STEP 1–2 — Global Auth Middleware</h2>
<p><strong>File:</strong> <code>middleware.ts</code> (project root)</p>
<ul>
<li><strong>Protected routes:</strong> <code>/dashboard</code>, <code>/pricing</code>, <code>/checkout</code> (and subpaths)</li>
<li><strong>Public routes:</strong> <code>/</code>, <code>/login</code>, <code>/auth/callback</code> (and subpaths) — not blocked</li>
<li><strong>Logic:</strong><ol>
<li>Create Supabase server client with <code>createServerClient</code> from <code>@supabase/ssr</code>, using request cookies (<code>getAll</code>) and response cookies (<code>setAll</code>) for session refresh.</li>
<li>Call <code>supabase.auth.getSession()</code>.</li>
<li>If path is protected and there is no session → redirect to <code>/login</code>.</li>
<li>Otherwise return <code>NextResponse.next()</code> (with optional refreshed cookies).</li>
</ol>
</li>
<li><strong>Matcher:</strong> All routes except static assets (<code>_next/static</code>, <code>_next/image</code>, favicon, images).</li>
</ul>
<p><strong>Note:</strong> Next.js 16 shows a deprecation warning for the <code>middleware</code> file convention in favor of <code>proxy</code>. Current setup uses <code>middleware.ts</code> as requested and works as intended.</p>
<hr>
<h2>STEP 3 — Logout Implementation</h2>
<p><strong>File:</strong> <code>lib/auth.ts</code></p>
<ul>
<li><code>signOut()</code> was already present; no change.</li>
<li>Implementation: <code>await supabase.auth.signOut()</code>.</li>
</ul>
<hr>
<h2>STEP 4 — Logout Button</h2>
<p><strong>File:</strong> <code>app/dashboard/page.tsx</code></p>
<ul>
<li><strong>Button:</strong> <code>&lt;button onClick={handleLogout}&gt;Logout&lt;/button&gt;</code> added in the dashboard header.</li>
<li><strong>Behavior:</strong> <code>handleLogout</code> calls <code>signOut()</code> then <code>router.replace(&quot;/login&quot;)</code>, so the user is sent to <code>/login</code> after logout.</li>
</ul>
<hr>
<h2>STEP 5 — Persistent Session</h2>
<ul>
<li><strong>Client:</strong> <code>lib/supabaseClient.ts</code> now uses <code>createBrowserClient</code> from <code>@supabase/ssr</code> instead of <code>createClient</code> from <code>@supabase/supabase-js</code>. Session is stored in <strong>cookies</strong> so that:<ul>
<li>Middleware can read the same session via <code>createServerClient</code> and request cookies.</li>
<li>Session survives page reload and tab refresh.</li>
</ul>
</li>
<li><strong>Usage:</strong> <code>supabase.auth.getSession()</code> (and <code>getUser()</code> in <code>lib/user.ts</code>) continue to be used; persistence is ensured by cookie-based storage.</li>
</ul>
<hr>
<h2>STEP 6 — User Profile Fetch</h2>
<p><strong>File:</strong> <code>lib/user.ts</code> (new)</p>
<ul>
<li><strong>Helper:</strong> <code>getCurrentUser(): Promise&lt;AuthUser | null&gt;</code></li>
<li><strong>Implementation:</strong> <code>const { data: { user } } = await supabase.auth.getUser()</code></li>
<li><strong>Return type:</strong> <code>{ id: string; email: string | null; provider: string }</code> (provider from <code>user.app_metadata?.provider ?? &#39;email&#39;</code>).</li>
</ul>
<hr>
<h2>STEP 7 — Dashboard Layout</h2>
<p><strong>File:</strong> <code>app/dashboard/page.tsx</code></p>
<ul>
<li><strong>Data:</strong> Uses <code>getCurrentUser()</code> instead of <code>getSession()</code> to display user info.</li>
<li><strong>Display:</strong><ul>
<li><strong>User email</strong> in a small card (label “User email”, value or “(이메일 없음)”).</li>
<li><strong>User id</strong> and <strong>provider</strong> in secondary text (e.g. “ID: … · Provider: google”).</li>
<li><strong>Logout</strong> button in the top-right (next to “대시보드”).</li>
</ul>
</li>
<li>Unauthenticated users are still redirected to <code>/login</code> (client-side guard kept for consistency).</li>
</ul>
<hr>
<h2>STEP 8 — Security Verification (Test Cases)</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Scenario</th>
<th>Expected</th>
<th>Implementation</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Logged-out user visits <code>/dashboard</code></td>
<td>Redirect to <code>/login</code></td>
<td>Middleware: protected path + no session → <code>NextResponse.redirect(&#39;/login&#39;)</code></td>
</tr>
<tr>
<td>2</td>
<td>Logged-in user visits <code>/dashboard</code></td>
<td>Access granted</td>
<td>Middleware: session present → <code>NextResponse.next()</code></td>
</tr>
<tr>
<td>3</td>
<td>User clicks Logout</td>
<td>Session destroyed, redirect to <code>/login</code></td>
<td><code>signOut()</code> + <code>router.replace(&#39;/login&#39;)</code></td>
</tr>
<tr>
<td>4</td>
<td>Page refresh while logged in</td>
<td>Session persists</td>
<td>Cookie-based client (<code>createBrowserClient</code>) + server client in middleware read same cookies</td>
</tr>
</tbody></table>
<p><strong>Additional:</strong> Same behavior applies to <code>/pricing</code> and <code>/checkout</code> (protected by the same middleware). Public routes <code>/</code>, <code>/login</code>, <code>/auth/callback</code> are never blocked.</p>
<hr>
<h2>STEP 9 — Final Checklist</h2>
<table>
<thead>
<tr>
<th>Item</th>
<th>Status</th>
</tr>
</thead>
<tbody><tr>
<td>Middleware protection</td>
<td>✔ <code>middleware.ts</code> protects <code>/dashboard</code>, <code>/pricing</code>, <code>/checkout</code></td>
</tr>
<tr>
<td>Logout implementation</td>
<td>✔ <code>signOut()</code> in <code>lib/auth.ts</code></td>
</tr>
<tr>
<td>Logout button</td>
<td>✔ Dashboard “Logout” button → signOut + redirect to <code>/login</code></td>
</tr>
<tr>
<td>Session persistence</td>
<td>✔ Cookie-based client (<code>createBrowserClient</code>); reload keeps session</td>
</tr>
<tr>
<td>Dashboard user display</td>
<td>✔ User email, id, provider + Logout button</td>
</tr>
</tbody></table>
<hr>
<h2>Files Changed / Added</h2>
<table>
<thead>
<tr>
<th>File</th>
<th>Action</th>
</tr>
</thead>
<tbody><tr>
<td><code>middleware.ts</code></td>
<td><strong>Created</strong> — Supabase session check, redirect for protected routes</td>
</tr>
<tr>
<td><code>lib/supabaseClient.ts</code></td>
<td><strong>Updated</strong> — <code>createBrowserClient</code> from <code>@supabase/ssr</code> for cookie-based session</td>
</tr>
<tr>
<td><code>lib/user.ts</code></td>
<td><strong>Created</strong> — <code>getCurrentUser()</code> (id, email, provider)</td>
</tr>
<tr>
<td><code>app/dashboard/page.tsx</code></td>
<td><strong>Updated</strong> — User display (email, id, provider) + Logout button</td>
</tr>
<tr>
<td><code>package.json</code></td>
<td><strong>Updated</strong> — Added <code>@supabase/ssr</code> (already present)</td>
</tr>
</tbody></table>
<p><strong>Not modified:</strong> OAuth flow, <code>lib/auth.ts</code> sign-in helpers, <code>app/login/page.tsx</code>, <code>app/auth/callback/page.tsx</code>.</p>
<hr>
<h2>Summary</h2>
<ul>
<li><strong>Middleware</strong> protects <code>/dashboard</code>, <code>/pricing</code>, <code>/checkout</code> and redirects unauthenticated users to <code>/login</code>.</li>
<li><strong>Logout</strong> is implemented and wired to a dashboard button with redirect to <code>/login</code>.</li>
<li><strong>Session</strong> is persisted in cookies and shared between client and middleware.</li>
<li><strong>Dashboard</strong> shows user email, id, provider and a Logout button.</li>
</ul>
<p>The authentication gate is in place and ready for production use within the current stack (Next.js App Router, Supabase Auth, Google/Kakao OAuth).</p>