fix error handling and more swagger info

This commit is contained in:
Fran Jurmanović
2023-10-05 19:06:01 +02:00
parent 71c9d1635b
commit d3dd15350c
7 changed files with 139 additions and 65 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{ {
"name": "legica-dana", "name": "legica-dana",
"version": "2.0.0", "version": "2.0.1",
"main": "src/app.ts", "main": "src/app.ts",
"scripts": { "scripts": {
"start": "bun src/app.ts" "start": "bun src/app.ts"
@@ -8,12 +8,12 @@
"author": "Fran Jurmanović <fjurma12@outlook.com>", "author": "Fran Jurmanović <fjurma12@outlook.com>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@elysiajs/cron": "^0.7.0",
"@elysiajs/static": "^0.7.1", "@elysiajs/static": "^0.7.1",
"@elysiajs/swagger": "^0.7.3", "@elysiajs/swagger": "^0.7.3",
"axios": "^0.26.0", "axios": "^0.26.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cheerio": "^1.0.0-rc.10", "cheerio": "^1.0.0-rc.10",
"cron": "^3.0.0",
"discord.js": "^12.5.1", "discord.js": "^12.5.1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"elysia": "^0.7.15", "elysia": "^0.7.15",

1
process-env.d.ts vendored
View File

@@ -5,6 +5,7 @@ declare global {
PORT: string; PORT: string;
CRON_LEGICA: string; CRON_LEGICA: string;
PASSWORD: string; PASSWORD: string;
TIMEZONE: string;
} }
} }
} }

View File

@@ -1,12 +1,12 @@
import { Client } from "discord.js"; import { Client } from "discord.js";
import { config } from "@constants"; import { config } from "@constants";
import { CronJob } from "cron";
import { sendDiscordMessage, sendNextMessage } from "@common"; import { sendDiscordMessage, sendNextMessage } from "@common";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { swagger } from "@elysiajs/swagger"; import { swagger } from "@elysiajs/swagger";
import { basicAuth } from "@core"; import { basicAuth, BasicAuthError } from "@core";
import pino from "pino"; import pino from "pino";
import staticPlugin from "@elysiajs/static"; import staticPlugin from "@elysiajs/static";
import cron from "@elysiajs/cron";
const client: Client = new Client(); const client: Client = new Client();
@@ -23,27 +23,39 @@ const logger = pino(
); );
const taskPlugin = new Elysia({ prefix: "/job" }) const taskPlugin = new Elysia({ prefix: "/job" })
.state("job", null as CronJob | null) .use(
.onStart(({ store }) => { cron({
client.on("ready", (): void => { name: "job",
if (store.job) { pattern: config.CRON_LEGICA,
store.job.stop(); run: () => sendNextMessage(client),
} paused: true,
store.job = new CronJob( timezone: config.TIMEZONE,
config.CRON_LEGICA, })
() => sendNextMessage(client), )
null, .onStart(
true, ({
"utc" store: {
); cron: { job },
}); },
}) }) => {
.onBeforeHandle(({ store: { job }, set }) => { client.on("ready", (): void => {
if (!job) { job.resume();
set.status = 400; });
return "Job is not running.";
} }
}) )
.onBeforeHandle(
({
store: {
cron: { job },
},
set,
}) => {
if (job.isStopped()) {
set.status = 400;
return "Job is not running.";
}
}
)
.use( .use(
basicAuth({ basicAuth({
users: [ users: [
@@ -55,26 +67,65 @@ const taskPlugin = new Elysia({ prefix: "/job" })
errorMessage: "Unauthorized", errorMessage: "Unauthorized",
}) })
) )
.get("/", ({ store: { job } }) => ({ .get(
running: job?.running ?? false, "/",
next: job?.nextDate().toISO(), ({
})) store: {
.post("/", ({ store: { job }, set }) => { cron: { job },
if (job?.running) { },
set.status = 400; }) => ({
return "Task already running"; running: job.isRunning() ?? false,
stopped: job.isStopped() ?? false,
next: job.nextRun()?.toISOString(),
}),
{
detail: {
summary: "Get CRON job status",
},
} }
job?.start(); )
return "Task started"; .post(
}) "/",
.delete("/", ({ store: { job }, set }) => { ({
if (!job?.running) { store: {
set.status = 400; cron: { job },
return "Task already stopped"; },
set,
}) => {
if (job.isRunning()) {
set.status = 400;
return "Job already running";
}
job.resume();
return "Job started";
},
{
detail: {
summary: "Start CRON job if it is not running",
},
} }
job?.stop(); )
return "Task stopped"; .delete(
}) "/",
({
store: {
cron: { job },
},
set,
}) => {
if (!job.isRunning()) {
set.status = 400;
return "Job already paused";
}
job.pause();
return "Job paused";
},
{
detail: {
summary: "Pause CRON job if it is not paused",
},
}
)
.post( .post(
"/send", "/send",
async ({ set, body }) => { async ({ set, body }) => {
@@ -95,18 +146,42 @@ const taskPlugin = new Elysia({ prefix: "/job" })
body: t.Object({ body: t.Object({
url: t.String(), url: t.String(),
}), }),
detail: {
summary: "Send legica-dana post to discord channels",
},
} }
) )
.get("/log", () => Bun.file("app.log")); .get("/log", () => Bun.file("app.log"), {
detail: {
client.login(config.TOKEN); summary: "Get the error log",
},
});
const app = new Elysia() const app = new Elysia()
.onError(({ error }) => { .error({ BASIC_AUTH_ERROR: BasicAuthError })
logger.error(error); .onError(({ error, code }) => {
return new Response(error.toString()); switch (code) {
case "BASIC_AUTH_ERROR":
return new Response(error.message, {
status: 401,
headers: {
"WWW-Authenticate": `Basic${
config.realm ? ` realm="${config.realm}"` : ""
}`,
},
});
case "NOT_FOUND":
return new Response(error.message, {
status: 404,
});
default:
logger.error(error);
}
})
.get("/", () => config.APP_VERSION, {
detail: {
summary: "Get current API version",
},
}) })
.get("/", () => config.APP_VERSION)
.use( .use(
swagger({ swagger({
documentation: { documentation: {
@@ -114,6 +189,14 @@ const app = new Elysia()
title: "Legica Bot", title: "Legica Bot",
version: config.APP_VERSION, version: config.APP_VERSION,
}, },
security: [
{
type: ["basic"],
},
],
},
swaggerOptions: {
withCredentials: true,
}, },
}) })
) )
@@ -121,6 +204,7 @@ const app = new Elysia()
.use(taskPlugin) .use(taskPlugin)
.listen(config.PORT); .listen(config.PORT);
client.login(config.TOKEN);
console.log( console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
); );

