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, }, };