From 8a3c10e7852bce1ccdf60806e3bc6aabe9a0b273 Mon Sep 17 00:00:00 2001 From: kokopi Date: Mon, 9 Mar 2026 15:31:04 +0900 Subject: [PATCH] update:routing --- frontend/src/App.tsx | 4 +- frontend/src/lib/storage.ts | 75 +++++++++----------------------- frontend/src/pages/AdminPage.tsx | 14 ++++-- frontend/src/pages/UserPage.tsx | 13 +++--- 4 files changed, 41 insertions(+), 65 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 59c65d2..ba0fc13 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -55,8 +55,8 @@ function SupportApp() { } > - {activeTab === 'tickets' && } - {activeTab === 'admin' && } + {activeTab === 'tickets' && } + {activeTab === 'admin' && } ) } diff --git a/frontend/src/lib/storage.ts b/frontend/src/lib/storage.ts index a2d08f6..4f9ffc3 100644 --- a/frontend/src/lib/storage.ts +++ b/frontend/src/lib/storage.ts @@ -1,57 +1,18 @@ import type { Ticket, TicketType } from './types' +import { env } from '../env' -const API = import.meta.env.VITE_API_URL ?? '' -const isProd = import.meta.env.PROD - -// ─── CSRF ──────────────────────────────────────────────────────────────────── -// In production we fetch a CSRF token once and attach it to all mutating -// requests via the x-csrf-token header (required by @fastify/csrf-protection). - -let csrfToken: string | null = null - -async function getCsrfToken(): Promise { - if (!isProd) return null - if (csrfToken) return csrfToken - const res = await fetch(`${API}/api/auth/csrf-token`, { credentials: 'include' }) - const json = await res.json() - csrfToken = json.token ?? null - return csrfToken -} - -// Invalidate cached token on 403 so the next call fetches a fresh one. -function invalidateCsrf() { - csrfToken = null -} +const API = env.apiUrl // ─── Fetch helper ───────────────────────────────────────────────────────────── async function apiFetch(path: string, init: RequestInit = {}): Promise { - const method = (init.method ?? 'GET').toUpperCase() - const isMutating = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) - - const headers: Record = { - 'Content-Type': 'application/json', - ...(init.headers as Record | undefined), - } - - if (isMutating) { - const token = await getCsrfToken() - if (token) headers['x-csrf-token'] = token - } - const res = await fetch(`${API}${path}`, { ...init, credentials: 'include', - headers, + headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}) }, }) - if (res.status === 401) throw new Error('unauthenticated') - if (res.status === 403) { - invalidateCsrf() - throw new Error('forbidden') - } if (!res.ok) throw new Error(`API error ${res.status}`) - return res.json() } @@ -73,6 +34,7 @@ function localSet(tickets: Ticket[]) { export const localAdapter = { getTickets: (): Ticket[] => localGet(), + createTicket: (data: { subject: string; description: string; type: TicketType }): Ticket => { const ticket: Ticket = { id: crypto.randomUUID(), @@ -83,28 +45,31 @@ export const localAdapter = { status: 'open', createdAt: new Date().toISOString(), } - localSet([...localGet(), ticket]) + localSet([ticket, ...localGet()]) return ticket }, + updateTicket: (id: string, patch: Partial): Ticket | null => { const tickets = localGet() - const idx = tickets.findIndex((t) => t.id === id) + const idx = tickets.findIndex(t => t.id === id) if (idx === -1) return null tickets[idx] = { ...tickets[idx], ...patch } localSet(tickets) return tickets[idx] }, + deleteTicket: (id: string): boolean => { const before = localGet() - const after = before.filter((t) => t.id !== id) + const after = before.filter(t => t.id !== id) localSet(after) return after.length < before.length }, } -// ─── API adapter (falls back to local on 401) ───────────────────────────────── +// ─── Storage API ────────────────────────────────────────────────────────────── export const storage = { + // User's own tickets — API when authenticated, localStorage when guest async getTickets(): Promise { try { return await apiFetch('/api/tickets') @@ -113,6 +78,16 @@ export const storage = { } }, + // Admin view — all DB tickets when authenticated, localStorage when guest + async getAllTickets(isAuthenticated: boolean): Promise { + if (!isAuthenticated) return localAdapter.getTickets() + try { + return await apiFetch('/api/tickets/all') + } catch { + return localAdapter.getTickets() + } + }, + async createTicket(data: { subject: string; description: string; type: TicketType }): Promise { try { return await apiFetch('/api/tickets', { @@ -143,12 +118,4 @@ export const storage = { return localAdapter.deleteTicket(id) } }, - - async getAllTickets(): Promise { - try { - return await apiFetch('/api/tickets/all') - } catch { - return localAdapter.getTickets() - } - }, } diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index e538fec..b552695 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -17,12 +17,16 @@ function StatCard({ label, value }: StatCardProps) { ) } -export function AdminPage() { +interface AdminPageProps { + isAuthenticated: boolean +} + +export function AdminPage({ isAuthenticated }: AdminPageProps) { const [tickets, setTickets] = useState([]) useEffect(() => { - storage.getTickets().then(setTickets) - }, []) + storage.getAllTickets(isAuthenticated).then(setTickets) + }, [isAuthenticated]) const stats = { total: tickets.length, @@ -35,7 +39,9 @@ export function AdminPage() { <>

Admin

-

All tickets across the system

+

+ {isAuthenticated ? 'All tickets across the system' : 'Your local tickets'} +

diff --git a/frontend/src/pages/UserPage.tsx b/frontend/src/pages/UserPage.tsx index 4dab831..7097c5e 100644 --- a/frontend/src/pages/UserPage.tsx +++ b/frontend/src/pages/UserPage.tsx @@ -6,14 +6,19 @@ import { NewTicketForm } from '../components/tickets/NewTicketForm.tsx' import { useModal } from '../hooks/useModal.ts' import { storage } from '../lib/storage.ts' import type { Ticket } from '../lib/types.ts' +import { PlusIcon } from '../components/icons/plus.tsx' -export function UserPage() { +interface UserPageProps { + isAuthenticated: boolean +} + +export function UserPage({ isAuthenticated }: UserPageProps) { const [tickets, setTickets] = useState([]) const newTicketModal = useModal() useEffect(() => { storage.getTickets().then(setTickets) - }, []) + }, [isAuthenticated]) const handleCreate = async (form: Pick) => { const ticket = await storage.createTicket(form) @@ -36,9 +41,7 @@ export function UserPage() {