diff --git a/bun.lockb b/bun.lockb index a98a554..0a73e01 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f1aac22..5ff5a98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "legica-dana", - "version": "2.0.0", + "version": "2.0.1", "main": "src/app.ts", "scripts": { "start": "bun src/app.ts" @@ -8,12 +8,12 @@ "author": "Fran Jurmanović ", "license": "MIT", "dependencies": { + "@elysiajs/cron": "^0.7.0", "@elysiajs/static": "^0.7.1", "@elysiajs/swagger": "^0.7.3", "axios": "^0.26.0", "body-parser": "^1.20.2", "cheerio": "^1.0.0-rc.10", - "cron": "^3.0.0", "discord.js": "^12.5.1", "dotenv": "^8.2.0", "elysia": "^0.7.15", diff --git a/process-env.d.ts b/process-env.d.ts index b614818..f2489d7 100644 --- a/process-env.d.ts +++ b/process-env.d.ts @@ -5,6 +5,7 @@ declare global { PORT: string; CRON_LEGICA: string; PASSWORD: string; + TIMEZONE: string; } } } diff --git a/src/app.ts b/src/app.ts index ef9948b..c0380d9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,12 +1,12 @@ import { Client } from "discord.js"; import { config } from "@constants"; -import { CronJob } from "cron"; import { sendDiscordMessage, sendNextMessage } from "@common"; import { Elysia, t } from "elysia"; import { swagger } from "@elysiajs/swagger"; -import { basicAuth } from "@core"; +import { basicAuth, BasicAuthError } from "@core"; import pino from "pino"; import staticPlugin from "@elysiajs/static"; +import cron from "@elysiajs/cron"; const client: Client = new Client(); @@ -23,27 +23,39 @@ const logger = pino( ); const taskPlugin = new Elysia({ prefix: "/job" }) - .state("job", null as CronJob | null) - .onStart(({ store }) => { - client.on("ready", (): void => { - if (store.job) { - store.job.stop(); - } - store.job = new CronJob( - config.CRON_LEGICA, - () => sendNextMessage(client), - null, - true, - "utc" - ); - }); - }) - .onBeforeHandle(({ store: { job }, set }) => { - if (!job) { - set.status = 400; - return "Job is not running."; + .use( + cron({ + name: "job", + pattern: config.CRON_LEGICA, + run: () => sendNextMessage(client), + paused: true, + timezone: config.TIMEZONE, + }) + ) + .onStart( + ({ + store: { + cron: { job }, + }, + }) => { + client.on("ready", (): void => { + job.resume(); + }); } - }) + ) + .onBeforeHandle( + ({ + store: { + cron: { job }, + }, + set, + }) => { + if (job.isStopped()) { + set.status = 400; + return "Job is not running."; + } + } + ) .use( basicAuth({ users: [ @@ -55,26 +67,65 @@ const taskPlugin = new Elysia({ prefix: "/job" }) errorMessage: "Unauthorized", }) ) - .get("/", ({ store: { job } }) => ({ - running: job?.running ?? false, - next: job?.nextDate().toISO(), - })) - .post("/", ({ store: { job }, set }) => { - if (job?.running) { - set.status = 400; - return "Task already running"; + .get( + "/", + ({ + store: { + cron: { job }, + }, + }) => ({ + running: job.isRunning() ?? false, + stopped: job.isStopped() ?? false, + next: job.nextRun()?.toISOString(), + }), + { + detail: { + summary: "Get CRON job status", + }, } - job?.start(); - return "Task started"; - }) - .delete("/", ({ store: { job }, set }) => { - if (!job?.running) { - set.status = 400; - return "Task already stopped"; + ) + .post( + "/", + ({ + store: { + cron: { job }, + }, + 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( "/send", async ({ set, body }) => { @@ -95,18 +146,42 @@ const taskPlugin = new Elysia({ prefix: "/job" }) body: t.Object({ url: t.String(), }), + detail: { + summary: "Send legica-dana post to discord channels", + }, } ) - .get("/log", () => Bun.file("app.log")); - -client.login(config.TOKEN); - + .get("/log", () => Bun.file("app.log"), { + detail: { + summary: "Get the error log", + }, + }); const app = new Elysia() - .onError(({ error }) => { - logger.error(error); - return new Response(error.toString()); + .error({ BASIC_AUTH_ERROR: BasicAuthError }) + .onError(({ error, code }) => { + 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( swagger({ documentation: { @@ -114,6 +189,14 @@ const app = new Elysia() title: "Legica Bot", version: config.APP_VERSION, }, + security: [ + { + type: ["basic"], + }, + ], + }, + swaggerOptions: { + withCredentials: true, }, }) ) @@ -121,6 +204,7 @@ const app = new Elysia() .use(taskPlugin) .listen(config.PORT); +client.login(config.TOKEN); console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` + `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}` ); diff --git a/src/constants/config.ts b/src/constants/config.ts index 93130aa..e807ab3 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -16,6 +16,7 @@ const config: ProjectConfig = { CRON_LEGICA: process.env.CRON_LEGICA || "0 9 * * *", APP_VERSION: version, LEGICA_URL: "https://sib.net.hr/legica-dana", + TIMEZONE: process.env.TIMEZONE || "utc", }; export { config }; diff --git a/src/core/basicAuth.ts b/src/core/basicAuth.ts index 3e34a63..e7456fb 100644 --- a/src/core/basicAuth.ts +++ b/src/core/basicAuth.ts @@ -22,7 +22,6 @@ export interface BasicAuthConfig { export const basicAuth = (config: BasicAuthConfig) => new Elysia({ name: "basic-auth", seed: config }) - .error({ BASIC_AUTH_ERROR: BasicAuthError }) .derive((ctx) => { const authorization = ctx.headers?.authorization; if (!authorization) return { basicAuth: { isAuthed: false, username: "" } }; @@ -40,19 +39,8 @@ export const basicAuth = (config: BasicAuthConfig) => !isPathExcluded(ctx.path, config.exclude) && ctx.request && ctx.request.method !== "OPTIONS" - ) + ) { 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}"` : "" - }`, - }, - }); } }); diff --git a/src/core/index.ts b/src/core/index.ts index 06d786c..bad3eba 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1 +1 @@ -export { basicAuth } from "./basicAuth"; +export { basicAuth, BasicAuthError } from "./basicAuth";