Fix API routing: apiFetch helper prefixes basePath for all /api/v1/ calls

Without this, fetch('/api/v1/...') from the browser hits Apache root,
which routes /api/ to OliVAS (port 8000) instead of DeckForge (port 8001).
apiFetch prepends NEXT_PUBLIC_BASE_PATH so requests go through Next.js
rewrites to the correct backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-19 22:12:25 +00:00
parent 3077159da3
commit bebe2ac390
22 changed files with 99 additions and 66 deletions

View file

@ -1,6 +1,7 @@
"use client";
import React, { useEffect, useState } from "react";
import { apiFetch } from '../../../lib/apiFetch';
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
@ -57,7 +58,7 @@ export default function ReviewWorkflow({ presentationId }: ReviewWorkflowProps)
const fetchReviewInfo = async () => {
try {
const response = await fetch(`/api/v1/ppt/presentation/${presentationId}/review`, {
const response = await apiFetch(`/api/v1/ppt/presentation/${presentationId}/review`, {
headers: getHeader(),
});
const data = await ApiResponseHandler.handleResponse(response, "Failed to fetch review info");
@ -72,7 +73,7 @@ export default function ReviewWorkflow({ presentationId }: ReviewWorkflowProps)
const handleStatusChange = async (newStatus: string) => {
setIsUpdating(true);
try {
const response = await fetch(`/api/v1/ppt/presentation/${presentationId}/status`, {
const response = await apiFetch(`/api/v1/ppt/presentation/${presentationId}/status`, {
method: "PUT",
headers: getHeader(),
body: JSON.stringify({ status: newStatus, comment: comment || null }),
@ -93,7 +94,7 @@ export default function ReviewWorkflow({ presentationId }: ReviewWorkflowProps)
if (!comment.trim()) return;
setIsUpdating(true);
try {
const response = await fetch(`/api/v1/ppt/presentation/${presentationId}/comment`, {
const response = await apiFetch(`/api/v1/ppt/presentation/${presentationId}/comment`, {
method: "POST",
headers: getHeader(),
body: JSON.stringify({ comment }),

View file

@ -1,4 +1,5 @@
import { useEffect } from "react";
import { apiFetch } from '../../../lib/apiFetch';
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { getHeader } from "../services/api/header";
@ -74,7 +75,7 @@ export function useBrandTheme() {
}
// Fetch brand config for selected client
fetch(`/api/v1/admin/clients/${selectedClientId}/brand`, {
apiFetch(`/api/v1/admin/clients/${selectedClientId}/brand`, {
headers: getHeader(),
})
.then((res) => {

View file

@ -1,4 +1,5 @@
import { getHeaderForFormData } from "./header";
import { apiFetch } from '../../../../lib/apiFetch';
import { ApiResponseHandler } from "./api-error-handler";
import { ImageAssetResponse } from "./types";
@ -9,7 +10,7 @@ export class ImagesApi {
try {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(`/api/v1/ppt/images/upload`, {
const response = await apiFetch(`/api/v1/ppt/images/upload`, {
method: "POST",
headers: getHeaderForFormData(),
body: formData,
@ -23,7 +24,7 @@ export class ImagesApi {
static async getUploadedImages(): Promise<ImageAssetResponse[]> {
try {
const response = await fetch(`/api/v1/ppt/images/uploaded`);
const response = await apiFetch(`/api/v1/ppt/images/uploaded`);
return await ApiResponseHandler.handleResponse(response, "Failed to get uploaded images") as ImageAssetResponse[];
} catch (error:any) {
console.log("Get uploaded images error:", error);
@ -33,7 +34,7 @@ export class ImagesApi {
static async deleteImage(image_id: string): Promise<{success: boolean, message?: string}> {
try {
const response = await fetch(`/api/v1/ppt/images/${image_id}`, {
const response = await apiFetch(`/api/v1/ppt/images/${image_id}`, {
method: "DELETE"
});
return await ApiResponseHandler.handleResponse(response, "Failed to delete image") as {success: boolean, message?: string};

View file

@ -1,10 +1,11 @@
import { ApiResponseHandler } from "./api-error-handler";
import { apiFetch } from '../../../../lib/apiFetch';
class TemplateService {
static async getCustomTemplateSummaries() {
try {
const response = await fetch(`/api/v1/ppt/template-management/summary`,);
const response = await apiFetch(`/api/v1/ppt/template-management/summary`,);
return await ApiResponseHandler.handleResponse(response, "Failed to get custom template summaries");
} catch (error) {
console.error("Failed to get custom template summaries", error);
@ -14,7 +15,7 @@ class TemplateService {
static async getCustomTemplateDetails(templateId: string) {
try {
const response = await fetch(`/api/v1/ppt/template-management/get-templates/${templateId}`,);
const response = await apiFetch(`/api/v1/ppt/template-management/get-templates/${templateId}`,);
return await ApiResponseHandler.handleResponse(response, "Failed to get custom template details");
} catch (error) {
console.error("Failed to get custom template details", error);
@ -24,7 +25,7 @@ class TemplateService {
static async deleteCustomTemplate(presentationId: string) {
try {
const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, { method: "DELETE" });
const response = await apiFetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, { method: "DELETE" });
return await ApiResponseHandler.handleResponseWithResult(response, "Failed to delete custom template");
} catch (error) {
console.error("Failed to delete custom template", error);

View file

@ -1,4 +1,5 @@
import { getHeader, getHeaderForFormData } from "./header";
import { apiFetch } from '../../../../lib/apiFetch';
import { ApiResponseHandler } from "./api-error-handler";
export interface ClientOption {
@ -30,7 +31,7 @@ export class WizardApi {
/** Fetch clients available to the current user */
static async getClients(): Promise<ClientOption[]> {
try {
const response = await fetch("/api/v1/admin/clients", {
const response = await apiFetch("/api/v1/admin/clients", {
method: "GET",
headers: getHeader(),
});
@ -46,7 +47,7 @@ export class WizardApi {
static async getMasterDecks(clientId?: string): Promise<MasterDeckOption[]> {
try {
const params = clientId ? `?client_id=${clientId}` : "";
const response = await fetch(`/api/v1/admin/master-decks${params}`, {
const response = await apiFetch(`/api/v1/admin/master-decks${params}`, {
method: "GET",
headers: getHeader(),
});
@ -63,7 +64,7 @@ export class WizardApi {
const formData = new FormData();
files.forEach((file) => formData.append("files", file));
const response = await fetch("/api/v1/ppt/files/upload", {
const response = await apiFetch("/api/v1/ppt/files/upload", {
method: "POST",
headers: getHeaderForFormData(),
body: formData,
@ -74,7 +75,7 @@ export class WizardApi {
/** Decompose uploaded documents */
static async decomposeFiles(filePaths: string[]): Promise<any[]> {
const response = await fetch("/api/v1/ppt/files/decompose", {
const response = await apiFetch("/api/v1/ppt/files/decompose", {
method: "POST",
headers: getHeader(),
body: JSON.stringify({ file_paths: filePaths }),
@ -94,7 +95,7 @@ export class WizardApi {
client_id?: string;
master_deck_id?: string;
}) {
const response = await fetch("/api/v1/ppt/presentation/generate/async", {
const response = await apiFetch("/api/v1/ppt/presentation/generate/async", {
method: "POST",
headers: getHeader(),
body: JSON.stringify(params),
@ -105,7 +106,7 @@ export class WizardApi {
/** Poll job status */
static async getJobStatus(jobId: string): Promise<JobStatus> {
const response = await fetch(`/api/v1/ppt/jobs/${jobId}`, {
const response = await apiFetch(`/api/v1/ppt/jobs/${jobId}`, {
method: "GET",
headers: getHeader(),
});
@ -114,7 +115,7 @@ export class WizardApi {
/** Cancel a job */
static async cancelJob(jobId: string): Promise<void> {
const response = await fetch(`/api/v1/ppt/jobs/${jobId}`, {
const response = await apiFetch(`/api/v1/ppt/jobs/${jobId}`, {
method: "DELETE",
headers: getHeader(),
});
@ -123,7 +124,7 @@ export class WizardApi {
/** Fetch URL content and extract text */
static async fetchUrl(url: string): Promise<string> {
const response = await fetch("/api/v1/ppt/files/fetch-url", {
const response = await apiFetch("/api/v1/ppt/files/fetch-url", {
method: "POST",
headers: getHeader(),
body: JSON.stringify({ url }),
@ -138,7 +139,7 @@ export class WizardApi {
static async checkFollowUpQuestions(content: string): Promise<string[]> {
if (!content || content.trim().length < 10) return [];
try {
const response = await fetch("/api/v1/ppt/content/follow-up-questions", {
const response = await apiFetch("/api/v1/ppt/content/follow-up-questions", {
method: "POST",
headers: getHeader(),
body: JSON.stringify({ content }),
@ -162,7 +163,7 @@ export class WizardApi {
client_id?: string;
master_deck_id?: string;
}) {
const response = await fetch("/api/v1/ppt/presentation/create", {
const response = await apiFetch("/api/v1/ppt/presentation/create", {
method: "POST",
headers: getHeader(),
body: JSON.stringify(params),

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useRef, useCallback, useState } from 'react';
import { apiFetch } from '../../../../../lib/apiFetch';
import { getHeader } from '@/app/(presentation-generator)/services/api/header';
import { ApiResponseHandler } from '@/app/(presentation-generator)/services/api/api-error-handler';
import { ProcessedSlide } from '@/app/(presentation-generator)/custom-template/types';
@ -77,7 +78,7 @@ export const useTemplateLayoutsAutoSave = ({
setSaveStatus('saving');
console.log('🔄 Auto-saving template layouts...');
const response = await fetch('/api/v1/ppt/template/update', {
const response = await apiFetch('/api/v1/ppt/template/update', {
method: 'PUT',
headers: getHeader(),
body: JSON.stringify({

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { apiFetch } from '../lib/apiFetch';
import { setCanChangeKeys, setLLMConfig } from '@/store/slices/userConfig';
import { hasValidLLMConfig } from '@/utils/storeHelpers';
import { usePathname, useRouter } from 'next/navigation';
@ -86,7 +87,7 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo
const checkIfSelectedCustomModelIsAvailable = async (llmConfig: LLMConfig) => {
try {
const response = await fetch('/api/v1/ppt/openai/models/available', {
const response = await apiFetch('/api/v1/ppt/openai/models/available', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -1,6 +1,7 @@
'use client';
import React, { useEffect, useState } from 'react';
import { apiFetch } from '../../../lib/apiFetch';
import { useSelector } from 'react-redux';
import { RootState } from '@/store/store';
import {
@ -56,7 +57,7 @@ interface AIUsageData {
async function fetchAnalytics(endpoint: string, clientId?: string) {
const params = clientId ? `?client_id=${clientId}` : '';
const response = await fetch(`/api/v1/admin/analytics/${endpoint}${params}`, {
const response = await apiFetch(`/api/v1/admin/analytics/${endpoint}${params}`, {
headers: getHeader(),
});
if (!response.ok) throw new Error(`Failed to fetch ${endpoint}`);

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { apiFetch } from '../../../../../lib/apiFetch';
import { useParams } from 'next/navigation';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from '@/store/store';
@ -78,7 +79,7 @@ export default function BrandConfigPage() {
formData.append('file', file);
try {
const res = await fetch(`/api/v1/admin/clients/${clientId}/brand/logo`, {
const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand/logo`, {
method: 'POST',
body: formData,
});
@ -95,7 +96,7 @@ export default function BrandConfigPage() {
const handleDeleteLogo = async (index: number) => {
try {
const res = await fetch(`/api/v1/admin/clients/${clientId}/brand/logo/${index}`, {
const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand/logo/${index}`, {
method: 'DELETE',
});
if (res.ok) {
@ -115,7 +116,7 @@ export default function BrandConfigPage() {
formData.append('file', file);
try {
const res = await fetch(`/api/v1/admin/clients/${clientId}/brand/guideline`, {
const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand/guideline`, {
method: 'POST',
body: formData,
});

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { apiFetch } from '../../../../lib/apiFetch';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
@ -17,7 +18,7 @@ export default function ClientDetailPage() {
useEffect(() => {
const fetchClient = async () => {
try {
const res = await fetch(`/api/v1/admin/clients/${clientId}`);
const res = await apiFetch(`/api/v1/admin/clients/${clientId}`);
if (res.ok) setClient(await res.json());
} finally {
setLoading(false);

View file

@ -1,6 +1,7 @@
'use client';
import React, { useEffect, useState, useCallback } from 'react';
import { apiFetch } from '../../../lib/apiFetch';
import {
Settings,
Loader2,
@ -78,7 +79,7 @@ export default function SettingsPage() {
setLoadingModels(true);
setAvailableModels([]);
try {
const res = await fetch(`/api/v1/admin/settings/models?provider=${provider}`, {
const res = await apiFetch(`/api/v1/admin/settings/models?provider=${provider}`, {
headers: getHeader(),
});
if (res.ok) {
@ -103,7 +104,7 @@ export default function SettingsPage() {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/v1/admin/settings', { headers: getHeader() });
const res = await apiFetch('/api/v1/admin/settings', { headers: getHeader() });
if (res.status === 403) {
setError('Super admin access required');
return;
@ -136,7 +137,7 @@ export default function SettingsPage() {
return;
}
const res = await fetch('/api/v1/admin/settings', {
const res = await apiFetch('/api/v1/admin/settings', {
method: 'PUT',
headers: { ...getHeader(), 'Content-Type': 'application/json' },
body: JSON.stringify(body),
@ -161,7 +162,7 @@ export default function SettingsPage() {
const body: Record<string, string> = { provider: 'google' };
if (googleKey) body.api_key = googleKey;
const res = await fetch('/api/v1/admin/settings/test-connection', {
const res = await apiFetch('/api/v1/admin/settings/test-connection', {
method: 'POST',
headers: { ...getHeader(), 'Content-Type': 'application/json' },
body: JSON.stringify(body),

View file

@ -1,6 +1,7 @@
'use client';
import React, { useEffect, useState, useCallback } from 'react';
import { apiFetch } from '../../../lib/apiFetch';
import { useSelector } from 'react-redux';
import { RootState } from '@/store/store';
import {
@ -90,7 +91,7 @@ export default function StoragePage() {
// Load client list for super_admin
useEffect(() => {
if (isSuperAdmin) {
fetch('/api/v1/admin/clients', { headers: getHeader() })
apiFetch('/api/v1/admin/clients', { headers: getHeader() })
.then((r) => (r.ok ? r.json() : []))
.then((data) => setClients(data))
.catch(() => {});
@ -104,8 +105,8 @@ export default function StoragePage() {
const params = selectedClientId ? `?client_id=${selectedClientId}` : '';
const headers = getHeader();
const [summaryRes, presRes] = await Promise.all([
fetch(`/api/v1/admin/storage/summary${params}`, { headers }),
fetch(`/api/v1/admin/storage/presentations${params}`, { headers }),
apiFetch(`/api/v1/admin/storage/summary${params}`, { headers }),
apiFetch(`/api/v1/admin/storage/presentations${params}`, { headers }),
]);
if (summaryRes.ok) setSummary(await summaryRes.json());
if (presRes.ok) setPresentations(await presRes.json());
@ -127,7 +128,7 @@ export default function StoragePage() {
const handleDelete = async () => {
if (!deleteTarget) return;
try {
const res = await fetch(`/api/v1/admin/storage/presentations/${deleteTarget.id}`, {
const res = await apiFetch(`/api/v1/admin/storage/presentations/${deleteTarget.id}`, {
method: 'DELETE',
headers: getHeader(),
});
@ -145,7 +146,7 @@ export default function StoragePage() {
const handleBulkDelete = async () => {
try {
const res = await fetch('/api/v1/admin/storage/presentations/bulk-delete', {
const res = await apiFetch('/api/v1/admin/storage/presentations/bulk-delete', {
method: 'POST',
headers: { ...getHeader(), 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: Array.from(selectedIds) }),
@ -167,7 +168,7 @@ export default function StoragePage() {
setPurging(true);
try {
const params = selectedClientId ? `?client_id=${selectedClientId}` : '';
const res = await fetch(`/api/v1/admin/storage/purge${params}`, {
const res = await apiFetch(`/api/v1/admin/storage/purge${params}`, {
method: 'POST',
headers: getHeader(),
});

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { apiFetch } from '../../../../lib/apiFetch';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
@ -43,7 +44,7 @@ export default function TemplateDetailPage() {
const load = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(`/api/v1/ppt/template-management/get-templates/${templateId}`);
const res = await apiFetch(`/api/v1/ppt/template-management/get-templates/${templateId}`);
if (!res.ok) throw new Error('Failed to load template');
const data = await res.json();

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { apiFetch } from '../../../lib/apiFetch';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import {
@ -35,7 +36,7 @@ export default function TemplatesPage() {
const load = useCallback(async () => {
setLoading(true);
try {
const res = await fetch('/api/v1/ppt/template-management/summary');
const res = await apiFetch('/api/v1/ppt/template-management/summary');
if (!res.ok) throw new Error('Failed to load templates');
const data = await res.json();
const mapped: TemplateSummary[] = (data.presentations || []).map((item: any) => ({
@ -58,7 +59,7 @@ export default function TemplatesPage() {
if (!deleteTarget) return;
setDeleting(true);
try {
const res = await fetch(`/api/v1/ppt/template-management/delete-templates/${deleteTarget.id}`, {
const res = await apiFetch(`/api/v1/ppt/template-management/delete-templates/${deleteTarget.id}`, {
method: 'DELETE',
});
if (!res.ok && res.status !== 204) throw new Error('Failed to delete');

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { apiFetch } from '../../../../lib/apiFetch';
import { useParams } from 'next/navigation';
import RoleBadge from '../../components/RoleBadge';
import { Button } from '@/components/ui/button';
@ -25,7 +26,7 @@ export default function UserDetailPage() {
useEffect(() => {
const fetchUser = async () => {
try {
const res = await fetch(`/api/v1/admin/users/${params.id}`);
const res = await apiFetch(`/api/v1/admin/users/${params.id}`);
if (res.ok) setUser(await res.json());
} finally {
setLoading(false);

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { apiFetch } from '../../lib/apiFetch';
import { useSelector } from 'react-redux';
import { RootState } from '@/store/store';
import { useRouter } from 'next/navigation';
@ -32,7 +33,7 @@ export default function LoginPage() {
setLoading(true);
try {
const response = await fetch('/api/v1/auth/dev-login', {
const response = await apiFetch('/api/v1/auth/dev-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),

View file

@ -1,5 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from '../lib/apiFetch';
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { Button } from "./ui/button";
import {
@ -53,7 +54,7 @@ export default function AnthropicConfig({
setModelsLoading(true);
try {
const response = await fetch('/api/v1/ppt/anthropic/models/available', {
const response = await apiFetch('/api/v1/ppt/anthropic/models/available', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -1,5 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from '../lib/apiFetch';
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { Button } from "./ui/button";
import {
@ -50,7 +51,7 @@ export default function GoogleConfig({
setModelsLoading(true);
try {
const response = await fetch('/api/v1/ppt/google/models/available', {
const response = await apiFetch('/api/v1/ppt/google/models/available', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -1,5 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { apiFetch } from '../lib/apiFetch';
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { Button } from "./ui/button";
import {
@ -41,7 +42,7 @@ export default function OllamaConfig({
const fetchOllamaModels = async () => {
try {
setOllamaModelsLoading(true);
const response = await fetch('/api/v1/ppt/ollama/models/supported');
const response = await apiFetch('/api/v1/ppt/ollama/models/supported');
if (response.ok) {
const data = await response.json();

View file

@ -1,5 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from '../lib/apiFetch';
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { Button } from "./ui/button";
import {
@ -52,7 +53,7 @@ export default function OpenAIConfig({
setModelsLoading(true);
try {
const response = await fetch('/api/v1/ppt/openai/models/available', {
const response = await apiFetch('/api/v1/ppt/openai/models/available', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

12
frontend/lib/apiFetch.ts Normal file
View file

@ -0,0 +1,12 @@
/**
* Wrapper around fetch that prepends the Next.js basePath so API calls
* reach the correct backend when deployed under a sub-path (e.g. /ppt-tool).
*
* Usage: apiFetch('/api/v1/...', options) identical to fetch(), just works.
*/
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
export function apiFetch(path: string, init?: RequestInit): Promise<Response> {
const url = path.startsWith('/api/') ? `${BASE_PATH}${path}` : path;
return fetch(url, init);
}

View file

@ -1,4 +1,5 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { apiFetch } from '../../lib/apiFetch';
export interface AdminUser {
id: string;
@ -94,7 +95,7 @@ const initialState: AdminState = {
export const fetchUsers = createAsyncThunk(
"admin/fetchUsers",
async (_, { rejectWithValue }) => {
const res = await fetch("/api/v1/admin/users");
const res = await apiFetch("/api/v1/admin/users");
if (!res.ok) return rejectWithValue("Failed to fetch users");
return await res.json();
}
@ -103,7 +104,7 @@ export const fetchUsers = createAsyncThunk(
export const updateUserRole = createAsyncThunk(
"admin/updateUserRole",
async ({ userId, role }: { userId: string; role: string }, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/users/${userId}/role?role=${role}`, { method: "PUT" });
const res = await apiFetch(`/api/v1/admin/users/${userId}/role?role=${role}`, { method: "PUT" });
if (!res.ok) return rejectWithValue("Failed to update role");
return await res.json();
}
@ -112,7 +113,7 @@ export const updateUserRole = createAsyncThunk(
export const deactivateUser = createAsyncThunk(
"admin/deactivateUser",
async (userId: string, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/users/${userId}`, { method: "DELETE" });
const res = await apiFetch(`/api/v1/admin/users/${userId}`, { method: "DELETE" });
if (!res.ok) return rejectWithValue("Failed to deactivate user");
return userId;
}
@ -123,7 +124,7 @@ export const deactivateUser = createAsyncThunk(
export const fetchClients = createAsyncThunk(
"admin/fetchClients",
async (_, { rejectWithValue }) => {
const res = await fetch("/api/v1/admin/clients");
const res = await apiFetch("/api/v1/admin/clients");
if (!res.ok) return rejectWithValue("Failed to fetch clients");
return await res.json();
}
@ -132,7 +133,7 @@ export const fetchClients = createAsyncThunk(
export const createClient = createAsyncThunk(
"admin/createClient",
async ({ name, review_policy }: { name: string; review_policy?: string }, { rejectWithValue }) => {
const res = await fetch("/api/v1/admin/clients", {
const res = await apiFetch("/api/v1/admin/clients", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, review_policy: review_policy || "self_approve" }),
@ -162,7 +163,7 @@ export const fetchTeams = createAsyncThunk(
export const fetchTeamDetail = createAsyncThunk(
"admin/fetchTeamDetail",
async (teamId: string) => {
const res = await fetch(`/api/v1/admin/teams/${teamId}`);
const res = await apiFetch(`/api/v1/admin/teams/${teamId}`);
if (!res.ok) throw new Error("Failed to fetch team");
return await res.json();
}
@ -171,7 +172,7 @@ export const fetchTeamDetail = createAsyncThunk(
export const addTeamMember = createAsyncThunk(
"admin/addTeamMember",
async ({ teamId, userId }: { teamId: string; userId: string }, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/teams/${teamId}/members`, {
const res = await apiFetch(`/api/v1/admin/teams/${teamId}/members`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId }),
@ -187,7 +188,7 @@ export const addTeamMember = createAsyncThunk(
export const removeTeamMember = createAsyncThunk(
"admin/removeTeamMember",
async ({ teamId, userId }: { teamId: string; userId: string }, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/teams/${teamId}/members/${userId}`, { method: "DELETE" });
const res = await apiFetch(`/api/v1/admin/teams/${teamId}/members/${userId}`, { method: "DELETE" });
if (!res.ok) return rejectWithValue("Failed to remove member");
return { teamId, userId };
}
@ -199,7 +200,7 @@ export const fetchAuditLogs = createAsyncThunk(
"admin/fetchAuditLogs",
async (params?: Record<string, string>) => {
const query = params ? "?" + new URLSearchParams(params).toString() : "";
const res = await fetch(`/api/v1/admin/audit-log${query}`);
const res = await apiFetch(`/api/v1/admin/audit-log${query}`);
if (!res.ok) throw new Error("Failed to fetch audit logs");
return await res.json();
}
@ -210,7 +211,7 @@ export const fetchAuditLogs = createAsyncThunk(
export const fetchBrandConfig = createAsyncThunk(
"admin/fetchBrandConfig",
async (clientId: string) => {
const res = await fetch(`/api/v1/admin/clients/${clientId}/brand`);
const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand`);
if (!res.ok) {
if (res.status === 404) return null;
throw new Error("Failed to fetch brand config");
@ -222,7 +223,7 @@ export const fetchBrandConfig = createAsyncThunk(
export const updateBrandConfig = createAsyncThunk(
"admin/updateBrandConfig",
async ({ clientId, data }: { clientId: string; data: Partial<BrandConfig> }, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/clients/${clientId}/brand`, {
const res = await apiFetch(`/api/v1/admin/clients/${clientId}/brand`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
@ -237,7 +238,7 @@ export const updateBrandConfig = createAsyncThunk(
export const fetchMasterDecks = createAsyncThunk(
"admin/fetchMasterDecks",
async (clientId: string) => {
const res = await fetch(`/api/v1/admin/clients/${clientId}/master-decks`);
const res = await apiFetch(`/api/v1/admin/clients/${clientId}/master-decks`);
if (!res.ok) throw new Error("Failed to fetch master decks");
return await res.json();
}
@ -248,7 +249,7 @@ export const uploadMasterDeck = createAsyncThunk(
async ({ clientId, file }: { clientId: string; file: File }, { rejectWithValue }) => {
const formData = new FormData();
formData.append("file", file);
const res = await fetch(`/api/v1/admin/clients/${clientId}/master-decks`, {
const res = await apiFetch(`/api/v1/admin/clients/${clientId}/master-decks`, {
method: "POST",
body: formData,
});
@ -263,7 +264,7 @@ export const uploadMasterDeck = createAsyncThunk(
export const fetchMasterDeckDetail = createAsyncThunk(
"admin/fetchMasterDeckDetail",
async (deckId: string) => {
const res = await fetch(`/api/v1/admin/master-decks/${deckId}`);
const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}`);
if (!res.ok) throw new Error("Failed to fetch master deck detail");
return await res.json();
}
@ -272,7 +273,7 @@ export const fetchMasterDeckDetail = createAsyncThunk(
export const updateMasterDeck = createAsyncThunk(
"admin/updateMasterDeck",
async ({ deckId, data }: { deckId: string; data: { name?: string; description?: string; is_active?: boolean } }, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/master-decks/${deckId}`, {
const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
@ -288,7 +289,7 @@ export const updateMasterDeckLayout = createAsyncThunk(
{ deckId, layoutIndex, data }: { deckId: string; layoutIndex: number; data: { layout_name?: string; layout_type?: string; react_code?: string } },
{ rejectWithValue }
) => {
const res = await fetch(`/api/v1/admin/master-decks/${deckId}/layouts/${layoutIndex}`, {
const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}/layouts/${layoutIndex}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
@ -302,7 +303,7 @@ export const reparseMasterDeck = createAsyncThunk(
"admin/reparseMasterDeck",
async ({ deckId, parseMode }: { deckId: string; parseMode?: string }, { rejectWithValue }) => {
const params = parseMode ? `?parse_mode=${parseMode}` : "";
const res = await fetch(`/api/v1/admin/master-decks/${deckId}/reparse${params}`, { method: "POST" });
const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}/reparse${params}`, { method: "POST" });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
return rejectWithValue(data.detail || "Failed to trigger reparse");
@ -314,7 +315,7 @@ export const reparseMasterDeck = createAsyncThunk(
export const deleteMasterDeck = createAsyncThunk(
"admin/deleteMasterDeck",
async (deckId: string, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/master-decks/${deckId}`, { method: "DELETE" });
const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}`, { method: "DELETE" });
if (!res.ok) return rejectWithValue("Failed to delete master deck");
return deckId;
}
@ -323,7 +324,7 @@ export const deleteMasterDeck = createAsyncThunk(
export const deleteMasterDeckLayout = createAsyncThunk(
"admin/deleteMasterDeckLayout",
async ({ deckId, layoutIndex }: { deckId: string; layoutIndex: number }, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/master-decks/${deckId}/layouts/${layoutIndex}`, {
const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}/layouts/${layoutIndex}`, {
method: "DELETE",
});
if (!res.ok) return rejectWithValue("Failed to delete layout");
@ -334,7 +335,7 @@ export const deleteMasterDeckLayout = createAsyncThunk(
export const bulkDeleteMasterDeckLayouts = createAsyncThunk(
"admin/bulkDeleteMasterDeckLayouts",
async ({ deckId, indices }: { deckId: string; indices: number[] }, { rejectWithValue }) => {
const res = await fetch(`/api/v1/admin/master-decks/${deckId}/layouts/bulk-delete`, {
const res = await apiFetch(`/api/v1/admin/master-decks/${deckId}/layouts/bulk-delete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ indices }),