generated from fahricansecer/boilerplate-fe
666 lines
19 KiB
TypeScript
666 lines
19 KiB
TypeScript
import { createApiClient } from './create-api-client';
|
|
|
|
const API_URL = typeof window === 'undefined'
|
|
? (process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api')
|
|
: (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api');
|
|
|
|
export const apiClient = createApiClient(API_URL);
|
|
|
|
// ── Type Definitions ─────────────────────────────────────────────────
|
|
|
|
export interface Project {
|
|
id: string;
|
|
title: string;
|
|
description?: string;
|
|
prompt: string;
|
|
status: ProjectStatus;
|
|
progress: number;
|
|
language: string;
|
|
aspectRatio: string;
|
|
videoStyle: string;
|
|
cinematicReference?: string;
|
|
targetDuration: number;
|
|
creditsUsed: number;
|
|
thumbnailUrl?: string;
|
|
finalVideoUrl?: string;
|
|
errorMessage?: string;
|
|
scriptJson?: ScriptJson;
|
|
scriptVersion: number;
|
|
scenes?: Scene[];
|
|
renderJobs?: RenderJob[];
|
|
sourceType?: 'MANUAL' | 'X_TWEET' | 'YOUTUBE';
|
|
sourceTweetData?: Record<string, unknown>;
|
|
// SEO Power Engine
|
|
seoTitle?: string;
|
|
seoDescription?: string;
|
|
seoKeywords?: string[];
|
|
seoTitleAlts?: string[];
|
|
seoScore?: number;
|
|
socialContent?: {
|
|
youtubeTitle?: string;
|
|
youtubeDescription?: string;
|
|
tiktokCaption?: string;
|
|
instagramCaption?: string;
|
|
twitterText?: string;
|
|
};
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
completedAt?: string;
|
|
}
|
|
|
|
export type ProjectStatus =
|
|
| 'DRAFT'
|
|
| 'GENERATING_SCRIPT'
|
|
| 'PENDING'
|
|
| 'GENERATING_MEDIA'
|
|
| 'RENDERING'
|
|
| 'COMPLETED'
|
|
| 'FAILED';
|
|
|
|
export interface Scene {
|
|
id: string;
|
|
order: number;
|
|
title?: string;
|
|
narrationText: string;
|
|
visualPrompt: string;
|
|
subtitleText?: string;
|
|
duration: number;
|
|
transitionType: string;
|
|
mediaAssets?: MediaAsset[];
|
|
}
|
|
|
|
export interface MediaAsset {
|
|
id: string;
|
|
type: string;
|
|
url?: string;
|
|
fileName?: string;
|
|
mimeType?: string;
|
|
sizeBytes?: number;
|
|
durationMs?: number;
|
|
aiProvider?: string;
|
|
}
|
|
|
|
export interface RenderJob {
|
|
id: string;
|
|
status: string;
|
|
currentStage?: string;
|
|
progress?: number;
|
|
attemptNumber: number;
|
|
processingTimeMs?: number;
|
|
errorMessage?: string;
|
|
finalVideoUrl?: string;
|
|
createdAt: string;
|
|
startedAt?: string;
|
|
completedAt?: string;
|
|
logs?: RenderLog[];
|
|
}
|
|
|
|
export interface RenderLog {
|
|
id: string;
|
|
stage: string;
|
|
level: string;
|
|
message: string;
|
|
durationMs?: number;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface ScriptJson {
|
|
metadata: {
|
|
title: string;
|
|
description: string;
|
|
totalDurationSeconds: number;
|
|
language: string;
|
|
hashtags: string[];
|
|
};
|
|
seo: {
|
|
title: string;
|
|
description: string;
|
|
keywords: string[];
|
|
hashtags: string[];
|
|
trendingHashtags?: string[];
|
|
estimatedSearchVolume?: string;
|
|
schemaMarkup: Record<string, unknown>;
|
|
};
|
|
seoTitleAlternatives?: string[];
|
|
seoScore?: number;
|
|
scenes: Array<{
|
|
order: number;
|
|
title?: string;
|
|
narrationText: string;
|
|
visualPrompt: string;
|
|
subtitleText: string;
|
|
durationSeconds: number;
|
|
transitionType: string;
|
|
}>;
|
|
musicPrompt: string;
|
|
voiceStyle: string;
|
|
socialContent?: {
|
|
youtubeTitle: string;
|
|
youtubeDescription: string;
|
|
tiktokCaption: string;
|
|
instagramCaption: string;
|
|
twitterText: string;
|
|
};
|
|
}
|
|
|
|
export interface PaginatedResponse<T> {
|
|
data: T[];
|
|
meta: {
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
totalPages: number;
|
|
};
|
|
}
|
|
|
|
export interface CreateProjectPayload {
|
|
title: string;
|
|
description?: string;
|
|
prompt: string;
|
|
language?: string;
|
|
aspectRatio?: string;
|
|
videoStyle?: string;
|
|
cinematicReference?: string;
|
|
targetDuration?: number;
|
|
seoKeywords?: string[];
|
|
referenceUrl?: string;
|
|
}
|
|
|
|
export interface UpdateProjectPayload {
|
|
title?: string;
|
|
description?: string;
|
|
prompt?: string;
|
|
language?: string;
|
|
aspectRatio?: string;
|
|
videoStyle?: string;
|
|
cinematicReference?: string;
|
|
targetDuration?: number;
|
|
seoKeywords?: string[];
|
|
}
|
|
|
|
export interface CreditBalance {
|
|
balance: number;
|
|
remaining: number;
|
|
total: number;
|
|
plan: string;
|
|
monthlyUsed: number;
|
|
monthlyLimit: number;
|
|
}
|
|
|
|
export interface Template {
|
|
id: string;
|
|
title: string;
|
|
description?: string;
|
|
thumbnailUrl?: string;
|
|
previewVideoUrl?: string;
|
|
category: string;
|
|
tags: string[];
|
|
language: string;
|
|
usageCount: number;
|
|
rating: number;
|
|
isFeatured: boolean;
|
|
}
|
|
|
|
export interface DashboardStats {
|
|
totalProjects: number;
|
|
completedVideos: number;
|
|
totalCreditsUsed: number;
|
|
creditsRemaining: number;
|
|
activeRenderJobs: number;
|
|
recentProjects: Project[];
|
|
}
|
|
|
|
// Tweet Types
|
|
export interface TweetAuthor {
|
|
id: string;
|
|
name: string;
|
|
username: string;
|
|
avatarUrl: string;
|
|
followersCount: number;
|
|
verified: boolean;
|
|
}
|
|
|
|
export interface TweetMetrics {
|
|
replies: number;
|
|
retweets: number;
|
|
likes: number;
|
|
views: number;
|
|
engagementRate: number;
|
|
}
|
|
|
|
export interface TweetMedia {
|
|
type: 'photo' | 'video' | 'gif';
|
|
url: string;
|
|
thumbnailUrl?: string;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface ParsedTweet {
|
|
id: string;
|
|
url: string;
|
|
text: string;
|
|
createdAt: string;
|
|
author: TweetAuthor;
|
|
metrics: TweetMetrics;
|
|
media: TweetMedia[];
|
|
quotedTweet?: ParsedTweet;
|
|
isThread: boolean;
|
|
threadTweets?: ParsedTweet[];
|
|
}
|
|
|
|
export interface TweetPreview {
|
|
tweet: ParsedTweet;
|
|
suggestedTitle: string;
|
|
suggestedPrompt: string;
|
|
viralScore: number;
|
|
contentType: 'tweet' | 'thread' | 'quote_tweet';
|
|
estimatedDuration: number;
|
|
}
|
|
|
|
export interface CreateFromTweetPayload {
|
|
tweetUrl: string;
|
|
title?: string;
|
|
language?: string;
|
|
aspectRatio?: string;
|
|
videoStyle?: string;
|
|
cinematicReference?: string;
|
|
targetDuration?: number;
|
|
}
|
|
|
|
export interface CreateFromYoutubePayload {
|
|
youtubeUrl: string;
|
|
title?: string;
|
|
language?: string;
|
|
aspectRatio?: string;
|
|
videoStyle?: string;
|
|
cinematicReference?: string;
|
|
targetDuration?: number;
|
|
}
|
|
|
|
export interface CreateFromDocumentPayload {
|
|
file: File;
|
|
title?: string;
|
|
language?: string;
|
|
aspectRatio?: string;
|
|
videoStyle?: string;
|
|
cinematicReference?: string;
|
|
targetDuration?: number;
|
|
}
|
|
|
|
export interface CreateFromTextPayload {
|
|
text: string;
|
|
title?: string;
|
|
language?: string;
|
|
aspectRatio?: string;
|
|
videoStyle?: string;
|
|
cinematicReference?: string;
|
|
targetDuration?: number;
|
|
}
|
|
|
|
export interface ExtractDocumentTopicsPayload {
|
|
file: File;
|
|
}
|
|
|
|
export interface ExtractDocumentTopicsResponse {
|
|
text: string;
|
|
topics: string[];
|
|
originalFilename: string;
|
|
}
|
|
|
|
export interface CreateFromExtractedTextPayload {
|
|
text: string;
|
|
topic: string;
|
|
originalFilename?: string;
|
|
language?: string;
|
|
aspectRatio?: string;
|
|
videoStyle?: string;
|
|
cinematicReference?: string;
|
|
targetDuration?: number;
|
|
}
|
|
|
|
export interface Notification {
|
|
id: string;
|
|
userId: string;
|
|
type: string;
|
|
title: string;
|
|
message?: string | null;
|
|
metadata?: Record<string, unknown> | null;
|
|
isRead: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface NotificationListResponse {
|
|
data: Notification[];
|
|
meta: {
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
totalPages: number;
|
|
unreadCount: number;
|
|
};
|
|
}
|
|
|
|
// ── API Functions ────────────────────────────────────────────────────
|
|
|
|
export const authApi = {
|
|
login: (data: any) =>
|
|
apiClient.post('/auth/login', data).then((r) => r.data),
|
|
register: (data: any) =>
|
|
apiClient.post('/auth/register', data).then((r) => r.data),
|
|
};
|
|
|
|
export const projectsApi = {
|
|
list: (params?: { page?: number; limit?: number; status?: string }) =>
|
|
apiClient.get<PaginatedResponse<Project>>('/projects', { params }).then((r) => r.data),
|
|
|
|
get: (id: string) =>
|
|
apiClient.get<Project>(`/projects/${id}`).then((r) => r.data),
|
|
|
|
create: (data: CreateProjectPayload) =>
|
|
apiClient.post<Project>('/projects', data).then((r) => r.data),
|
|
|
|
update: (id: string, data: Partial<CreateProjectPayload>) =>
|
|
apiClient.patch<Project>(`/projects/${id}`, data).then((r) => r.data),
|
|
|
|
delete: (id: string) =>
|
|
apiClient.delete(`/projects/${id}`).then((r) => r.data),
|
|
|
|
generateScript: (id: string) =>
|
|
apiClient.post<Project>(`/projects/${id}/generate-script`).then((r) => r.data),
|
|
|
|
approveAndQueue: (id: string, data?: { ttsProvider?: string; visualEffect?: string }) =>
|
|
apiClient.post<{ projectId: string; renderJobId: string; bullJobId: string }>(
|
|
`/projects/${id}/approve`,
|
|
data || {}
|
|
).then((r) => r.data),
|
|
|
|
cancelRender: (id: string) =>
|
|
apiClient.post<{ message: string; projectId: string; renderJobId: string; status: string }>(
|
|
`/projects/${id}/cancel-render`,
|
|
).then((r) => r.data),
|
|
|
|
generateSeoTitles: (id: string) =>
|
|
apiClient.post<{ titles: string[]; seoScore: number; currentTitle: string }>(
|
|
`/projects/${id}/generate-seo-titles`,
|
|
).then((r) => r.data),
|
|
|
|
generateSocialContent: (id: string) =>
|
|
apiClient.post<any>(`/projects/${id}/generate-social-content`).then((r) => r.data),
|
|
|
|
selectSeoTitle: (id: string, title: string) =>
|
|
apiClient.patch<Project>(
|
|
`/projects/${id}/select-title`,
|
|
{ title },
|
|
).then((r) => r.data),
|
|
|
|
getRenderQueue: () =>
|
|
apiClient.get<any>('/projects/render-queue').then((r) => r.data),
|
|
|
|
createFromTweet: (data: CreateFromTweetPayload) =>
|
|
apiClient.post<Project>('/projects/from-tweet', data).then((r) => r.data),
|
|
|
|
createFromYoutube: (data: CreateFromYoutubePayload) =>
|
|
apiClient.post<Project>('/projects/from-youtube', data).then((r) => r.data),
|
|
|
|
createFromDocument: (data: CreateFromDocumentPayload) => {
|
|
const formData = new FormData();
|
|
formData.append('file', data.file);
|
|
if (data.title) formData.append('title', data.title);
|
|
if (data.language) formData.append('language', data.language);
|
|
if (data.aspectRatio) formData.append('aspectRatio', data.aspectRatio);
|
|
if (data.videoStyle) formData.append('videoStyle', data.videoStyle);
|
|
if (data.targetDuration) formData.append('targetDuration', data.targetDuration.toString());
|
|
|
|
return apiClient.post<Project>('/projects/from-document', formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
}).then((r) => r.data);
|
|
},
|
|
|
|
createFromText: (data: CreateFromTextPayload) =>
|
|
apiClient.post<Project>('/projects/from-text', data).then((r) => r.data),
|
|
|
|
extractDocumentTopics: (data: ExtractDocumentTopicsPayload) => {
|
|
const formData = new FormData();
|
|
formData.append('file', data.file);
|
|
return apiClient.post<ExtractDocumentTopicsResponse>('/projects/extract-document-topics', formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
}).then((r) => r.data);
|
|
},
|
|
|
|
createFromExtractedText: (data: CreateFromExtractedTextPayload) =>
|
|
apiClient.post<Project>('/projects/document-from-topic', data).then((r) => r.data),
|
|
|
|
updateScene: (projectId: string, sceneId: string, data: Partial<Scene>) =>
|
|
apiClient.patch<Scene>(`/projects/${projectId}/scenes/${sceneId}`, data).then((r) => r.data),
|
|
|
|
regenerateScene: (projectId: string, sceneId: string) =>
|
|
apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/regenerate`).then((r) => r.data),
|
|
|
|
generateSceneImage: (projectId: string, sceneId: string, customPrompt?: string) =>
|
|
apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/generate-image`, { customPrompt }).then((r) => r.data),
|
|
|
|
upscaleSceneImage: (projectId: string, sceneId: string) =>
|
|
apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/upscale-image`).then((r) => r.data),
|
|
};
|
|
|
|
export const toolsApi = {
|
|
analyzeYoutubeVideo: (url: string) =>
|
|
apiClient.post<any>('/youtube-tools/analyze', { url }).then((r) => r.data),
|
|
getYoutubeAnalysisHistory: () =>
|
|
apiClient.get<any[]>('/youtube-tools/history').then((r) => r.data),
|
|
getYoutubeAnalysisById: (id: string) =>
|
|
apiClient.get<any>(`/youtube-tools/analyze/${id}`).then((r) => r.data),
|
|
|
|
// SEO
|
|
analyzeYoutubeSEO: (url: string) =>
|
|
apiClient.post<any>('/youtube-tools/seo/analyze', { url }).then((r) => r.data),
|
|
getYoutubeSeoHistory: () =>
|
|
apiClient.get<any[]>('/youtube-tools/seo/history').then((r) => r.data),
|
|
getYoutubeSeoAnalysisById: (id: string) =>
|
|
apiClient.get<any>(`/youtube-tools/seo/analyze/${id}`).then((r) => r.data),
|
|
generateYoutubeSeoImage: (prompt: string) =>
|
|
apiClient.post<{ url: string }>('/youtube-tools/seo/generate-image', { prompt }).then((r) => r.data),
|
|
};
|
|
|
|
// Backend path: /billing/credits/balance (billing controller prefix)
|
|
export const creditsApi = {
|
|
getBalance: () =>
|
|
apiClient.get<CreditBalance>('/billing/credits/balance').then((r) => r.data),
|
|
|
|
getHistory: (params?: { page?: number; limit?: number }) =>
|
|
apiClient.get('/billing/credits/history', { params }).then((r) => r.data),
|
|
};
|
|
|
|
export const billingApi = {
|
|
createCheckout: (planName: string, billingCycle: 'monthly' | 'yearly') =>
|
|
apiClient.post<{ sessionId: string; url: string }>('/billing/checkout', { planName, billingCycle }).then((r) => r.data),
|
|
|
|
getSubscription: () =>
|
|
apiClient.get('/billing/subscription').then((r) => r.data),
|
|
|
|
getPlans: () =>
|
|
apiClient.get('/billing/plans').then((r) => r.data),
|
|
};
|
|
|
|
export const usersApi = {
|
|
getMe: () =>
|
|
apiClient.get('/users/me').then((r) => r.data),
|
|
|
|
updateProfile: (data: { firstName?: string; lastName?: string }) =>
|
|
apiClient.patch('/users/me', data).then((r) => r.data),
|
|
|
|
changePassword: (data: { currentPassword: string; newPassword: string }) =>
|
|
apiClient.patch('/users/me/password', data).then((r) => r.data),
|
|
};
|
|
|
|
export const templatesApi = {
|
|
list: (params?: { category?: string; language?: string; page?: number; limit?: number }) =>
|
|
apiClient.get<PaginatedResponse<Template>>('/templates', { params }).then((r) => r.data),
|
|
|
|
get: (id: string) =>
|
|
apiClient.get<Template>(`/templates/${id}`).then((r) => r.data),
|
|
|
|
clone: (id: string) =>
|
|
apiClient.post<Project>(`/templates/${id}/clone`).then((r) => r.data),
|
|
};
|
|
|
|
export const dashboardApi = {
|
|
getStats: () =>
|
|
apiClient.get<DashboardStats>('/dashboard/stats').then((r) => r.data),
|
|
};
|
|
|
|
export const xTwitterApi = {
|
|
preview: (tweetUrl: string) =>
|
|
apiClient.post<TweetPreview>('/x-twitter/preview', { tweetUrl }).then((r) => r.data),
|
|
|
|
fetch: (tweetUrl: string) =>
|
|
apiClient.post<ParsedTweet>('/x-twitter/fetch', { tweetUrl }).then((r) => r.data),
|
|
};
|
|
|
|
export const notificationsApi = {
|
|
list: (params?: { page?: number; limit?: number; unreadOnly?: boolean }) =>
|
|
apiClient.get<NotificationListResponse>('/notifications', { params }).then((r) => r.data),
|
|
|
|
getUnreadCount: () =>
|
|
apiClient.get<{ count: number }>('/notifications/unread-count').then((r) => r.data),
|
|
|
|
markAsRead: (id: string) =>
|
|
apiClient.patch<Notification>(`/notifications/${id}/read`).then((r) => r.data),
|
|
|
|
markAllAsRead: () =>
|
|
apiClient.patch<{ count: number }>('/notifications/read-all').then((r) => r.data),
|
|
|
|
delete: (id: string) =>
|
|
apiClient.delete(`/notifications/${id}`).then((r) => r.data),
|
|
};
|
|
|
|
// ── Admin API Types ────────────────────────────────────────────────────────
|
|
|
|
export interface AdminUser {
|
|
id: string;
|
|
email: string;
|
|
firstName?: string;
|
|
lastName?: string;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
roles?: Array<{ role: { id: string; name: string } }>;
|
|
}
|
|
|
|
export interface AdminProject {
|
|
id: string;
|
|
title: string;
|
|
status: string;
|
|
progress: number;
|
|
creditsUsed: number;
|
|
language: string;
|
|
sourceType: string;
|
|
createdAt: string;
|
|
user: { id: string; email: string; firstName?: string; lastName?: string };
|
|
_count: { scenes: number; renderJobs: number };
|
|
}
|
|
|
|
export interface AdminRenderJob {
|
|
id: string;
|
|
status: string;
|
|
currentStage?: string;
|
|
attemptNumber: number;
|
|
processingTimeMs?: number;
|
|
errorMessage?: string;
|
|
workerHostname?: string;
|
|
createdAt: string;
|
|
startedAt?: string;
|
|
completedAt?: string;
|
|
project: {
|
|
id: string;
|
|
title: string;
|
|
user: { id: string; email: string; firstName?: string; lastName?: string };
|
|
};
|
|
}
|
|
|
|
export interface AdminPlan {
|
|
id: string;
|
|
name: string;
|
|
displayName: string;
|
|
description?: string;
|
|
monthlyPrice: number;
|
|
yearlyPrice?: number;
|
|
currency: string;
|
|
monthlyCredits: number;
|
|
maxDuration: number;
|
|
maxResolution: string;
|
|
maxProjects: number;
|
|
isActive: boolean;
|
|
sortOrder: number;
|
|
features?: Record<string, unknown>;
|
|
_count: { subscriptions: number };
|
|
}
|
|
|
|
export interface AdminStats {
|
|
users: { total: number; active: number; inactive: number };
|
|
projects: { total: number; byStatus: Record<string, number> };
|
|
renderJobs: { byStatus: Record<string, number>; active: number };
|
|
credits: { totalGranted: number; totalUsed: number };
|
|
plans: { total: number };
|
|
templates: { total: number };
|
|
storage?: unknown;
|
|
recentUsers: AdminUser[];
|
|
}
|
|
|
|
// ── Admin API Functions ─────────────────────────────────────────────────────
|
|
|
|
export const adminApi = {
|
|
getStats: () =>
|
|
apiClient.get<AdminStats>('/admin/stats').then((r) => r.data),
|
|
|
|
getUsers: (params?: { page?: number; limit?: number }) =>
|
|
apiClient.get<PaginatedResponse<AdminUser>>('/admin/users', { params }).then((r) => r.data),
|
|
|
|
getUserDetail: (id: string) =>
|
|
apiClient.get(`/admin/users/${id}/detail`).then((r) => r.data),
|
|
|
|
toggleUserActive: (id: string) =>
|
|
apiClient.put(`/admin/users/${id}/toggle-active`).then((r) => r.data),
|
|
|
|
banUser: (id: string) =>
|
|
apiClient.put(`/admin/users/${id}/ban`).then((r) => r.data),
|
|
|
|
activateUser: (id: string) =>
|
|
apiClient.put(`/admin/users/${id}/activate`).then((r) => r.data),
|
|
|
|
grantCredits: (userId: string, data: { amount: number; description: string }) =>
|
|
apiClient.post(`/admin/users/${userId}/credits`, data).then((r) => r.data),
|
|
|
|
assignRole: (userId: string, roleId: string) =>
|
|
apiClient.post(`/admin/users/${userId}/roles/${roleId}`).then((r) => r.data),
|
|
|
|
removeRole: (userId: string, roleId: string) =>
|
|
apiClient.delete(`/admin/users/${userId}/roles/${roleId}`).then((r) => r.data),
|
|
|
|
getProjects: (params?: { page?: number; limit?: number; status?: string; userId?: string }) =>
|
|
apiClient.get<PaginatedResponse<AdminProject>>('/admin/projects', { params }).then((r) => r.data),
|
|
|
|
deleteProject: (id: string) =>
|
|
apiClient.delete(`/admin/projects/${id}`).then((r) => r.data),
|
|
|
|
getRenderJobs: (params?: { page?: number; limit?: number; status?: string }) =>
|
|
apiClient.get<PaginatedResponse<AdminRenderJob>>('/admin/render-jobs', { params }).then((r) => r.data),
|
|
|
|
getPlans: () =>
|
|
apiClient.get<AdminPlan[]>('/admin/plans').then((r) => r.data),
|
|
|
|
updatePlan: (id: string, data: Partial<AdminPlan>) =>
|
|
apiClient.put(`/admin/plans/${id}`, data).then((r) => r.data),
|
|
|
|
getRoles: () =>
|
|
apiClient.get('/admin/roles').then((r) => r.data),
|
|
|
|
createRole: (data: { name: string; description?: string }) =>
|
|
apiClient.post('/admin/roles', data).then((r) => r.data),
|
|
};
|