From 88015911322f170a6e461145d73852894edd73f3 Mon Sep 17 00:00:00 2001 From: ddshi <8811906+ddshi@user.noreply.gitee.com> Date: Thu, 29 Jan 2026 12:58:49 +0800 Subject: [PATCH] refactor: replace Supabase with self-hosted backend API --- src/services/api.ts | 141 ++++++++++++++++++++++++++++++++++++++++++++ src/stores/index.ts | 72 +++++++++++++--------- 2 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 src/services/api.ts diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..b2cea30 --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,141 @@ +import type { User, Event, Note, AIConversation, EventType } from '../types'; + +// API Base URL - from environment variable +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; + +// Token storage +const TOKEN_KEY = 'qia_token'; +const REFRESH_TOKEN_KEY = 'qia_refresh_token'; + +export const api = { + // Token management + getToken: () => localStorage.getItem(TOKEN_KEY), + getRefreshToken: () => localStorage.getItem(REFRESH_TOKEN_KEY), + setTokens: (token: string, refreshToken: string) => { + localStorage.setItem(TOKEN_KEY, token); + localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); + }, + clearTokens: () => { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_TOKEN_KEY); + }, + + // Generic request helper + async request(endpoint: string, options: RequestInit = {}): Promise { + const token = api.getToken(); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + ...options.headers, + }; + + const response = await fetch(`${API_URL}${endpoint}`, { + ...options, + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Request failed'); + } + + return data; + }, + + // Auth API + auth: { + register: async (email: string, password: string, nickname?: string) => { + return api.request<{ user: User; token: string; refreshToken: string }>( + '/auth/register', + { + method: 'POST', + body: JSON.stringify({ email, password, nickname }), + } + ); + }, + + login: async (email: string, password: string) => { + return api.request<{ user: User; token: string; refreshToken: string }>( + '/auth/login', + { + method: 'POST', + body: JSON.stringify({ email, password }), + } + ); + }, + + logout: async () => { + return api.request('/auth/logout', { method: 'POST' }); + }, + + getCurrentUser: async () => { + return api.request<{ user: User }>('/auth/me'); + }, + + refreshToken: async () => { + const refreshToken = api.getRefreshToken(); + if (!refreshToken) throw new Error('No refresh token'); + + return api.request<{ token: string }>('/auth/refresh', { + method: 'POST', + body: JSON.stringify({ refreshToken }), + }); + }, + }, + + // Events API + events: { + getAll: async (type?: EventType) => { + const query = type ? `?type=${type}` : ''; + return api.request(`/events${query}`); + }, + + getById: async (id: string) => { + return api.request(`/events/${id}`); + }, + + create: async (event: Partial) => { + return api.request('/events', { + method: 'POST', + body: JSON.stringify(event), + }); + }, + + update: async (id: string, updates: Partial) => { + return api.request(`/events/${id}`, { + method: 'PUT', + body: JSON.stringify(updates), + }); + }, + + delete: async (id: string) => { + return api.request(`/events/${id}`, { method: 'DELETE' }); + }, + }, + + // Notes API + notes: { + get: async () => { + return api.request('/notes'); + }, + + update: async (content: string) => { + return api.request('/notes', { + method: 'PUT', + body: JSON.stringify({ content }), + }); + }, + }, + + // AI API + ai: { + parse: async (message: string) => { + return api.request<{ parsed: any; response: string }>('/ai/parse', { + method: 'POST', + body: JSON.stringify({ message }), + }); + }, + }, +}; diff --git a/src/stores/index.ts b/src/stores/index.ts index fa5fbad..b67539d 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { User, Event, Note, AIConversation } from '../types'; -import { auth } from '../services/supabase'; +import { api } from '../services/api'; interface AppState { // Auth state @@ -34,7 +34,7 @@ interface AppState { export const useAppStore = create()( persist( - (set, get) => ({ + (set) => ({ // Initial state user: null, isAuthenticated: false, @@ -62,45 +62,61 @@ export const useAppStore = create()( // Auth actions login: async (email, password) => { - const { error } = await auth.signIn(email, password); - if (!error) { - const { session } = await auth.getSession(); - if (session?.user) { - const user: User = { - id: session.user.id, - email: session.user.email!, - created_at: session.user.created_at, - updated_at: session.user.updated_at, - }; - set({ user, isAuthenticated: true }); - } + try { + const { user, token, refreshToken } = await api.auth.login(email, password); + api.setTokens(token, refreshToken); + set({ user, isAuthenticated: true }); + return { error: null }; + } catch (error: any) { + return { error: error.message || '登录失败' }; } - return { error }; }, register: async (email, password, nickname) => { - const { error } = await auth.signUp(email, password, nickname); - return { error }; + try { + const { user, token, refreshToken } = await api.auth.register(email, password, nickname); + api.setTokens(token, refreshToken); + set({ user, isAuthenticated: true }); + return { error: null }; + } catch (error: any) { + return { error: error.message || '注册失败' }; + } }, logout: async () => { - await auth.signOut(); + try { + await api.auth.logout(); + } catch { + // Ignore logout error + } + api.clearTokens(); set({ user: null, isAuthenticated: false, events: [], notes: null }); }, checkAuth: async () => { set({ isLoading: true }); - const { session } = await auth.getSession(); - if (session?.user) { - const user: User = { - id: session.user.id, - email: session.user.email!, - created_at: session.user.created_at, - updated_at: session.user.updated_at, - }; - set({ user, isAuthenticated: true, isLoading: false }); - } else { + const token = api.getToken(); + + if (!token) { set({ isAuthenticated: false, isLoading: false }); + return; + } + + try { + const { user } = await api.auth.getCurrentUser(); + set({ user, isAuthenticated: true, isLoading: false }); + } catch { + // Token invalid, try refresh + try { + const { token: newToken } = await api.auth.refreshToken(); + const { user } = await api.auth.getCurrentUser(); + const refreshToken = api.getRefreshToken()!; + api.setTokens(newToken, refreshToken); + set({ user, isAuthenticated: true, isLoading: false }); + } catch { + api.clearTokens(); + set({ isAuthenticated: false, isLoading: false }); + } } }, }),