refactor: replace Supabase with self-hosted backend API

This commit is contained in:
ddshi 2026-01-29 12:58:49 +08:00
parent e1f2c8d536
commit 8801591132
2 changed files with 185 additions and 28 deletions

141
src/services/api.ts Normal file
View File

@ -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<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
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<Event[]>(`/events${query}`);
},
getById: async (id: string) => {
return api.request<Event>(`/events/${id}`);
},
create: async (event: Partial<Event>) => {
return api.request<Event>('/events', {
method: 'POST',
body: JSON.stringify(event),
});
},
update: async (id: string, updates: Partial<Event>) => {
return api.request<Event>(`/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<Note>('/notes');
},
update: async (content: string) => {
return api.request<Note>('/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 }),
});
},
},
};

View File

@ -1,7 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import type { User, Event, Note, AIConversation } from '../types'; import type { User, Event, Note, AIConversation } from '../types';
import { auth } from '../services/supabase'; import { api } from '../services/api';
interface AppState { interface AppState {
// Auth state // Auth state
@ -34,7 +34,7 @@ interface AppState {
export const useAppStore = create<AppState>()( export const useAppStore = create<AppState>()(
persist( persist(
(set, get) => ({ (set) => ({
// Initial state // Initial state
user: null, user: null,
isAuthenticated: false, isAuthenticated: false,
@ -62,45 +62,61 @@ export const useAppStore = create<AppState>()(
// Auth actions // Auth actions
login: async (email, password) => { login: async (email, password) => {
const { error } = await auth.signIn(email, password); try {
if (!error) { const { user, token, refreshToken } = await api.auth.login(email, password);
const { session } = await auth.getSession(); api.setTokens(token, refreshToken);
if (session?.user) { set({ user, isAuthenticated: true });
const user: User = { return { error: null };
id: session.user.id, } catch (error: any) {
email: session.user.email!, return { error: error.message || '登录失败' };
created_at: session.user.created_at,
updated_at: session.user.updated_at,
};
set({ user, isAuthenticated: true });
}
} }
return { error };
}, },
register: async (email, password, nickname) => { register: async (email, password, nickname) => {
const { error } = await auth.signUp(email, password, nickname); try {
return { error }; 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 () => { logout: async () => {
await auth.signOut(); try {
await api.auth.logout();
} catch {
// Ignore logout error
}
api.clearTokens();
set({ user: null, isAuthenticated: false, events: [], notes: null }); set({ user: null, isAuthenticated: false, events: [], notes: null });
}, },
checkAuth: async () => { checkAuth: async () => {
set({ isLoading: true }); set({ isLoading: true });
const { session } = await auth.getSession(); const token = api.getToken();
if (session?.user) {
const user: User = { if (!token) {
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 {
set({ isAuthenticated: false, isLoading: false }); 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 });
}
} }
}, },
}), }),