llm providers | app config | admin routes
This commit is contained in:
parent
b639b83b87
commit
0e634546b1
@ -18,3 +18,20 @@ Going beyond simple contact extraction, PAC-BOT provides structured, multi-dimen
|
||||
|
||||
As part of its ongoing product roadmap, [Company Name] confirmed that PAC-BOT will soon evolve from a lead discovery engine into an automated global sales deployment. Upcoming features will enable the system to automatically generate hyper-tailored, localized outreach emails for any niche, in any language—allowing businesses to drop a pin anywhere in the world and instantly dispatch the perfect customized pitch.
|
||||
|
||||
|
||||
- [ ] gridsearch progress | pause | resume | settings : presets ( lang , overview, nearby, discover )
|
||||
- [ ] share => noFilters | columns | filters
|
||||
- [ ] types => partial / fuzzy match | post filters => import contacts
|
||||
- [ ] report => email
|
||||
- [ ] notifications => email
|
||||
- [ ] summary - business intelligence
|
||||
- [ ] expand => easy => country | lang match
|
||||
- [ ] llm filters : places | areas
|
||||
- [ ] ui : track / trail / done zones
|
||||
- [ ] sell search
|
||||
- [ ] Centers: LOD
|
||||
|
||||
### GADM
|
||||
|
||||
- [ ] Geo reverse / local names cache
|
||||
- [ ] Resolution
|
||||
5
packages/ui/shared/src/config/config.d.ts
vendored
5
packages/ui/shared/src/config/config.d.ts
vendored
@ -5,6 +5,7 @@ export interface AppConfig {
|
||||
core: Core;
|
||||
footer_left: FooterEmail[];
|
||||
footer_email: FooterEmail[];
|
||||
email: Email;
|
||||
footer_right: any[];
|
||||
settings: Settings;
|
||||
params: Params;
|
||||
@ -73,6 +74,10 @@ export interface Ecommerce {
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface Email {
|
||||
languages: string[];
|
||||
}
|
||||
|
||||
export interface FooterEmail {
|
||||
href: string;
|
||||
text: string;
|
||||
|
||||
@ -36,6 +36,10 @@ export const footerEmailSchema = z.object({
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
export const emailSchema = z.object({
|
||||
languages: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const settingsSchema = z.object({
|
||||
search: z.boolean(),
|
||||
account: z.boolean(),
|
||||
@ -216,6 +220,7 @@ export const appConfigSchema = z.object({
|
||||
core: coreSchema,
|
||||
footer_left: z.array(footerEmailSchema),
|
||||
footer_email: z.array(footerEmailSchema),
|
||||
email: emailSchema,
|
||||
footer_right: z.array(z.any()),
|
||||
settings: settingsSchema,
|
||||
params: paramsSchema,
|
||||
|
||||
@ -23,16 +23,10 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { serverUrl } from "@/lib/db";
|
||||
import { getAdminBanList, postAdminUnbanIp, postAdminUnbanUser, type AdminBanList } from "@/modules/admin/client-admin";
|
||||
|
||||
interface BanList {
|
||||
bannedIPs: string[];
|
||||
bannedUserIds: string[];
|
||||
bannedTokens: string[];
|
||||
}
|
||||
|
||||
export const BansManager = ({ session }: { session: any }) => {
|
||||
const [banList, setBanList] = useState<BanList>({
|
||||
export const BansManager = () => {
|
||||
const [banList, setBanList] = useState<AdminBanList>({
|
||||
bannedIPs: [],
|
||||
bannedUserIds: [],
|
||||
bannedTokens: [],
|
||||
@ -43,17 +37,7 @@ export const BansManager = ({ session }: { session: any }) => {
|
||||
const fetchBanList = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch(`${serverUrl}/api/admin/bans`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session?.access_token || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch ban list');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data = await getAdminBanList();
|
||||
setBanList(data);
|
||||
} catch (err: any) {
|
||||
toast.error(translate("Failed to fetch ban list"), {
|
||||
@ -68,28 +52,10 @@ export const BansManager = ({ session }: { session: any }) => {
|
||||
if (!unbanTarget) return;
|
||||
|
||||
try {
|
||||
const endpoint = unbanTarget.type === 'ip'
|
||||
? '/api/admin/bans/unban-ip'
|
||||
: '/api/admin/bans/unban-user';
|
||||
|
||||
const body = unbanTarget.type === 'ip'
|
||||
? { ip: unbanTarget.value }
|
||||
: { userId: unbanTarget.value };
|
||||
|
||||
const res = await fetch(`${serverUrl}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session?.access_token || ''}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to unban');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data =
|
||||
unbanTarget.type === 'ip'
|
||||
? await postAdminUnbanIp(unbanTarget.value)
|
||||
: await postAdminUnbanUser(unbanTarget.value);
|
||||
|
||||
if (data.success) {
|
||||
toast.success(translate("Unbanned successfully"), {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { T, translate } from '@/i18n';
|
||||
import { createAdminUserAPI } from '@/modules/user/client-user';
|
||||
import { createAdminUserAPI } from '@/modules/admin/client-admin';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
@ -2,7 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { T, translate } from '@/i18n';
|
||||
import { Tables } from '@/integrations/supabase/types';
|
||||
import { getUserApiKeys as getUserSecrets, updateUserSecrets, updateAdminUserAPI } from '@/modules/user/client-user';
|
||||
import { getUserApiKeys as getUserSecrets, updateUserSecrets } from '@/modules/user/client-user';
|
||||
import { updateAdminUserAPI } from '@/modules/admin/client-admin';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
@ -12,7 +12,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
||||
import { EditUserDialog } from './EditUserDialog';
|
||||
import { DeleteUserDialog } from './DeleteUserDialog';
|
||||
import { CreateUserDialog } from './CreateUserDialog';
|
||||
import { fetchAdminUsersAPI, deleteAdminUserAPI } from '@/modules/user/client-user';
|
||||
import { fetchAdminUsersAPI, deleteAdminUserAPI } from '@/modules/admin/client-admin';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { GroupManager } from './GroupManager';
|
||||
import { fetchUserGroups, fetchUserEffectiveGroups } from '@/modules/user/client-acl';
|
||||
|
||||
@ -5,7 +5,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { toast } from "sonner";
|
||||
import { AlertTriangle, RefreshCw } from "lucide-react";
|
||||
import { T, translate } from "@/i18n";
|
||||
import { serverUrl } from "@/lib/db";
|
||||
import { getAdminViolationStats, type AdminViolationStats } from "@/modules/admin/client-admin";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@ -15,20 +15,8 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
interface ViolationRecord {
|
||||
key: string;
|
||||
count: number;
|
||||
firstViolation: number;
|
||||
lastViolation: number;
|
||||
}
|
||||
|
||||
interface ViolationStats {
|
||||
totalViolations: number;
|
||||
violations: ViolationRecord[];
|
||||
}
|
||||
|
||||
export const ViolationsMonitor = ({ session }: { session: any }) => {
|
||||
const [stats, setStats] = useState<ViolationStats>({
|
||||
export const ViolationsMonitor = () => {
|
||||
const [stats, setStats] = useState<AdminViolationStats>({
|
||||
totalViolations: 0,
|
||||
violations: [],
|
||||
});
|
||||
@ -37,17 +25,7 @@ export const ViolationsMonitor = ({ session }: { session: any }) => {
|
||||
const fetchViolationStats = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch(`${serverUrl}/api/admin/bans/violations`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session?.access_token || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch violation stats');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data = await getAdminViolationStats();
|
||||
setStats(data);
|
||||
} catch (err: any) {
|
||||
toast.error(translate("Failed to fetch violation stats"), {
|
||||
|
||||
@ -5,8 +5,14 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { apiClient } from '@/lib/db';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import {
|
||||
fetchProviderConfigs,
|
||||
createProviderConfig,
|
||||
updateProviderConfig,
|
||||
deleteProviderConfig,
|
||||
type ProviderConfigRow,
|
||||
} from '@/modules/providers/client-providers';
|
||||
import { DEFAULT_PROVIDERS, fetchProviderModelInfo } from '@/llm/filters/providers';
|
||||
import { groupModelsByCompany } from '@/llm/filters/providers/openrouter';
|
||||
import { groupOpenAIModelsByType } from '@/llm/filters/providers/openai';
|
||||
@ -69,19 +75,7 @@ import {
|
||||
import { toast } from 'sonner';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
type ProviderConfig = {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
base_url?: string | null;
|
||||
models?: string[] | any;
|
||||
rate_limits?: Record<string, any> | null;
|
||||
is_active?: boolean | null;
|
||||
settings?: Record<string, any> | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
type ProviderConfig = ProviderConfigRow;
|
||||
|
||||
interface ProviderSettings {
|
||||
apiKey?: string;
|
||||
@ -120,7 +114,7 @@ export const ProviderManagement: React.FC = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [loadedProvidersRaw, userSecrets] = await Promise.all([
|
||||
apiClient<ProviderConfig[]>(`/api/provider-configs?userId=${user.id}`),
|
||||
fetchProviderConfigs(user.id),
|
||||
getUserSecrets(user.id)
|
||||
]);
|
||||
|
||||
@ -170,7 +164,7 @@ export const ProviderManagement: React.FC = () => {
|
||||
if (!deletingProvider) return;
|
||||
|
||||
try {
|
||||
await apiClient(`/api/provider-configs/${deletingProvider.id}`, { method: 'DELETE' });
|
||||
await deleteProviderConfig(user.id, deletingProvider.id);
|
||||
|
||||
toast.success(`Provider "${deletingProvider.display_name}" deleted successfully`);
|
||||
setDeletingProvider(null);
|
||||
@ -567,7 +561,7 @@ const ProviderEditDialog: React.FC<ProviderEditDialogProps> = ({
|
||||
}
|
||||
|
||||
// Update existing provider
|
||||
await apiClient(`/api/provider-configs/${provider.id}`, { method: 'PATCH', body: JSON.stringify(data) });
|
||||
await updateProviderConfig(user.id, provider.id, data);
|
||||
toast.success('Provider updated successfully');
|
||||
} else {
|
||||
// Create new provider with user_id
|
||||
@ -581,7 +575,7 @@ const ProviderEditDialog: React.FC<ProviderEditDialogProps> = ({
|
||||
console.error('Failed to update user secrets:', secretError);
|
||||
}
|
||||
}
|
||||
await apiClient('/api/provider-configs', { method: 'POST', body: JSON.stringify({ ...data, user_id: user.id }) });
|
||||
await createProviderConfig(user.id, { ...data, user_id: user.id });
|
||||
toast.success('Provider created successfully');
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ProviderConfig } from '@/llm/filters/types';
|
||||
import { apiClient } from '@/lib/db';
|
||||
import { fetchProviderConfigs } from '@/modules/providers/client-providers';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@ -69,7 +69,7 @@ export const ProviderSelector: React.FC<ProviderSelectorProps> = ({
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const userProviders = await apiClient<any[]>(`/api/provider-configs?userId=${user.id}&is_active=true`);
|
||||
const userProviders = await fetchProviderConfigs(user.id, { is_active: true });
|
||||
if (userProviders) {
|
||||
const providers = userProviders.map(dbProvider => ({
|
||||
name: dbProvider.name,
|
||||
|
||||
@ -7,7 +7,7 @@ import { AIPromptPopup } from './AIPromptPopup';
|
||||
import { generateText } from '@/lib/openai';
|
||||
import { toast } from 'sonner';
|
||||
import { getUserApiKeys as getUserSecrets } from '@/modules/user/client-user';
|
||||
import { apiClient } from '@/lib/db';
|
||||
import { fetchProviderConfigs } from '@/modules/providers/client-providers';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { formatTextGenPrompt } from '@/constants';
|
||||
|
||||
@ -33,7 +33,7 @@ const useProviderApiKey = () => {
|
||||
|
||||
try {
|
||||
console.log('Fetching API key for user:', user.id, 'provider:', provider);
|
||||
const configs = await apiClient<any[]>(`/api/provider-configs?userId=${user.id}&name=${provider}&is_active=true`);
|
||||
const configs = await fetchProviderConfigs(user.id, { name: provider, is_active: true });
|
||||
const userProvider = configs?.[0];
|
||||
if (!userProvider) return null;
|
||||
const settings = userProvider.settings as any;
|
||||
|
||||
@ -1,17 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { apiClient } from '@/lib/db';
|
||||
import { fetchProviderConfigs, type ProviderConfigRow } from '@/modules/providers/client-providers';
|
||||
|
||||
export type ProviderConfig = {
|
||||
name: string;
|
||||
display_name: string;
|
||||
base_url?: string;
|
||||
models?: string[];
|
||||
rate_limits?: Record<string, any>;
|
||||
is_active?: boolean;
|
||||
settings?: Record<string, any>;
|
||||
user_id?: string;
|
||||
};
|
||||
export type ProviderConfig = ProviderConfigRow;
|
||||
|
||||
const STORAGE_KEY = 'provider-settings';
|
||||
|
||||
@ -42,7 +33,7 @@ export const useProviderSettings = () => {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await apiClient<ProviderConfig[]>(`/api/provider-configs?userId=${user.id}&is_active=true`);
|
||||
const data = await fetchProviderConfigs(user.id, { is_active: true });
|
||||
|
||||
setProviders(data || []);
|
||||
|
||||
|
||||
@ -4,10 +4,11 @@ import { apiClient, serverUrl } from '@/lib/db.js';
|
||||
|
||||
interface SystemInfo {
|
||||
env: Record<string, string> & { appConfig?: AppConfig };
|
||||
/** Product names with `enabled: true` in server config/products.json */
|
||||
enabledProducts: string[];
|
||||
}
|
||||
|
||||
export const useSystemInfo = () => {
|
||||
console.log(`useSystemInfo : ${serverUrl}`);
|
||||
return useQuery<SystemInfo>({
|
||||
queryKey: ['system-info'],
|
||||
queryFn: () => apiClient<SystemInfo>('/api/system-info'),
|
||||
@ -27,3 +28,9 @@ export const useAppConfig = (): AppConfig | undefined => {
|
||||
const { data } = useSystemInfo();
|
||||
return data?.env as unknown as AppConfig;
|
||||
};
|
||||
|
||||
/** Shorthand: product names enabled in server config/products.json */
|
||||
export const useEnabledProducts = (): string[] | undefined => {
|
||||
const { data } = useSystemInfo();
|
||||
return data?.enabledProducts;
|
||||
};
|
||||
|
||||
@ -32,6 +32,7 @@ const KEY_PREFIXES: [RegExp, (...groups: string[]) => string[]][] = [
|
||||
[/^google-(.+)$/, (_, id) => ['users', 'google', id]],
|
||||
[/^user-secrets-(.+)$/, (_, id) => ['users', 'secrets', id]],
|
||||
[/^provider-(.+)-(.+)$/, (_, uid, prov) => ['users', 'provider', uid, prov]],
|
||||
[/^provider-configs-([0-9a-f-]{36})-(.+)$/, (_, uid, rest) => ['users', 'provider-configs', uid, rest]],
|
||||
[/^type-(.+)$/, (_, id) => ['types', id]],
|
||||
[/^types-(.+)$/, (_, rest) => ['types', rest]],
|
||||
[/^i18n-(.+)$/, (_, rest) => ['i18n', rest]],
|
||||
@ -69,6 +70,14 @@ export const fetchWithDeduplication = async <T>(
|
||||
|
||||
export const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
|
||||
|
||||
/** Join `serverUrl` with a path, or pass through absolute `http(s)://` URLs unchanged. */
|
||||
export function resolveApiUrl(pathOrUrl: string): string {
|
||||
if (pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')) return pathOrUrl;
|
||||
const base = String(serverUrl).replace(/\/$/, '');
|
||||
const path = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`;
|
||||
return `${base}${path}`;
|
||||
}
|
||||
|
||||
export const getAuthToken = async (): Promise<string | undefined> => {
|
||||
const user = await userManager.getUser();
|
||||
return user?.access_token ?? undefined;
|
||||
@ -164,8 +173,7 @@ async function fetchWithApiRetries(url: string, init: RequestInit, labelForLog:
|
||||
|
||||
export async function apiClient<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const headers = await getAuthHeaders();
|
||||
const isAbsolute = endpoint.startsWith('http://') || endpoint.startsWith('https://');
|
||||
const url = isAbsolute ? endpoint : `${serverUrl}${endpoint}`;
|
||||
const url = resolveApiUrl(endpoint);
|
||||
const response = await fetchWithApiRetries(url, {
|
||||
...options,
|
||||
headers: { ...headers, ...options.headers },
|
||||
@ -175,8 +183,7 @@ export async function apiClient<T>(endpoint: string, options: RequestInit = {}):
|
||||
|
||||
export async function apiClientOr404<T>(endpoint: string, options: RequestInit = {}): Promise<T | null> {
|
||||
const headers = await getAuthHeaders();
|
||||
const isAbsolute = endpoint.startsWith('http://') || endpoint.startsWith('https://');
|
||||
const url = isAbsolute ? endpoint : `${serverUrl}${endpoint}`;
|
||||
const url = resolveApiUrl(endpoint);
|
||||
const response = await fetchWithApiRetries(url, {
|
||||
...options,
|
||||
headers: { ...headers, ...options.headers },
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { ProviderConfig } from './types';
|
||||
import { generateText } from '@/lib/openai';
|
||||
import { apiClient } from '@/lib/db';
|
||||
import { fetchProviderConfigs } from '@/modules/providers/client-providers';
|
||||
import { fetchOpenRouterModelInfo } from './providers/openrouter';
|
||||
import { fetchOpenAIModelInfo } from './providers/openai';
|
||||
|
||||
@ -67,9 +67,13 @@ export const getProviderConfig = (providerName: string): ProviderConfig | null =
|
||||
/**
|
||||
* Get all available providers from API or defaults
|
||||
*/
|
||||
export const getAvailableProviders = async (): Promise<ProviderConfig[]> => {
|
||||
/** Pass `userId` (app UUID) to load saved providers; without it, defaults are returned. */
|
||||
export const getAvailableProviders = async (userId?: string): Promise<ProviderConfig[]> => {
|
||||
if (!userId) {
|
||||
return Object.values(DEFAULT_PROVIDERS).filter(provider => provider.isActive);
|
||||
}
|
||||
try {
|
||||
const data = await apiClient<any[]>('/api/provider-configs?is_active=true');
|
||||
const data = await fetchProviderConfigs(userId, { is_active: true });
|
||||
if (data && data.length > 0) {
|
||||
return data.map(dbProvider => convertDbToConfig(dbProvider));
|
||||
}
|
||||
|
||||
111
packages/ui/src/modules/admin/client-admin.ts
Normal file
111
packages/ui/src/modules/admin/client-admin.ts
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Authenticated admin API helpers (Bearer via `@/lib/db` `apiClient`).
|
||||
* Server routes are registered as admin-only (`AdminEndpointRegistry` + `adminMiddleware`).
|
||||
*/
|
||||
|
||||
import { apiClient } from '@/lib/db';
|
||||
|
||||
// --- Server / cache ---
|
||||
|
||||
export async function postFlushCache(body?: { mirror?: boolean }): Promise<{ success: boolean; message: string }> {
|
||||
// `apiClient` sets `Content-Type: application/json`; an empty body is invalid JSON and OpenAPI validation returns 400.
|
||||
return apiClient<{ success: boolean; message: string }>('/api/flush-cache', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function postAdminSystemRestart(): Promise<{ message: string; pid: number }> {
|
||||
return apiClient<{ message: string; pid: number }>('/api/admin/system/restart', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAdminSystem(): Promise<unknown> {
|
||||
return apiClient('/api/admin/system');
|
||||
}
|
||||
|
||||
// --- Bans ---
|
||||
|
||||
export type AdminBanList = {
|
||||
bannedIPs: string[];
|
||||
bannedUserIds: string[];
|
||||
bannedTokens: string[];
|
||||
};
|
||||
|
||||
export async function getAdminBanList(): Promise<AdminBanList> {
|
||||
return apiClient<AdminBanList>('/api/admin/bans');
|
||||
}
|
||||
|
||||
export async function postAdminBanIps(ips: string[]): Promise<{ banned: number; skipped: number; message: string }> {
|
||||
return apiClient<{ banned: number; skipped: number; message: string }>('/api/admin/bans/ban-ip', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ips }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function postAdminUnbanIp(ip: string): Promise<{ success: boolean; message: string }> {
|
||||
return apiClient<{ success: boolean; message: string }>('/api/admin/bans/unban-ip', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ip }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function postAdminUnbanUser(userId: string): Promise<{ success: boolean; message: string }> {
|
||||
return apiClient<{ success: boolean; message: string }>('/api/admin/bans/unban-user', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userId }),
|
||||
});
|
||||
}
|
||||
|
||||
export type AdminViolationStats = {
|
||||
totalViolations: number;
|
||||
violations: Array<{
|
||||
key: string;
|
||||
count: number;
|
||||
firstViolation: number;
|
||||
lastViolation: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export async function getAdminViolationStats(): Promise<AdminViolationStats> {
|
||||
return apiClient<AdminViolationStats>('/api/admin/bans/violations');
|
||||
}
|
||||
|
||||
// --- User management (admin) — `/api/admin/users` ---
|
||||
|
||||
export const fetchAdminUsersAPI = async (): Promise<unknown[]> => {
|
||||
return apiClient<unknown[]>('/api/admin/users');
|
||||
};
|
||||
|
||||
export const createAdminUserAPI = async (
|
||||
email: string,
|
||||
password: string,
|
||||
copySettings: boolean,
|
||||
): Promise<unknown> => {
|
||||
return apiClient('/api/admin/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password, copySettings }),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAdminUserAPI = async (
|
||||
userId: string,
|
||||
profile: {
|
||||
username?: string | null;
|
||||
display_name?: string | null;
|
||||
bio?: string | null;
|
||||
},
|
||||
): Promise<unknown> => {
|
||||
return apiClient(`/api/admin/users/${encodeURIComponent(userId)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(profile),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAdminUserAPI = async (userId: string): Promise<void> => {
|
||||
await apiClient(`/api/admin/users/${encodeURIComponent(userId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
@ -11,7 +11,7 @@ import { toast } from 'sonner';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { usePromptHistory } from '@/hooks/usePromptHistory';
|
||||
import { useDragDrop } from '@/contexts/DragDropContext';
|
||||
import { getProviderConfig } from '@/modules/user/client-user';
|
||||
import { getProviderConfig } from '@/modules/providers/client-providers';
|
||||
import { serverUrl } from '@/lib/db';
|
||||
import { createOpenAIClient } from '@/lib/openai';
|
||||
import { createSearchToolPreset, createWebSearchToolPreset } from '@/modules/ai/searchTools';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
import { serverUrl } from '@/lib/db';
|
||||
import { apiClient } from '@/lib/db';
|
||||
|
||||
export interface Campaign {
|
||||
id: string;
|
||||
@ -23,30 +23,6 @@ export interface Campaign {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// ─── Auth helper ──────────────────────────────────────────────────────────────
|
||||
|
||||
async function authHeaders(contentType?: string): Promise<HeadersInit> {
|
||||
const { getAuthToken } = await import('@/lib/db');
|
||||
const token = await getAuthToken();
|
||||
const h: HeadersInit = {};
|
||||
if (token) h['Authorization'] = `Bearer ${token}`;
|
||||
if (contentType) h['Content-Type'] = contentType;
|
||||
return h;
|
||||
}
|
||||
|
||||
const SERVER_URL = serverUrl;
|
||||
|
||||
async function apiFetch(path: string, init?: RequestInit) {
|
||||
const url = SERVER_URL ? `${SERVER_URL}${path}` : path;
|
||||
const res = await fetch(url, init);
|
||||
if (!res.ok) {
|
||||
const err = await res.text().catch(() => res.statusText);
|
||||
throw new Error(`${path} — ${res.status}: ${err}`);
|
||||
}
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
return ct.includes('json') ? res.json() : res.text();
|
||||
}
|
||||
|
||||
// ─── Campaigns CRUD ───────────────────────────────────────────────────────────
|
||||
|
||||
export const fetchCampaigns = async (options?: {
|
||||
@ -60,41 +36,40 @@ export const fetchCampaigns = async (options?: {
|
||||
if (options?.q) params.set('q', options.q);
|
||||
if (options?.limit != null) params.set('limit', String(options.limit));
|
||||
if (options?.offset != null) params.set('offset', String(options.offset));
|
||||
const headers = await authHeaders();
|
||||
return apiFetch(`/api/campaigns?${params}`, { headers });
|
||||
return apiClient<Campaign[]>(`/api/campaigns?${params}`);
|
||||
};
|
||||
|
||||
export const getCampaign = async (id: string): Promise<Campaign> => {
|
||||
return apiFetch(`/api/campaigns/${id}`, { headers: await authHeaders() });
|
||||
return apiClient<Campaign>(`/api/campaigns/${id}`);
|
||||
};
|
||||
|
||||
export const createCampaign = async (data: Partial<Campaign>): Promise<Campaign> => {
|
||||
return apiFetch('/api/campaigns', {
|
||||
return apiClient<Campaign>('/api/campaigns', {
|
||||
method: 'POST',
|
||||
headers: await authHeaders('application/json'),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateCampaign = async (id: string, data: Partial<Campaign>): Promise<Campaign> => {
|
||||
return apiFetch(`/api/campaigns/${id}`, {
|
||||
return apiClient<Campaign>(`/api/campaigns/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: await authHeaders('application/json'),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCampaign = async (id: string): Promise<void> => {
|
||||
await apiFetch(`/api/campaigns/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: await authHeaders(),
|
||||
});
|
||||
await apiClient<unknown>(`/api/campaigns/${id}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
export const sendCampaign = async (id: string, intervalSecs: number = 1): Promise<{ total: number; sent: number; failed: number; skipped: number }> => {
|
||||
return apiFetch(`/api/campaigns/${id}/send`, {
|
||||
method: 'POST',
|
||||
headers: await authHeaders('application/json'),
|
||||
body: JSON.stringify({ interval: intervalSecs }),
|
||||
});
|
||||
export const sendCampaign = async (
|
||||
id: string,
|
||||
intervalSecs: number = 1
|
||||
): Promise<{ total: number; sent: number; failed: number; skipped: number }> => {
|
||||
return apiClient<{ total: number; sent: number; failed: number; skipped: number }>(
|
||||
`/api/campaigns/${id}/send`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ interval: intervalSecs }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,18 +3,13 @@
|
||||
* Calls server routes at /api/contacts/mailboxes
|
||||
*/
|
||||
|
||||
import { serverUrl } from '@/lib/db';
|
||||
import { apiClient } from '@/lib/db';
|
||||
|
||||
const SERVER_URL = serverUrl;
|
||||
const API_BASE = `${serverUrl}/api/contacts/mailboxes`;
|
||||
const API_BASE = '/api/contacts/mailboxes';
|
||||
|
||||
async function authHeaders(contentType?: string): Promise<HeadersInit> {
|
||||
const { getAuthToken } = await import('@/lib/db');
|
||||
const token = await getAuthToken();
|
||||
const h: HeadersInit = {};
|
||||
if (token) h['Authorization'] = `Bearer ${token}`;
|
||||
if (contentType) h['Content-Type'] = contentType;
|
||||
return h;
|
||||
/** Same as apiClient but never sends cookies (cross-origin API host). */
|
||||
function mailboxApi<T>(endpoint: string, init?: RequestInit): Promise<T> {
|
||||
return apiClient<T>(endpoint, { ...init, credentials: 'omit' });
|
||||
}
|
||||
|
||||
export interface MailboxItem {
|
||||
@ -45,36 +40,23 @@ export interface MailboxInput {
|
||||
provider?: 'generic' | 'mailersend' | 'mailersend_api';
|
||||
}
|
||||
|
||||
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const url = SERVER_URL ? `${SERVER_URL}${path}` : path;
|
||||
const ct = init?.body && typeof init.body === 'string' ? 'application/json' : undefined;
|
||||
const headers = { ...(await authHeaders(ct)), ...(init?.headers || {}) };
|
||||
|
||||
const res = await fetch(url, { ...init, headers, credentials: 'omit' });
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function listMailboxes(): Promise<MailboxItem[]> {
|
||||
return apiFetch<MailboxItem[]>(API_BASE);
|
||||
return mailboxApi<MailboxItem[]>(API_BASE);
|
||||
}
|
||||
|
||||
export async function saveMailbox(input: MailboxInput): Promise<MailboxItem> {
|
||||
return apiFetch<MailboxItem>(API_BASE, {
|
||||
return mailboxApi<MailboxItem>(API_BASE, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteMailbox(id: string): Promise<void> {
|
||||
await apiFetch<{ ok: boolean }>(`${API_BASE}/${id}`, { method: 'DELETE' });
|
||||
await mailboxApi<{ ok: boolean }>(`${API_BASE}/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function testMailbox(id: string): Promise<{ ok: boolean; error?: string }> {
|
||||
return apiFetch<{ ok: boolean; error?: string }>(`${API_BASE}/${id}/test`, {
|
||||
return mailboxApi<{ ok: boolean; error?: string }>(`${API_BASE}/${id}/test`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
@ -83,32 +65,34 @@ export async function testSendMailbox(
|
||||
id: string,
|
||||
input: { to: string; subject?: string; message?: string }
|
||||
): Promise<{ ok: boolean; messageId?: string; error?: string }> {
|
||||
return apiFetch<{ ok: boolean; messageId?: string; error?: string }>(`${API_BASE}/${id}/test-send`, {
|
||||
return mailboxApi<{ ok: boolean; messageId?: string; error?: string }>(`${API_BASE}/${id}/test-send`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getGmailAuthUrl(): Promise<{ url: string }> {
|
||||
return apiFetch<{ url: string }>(`${API_BASE}/gmail/authorize`);
|
||||
return mailboxApi<{ url: string }>(`${API_BASE}/gmail/authorize`);
|
||||
}
|
||||
|
||||
export async function syncMailbox(id: string, options: { groupId?: string; deepScan?: boolean }): Promise<{ status: string }> {
|
||||
return apiFetch<{ status: string }>(`${API_BASE}/${id}/sync`, {
|
||||
export async function syncMailbox(
|
||||
id: string,
|
||||
options: { groupId?: string; deepScan?: boolean }
|
||||
): Promise<{ status: string }> {
|
||||
return mailboxApi<{ status: string }>(`${API_BASE}/${id}/sync`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
}
|
||||
|
||||
export async function stopSyncMailbox(id: string): Promise<{ ok: boolean }> {
|
||||
return apiFetch<{ ok: boolean }>(`${API_BASE}/${id}/sync-stop`, {
|
||||
return mailboxApi<{ ok: boolean }>(`${API_BASE}/${id}/sync-stop`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function resetSyncMailbox(id: string): Promise<{ ok: boolean }> {
|
||||
return apiFetch<{ ok: boolean }>(`${API_BASE}/${id}/sync-reset`, {
|
||||
return mailboxApi<{ ok: boolean }>(`${API_BASE}/${id}/sync-reset`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,31 +1,9 @@
|
||||
/**
|
||||
* Client-side ecommerce module — transaction CRUD via authenticated API calls.
|
||||
* Mirrors the pattern from client-user.ts: uses getAuthToken() + fetch.
|
||||
* Uses apiClient (@/lib/db) for auth, URL resolution, and retries.
|
||||
*/
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth helper (reuse from client-user pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getAuthToken(): Promise<string | null> {
|
||||
const { getAuthToken: getZitadelToken } = await import('@/lib/db');
|
||||
return (await getZitadelToken()) ?? null;
|
||||
}
|
||||
|
||||
async function authFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
||||
const token = await getAuthToken();
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
|
||||
const headers = new Headers(options.headers);
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
if (!headers.has('Content-Type') && options.body) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
return fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
}
|
||||
import { apiClient } from '@/lib/db';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transaction Types
|
||||
@ -82,34 +60,26 @@ export interface UpdateTransactionInput {
|
||||
|
||||
/** List all transactions for the current user (admins see all) */
|
||||
export async function listTransactions(): Promise<Transaction[]> {
|
||||
const res = await authFetch('/api/transactions');
|
||||
if (!res.ok) throw new Error(`Failed to list transactions: ${res.status}`);
|
||||
return res.json();
|
||||
return apiClient<Transaction[]>('/api/transactions');
|
||||
}
|
||||
|
||||
/** Get a single transaction by ID */
|
||||
export async function getTransaction(id: string): Promise<Transaction> {
|
||||
const res = await authFetch(`/api/transactions/${id}`);
|
||||
if (!res.ok) throw new Error(`Failed to get transaction: ${res.status}`);
|
||||
return res.json();
|
||||
return apiClient<Transaction>(`/api/transactions/${id}`);
|
||||
}
|
||||
|
||||
/** Create a new transaction */
|
||||
export async function createTransaction(input: CreateTransactionInput): Promise<Transaction> {
|
||||
const res = await authFetch('/api/transactions', {
|
||||
return apiClient<Transaction>('/api/transactions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to create transaction: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** Update a transaction (status, metadata, etc.) */
|
||||
export async function updateTransaction(id: string, input: UpdateTransactionInput): Promise<Transaction> {
|
||||
const res = await authFetch(`/api/transactions/${id}`, {
|
||||
return apiClient<Transaction>(`/api/transactions/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to update transaction: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
92
packages/ui/src/modules/providers/client-providers.ts
Normal file
92
packages/ui/src/modules/providers/client-providers.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Client API for `public.provider_configs` (auth-scoped on the server).
|
||||
* Mirrors the pattern in `@/modules/user/client-user` + `@/lib/db`.
|
||||
*/
|
||||
|
||||
import { fetchWithDeduplication, apiClient, getAuthToken as getZitadelToken, serverUrl as serverBaseUrl } from '@/lib/db';
|
||||
|
||||
const serverUrl = (path: string) => {
|
||||
const baseUrl = serverBaseUrl || window.location.origin;
|
||||
return `${baseUrl}${path}`;
|
||||
};
|
||||
|
||||
const getAuthToken = async (): Promise<string> => {
|
||||
const token = await getZitadelToken();
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
return token;
|
||||
};
|
||||
|
||||
/** Row shape returned by GET/PATCH/POST `/api/provider-configs` */
|
||||
export type ProviderConfigRow = {
|
||||
id: string;
|
||||
user_id?: string | null;
|
||||
name: string;
|
||||
display_name: string;
|
||||
base_url?: string | null;
|
||||
models?: unknown;
|
||||
rate_limits?: Record<string, unknown> | null;
|
||||
is_active?: boolean | null;
|
||||
settings?: Record<string, unknown> | null;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
|
||||
const listCacheKey = (userId: string, opts?: { is_active?: boolean; name?: string }) =>
|
||||
`provider-configs-${userId}-${JSON.stringify({ a: opts?.is_active ?? null, n: opts?.name ?? null })}`;
|
||||
|
||||
/**
|
||||
* List provider_configs for the authenticated user. The server resolves the user from the Bearer token;
|
||||
* `userId` is only used for React Query / deduplication keys.
|
||||
*/
|
||||
export const fetchProviderConfigs = async (
|
||||
userId: string,
|
||||
opts?: { is_active?: boolean; name?: string },
|
||||
): Promise<ProviderConfigRow[]> => {
|
||||
return fetchWithDeduplication(listCacheKey(userId, opts), async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (opts?.is_active !== undefined) params.set('is_active', String(opts.is_active));
|
||||
if (opts?.name) params.set('name', opts.name);
|
||||
const qs = params.toString();
|
||||
return apiClient<ProviderConfigRow[]>(`/api/provider-configs${qs ? `?${qs}` : ''}`);
|
||||
});
|
||||
};
|
||||
|
||||
export const createProviderConfig = async (
|
||||
_userId: string,
|
||||
body: Omit<ProviderConfigRow, 'id' | 'created_at' | 'updated_at'> & { user_id?: string },
|
||||
): Promise<ProviderConfigRow> => {
|
||||
return apiClient<ProviderConfigRow>('/api/provider-configs', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateProviderConfig = async (
|
||||
_userId: string,
|
||||
id: string,
|
||||
body: Partial<Omit<ProviderConfigRow, 'id' | 'created_at' | 'updated_at'>>,
|
||||
): Promise<ProviderConfigRow> => {
|
||||
return apiClient<ProviderConfigRow>(`/api/provider-configs/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteProviderConfig = async (_userId: string, id: string): Promise<void> => {
|
||||
await apiClient(`/api/provider-configs/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/me/provider-config/:provider — returns `{ settings }` for that named provider (or 404).
|
||||
*/
|
||||
export const getProviderConfig = async (userId: string, provider: string) => {
|
||||
return fetchWithDeduplication(`provider-${userId}-${provider}`, async () => {
|
||||
const token = await getAuthToken();
|
||||
const res = await fetch(serverUrl(`/api/me/provider-config/${encodeURIComponent(provider)}`), {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) throw new Error(`Failed to fetch provider config: ${res.statusText}`);
|
||||
return await res.json();
|
||||
});
|
||||
};
|
||||
@ -99,20 +99,6 @@ export const getUserSecrets = async (userId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getProviderConfig = async (userId: string, provider: string) => {
|
||||
return fetchWithDeduplication(`provider-${userId}-${provider}`, async () => {
|
||||
const token = await getAuthToken();
|
||||
const res = await fetch(serverUrl(`/api/me/provider-config/${encodeURIComponent(provider)}`), {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) throw new Error(`Failed to fetch provider config: ${res.statusText}`);
|
||||
return await res.json();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const fetchUserRoles = async (userId: string) => {
|
||||
return fetchWithDeduplication(`roles-${userId}`, async () => {
|
||||
const token = await getAuthToken();
|
||||
@ -347,10 +333,6 @@ export const updateUserEmail = async (newEmail: string): Promise<void> => {
|
||||
});
|
||||
};
|
||||
|
||||
// =============================================
|
||||
// Admin User Management API Wrappers
|
||||
// =============================================
|
||||
|
||||
const getAuthToken = async (): Promise<string> => {
|
||||
const token = await getZitadelToken();
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
@ -378,62 +360,12 @@ export const fetchProfilesByUserIds = async (
|
||||
return map;
|
||||
};
|
||||
|
||||
/** Fetch all users (admin) */
|
||||
export const fetchAdminUsersAPI = async (): Promise<any[]> => {
|
||||
const token = await getAuthToken();
|
||||
const res = await fetch(serverUrl('/api/admin/users'), {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to fetch users: ${res.statusText}`);
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
/** Create a new user (admin) */
|
||||
export const createAdminUserAPI = async (email: string, password: string, copySettings: boolean): Promise<any> => {
|
||||
const token = await getAuthToken();
|
||||
const res = await fetch(serverUrl('/api/admin/users'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ email, password, copySettings })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || `Failed to create user: ${res.statusText}`);
|
||||
}
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
/** Update a user's profile (admin) */
|
||||
export const updateAdminUserAPI = async (userId: string, profile: {
|
||||
username?: string | null;
|
||||
display_name?: string | null;
|
||||
bio?: string | null;
|
||||
}): Promise<any> => {
|
||||
const token = await getAuthToken();
|
||||
const res = await fetch(serverUrl(`/api/admin/users/${userId}`), {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(profile)
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to update user: ${res.statusText}`);
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
/** Delete a user (admin) */
|
||||
export const deleteAdminUserAPI = async (userId: string): Promise<void> => {
|
||||
const token = await getAuthToken();
|
||||
const res = await fetch(serverUrl(`/api/admin/users/${userId}`), {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to delete user: ${res.statusText}`);
|
||||
};
|
||||
export {
|
||||
fetchAdminUsersAPI,
|
||||
createAdminUserAPI,
|
||||
updateAdminUserAPI,
|
||||
deleteAdminUserAPI,
|
||||
} from '@/modules/admin/client-admin';
|
||||
|
||||
/** Notify the server to flush its auth cache for the current user (best-effort, never throws) */
|
||||
export const notifyServerLogout = (token: string): void => {
|
||||
|
||||
@ -15,13 +15,13 @@ import { BansManager } from "@/components/admin/BansManager";
|
||||
import { ViolationsMonitor } from "@/components/admin/ViolationsMonitor";
|
||||
import React, { Suspense } from "react";
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import { serverUrl } from "@/lib/db";
|
||||
import { postFlushCache, postAdminSystemRestart } from "@/modules/admin/client-admin";
|
||||
|
||||
// Lazy load AnalyticsDashboard
|
||||
const AnalyticsDashboard = React.lazy(() => import("@/modules/analytics").then(module => ({ default: module.AnalyticsDashboard })));
|
||||
|
||||
const AdminPage = () => {
|
||||
const { user, session, loading, roles } = useAuth();
|
||||
const { user, loading, roles } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return <div className="min-h-screen bg-background flex items-center justify-center">
|
||||
@ -47,9 +47,9 @@ const AdminPage = () => {
|
||||
<Route path="/" element={<Navigate to="users" replace />} />
|
||||
<Route path="users" element={<UserManagerSection />} />
|
||||
<Route path="dashboard" element={<DashboardSection />} />
|
||||
<Route path="server" element={<ServerSection session={session} />} />
|
||||
<Route path="bans" element={<BansSection session={session} />} />
|
||||
<Route path="violations" element={<ViolationsSection session={session} />} />
|
||||
<Route path="server" element={<ServerSection />} />
|
||||
<Route path="bans" element={<BansSection />} />
|
||||
<Route path="violations" element={<ViolationsSection />} />
|
||||
<Route path="analytics" element={
|
||||
<Suspense fallback={<div><T>Loading analytics...</T></div>}>
|
||||
<AnalyticsDashboard />
|
||||
@ -95,31 +95,21 @@ const DashboardSection = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const BansSection = ({ session }: { session: any }) => (
|
||||
<BansManager session={session} />
|
||||
const BansSection = () => (
|
||||
<BansManager />
|
||||
);
|
||||
|
||||
const ViolationsSection = ({ session }: { session: any }) => (
|
||||
<ViolationsMonitor session={session} />
|
||||
const ViolationsSection = () => (
|
||||
<ViolationsMonitor />
|
||||
);
|
||||
|
||||
const ServerSection = ({ session }: { session: any }) => {
|
||||
const ServerSection = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFlushCache = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch(`${serverUrl}/api/flush-cache`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session?.access_token || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Failed to flush cache');
|
||||
}
|
||||
await postFlushCache();
|
||||
|
||||
toast.success(translate("Cache flushed successfully"), {
|
||||
description: translate("Access and Content caches have been cleared.")
|
||||
@ -139,19 +129,7 @@ const ServerSection = ({ session }: { session: any }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${serverUrl}/api/admin/system/restart`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session?.access_token || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Failed to restart server');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data = await postAdminSystemRestart();
|
||||
toast.success(translate("Server restarting"), {
|
||||
description: data.message
|
||||
});
|
||||
@ -192,7 +170,7 @@ const ServerSection = ({ session }: { session: any }) => {
|
||||
|
||||
<div className="bg-card border rounded-lg p-6 max-w-2xl mt-6">
|
||||
<h2 className="text-lg font-semibold mb-4"><T>System Resources</T></h2>
|
||||
<SystemStats session={session} />
|
||||
<SystemStats />
|
||||
</div>
|
||||
|
||||
<div className="bg-card border rounded-lg p-6 max-w-2xl mt-6">
|
||||
@ -247,7 +225,7 @@ interface WorkerPing {
|
||||
|
||||
import { useWebSocket } from '@/contexts/WS_Socket';
|
||||
|
||||
const SystemStats = ({ session }: { session: any }) => {
|
||||
const SystemStats = () => {
|
||||
const [stats, setStats] = useState<SystemData | null>(null);
|
||||
const { addListener, removeListener, sendCommand } = useWebSocket();
|
||||
|
||||
@ -377,7 +355,7 @@ const SystemStats = ({ session }: { session: any }) => {
|
||||
</div>
|
||||
|
||||
{/* Worker Threads */}
|
||||
<WorkerThreadStats session={session} />
|
||||
<WorkerThreadStats />
|
||||
|
||||
{/* Task Runner Stats */}
|
||||
{stats.tasks && stats.tasks.length > 0 && (
|
||||
@ -422,7 +400,7 @@ const SystemStats = ({ session }: { session: any }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const WorkerThreadStats = ({ session }: { session: any }) => {
|
||||
const WorkerThreadStats = () => {
|
||||
const [pings, setPings] = useState<WorkerPing[]>([]);
|
||||
const { addListener, removeListener } = useWebSocket();
|
||||
|
||||
|
||||
@ -14,7 +14,8 @@ import { createMarkdownToolPreset } from '@/lib/markdownImageTools';
|
||||
import { toast } from 'sonner';
|
||||
import OpenAI from 'openai';
|
||||
import SimpleLogViewer from '@/components/SimpleLogViewer';
|
||||
import { getUserSettings, updateUserSettings, getProviderConfig } from '@/modules/user/client-user';
|
||||
import { getUserSettings, updateUserSettings } from '@/modules/user/client-user';
|
||||
import { getProviderConfig } from '@/modules/providers/client-providers';
|
||||
import { usePromptHistory } from '@/hooks/usePromptHistory';
|
||||
|
||||
const PlaygroundEditorLLM: React.FC = () => {
|
||||
|
||||
@ -1,126 +1,63 @@
|
||||
/// <reference lib="webworker" />
|
||||
import { NetworkFirst, CacheFirst } from 'workbox-strategies';
|
||||
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
||||
import { registerRoute, NavigationRoute } from 'workbox-routing'
|
||||
import { ExpirationPlugin } from 'workbox-expiration'
|
||||
import { set } from 'idb-keyval'
|
||||
/**
|
||||
* Minimal service worker: Web Share Target only (see manifest.webmanifest share_target).
|
||||
* No app shell / asset / navigation caching — the network handles everything else.
|
||||
*/
|
||||
import { precacheAndRoute } from 'workbox-precaching';
|
||||
import { set } from 'idb-keyval';
|
||||
|
||||
const SW_VERSION = '2.0.0';
|
||||
declare let self: ServiceWorkerGlobalScope;
|
||||
|
||||
console.log(`[SW] Initializing Version: ${SW_VERSION}`);
|
||||
// Required by vite-plugin-pwa injectManifest; globPatterns in vite.config.ts are empty.
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
declare let self: ServiceWorkerGlobalScope
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting()
|
||||
}
|
||||
})
|
||||
|
||||
// Only precache the app shell — not all JS chunks
|
||||
const manifest = self.__WB_MANIFEST;
|
||||
const shellOnly = manifest.filter((entry) => {
|
||||
const url = typeof entry === 'string' ? entry : entry.url;
|
||||
return url === 'index.html'
|
||||
|| url === 'favicon.ico'
|
||||
|| url.endsWith('.png')
|
||||
|| url.endsWith('.svg')
|
||||
|| url === 'manifest.webmanifest';
|
||||
self.addEventListener('install', () => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
precacheAndRoute(shellOnly);
|
||||
|
||||
// clean old assets
|
||||
cleanupOutdatedCaches()
|
||||
|
||||
// Runtime cache: hashed static assets (JS/CSS) — cache-first since Vite hashes them
|
||||
registerRoute(
|
||||
({ url }) => url.pathname.startsWith('/assets/'),
|
||||
new CacheFirst({
|
||||
cacheName: 'assets-v1',
|
||||
plugins: [
|
||||
new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// allow only fallback in dev: we don't want to cache everything
|
||||
let allowlist: undefined | RegExp[]
|
||||
if (location.hostname === 'localhost' || location.hostname.includes('127.0.0.1'))
|
||||
allowlist = [/^\/$/]
|
||||
|
||||
// Handle Share Target POST requests
|
||||
// MUST be registered before NavigationRoute to take precedence
|
||||
registerRoute(
|
||||
({ request, url }) => request.method === 'POST' && url.pathname === '/upload-share-target',
|
||||
async ({ event, request }) => {
|
||||
console.log('SW: Intercepting Share Target request!');
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const files = formData.getAll('file')
|
||||
const title = formData.get('title')
|
||||
const text = formData.get('text')
|
||||
const url = formData.get('url')
|
||||
|
||||
console.log('SW: Share data received:', { filesLen: files.length, title, text, url });
|
||||
|
||||
// Store in IDB
|
||||
await set('share-target', { files, title, text, url, timestamp: Date.now() })
|
||||
console.log('SW: Data stored in IDB');
|
||||
|
||||
// Redirect to the app with success status
|
||||
return Response.redirect('/new?shared=true&sw_status=success', 303)
|
||||
} catch (err) {
|
||||
console.error('SW: Share Target Error:', err);
|
||||
// Safe error string
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
return Response.redirect('/new?shared=true&sw_status=error&sw_error=' + encodeURIComponent(errMsg), 303);
|
||||
}
|
||||
},
|
||||
'POST'
|
||||
);
|
||||
|
||||
// Navigation handler: Prefer network to get server injection, fallback to cached index.html
|
||||
const navigationHandler = async (params: any) => {
|
||||
try {
|
||||
const strategy = new NetworkFirst({
|
||||
cacheName: 'pages',
|
||||
networkTimeoutSeconds: 3,
|
||||
plugins: [
|
||||
{
|
||||
cacheWillUpdate: async ({ response }) => {
|
||||
return response && response.status === 200 ? response : null;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
return await strategy.handle(params);
|
||||
} catch (error) {
|
||||
// Fallback: serve cached index.html from precache
|
||||
const cache = await caches.match('index.html');
|
||||
if (cache) return cache;
|
||||
return new Response('Offline', { status: 503 });
|
||||
}
|
||||
};
|
||||
|
||||
// to allow work offline
|
||||
registerRoute(new NavigationRoute(
|
||||
navigationHandler,
|
||||
{
|
||||
allowlist,
|
||||
denylist: [/^\/upload-share-target/]
|
||||
}
|
||||
))
|
||||
|
||||
// On activate, clean up old caches but keep current ones
|
||||
self.addEventListener('activate', (event) => {
|
||||
const currentCaches = new Set(['assets-v1', 'pages']);
|
||||
event.waitUntil(
|
||||
caches.keys().then((names) =>
|
||||
Promise.all(
|
||||
names
|
||||
.filter((name) => !currentCaches.has(name) && !name.startsWith('workbox-precache'))
|
||||
.map((name) => caches.delete(name))
|
||||
)
|
||||
)
|
||||
(async () => {
|
||||
// Drop caches from older SW versions that used Workbox caching.
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map((name) => caches.delete(name)));
|
||||
await self.clients.claim();
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
if (
|
||||
event.request.method === 'POST' &&
|
||||
url.pathname === '/upload-share-target'
|
||||
) {
|
||||
event.respondWith(handleShareTarget(event.request));
|
||||
}
|
||||
});
|
||||
|
||||
async function handleShareTarget(request: Request): Promise<Response> {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const files = formData.getAll('file');
|
||||
const title = formData.get('title');
|
||||
const text = formData.get('text');
|
||||
const urlField = formData.get('url');
|
||||
|
||||
await set('share-target', {
|
||||
files,
|
||||
title,
|
||||
text,
|
||||
url: urlField,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return Response.redirect('/new?shared=true&sw_status=success', 303);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
return Response.redirect(
|
||||
'/new?shared=true&sw_status=error&sw_error=' + encodeURIComponent(errMsg),
|
||||
303
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user