View File

@@ -16,6 +16,7 @@ const config: ProjectConfig = {
CRON_LEGICA: process.env.CRON_LEGICA || "0 9 * * *", CRON_LEGICA: process.env.CRON_LEGICA || "0 9 * * *",
APP_VERSION: version, APP_VERSION: version,
LEGICA_URL: "https://sib.net.hr/legica-dana", LEGICA_URL: "https://sib.net.hr/legica-dana",
TIMEZONE: process.env.TIMEZONE || "utc",
}; };
export { config }; export { config };

View File

@@ -22,7 +22,6 @@ export interface BasicAuthConfig {
export const basicAuth = (config: BasicAuthConfig) => export const basicAuth = (config: BasicAuthConfig) =>
new Elysia({ name: "basic-auth", seed: config }) new Elysia({ name: "basic-auth", seed: config })
.error({ BASIC_AUTH_ERROR: BasicAuthError })
.derive((ctx) => { .derive((ctx) => {
const authorization = ctx.headers?.authorization; const authorization = ctx.headers?.authorization;
if (!authorization) return { basicAuth: { isAuthed: false, username: "" } }; if (!authorization) return { basicAuth: { isAuthed: false, username: "" } };
@@ -40,19 +39,8 @@ export const basicAuth = (config: BasicAuthConfig) =>
!isPathExcluded(ctx.path, config.exclude) && !isPathExcluded(ctx.path, config.exclude) &&
ctx.request && ctx.request &&
ctx.request.method !== "OPTIONS" ctx.request.method !== "OPTIONS"
) ) {
throw new BasicAuthError(config.errorMessage ?? "Unauthorized"); throw new BasicAuthError(config.errorMessage ?? "Unauthorized");
})
.onError((ctx) => {
if (ctx.code === "BASIC_AUTH_ERROR") {
return new Response(ctx.error.message, {
status: 401,
headers: {
"WWW-Authenticate": `Basic${
config.realm ? ` realm="${config.realm}"` : ""
}`,
},
});
} }
}); });

View File

@@ -1 +1 @@
export { basicAuth } from "./basicAuth"; export { basicAuth, BasicAuthError } from "./basicAuth";