mirror of
https://github.com/rajnandan1/kener.git
synced 2026-02-14 12:48:48 -06:00
feat: pre release 3.0.0
This commit is contained in:
@@ -99,8 +99,8 @@ section {
|
||||
}
|
||||
/*Needed overlay content on top of dotted bg*/
|
||||
.blurry-bg {
|
||||
background-color: var(--background-kener-rgba);
|
||||
box-shadow: 0 0 64px 64px var(--background-kener-rgba);
|
||||
/* background-color: var(--background-kener-rgba);
|
||||
box-shadow: 0 0 64px 64px var(--background-kener-rgba); */
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="px-8">
|
||||
<div class="">
|
||||
<div class="scroll-m-20 text-xl font-medium tracking-tight">
|
||||
{#if variant.includes("monitor")}
|
||||
{monitor.name} -
|
||||
@@ -138,7 +138,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Title>
|
||||
<Card.Description class="-mt-4 px-8 text-xs ">
|
||||
<Card.Description class="-mt-4 text-xs ">
|
||||
{moment(incidentCreatedAt * 1000).format("MMMM Do YYYY, h:mm:ss a")}
|
||||
|
||||
<p class="mt-0 flex gap-2 leading-8">
|
||||
@@ -167,7 +167,7 @@
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
{#if (variant.includes("body") || variant.includes("comments")) && state == "open"}
|
||||
<Card.Content class="px-14">
|
||||
<Card.Content class="px-7">
|
||||
{#if variant.includes("body")}
|
||||
<div
|
||||
class="prose prose-stone max-w-none text-sm dark:prose-invert prose-code:rounded prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm"
|
||||
|
||||
266
src/lib/components/manage/apiKeys.svelte
Normal file
266
src/lib/components/manage/apiKeys.svelte
Normal file
@@ -0,0 +1,266 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader, Copy, Check } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
|
||||
let apiKeys = [];
|
||||
let loaderLoadingAll = false;
|
||||
let loaderCreateNew = false;
|
||||
let newAPIKeyName = "";
|
||||
let newKeyResp = {};
|
||||
let showCreateModal = false;
|
||||
|
||||
async function loadAPIKeys() {
|
||||
loaderLoadingAll = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "getAPIKeys",
|
||||
data: {}
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
apiKeys = resp;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderLoadingAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createNew() {
|
||||
newKeyResp = {};
|
||||
loaderCreateNew = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "createNewApiKey",
|
||||
data: {
|
||||
name: newAPIKeyName
|
||||
}
|
||||
})
|
||||
});
|
||||
newKeyResp = await apiResp.json();
|
||||
loadAPIKeys();
|
||||
showCreateModal = false;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderCreateNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadAPIKeys();
|
||||
});
|
||||
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKeyResp.apiKey);
|
||||
}
|
||||
|
||||
function updateStatus(apiKey) {
|
||||
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
fetch(base + "/manage/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "updateApiKeyStatus",
|
||||
data: {
|
||||
id: apiKey.id,
|
||||
status: apiKey.status
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<div>
|
||||
{#if loaderLoadingAll}
|
||||
<Loader class="mt-6 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
on:click={(e) => {
|
||||
showCreateModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" /> Create New API Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if !!newKeyResp && !!newKeyResp.apiKey}
|
||||
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
|
||||
<p class="font-medium">
|
||||
<picture class="mr-1 inline-block">
|
||||
<source
|
||||
srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp"
|
||||
type="image/webp"
|
||||
/>
|
||||
<img
|
||||
src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif"
|
||||
alt="🎉"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</picture>
|
||||
API Key Created
|
||||
</p>
|
||||
<p
|
||||
class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium"
|
||||
>
|
||||
{newKeyResp.apiKey}
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="copybtn absolute right-2 top-2 h-5 w-5 p-1"
|
||||
on:click={copyKey}
|
||||
>
|
||||
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
|
||||
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
|
||||
</Button>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your new API key has been created. It will be not shown again, so make sure to save it.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<div class="-m-1.5 overflow-x-auto">
|
||||
<div class="inline-block min-w-full p-1.5 align-middle">
|
||||
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Name</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Key</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Created At</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{#each apiKeys as apiKey}
|
||||
<tr>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{apiKey.name}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{apiKey.maskedKey.slice(-32)}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{moment(apiKey.createdAt).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={apiKey.status == "ACTIVE"}
|
||||
on:change={() => {
|
||||
updateStatus(apiKey);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
|
||||
></div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if showCreateModal}
|
||||
<div
|
||||
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
showCreateModal = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Create a new API Key</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
API keys are used to authenticate your requests to the API. They are unique to
|
||||
your account and should be kept secret.
|
||||
</p>
|
||||
<hr class="my-4" />
|
||||
<form on:submit|preventDefault={createNew}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label for="newAPIKeyName">Name</Label>
|
||||
<Input
|
||||
bind:value={newAPIKeyName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="newAPIKeyName"
|
||||
placeholder="eg. My API Key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
|
||||
Create
|
||||
{#if loaderCreateNew}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -10,6 +10,8 @@
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
|
||||
export let categories = [];
|
||||
export let colorDown = "#777";
|
||||
export let colorDegraded = "#777";
|
||||
let monitors = [];
|
||||
let status = "ACTIVE";
|
||||
let showAddMonitor = false;
|
||||
@@ -313,8 +315,11 @@
|
||||
<hr class="my-4" />
|
||||
{#each Object.entries(monitorTriggers) as [key, data]}
|
||||
<div class="flex justify-between">
|
||||
<h3 class="font-semibold">
|
||||
{data.triggerType}
|
||||
<h3
|
||||
class="font-semibold"
|
||||
style="color:{data.triggerType == 'DOWN' ? colorDown : colorDegraded};"
|
||||
>
|
||||
If Monitor {data.triggerType}
|
||||
</h3>
|
||||
<div>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
@@ -409,7 +414,9 @@
|
||||
/>
|
||||
</div>
|
||||
{#each triggers as trigger}
|
||||
<div class="col-span-1 mt-2">
|
||||
<div
|
||||
class="col-span-1 mt-2 overflow-hidden overflow-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<label class="cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -393,7 +393,7 @@
|
||||
<div class="rounded-md border bg-card p-2 text-xs">
|
||||
<p class="text-sm font-semibold">Email Trigger</p>
|
||||
<p class="text-xs">
|
||||
Kener used <a
|
||||
Kener uses <a
|
||||
href="https://resend.com/"
|
||||
class="text-blue-500"
|
||||
target="_blank">resend</a
|
||||
|
||||
@@ -67,7 +67,7 @@ async function createGHIncident(monitor, alert, commonData) {
|
||||
|
||||
payload.body = description;
|
||||
|
||||
let { title, body, githubLabels, error } = ParseIncidentPayload(payload);
|
||||
let { title, body, githubLabels, error } = await ParseIncidentPayload(payload);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ async function createGHIncident(monitor, alert, commonData) {
|
||||
githubLabels.push("auto");
|
||||
let resp = await CreateIssue(title, body, githubLabels);
|
||||
|
||||
return GHIssueToKenerIncident(resp);
|
||||
return await GHIssueToKenerIncident(resp);
|
||||
}
|
||||
|
||||
async function closeGHIncident(alert) {
|
||||
@@ -101,7 +101,7 @@ async function closeGHIncident(alert) {
|
||||
return;
|
||||
}
|
||||
await CloseIssue(incidentNumber);
|
||||
return GHIssueToKenerIncident(resp);
|
||||
return await GHIssueToKenerIncident(resp);
|
||||
}
|
||||
|
||||
//add comment to incident
|
||||
@@ -117,7 +117,8 @@ function createClosureComment(alert, commonJSON) {
|
||||
return comment;
|
||||
}
|
||||
|
||||
async function alerting(monitor) {
|
||||
async function alerting(m) {
|
||||
let monitor = await db.getMonitorByTag(m.tag);
|
||||
let siteData = await GetAllSiteData();
|
||||
const githubData = await GetGithubData();
|
||||
const triggers = await GetAllTriggers({
|
||||
@@ -157,6 +158,10 @@ async function alerting(monitor) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (trigger.triggerStatus !== "ACTIVE") {
|
||||
console.error(`Triggers ${triggerID} is not active`);
|
||||
continue;
|
||||
}
|
||||
const notificationClient = new notification(trigger, siteData, monitor);
|
||||
allMonitorClients.push(notificationClient);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import db from "../db/db.js";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
|
||||
const saltRounds = 10;
|
||||
const DUMMY_SECRET = "DUMMY_SECRET";
|
||||
@@ -254,7 +255,9 @@ export const VerifyPassword = async (plainTextPassword, hashedPassword) => {
|
||||
|
||||
export const GenerateToken = async (data) => {
|
||||
try {
|
||||
const token = jwt.sign(data, process.env.JWT_SECRET || DUMMY_SECRET, { expiresIn: "1y" });
|
||||
const token = jwt.sign(data, process.env.KENER_SECRET_KEY || DUMMY_SECRET, {
|
||||
expiresIn: "1y"
|
||||
});
|
||||
return token;
|
||||
} catch (err) {
|
||||
console.error("Error generating token:", err);
|
||||
@@ -264,11 +267,11 @@ export const GenerateToken = async (data) => {
|
||||
|
||||
export const VerifyToken = async (token) => {
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || DUMMY_SECRET);
|
||||
const decoded = jwt.verify(token, process.env.KENER_SECRET_KEY || DUMMY_SECRET);
|
||||
return decoded; // Returns the decoded payload if the token is valid
|
||||
} catch (err) {
|
||||
console.error("Error verifying token:", err);
|
||||
throw new Error("Invalid or expired token");
|
||||
return undefined; // Returns null if the token is invalid
|
||||
}
|
||||
};
|
||||
|
||||
@@ -278,3 +281,58 @@ export const GetAllAlertsPaginated = async (data) => {
|
||||
total: await db.getMonitorAlertsCount()
|
||||
};
|
||||
};
|
||||
function generateApiKey() {
|
||||
const prefix = "kener_";
|
||||
const randomKey = crypto.randomBytes(32).toString("hex"); // 64-character hexadecimal string
|
||||
return prefix + randomKey;
|
||||
}
|
||||
function createHash(apiKey) {
|
||||
return crypto
|
||||
.createHmac("sha256", process.env.KENER_SECRET_KEY || DUMMY_SECRET)
|
||||
.update(apiKey)
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
export const MaskString = (str) => {
|
||||
const len = str.length;
|
||||
const mask = "*";
|
||||
const masked = mask.repeat(len - 4) + str.substring(len - 4);
|
||||
return masked;
|
||||
};
|
||||
|
||||
export const CreateNewAPIKey = async (data) => {
|
||||
//generate a new key
|
||||
const apiKey = generateApiKey();
|
||||
const hashedKey = await createHash(apiKey);
|
||||
//insert into db
|
||||
await db.createNewApiKey({
|
||||
name: data.name,
|
||||
hashedKey: hashedKey,
|
||||
maskedKey: MaskString(apiKey)
|
||||
});
|
||||
|
||||
return {
|
||||
apiKey: apiKey,
|
||||
name: data.name
|
||||
};
|
||||
};
|
||||
|
||||
export const GetAllAPIKeys = async () => {
|
||||
return await db.getAllApiKeys();
|
||||
};
|
||||
|
||||
//update status of api key
|
||||
export const UpdateApiKeyStatus = async (data) => {
|
||||
return await db.updateApiKeyStatus(data);
|
||||
};
|
||||
|
||||
export const VerifyAPIKey = async (apiKey) => {
|
||||
const hashedKey = createHash(apiKey);
|
||||
// Check if the hash exists in the database
|
||||
const record = await db.getApiKeyByHashedKey(hashedKey);
|
||||
|
||||
if (!!record) {
|
||||
return record.status == "ACTIVE";
|
||||
} // Adjust this for your DB query
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -98,6 +98,17 @@ class Sqlite {
|
||||
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- create table ApiKeys
|
||||
CREATE TABLE IF NOT EXISTS ApiKeys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
hashedKey TEXT NOT NULL UNIQUE,
|
||||
maskedKey TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'ACTIVE',
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -455,6 +466,15 @@ class Sqlite {
|
||||
return stmt.all({ status: data.status });
|
||||
}
|
||||
|
||||
//get monitor by tag
|
||||
async getMonitorByTag(tag) {
|
||||
let stmt = this.db.prepare(`
|
||||
SELECT * FROM Monitors
|
||||
WHERE tag = @tag;
|
||||
`);
|
||||
return stmt.get({ tag });
|
||||
}
|
||||
|
||||
//insert alert
|
||||
async createNewTrigger(data) {
|
||||
let stmt = this.db.prepare(`
|
||||
@@ -524,6 +544,42 @@ class Sqlite {
|
||||
return stmt.run(data);
|
||||
}
|
||||
|
||||
//new api key
|
||||
async createNewApiKey(data) {
|
||||
let stmt = this.db.prepare(`
|
||||
INSERT INTO ApiKeys (name, hashedKey, maskedKey)
|
||||
VALUES (@name, @hashedKey, @maskedKey);
|
||||
`);
|
||||
return stmt.run(data);
|
||||
}
|
||||
|
||||
//update status of api key
|
||||
async updateApiKeyStatus(data) {
|
||||
let stmt = this.db.prepare(`
|
||||
UPDATE ApiKeys
|
||||
SET status = @status, updatedAt = CURRENT_TIMESTAMP
|
||||
WHERE id = @id;
|
||||
`);
|
||||
return stmt.run(data);
|
||||
}
|
||||
|
||||
//get key by hashedKey
|
||||
async getApiKeyByHashedKey(hashedKey) {
|
||||
let stmt = this.db.prepare(`
|
||||
SELECT * FROM ApiKeys
|
||||
WHERE hashedKey = @hashedKey;
|
||||
`);
|
||||
return stmt.get({ hashedKey });
|
||||
}
|
||||
|
||||
//get all api keys
|
||||
async getAllApiKeys() {
|
||||
let stmt = this.db.prepare(`
|
||||
SELECT * FROM ApiKeys order by id desc;
|
||||
`);
|
||||
return stmt.all();
|
||||
}
|
||||
|
||||
//close
|
||||
close() {
|
||||
this.db.close();
|
||||
|
||||
@@ -5,9 +5,8 @@ class Discord {
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
envSecrets;
|
||||
|
||||
constructor(url, siteData, monitorData, envSecrets) {
|
||||
constructor(url, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener"
|
||||
@@ -18,7 +17,6 @@ class Discord {
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
this.envSecrets = envSecrets;
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
@@ -36,7 +34,6 @@ class Discord {
|
||||
}
|
||||
return {
|
||||
username: this.siteData.siteName,
|
||||
avatar_url: logo,
|
||||
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
|
||||
embeds: [
|
||||
{
|
||||
@@ -67,8 +64,7 @@ class Discord {
|
||||
}
|
||||
],
|
||||
footer: {
|
||||
text: "Kener",
|
||||
icon_url: logo
|
||||
text: "Kener"
|
||||
},
|
||||
timestamp: data.timestamp
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import { monitorsStore } from "./stores/monitors.js";
|
||||
import { get } from "svelte/store";
|
||||
import {
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
@@ -14,32 +12,37 @@ const API_IP = process.env.API_IP;
|
||||
const API_IP_REGEX = process.env.API_IP_REGEX;
|
||||
import db from "./db/db.js";
|
||||
|
||||
const GetAllTags = function () {
|
||||
import { GetMonitors, VerifyAPIKey } from "./controllers/controller.js";
|
||||
|
||||
const GetAllTags = async function () {
|
||||
let tags = [];
|
||||
let monitors = [];
|
||||
try {
|
||||
monitors = get(monitorsStore);
|
||||
monitors = await GetMonitors({
|
||||
status: "ACTIVE"
|
||||
});
|
||||
tags = monitors.map((monitor) => monitor.tag);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
return tags;
|
||||
};
|
||||
const CheckIfValidTag = function (tag) {
|
||||
const CheckIfValidTag = async function (tag) {
|
||||
let tags = [];
|
||||
let monitors = [];
|
||||
try {
|
||||
monitors = get(monitorsStore);
|
||||
tags = monitors.map((monitor) => monitor.tag);
|
||||
if (tags.indexOf(tag) == -1) {
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
throw new Error("not a valid tag");
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
throw new Error("monitor is not active");
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auth = function (request) {
|
||||
const auth = async function (request) {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const authToken = authHeader?.replace("Bearer ", "");
|
||||
let ip = "";
|
||||
@@ -57,7 +60,7 @@ const auth = function (request) {
|
||||
} catch (err) {
|
||||
console.log("IP Not Found " + err.message);
|
||||
}
|
||||
if (authToken !== API_TOKEN) {
|
||||
if ((await VerifyAPIKey(authToken)) === false) {
|
||||
return new Error("invalid token");
|
||||
}
|
||||
if (API_IP !== undefined && ip != "") {
|
||||
@@ -108,13 +111,15 @@ const store = async function (data) {
|
||||
return { error: err.message, status: 400 };
|
||||
}
|
||||
//check if tag is valid
|
||||
if (!CheckIfValidTag(tag)) {
|
||||
return { error: "invalid tag", status: 400 };
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
return { error: "no monitor with tag found", status: 400 };
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
return { error: "monitor with the given tag is not active", status: 400 };
|
||||
}
|
||||
|
||||
//get the monitor object matching the tag
|
||||
let monitors = get(monitorsStore);
|
||||
const monitor = monitors.find((monitor) => monitor.tag === tag);
|
||||
|
||||
await db.insertData({
|
||||
monitorTag: tag,
|
||||
@@ -125,7 +130,7 @@ const store = async function (data) {
|
||||
});
|
||||
return { status: 200, message: "success at " + data.timestampInSeconds };
|
||||
};
|
||||
const GHIssueToKenerIncident = function (issue) {
|
||||
const GHIssueToKenerIncident = async function (issue) {
|
||||
if (!!!issue) {
|
||||
return null;
|
||||
}
|
||||
@@ -133,7 +138,7 @@ const GHIssueToKenerIncident = function (issue) {
|
||||
let issueLabels = issue.labels.map((label) => {
|
||||
return label.name;
|
||||
});
|
||||
let tagsAvailable = GetAllTags();
|
||||
let tagsAvailable = await GetAllTags();
|
||||
|
||||
//get common tags as array
|
||||
let commonTags = tagsAvailable.filter((tag) => issueLabels.includes(tag));
|
||||
@@ -174,7 +179,7 @@ const GHIssueToKenerIncident = function (issue) {
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
const ParseIncidentPayload = function (payload) {
|
||||
const ParseIncidentPayload = async function (payload) {
|
||||
let startDatetime = payload.startDatetime; //in utc seconds optional
|
||||
let endDatetime = payload.endDatetime; //in utc seconds optional
|
||||
let title = payload.title; //string required
|
||||
@@ -216,7 +221,7 @@ const ParseIncidentPayload = function (payload) {
|
||||
return { error: "Invalid impact" };
|
||||
}
|
||||
//check if tags are valid
|
||||
const allTags = GetAllTags();
|
||||
const allTags = await GetAllTags();
|
||||
if (tags.some((tag) => allTags.indexOf(tag) === -1)) {
|
||||
return { error: "Unknown tags" };
|
||||
}
|
||||
@@ -247,16 +252,23 @@ const ParseIncidentPayload = function (payload) {
|
||||
|
||||
return { title, body, githubLabels };
|
||||
};
|
||||
const GetMonitorStatusByTag = function (tag, timestamp) {
|
||||
if (!CheckIfValidTag(tag)) {
|
||||
return { error: "invalid tag", status: 400 };
|
||||
const GetMonitorStatusByTag = async function (tag, timestamp) {
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
return { error: "no monitor with tag found", status: 400 };
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
return { error: "monitor with the given tag is not active", status: 400 };
|
||||
}
|
||||
const resp = {
|
||||
status: null,
|
||||
uptime: null,
|
||||
lastUpdatedAt: null
|
||||
};
|
||||
let monitors = get(monitorsStore);
|
||||
let monitors = await GetMonitors({
|
||||
status: "ACTIVE"
|
||||
});
|
||||
|
||||
const { includeDegradedInDowntime } = monitors.find((monitor) => monitor.tag === tag);
|
||||
|
||||
let now = GetMinuteStartNowTimestampUTC();
|
||||
@@ -265,7 +277,7 @@ const GetMonitorStatusByTag = function (tag, timestamp) {
|
||||
}
|
||||
let start = GetDayStartTimestampUTC(now);
|
||||
|
||||
let dayDataNew = db.getData(tag, start, now);
|
||||
let dayDataNew = await db.getData(tag, start, now);
|
||||
let ups = 0;
|
||||
let downs = 0;
|
||||
let degradeds = 0;
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
.tabs [data-state="active"] {
|
||||
border-color: orange;
|
||||
}
|
||||
.copybtn .copy-btn {
|
||||
transform: scale(1);
|
||||
}
|
||||
.copybtn .check-btn {
|
||||
transform: scale(0);
|
||||
}
|
||||
.copybtn:focus .copy-btn {
|
||||
transform: scale(0);
|
||||
}
|
||||
.copybtn:focus .check-btn {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
background-color: rgba(0, 0, 0, 0.02) !important;
|
||||
}
|
||||
.dark input,
|
||||
.dark textarea {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ export async function load({ params, route, url, cookies }) {
|
||||
let tokenData = cookies.get("kener-user");
|
||||
if (!!tokenData) {
|
||||
let tokenUser = await VerifyToken(tokenData);
|
||||
if (!!!tokenUser) {
|
||||
throw redirect(302, base + "/signin/logout");
|
||||
}
|
||||
let userDB = await db.getUserByEmail(tokenUser.email);
|
||||
if (!!userDB) {
|
||||
throw redirect(302, base + "/manage");
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CreateIssue, SearchIssue } from "$lib/server/github";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
@@ -16,7 +16,7 @@ export async function POST({ request }) {
|
||||
);
|
||||
}
|
||||
|
||||
let { title, body, githubLabels, error } = ParseIncidentPayload(payload);
|
||||
let { title, body, githubLabels, error } = await ParseIncidentPayload(payload);
|
||||
if (error) {
|
||||
return json(
|
||||
{ error },
|
||||
@@ -25,8 +25,6 @@ export async function POST({ request }) {
|
||||
}
|
||||
);
|
||||
}
|
||||
let site = get(siteStore);
|
||||
let github = site.github;
|
||||
githubLabels.push("manual");
|
||||
let resp = await CreateIssue(title, body, githubLabels);
|
||||
if (resp === null) {
|
||||
@@ -38,13 +36,13 @@ export async function POST({ request }) {
|
||||
);
|
||||
}
|
||||
|
||||
return json(GHIssueToKenerIncident(resp), {
|
||||
return json(await GHIssueToKenerIncident(resp), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
@@ -107,7 +105,9 @@ export async function GET({ request, url }) {
|
||||
}
|
||||
const resp = await SearchIssue(filterArray, page, per_page);
|
||||
|
||||
const incidents = resp.items.map((issue) => GHIssueToKenerIncident(issue));
|
||||
const incidents = await Promise.all(
|
||||
resp.items.map(async (issue) => await GHIssueToKenerIncident(issue))
|
||||
);
|
||||
|
||||
return json(incidents, {
|
||||
status: 200
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "$lib/server/github";
|
||||
|
||||
export async function PATCH({ request, params }) {
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
@@ -29,7 +29,7 @@ export async function PATCH({ request, params }) {
|
||||
}
|
||||
);
|
||||
}
|
||||
let { title, body, githubLabels, error } = ParseIncidentPayload(payload);
|
||||
let { title, body, githubLabels, error } = await ParseIncidentPayload(payload);
|
||||
if (error) {
|
||||
return json(
|
||||
{ error },
|
||||
@@ -48,13 +48,13 @@ export async function PATCH({ request, params }) {
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(GHIssueToKenerIncident(resp), {
|
||||
return json(await GHIssueToKenerIncident(resp), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
@@ -76,7 +76,7 @@ export async function GET({ request, params }) {
|
||||
);
|
||||
}
|
||||
|
||||
return json(GHIssueToKenerIncident(issue), {
|
||||
return json(await GHIssueToKenerIncident(issue), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { auth } from "$lib/server/webhook";
|
||||
import { AddComment, GetCommentsForIssue } from "$lib/server/github";
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
@@ -41,7 +41,7 @@ export async function GET({ request, params }) {
|
||||
}
|
||||
export async function POST({ request, params }) {
|
||||
// const headers = await request.headers();
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function POST({ request, params }) {
|
||||
const payload = await request.json();
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
// const headers = await request.headers();
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
@@ -82,7 +82,7 @@ export async function POST({ request, params }) {
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(GHIssueToKenerIncident(resp), {
|
||||
return json(await GHIssueToKenerIncident(resp), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { json } from "@sveltejs/kit";
|
||||
import { store, auth, GetMonitorStatusByTag } from "$lib/server/webhook";
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
@@ -19,7 +19,7 @@ export async function POST({ request }) {
|
||||
});
|
||||
}
|
||||
export async function GET({ request, url }) {
|
||||
const authError = auth(request);
|
||||
const authError = await auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
@@ -39,7 +39,7 @@ export async function GET({ request, url }) {
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(GetMonitorStatusByTag(tag, timestamp), {
|
||||
return json(await GetMonitorStatusByTag(tag, timestamp), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ export async function load({ params, route, url, cookies, request }) {
|
||||
|
||||
//get user by email
|
||||
let tokenUser = await VerifyToken(tokenData);
|
||||
if (!!!tokenUser) {
|
||||
//redirect to signin page if user is not authenticated
|
||||
throw redirect(302, base + "/signin/logout");
|
||||
}
|
||||
let userDB = await db.getUserByEmail(tokenUser.email);
|
||||
if (!!!userDB) {
|
||||
//redirect to signin page if user is not authenticated
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
} else {
|
||||
setMode("light");
|
||||
}
|
||||
|
||||
analyticsEvent("theme_change", {
|
||||
theme: $mode
|
||||
});
|
||||
}
|
||||
|
||||
setMode("dark");
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import MonitorsAdd from "$lib/components/manage/monitorsAdd.svelte";
|
||||
import TriggerInfo from "$lib/components/manage/triggerInfo.svelte";
|
||||
import AlertsInfo from "$lib/components/manage/alertsInfo.svelte";
|
||||
import APIKeys from "$lib/components/manage/apiKeys.svelte";
|
||||
import { Tabs } from "bits-ui";
|
||||
import { Plane } from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
@@ -84,6 +85,12 @@
|
||||
>
|
||||
Alerts
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="APIKeys"
|
||||
class="border-b-2 border-transparent px-2 pb-1 text-sm font-medium"
|
||||
>
|
||||
API Keys
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="SiteInfo" class="min-h-[70vh] pt-3">
|
||||
<SiteInfo {data} />
|
||||
@@ -101,7 +108,11 @@
|
||||
<ThemeInfo {data} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="MonitorsAdd" class="min-h-[70vh] pt-3">
|
||||
<MonitorsAdd categories={data.siteData?.categories} />
|
||||
<MonitorsAdd
|
||||
categories={data.siteData?.categories}
|
||||
colorDown={data.siteData?.colors.DOWN}
|
||||
colorDegraded={data.siteData?.colors.DEGRADED}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="TriggerInfo" class="min-h-[70vh] pt-3">
|
||||
<TriggerInfo />
|
||||
@@ -109,5 +120,8 @@
|
||||
<Tabs.Content value="AlertsInfo" class="min-h-[70vh] pt-3">
|
||||
<AlertsInfo {data} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="APIKeys" class="min-h-[70vh] pt-3">
|
||||
<APIKeys />
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
|
||||
@@ -8,12 +8,15 @@ import {
|
||||
CreateUpdateTrigger,
|
||||
GetAllTriggers,
|
||||
UpdateTriggerData,
|
||||
GetAllAlertsPaginated
|
||||
GetAllAlertsPaginated,
|
||||
GetAllAPIKeys,
|
||||
CreateNewAPIKey,
|
||||
UpdateApiKeyStatus
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
let action = payload.action;
|
||||
let data = payload.data;
|
||||
let data = payload.data || {};
|
||||
let resp = {};
|
||||
try {
|
||||
if (action === "storeSiteData") {
|
||||
@@ -30,6 +33,12 @@ export async function POST({ request }) {
|
||||
resp = await UpdateTriggerData(data);
|
||||
} else if (action == "getAllAlertsPaginated") {
|
||||
resp = await GetAllAlertsPaginated(data);
|
||||
} else if (action == "getAPIKeys") {
|
||||
resp = await GetAllAPIKeys();
|
||||
} else if (action == "createNewApiKey") {
|
||||
resp = await CreateNewAPIKey(data);
|
||||
} else if (action == "updateApiKeyStatus") {
|
||||
resp = await UpdateApiKeyStatus(data);
|
||||
}
|
||||
} catch (error) {
|
||||
resp = { error: error.message };
|
||||
|
||||
Reference in New Issue
Block a user