feat(standalone@2.0.17): switch to Bun SQL instead of Bun SQLite
This commit is contained in:
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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
@@ -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 };
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user