feat(standalone@2.0.17): switch to Bun SQL instead of Bun SQLite

This commit is contained in:
tiago
2025-11-12 18:46:18 +00:00
parent 28075efe7c
commit 490fda5c0f
8 changed files with 164 additions and 283 deletions
+5 -8
View File
@@ -51,14 +51,11 @@ If you're interested in an option to fully disable ratelimiting, let us know usi
If you would like the data to be stored in a custom path inside the container, you can set the `DATA_PATH` environment variable to the desired path. Note that this only works in Standalone 2.0.9 or above.
## Custom DB connectors
## Custom DB URLs
Cap Standalone uses SQLite as the default database for storing keys and challenges. If you'd like to use a different database, you can set the `DB_CONNECTOR` environment variable to the name of the database connector you want to use.
If you would like to use a different database, you can set the `DB_URL` environment variable to the URL of the database you want to use.
The current accepted values are:
We recommend keeping this to the default value, which is `sqlite://./.data/db.sqlite.`. However, using Postgres, MySQL, or any other database supported by Bun SQL might be possible, even though not officially supported or tested:
- `bun-sqlite` (default): Bun's built-in SQLite support
- `turso` (experimental): Uses [turso](https://turso.tech/) instead of SQLite.
Both can be exchanged at any time and data won't be lost, however Turso is experimental and doesn't support two or more processes using the same database at the same time (at least at the time of writing).
- `postgres://user:pass@localhost:5432/mydb`
- `mysql://user:password@localhost:3306/database`
+1 -13
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "cap-standalone",
@@ -8,7 +9,6 @@
"@elysiajs/cors": "^1.4.0",
"@elysiajs/static": "^1.4.4",
"@elysiajs/swagger": "^1.3.1",
"@tursodatabase/database": "^0.2.2",
"elysia": "^1.4.12",
"elysia-rate-limit": "4.4.0",
},
@@ -40,18 +40,6 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@tursodatabase/database": ["@tursodatabase/database@0.2.2", "", { "dependencies": { "@tursodatabase/database-common": "^0.2.2" }, "optionalDependencies": { "@tursodatabase/database-darwin-arm64": "0.2.2", "@tursodatabase/database-linux-arm64-gnu": "0.2.2", "@tursodatabase/database-linux-x64-gnu": "0.2.2", "@tursodatabase/database-win32-x64-msvc": "0.2.2" } }, "sha512-BlvoyiwIWIIQA65KEYlCqLDpRKIOM1AN99tBMjz1B+ydO9h0cDb+lCbfBf6f7+Lk696KB1ODpePiF5EfGm9qpw=="],
"@tursodatabase/database-common": ["@tursodatabase/database-common@0.2.2", "", {}, "sha512-cSNpms6MIaRj29B37XzIu9yGbea0HSGDupZs8QrMxR3rKqgHJIfY04ysqDD/4lS8TNIImPlPZrZgqQz+b/FoKQ=="],
"@tursodatabase/database-darwin-arm64": ["@tursodatabase/database-darwin-arm64@0.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9hfG9gncCnCU8fnYcw5w8iUheV8GVHBA1kS9VWyGxtFrlaajpNv3WVKM67BznacvtGhz35EpAGoG5ayxpEbQKA=="],
"@tursodatabase/database-linux-arm64-gnu": ["@tursodatabase/database-linux-arm64-gnu@0.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-u3VFXFzTZd+5RxxyQVClxMHVfj6aRxV0AbdoTLJ4flUao5YMebL4p5w30vozl8wNzlSq7g4ocmCiG71btofDGg=="],
"@tursodatabase/database-linux-x64-gnu": ["@tursodatabase/database-linux-x64-gnu@0.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-tAu9ZQBP3sZjaeQEOyEAQoHz/oqONBQwrtE/lYZQhqpR8nZ0DrKsYUIiu4lBOcWcofwtJmOgTVM8WYayJOm4HA=="],
"@tursodatabase/database-win32-x64-msvc": ["@tursodatabase/database-win32-x64-msvc@0.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-C1Z1xOUSln1/qtzR60xZYHgmfrQIhNm0glgqsnZt2wQI354dDtQGZH/oG/tKWCHrvtFvWCbnd9DEZif/VCK56A=="],
"@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="],
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "cap-standalone",
"version": "2.0.16",
"version": "2.0.17",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.js",
@@ -49,7 +49,6 @@
"@elysiajs/cors": "^1.4.0",
"@elysiajs/static": "^1.4.4",
"@elysiajs/swagger": "^1.3.1",
"@tursodatabase/database": "^0.2.2",
"elysia": "^1.4.12",
"elysia-rate-limit": "4.4.0"
},
+10 -13
View File
@@ -6,14 +6,6 @@ import { ratelimitGenerator } from "./ratelimit.js";
const { ADMIN_KEY } = process.env;
const loginQuery = db.prepare(`
INSERT INTO sessions (token, created, expires)
VALUES (?, ?, ?)
`);
const getValidTokenQuery = db.prepare(`
SELECT * FROM sessions WHERE token = ? AND expires > ? LIMIT 1
`);
if (!ADMIN_KEY) throw new Error("auth: Admin key missing. Please add one");
if (ADMIN_KEY.length < 30)
throw new Error(
@@ -66,7 +58,10 @@ export const auth = new Elysia({
const hashedToken = await Bun.password.hash(session_token);
loginQuery.run(hashedToken, created, expires);
await db`
INSERT INTO sessions (token, created, expires)
VALUES (${hashedToken}, ${created}, ${expires})
`;
cookie.cap_authed.set({
value: "yes",
@@ -92,9 +87,9 @@ export const authBeforeHandle = async ({ set, headers }) => {
return { success: false, error: "Unauthorized. Invalid bot token." };
}
const apiKey = await db
.prepare(`SELECT * FROM api_keys WHERE id = ?`)
.get(id);
const apiKey = await db`SELECT * FROM api_keys WHERE id = ${id}`.then(
(rows) => rows[0]
);
if (!apiKey || !apiKey.tokenHash) {
set.status = 401;
@@ -125,7 +120,9 @@ export const authBeforeHandle = async ({ set, headers }) => {
atob(authorization.replace("Bearer ", "").trim())
);
const validToken = await getValidTokenQuery.get(hash, Date.now());
const [validToken] = await db`
SELECT * FROM sessions WHERE token = ${hash} AND expires > ${Date.now()} LIMIT 1
`;
if (!validToken) {
set.status = 401;
+19 -38
View File
@@ -6,33 +6,6 @@ import { rateLimit } from "elysia-rate-limit";
import { db } from "./db.js";
import { ratelimitGenerator } from "./ratelimit.js";
const getSitekeyConfigQuery = db.prepare(
`SELECT (config) FROM keys WHERE siteKey = ?`,
);
const insertChallengeQuery = db.prepare(`
INSERT INTO challenges (siteKey, token, data, expires)
VALUES (?, ?, ?, ?)
`);
const getChallengeQuery = db.prepare(`
SELECT * FROM challenges WHERE siteKey = ? AND token = ?
`);
const deleteChallengeQuery = db.prepare(`
DELETE FROM challenges WHERE siteKey = ? AND token = ?
`);
const insertTokenQuery = db.prepare(`
INSERT INTO tokens (siteKey, token, expires)
VALUES (?, ?, ?)
`);
const upsertSolutionQuery = db.prepare(`
INSERT INTO solutions (siteKey, bucket, count)
VALUES (?, ?, 1)
ON CONFLICT (siteKey, bucket)
DO UPDATE SET count = count + 1
`);
export const capServer = new Elysia({
detail: {
tags: ["Challenges"],
@@ -56,7 +29,7 @@ export const capServer = new Elysia({
const cap = new Cap({
noFSState: true,
});
const _keyConfig = await getSitekeyConfigQuery.get(params.siteKey);
const [_keyConfig] = await db`SELECT (config) FROM keys WHERE siteKey = ${params.siteKey}`;
if (!_keyConfig) {
set.status = 404;
@@ -71,20 +44,20 @@ export const capServer = new Elysia({
challengeDifficulty: keyConfig.difficulty,
});
insertChallengeQuery.run(
params.siteKey,
challenge.token,
Object.values(challenge.challenge).join(","),
challenge.expires,
);
await db`
INSERT INTO challenges (siteKey, token, data, expires)
VALUES (${params.siteKey}, ${challenge.token}, ${Object.values(challenge.challenge).join(",")}, ${challenge.expires})
`;
return challenge;
})
.post("/:siteKey/redeem", async ({ body, set, params }) => {
const challenge = await getChallengeQuery.get(params.siteKey, body.token);
const [challenge] = await db`
SELECT * FROM challenges WHERE siteKey = ${params.siteKey} AND token = ${body.token}
`;
try {
deleteChallengeQuery.run(params.siteKey, body.token);
await db`DELETE FROM challenges WHERE siteKey = ${params.siteKey} AND token = ${body.token}`;
} catch {
set.status = 404;
return { error: "Challenge not found" };
@@ -118,11 +91,19 @@ export const capServer = new Elysia({
return { error: "Invalid solution" };
}
insertTokenQuery.run(params.siteKey, token, expires);
await db`
INSERT INTO tokens (siteKey, token, expires)
VALUES (${params.siteKey}, ${token}, ${expires})
`;
const now = Math.floor(Date.now() / 1000);
const hourlyBucket = Math.floor(now / 3600) * 3600;
upsertSolutionQuery.run(params.siteKey, hourlyBucket);
await db`
INSERT INTO solutions (siteKey, bucket, count)
VALUES (${params.siteKey}, ${hourlyBucket}, 1)
ON CONFLICT (siteKey, bucket)
DO UPDATE SET count = count + 1
`;
return {
success: true,
+52 -68
View File
@@ -1,5 +1,6 @@
import fs from "node:fs";
import { join } from "node:path";
import { SQL } from "bun";
fs.mkdirSync(process.env.DATA_PATH || "./.data", {
recursive: true,
@@ -8,85 +9,68 @@ fs.mkdirSync(process.env.DATA_PATH || "./.data", {
let db;
async function initDb() {
const connector = process.env.DB_CONNECTOR;
const dbUrl = process.env.DB_URL || `sqlite://${join(process.env.DATA_PATH || "./.data", "db.sqlite")}`;
if (!connector || connector === "bun-sqlite") {
const { Database } = await import("bun:sqlite");
db = new SQL(dbUrl);
db = new Database(join(process.env.DATA_PATH || "./.data", "db.sqlite"));
} else if (connector === "turso") {
const { connect } = await import("@tursodatabase/database");
await db`create table if not exists sessions (
token text primary key not null,
expires integer not null,
created integer not null
)`.simple();
db = await connect(join(process.env.DATA_PATH || "./.data", "db.sqlite"));
} else {
throw new Error(`Unsupported DB connector "${connector}"`);
}
await db`create table if not exists keys (
siteKey text primary key not null,
name text not null,
secretHash text not null,
config text not null,
created integer not null
)`.simple();
await db.exec(
`create table if not exists sessions (
token text primary key not null,
expires integer not null,
created integer not null
)`
);
await db`create table if not exists solutions (
siteKey text not null,
bucket integer not null,
count integer default 0,
primary key (siteKey, bucket)
)`.simple();
await db.exec(
`create table if not exists keys (
siteKey text primary key not null,
name text not null,
secretHash text not null,
config text not null,
created integer not null
)`
);
await db`create table if not exists challenges (
siteKey text not null,
token text not null,
data text not null,
expires integer not null,
primary key (siteKey, token)
)`.simple();
await db.exec(
`create table if not exists solutions (
siteKey text not null,
bucket integer not null,
count integer default 0,
primary key (siteKey, bucket)
)`
);
await db`create table if not exists tokens (
siteKey text not null,
token text not null,
expires integer not null,
primary key (siteKey, token)
)`.simple();
await db.exec(
`create table if not exists challenges (
siteKey text not null,
token text not null,
data text not null,
expires integer not null,
primary key (siteKey, token)
)`
);
await db`create table if not exists api_keys (
id text not null,
name text not null,
tokenHash text not null,
created integer not null,
primary key (id, tokenHash)
)`.simple();
await db.exec(
`create table if not exists tokens (
siteKey text not null,
token text not null,
expires integer not null,
primary key (siteKey, token)
)`
);
setInterval(async () => {
const now = Date.now();
await db.exec(
`create table if not exists api_keys (
id text not null,
name text not null,
tokenHash text not null,
created integer not null,
primary key (id, tokenHash)
)`
);
setInterval(() => {
db.prepare("delete from sessions where expires < ?").run(Date.now());
db.prepare("delete from tokens where expires < ?").run(Date.now());
db.prepare("delete from challenges where expires < ?").run(Date.now());
await db`delete from sessions where expires < ${now}`;
await db`delete from tokens where expires < ${now}`;
await db`delete from challenges where expires < ${now}`;
}, 60 * 1000);
db.prepare("delete from sessions where expires < ?").run(Date.now());
db.prepare("delete from tokens where expires < ?").run(Date.now());
db.prepare("delete from challenges where expires < ?").run(Date.now());
const now = Date.now();
await db`delete from sessions where expires < ${now}`;
await db`delete from tokens where expires < ${now}`;
await db`delete from challenges where expires < ${now}`;
return db;
}
+71 -126
View File
@@ -11,15 +11,6 @@ const keyDefaults = {
saltSize: 32,
};
const get24hSolvesQuery = db.prepare(
`SELECT SUM(count) as total FROM solutions WHERE siteKey = ? AND bucket >= ?`
);
const get24hPreviousQuery = db.prepare(`
SELECT SUM(count) as total FROM solutions
WHERE siteKey = ? AND bucket >= ? AND bucket < ?
`);
const getKeysQuery = db.prepare(`SELECT * FROM keys ORDER BY created DESC`);
export const server = new Elysia({
prefix: "/server",
detail: {
@@ -48,19 +39,17 @@ export const server = new Elysia({
const currentStart = now - day;
const previousStart = now - 2 * day;
const keys = await getKeysQuery.all();
const keys = await db`SELECT * FROM keys ORDER BY created DESC`;
return await Promise.all(
keys.map(async (key) => {
const currentResult = await get24hSolvesQuery.get(
key.sitekey || key.siteKey,
currentStart
);
const previousResult = await get24hPreviousQuery.get(
key.sitekey || key.siteKey,
previousStart,
currentStart
);
const [currentResult] = await db`
SELECT SUM(count) as total FROM solutions WHERE siteKey = ${key.sitekey || key.siteKey} AND bucket >= ${currentStart}
`;
const [previousResult] = await db`
SELECT SUM(count) as total FROM solutions
WHERE siteKey = ${key.sitekey || key.siteKey} AND bucket >= ${previousStart} AND bucket < ${currentStart}
`;
const current = currentResult?.total || 0;
const previous = previousResult?.total || 0;
@@ -106,15 +95,10 @@ export const server = new Elysia({
.replace(/\//g, "")
.replace(/=+$/, "");
db.prepare(
`INSERT INTO keys (siteKey, name, secretHash, config, created) VALUES (?, ?, ?, ?, ?)`
).run(
siteKey,
body?.name || siteKey,
await Bun.password.hash(secretKey),
JSON.stringify(keyDefaults),
Date.now()
);
await db`
INSERT INTO keys (siteKey, name, secretHash, config, created)
VALUES (${siteKey}, ${body?.name || siteKey}, ${await Bun.password.hash(secretKey)}, ${JSON.stringify(keyDefaults)}, ${Date.now()})
`;
return {
siteKey,
@@ -133,9 +117,7 @@ export const server = new Elysia({
.get(
"/keys/:siteKey",
async ({ params, query }) => {
const key = await db
.prepare(`SELECT * FROM keys WHERE siteKey = ?`)
.get(params.siteKey);
const [key] = await db`SELECT * FROM keys WHERE siteKey = ${params.siteKey}`;
if (!key) {
return { success: false, error: "Key not found" };
}
@@ -154,97 +136,73 @@ export const server = new Elysia({
case "today":
bucketSize = 3600;
startTime = Math.floor(Date.now() / 1000 / 86400) * 86400;
dataQuery = db.prepare(`
SELECT bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ? AND bucket >= ? AND bucket <= ?
GROUP BY bucket
ORDER BY bucket
`);
dataQuery = { type: "today" };
break;
case "yesterday":
bucketSize = 3600;
startTime = Math.floor(Date.now() / 1000 / 86400) * 86400 - 86400;
dataQuery = db.prepare(`
SELECT bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ? AND bucket >= ? AND bucket < ?
GROUP BY bucket
ORDER BY bucket
`);
dataQuery = { type: "yesterday" };
break;
case "last7days":
bucketSize = 86400;
startTime = Math.floor((now - 7 * 86400) / 86400) * 86400;
dataQuery = db.prepare(`
SELECT (bucket / 86400) * 86400 as bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ? AND bucket >= ?
GROUP BY (bucket / 86400) * 86400
ORDER BY bucket
`);
dataQuery = { type: "aggregate" };
break;
case "last28days":
bucketSize = 86400;
startTime = Math.floor((now - 28 * 86400) / 86400) * 86400;
dataQuery = db.prepare(`
SELECT (bucket / 86400) * 86400 as bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ? AND bucket >= ?
GROUP BY (bucket / 86400) * 86400
ORDER BY bucket
`);
dataQuery = { type: "aggregate" };
break;
case "last91days":
bucketSize = 86400;
startTime = Math.floor((now - 91 * 86400) / 86400) * 86400;
dataQuery = db.prepare(`
SELECT (bucket / 86400) * 86400 as bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ? AND bucket >= ?
GROUP BY (bucket / 86400) * 86400
ORDER BY bucket
`);
dataQuery = { type: "aggregate" };
break;
case "alltime":
bucketSize = 86400;
startTime = 0;
dataQuery = db.prepare(`
SELECT (bucket / 86400) * 86400 as bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ? AND bucket >= ?
GROUP BY (bucket / 86400) * 86400
ORDER BY bucket
`);
dataQuery = { type: "aggregate" };
break;
default:
bucketSize = 3600;
startTime = currentStart;
dataQuery = db.prepare(`
SELECT bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ? AND bucket >= ?
GROUP BY bucket
ORDER BY bucket
`);
dataQuery = { type: "default" };
}
let historyData;
if (chartDuration === "yesterday") {
historyData = await dataQuery.all(
params.siteKey,
startTime,
startTime + 86400
);
historyData = await db`
SELECT bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ${params.siteKey} AND bucket >= ${startTime} AND bucket < ${startTime + 86400}
GROUP BY bucket
ORDER BY bucket
`;
} else if (chartDuration === "today") {
const currentHourBucket = Math.floor(now / 3600) * 3600;
historyData = await dataQuery.all(
params.siteKey,
startTime,
currentHourBucket
);
historyData = await db`
SELECT bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ${params.siteKey} AND bucket >= ${startTime} AND bucket <= ${currentHourBucket}
GROUP BY bucket
ORDER BY bucket
`;
} else if (dataQuery.type === "aggregate") {
historyData = await db`
SELECT (bucket / 86400) * 86400 as bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ${params.siteKey} AND bucket >= ${startTime}
GROUP BY (bucket / 86400) * 86400
ORDER BY bucket
`;
} else {
historyData = await dataQuery.all(params.siteKey, startTime);
historyData = await db`
SELECT bucket, SUM(count) as count
FROM solutions
WHERE siteKey = ${params.siteKey} AND bucket >= ${startTime}
GROUP BY bucket
ORDER BY bucket
`;
}
if (
@@ -294,8 +252,10 @@ export const server = new Elysia({
historyData = completeData;
}
const currentSolves =
(await get24hSolvesQuery.get(params.siteKey, currentStart))?.total || 0;
const [currentSolvesResult] = await db`
SELECT SUM(count) as total FROM solutions WHERE siteKey = ${params.siteKey} AND bucket >= ${currentStart}
`;
const currentSolves = currentSolvesResult?.total || 0;
return {
key: {
@@ -338,9 +298,7 @@ export const server = new Elysia({
.put(
"/keys/:siteKey/config",
async ({ params, body }) => {
const key = await db
.prepare(`SELECT * FROM keys WHERE siteKey = ?`)
.get(params.siteKey);
const [key] = await db`SELECT * FROM keys WHERE siteKey = ${params.siteKey}`;
if (!key) {
return { success: false, error: "Key not found" };
}
@@ -354,11 +312,7 @@ export const server = new Elysia({
saltSize,
};
db.prepare(`UPDATE keys SET name = ?, config = ? WHERE siteKey = ?`).run(
config.name || key.name,
JSON.stringify(config),
params.siteKey
);
await db`UPDATE keys SET name = ${config.name || key.name}, config = ${JSON.stringify(config)} WHERE siteKey = ${params.siteKey}`;
return { success: true };
},
@@ -377,16 +331,14 @@ export const server = new Elysia({
.delete(
"/keys/:siteKey",
async ({ params, set }) => {
const key = await db
.prepare(`SELECT * FROM keys WHERE siteKey = ?`)
.get(params.siteKey);
const [key] = await db`SELECT * FROM keys WHERE siteKey = ${params.siteKey}`;
if (!key) {
set.status = 404;
return { success: false, error: "Key not found" };
}
db.prepare(`DELETE FROM keys WHERE siteKey = ?`).run(params.siteKey);
db.prepare(`DELETE FROM solutions WHERE siteKey = ?`).run(params.siteKey);
await db`DELETE FROM keys WHERE siteKey = ${params.siteKey}`;
await db`DELETE FROM solutions WHERE siteKey = ${params.siteKey}`;
return { success: true };
},
@@ -402,9 +354,7 @@ export const server = new Elysia({
.post(
"/keys/:siteKey/rotate-secret",
async ({ params }) => {
const key = await db
.prepare(`SELECT * FROM keys WHERE siteKey = ?`)
.get(params.siteKey);
const [key] = await db`SELECT * FROM keys WHERE siteKey = ${params.siteKey}`;
if (!key) {
return { success: false, error: "Key not found" };
}
@@ -414,10 +364,7 @@ export const server = new Elysia({
.replace(/\//g, "")
.replace(/=+$/, "");
db.prepare(`UPDATE keys SET secretHash = ? WHERE siteKey = ?`).run(
await Bun.password.hash(newSecretKey),
params.siteKey
);
await db`UPDATE keys SET secretHash = ${await Bun.password.hash(newSecretKey)} WHERE siteKey = ${params.siteKey}`;
return {
secretKey: newSecretKey,
};
@@ -434,7 +381,7 @@ export const server = new Elysia({
.get(
"/settings/sessions",
async () => {
const sessions = await db.prepare(`SELECT * FROM sessions`).all();
const sessions = await db`SELECT * FROM sessions`;
return sessions.map((session) => ({
token: session.token.slice(-14),
expires: new Date(session.expires).toISOString(),
@@ -450,7 +397,7 @@ export const server = new Elysia({
.get(
"/settings/apikeys",
async () => {
const apikeys = await db.prepare(`SELECT * FROM api_keys`).all();
const apikeys = await db`SELECT * FROM api_keys`;
return apikeys.map((key) => ({
name: key.name,
@@ -476,9 +423,10 @@ export const server = new Elysia({
const name = body.name;
db.prepare(
`INSERT INTO api_keys (id, name, tokenHash, created) VALUES (?, ?, ?, ?)`
).run(id, name, await Bun.password.hash(token), Date.now());
await db`
INSERT INTO api_keys (id, name, tokenHash, created)
VALUES (${id}, ${name}, ${await Bun.password.hash(token)}, ${Date.now()})
`;
return {
apiKey: `${id}_${token}`,
};
@@ -495,14 +443,12 @@ export const server = new Elysia({
.delete(
"/settings/apikeys/:id",
async ({ params, set }) => {
const key = await db
.prepare(`SELECT * FROM api_keys WHERE id = ?`)
.get(params.id);
const [key] = await db`SELECT * FROM api_keys WHERE id = ${params.id}`;
if (!key) {
set.status = 404;
return { success: false, error: "API key not found" };
}
db.prepare(`DELETE FROM api_keys WHERE id = ?`).run(params.id);
await db`DELETE FROM api_keys WHERE id = ${params.id}`;
return { success: true };
},
{
@@ -545,9 +491,8 @@ export const server = new Elysia({
// body.session are the last characters of the session token
// e.g. body.session = (...)8KdbcHjqxWPR6Q
const sessionRow = await db
.prepare(`SELECT token FROM sessions WHERE token LIKE ?`)
.get(`%${body.session}`);
const sessionRows = await db`SELECT token FROM sessions WHERE token LIKE ${'%' + body.session}`;
const sessionRow = sessionRows[0];
if (!sessionRow) {
set.status = 404;
@@ -556,7 +501,7 @@ export const server = new Elysia({
session = sessionRow.token;
}
db.prepare(`DELETE FROM sessions WHERE token = ?`).run(session);
await db`DELETE FROM sessions WHERE token = ${session}`;
return { success: true };
},
+5 -15
View File
@@ -4,17 +4,6 @@ import { Elysia } from "elysia";
import { db } from "./db.js";
import { ratelimitGenerator } from "./ratelimit.js";
const getSitekeyWithSecretQuery = db.prepare(
`SELECT * FROM keys WHERE siteKey = ?`,
);
const getTokenQuery = db.prepare(`
SELECT * FROM tokens WHERE siteKey = ? AND token = ?
`);
const deleteTokenQuery = db.prepare(`
DELETE FROM tokens WHERE siteKey = ? AND token = ?
`);
const blockedIPs = new Map();
setInterval(() => {
@@ -60,7 +49,8 @@ export const siteverifyServer = new Elysia({
return { error: "Missing required parameters" };
}
const keyHash = (await getSitekeyWithSecretQuery.get(sitekey))?.secretHash;
const [keyData] = await db`SELECT * FROM keys WHERE siteKey = ${sitekey}`;
const keyHash = keyData?.secretHash;
if (!keyHash || !secret) {
set.status = 404;
return { error: "Invalid site key or secret" };
@@ -74,7 +64,7 @@ export const siteverifyServer = new Elysia({
return { error: "Invalid site key or secret" };
}
const token = await getTokenQuery.get(params.siteKey, response);
const [token] = await db`SELECT * FROM tokens WHERE siteKey = ${params.siteKey} AND token = ${response}`;
if (!token) {
set.status = 404;
@@ -82,11 +72,11 @@ export const siteverifyServer = new Elysia({
}
if (token.expires < Date.now()) {
deleteTokenQuery.run(params.siteKey, response);
await db`DELETE FROM tokens WHERE siteKey = ${params.siteKey} AND token = ${response}`;
set.status = 403;
return { error: "Token expired" };
}
deleteTokenQuery.run(params.siteKey, response);
await db`DELETE FROM tokens WHERE siteKey = ${params.siteKey} AND token = ${response}`;
return { success: true };
});