Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/plans/2026-02-28-login-modal-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Login Modal Design

**Date:** 2026-02-28
**Status:** Approved

## Problem

The Navbar currently shows two separate login buttons (Google + GitHub) side by side when the user is not logged in. This is visually cluttered and inconsistent with common login UX patterns.

## Solution

Replace the two inline login buttons with a single "Log In" button. Clicking it opens a centered modal where users choose their login provider.

## UI Design

### Navbar (unauthenticated state)

Before:
```
[ 中文 ] [ GitHub ] [ 🔵 Google ] [ ⚫ GitHub ]
```

After:
```
[ 中文 ] [ GitHub ] [ Log In ]
```

The "Log In" button uses the brand accent color (orange).

### Modal

```
┌─────────────────────────────────────┐
│ Welcome to MoltMarket [×] │
│ │
│ Choose a login method │
│ │
│ ┌─────────────────────────────┐ │
│ │ 🔵 Continue with Google │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ ⚫ Continue with GitHub │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────┘
```

- Semi-transparent black overlay; clicking overlay closes modal
- Centered card with rounded corners and light shadow
- Close button (×) in top-right corner
- Google button: border style (existing style)
- GitHub button: dark fill style (existing style)

## Technical Plan

### Files to change

1. **`src/components/Navbar.tsx`**
- Remove the two inline login `<a>` tags
- Add a "Log In" `<button>` with `onClick` to open modal
- Add `useState(false)` for modal visibility
- Add modal JSX: overlay + card with Google and GitHub login links

2. **`messages/en.json`**
- Add `Navbar.login`, `Navbar.loginTitle`, `Navbar.loginSubtitle`

3. **`messages/zh.json`**
- Add corresponding Chinese translations

### No new files required

All changes are self-contained within the existing Navbar component and translation files.
196 changes: 196 additions & 0 deletions docs/plans/2026-02-28-login-modal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# Login Modal Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace the two inline login buttons (Google + GitHub) in the Navbar with a single "Log In" button that opens a centered modal for the user to choose a login provider.

**Architecture:** All changes are self-contained in `Navbar.tsx` — add a `useState` boolean for modal visibility, replace the two `<a>` login tags with one `<button>`, and append a modal overlay + card as a portal-free JSX sibling inside the `<nav>`. Translation keys for the new copy live in `messages/en.json` and `messages/zh.json`.

**Tech Stack:** Next.js 14, TypeScript, Tailwind CSS, next-intl

---

### Task 1: Add translation keys

**Files:**
- Modify: `messages/en.json`
- Modify: `messages/zh.json`

**Step 1: Add keys to `messages/en.json`**

Inside the `"Navbar"` object, add three keys:

```json
"login": "Log In",
"loginTitle": "Welcome to MoltMarket",
"loginSubtitle": "Choose a login method"
```

After editing, the `"Navbar"` block should look like:
```json
"Navbar": {
"home": "Home",
"overview": "Overview",
"tokenMarket": "Token Recycling Market",
"logout": "Logout",
"startEarning": "Start Earning",
"login": "Log In",
"loginTitle": "Welcome to MoltMarket",
"loginSubtitle": "Choose a login method"
}
```

**Step 2: Add keys to `messages/zh.json`**

Inside the `"Navbar"` object, add the same three keys in Chinese:

```json
"login": "登录",
"loginTitle": "欢迎来到 MoltMarket",
"loginSubtitle": "选择登录方式"
```

**Step 3: Commit**

```bash
git add messages/en.json messages/zh.json
git commit -m "feat: add login modal translation keys"
```

---

### Task 2: Rewrite the unauthenticated login section in Navbar

**Files:**
- Modify: `src/components/Navbar.tsx`

**Step 1: Add `useState` import and modal state**

At the top of the component function body (after the existing `const pathname = usePathname();` line), add:

```tsx
const [loginOpen, setLoginOpen] = useState(false);
```

Also add `useState` to the React import at the top of the file:

```tsx
import { useState } from "react";
```

**Step 2: Replace the unauthenticated login JSX**

Find the `else` branch of `{userName ? (...) : (...)}` — it currently renders a `<div className="flex items-center gap-2">` containing the Google `<a>` and GitHub `<a>` tags.

Replace that entire `<div>` (lines 109–148 in the original file) with a single button:

```tsx
<button
onClick={() => setLoginOpen(true)}
className="font-inter text-[15px] font-semibold text-white rounded-lg px-4 py-2 bg-[var(--accent)] hover:opacity-90 transition-opacity cursor-pointer"
>
{t("login")}
</button>
```

**Step 3: Add the modal JSX**

Immediately before the closing `</nav>` tag, add:

```tsx
{loginOpen && (
<>
{/* Overlay */}
<div
className="fixed inset-0 z-[100] bg-black/50 backdrop-blur-sm"
onClick={() => setLoginOpen(false)}
/>
{/* Modal card */}
<div className="fixed inset-0 z-[101] flex items-center justify-center pointer-events-none">
<div className="pointer-events-auto w-[360px] bg-[var(--bg-primary)] rounded-2xl shadow-2xl p-8 flex flex-col gap-6">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h2 className="font-inter text-[20px] font-bold text-[var(--text-primary)]">
{t("loginTitle")}
</h2>
<p className="font-inter text-[14px] text-[var(--text-muted)] mt-1">
{t("loginSubtitle")}
</p>
</div>
<button
onClick={() => setLoginOpen(false)}
className="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors text-[20px] leading-none cursor-pointer"
>
×
</button>
</div>

{/* Login options */}
<div className="flex flex-col gap-3">
{/* Google */}
<a
href="/api/auth/login?provider=google"
className="flex items-center gap-3 font-inter text-[15px] font-medium text-[var(--text-primary)] rounded-xl px-4 py-3 border border-[var(--border-medium)] hover:bg-[var(--bg-tag)] transition-colors"
>
<svg className="w-5 h-5 shrink-0" viewBox="0 0 24 24">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
</svg>
Continue with Google
</a>

{/* GitHub */}
<a
href="/api/auth/login?provider=github"
className="flex items-center gap-3 font-inter text-[15px] font-semibold text-white rounded-xl px-4 py-3 bg-[#24292e] hover:bg-[#1a1e22] transition-colors"
>
<svg className="w-5 h-5 shrink-0" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
Continue with GitHub
</a>
</div>
</div>
</div>
</>
)}
```

