Merge pull request #12 from TTTaevas/sql2

Use Bun's `SQL` over the `mongodb` package
This commit is contained in:
Taevas 2025-03-22 17:04:28 +01:00 committed by GitHub
commit 5e09b7ba77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 93 additions and 109 deletions

View file

@ -24,6 +24,7 @@ Set the environment variable IBM_TELEMETRY_DISABLED to true
This website makes use of several online APIs in order to deliver the `Infos` that are available on the right side of the main page. Accessing most of these APIs requires a key (or similar), which can be set through the following environment variables (with dotenv support for development):
- `URL_POSTGRESQL`
- `API_GITHUB`
- `API_GITLAB`
- `API_KITSUCLUB`
@ -32,4 +33,3 @@ This website makes use of several online APIs in order to deliver the `Infos` th
- `API_WANIKANI`
- `USERNAME_UMAMI`
- `PASSWORD_UMAMI`
- `URL_MONGODB`

View file

@ -1,17 +1,10 @@
import * as osu from "osu-api-v2-js";
import {type OsuInfo} from "#Infos/Gaming/Osu.tsx";
import {MongoClient} from "mongodb";
import {type Token} from "./token.tsx";
import type { Handler } from "../index.ts";
import { db, getToken } from "../database.ts";
export const gaming_osu: Handler = async (params) => {
const client = new MongoClient(process.env["URL_MONGODB"]!);
await client.connect();
const db = client.db("tokens");
const collection = db.collection<Token>("osu");
const token = await collection.findOne();
void client.close();
const token = await getToken(db, "osu");
let ruleset = params.has("ruleset") ? Number(params.get("ruleset")) : undefined;
if (ruleset && isNaN(ruleset)) {ruleset = undefined;}

View file

@ -1,95 +1,43 @@
import {MongoClient, type InsertOneResult} from "mongodb";
import { addToken, createTables, db, getToken, removeExpiredTokens } from "../database";
import {API} from "osu-api-v2-js";
import type { Handler } from "..";
const allowed_services = ["osu", "umami"];
export interface Token {
access_token: string;
expires: Date;
expires: number;
service: string;
}
export const token: Handler = async (params) => {
const service = params.get("service");
if (!service) {
if (!service || !allowed_services.includes(service)) {
return new Response("Bad Request", {status: 400});
}
const client = new MongoClient(process.env["URL_MONGODB"]!);
await client.connect();
const db = client.db("tokens");
const collection = db.collection<Token>(service);
const tokens = await collection.find().toArray();
const now = new Date();
const token = tokens.find((t) => t.expires > now);
const expiredTokens = tokens.filter((t) => now > t.expires);
const promises: Promise<void>[] = [];
await createTables(db);
removeExpiredTokens(db);
const token = await getToken(db, service);
if (!token) {
const collections = await db.listCollections().toArray();
if (!collections.find((c) => c.name === service)) {client.close(); return new Response("Not Found", {status: 404});}
if (service === "osu") {
const api = await API.createAsync(11451, process.env["API_OSU"]!);
await addToken(db, {access_token: api.access_token, service: "osu", expires: api.expires});
}
promises.push(new Promise(async (resolve, reject) => {
console.log(`Setting a new token for ${service}...`);
let insertion: InsertOneResult;
if (service === "osu") {
const api = await API.createAsync(11451, process.env["API_OSU"]!);
insertion = await collection.insertOne({
access_token: api.access_token,
expires: api.expires,
});
}
else if (service === "umami") {
const response = await fetch("https://visitors.taevas.xyz/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `username=${process.env["USERNAME_UMAMI"]}&password=${process.env["PASSWORD_UMAMI"]}`
});
const json: {token: string} = await response.json();
// Assume it expires in one day
const date = new Date();
date.setHours(date.getHours() + 24);
insertion = await collection.insertOne({
access_token: json.token,
expires: date,
});
}
else {
console.error(`Service "${service}" doesn't exist! Unable to set a new token...`);
return reject();
}
console.log(`New ${service} token in the database: ${insertion.insertedId.toString()}`);
resolve();
}));
if (service === "umami") {
const response = await fetch("https://visitors.taevas.xyz/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `username=${process.env["USERNAME_UMAMI"]}&password=${process.env["PASSWORD_UMAMI"]}`
});
const json: {token: string} = await response.json();
await addToken(db, {access_token: json.token, service: "umami"});
}
}
if (expiredTokens.length) {
promises.push(new Promise(async (resolve) => {
console.log(`Deleting old tokens for ${service}...`);
await Promise.all(expiredTokens.map(async (t) => {
return new Promise<void>(async (resolve) => {
const deletion = await collection.deleteOne({_id: t._id});
if (deletion.deletedCount) {
console.log(`Old ${service} token deleted from the database: ${t._id.toString()}`);
}
resolve();
});
}));
resolve();
}));
}
await Promise.all(promises);
void client.close();
return new Response(null, {status: 200});
};

