This commit is contained in:
bipproduction
2025-10-21 14:32:36 +08:00
parent b997d2ce33
commit fe397ef469
25 changed files with 2838 additions and 858 deletions

File diff suppressed because one or more lines are too long

View File

@@ -164,6 +164,18 @@ exports.Prisma.WaHookScalarFieldEnum = {
updatedAt: 'updatedAt'
};
exports.Prisma.ChatFlowsScalarFieldEnum = {
id: 'id',
flows: 'flows',
defaultFlow: 'defaultFlow',
defaultData: 'defaultData',
active: 'active',
flowUrl: 'flowUrl',
flowToken: 'flowToken',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -195,7 +207,8 @@ exports.Prisma.ModelName = {
User: 'User',
ApiKey: 'ApiKey',
WebHook: 'WebHook',
WaHook: 'WaHook'
WaHook: 'WaHook',
ChatFlows: 'ChatFlows'
};
/**

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"name": "prisma-client-b7159a5a3af13766d165b3bf5d09869b273a4102920455b6b9bd965ee512be7e",
"name": "prisma-client-0c787597671925c5dbd90480bfc5d45c3aa9b494d0bff63e2482cd62c5107b7b",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",

View File

@@ -53,3 +53,15 @@ model WaHook {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ChatFlows {
id String @id @default(cuid())
flows Json?
defaultFlow String?
defaultData Json?
active Boolean @default(true)
flowUrl String? @unique
flowToken String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -136,6 +136,18 @@ exports.Prisma.WaHookScalarFieldEnum = {
updatedAt: 'updatedAt'
};
exports.Prisma.ChatFlowsScalarFieldEnum = {
id: 'id',
flows: 'flows',
defaultFlow: 'defaultFlow',
defaultData: 'defaultData',
active: 'active',
flowUrl: 'flowUrl',
flowToken: 'flowToken',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -167,7 +179,8 @@ exports.Prisma.ModelName = {
User: 'User',
ApiKey: 'ApiKey',
WebHook: 'WebHook',
WaHook: 'WaHook'
WaHook: 'WaHook',
ChatFlows: 'ChatFlows'
};
/**
* Create the Client
@@ -216,13 +229,13 @@ const config = {
}
}
},
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel User {\n id String @id @default(cuid())\n name String?\n email String? @unique\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ApiKey ApiKey[]\n}\n\nmodel ApiKey {\n id String @id @default(cuid())\n User User? @relation(fields: [userId], references: [id])\n userId String\n name String\n key String @unique @db.Text\n description String?\n expiredAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel WebHook {\n id String @id @default(cuid())\n name String?\n description String?\n url String\n payload String? @default(\"{}\")\n method String @default(\"POST\")\n headers String? @default(\"{}\")\n apiToken String?\n retries Int? @default(3)\n enabled Boolean @default(true)\n replay Boolean @default(false)\n replayKey String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel WaHook {\n id String @id @default(cuid())\n data Json? @db.Json\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
"inlineSchemaHash": "f672201c199f0f043f3266324775af184d82ebd00379f5c23166510b25c889d0",
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel User {\n id String @id @default(cuid())\n name String?\n email String? @unique\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ApiKey ApiKey[]\n}\n\nmodel ApiKey {\n id String @id @default(cuid())\n User User? @relation(fields: [userId], references: [id])\n userId String\n name String\n key String @unique @db.Text\n description String?\n expiredAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel WebHook {\n id String @id @default(cuid())\n name String?\n description String?\n url String\n payload String? @default(\"{}\")\n method String @default(\"POST\")\n headers String? @default(\"{}\")\n apiToken String?\n retries Int? @default(3)\n enabled Boolean @default(true)\n replay Boolean @default(false)\n replayKey String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel WaHook {\n id String @id @default(cuid())\n data Json? @db.Json\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel ChatFlows {\n id String @id @default(cuid())\n flows Json?\n defaultFlow String?\n defaultData Json?\n active Boolean @default(true)\n flowUrl String? @unique\n flowToken String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
"inlineSchemaHash": "853d49417068ff1819c3dbb60ac7aeea59288f307f7939669c83fdd63bb495e3",
"copyEngine": true
}
config.dirname = '/'
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ApiKey\",\"kind\":\"object\",\"type\":\"ApiKey\",\"relationName\":\"ApiKeyToUser\"}],\"dbName\":null},\"ApiKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"User\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ApiKeyToUser\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"WebHook\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"payload\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"method\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"headers\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"apiToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"retries\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"enabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"replay\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"replayKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"WaHook\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ApiKey\",\"kind\":\"object\",\"type\":\"ApiKey\",\"relationName\":\"ApiKeyToUser\"}],\"dbName\":null},\"ApiKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"User\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ApiKeyToUser\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"WebHook\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"payload\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"method\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"headers\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"apiToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"retries\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"enabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"replay\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"replayKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"WaHook\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ChatFlows\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"flows\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"defaultFlow\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"defaultData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"flowUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"flowToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
config.engineWasm = {
getRuntime: async () => require('./query_engine_bg.js'),

View File

@@ -48,8 +48,20 @@ model WebHook {
}
model WaHook {
id String @id @default(cuid())
data Json? @db.Json
id String @id @default(cuid())
data Json? @db.Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ChatFlows {
id String @id @default(cuid())
flows Json?
defaultFlow String?
defaultData Json?
active Boolean @default(true)
flowUrl String? @unique
flowToken String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -7,7 +7,7 @@ import AppRoutes from "./AppRoutes";
export function App() {
return (
<MantineProvider>
<MantineProvider defaultColorScheme="dark">
<Notifications />
<ModalsProvider>
<AppRoutes />

View File

@@ -7,6 +7,9 @@ import WebhookHome from "./pages/sq/dashboard/webhook/webhook_home";
import WebhookLayout from "./pages/sq/dashboard/webhook/webhook_layout";
import WajsHome from "./pages/sq/dashboard/wajs/wajs_home";
import WajsLayout from "./pages/sq/dashboard/wajs/wajs_layout";
import WaHookHome from "./pages/sq/dashboard/wa-hook/wa_hook_home";
import FlowWaHook from "./pages/sq/dashboard/wa-hook/flow_wa_hook";
import WaHookLayout from "./pages/sq/dashboard/wa-hook/wa_hook_layout";
import ApikeyPage from "./pages/sq/dashboard/apikey/apikey_page";
import DashboardPage from "./pages/sq/dashboard/dashboard_page";
import DashboardLayout from "./pages/sq/dashboard/dashboard_layout";
@@ -48,6 +51,19 @@ export default function AppRoutes() {
element={<WajsHome />}
/>
</Route>
<Route path="/sq/dashboard/wa-hook" element={<WaHookLayout />}>
<Route index element={<WaHookHome />} />
<Route
path="/sq/dashboard/wa-hook/wa-hook-home"
element={<WaHookHome />}
/>
<Route
path="/sq/dashboard/wa-hook/flow-wa-hook"
element={<FlowWaHook />}
/>
</Route>
<Route
path="/sq/dashboard/apikey/apikey"
element={<ApikeyPage />}

View File

@@ -9,6 +9,9 @@ const clientRoutes = {
"/sq/dashboard/webhook/webhook-home": "/sq/dashboard/webhook/webhook-home",
"/sq/dashboard/wajs": "/sq/dashboard/wajs",
"/sq/dashboard/wajs/wajs-home": "/sq/dashboard/wajs/wajs-home",
"/sq/dashboard/wa-hook": "/sq/dashboard/wa-hook",
"/sq/dashboard/wa-hook/wa-hook-home": "/sq/dashboard/wa-hook/wa-hook-home",
"/sq/dashboard/wa-hook/flow-wa-hook": "/sq/dashboard/wa-hook/flow-wa-hook",
"/sq/dashboard/apikey/apikey": "/sq/dashboard/apikey/apikey",
"/sq/dashboard/dashboard": "/sq/dashboard/dashboard",
"/login": "/login",

View File

@@ -10,6 +10,7 @@ import WaRoute from "./server/routes/wa_route";
import WebhookRoute from "./server/routes/webhook_route";
import cors from "@elysiajs/cors";
import WaHookRoute from "./server/routes/wa_hook_route";
import FlowRoute from "./server/routes/flow_route";
const Docs = new Elysia().use(
Swagger({
@@ -35,7 +36,7 @@ const Api = new Elysia({
.use(ApiUser)
.use(WaRoute)
.use(WebhookRoute)
.use(FlowRoute);
const app = new Elysia()
.use(cors())

View File

@@ -1,7 +1,15 @@
import clientRoutes from "@/clientRoutes";
import { Button, Container } from "@mantine/core";
import { useNavigate } from "react-router-dom";
export default function Home() {
const navigate = useNavigate();
return (
<div>
<Container>
<h1>Home</h1>
</div>
<Button onClick={() => navigate(clientRoutes["/sq/dashboard"])}>
Go to SQ
</Button>
</Container>
);
}

View File

@@ -254,25 +254,17 @@ function ListApiKey({ refresh }: { refresh: boolean }) {
}}
>
<Table.Td>{apiKey.name}</Table.Td>
<Table.Td c="#9A9A9A">
{apiKey.description || "—"}
</Table.Td>
<Table.Td c="#9A9A9A">{apiKey.description || "—"}</Table.Td>
<Table.Td>
{apiKey.expiredAt
? new Date(apiKey.expiredAt)
.toISOString()
.split("T")[0]
? new Date(apiKey.expiredAt).toISOString().split("T")[0]
: "—"}
</Table.Td>
<Table.Td>
{new Date(apiKey.createdAt)
.toISOString()
.split("T")[0]}
{new Date(apiKey.createdAt).toISOString().split("T")[0]}
</Table.Td>
<Table.Td>
{new Date(apiKey.updatedAt)
.toISOString()
.split("T")[0]}
{new Date(apiKey.updatedAt).toISOString().split("T")[0]}
</Table.Td>
<Table.Td align="right">
<Group gap={4} justify="right">
@@ -301,7 +293,7 @@ function ListApiKey({ refresh }: { refresh: boolean }) {
id: apiKey.id,
});
setApiKeys((prev) =>
prev.filter((a) => a.id !== apiKey.id)
prev.filter((a) => a.id !== apiKey.id),
);
showNotification({
title: "Deleted",

View File

@@ -258,6 +258,12 @@ function NavigationDashboard() {
icon: <IconWebhook size={20} color="#00FFFF" />,
desc: "Incoming and outgoing event handlers",
},
{
path: clientRoutes["/sq/dashboard/wa-hook/wa-hook-home"],
label: "WA Hook",
icon: <IconWebhook size={20} color="#00FFFF" />,
desc: "WA Hook",
},
];
return (

View File

@@ -0,0 +1,195 @@
import { useState, useCallback } from "react";
import {
Container,
Stack,
Card,
Group,
Button,
Table,
TextInput,
PasswordInput,
ActionIcon,
Checkbox,
Text,
Title,
Flex,
Loader,
} from "@mantine/core";
import { IconReload, IconCheck, IconCopy } from "@tabler/icons-react";
import { showNotification } from "@mantine/notifications";
import { useShallowEffect } from "@mantine/hooks";
import apiFetch from "@/lib/apiFetch";
export default function FlowWaHook() {
return (
<Container size="xl" px="md">
<Stack gap="xl">
<FlowWaHookForm />
<FlowWaHookList />
</Stack>
</Container>
);
}
function FlowWaHookList() {
const [flows, setFlows] = useState<{ id: string; name: string; type: string }[]>([]);
const [loading, setLoading] = useState(false);
const [defaultFlow, setDefaultFlow] = useState("");
const loadFlows = useCallback(async () => {
setLoading(true);
const { data, error } = await apiFetch.api.chatflows.find.get();
if (error) {
showNotification({ title: "Error", message: "Failed to load flows", color: "red" });
} else {
setFlows(data?.flows || []);
setDefaultFlow(data?.defaultFlow || "");
}
setLoading(false);
}, []);
useShallowEffect(() => {
loadFlows();
}, [loadFlows]);
const syncFlows = async () => {
setLoading(true);
const { error } = await apiFetch.api.chatflows.sync.get();
if (error) {
showNotification({ title: "Error", message: "Sync failed", color: "red" });
} else {
await loadFlows();
showNotification({ title: "Success", message: "Flows synchronized", color: "green" });
}
setLoading(false);
};
const setAsDefault = async (id: string) => {
setLoading(true);
const { error } = await apiFetch.api.chatflows.default.put({ id, defaultData: {} });
if (error) {
showNotification({ title: "Error", message: "Failed to set default flow", color: "red" });
} else {
await loadFlows();
showNotification({ title: "Success", message: "Default flow updated", color: "green" });
}
setLoading(false);
};
return (
<Card radius="md" p="lg" withBorder>
<Stack gap="lg">
<Flex justify="space-between" align="center">
<Title order={3}>Flow Management</Title>
<Button leftSection={<IconReload size={16} />} onClick={syncFlows} loading={loading}>
Sync Flows
</Button>
</Flex>
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Default</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{flows.map((flow) => (
<Table.Tr key={flow.id} bg={defaultFlow === flow.id ? "dark.4" : undefined}>
<Table.Td>{flow.name}</Table.Td>
<Table.Td>{flow.type}</Table.Td>
<Table.Td>
<Checkbox checked={defaultFlow === flow.id} onChange={() => setAsDefault(flow.id)} />
</Table.Td>
</Table.Tr>
))}
{!loading && flows.length === 0 && (
<Table.Tr>
<Table.Td colSpan={3}>
<Text ta="center" c="dimmed">
No flows available
</Text>
</Table.Td>
</Table.Tr>
)}
{loading && (
<Table.Tr>
<Table.Td colSpan={3}>
<Flex justify="center" align="center" py="md">
<Loader size="sm" />
</Flex>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Stack>
</Card>
);
}
function FlowWaHookForm() {
const [flowUrl, setFlowUrl] = useState("");
const [flowToken, setFlowToken] = useState("");
const [loading, setLoading] = useState(false);
useShallowEffect(() => {
const loadCredentials = async () => {
const { data, error } = await apiFetch.api.chatflows["url-token"].get();
if (error) {
showNotification({ title: "Error", message: "Failed to load credentials", color: "red" });
} else {
setFlowUrl(data?.data?.flowUrl || "");
setFlowToken(data?.data?.flowToken || "");
}
};
loadCredentials();
}, []);
const saveCredentials = async () => {
if (!flowUrl || !flowToken) {
showNotification({ title: "Error", message: "URL and token are required", color: "red" });
return;
}
setLoading(true);
const { error } = await apiFetch.api.chatflows["url-token"].put({ flowUrl, flowToken });
if (error) {
showNotification({ title: "Error", message: "Failed to update credentials", color: "red" });
} else {
showNotification({ title: "Success", message: "Credentials updated", color: "green" });
}
setLoading(false);
};
const copyToken = () => {
navigator.clipboard.writeText(flowToken);
showNotification({ title: "Copied", message: "Token copied to clipboard", color: "green" });
};
return (
<Card radius="md" p="lg" withBorder>
<Stack gap="lg">
<Title order={3}>Flow Credentials</Title>
<Stack gap="md">
<TextInput label="Flow URL" placeholder="Enter flow URL" value={flowUrl} onChange={(e) => setFlowUrl(e.currentTarget.value)} />
<PasswordInput
label="Flow Token"
placeholder="Enter flow token"
value={flowToken}
onChange={(e) => setFlowToken(e.currentTarget.value)}
rightSection={
<ActionIcon onClick={copyToken}>
<IconCopy size={16} />
</ActionIcon>
}
/>
</Stack>
<Group justify="flex-end">
<Button leftSection={<IconCheck size={16} />} onClick={saveCredentials} loading={loading}>
Save Changes
</Button>
</Group>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,29 @@
import apiFetch from "@/lib/apiFetch";
import { Skeleton, Stack, Text, Title } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import useSWR from "swr";
export default function WaHookHome() {
const { data, error, isLoading, mutate } = useSWR("/wa-hook", apiFetch["wa-hook"].list.get,{
refreshInterval: 3000,
revalidateOnFocus: true,
revalidateOnReconnect: true,
revalidateIfStale: true,
refreshWhenHidden: true,
refreshWhenOffline: true,
dedupingInterval: 3000,
})
useShallowEffect(() => {
mutate()
}, [])
if (isLoading) return <Skeleton height={500} />
if (error) return <div>Error: {error.message}</div>
return (
<Stack>
<Title order={2}>WaHookHome</Title>
<pre>{JSON.stringify(data?.data?.list, null, 2)}</pre>
</Stack>
);
}

View File

@@ -0,0 +1,23 @@
import clientRoutes from "@/clientRoutes";
import { Button, Container, Group, Stack } from "@mantine/core";
import { Outlet, useNavigate } from "react-router-dom";
export default function WaHookLayout() {
const navigate = useNavigate();
return (
<Container size="xl" w={"100%"}>
<Group justify="flex-start" p={"md"}>
<Button
size="compact-xs"
radius={"lg"}
onClick={() =>
navigate(clientRoutes["/sq/dashboard/wa-hook/flow-wa-hook"])
}
>
Flow WA Hook
</Button>
</Group>
<Outlet />
</Container>
);
}

View File

@@ -1,15 +1,15 @@
import { useState, useMemo } from "react";
import {
Button,
Card,
Checkbox,
Group,
Stack,
Text,
TextInput,
Select,
Divider,
Title,
Button,
Card,
Checkbox,
Group,
Stack,
Text,
TextInput,
Select,
Divider,
Title,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { IconCode, IconCheck, IconX } from "@tabler/icons-react";
@@ -38,279 +38,273 @@ Available variables:
`;
export default function WebhookCreate() {
const navigate = useNavigate();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [url, setUrl] = useState("");
const [method, setMethod] = useState("POST");
const [headers, setHeaders] = useState(
JSON.stringify({ "Content-Type": "application/json" }, null, 2),
);
const [payload, setPayload] = useState("{}");
const [apiToken, setApiToken] = useState("");
const [enabled, setEnabled] = useState(true);
const [replay, setReplay] = useState(false);
const [replayKey, setReplayKey] = useState("");
const navigate = useNavigate();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [url, setUrl] = useState("");
const [method, setMethod] = useState("POST");
const [headers, setHeaders] = useState(
JSON.stringify({ "Content-Type": "application/json" }, null, 2),
);
const [payload, setPayload] = useState("{}");
const [apiToken, setApiToken] = useState("");
const [enabled, setEnabled] = useState(true);
const [replay, setReplay] = useState(false);
const [replayKey, setReplayKey] = useState("");
const safeJson = (value: string) => {
try {
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
} catch {
return value || "{}";
}
};
const safeJson = (value: string) => {
try {
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
} catch {
return value || "{}";
}
};
const previewCode = useMemo(() => {
let headerObj: Record<string, string> = {};
try {
headerObj = JSON.parse(headers);
} catch { }
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
const prettyHeaders = safeJson(JSON.stringify(headerObj));
const prettyPayload = safeJson(payload);
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
const previewCode = useMemo(() => {
let headerObj: Record<string, string> = {};
try {
headerObj = JSON.parse(headers);
} catch {}
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
const prettyHeaders = safeJson(JSON.stringify(headerObj));
const prettyPayload = safeJson(payload);
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
return `fetch("${url || "https://example.com/webhook"}", {
return `fetch("${url || "https://example.com/webhook"}", {
method: "${method}",
headers: ${prettyHeaders},${includeBody ? `\n body: ${prettyPayload},` : ""}
})
.then(res => res.json())
.then(console.log)
.catch(console.error);`;
}, [url, method, headers, payload, apiToken]);
}, [url, method, headers, payload, apiToken]);
async function onSubmit() {
const { data } = await apiFetch.api.webhook.create.post({
name,
description,
apiToken,
url,
method,
headers,
payload,
enabled,
replay,
replayKey,
});
async function onSubmit() {
const { data } = await apiFetch.api.webhook.create.post({
name,
description,
apiToken,
url,
method,
headers,
payload,
enabled,
replay,
replayKey,
});
if (data?.success) {
notifications.show({
title: "Webhook Created",
message: data.message,
color: "teal",
icon: <IconCheck />,
});
if (data?.success) {
notifications.show({
title: "Webhook Created",
message: data.message,
color: "teal",
icon: <IconCheck />,
});
navigate(clientRoutes["/sq/dashboard/webhook"]);
} else {
notifications.show({
title: "Creation Failed",
message: data?.message || "Unable to create webhook",
color: "red",
icon: <IconX />,
});
}
navigate(clientRoutes["/sq/dashboard/webhook"]);
} else {
notifications.show({
title: "Creation Failed",
message: data?.message || "Unable to create webhook",
color: "red",
icon: <IconX />,
});
}
}
return (
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Stack
gap="md"
maw={900}
mx="auto"
bg="rgba(45,45,45,0.6)"
p="xl"
style={{
borderRadius: "20px",
backdropFilter: "blur(12px)",
border: "1px solid rgba(0,255,200,0.2)",
// boxShadow: "0 0 25px rgba(0,255,200,0.15)",
}}
>
<Group justify="space-between">
<Title order={2} c="#EAEAEA" fw={600}>
Create Webhook
</Title>
<IconCode color="#00FFFF" size={28} />
</Group>
return (
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Stack
gap="md"
maw={900}
mx="auto"
bg="rgba(45,45,45,0.6)"
p="xl"
style={{
borderRadius: "20px",
backdropFilter: "blur(12px)",
border: "1px solid rgba(0,255,200,0.2)",
// boxShadow: "0 0 25px rgba(0,255,200,0.15)",
}}
>
<Group justify="space-between">
<Title order={2} c="#EAEAEA" fw={600}>
Create Webhook
</Title>
<IconCode color="#00FFFF" size={28} />
</Group>
<Divider color="rgba(0,255,200,0.2)" />
<Divider color="rgba(0,255,200,0.2)" />
<TextInput
label="Name"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextInput
label="Name"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextInput
label="Description"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<TextInput
label="Description"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<TextInput
label="Webhook URL"
placeholder="https://example.com/webhook"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<TextInput
label="Webhook URL"
placeholder="https://example.com/webhook"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<Select
label="HTTP Method"
placeholder="Select method"
value={method}
onChange={(v) => setMethod(v || "POST")}
data={["POST", "GET", "PUT", "PATCH", "DELETE"].map((v) => ({
value: v,
label: v,
}))}
/>
<Select
label="HTTP Method"
placeholder="Select method"
value={method}
onChange={(v) => setMethod(v || "POST")}
data={["POST", "GET", "PUT", "PATCH", "DELETE"].map((v) => ({
value: v,
label: v,
}))}
/>
<TextInput
label="API Token"
placeholder="Bearer ..."
value={apiToken}
onChange={(e) => {
setApiToken(e.target.value);
try {
const current = JSON.parse(headers);
if (!e.target.value) {
delete current["Authorization"];
} else {
current["Authorization"] = `Bearer ${e.target.value}`;
}
setHeaders(JSON.stringify(current, null, 2));
} catch { }
}}
/>
<TextInput
label="API Token"
placeholder="Bearer ..."
value={apiToken}
onChange={(e) => {
setApiToken(e.target.value);
try {
const current = JSON.parse(headers);
if (!e.target.value) {
delete current["Authorization"];
} else {
current["Authorization"] = `Bearer ${e.target.value}`;
}
setHeaders(JSON.stringify(current, null, 2));
} catch {}
}}
/>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Headers (JSON)
</Text>
<Editor
theme="vs-dark"
height="20vh"
language="json"
value={headers}
onChange={(val) => setHeaders(val ?? "{}")}
options={{
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
lineNumbers: "off",
automaticLayout: true,
}}
/>
</Stack>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Payload
</Text>
<Text size="xs" c="#9A9A9A" mb="xs">
{templateData}
</Text>
<Editor
theme="vs-dark"
height="35vh"
language="json"
value={payload}
onChange={(val) => setPayload(val ?? "{}")}
options={{
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Stack>
<Checkbox
label="Enable Webhook"
checked={enabled}
onChange={(e) => setEnabled(e.currentTarget.checked)}
color="teal"
styles={{
label: { color: "#EAEAEA" },
}}
/>
<Checkbox
label="Enable Replay"
checked={replay}
onChange={(e) => setReplay(e.currentTarget.checked)}
color="teal"
styles={{
label: { color: "#EAEAEA" },
}}
/>
<TextInput
description="Replay Key is used to identify the webhook example: data.text"
label="Replay Key"
placeholder="Replay Key"
value={replayKey}
onChange={(e) => setReplayKey(e.target.value)}
/>
<Card
radius="xl"
p="md"
style={{
background: "rgba(25,25,25,0.6)",
border: "1px solid rgba(0,255,200,0.3)",
// boxShadow: "0 0 15px rgba(0,255,200,0.15)",
}}
>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Request Preview
</Text>
<Editor
theme="vs-dark"
height="35vh"
language="javascript"
value={previewCode}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Stack>
</Card>
<Group justify="flex-end" mt="md">
<Button
onClick={() => navigate(clientRoutes["/sq/dashboard/webhook"])}
variant="subtle"
c="#EAEAEA"
styles={{
root: { backgroundColor: "#2D2D2D", borderColor: "#00FFC8" },
}}
>
Cancel
</Button>
<Button
onClick={onSubmit}
style={{
background: "linear-gradient(90deg, #00FFC8, #00FFFF)",
color: "#191919",
}}
>
Save Webhook
</Button>
</Group>
</Stack>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Headers (JSON)
</Text>
<Editor
theme="vs-dark"
height="20vh"
language="json"
value={headers}
onChange={(val) => setHeaders(val ?? "{}")}
options={{
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
lineNumbers: "off",
automaticLayout: true,
}}
/>
</Stack>
);
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Payload
</Text>
<Text size="xs" c="#9A9A9A" mb="xs">
{templateData}
</Text>
<Editor
theme="vs-dark"
height="35vh"
language="json"
value={payload}
onChange={(val) => setPayload(val ?? "{}")}
options={{
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Stack>
<Checkbox
label="Enable Webhook"
checked={enabled}
onChange={(e) => setEnabled(e.currentTarget.checked)}
color="teal"
styles={{
label: { color: "#EAEAEA" },
}}
/>
<Checkbox
label="Enable Replay"
checked={replay}
onChange={(e) => setReplay(e.currentTarget.checked)}
color="teal"
styles={{
label: { color: "#EAEAEA" },
}}
/>
<TextInput
description="Replay Key is used to identify the webhook example: data.text"
label="Replay Key"
placeholder="Replay Key"
value={replayKey}
onChange={(e) => setReplayKey(e.target.value)}
/>
<Card
radius="xl"
p="md"
style={{
background: "rgba(25,25,25,0.6)",
border: "1px solid rgba(0,255,200,0.3)",
// boxShadow: "0 0 15px rgba(0,255,200,0.15)",
}}
>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Request Preview
</Text>
<Editor
theme="vs-dark"
height="35vh"
language="javascript"
value={previewCode}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Stack>
</Card>
<Group justify="flex-end" mt="md">
<Button
onClick={() => navigate(clientRoutes["/sq/dashboard/webhook"])}
variant="subtle"
c="#EAEAEA"
styles={{
root: { backgroundColor: "#2D2D2D", borderColor: "#00FFC8" },
}}
>
Cancel
</Button>
<Button
onClick={onSubmit}
style={{
background: "linear-gradient(90deg, #00FFC8, #00FFFF)",
color: "#191919",
}}
>
Save Webhook
</Button>
</Group>
</Stack>
</Stack>
);
}

View File

@@ -7,7 +7,18 @@ import { useMemo } from "react";
import { notifications } from "@mantine/notifications";
import { IconCode, IconCheck, IconX } from "@tabler/icons-react";
import Editor from "@monaco-editor/react";
import { Stack, Group, Title, Divider, TextInput, Select, Checkbox, Card, Button, Text } from "@mantine/core";
import {
Stack,
Group,
Title,
Divider,
TextInput,
Select,
Checkbox,
Card,
Button,
Text,
} from "@mantine/core";
import { modals } from "@mantine/modals";
import clientRoutes from "@/clientRoutes";
import { useShallowEffect } from "@mantine/hooks";
@@ -18,344 +29,359 @@ Available variables:
`;
export default function WebhookEdit() {
const [searchParams] = useSearchParams();
const id = searchParams.get("id");
const { data, error, isLoading, mutate } = useSWR("/", () => apiFetch.api.webhook.find({
id: id!
}).get(), {dedupingInterval: 3000})
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const id = searchParams.get("id");
const { data, error, isLoading, mutate } = useSWR(
"/",
() =>
apiFetch.api.webhook
.find({
id: id!,
})
.get(),
{ dedupingInterval: 3000 },
);
const navigate = useNavigate();
useShallowEffect(() => {
mutate();
}, [data]);
useShallowEffect(() => {
mutate();
}, [data]);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data?.data?.webhook) return <div>No data</div>;
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data?.data?.webhook) return <div>No data</div>;
return (
<Stack>
<Group justify="space-between">
<Title order={2}>Edit Webhook</Title>
<Button variant="outline" onClick={() => {
modals.openConfirmModal({
title: "Remove Webhook",
children: <Text>Are you sure you want to remove this webhook?</Text>,
confirmProps: { color: "red" },
labels: {
cancel: "Cancel",
confirm: "Remove",
},
onConfirm: () => {
apiFetch.api.webhook.remove({
id: id!
}).delete()
navigate(clientRoutes["/sq/dashboard/webhook"]);
},
onCancel: () => {
navigate(clientRoutes["/sq/dashboard/webhook/webhook-edit"] + "?id=" + id);
},
})
}}>Remove</Button>
</Group>
<EditView webhook={data.data?.webhook || null} />
</Stack>
);
return (
<Stack>
<Group justify="space-between">
<Title order={2}>Edit Webhook</Title>
<Button
variant="outline"
onClick={() => {
modals.openConfirmModal({
title: "Remove Webhook",
children: (
<Text>Are you sure you want to remove this webhook?</Text>
),
confirmProps: { color: "red" },
labels: {
cancel: "Cancel",
confirm: "Remove",
},
onConfirm: () => {
apiFetch.api.webhook
.remove({
id: id!,
})
.delete();
navigate(clientRoutes["/sq/dashboard/webhook"]);
},
onCancel: () => {
navigate(
clientRoutes["/sq/dashboard/webhook/webhook-edit"] +
"?id=" +
id,
);
},
});
}}
>
Remove
</Button>
</Group>
<EditView webhook={data.data?.webhook || null} />
</Stack>
);
}
function EditView({ webhook }: { webhook: WebHook | null }) {
const navigate = useNavigate();
const [name, setName] = useState(webhook?.name || "");
const [description, setDescription] = useState(webhook?.description || "");
const [url, setUrl] = useState(webhook?.url || "");
const [method, setMethod] = useState(webhook?.method || "POST");
const [headers, setHeaders] = useState(webhook?.headers || "{}");
const [payload, setPayload] = useState(webhook?.payload || "{}");
const [apiToken, setApiToken] = useState(webhook?.apiToken || "");
const [enabled, setEnabled] = useState(webhook?.enabled || true);
const [replay, setReplay] = useState(webhook?.replay || false);
const [replayKey, setReplayKey] = useState(webhook?.replayKey || "");
const navigate = useNavigate();
const [name, setName] = useState(webhook?.name || "");
const [description, setDescription] = useState(webhook?.description || "");
const [url, setUrl] = useState(webhook?.url || "");
const [method, setMethod] = useState(webhook?.method || "POST");
const [headers, setHeaders] = useState(webhook?.headers || "{}");
const [payload, setPayload] = useState(webhook?.payload || "{}");
const [apiToken, setApiToken] = useState(webhook?.apiToken || "");
const [enabled, setEnabled] = useState(webhook?.enabled || true);
const [replay, setReplay] = useState(webhook?.replay || false);
const [replayKey, setReplayKey] = useState(webhook?.replayKey || "");
const safeJson = (value: string) => {
try {
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
} catch {
return value || "{}";
}
};
const safeJson = (value: string) => {
try {
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
} catch {
return value || "{}";
}
};
// useShallowEffect(() => {
// let headerObj: Record<string, string> = {};
// try {
// headerObj = JSON.parse(headers);
// } catch { }
// if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
// setHeaders(JSON.stringify(headerObj, null, 2));
// }, [apiToken]);
// useShallowEffect(() => {
// let headerObj: Record<string, string> = {};
// try {
// headerObj = JSON.parse(headers);
// } catch { }
// if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
// setHeaders(JSON.stringify(headerObj, null, 2));
// }, [apiToken]);
const previewCode = useMemo(() => {
let headerObj: Record<string, string> = {};
try {
headerObj = JSON.parse(headers);
} catch { }
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
const prettyHeaders = safeJson(JSON.stringify(headerObj));
const prettyPayload = safeJson(payload);
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
const previewCode = useMemo(() => {
let headerObj: Record<string, string> = {};
try {
headerObj = JSON.parse(headers);
} catch {}
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
const prettyHeaders = safeJson(JSON.stringify(headerObj));
const prettyPayload = safeJson(payload);
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
return `fetch("${url || "https://example.com/webhook"}", {
return `fetch("${url || "https://example.com/webhook"}", {
method: "${method}",
headers: ${prettyHeaders},${includeBody ? `\n body: ${prettyPayload},` : ""}
})
.then(res => res.json())
.then(console.log)
.catch(console.error);`;
}, [url, method, headers, payload, apiToken]);
}, [url, method, headers, payload, apiToken]);
async function onSubmit() {
if (!webhook?.id) {
return notifications.show({
title: "Webhook ID Not Found",
message: "Unable to update webhook",
color: "red",
icon: <IconX />,
});
}
const { data } = await apiFetch.api.webhook.update({
id: webhook?.id,
}).put({
name,
description,
apiToken,
url,
method,
headers,
payload,
enabled,
replay,
replayKey,
});
if (data?.success) {
notifications.show({
title: "Webhook Created",
message: data.message,
color: "teal",
icon: <IconCheck />,
});
navigate(clientRoutes["/sq/dashboard/webhook"]);
} else {
notifications.show({
title: "Creation Failed",
message: data?.message || "Unable to create webhook",
color: "red",
icon: <IconX />,
});
}
async function onSubmit() {
if (!webhook?.id) {
return notifications.show({
title: "Webhook ID Not Found",
message: "Unable to update webhook",
color: "red",
icon: <IconX />,
});
}
const { data } = await apiFetch.api.webhook
.update({
id: webhook?.id,
})
.put({
name,
description,
apiToken,
url,
method,
headers,
payload,
enabled,
replay,
replayKey,
});
return (
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Stack
gap="md"
maw={900}
mx="auto"
bg="rgba(45,45,45,0.6)"
p="xl"
style={{
borderRadius: "20px",
backdropFilter: "blur(12px)",
border: "1px solid rgba(0,255,200,0.2)",
// boxShadow: "0 0 25px rgba(0,255,200,0.15)",
}}
>
<Group justify="space-between">
<Title order={2} c="#EAEAEA" fw={600}>
Create Webhook
</Title>
<IconCode color="#00FFFF" size={28} />
</Group>
if (data?.success) {
notifications.show({
title: "Webhook Created",
message: data.message,
color: "teal",
icon: <IconCheck />,
});
navigate(clientRoutes["/sq/dashboard/webhook"]);
} else {
notifications.show({
title: "Creation Failed",
message: data?.message || "Unable to create webhook",
color: "red",
icon: <IconX />,
});
}
}
<Divider color="rgba(0,255,200,0.2)" />
return (
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Stack
gap="md"
maw={900}
mx="auto"
bg="rgba(45,45,45,0.6)"
p="xl"
style={{
borderRadius: "20px",
backdropFilter: "blur(12px)",
border: "1px solid rgba(0,255,200,0.2)",
// boxShadow: "0 0 25px rgba(0,255,200,0.15)",
}}
>
<Group justify="space-between">
<Title order={2} c="#EAEAEA" fw={600}>
Create Webhook
</Title>
<IconCode color="#00FFFF" size={28} />
</Group>
<TextInput
label="Name"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Divider color="rgba(0,255,200,0.2)" />
<TextInput
label="Description"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<TextInput
label="Name"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextInput
label="Webhook URL"
placeholder="https://example.com/webhook"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<TextInput
label="Description"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<Select
label="HTTP Method"
placeholder="Select method"
value={method}
onChange={(v) => setMethod(v || "POST")}
data={["POST", "GET", "PUT", "PATCH", "DELETE"].map((v) => ({
value: v,
label: v,
}))}
/>
<TextInput
label="Webhook URL"
placeholder="https://example.com/webhook"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<TextInput
label="API Token"
placeholder="Bearer ..."
value={apiToken}
onChange={(e) => {
setApiToken(e.target.value);
try {
const current = JSON.parse(headers);
if (!e.target.value) {
delete current["Authorization"];
} else {
current["Authorization"] = `Bearer ${e.target.value}`;
}
setHeaders(JSON.stringify(current, null, 2));
} catch { }
}}
/>
<Select
label="HTTP Method"
placeholder="Select method"
value={method}
onChange={(v) => setMethod(v || "POST")}
data={["POST", "GET", "PUT", "PATCH", "DELETE"].map((v) => ({
value: v,
label: v,
}))}
/>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Headers (JSON)
</Text>
<Editor
theme="vs-dark"
height="20vh"
language="json"
value={headers}
onChange={(val) => setHeaders(val ?? "{}")}
options={{
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
lineNumbers: "off",
automaticLayout: true,
}}
/>
</Stack>
<TextInput
label="API Token"
placeholder="Bearer ..."
value={apiToken}
onChange={(e) => {
setApiToken(e.target.value);
try {
const current = JSON.parse(headers);
if (!e.target.value) {
delete current["Authorization"];
} else {
current["Authorization"] = `Bearer ${e.target.value}`;
}
setHeaders(JSON.stringify(current, null, 2));
} catch {}
}}
/>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Payload
</Text>
<Text size="xs" c="#9A9A9A" mb="xs">
{templateData}
</Text>
<Editor
theme="vs-dark"
height="35vh"
language="json"
value={payload}
onChange={(val) => setPayload(val ?? "{}")}
options={{
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Stack>
<Checkbox
label="Enable Webhook"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked as any)}
color="teal"
styles={{
label: { color: "#EAEAEA" },
}}
/>
<Checkbox
label="Enable Replay"
checked={replay}
onChange={(e) => setReplay(e.target.checked as any)}
color="teal"
styles={{
label: { color: "#EAEAEA" },
}}
/>
<TextInput
description="Replay Key is used to identify the webhook example: data.text"
label="Replay Key"
placeholder="Replay Key"
value={replayKey}
onChange={(e) => setReplayKey(e.target.value)}
/>
<Card
radius="xl"
p="md"
style={{
background: "rgba(25,25,25,0.6)",
border: "1px solid rgba(0,255,200,0.3)",
// boxShadow: "0 0 15px rgba(0,255,200,0.15)",
}}
>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Request Preview
</Text>
<Editor
theme="vs-dark"
height="35vh"
language="javascript"
value={previewCode}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Stack>
</Card>
<Group justify="flex-end" mt="md">
<Button
onClick={() => navigate(clientRoutes["/sq/dashboard/webhook"])}
variant="subtle"
c="#EAEAEA"
styles={{
root: { backgroundColor: "#2D2D2D", borderColor: "#00FFC8" },
}}
>
Cancel
</Button>
<Button
onClick={onSubmit}
style={{
background: "linear-gradient(90deg, #00FFC8, #00FFFF)",
color: "#191919",
}}
>
Save Webhook
</Button>
</Group>
</Stack>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Headers (JSON)
</Text>
<Editor
theme="vs-dark"
height="20vh"
language="json"
value={headers}
onChange={(val) => setHeaders(val ?? "{}")}
options={{
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
lineNumbers: "off",
automaticLayout: true,
}}
/>
</Stack>
);
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Payload
</Text>
<Text size="xs" c="#9A9A9A" mb="xs">
{templateData}
</Text>
<Editor
theme="vs-dark"
height="35vh"
language="json"
value={payload}
onChange={(val) => setPayload(val ?? "{}")}
options={{
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Stack>
<Checkbox
label="Enable Webhook"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked as any)}
color="teal"
styles={{
label: { color: "#EAEAEA" },
}}
/>
<Checkbox
label="Enable Replay"
checked={replay}
onChange={(e) => setReplay(e.target.checked as any)}
color="teal"
styles={{
label: { color: "#EAEAEA" },
}}
/>
<TextInput
description="Replay Key is used to identify the webhook example: data.text"
label="Replay Key"
placeholder="Replay Key"
value={replayKey}
onChange={(e) => setReplayKey(e.target.value)}
/>
<Card
radius="xl"
p="md"
style={{
background: "rgba(25,25,25,0.6)",
border: "1px solid rgba(0,255,200,0.3)",
// boxShadow: "0 0 15px rgba(0,255,200,0.15)",
}}
>
<Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Request Preview
</Text>
<Editor
theme="vs-dark"
height="35vh"
language="javascript"
value={previewCode}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Stack>
</Card>
<Group justify="flex-end" mt="md">
<Button
onClick={() => navigate(clientRoutes["/sq/dashboard/webhook"])}
variant="subtle"
c="#EAEAEA"
styles={{
root: { backgroundColor: "#2D2D2D", borderColor: "#00FFC8" },
}}
>
Cancel
</Button>
<Button
onClick={onSubmit}
style={{
background: "linear-gradient(90deg, #00FFC8, #00FFFF)",
color: "#191919",
}}
>
Save Webhook
</Button>
</Group>
</Stack>
</Stack>
);
}

View File

@@ -1,28 +1,28 @@
import { useMemo } from "react";
import {
Card,
Group,
Text,
Title,
Badge,
Loader,
Center,
Tooltip,
ActionIcon,
Stack,
Divider,
Button,
Card,
Group,
Text,
Title,
Badge,
Loader,
Center,
Tooltip,
ActionIcon,
Stack,
Divider,
Button,
} from "@mantine/core";
import {
IconLink,
IconCode,
IconKey,
IconCheck,
IconX,
IconRefresh,
IconEdit,
IconPlus,
IconMessageReply,
IconLink,
IconCode,
IconKey,
IconCheck,
IconX,
IconRefresh,
IconEdit,
IconPlus,
IconMessageReply,
} from "@tabler/icons-react";
import { notifications } from "@mantine/notifications";
import useSWR from "swr";
@@ -32,218 +32,228 @@ import clientRoutes from "@/clientRoutes";
import { useShallowEffect } from "@mantine/hooks";
export default function WebhookHome() {
const navigate = useNavigate();
const { data, error, isLoading, mutate } = useSWR(
"/",
apiFetch.api.webhook.list.get, { dedupingInterval: 3000, refreshInterval: 3000 });
const navigate = useNavigate();
const { data, error, isLoading, mutate } = useSWR(
"/",
apiFetch.api.webhook.list.get,
{ dedupingInterval: 3000, refreshInterval: 3000 },
);
const webhooks = useMemo(() => data?.data?.list ?? [], [data]);
const webhooks = useMemo(() => data?.data?.list ?? [], [data]);
useShallowEffect(() => {
mutate();
}, []);
function ButtonCreate() {
return <Tooltip label="Create new webhook" withArrow color="teal">
<Button
radius="xl"
size="md"
leftSection={<IconPlus size={18} />}
variant="gradient"
gradient={{ from: "#00FFC8", to: "#00FFFF", deg: 135 }}
style={{
color: "#191919",
fontWeight: 600,
// boxShadow: "0 0 12px rgba(0,255,200,0.25)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-2px)";
e.currentTarget.style.boxShadow =
"0 0 20px rgba(0,255,200,0.4)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow =
"0 0 12px rgba(0,255,200,0.25)";
}}
onClick={() => navigate("/sq/dashboard/webhook/webhook-create")}
>
Create Webhook
</Button>
</Tooltip>
}
if (isLoading)
return (
<Center h="100vh" bg="#191919">
<Loader color="teal" size="lg" />
</Center>
);
if (error)
return (
<Center h="100vh" bg="#191919">
<Text c="#FF4B4B" fw={500}>
Failed to load webhooks. Please try again.
</Text>
</Center>
);
if (!webhooks.length)
return (
<Center h="100vh" bg="#191919">
<Stack align="center" gap="sm">
<Text c="#9A9A9A" size="lg">
No webhooks found
</Text>
<Text c="#00FFC8" size="sm">
Connect your first webhook to start managing events
</Text>
<ButtonCreate />
</Stack>
</Center>
);
useShallowEffect(() => {
mutate();
}, []);
function ButtonCreate() {
return (
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Group justify="space-between" mb="lg">
<Title order={2} c="#EAEAEA" fw={600}>
Webhook Manager
</Title>
<ButtonCreate />
<Tooltip label="Refresh webhooks" withArrow color="cyan">
<ActionIcon
variant="light"
size="lg"
radius="xl"
onClick={() => {
mutate();
notifications.show({
title: "Refreshing data",
message: "Webhook list is being updated...",
color: "teal",
});
}}
>
<IconRefresh color="#00FFFF" />
</ActionIcon>
</Tooltip>
<Tooltip label="Create new webhook" withArrow color="teal">
<Button
radius="xl"
size="md"
leftSection={<IconPlus size={18} />}
variant="gradient"
gradient={{ from: "#00FFC8", to: "#00FFFF", deg: 135 }}
style={{
color: "#191919",
fontWeight: 600,
// boxShadow: "0 0 12px rgba(0,255,200,0.25)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-2px)";
e.currentTarget.style.boxShadow = "0 0 20px rgba(0,255,200,0.4)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow = "0 0 12px rgba(0,255,200,0.25)";
}}
onClick={() => navigate("/sq/dashboard/webhook/webhook-create")}
>
Create Webhook
</Button>
</Tooltip>
);
}
if (isLoading)
return (
<Center h="100vh" bg="#191919">
<Loader color="teal" size="lg" />
</Center>
);
if (error)
return (
<Center h="100vh" bg="#191919">
<Text c="#FF4B4B" fw={500}>
Failed to load webhooks. Please try again.
</Text>
</Center>
);
if (!webhooks.length)
return (
<Center h="100vh" bg="#191919">
<Stack align="center" gap="sm">
<Text c="#9A9A9A" size="lg">
No webhooks found
</Text>
<Text c="#00FFC8" size="sm">
Connect your first webhook to start managing events
</Text>
<ButtonCreate />
</Stack>
</Center>
);
return (
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Group justify="space-between" mb="lg">
<Title order={2} c="#EAEAEA" fw={600}>
Webhook Manager
</Title>
<ButtonCreate />
<Tooltip label="Refresh webhooks" withArrow color="cyan">
<ActionIcon
variant="light"
size="lg"
radius="xl"
onClick={() => {
mutate();
notifications.show({
title: "Refreshing data",
message: "Webhook list is being updated...",
color: "teal",
});
}}
>
<IconRefresh color="#00FFFF" />
</ActionIcon>
</Tooltip>
</Group>
<Stack gap="md">
{webhooks.map((webhook) => (
<Card
key={webhook.id}
p="lg"
radius="xl"
style={{
background: "rgba(45,45,45,0.6)",
backdropFilter: "blur(12px)",
border: "1px solid rgba(0,255,200,0.2)",
// boxShadow: "0 0 12px rgba(0,255,200,0.15)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
>
<Group justify="end" mb="sm">
<Group>
<IconLink color="#00FFFF" />
<Text c="#EAEAEA" fw={500} size="lg">
{webhook.name}
</Text>
</Group>
<ActionIcon
c={"teal"}
variant="light"
size="lg"
radius="xl"
onClick={() =>
navigate(
`${clientRoutes["/sq/dashboard/webhook/webhook-edit"]}?id=${webhook.id}`,
)
}
>
<IconEdit />
</ActionIcon>
</Group>
<Stack gap="md">
{webhooks.map((webhook) => (
<Card
key={webhook.id}
p="lg"
radius="xl"
style={{
background: "rgba(45,45,45,0.6)",
backdropFilter: "blur(12px)",
border: "1px solid rgba(0,255,200,0.2)",
// boxShadow: "0 0 12px rgba(0,255,200,0.15)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
>
<Group justify="end" mb="sm">
<Group>
<IconLink color="#00FFFF" />
<Text c="#EAEAEA" fw={500} size="lg">
{webhook.name}
</Text>
</Group>
<ActionIcon
c={"teal"}
variant="light"
size="lg"
radius="xl"
onClick={() => navigate(`${clientRoutes["/sq/dashboard/webhook/webhook-edit"]}?id=${webhook.id}`)}
>
<IconEdit />
</ActionIcon>
</Group>
<Stack gap={"md"}>
<Group>
<Badge
color={webhook.enabled ? "teal" : "red"}
radius="xl"
leftSection={
webhook.enabled ? (
<IconCheck size={14} />
) : (
<IconX size={14} />
)
}
>
{webhook.enabled ? "Active" : "Disabled"}
</Badge>
<Badge bg={"teal"} leftSection={<IconMessageReply size={16} color="#00FFC8" />}>
{webhook.replay ? "Replay" : "Not Replay"}
</Badge>
</Group>
<Text c="#9A9A9A" size="sm">{webhook.description}</Text>
</Stack>
<Divider color="rgba(0,255,200,0.2)" my="sm" />
<Stack gap="xs">
<Group gap="xs">
<IconCode size={16} color="#00FFC8" />
<Text c="#9A9A9A" size="sm">
Method:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{webhook.method}
</Text>
</Group>
<Group gap="xs">
<IconLink size={16} color="#00FFC8" />
<Text c="#9A9A9A" size="sm">
URL:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{webhook.url}
</Text>
</Group>
<Group gap="xs">
<IconKey size={16} color="#00FFC8" />
<Text c="#9A9A9A" size="sm">
API Token:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{webhook.apiToken?.slice(0, 6) + "..." || "—"}
</Text>
</Group>
<Group gap="xs">
<Text c="#9A9A9A" size="sm">
Headers:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{Object.keys(webhook.headers || {}).length
? webhook.headers
: "No headers configured"}
</Text>
</Group>
<Group gap="xs">
<Text c="#9A9A9A" size="sm">
Payload:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{Object.keys(webhook.payload || {}).length
? webhook.payload
: "Empty payload"}
</Text>
</Group>
</Stack>
</Card>
))}
<Stack gap={"md"}>
<Group>
<Badge
color={webhook.enabled ? "teal" : "red"}
radius="xl"
leftSection={
webhook.enabled ? (
<IconCheck size={14} />
) : (
<IconX size={14} />
)
}
>
{webhook.enabled ? "Active" : "Disabled"}
</Badge>
<Badge
bg={"teal"}
leftSection={<IconMessageReply size={16} color="#00FFC8" />}
>
{webhook.replay ? "Replay" : "Not Replay"}
</Badge>
</Group>
<Text c="#9A9A9A" size="sm">
{webhook.description}
</Text>
</Stack>
</Stack>
);
<Divider color="rgba(0,255,200,0.2)" my="sm" />
<Stack gap="xs">
<Group gap="xs">
<IconCode size={16} color="#00FFC8" />
<Text c="#9A9A9A" size="sm">
Method:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{webhook.method}
</Text>
</Group>
<Group gap="xs">
<IconLink size={16} color="#00FFC8" />
<Text c="#9A9A9A" size="sm">
URL:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{webhook.url}
</Text>
</Group>
<Group gap="xs">
<IconKey size={16} color="#00FFC8" />
<Text c="#9A9A9A" size="sm">
API Token:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{webhook.apiToken?.slice(0, 6) + "..." || "—"}
</Text>
</Group>
<Group gap="xs">
<Text c="#9A9A9A" size="sm">
Headers:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{Object.keys(webhook.headers || {}).length
? webhook.headers
: "No headers configured"}
</Text>
</Group>
<Group gap="xs">
<Text c="#9A9A9A" size="sm">
Payload:
</Text>
<Text c="#EAEAEA" size="sm" fw={500}>
{Object.keys(webhook.payload || {}).length
? webhook.payload
: "Empty payload"}
</Text>
</Group>
</Stack>
</Card>
))}
</Stack>
</Stack>
);
}

View File

@@ -1,20 +1,18 @@
import {
Button,
Group,
Stack,
Title,
Tooltip,
Divider,
Container,
Paper,
Button,
Group,
Stack,
Title,
Tooltip,
Divider,
Container,
Paper,
} from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import { useNavigate, Outlet } from "react-router-dom";
export default function WebhookLayout() {
const navigate = useNavigate();
const navigate = useNavigate();
return (
<Outlet />
);
return <Outlet />;
}

View File

@@ -0,0 +1,247 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Elysia, { t } from 'elysia'
import { prisma } from '../lib/prisma'
import _ from 'lodash'
const getUrlToken = async () => await prisma.chatFlows.findUnique({ where: { id: "1" }, select: { flowUrl: true, flowToken: true } })
const FlowRoute = new Elysia({
prefix: '/chatflows',
detail: { tags: ['chatflows'] },
})
.get('/sync', async ctx => {
const result = await getUrlToken()
if (!result) {
return { error: 'Flow URL and Token not found' }
}
const { flowUrl, flowToken } = result
const response = await fetch(flowUrl + '/chatflows', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + flowToken,
Accept: '*/*',
},
})
if (!response.ok) {
return { error: 'Failed to fetch flows' }
}
const data = await response.json()
const chatflows = await prisma.chatFlows.upsert({
where: {
id: "1",
},
update: {
flows: data,
},
create: {
flows: data,
},
})
return { data: chatflows }
}, {
detail: {
summary: "Sync chatflows",
description: "Sync chatflows",
}
})
.get('/find', async ctx => {
const result = await prisma.chatFlows.findUnique({
where: { id: "1" },
})
if (!result) {
return { flows: [], defaultFlow: null, flowUrl: null, flowToken: null }
}
const flows = _.orderBy(result?.flows as any[], ['type'], ['asc'])
const defaultFlow = result?.defaultFlow
const flowUrl = result?.flowUrl
const flowToken = result?.flowToken
return { flows, defaultFlow, flowUrl, flowToken }
}, {
detail: {
summary: "Find chatflows",
description: "Find chatflows",
}
})
.get("/default", async ctx => {
const result = await prisma.chatFlows.findUnique({
where: { id: "1" },
})
if (!result) {
return { defaultFlow: null, defaultData: null }
}
const defaultFlow = result?.defaultFlow
const defaultData = result?.defaultData
return { defaultFlow, defaultData }
}, {
detail: {
summary: "Get default chatflows",
description: "Get default chatflows",
}
})
.put(
'/default',
async ctx => {
const { id, defaultData } = ctx.body
const result = await prisma.chatFlows.update({
where: {
id: "1",
},
data: {
defaultFlow: id,
defaultData: defaultData,
},
})
return { data: result }
},
{
body: t.Object({
id: t.String(),
defaultData: t.Optional(t.Any()),
}),
detail: {
summary: "Update default chatflows",
description: "Update default chatflows",
}
}
)
.post(
'/query',
async ctx => {
const { flowId, question } = ctx.body
const result = await chatFlowQuery({ flowId, question })
return { data: result }
},
{
body: t.Object({
flowId: t.String(),
question: t.String(),
}),
detail: {
summary: "Query chatflows",
description: "Query chatflows",
}
}
)
.put(
'/flow-active',
async ctx => {
const { active } = ctx.body
const result = await prisma.chatFlows.upsert({
where: {
id: "1",
},
update: {
active: active,
},
create: {
active: active,
},
})
return { data: result }
},
{
body: t.Object({
active: t.Boolean(),
}),
detail: {
summary: "Update flow active",
description: "Update flow active",
}
}
)
.get('/url-token', async ctx => {
const result = await prisma.chatFlows.findUnique({
where: { id: "1" },
select: {
flowUrl: true,
flowToken: true,
},
})
if (!result) {
return { data: { flowUrl: null, flowToken: null } }
}
return { data: { flowUrl: result.flowUrl, flowToken: result.flowToken } }
}, {
detail: {
summary: "Get flow url and token",
description: "Get flow url and token",
}
})
.put(
'/url-token',
async ctx => {
const { flowUrl, flowToken } = ctx.body
const result = await prisma.chatFlows.upsert({
where: {
id: "1",
},
update: {
flowUrl: flowUrl,
flowToken: flowToken,
},
create: {
id: "1",
flowUrl: flowUrl,
flowToken: flowToken,
},
})
return { data: result }
},
{
body: t.Object({
flowUrl: t.String(),
flowToken: t.String(),
}),
detail: {
summary: "Update flow url and token",
description: "Update flow url and token",
}
}
)
.on('error', ctx => {
console.log(ctx.error)
return { error: ctx.error }
})
export default FlowRoute
async function chatFlowQuery({
flowId,
question,
}: {
flowId: string
question: string
}) {
try {
const resultUrlToken = await prisma.chatFlows.findUnique({ where: { id: "1" }, select: { flowUrl: true, flowToken: true } })
if (!resultUrlToken) {
return { error: 'Flow URL and Token not found' }
}
const { flowUrl, flowToken } = resultUrlToken
if (!flowUrl || !flowToken) {
return { error: 'Flow URL and Token not found' }
}
const response = await fetch(`${flowUrl}/prediction/${flowId}`, {
headers: {
Authorization: `Bearer ${flowToken}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
question,
overrideConfig: {
sessionId: "1",
},
}),
})
const result = await response.text()
return JSON.parse(result).text
} catch (error) {
console.log(error)
return 'Failed to fetch response'
}
}

13
x.sh
View File

@@ -1,8 +1,7 @@
TOKEN="EAALP22EWyC4BPnZCfcPQNmD5pGLKV6Ao3GIeWZCc81aPivDFc2FXGA1ZBgrRGcB60LaZCdAr1sbnfP1ufrH3dGthxQzpf18BTjDZBkgG3vBiYZAMpHa7MEZBiRIUZCBe4BDXe8KV0r7DsDmQHJqhA3yZBDKPOL1PKJPEqIq40tLxPwMqWYg4o7xf0sBmZCzx2wI1KtJL8I20MV1ggldngHZCIcnOKDL0uPzDAhc2LAQuI7ZBsgZDZD"
MEDIA_ID="24893686766920074"
BUSINESS_PHONE_NUMBER_ID="783866307805501"
#!/bin/bash
TOKEN="EAALP22EWyC4BPv7XnK1xSaZCWccblEoJFbHzPZAf5mlp4678lSM7cqhQl1ExATf8abrOpinvvFF6U6ruK2FsJqIk8wg6DiUz2fc0NYfcwjon3ng7I3C5HSDQHecgTiJLUBxfZAcvE4IIlhks722jakXaJpojlByo8QJ0CEURtzwEU1guFq7YTX3Et0ZCkbhkdftZCOGmpUKFjL5w5nUdd26Nd58YrLVZCoT8NKhxpWFQZDZD"
curl -i -X POST \
https://graph.facebook.com/v22.0/838757782652201/messages \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json' \
-d '{ "messaging_product": "whatsapp", "to": "6289505046093", "type": "template", "template": { "name": "hello_world", "language": { "code": "en_US" } } }'
curl 'https://graph.facebook.com/v19.0/$MEDIA_ID?phone_number_id=$BUSINESS_PHONE_NUMBER_ID' \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json'

24
xx.ts Normal file
View File

@@ -0,0 +1,24 @@
import fetch from "node-fetch";
import fs from "fs";
const token = "EAALP22EWyC4BPrjshjjYBbPVKWp4Gp2ljkb7hCmgpZArLigB8XNmRoXBomDJm6aWnjpKpqehdVatbfFAHeGaQftGkNBp4Oyds9apr4lOQjG2YWYEzZC05ZAo7MARnfXn7FVua0iaeNMh2gunMZBd6pO58wjAUP3gqLiUrwASeOnJu5pW3tKg6fHubALBlQZDZD"; // dari Meta Developer > App > Access Token
const mediaId = "838467435201133"; // dari webhook
// 1. Dapatkan URL file asli
const mediaInfo = await fetch(
`https://graph.facebook.com/v19.0/${mediaId}?access_token=${token}`
).then(res => res.json()) as any;
// mediaInfo.url berisi link unduhan sementara
const fileUrl = mediaInfo.url;
const fileResponse = await fetch(fileUrl, {
headers: {
Authorization: `Bearer ${token}`, // wajib!
},
});
const buffer = await fileResponse.arrayBuffer();
fs.writeFileSync("sticker.webp", Buffer.from(buffer));
console.log("Sticker berhasil diunduh!");