**Step 4: Verify the file compiles**

```bash
npx tsc --noEmit
```

Expected: no errors

**Step 5: Commit**

```bash
git add src/components/Navbar.tsx
git commit -m "feat: replace login buttons with Log In modal in Navbar"
```

---

### Task 3: Manual smoke test

**Step 1: Start dev server**

```bash
npm run dev
```

**Step 2: Verify checklist**

- [ ] Navbar shows single "Log In" button when logged out
- [ ] Clicking "Log In" opens the modal with Google and GitHub options
- [ ] Clicking the overlay (outside the card) closes the modal
- [ ] Clicking the × button closes the modal
- [ ] Clicking "Continue with Google" redirects to `/api/auth/login?provider=google`
- [ ] Clicking "Continue with GitHub" redirects to `/api/auth/login?provider=github`
- [ ] Switching locale (EN ↔ 中文) shows correct translated text in modal title/subtitle
- [ ] Logged-in state still shows username + Logout (unchanged)
95 changes: 95 additions & 0 deletions docs/plans/2026-02-28-supabase-oauth-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Supabase OAuth 替换 SecondMe OAuth 设计文档

**日期:** 2026-02-28
**分支:** feature/OAuth

## 背景

将现有的 SecondMe OAuth 认证替换为 Supabase OAuth,支持 Google 和 GitHub 登录。同时删除所有依赖 SecondMe access token 的 API 路由。

## 技术选型

- **认证库:** `@supabase/ssr`(官方推荐的 Next.js App Router 集成方案)
- **数据库:** 保留 Prisma + PostgreSQL,新增 `supabaseUserId` 字段替换 `secondmeUserId`
- **Session 管理:** Supabase cookie-based session(`@supabase/ssr` 自动处理刷新)+ 内部 `session_user_id` cookie 指向 Prisma User

## Auth 流程

```
用户点击 "Sign in with Google/GitHub"
→ GET /api/auth/login?provider=google|github
→ supabase.auth.signInWithOAuth({ provider, redirectTo })
→ 302 到 Google/GitHub 授权页
→ 授权成功 → 302 到 /api/auth/callback?code=...
→ supabase.auth.exchangeCodeForSession(code)
→ 获取 supabase user (email, name, avatar_url)
→ upsert Prisma User (where: supabaseUserId)
→ 设置 session_user_id cookie (Prisma user ID)
→ 302 到 /
```

## 文件改动

### 新增
- `src/lib/supabase.ts` — Supabase browser client + server client 工厂(基于 `@supabase/ssr`)

### 修改
| 文件 | 改动描述 |
|------|---------|
| `package.json` | 添加 `@supabase/ssr` |
| `src/lib/auth.ts` | 删除 `getValidAccessToken`(SecondMe token 刷新逻辑),保留 `getSessionUserId / setSessionUserId / clearSession / getCurrentUser` |
| `src/app/api/auth/login/route.ts` | 读取 `provider` query param,调用 `supabase.auth.signInWithOAuth()` |
| `src/app/api/auth/callback/route.ts` | 调用 `exchangeCodeForSession`,upsert Prisma User,保留 claimCode 逻辑 |
| `src/app/api/auth/logout/route.ts` | 调用 `supabase.auth.signOut()`,清除 session cookie |
| `src/middleware.ts` | 添加 Supabase session 刷新(createServerClient + getUser) |
| `prisma/schema.prisma` | User 模型:`secondmeUserId` → `supabaseUserId`,删除 `accessToken/refreshToken/tokenExpiresAt`;删除 `ChatSession/ChatMessage/Note` 模型 |
| `src/components/Navbar.tsx` | 登录区域改为 Google / GitHub 两个按钮(`/api/auth/login?provider=google` 和 `?provider=github`) |

### 删除
- `src/app/api/user/info/route.ts`
- `src/app/api/user/shades/route.ts`
- `src/app/api/chat/route.ts`
- `src/app/api/act/route.ts`
- `src/app/api/note/route.ts`
- `src/app/api/sessions/route.ts`

## Prisma Schema 变更

```prisma
model User {
id String @id @default(cuid())
supabaseUserId String @unique @map("supabase_user_id") // 替换 secondmeUserId
email String?
name String?
avatarUrl String? @map("avatar_url")
// 删除: accessToken, refreshToken, tokenExpiresAt
// 其余字段保持不变
...
}
// 删除: ChatSession, ChatMessage, Note 模型
```

## 环境变量

新增:
```
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
```

删除:
```
SECONDME_CLIENT_ID
SECONDME_CLIENT_SECRET
SECONDME_OAUTH_URL
SECONDME_REDIRECT_URI
SECONDME_TOKEN_ENDPOINT
SECONDME_API_BASE_URL
```

## Supabase 控制台配置

在 Supabase Dashboard 中需要配置:
1. Authentication → Providers → 启用 Google(填写 Client ID + Secret)
2. Authentication → Providers → 启用 GitHub(填写 Client ID + Secret)
3. Authentication → URL Configuration → 添加 Redirect URL:`{APP_URL}/api/auth/callback`
Loading
Loading