diff --git a/docs/auth-integration.md b/docs/auth-integration.md
new file mode 100644
index 0000000..c3c4f18
--- /dev/null
+++ b/docs/auth-integration.md
@@ -0,0 +1,147 @@
+# 认证集成说明
+
+## 概述
+
+本应用已实现与网页端(https://cstb.upup.cool/)的邮箱注册登录功能集成。用户可以通过网页端登录/注册,然后通过 deep-link 回调到本地应用。
+
+## 实现方案
+
+### 1. 用户流程
+
+1. 用户在本地应用中点击"登录"或"注册"按钮
+2. 应用打开浏览器,跳转到网页端登录/注册页面
+3. 用户在网页端完成登录/注册
+4. 网页端重定向到 `cstb://auth` deep-link,携带认证信息
+5. 本地应用接收 deep-link 回调,解析并设置 session
+6. 用户状态同步到本地应用
+
+### 2. 技术实现
+
+#### 前端(Next.js + Tauri)
+
+- **认证 Store** (`src/store/auth.ts`): 管理用户状态和会话
+- **认证工具** (`src/utils/auth.ts`): 处理登录/注册跳转和回调解析
+- **认证 Provider** (`src/components/auth/AuthProvider.tsx`): 监听 deep-link 和认证状态变化
+- **认证按钮** (`src/components/auth/AuthButton.tsx`): UI 组件,显示登录状态和用户信息
+
+#### Deep-link 配置
+
+- Scheme: `cstb`
+- 回调 URL 格式: `cstb://auth?access_token=xxx&refresh_token=xxx` 或 `cstb://auth?code=xxx`
+
+### 3. 网页端集成要求
+
+网页端需要在登录/注册成功后,重定向到 deep-link URL。以下是几种实现方式:
+
+#### 方式 1: 使用 access_token 和 refresh_token(推荐)
+
+```typescript
+// 在登录成功后
+const { data } = await supabase.auth.signInWithPassword({ email, password })
+
+if (data.session) {
+ const redirectUrl = new URL('cstb://auth', window.location.href)
+ redirectUrl.searchParams.set('access_token', data.session.access_token)
+ redirectUrl.searchParams.set('refresh_token', data.session.refresh_token)
+ window.location.href = redirectUrl.toString()
+}
+```
+
+#### 方式 2: 使用 PKCE flow(更安全)
+
+```typescript
+// 在登录页面初始化时
+const { data } = await supabase.auth.signInWithOAuth({
+ provider: 'email',
+ options: {
+ redirectTo: 'cstb://auth'
+ }
+})
+
+// 处理回调
+const { data: { session } } = await supabase.auth.getSession()
+if (session) {
+ window.location.href = 'cstb://auth?code=' + session.access_token
+}
+```
+
+#### 方式 3: 使用完整 session JSON
+
+```typescript
+// 在登录成功后
+const { data } = await supabase.auth.signInWithPassword({ email, password })
+
+if (data.session) {
+ const redirectUrl = new URL('cstb://auth', window.location.href)
+ redirectUrl.searchParams.set('session', encodeURIComponent(JSON.stringify(data.session)))
+ window.location.href = redirectUrl.toString()
+}
+```
+
+### 4. 网页端修改示例
+
+在 `https://cstb.upup.cool/auth/login` 和 `https://cstb.upup.cool/auth/signup` 页面中:
+
+```typescript
+// 检查是否有 redirect 参数
+const urlParams = new URLSearchParams(window.location.search)
+const redirectTo = urlParams.get('redirect') // 应该是 'cstb://auth'
+
+// 登录成功后
+const handleLogin = async (email: string, password: string) => {
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ })
+
+ if (error) {
+ // 处理错误
+ return
+ }
+
+ if (data.session && redirectTo) {
+ // 重定向到 deep-link
+ const redirectUrl = new URL(redirectTo)
+ redirectUrl.searchParams.set('access_token', data.session.access_token)
+ redirectUrl.searchParams.set('refresh_token', data.session.refresh_token)
+ window.location.href = redirectUrl.toString()
+ } else {
+ // 正常网页端跳转
+ window.location.href = '/'
+ }
+}
+```
+
+### 5. 环境变量
+
+确保以下环境变量已配置:
+
+```env
+NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
+```
+
+### 6. 测试
+
+1. 启动应用
+2. 点击导航栏的用户图标
+3. 选择"登录"或"注册"
+4. 在浏览器中完成登录/注册
+5. 应用应该自动接收回调并显示登录成功提示
+
+## 注意事项
+
+1. **安全性**: 虽然 access_token 和 refresh_token 通过 URL 传递,但由于 deep-link 是本地协议,相对安全。但建议使用 PKCE flow 以获得更好的安全性。
+
+2. **错误处理**: 网页端应该处理登录失败的情况,不要重定向到 deep-link。
+
+3. **用户体验**: 可以在网页端显示"正在跳转到应用..."的提示,提升用户体验。
+
+4. **兼容性**: 确保 deep-link 在所有目标平台上都已正确注册(Windows/macOS/Linux)。
+
+## 故障排查
+
+1. **Deep-link 未触发**: 检查 `tauri.conf.json` 中 deep-link 配置是否正确
+2. **Session 未保存**: 检查 Supabase client 配置,确保使用 localStorage
+3. **回调参数错误**: 检查网页端传递的参数格式是否正确
+
diff --git a/docs/web-integration-example.md b/docs/web-integration-example.md
new file mode 100644
index 0000000..0d91d47
--- /dev/null
+++ b/docs/web-integration-example.md
@@ -0,0 +1,271 @@
+# 网页端集成示例代码
+
+本文档提供网页端(https://cstb.upup.cool/)实现登录/注册回调的示例代码。
+
+## 登录页面示例
+
+在 `auth/login` 页面中:
+
+```typescript
+'use client'
+
+import { useState } from 'react'
+import { createClient } from '@/utils/supabase/client'
+import { useSearchParams } from 'next/navigation'
+
+export default function LoginPage() {
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [loading, setLoading] = useState(false)
+ const searchParams = useSearchParams()
+ const redirectTo = searchParams.get('redirect') // 应该是 'cstb://auth'
+
+ const supabase = createClient()
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+
+ try {
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ })
+
+ if (error) {
+ alert(`登录失败: ${error.message}`)
+ setLoading(false)
+ return
+ }
+
+ if (data.session) {
+ // 如果有 redirect 参数,说明是从应用跳转过来的
+ if (redirectTo) {
+ // 构建 deep-link URL
+ const redirectUrl = new URL(redirectTo)
+ redirectUrl.searchParams.set('access_token', data.session.access_token)
+ redirectUrl.searchParams.set('refresh_token', data.session.refresh_token)
+
+ // 重定向到应用
+ window.location.href = redirectUrl.toString()
+ } else {
+ // 正常网页端跳转
+ window.location.href = '/'
+ }
+ }
+ } catch (error) {
+ console.error('Login error:', error)
+ alert('登录时发生错误')
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+```
+
+## 注册页面示例
+
+在 `auth/signup` 页面中:
+
+```typescript
+'use client'
+
+import { useState } from 'react'
+import { createClient } from '@/utils/supabase/client'
+import { useSearchParams } from 'next/navigation'
+
+export default function SignupPage() {
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [loading, setLoading] = useState(false)
+ const searchParams = useSearchParams()
+ const redirectTo = searchParams.get('redirect') // 应该是 'cstb://auth'
+
+ const supabase = createClient()
+
+ const handleSignup = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ if (password !== confirmPassword) {
+ alert('两次输入的密码不一致')
+ return
+ }
+
+ setLoading(true)
+
+ try {
+ const { data, error } = await supabase.auth.signUp({
+ email,
+ password,
+ })
+
+ if (error) {
+ alert(`注册失败: ${error.message}`)
+ setLoading(false)
+ return
+ }
+
+ if (data.session) {
+ // 如果有 redirect 参数,说明是从应用跳转过来的
+ if (redirectTo) {
+ // 构建 deep-link URL
+ const redirectUrl = new URL(redirectTo)
+ redirectUrl.searchParams.set('access_token', data.session.access_token)
+ redirectUrl.searchParams.set('refresh_token', data.session.refresh_token)
+
+ // 重定向到应用
+ window.location.href = redirectUrl.toString()
+ } else {
+ // 正常网页端跳转
+ alert('注册成功!请检查邮箱验证链接。')
+ window.location.href = '/'
+ }
+ } else {
+ // 需要邮箱验证
+ alert('注册成功!请检查邮箱中的验证链接。')
+ if (!redirectTo) {
+ window.location.href = '/'
+ }
+ }
+ } catch (error) {
+ console.error('Signup error:', error)
+ alert('注册时发生错误')
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+```
+
+## 使用 OAuth 提供商的示例
+
+如果使用第三方登录(如 Google、GitHub 等):
+
+```typescript
+const handleOAuthLogin = async (provider: 'google' | 'github') => {
+ const { data, error } = await supabase.auth.signInWithOAuth({
+ provider,
+ options: {
+ redirectTo: redirectTo || `${window.location.origin}/auth/callback`,
+ },
+ })
+
+ if (error) {
+ alert(`登录失败: ${error.message}`)
+ return
+ }
+
+ // OAuth 会重定向到回调页面,在回调页面中处理
+}
+```
+
+## 回调页面处理
+
+如果需要处理 OAuth 回调:
+
+```typescript
+// auth/callback/page.tsx
+'use client'
+
+import { useEffect } from 'react'
+import { createClient } from '@/utils/supabase/client'
+import { useSearchParams } from 'next/navigation'
+
+export default function CallbackPage() {
+ const searchParams = useSearchParams()
+ const redirectTo = searchParams.get('redirect') // 从应用跳转时的原始 redirect
+
+ useEffect(() => {
+ const handleCallback = async () => {
+ const supabase = createClient()
+ const { data: { session }, error } = await supabase.auth.getSession()
+
+ if (error) {
+ console.error('Error getting session:', error)
+ return
+ }
+
+ if (session) {
+ // 如果是从应用跳转过来的,重定向回应用
+ if (redirectTo) {
+ const redirectUrl = new URL(redirectTo)
+ redirectUrl.searchParams.set('access_token', session.access_token)
+ redirectUrl.searchParams.set('refresh_token', session.refresh_token)
+ window.location.href = redirectUrl.toString()
+ } else {
+ // 正常网页端跳转
+ window.location.href = '/'
+ }
+ }
+ }
+
+ void handleCallback()
+ }, [redirectTo])
+
+ return 正在处理登录...
+}
+```
+
+## 注意事项
+
+1. **安全性**: 确保只在 HTTPS 环境下使用,避免 token 泄露
+2. **错误处理**: 始终处理登录/注册失败的情况
+3. **用户体验**: 在重定向前显示加载状态
+4. **验证**: 如果启用了邮箱验证,需要处理未验证的情况
+
+## 测试步骤
+
+1. 在应用中使用 `https://cstb.upup.cool/auth/login?redirect=cstb://auth` 打开登录页面
+2. 完成登录后,应该自动跳转回应用
+3. 检查应用是否成功接收并设置了 session
+
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 69fafa8..8ad1e17 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -88,7 +88,7 @@ fn main() {
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
// Deep Link
- #[cfg(all(desktop, not(target_os = "macos")))]
+ #[cfg(desktop)]
app.deep_link().register("cstb")?;
// Tray
@@ -120,6 +120,7 @@ fn main() {
cmds::greet,
cmds::launch_game,
cmds::kill_game,
+ cmds::check_process_running,
cmds::kill_steam,
cmds::get_steam_path,
cmds::get_cs_path,
@@ -134,6 +135,9 @@ fn main() {
cmds::analyze_replay,
cmds::get_console_log_path,
cmds::read_vprof_report,
+ cmds::check_app_update,
+ cmds::download_app_update,
+ cmds::install_app_update,
on_button_clicked
])
.run(tauri::generate_context!())
diff --git a/src/components/auth/AuthButton.tsx b/src/components/auth/AuthButton.tsx
new file mode 100644
index 0000000..87bd357
--- /dev/null
+++ b/src/components/auth/AuthButton.tsx
@@ -0,0 +1,142 @@
+"use client"
+
+import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Avatar, Spinner } from "@heroui/react"
+import { User, Logout, Login, AddUser } from "@icon-park/react"
+import { useAuthStore } from "@/store/auth"
+import { openLoginPage, openSignupPage } from "@/utils/auth"
+import { useDisclosure } from "@heroui/react"
+import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"
+
+export function AuthButton() {
+ const { state, signOut } = useAuthStore()
+ const { isOpen, onOpen, onOpenChange } = useDisclosure()
+
+ const handleLogin = async () => {
+ await openLoginPage()
+ }
+
+ const handleSignup = async () => {
+ await openSignupPage()
+ }
+
+ const handleSignOut = async () => {
+ await signOut()
+ onOpenChange()
+ }
+
+ if (state.isLoading) {
+ return (
+
+ )
+ }
+
+ if (state.isAuthenticated && state.user) {
+ return (
+ <>
+
+
+
+
+
+ }
+ textValue="用户信息"
+ >
+
+ {state.user.email}
+ {state.user.user_metadata?.name && (
+ {state.user.user_metadata.name}
+ )}
+
+
+ }
+ color="danger"
+ onPress={onOpen}
+ >
+ 退出登录
+
+
+
+
+
+ {(onClose) => (
+ <>
+ 退出登录
+
+ 确认要退出登录吗?
+
+
+
+
+
+ >
+ )}
+
+
+ >
+ )
+ }
+
+ return (
+
+
+
+
+
+ }
+ onPress={handleLogin}
+ >
+ 登录
+
+ }
+ onPress={handleSignup}
+ >
+ 注册
+
+
+
+ )
+}
+
diff --git a/src/components/auth/AuthProvider.tsx b/src/components/auth/AuthProvider.tsx
new file mode 100644
index 0000000..80c91db
--- /dev/null
+++ b/src/components/auth/AuthProvider.tsx
@@ -0,0 +1,75 @@
+"use client"
+
+import { onOpenUrl } from "@tauri-apps/plugin-deep-link"
+import { useEffect } from "react"
+import { useAuthStore } from "@/store/auth"
+import { handleAuthCallback } from "@/utils/auth"
+import { createClient } from "@/utils/supabase/client"
+import { addToast } from "@heroui/react"
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const { checkSession, setSession, setLoading } = useAuthStore()
+
+ // 初始化时检查现有会话
+ useEffect(() => {
+ void checkSession()
+ }, [checkSession])
+
+ // 监听 deep-link 认证回调
+ useEffect(() => {
+ const unlisten = onOpenUrl(async (urls) => {
+ if (urls.length === 0) return
+
+ const url = urls[0]
+ if (!url.startsWith("cstb://auth")) return
+
+ setLoading(true)
+ try {
+ const session = await handleAuthCallback(url)
+ if (session) {
+ setSession(session)
+ addToast({
+ title: "登录成功",
+ description: `欢迎回来,${session.user.email || session.user.id}`,
+ })
+ } else {
+ addToast({
+ title: "登录失败",
+ description: "无法验证登录信息,请重试",
+ variant: "danger",
+ })
+ }
+ } catch (error) {
+ console.error("Auth callback error:", error)
+ addToast({
+ title: "登录失败",
+ description: error instanceof Error ? error.message : "未知错误",
+ variant: "danger",
+ })
+ } finally {
+ setLoading(false)
+ }
+ })
+
+ return () => {
+ void unlisten.then((fn) => fn())
+ }
+ }, [setSession, setLoading])
+
+ // 监听 Supabase 认证状态变化
+ useEffect(() => {
+ const supabase = createClient()
+ const {
+ data: { subscription },
+ } = supabase.auth.onAuthStateChange((_event, session) => {
+ setSession(session)
+ })
+
+ return () => {
+ subscription.unsubscribe()
+ }
+ }, [setSession])
+
+ return <>{children}>
+}
+
diff --git a/src/store/auth.ts b/src/store/auth.ts
new file mode 100644
index 0000000..0814800
--- /dev/null
+++ b/src/store/auth.ts
@@ -0,0 +1,76 @@
+import { store } from "@tauri-store/valtio"
+import { useSnapshot } from "valtio"
+import { DEFAULT_STORE_CONFIG } from "./config"
+import { createClient } from "@/utils/supabase/client"
+import type { User, Session } from "@supabase/supabase-js"
+
+interface AuthState {
+ user: User | null
+ session: Session | null
+ isLoading: boolean
+ isAuthenticated: boolean
+}
+
+const defaultValue: AuthState = {
+ user: null,
+ session: null,
+ isLoading: true,
+ isAuthenticated: false,
+}
+
+export const authStore = store("auth", { ...defaultValue }, DEFAULT_STORE_CONFIG)
+
+export const useAuthStore = () => {
+ void authStore.start
+ const state = useSnapshot(authStore.state)
+
+ return {
+ state,
+ store: authStore,
+ setUser,
+ setSession,
+ setLoading,
+ signOut,
+ checkSession,
+ }
+}
+
+const setUser = (user: User | null) => {
+ authStore.state.user = user
+ authStore.state.isAuthenticated = !!user
+}
+
+const setSession = (session: Session | null) => {
+ authStore.state.session = session
+ authStore.state.user = session?.user ?? null
+ authStore.state.isAuthenticated = !!session
+}
+
+const setLoading = (isLoading: boolean) => {
+ authStore.state.isLoading = isLoading
+}
+
+const signOut = async () => {
+ const supabase = createClient()
+ await supabase.auth.signOut()
+ authStore.state.user = null
+ authStore.state.session = null
+ authStore.state.isAuthenticated = false
+}
+
+const checkSession = async () => {
+ setLoading(true)
+ try {
+ const supabase = createClient()
+ const {
+ data: { session },
+ } = await supabase.auth.getSession()
+ setSession(session)
+ } catch (error) {
+ console.error("Error checking session:", error)
+ setSession(null)
+ } finally {
+ setLoading(false)
+ }
+}
+
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
new file mode 100644
index 0000000..6ad2093
--- /dev/null
+++ b/src/utils/auth.ts
@@ -0,0 +1,92 @@
+import { open } from "@tauri-apps/plugin-shell"
+import { createClient } from "@/utils/supabase/client"
+import type { Session } from "@supabase/supabase-js"
+
+/**
+ * 打开网页端登录页面
+ */
+export async function openLoginPage() {
+ const loginUrl = "https://cstb.upup.cool/auth/login?redirect=cstb://auth"
+ await open(loginUrl)
+}
+
+/**
+ * 打开网页端注册页面
+ */
+export async function openSignupPage() {
+ const signupUrl = "https://cstb.upup.cool/auth/signup?redirect=cstb://auth"
+ await open(signupUrl)
+}
+
+/**
+ * 处理 deep-link 回调中的认证信息
+ * @param url - deep-link URL,格式: cstb://auth?access_token=xxx&refresh_token=xxx 或 cstb://auth?session=xxx
+ */
+export async function handleAuthCallback(url: string): Promise {
+ try {
+ const urlObj = new URL(url)
+ const params = new URLSearchParams(urlObj.search)
+
+ // 方式1: 从 URL 参数中获取 access_token 和 refresh_token
+ const accessToken = params.get("access_token")
+ const refreshToken = params.get("refresh_token")
+
+ if (accessToken && refreshToken) {
+ const supabase = createClient()
+ const { data, error } = await supabase.auth.setSession({
+ access_token: accessToken,
+ refresh_token: refreshToken,
+ })
+
+ if (error) {
+ console.error("Error setting session:", error)
+ return null
+ }
+
+ return data.session
+ }
+
+ // 方式2: 从 session 参数中获取(如果网页端返回的是完整 session)
+ const sessionParam = params.get("session")
+ if (sessionParam) {
+ try {
+ const session = JSON.parse(decodeURIComponent(sessionParam)) as Session
+ const supabase = createClient()
+ const { data, error } = await supabase.auth.setSession({
+ access_token: session.access_token,
+ refresh_token: session.refresh_token,
+ })
+
+ if (error) {
+ console.error("Error setting session:", error)
+ return null
+ }
+
+ return data.session
+ } catch (e) {
+ console.error("Error parsing session:", e)
+ return null
+ }
+ }
+
+ // 方式3: 如果网页端通过 code 参数返回(PKCE flow)
+ const code = params.get("code")
+ if (code) {
+ const supabase = createClient()
+ const { data, error } = await supabase.auth.exchangeCodeForSession(code)
+
+ if (error) {
+ console.error("Error exchanging code for session:", error)
+ return null
+ }
+
+ return data.session
+ }
+
+ return null
+ } catch (error) {
+ console.error("Error handling auth callback:", error)
+ return null
+ }
+}
+