Saving initial state

This commit is contained in:
Luke Hagar
2024-10-20 04:01:48 +00:00
commit 03bc0aab7f
48 changed files with 3831 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

32
eslint.config.js Normal file
View File

@@ -0,0 +1,32 @@
import eslint from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
);

53
package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "hooky",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@ngrok/ngrok": "^1.4.1",
"@skeletonlabs/skeleton": "^2.10.2",
"@skeletonlabs/tw-plugin": "^0.4.0",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^9.6.0",
"@types/node": "^22.7.5",
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"highlight.js": "^11.10.0",
"paneforge": "^0.0.6",
"postcss": "^8.4.47",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"prisma": "^5.20.0",
"svelte": "^4.2.7",
"svelte-check": "^4.0.0",
"svelte-french-toast": "^1.2.0",
"svelte-highlight": "^7.7.0",
"svelte-persisted-store": "^0.11.0",
"tailwindcss": "^3.4.13",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3",
"vitest": "^2.0.0"
},
"type": "module",
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"dependencies": {
"@prisma/client": "5.20.0",
"lucide-svelte": "^0.453.0"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
prisma/dev.db Normal file

Binary file not shown.

BIN
prisma/dev.db-journal Normal file

Binary file not shown.

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"name" TEXT
);
-- CreateTable
CREATE TABLE "Webhook" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"content" TEXT,
"ownerId" INTEGER NOT NULL,
CONSTRAINT "Webhook_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

View File

@@ -0,0 +1,32 @@
/*
Warnings:
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the column `content` on the `Webhook` table. All the data in the column will be lost.
- You are about to drop the column `ownerId` on the `Webhook` table. All the data in the column will be lost.
*/
-- DropIndex
DROP INDEX "User_email_key";
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "User";
PRAGMA foreign_keys=on;
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Webhook" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"headers" TEXT,
"method" TEXT,
"path" TEXT,
"body" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_Webhook" ("id") SELECT "id" FROM "Webhook";
DROP TABLE "Webhook";
ALTER TABLE "new_Webhook" RENAME TO "Webhook";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Webhook" ADD COLUMN "query" TEXT;

View File

@@ -0,0 +1,27 @@
/*
Warnings:
- Made the column `body` on table `Webhook` required. This step will fail if there are existing NULL values in that column.
- Made the column `headers` on table `Webhook` required. This step will fail if there are existing NULL values in that column.
- Made the column `method` on table `Webhook` required. This step will fail if there are existing NULL values in that column.
- Made the column `path` on table `Webhook` required. This step will fail if there are existing NULL values in that column.
- Made the column `query` on table `Webhook` required. This step will fail if there are existing NULL values in that column.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Webhook" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"headers" TEXT NOT NULL,
"method" TEXT NOT NULL,
"path" TEXT NOT NULL,
"body" TEXT NOT NULL,
"query" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_Webhook" ("body", "createdAt", "headers", "id", "method", "path", "query") SELECT "body", "createdAt", "headers", "id", "method", "path", "query" FROM "Webhook";
DROP TABLE "Webhook";
ALTER TABLE "new_Webhook" RENAME TO "Webhook";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,7 @@
-- CreateTable
CREATE TABLE "RelayTarget" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"url" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -0,0 +1,22 @@
/*
Warnings:
- You are about to drop the column `query` on the `Webhook` table. All the data in the column will be lost.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Webhook" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"headers" TEXT NOT NULL,
"method" TEXT NOT NULL,
"path" TEXT NOT NULL,
"body" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_Webhook" ("body", "createdAt", "headers", "id", "method", "path") SELECT "body", "createdAt", "headers", "id", "method", "path" FROM "Webhook";
DROP TABLE "Webhook";
ALTER TABLE "new_Webhook" RENAME TO "Webhook";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,15 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_RelayTarget" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"url" TEXT NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_RelayTarget" ("createdAt", "id", "name", "url") SELECT "createdAt", "id", "name", "url" FROM "RelayTarget";
DROP TABLE "RelayTarget";
ALTER TABLE "new_RelayTarget" RENAME TO "RelayTarget";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

28
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,28 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Webhook {
id Int @id @default(autoincrement())
headers String
method String
path String
body String
createdAt DateTime @default(now())
}
model RelayTarget {
id Int @id @default(autoincrement())
name String
url String
active Boolean @default(true)
createdAt DateTime @default(now())
}

25
script.ts Normal file
View File

@@ -0,0 +1,25 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const user = await prisma.webhook.create({
data: {
headers: '{"Content-Type":"application/json"}',
method: 'POST',
path: '/',
body: '{"message":"Hello, world!"}'
}
})
console.log(user)
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})

3
src/app.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

57
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,57 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
/**
* Represents an error according to the RFC 7807 `application/problem+json` standard.
* @see https://datatracker.ietf.org/doc/html/rfc7807
*/
interface Error {
/**
* A URI reference that identifies the problem type.
* This URI should ideally point to human-readable documentation.
* @example "https://example.com/probs/out-of-stock"
*/
type?: string;
/**
* A short, human-readable summary of the problem.
* @example "Out of Stock"
*/
title?: string;
/**
* The HTTP status code associated with the problem.
* @example 404
*/
status?: number;
/**
* A detailed, human-readable explanation of the problem.
* @example "The requested item is no longer available."
*/
detail?: string;
/**
* A URI reference that identifies the specific occurrence of the problem.
* This can be used for tracking and debugging.
* @example "/products/12345"
*/
instance?: string;
/**
* Additional fields providing extra information about the problem.
* @remarks Use `unknown` to ensure type safety when accessing these fields.
*/
[key: string]: unknown;
}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en" dark>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="wintry">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
src/index.test.ts Normal file
View File