View file

@ -1,16 +1,9 @@
import { MongoClient } from "mongodb";
import type { Handler } from "../index.ts";
import type { UmamiInfo } from "#Infos/Website/Umami.tsx";
import type { Token } from "./token.ts";
import { db, getToken } from "../database.ts";
export const website_umami: Handler = async () => {
const client = new MongoClient(process.env["URL_MONGODB"]!);
await client.connect();
const db = client.db("tokens");
const collection = db.collection<Token>("umami");
const token = await collection.findOne();
void client.close();
const token = await getToken(db, "umami");
const api_server = "https://visitors.taevas.xyz/api";
const website_id = "f196d626-e609-4841-9a80-0dc60f523ed5";

BIN
bun.lockb

Binary file not shown.

53
database.ts Normal file
View file

@ -0,0 +1,53 @@
import { SQL } from "bun";
import type { Token } from "./api/token";
export const db = new SQL({url: process.env["URL_POSTGRESQL"]});
export const createTables = async (database: SQL): Promise<void> => {
return await database.begin(sql => sql`
CREATE TABLE IF NOT EXISTS tokens (
access_token text NOT NULL,
service text NOT NULL,
expires bigserial NOT NULL
)
`);
};
export const removeExpiredTokens = async (database: SQL): Promise<number> => {
const now = new Date();
const deleted_tokens: Token[] = await database.begin(sql => sql`
DELETE FROM tokens
WHERE expires <= ${Number(now)}
RETURNING *
`);
deleted_tokens.forEach(token => console.log("(DATABASE)", token.service, "token had expired on", new Date(Number(token.expires)), "and has been removed!"));
return deleted_tokens.length;
};
export const addToken = async (database: SQL, token: {access_token: string, service: string, expires?: Date}): Promise<Token> => {
if (!token.expires) {
// Assume it expires in one day
token.expires = new Date();
token.expires.setHours(token.expires.getHours() + 24);
}
const returned: Token[] = await database.begin(sql => sql`
INSERT INTO tokens (access_token, service, expires)
VALUES (${token.access_token}, ${token.service}, ${Number(token.expires)})
RETURNING *
`);
returned.forEach(token => console.log("(DATABASE)", token.service, "token has been added"));
return returned[0];
};
export const getToken = async (database: SQL, service: string): Promise<Token> => {
const now = new Date();
const tokens: Token[] = await database.begin(sql => sql`
SELECT * FROM tokens
WHERE service = ${service}
AND expires > ${Number(now)}
`);
return tokens[Math.floor(Math.random() * tokens.length)];
};

View file

@ -51,7 +51,6 @@ const api_endpoints: Handler[] = [
const servers: Server[] = ports.map((port) => Bun.serve({
idleTimeout: 30,
// @ts-expect-error https://github.com/oven-sh/bun/issues/17772
tls: port !== 80 ? tls : undefined,
port,
fetch: async (req) => {
@ -106,4 +105,3 @@ const servers: Server[] = ports.map((port) => Bun.serve({
servers.forEach((server) => console.log(`Listening on ${server.hostname}:${server.port}`));
console.log("\n\n--------\n\n");

View file

@ -7,35 +7,34 @@
"start": "bun getready && bun run index.ts"
},
"dependencies": {
"@bachmacintosh/wanikani-api-types": "^1.7.0",
"@carbon/icons-react": "^11.56.0",
"@bachmacintosh/wanikani-api-types": "^1.8.0",
"@carbon/icons-react": "^11.57.0",
"@gitbeaker/rest": "^42.1.0",
"@octokit/rest": "^20.1.2",
"mongodb": "^6.14.2",
"osu-api-v2-js": "^1.1.1",
"osu-api-v2-js": "^1.1.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"timeago.js": "^4.0.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.22.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.23.0",
"@stylistic/eslint-plugin": "^3.1.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.0.13",
"@tailwindcss/postcss": "^4.0.15",
"@types/bun": "latest",
"@types/react": "^19.0.10",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"dotenv": "^16.4.7",
"eslint": "^9.22.0",
"eslint": "^9.23.0",
"eslint-config-xo-typescript": "^7.0.0",
"eslint-plugin-react": "^7.37.4",
"postcss": "^8.5.3",
"react-animate-height": "^3.2.3",
"tailwindcss": "^4.0.13",
"tailwindcss": "^4.0.15",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1",
"vite": "^6.2.1"
"typescript-eslint": "^8.27.0",
"vite": "^6.2.2"
},
"imports": {
"#Main/*": "./src/Main/*",