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
10 changes: 7 additions & 3 deletions docs/supabase-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,12 @@ https://your-project.supabase.co/auth/v1/callback
```

并在 Supabase Dashboard → **Authentication** → **URL Configuration** 中添加:
- **Site URL:** `https://your-domain.com`
- **Redirect URLs:** `https://your-domain.com/auth/callback`
- **Site URL:** `https://echo-type.app`
- **Redirect URLs:**
- `https://echo-type.app/auth/callback`(Web 端 OAuth 回调)
- `https://echo-type.app/auth/desktop-callback`(桌面端 OAuth 回调)
- `http://localhost:3000/auth/callback`(本地开发)
- `http://localhost:3000/auth/desktop-callback`(本地桌面开发)

---

Expand All @@ -269,7 +273,7 @@ https://your-project.supabase.co/auth/v1/callback
| 问题 | 解决方案 |
|------|---------|
| 页面报 "Your project's URL and Key are required" | 检查 `.env.local` 是否正确配置并重启 dev server |
| OAuth 登录后跳转到空白页 | 检查 Supabase Dashboard 的 Redirect URLs 是否包含 `http://localhost:3000/auth/callback` |
| OAuth 登录后跳转到空白页 | 检查 Supabase Dashboard 的 Redirect URLs 是否包含 `http://localhost:3000/auth/callback` 和 `http://localhost:3000/auth/desktop-callback` |
| 同步失败 "Not authenticated" | 确认已登录,检查 Supabase session 是否过期 |
| 数据未同步 | 检查 Settings → Account 中同步是否已启用 |
| RLS 错误 | 确认 SQL 中的 RLS policies 已正确创建 |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@supabase/supabase-js": "^2.99.3",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.0",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.36.3",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions src/app/(app)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ function LoginContent() {
verifyEmailOtp,
resetEmailAuth,
isAuthenticated,
oauthLoading,
oauthError,
emailAuthLoading,
emailAuthError,
emailOtpSent,
Expand Down Expand Up @@ -164,9 +166,9 @@ function LoginContent() {
</div>

<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
{(authError || emailAuthError) && (
{(authError || emailAuthError || oauthError) && (
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{getLocalizedError(emailAuthError)}
{getLocalizedError(emailAuthError || oauthError)}
</div>
)}

Expand Down Expand Up @@ -231,16 +233,18 @@ function LoginContent() {
variant="outline"
className="h-11 gap-2 text-sm font-medium cursor-pointer"
onClick={() => signInWithGoogle()}
disabled={oauthLoading}
>
<GoogleIcon className="h-4 w-4" />
{oauthLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <GoogleIcon className="h-4 w-4" />}
Google
</Button>
<Button
variant="outline"
className="h-11 gap-2 text-sm font-medium cursor-pointer"
onClick={() => signInWithGitHub()}
disabled={oauthLoading}
>
<GitHubIcon className="h-4 w-4" />
{oauthLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <GitHubIcon className="h-4 w-4" />}
GitHub
</Button>
</div>
Expand Down
44 changes: 44 additions & 0 deletions src/app/api/auth/session-exchange/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server';

const pendingSessions = new Map<string, { accessToken: string; refreshToken: string; expiresAt: number }>();

setInterval(() => {
const now = Date.now();
for (const [key, value] of pendingSessions) {
if (value.expiresAt < now) pendingSessions.delete(key);
}
}, 30_000);

export async function POST(req: NextRequest) {
const { exchangeId, accessToken, refreshToken } = await req.json();
if (!exchangeId || !accessToken || !refreshToken) {
return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
}

pendingSessions.set(exchangeId, {
accessToken,
refreshToken,
expiresAt: Date.now() + 120_000,
});

return NextResponse.json({ ok: true });
}

export async function GET(req: NextRequest) {
const exchangeId = req.nextUrl.searchParams.get('id');
if (!exchangeId) {
return NextResponse.json({ error: 'Missing id' }, { status: 400 });
}

const session = pendingSessions.get(exchangeId);
if (!session) {
return NextResponse.json({ found: false });
}

pendingSessions.delete(exchangeId);
return NextResponse.json({
found: true,
accessToken: session.accessToken,
refreshToken: session.refreshToken,
});
}
81 changes: 81 additions & 0 deletions src/app/auth/desktop-callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client';

import { useEffect, useState } from 'react';

export default function DesktopCallbackPage() {
const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing');
const [errorMessage, setErrorMessage] = useState('');

useEffect(() => {
async function handleCallback() {
const url = new URL(window.location.href);
const exchangeId = url.searchParams.get('exchange_id');

const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');

if (!accessToken || !refreshToken) {
setStatus('error');
setErrorMessage(params.get('error_description') || 'No tokens received');
return;
}

if (!exchangeId) {
setStatus('error');
setErrorMessage('Missing exchange ID');
return;
}

try {
const res = await fetch('/api/auth/session-exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ exchangeId, accessToken, refreshToken }),
});

if (!res.ok) throw new Error('Failed to store session');

setStatus('success');
} catch {
setStatus('error');
setErrorMessage('Failed to complete sign-in');
}
}

handleCallback();
}, []);

return (
<div className="flex min-h-screen items-center justify-center bg-slate-50 px-4">
<div className="w-full max-w-sm text-center">
<div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 shadow-lg shadow-indigo-200/50">
<span className="text-2xl font-bold text-white">E</span>
</div>

{status === 'processing' && (
<>
<h2 className="text-xl font-semibold text-slate-900">Signing you in...</h2>
<p className="mt-2 text-sm text-slate-500">Please wait while we complete authentication.</p>
</>
)}

{status === 'success' && (
<>
<h2 className="text-xl font-semibold text-green-700">Login successful!</h2>
<p className="mt-2 text-sm text-slate-500">You can close this tab and return to EchoType.</p>
</>
)}

{status === 'error' && (
<>
<h2 className="text-xl font-semibold text-red-700">Login failed</h2>
<p className="mt-2 text-sm text-slate-500">{errorMessage}</p>
<p className="mt-4 text-xs text-slate-400">Please close this tab and try again in the app.</p>
</>
)}
</div>
</div>
);
}
Loading
Loading