Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab17126b76 | ||
|
|
985ccf3e8a | ||
|
|
01866b302e | ||
|
|
def1757371 | ||
|
|
7e0c088fa7 | ||
|
|
43b4757696 | ||
|
|
3c6bdeab60 | ||
|
|
11a26bff83 | ||
|
|
e1cf854d27 | ||
|
|
972ee67f53 | ||
|
|
9ecfdef62b | ||
|
|
e56f0883cf | ||
|
|
5aa69588fb | ||
|
|
28b028d056 | ||
|
|
4ac04d4457 | ||
|
|
9d137b6b52 | ||
|
|
79110bdb1c | ||
|
|
0d2fba3883 | ||
|
|
77aa924161 | ||
|
|
b4b6f42a1a | ||
|
|
dd0b7cde36 | ||
|
|
162708812d | ||
|
|
71c9d1635b | ||
|
|
07f9b5c581 |
28
.env.example
Normal file
28
.env.example
Normal file
@@ -0,0 +1,28 @@
|
||||
# Discord Bot Configuration
|
||||
# -----------------------
|
||||
# Required: Your Discord bot token
|
||||
TOKEN=your_discord_bot_token_here
|
||||
|
||||
# API Configuration
|
||||
# -----------------------
|
||||
# Required: Password for admin API access
|
||||
PASSWORD=your_secure_password_here
|
||||
|
||||
# Web Server Settings
|
||||
# -----------------------
|
||||
# Port for the API server
|
||||
PORT=3000
|
||||
|
||||
# Scheduling
|
||||
# -----------------------
|
||||
# CRON schedule for when to post (default: every day at 9 AM)
|
||||
CRON_LEGICA=0 9 * * *
|
||||
# Timezone for the CRON job (e.g. 'Europe/Zagreb', 'America/New_York', etc.)
|
||||
TIMEZONE=utc
|
||||
|
||||
# Content Settings
|
||||
# -----------------------
|
||||
# Date format used in post titles
|
||||
LEGICA_DATE_FORMAT=D.M.YYYY
|
||||
# Number of hourly retry attempts if date check fails
|
||||
RETRY_ATTEMPTS=3
|
||||
29
README.md
29
README.md
@@ -1 +1,28 @@
|
||||
# monke-bot
|
||||
# Legica Bot
|
||||
|
||||
Discord bot that scrapes the https://sib.net.hr/legica-dana website and posts the latest legica-dana post to all discord text channels it has permissions to.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically posts new content from the website daily
|
||||
- Built-in retry mechanism if the post isn't available yet
|
||||
- Adds rating reactions (1-10) to each post
|
||||
- REST API for controlling the bot
|
||||
|
||||
## Configuration
|
||||
|
||||
The bot can be configured using environment variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| TOKEN | Discord Bot Token | Required |
|
||||
| PASSWORD | Admin password for API | Required |
|
||||
| PORT | Port for the API server | 3000 |
|
||||
| CRON_LEGICA | CRON schedule for posting | 0 9 * * * |
|
||||
| TIMEZONE | Timezone for the CRON job | utc |
|
||||
| LEGICA_DATE_FORMAT | Date format used in posts | D.M.YYYY |
|
||||
| RETRY_ATTEMPTS | Number of hourly retries if date check fails | 3 |
|
||||
|
||||
## Documentation
|
||||
|
||||
[Documentation](https://legica.jurmanovic.com/swagger)
|
||||
|
||||
11
dockerfile
11
dockerfile
@@ -1,17 +1,20 @@
|
||||
# Use oven/bun as parent image
|
||||
FROM oven/bun:latest
|
||||
FROM oven/bun:1.0.27
|
||||
|
||||
# Change the working directory on the Docker image to /app
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json to the /app directory
|
||||
COPY . .
|
||||
COPY package.json ./
|
||||
COPY bun.lockb ./
|
||||
COPY src ./src
|
||||
COPY tsconfig.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Expose application port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD bun start
|
||||
CMD ["bun", "start"]
|
||||
12
package.json
12
package.json
@@ -1,27 +1,33 @@
|
||||
{
|
||||
"name": "legica-dana",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.2",
|
||||
"main": "src/app.ts",
|
||||
"scripts": {
|
||||
"start": "bun src/app.ts"
|
||||
"start": "bun run src/app.ts",
|
||||
"build-script": "bun build src/app.ts --outdir dist --target bun",
|
||||
"start-build": "bun run dist/app.js"
|
||||
},
|
||||
"author": "Fran Jurmanović <fjurma12@outlook.com>",
|
||||
"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",
|
||||
"dayjs": "^1.11.10",
|
||||
"discord.js": "^12.5.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"elysia": "^0.7.15",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^9.0.3",
|
||||
"pino": "^8.15.4",
|
||||
"typescript": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/node": "^20.8.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
|
||||
3
process-env.d.ts
vendored
3
process-env.d.ts
vendored
@@ -5,6 +5,9 @@ declare global {
|
||||
PORT: string;
|
||||
CRON_LEGICA: string;
|
||||
PASSWORD: string;
|
||||
TIMEZONE: string;
|
||||
LEGICA_DATE_FORMAT: string;
|
||||
RETRY_ATTEMPTS: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
329
src/app.ts
329
src/app.ts
@@ -1,12 +1,11 @@
|
||||
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();
|
||||
|
||||
@@ -22,28 +21,25 @@ const logger = pino(
|
||||
fileTransport
|
||||
);
|
||||
|
||||
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"
|
||||
);
|
||||
async function jobRunner() {
|
||||
try {
|
||||
await sendNextMessage(client);
|
||||
} catch (err) {
|
||||
// Log detailed error information, including retry attempts
|
||||
if (err instanceof Error && err.message.includes("Failed after")) {
|
||||
logger.error({
|
||||
msg: "All retry attempts failed in job runner",
|
||||
error: err.message,
|
||||
});
|
||||
} else {
|
||||
logger.error({
|
||||
msg: "Error in job runner, no retries attempted",
|
||||
error: err,
|
||||
});
|
||||
})
|
||||
.onBeforeHandle(({ store: { job }, set }) => {
|
||||
if (!job) {
|
||||
set.status = 400;
|
||||
return "Job is not running.";
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const botPlugin = new Elysia({ prefix: "/bot" })
|
||||
.use(
|
||||
basicAuth({
|
||||
users: [
|
||||
@@ -55,58 +51,270 @@ 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(
|
||||
"/",
|
||||
() => ({
|
||||
uptime: client.uptime,
|
||||
readyAt: client.readyAt,
|
||||
readyTimestamp: client.readyTimestamp,
|
||||
}),
|
||||
{
|
||||
detail: {
|
||||
summary: "Get BOT status",
|
||||
},
|
||||
}
|
||||
job?.start();
|
||||
return "Task started";
|
||||
})
|
||||
.delete("/", ({ store: { job }, set }) => {
|
||||
if (!job?.running) {
|
||||
set.status = 400;
|
||||
return "Task already stopped";
|
||||
)
|
||||
.post(
|
||||
"/",
|
||||
() => {
|
||||
client.login(config.TOKEN);
|
||||
return "Bot logged in started";
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
summary: "Start BOT if it is not running",
|
||||
},
|
||||
}
|
||||
job?.stop();
|
||||
return "Task stopped";
|
||||
)
|
||||
.delete(
|
||||
"/",
|
||||
() => {
|
||||
client.destroy();
|
||||
return "Bot logged out";
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
summary: "Stops the BOT.",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const taskPlugin = new Elysia({ prefix: "/job" })
|
||||
.use(
|
||||
cron({
|
||||
name: "job",
|
||||
pattern: config.CRON_LEGICA,
|
||||
run: jobRunner,
|
||||
paused: true,
|
||||
timezone: config.TIMEZONE,
|
||||
})
|
||||
)
|
||||
.onStart(
|
||||
({
|
||||
store: {
|
||||
cron: { job },
|
||||
},
|
||||
}: {
|
||||
store: {
|
||||
cron: {
|
||||
job: {
|
||||
resume: () => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
}) => {
|
||||
client.on("ready", (): void => {
|
||||
job.resume();
|
||||
});
|
||||
}
|
||||
)
|
||||
.onBeforeHandle(
|
||||
({
|
||||
store: {
|
||||
cron: { job },
|
||||
},
|
||||
set,
|
||||
}: {
|
||||
store: {
|
||||
cron: {
|
||||
job: {
|
||||
isStopped: () => boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
set: {
|
||||
status: number;
|
||||
};
|
||||
}) => {
|
||||
if (job.isStopped()) {
|
||||
set.status = 400;
|
||||
return "Job is not running.";
|
||||
}
|
||||
}
|
||||
)
|
||||
.use(
|
||||
basicAuth({
|
||||
users: [
|
||||
{
|
||||
username: "admin",
|
||||
password: config.PASSWORD,
|
||||
},
|
||||
],
|
||||
errorMessage: "Unauthorized",
|
||||
})
|
||||
)
|
||||
.get(
|
||||
"/",
|
||||
({
|
||||
store: {
|
||||
cron: { job },
|
||||
},
|
||||
}: {
|
||||
store: {
|
||||
cron: {
|
||||
job: {
|
||||
isRunning: () => boolean;
|
||||
isStopped: () => boolean;
|
||||
nextRun: () => Date | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
}) => ({
|
||||
running: job.isRunning() ?? false,
|
||||
stopped: job.isStopped() ?? false,
|
||||
next: job.nextRun()?.toISOString(),
|
||||
}),
|
||||
{
|
||||
detail: {
|
||||
summary: "Get CRON job status",
|
||||
},
|
||||
}
|
||||
)
|
||||
.post(
|
||||
"/",
|
||||
({
|
||||
store: {
|
||||
cron: { job },
|
||||
},
|
||||
set,
|
||||
}: {
|
||||
store: {
|
||||
cron: {
|
||||
job: {
|
||||
isRunning: () => boolean;
|
||||
resume: () => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
set: {
|
||||
status: number;
|
||||
};
|
||||
}) => {
|
||||
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",
|
||||
},
|
||||
}
|
||||
)
|
||||
.delete(
|
||||
"/",
|
||||
({
|
||||
store: {
|
||||
cron: { job },
|
||||
},
|
||||
set,
|
||||
}: {
|
||||
store: {
|
||||
cron: {
|
||||
job: {
|
||||
isRunning: () => boolean;
|
||||
pause: () => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
set: {
|
||||
status: number;
|
||||
};
|
||||
}) => {
|
||||
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 }) => {
|
||||
async ({
|
||||
set,
|
||||
body,
|
||||
}: {
|
||||
set: {
|
||||
status: number;
|
||||
};
|
||||
body?: {
|
||||
url?: string;
|
||||
};
|
||||
}) => {
|
||||
try {
|
||||
const url = body.url;
|
||||
const url = body?.url;
|
||||
if (url) {
|
||||
await sendDiscordMessage(client, url);
|
||||
} else {
|
||||
await sendNextMessage(client);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
set.status = 400;
|
||||
return err;
|
||||
logger.error(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
url: t.String(),
|
||||
}),
|
||||
body: t.Optional(
|
||||
t.Object({
|
||||
url: t.Optional(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 }) => {
|
||||
.error({ BASIC_AUTH_ERROR: BasicAuthError })
|
||||
.onError(({ error, code }: { error: Error; code: string }) => {
|
||||
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);
|
||||
return new Response(error.toString());
|
||||
}
|
||||
})
|
||||
.get("/", () => config.APP_VERSION, {
|
||||
detail: {
|
||||
summary: "Get current API version",
|
||||
},
|
||||
})
|
||||
.get("/", () => config.APP_VERSION)
|
||||
.use(
|
||||
swagger({
|
||||
documentation: {
|
||||
@@ -114,13 +322,22 @@ const app = new Elysia()
|
||||
title: "Legica Bot",
|
||||
version: config.APP_VERSION,
|
||||
},
|
||||
security: [
|
||||
{
|
||||
type: ["basic"],
|
||||
},
|
||||
],
|
||||
},
|
||||
swaggerOptions: {
|
||||
withCredentials: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
.use(staticPlugin())
|
||||
.use(taskPlugin)
|
||||
.use(botPlugin)
|
||||
.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}`
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { config } from "@constants";
|
||||
import axios from "axios";
|
||||
import cheerio from "cheerio";
|
||||
|
||||
export async function getFirstHtml(): Promise<string> {
|
||||
const response = await axios.get("https://sib.net.hr/legica-dana");
|
||||
export async function getFirstHtml(): Promise<string | undefined> {
|
||||
const response = await axios.get(config.LEGICA_URL);
|
||||
const html = response.data;
|
||||
const $ = cheerio.load(html);
|
||||
const { href } = $(".News-link.c-def")?.attr() || {};
|
||||
const href = $(".legica-dana").first().find("a").attr("href");
|
||||
return href;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ export async function getImgTitle(href: string): Promise<Legica> {
|
||||
const html = response.data;
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const title = $(".Article-inner > h1").text();
|
||||
const { src: img } = $(".Article-media > img").attr() || {};
|
||||
const title = $(".article-title-container > h1").text();
|
||||
const src = $(".image-holder", ".article-content").find("img").attr("src");
|
||||
if (!src) throw new Error(`Image not found at ${href}.`);
|
||||
|
||||
return { title, img };
|
||||
return { title, img: src };
|
||||
}
|
||||
|
||||
@@ -1,14 +1,48 @@
|
||||
import { getFirstHtml, getImgTitle } from "@common";
|
||||
import { config } from "@constants";
|
||||
import dayjs from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import { Client, MessageEmbed, TextChannel } from "discord.js";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
/**
|
||||
* Retry mechanism for failed date checks
|
||||
*
|
||||
* This implementation allows the bot to retry fetching and posting content
|
||||
* when a date check fails. It will retry at hourly intervals for a number of attempts
|
||||
* specified by the RETRY_ATTEMPTS environment variable (defaults to 3).
|
||||
*
|
||||
* This is useful when:
|
||||
* 1. The website hasn't updated with today's post yet
|
||||
* 2. There are temporary network issues
|
||||
* 3. The website structure changed temporarily
|
||||
*/
|
||||
|
||||
// Sleep function to delay between retry attempts
|
||||
const sleep = (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export async function sendDiscordMessage(
|
||||
client: Client,
|
||||
url: string
|
||||
url: string,
|
||||
dateCheck?: dayjs.Dayjs
|
||||
): Promise<void> {
|
||||
if (!url) return;
|
||||
const { img, title } = await getImgTitle(url);
|
||||
|
||||
client.channels.cache.forEach(async (channel) => {
|
||||
if (dateCheck) {
|
||||
const dateRegex = /\d{1,2}.\d{1,2}.\d{4}/g;
|
||||
const date = dateRegex.exec(title)?.[0];
|
||||
const dayjsDate = dayjs(date, config.LEGICA_DATE_FORMAT);
|
||||
if (!dateCheck.isSame(dayjsDate, "D"))
|
||||
throw new Error(
|
||||
`Post failed date check, date from post ${date}, date checked ${dateCheck.format(
|
||||
config.LEGICA_DATE_FORMAT
|
||||
)}`
|
||||
);
|
||||
}
|
||||
try {
|
||||
const promises = client.channels.cache.map(async (channel) => {
|
||||
try {
|
||||
if (channel.type !== "text") return null;
|
||||
const embeddedMessage = new MessageEmbed().setTitle(title).setImage(img);
|
||||
@@ -32,17 +66,69 @@ export async function sendDiscordMessage(
|
||||
console.error(`Reaction ${reaction} to channel ${channel.id} failed.`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error(`Message to channel ${channel.id} failed.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendNextMessage(client: Client): Promise<void> {
|
||||
try {
|
||||
const href = await getFirstHtml();
|
||||
await sendDiscordMessage(client, href);
|
||||
await Promise.all(promises);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and sends the next legica-dana post to all Discord channels
|
||||
*
|
||||
* This function implements a retry mechanism that will:
|
||||
* 1. Try to fetch and post the latest content
|
||||
* 2. If it fails (especially due to date check), wait for 1 hour
|
||||
* 3. Retry up to the number of times specified in RETRY_ATTEMPTS env var
|
||||
*
|
||||
* @param client The Discord client used to send messages
|
||||
* @throws Error if all retry attempts fail
|
||||
*/
|
||||
export async function sendNextMessage(client: Client): Promise<void> {
|
||||
// Get max retry attempts from config
|
||||
const maxRetries = config.RETRY_ATTEMPTS;
|
||||
let attempts = 0;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
// Keep trying until we've reached max attempts
|
||||
while (attempts < maxRetries) {
|
||||
try {
|
||||
// Get the URL of the latest post
|
||||
const href = await getFirstHtml();
|
||||
if (!href) throw new Error("URL cannot be empty!");
|
||||
|
||||
// Try to send the message
|
||||
await sendDiscordMessage(client, href, dayjs());
|
||||
|
||||
// If successful, return
|
||||
return;
|
||||
} catch (error: unknown) {
|
||||
attempts++;
|
||||
const typedError = error instanceof Error ? error : new Error(String(error));
|
||||
lastError = typedError;
|
||||
|
||||
// Log the retry attempt
|
||||
console.error(
|
||||
`Attempt ${attempts}/${maxRetries} failed: ${typedError.message}`
|
||||
);
|
||||
|
||||
// If we've reached max attempts, throw the last error
|
||||
if (attempts >= maxRetries) {
|
||||
throw new Error(
|
||||
`Failed after ${attempts} attempts. Last error: ${
|
||||
lastError?.message || "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for 1 hour before retrying (3600000 ms)
|
||||
console.log(
|
||||
`Waiting 1 hour before retry attempt ${attempts + 1}/${maxRetries}...`
|
||||
);
|
||||
await sleep(3600000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ dotenv();
|
||||
type Config = {
|
||||
APP_VERSION: string;
|
||||
LEGICA_URL: string;
|
||||
RETRY_ATTEMPTS: number;
|
||||
};
|
||||
|
||||
export type ProjectConfig = Config & NodeJS.ProcessEnv;
|
||||
@@ -16,6 +17,9 @@ 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",
|
||||
LEGICA_DATE_FORMAT: process.env.LEGICA_DATE_FORMAT || "D.M.YYYY",
|
||||
RETRY_ATTEMPTS: parseInt(process.env.RETRY_ATTEMPTS || "3", 10),
|
||||
};
|
||||
|
||||
export { config };
|
||||
|
||||
@@ -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}"` : ""
|
||||
}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { basicAuth } from "./basicAuth";
|
||||
export { basicAuth, BasicAuthError } from "./basicAuth";
|
||||
|
||||
Reference in New Issue
Block a user