diff --git a/src/kener.css b/src/kener.css index 9f88244..7baab0e 100644 --- a/src/kener.css +++ b/src/kener.css @@ -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 { diff --git a/src/lib/components/incident.svelte b/src/lib/components/incident.svelte index d1461d6..9591bd7 100644 --- a/src/lib/components/incident.svelte +++ b/src/lib/components/incident.svelte @@ -107,7 +107,7 @@ /> {/if} -
@@ -167,7 +167,7 @@
+ + + + + API Key Created +
+ {newKeyResp.apiKey} + + + + + +
+ Your new API key has been created. It will be not shown again, so make sure to save it. +
+ API keys are used to authenticate your requests to the API. They are unique to + your account and should be kept secret. +
Email Trigger
- Kener used resend { 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; +}; diff --git a/src/lib/server/db/sqlite.js b/src/lib/server/db/sqlite.js index cef3624..32d5193 100644 --- a/src/lib/server/db/sqlite.js +++ b/src/lib/server/db/sqlite.js @@ -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(); diff --git a/src/lib/server/notification/discord.js b/src/lib/server/notification/discord.js index 0673d47..789daa2 100644 --- a/src/lib/server/notification/discord.js +++ b/src/lib/server/notification/discord.js @@ -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 } diff --git a/src/lib/server/webhook.js b/src/lib/server/webhook.js index bbf6385..c692cda 100644 --- a/src/lib/server/webhook.js +++ b/src/lib/server/webhook.js @@ -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; diff --git a/src/manage.css b/src/manage.css index 3b525c6..8624860 100644 --- a/src/manage.css +++ b/src/manage.css @@ -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; +} diff --git a/src/routes/(account)/signin/+page.server.js b/src/routes/(account)/signin/+page.server.js index c756868..883e339 100644 --- a/src/routes/(account)/signin/+page.server.js +++ b/src/routes/(account)/signin/+page.server.js @@ -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"); diff --git a/src/routes/(kener)/api/incident/+server.js b/src/routes/(kener)/api/incident/+server.js index d1c5067..2dcff43 100644 --- a/src/routes/(kener)/api/incident/+server.js +++ b/src/routes/(kener)/api/incident/+server.js @@ -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 diff --git a/src/routes/(kener)/api/incident/[incidentNumber]/+server.js b/src/routes/(kener)/api/incident/[incidentNumber]/+server.js index 4323c80..c126a43 100644 --- a/src/routes/(kener)/api/incident/[incidentNumber]/+server.js +++ b/src/routes/(kener)/api/incident/[incidentNumber]/+server.js @@ -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 }); } diff --git a/src/routes/(kener)/api/incident/[incidentNumber]/comment/+server.js b/src/routes/(kener)/api/incident/[incidentNumber]/comment/+server.js index d2b60f3..e1deeb2 100644 --- a/src/routes/(kener)/api/incident/[incidentNumber]/comment/+server.js +++ b/src/routes/(kener)/api/incident/[incidentNumber]/comment/+server.js @@ -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 }, diff --git a/src/routes/(kener)/api/incident/[incidentNumber]/status/+server.js b/src/routes/(kener)/api/incident/[incidentNumber]/status/+server.js index 767cb8d..1c3b6a1 100644 --- a/src/routes/(kener)/api/incident/[incidentNumber]/status/+server.js +++ b/src/routes/(kener)/api/incident/[incidentNumber]/status/+server.js @@ -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 }); } diff --git a/src/routes/(kener)/api/status/+server.js b/src/routes/(kener)/api/status/+server.js index 7640886..d2e71aa 100644 --- a/src/routes/(kener)/api/status/+server.js +++ b/src/routes/(kener)/api/status/+server.js @@ -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 }); } diff --git a/src/routes/(manage)/+layout.server.js b/src/routes/(manage)/+layout.server.js index 9fe9742..8419b00 100644 --- a/src/routes/(manage)/+layout.server.js +++ b/src/routes/(manage)/+layout.server.js @@ -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 diff --git a/src/routes/(manage)/+layout.svelte b/src/routes/(manage)/+layout.svelte index 3daf924..54a13a2 100644 --- a/src/routes/(manage)/+layout.svelte +++ b/src/routes/(manage)/+layout.svelte @@ -19,10 +19,6 @@ } else { setMode("light"); } - - analyticsEvent("theme_change", { - theme: $mode - }); } setMode("dark"); diff --git a/src/routes/(manage)/manage/+page.svelte b/src/routes/(manage)/manage/+page.svelte index 3b63aef..8e1de1c 100644 --- a/src/routes/(manage)/manage/+page.svelte +++ b/src/routes/(manage)/manage/+page.svelte @@ -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 + + API Keys + @@ -101,7 +108,11 @@ - + @@ -109,5 +120,8 @@ + + +