feat(auth): add Supabase auth with Google, GitHub, and email OTP login

Introduce user registration/login gated behind optional NEXT_PUBLIC_SUPABASE_*
env vars (leave blank to disable — app behaves exactly as before). Adds
proxy.ts for automatic cookie session refresh, requireUser() API route
guards on all 7 compute-consuming routes, AuthModal (Google/GitHub OAuth +
6-digit email OTP), UserChip header component, and login_success analytics
event. Identity is fully decoupled from Session/engine — no type changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yuanzonghao
2026-06-13 17:33:55 +08:00
parent 2a2d58a64f
commit 87a2f93edb
22 changed files with 646 additions and 11 deletions
+33 -1
View File
@@ -16,6 +16,10 @@ import { analyzeImageDataUrl } from "@infiplot/ai-client";
import { readStoredModelConfig, resolveEngineConfig } from "@/lib/clientModelConfig";
import { STYLE_EXTRACTION_PROMPT } from "@/lib/styleExtraction";
import { STORY_SHARE_STORAGE_KEY, parseStoryShareDoc } from "@/lib/storyShare";
import { AUTH_ENABLED } from "@/lib/supabase/config";
import { createClient as createSupabaseClient } from "@/lib/supabase/client";
import { AuthModal } from "@/components/AuthModal";
import { UserChip } from "@/components/UserChip";
/* ============================================================================
InfiPlot · 首页(编辑式视觉风格 · 居中构图,呼应低保真原型)
@@ -1281,6 +1285,8 @@ export default function HomePage() {
const [ttsConfigured, setTtsConfigured] = useState(false);
const [playerName, setPlayerName] = useState("");
const [visionClickEnabled, setVisionClickEnabled] = useState(true);
const [authModalOpen, setAuthModalOpen] = useState(false);
const [pendingAction, setPendingAction] = useState<"start" | null>(null);
const styleRow = OPTS.findIndex((o) => o.modal);
const voiceRow = OPTS.findIndex((o) => o.label === "语音配音");
@@ -1350,7 +1356,17 @@ export default function HomePage() {
}
};
const start = () => {
const start = async () => {
if (AUTH_ENABLED) {
const sb = createSupabaseClient();
const { data } = await sb.auth.getUser();
if (!data.user) {
setPendingAction("start");
setAuthModalOpen(true);
return;
}
}
// 空输入时落回 Typewriter 当前闪动的示例——用户看到啥就玩啥,
// 不会再出现「点开始 → 剧情和占位文字毫无关系」的体验断层。
const userPrompt =
@@ -1525,6 +1541,7 @@ export default function HomePage() {
>
<i className="fa-brands fa-x-twitter" />
</a>
<UserChip onLoginClick={() => setAuthModalOpen(true)} />
</div>
</header>
@@ -1813,6 +1830,21 @@ export default function HomePage() {
}}
/>
)}
{authModalOpen && (
<AuthModal
onClose={() => {
setAuthModalOpen(false);
setPendingAction(null);
}}
onSuccess={() => {
setAuthModalOpen(false);
if (pendingAction === "start") {
setPendingAction(null);
start();
}
}}
/>
)}
</div>
);
}