@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

46
src/lib/client/targets.ts Normal file
View File

@@ -0,0 +1,46 @@
import axios from 'axios';
import toast from 'svelte-french-toast';
export function deleteTarget(id: number) {
toast.promise(
axios.delete(`/targets/${id}`)
.then(response => {
if (response.status >= 200 && response.status < 300) {
return 'Target deleted successfully';
} else {
throw new Error(`Failed to delete Target: ${response.statusText}`);
}
})
.catch(error => {
throw new Error(`Failed to delete Target: ${error?.response?.data?.message || error.message}`);
}),
{
loading: 'Deleting Target...',
success: 'Target deleted successfully',
error: (error) => `Failed to delete Target: ${error.message}`
},
{ position: 'bottom-center' }
);
}
export function createTarget(name: string, url: string) {
toast.promise(
axios.post(`/targets`, { name, url })
.then(response => {
if (response.status >= 200 && response.status < 300) {
return 'Target created successfully';
} else {
throw new Error(response.statusText);
}
})
.catch(error => {
throw new Error(error?.response?.data?.message || error.message);
}),
{
loading: 'Creating Target...',
success: 'Target created successfully',
error: (error) => `Failed to create Target: ${error.message}`
},
{ position: 'bottom-center' }
);
}

View File

@@ -0,0 +1,89 @@
import axios from 'axios';
import type { Webhook } from '@prisma/client';
import toast from 'svelte-french-toast';
export async function checkForNewWebhooks() {
return await axios
.head('/webhooks')
.then((response) => {
if (response.status == 200) {
return true;
} else {
return false;
}
})
.catch((error) => {
throw new Error(error?.response?.data?.message || error.message);
});
}
export function getWebhooks() {
return toast.promise<Webhook[]>(
axios
.get('/webhooks')
.then((response) => {
if (response.status >= 200 && response.status < 300) {
return response.data;
} else {
throw new Error(response.statusText);
}
})
.catch((error) => {
throw new Error(error?.response?.data?.message || error.message);
}),
{
loading: 'Fetching webhooks...',
success: 'updated Webhooks',
error: (error) => `Failed to fetch webhooks: ${error.message}`
},
{ position: 'bottom-center' }
);
}
export function deleteWebhook(id: number) {
toast.promise(
axios
.delete(`/webhooks/${id}`)
.then((response) => {
if (response.status >= 200 && response.status < 300) {
return 'Webhook deleted successfully';
} else {
throw new Error(`Failed to delete webhook: ${response.statusText}`);
}
})
.catch((error) => {
throw new Error(
`Failed to delete webhook: ${error?.response?.data?.message || error.message}`
);
}),
{
loading: 'Deleting webhook...',
success: 'Webhook deleted successfully',
error: (error) => `Failed to delete webhook: ${error.message}`
},
{ position: 'bottom-center' }
);
}
export function sendWebhook(webhookTarget: string, id: number) {
toast.promise(
axios
.post(`/webhooks/${id}?webhookTarget=${webhookTarget}`)
.then((response) => {
if (response.status >= 200 && response.status < 300) {
return 'Webhook sent successfully';
} else {
throw new Error(response.statusText);
}
})
.catch((error) => {
throw new Error(error?.response?.data?.message || error.message);
}),
{
loading: 'Sending webhook...',
success: 'Webhook sent successfully',
error: (error) => `Failed to send webhook: ${error.message}`
},
{ position: 'bottom-center' }
);
}

