diff --git a/apps/web/app/login/login-form.tsx b/apps/web/app/login/login-form.tsx
index 24038e6c..98bebd35 100644
--- a/apps/web/app/login/login-form.tsx
+++ b/apps/web/app/login/login-form.tsx
@@ -1,13 +1,14 @@
'use client'
import { useState, useEffect } from 'react'
-import { useSearchParams } from 'next/navigation'
+import { useSearchParams, useRouter } from 'next/navigation'
import { useGoogleLogin } from '@react-oauth/google'
import { Button } from '@multica/ui/components/ui/button'
import { Input } from '@multica/ui/components/ui/input'
import { Label } from '@multica/ui/components/ui/label'
import { MulticaIcon } from '@multica/ui/components/multica-icon'
import { LoginAuthType, UserInfo } from '@/lib/interface'
+import { saveSession, isAuthenticated } from '@/lib/auth'
import { userLogin } from '@/service/user'
type LoginStep = 'email' | 'code'
@@ -37,8 +38,16 @@ function GoogleIcon() {
export function LoginForm() {
const searchParams = useSearchParams()
+ const router = useRouter()
const next = searchParams.get('next')
+ // Redirect if already authenticated (and not desktop flow)
+ useEffect(() => {
+ if (!next && isAuthenticated()) {
+ router.replace('/')
+ }
+ }, [next, router])
+
// Form state
const [email, setEmail] = useState('')
const [code, setCode] = useState('')
@@ -95,6 +104,9 @@ export function LoginForm() {
// Handle login success
const handleLoginSuccess = (sid: string, user: UserInfo) => {
+ // Save session to cookie for web app
+ saveSession(sid, user)
+
if (next) {
// Desktop flow - parse next URL to get port, then redirect directly to callback
try {
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index 50d9c193..dada2a25 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -1,5 +1,10 @@
import ChatPage from "@/components/pages/chat-page";
+import { AuthGuard } from "@/components/auth-guard";
export default function Page() {
- return ;
+ return (
+
+
+
+ );
}
diff --git a/apps/web/components/auth-guard.tsx b/apps/web/components/auth-guard.tsx
new file mode 100644
index 00000000..ed17c329
--- /dev/null
+++ b/apps/web/components/auth-guard.tsx
@@ -0,0 +1,38 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { isAuthenticated } from '@/lib/auth'
+
+interface AuthGuardProps {
+ children: React.ReactNode
+}
+
+export function AuthGuard({ children }: AuthGuardProps) {
+ const router = useRouter()
+ const [isChecking, setIsChecking] = useState(true)
+ const [isAuthed, setIsAuthed] = useState(false)
+
+ useEffect(() => {
+ if (isAuthenticated()) {
+ setIsAuthed(true)
+ } else {
+ router.replace('/login')
+ }
+ setIsChecking(false)
+ }, [router])
+
+ if (isChecking) {
+ return (
+
+ )
+ }
+
+ if (!isAuthed) {
+ return null
+ }
+
+ return <>{children}>
+}
diff --git a/apps/web/lib/auth.ts b/apps/web/lib/auth.ts
new file mode 100644
index 00000000..d288021f
--- /dev/null
+++ b/apps/web/lib/auth.ts
@@ -0,0 +1,60 @@
+/**
+ * Client-side auth utilities
+ * Stores session in cookie for API authentication
+ */
+
+import type { UserInfo } from './interface'
+
+const SID_COOKIE_NAME = 'multica_sid'
+const USER_COOKIE_NAME = 'multica_user'
+
+// Cookie helpers
+function setCookie(name: string, value: string, days = 30) {
+ const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString()
+ document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`
+}
+
+function getCookie(name: string): string | null {
+ const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`))
+ return match ? decodeURIComponent(match[2]) : null
+}
+
+function deleteCookie(name: string) {
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
+}
+
+// Auth functions
+export function saveSession(sid: string, user: UserInfo) {
+ setCookie(SID_COOKIE_NAME, sid)
+ setCookie(USER_COOKIE_NAME, JSON.stringify(user))
+}
+
+export function getSession(): { sid: string; user: UserInfo } | null {
+ if (typeof window === 'undefined') return null
+
+ const sid = getCookie(SID_COOKIE_NAME)
+ const userJson = getCookie(USER_COOKIE_NAME)
+
+ if (!sid || !userJson) return null
+
+ try {
+ const user = JSON.parse(userJson) as UserInfo
+ return { sid, user }
+ } catch {
+ return null
+ }
+}
+
+export function getSid(): string | null {
+ if (typeof window === 'undefined') return null
+ return getCookie(SID_COOKIE_NAME)
+}
+
+export function clearSession() {
+ deleteCookie(SID_COOKIE_NAME)
+ deleteCookie(USER_COOKIE_NAME)
+}
+
+export function isAuthenticated(): boolean {
+ return !!getSid()
+}
diff --git a/apps/web/service/request.ts b/apps/web/service/request.ts
index 4a2ef598..e4ac3921 100644
--- a/apps/web/service/request.ts
+++ b/apps/web/service/request.ts
@@ -1,13 +1,16 @@
import { API_HOST } from '@/lib/constant';
import { generateDeviceIdHeader, getOrCreateDeviceId } from '@/lib/device';
+import { getSid } from '@/lib/auth';
// Fetch request wrapper
export async function request(url: string, options: RequestInit = {}): Promise {
// Get or generate Device ID
let deviceIdHeader = '';
+ let sid: string | null = null;
if (typeof window !== 'undefined') {
const deviceId = getOrCreateDeviceId();
deviceIdHeader = await generateDeviceIdHeader(deviceId);
+ sid = getSid();
}
const config: RequestInit = {
@@ -16,6 +19,7 @@ export async function request(url: string, options: RequestInit = {
'Content-Type': 'application/json',
'os-type': '3',
...(deviceIdHeader && { 'Device-Id': deviceIdHeader }),
+ ...(sid && { 'Authorization': `Bearer ${sid}` }),
...options.headers,
},
};