View File

@@ -0,0 +1,123 @@
<script lang="ts">
import type { RelayTarget } from '@prisma/client';
import { createEventDispatcher, onMount } from 'svelte';
import { fade, fly } from 'svelte/transition';
export let target: RelayTarget | null = null;
const dispatch = createEventDispatcher();
let dialog: HTMLDialogElement;
let nickname = '';
let url = '';
let method = 'POST';
let headers = '';
let isActive = true;
$: isEditMode = !!target;
$: if (target) {
nickname = target.name;
url = target.url;
method = 'Post';
headers = `{"Content-Type": "application/json"}`;
isActive = true;
}
onMount(() => {
dialog.addEventListener('close', () => {
resetForm();
});
});
function openModal() {
dialog.showModal();
}
function handleSubmit() {
if (!target) {
return;
}
const parsedHeaders = JSON.parse(headers);
const payload = {
nickname,
url,
method,
headers: parsedHeaders,
isActive
};
if (isEditMode) {
dispatch('update', { id: target.id, ...payload });
} else {
dispatch('create', payload);
}
closeModal();
}
function closeModal() {
dialog.close();
}
function resetForm() {
if (!isEditMode) {
nickname = '';
url = '';
method = 'POST';
headers = '';
isActive = true;
}
}
</script>
<button on:click={openModal} class="btn variant-outline btn-sm">
{isEditMode ? 'Update' : 'Add'}
</button>
<div>
<dialog bind:this={dialog} class="card text-on-surface-token">
<div class="p-6" transition:fly={{ y: 20, duration: 300 }}>
<h2 class="text-2xl font-bold mb-4">
{isEditMode ? 'Update' : 'Create'} Webhook Relay Target
</h2>
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
<div>
<label for="nickname" class="label">Nickname</label>
<input class="input" type="text" id="nickname" bind:value={nickname} required />
</div>
<div>
<label for="url" class="label">URL</label>
<input type="url" id="url" bind:value={url} class="input" required />
</div>
<label class="flex items-center space-x-2">
<input class="checkbox" type="checkbox" checked />
<p>Active</p>
</label>
<div class="flex justify-end space-x-3">
<button
type="button"
on:click={closeModal}
class="px-4 py-2 border rounded-md text-sm font-medium hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Cancel
</button>
<button
on:click={handleSubmit}
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{isEditMode ? 'Update' : 'Create'}
</button>
</div>
</form>
</div>
</dialog>
</div>
<style>
dialog::backdrop {
background: rgba(0, 0, 0, 0.3);
}
</style>

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import Highlight from 'svelte-highlight';
import json from 'svelte-highlight/languages/json';
import { modeCurrent } from '@skeletonlabs/skeleton';
import { github, githubDark } from 'svelte-highlight/styles';
import type { LanguageType } from 'svelte-highlight/languages';
export let code = '';
export let language: LanguageType<string> = json
export let label = '';
let header: HTMLElement
$: console.log($modeCurrent);
</script>
<svelte:head>
{#if $modeCurrent == true}
{@html github}
{:else}
{@html githubDark}
{/if}
</svelte:head>
<div class="card w-full h-full overflow-hidden ">
<header bind:this={header} class=" flex justify-between items-center p-2 bg-surface-200-700-token">
<div class="flex items-center space-x-2 ">
{#if label}
<span class="badge variant-ghost">{label}</span>
{/if}
<!-- <span class="badge variant-ghost">{language.name.toUpperCase()}</span> -->
</div>
<button class="btn btn-sm variant-ghost" on:click={() => navigator.clipboard.writeText(code)}>
Copy
</button>
</header>
<section class="p-4 pb-14 h-full overflow-y-scroll overflow-x-auto">
<Highlight {language} code={code} />
<!-- <pre class="text-sm"><code class="hljs {language} font-mono">{@html highlighted}</code></pre> -->
</section>
</div>
<style>
/* Ensure Highlight.js styles don't conflict with Skeleton */
:global(.hljs) {
background: transparent !important;
padding: 0 !important;
}
</style>

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import type { Writable } from "svelte/store";
import AddUpdateModal from "./AddUpdateModal.svelte";
export let webhookTarget: Writable<string>
export let intervalInSeconds: Writable<number>
</script>
<div class="card p-2">
<!-- Form for custom webhook URL and interval -->
<div class="flex flex-row items-end space-x-2">
<div class="flex-1">
<label for="custom-url" class="sr-only">Custom Webhook URL</label>
<input
id="custom-url"
class="w-full input text-sm"
placeholder="Enter webhook URL"
name="webhookTarget"
bind:value={$webhookTarget}
/>
</div>
<AddUpdateModal/>
<!-- <select bind:value={$intervalInSeconds} class="select text-sm w-[100px]">
<option value={1}>1s</option>
<option value={10}>10s</option>
<option value={30}>30s</option>
<option value={60}>1m</option>
<option value={300}>5m</option>
<option value={600}>10m</option>
</select> -->
</div>
</div>

View File

@@ -0,0 +1,59 @@
<script lang="ts">
import CodeBlock from '$lib/components/CodeBlock.svelte';
import { RadioGroup, RadioItem } from '@skeletonlabs/skeleton';
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
import type { Webhook } from '@prisma/client';
import { type Writable } from 'svelte/store';
import { GripVertical, GripHorizontal } from 'lucide-svelte';
export let selectedWebhook: Writable<Webhook | undefined>;
let showHeaders = true;
let showBody = true;
let value: number = 0;
$: console.log(value);
</script>
{#if $selectedWebhook}
<div class="flex justify-end gap-2 items-center ">
<!-- <RadioGroup>
<RadioItem bind:group={value} name="justify" value={0}>(label)</RadioItem>
<RadioItem bind:group={value} name="justify" value={1}>(label)</RadioItem>
<RadioItem bind:group={value} name="justify" value={2}>(label)</RadioItem>
</RadioGroup>
<button class="btn btn-sm variant-outline" on:click={() => (showHeaders = !showHeaders)}>
{(showHeaders ? 'Hide' : 'Show') + ' Headers'}
</button>
<button class="btn btn-sm variant-outline" on:click={() => (showBody = !showBody)}>
{(showBody ? 'Hide' : 'Show') + ' Body'}
</button> -->
</div>
<PaneGroup direction="vertical" class="w-full rounded-lg" autoSaveId="inspectPane">
{#if showHeaders}
<Pane defaultSize={25} class="rounded-lg bg-muted">
<CodeBlock
label="Headers"
code={JSON.stringify(JSON.parse($selectedWebhook.headers), null, 2)}
/>
</Pane>
{/if}
{#if showHeaders && showBody}
<PaneResizer
class="relative flex h-2 my-4 items-center justify-center bg-surface-backdrop-token rounded-full"
>
<div
class="z-10 flex h-5 w-7 items-center justify-center rounded-sm border bg-primary-backdrop-token bg-primary-hover-token"
>
<GripHorizontal class="size-4 text-black" />
</div>
</PaneResizer>
{/if}
{#if showBody}
<Pane defaultSize={75} class="rounded-lg bg-muted">
<CodeBlock label="Body" code={JSON.stringify(JSON.parse($selectedWebhook.body), null, 2)} />
</Pane>
{/if}
</PaneGroup>
{/if}

3
src/lib/server/prisma.ts Normal file
View File

@@ -0,0 +1,3 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

32
src/lib/server/targets.ts Normal file
View File

@@ -0,0 +1,32 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function createTarget(name: string, url: string) {
return await prisma.relayTarget.create({
data: {
name,
url
}
});
}
export async function listTargets() {
return await prisma.relayTarget.findMany();
}
export async function deleteTarget(id: number) {
return await prisma.relayTarget.delete({
where: {
id
}
});
}
export async function getTarget(id: number) {
return await prisma.relayTarget.findUnique({
where: {
id
}
});
}

View File

@@ -0,0 +1,51 @@
import { writable } from "svelte/store";
import { prisma } from "./prisma";
export const newWebhooks = writable<boolean>(true);
export async function handleWebhook(req: Request) {
const headers = JSON.stringify(Object.fromEntries([...req.headers]));
const url = new URL(req.url);
let cleanedPath = url.pathname.replace('/ingest', '');
if (cleanedPath === '') {
cleanedPath = '/';
}
console.log(cleanedPath);
const webhook = await prisma.webhook.create({
data: {
headers,
method: req.method,
path: cleanedPath + url.search,
body: await req.text()
}
});
console.log(webhook);
newWebhooks.set(true);
}
export async function listWebhooks() {
return await prisma.webhook.findMany();
}
export async function deleteWebhook(id: number) {
return await prisma.webhook.delete({
where: {
id
}
});
}
export async function getWebhook(id: number) {
return await prisma.webhook.findUnique({
where: {
id
}
});
}
export async function clearWebhooks() {
return await prisma.webhook.deleteMany();
}

30
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,30 @@
<script lang="ts">
import { storeHighlightJs } from '@skeletonlabs/skeleton';
import hljs from 'highlight.js/lib/core';
import { Toaster } from 'svelte-french-toast';
import '../app.css';
// Import required language modules
import css from 'highlight.js/lib/languages/css';
import javascript from 'highlight.js/lib/languages/javascript';
import json from 'highlight.js/lib/languages/json';
import shell from 'highlight.js/lib/languages/shell';
import typescript from 'highlight.js/lib/languages/typescript';
import xml from 'highlight.js/lib/languages/xml';
// Register languages with highlight.js
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('css', css);
hljs.registerLanguage('json', json);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('shell', shell);
storeHighlightJs.set(hljs);
</script>
<Toaster />
<slot />

View File

@@ -0,0 +1,70 @@
import type { Actions } from './$types';
import { createTarget } from '$lib/server/targets';
import { getWebhook, sendWebhook, clearWebhooks } from '$lib/server/webhooks';
import { fail } from '@sveltejs/kit';
export const actions = {
create: async ({ request }) => {
const data = await request.formData();
const name = data.get('relayName');
const url = data.get('relayUrl');
if (!name) {
return fail(400, { message: 'Unable to add relay target: missing name' });
}
if (!url) {
return fail(400, { message: 'Unable to add relay target: missing url' });
}
try {
const target = await createTarget(name.toString(), url.toString());
return { success: true, type: 'relay', message: 'Relay target created successfully', target };
} catch (error) {
console.error(error);
return fail(500, { message: 'Failed to create target' });
}
},
clear: async () => {
await clearWebhooks();
return { success: true, type: 'webhook', message: 'Webhooks cleared successfully' };
},
send: async ({ request }) => {
const data = await request.formData();
const type = "webhook"
const webhookId = data.get('webhookId');
const webhookTarget = data.get('webhookTarget');
if (!webhookId) {
return fail(400, { type,error: 'Missing webhookId' });
}
if (!webhookTarget) {
return fail(400, { type,error: 'Missing webhookTarget' });
}
const webhook = await getWebhook(Number(webhookId));
if (!webhook) {
return fail(404, { type,error: 'Webhook with id ' + webhookId + ' not found' });
}
try {
const webhookResp = await sendWebhook(webhookTarget.toString(), webhook);
if (webhookResp.status === 200) {
return { success: true,type, message: 'Webhook sent successfully', webhook };
} else {
return fail(500, { type,error: 'Failed to send webhook' });
}
} catch (error) {
console.error(error);
return fail(500, { type,error: 'Failed to send webhook' });
}
}
} satisfies Actions;

131
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,131 @@
<script lang="ts">
import { browser } from '$app/environment';
import { deleteWebhook, getWebhooks, sendWebhook } from '$lib/client/webhooks';
import RelayControl from '$lib/components/RelayControl.svelte';
import WebhookInspect from '$lib/components/WebhookInspect.svelte';
import type { Webhook } from '@prisma/client';
import { AppBar, LightSwitch } from '@skeletonlabs/skeleton';
import { RefreshCw, Trash2 } from 'lucide-svelte';
import { onDestroy } from 'svelte';
import { persisted } from 'svelte-persisted-store';
import { writable, type Writable } from 'svelte/store';
import { fly } from 'svelte/transition';
const webhooks: Writable<Webhook[]> = writable([]);
let refreshInterval: NodeJS.Timeout;
const intervalInSeconds: Writable<number> = persisted('intervalInSeconds', 1);
const webhookTarget: Writable<string> = persisted('webhookTarget', '');
const selectedWebhook: Writable<Webhook | undefined> = persisted('selectedWebhook', undefined);
function createInterval(intervalInSeconds: number) {
clearInterval(refreshInterval);
if (browser) {
// Fetch webhooks on load, and on interval update
getWebhooks().then((data) => webhooks.set(data));
}
// Start the interval
refreshInterval = setInterval(async () => {
console.log('Refreshing webhooks');
webhooks.set(await (await fetch('/webhooks')).json());
}, 1000 * intervalInSeconds);
}
intervalInSeconds.subscribe((value) => createInterval(value));
webhooks.subscribe((webhooks) => {
if ($selectedWebhook == null && webhooks?.length > 0) {
selectedWebhook.set(webhooks[0]);
}
});
onDestroy(() => {
clearInterval(refreshInterval);
});
</script>
<div class="flex flex-col h-screen">
<AppBar padding="p-2">
<svelte:fragment slot="lead">Hooky</svelte:fragment>
<svelte:fragment slot="trail">
<LightSwitch />
</svelte:fragment>
</AppBar>
<div class="h-full overflow-hidden">
<div class="h-full flex gap-4 p-4">
<!-- Sidebar (Webhooks List) -->
<div class="w-1/3 flex flex-col space-y-2">
<RelayControl {webhookTarget} {intervalInSeconds} />
<div class="flex-1 overflow-hidden">
<div class="h-full overflow-y-auto px-1 space-y-2 custom-scrollbar">
{#each $webhooks as webhook (webhook.id)}
<button
class="relative overflow-hidden bg-white bg-opacity-80 backdrop-blur-sm card shadow cursor-pointer hover:shadow-md transition-shadow duration-200 w-full"
class:selected={$selectedWebhook?.id == webhook.id}
on:click|preventDefault={() => selectedWebhook.set(webhook)}
transition:fly|local={{ x: -200, duration: 200 }}
>
{#if $selectedWebhook?.id == webhook.id}
<!-- Active border indicator -->
<div
class="absolute inset-0 border-2 border-blue-500 rounded-container-token pointer-events-none"
/>
{/if}
<div class="p-2 relative">
<div class="flex justify-between items-center mb-1">
<div class="flex items-center space-x-2">
<span class="px-1 py-0.5 rounded-token text-xs font-mono bg-gray-200">
{webhook.method}
</span>
<span class="text-xs font-medium truncate" title={webhook.path}>
{webhook.path}
</span>
</div>
<span class="text-xs text-gray-500">
{new Date(webhook.createdAt).toLocaleString()}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-xs text-gray-500">
ID: <span class="font-mono" title={webhook.id.toString()}>
{webhook.id}
</span>
</span>
<span>
<button
type="submit"
on:click|stopPropagation={() => sendWebhook($webhookTarget, webhook.id)}
class="btn variant-outline btn-sm"
aria-label="Resend webhook"
>
<RefreshCw class="h-3 w-3 mr-1" />
Resend
</button>
<button
on:click|stopPropagation={() => deleteWebhook(webhook.id)}
type="submit"
formaction="?/delete?webhookId={webhook.id}"
class="btn variant-outline btn-sm"
aria-label="Delete webhook"
>
<Trash2 class="h-3 w-3 mr-1" />
Delete
</button>
</span>
</div>
</div>
</button>
{/each}
</div>
</div>
</div>
<!-- Main Content (Headers and Request Body) -->
<div class="w-2/3 flex flex-col space-y-2">
<WebhookInspect {selectedWebhook} />
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
import { handleWebhook } from '$lib/server/webhooks';
export async function POST({ request }) {
await handleWebhook(request);
//return a simple 200 response
return new Response(null, {
status: 200,
});
}

View File

@@ -0,0 +1,11 @@
import { handleWebhook } from '$lib/server/webhooks';
export async function POST({ request }) {
await handleWebhook(request);
//return a simple 200 response
return new Response(null, {
status: 200,
});
}

View File

@@ -0,0 +1,23 @@
import { createTarget, listTargets } from '$lib/server/targets';
import { error, json } from '@sveltejs/kit';
export async function GET() {
return json(await listTargets());
}
export async function POST({ request }) {
const data = await request.json();
if (!data.name) {
return error(400, { title: 'Missing name', message: 'Relay Target Name is required' });
}
if (!data.url) {
return error(400, { title: 'Missing url', message: 'Relay Target URL is required' });
}
await createTarget(data.name, data.url);
return new Response(null, {
status: 201
});
}

View File

@@ -0,0 +1,22 @@
import { deleteTarget, getTarget } from '$lib/server/targets';
import { error, json } from '@sveltejs/kit';
export async function DELETE({ params }) {
if (!params.id) {
return error(400, { message: 'Missing targetId' });
}
const Target = await getTarget(Number(params.id));
if (!Target) {
return error(404, { message: `Target with id ${params.id} not found` });
}
try {
await deleteTarget(Target.id);
return json({ message: 'Target deleted successfully' });
} catch (err) {
console.error(err);
return error(500, { message: `Failed to delete Target: ${err}` });
}
}

View File

@@ -0,0 +1,12 @@
import { listWebhooks, newWebhooks } from '$lib/server/webhooks';
import { json } from '@sveltejs/kit';
import { get } from 'svelte/store';
export async function HEAD() {
return get(newWebhooks) === true ? new Response(null, { status: 200 }) : new Response(null, { status: 204 });
}
export async function GET() {
newWebhooks.set(false);
return json(await listWebhooks());
}

View File

@@ -0,0 +1,61 @@
import { deleteWebhook, getWebhook } from '$lib/server/webhooks';
import { error, json } from '@sveltejs/kit';
import axios, { AxiosError } from 'axios';
export async function DELETE({ params }) {
if (!params.id) {
return error(400, { message: 'Missing webhookId' });
}
const webhook = await getWebhook(Number(params.id));
if (!webhook) {
return error(404, { message: `Webhook with id ${params.id} not found` });
}
try {
await deleteWebhook(webhook.id);
return json({ message: 'Webhook deleted successfully' });
} catch (err) {
console.error(err);
return error(500, { message: `Failed to delete webhook: ${err}` });
}
}
export async function POST({ params, url }) {
if (!params.id) {
return error(400, { message: 'Missing webhookId' });
}
const webhookTarget = url.searchParams.get('webhookTarget');
if (!webhookTarget) {
return error(400, { message: 'Missing webhookTarget' });
}
const webhook = await getWebhook(Number(params.id));
if (!webhook) {
return error(404, {
title: 'Webhook not found',
message: `Webhook with id ${params.id} not found`
});
}
try {
await axios.post(webhookTarget + webhook.path, webhook.body, {
headers: JSON.parse(webhook.headers)
});
return json({ message: 'Webhook sent successfully' });
} catch (err) {
const Error = err as Error | AxiosError;
if (axios.isAxiosError(Error)) {
return error(500, {
title: 'Failed to send Webhook',
message: Error.cause?.message || Error.message
});
} else {
return error(500, { title: 'Failed to send Webhook', message: Error.message });
}
}
}

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
svelte.config.js Normal file
View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

31
tailwind.config.ts Normal file
View File

@@ -0,0 +1,31 @@
import { join } from 'path';
import type { Config } from 'tailwindcss';
// 1. Import the Skeleton plugin
import { skeleton } from '@skeletonlabs/tw-plugin';
const config = {
// 2. Opt for dark mode to be handled via the class method
darkMode: 'class',
content: [
'./src/**/*.{html,js,svelte,ts}',
// 3. Append the path to the Skeleton package
join(require.resolve(
'@skeletonlabs/skeleton'),
'../**/*.{html,js,svelte,ts}'
)
],
theme: {
extend: {},
},
plugins: [
// 4. Append the Skeleton plugin (after other plugins)
skeleton({
themes: { preset: [ {name: "skeleton", enhancements: true},{name: "wintry", enhancements: true},{name: "hamlindigo", enhancements: true} ] }
})
]
} satisfies Config;
export default config;

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

9
vite.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});

2479
yarn.lock Normal file

File diff suppressed because it is too large Load Diff