refactor: improve maintainability by reducing LOC/reuse
This commit is contained in:
+2
-76
@@ -1,4 +1,5 @@
|
||||
import postgres from "postgres";
|
||||
import { migrate as sharedMigrate } from "../../shared/db";
|
||||
|
||||
const sql = postgres(process.env.DATABASE_URL ?? "postgres://pingql:pingql@localhost:5432/pingql", {
|
||||
max: 20,
|
||||
@@ -9,80 +10,5 @@ const sql = postgres(process.env.DATABASE_URL ?? "postgres://pingql:pingql@local
|
||||
export default sql;
|
||||
|
||||
export async function migrate() {
|
||||
await sql`CREATE EXTENSION IF NOT EXISTS pgcrypto`;
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
email_hash TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS monitors (
|
||||
id TEXT PRIMARY KEY DEFAULT encode(gen_random_bytes(8), 'hex'),
|
||||
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
method TEXT NOT NULL DEFAULT 'GET',
|
||||
request_headers JSONB,
|
||||
request_body TEXT,
|
||||
timeout_ms INTEGER NOT NULL DEFAULT 30000,
|
||||
interval_s INTEGER NOT NULL DEFAULT 60,
|
||||
query JSONB,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS pings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
checked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
scheduled_at TIMESTAMPTZ,
|
||||
jitter_ms INTEGER,
|
||||
status_code INTEGER,
|
||||
latency_ms INTEGER,
|
||||
up BOOLEAN NOT NULL,
|
||||
error TEXT,
|
||||
meta JSONB
|
||||
)
|
||||
`;
|
||||
|
||||
// Migrations for existing deployments
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ`;
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS jitter_ms INTEGER`;
|
||||
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS regions TEXT[] NOT NULL DEFAULT '{}'`;
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS region TEXT`;
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS run_id TEXT`;
|
||||
|
||||
await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan TEXT NOT NULL DEFAULT 'free'`;
|
||||
await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan_expires_at TIMESTAMPTZ`;
|
||||
await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan_stack JSONB NOT NULL DEFAULT '[]'`;
|
||||
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_pings_monitor ON pings(monitor_id, checked_at DESC)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_pings_checked_at ON pings(checked_at)`;
|
||||
|
||||
// Response bodies stored separately to keep pings table lean
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS ping_bodies (
|
||||
ping_id BIGINT PRIMARY KEY REFERENCES pings(id) ON DELETE CASCADE,
|
||||
body TEXT
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
label TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
last_used_at TIMESTAMPTZ
|
||||
)
|
||||
`;
|
||||
|
||||
console.log("DB ready");
|
||||
await sharedMigrate(sql);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { Elysia } from "elysia";
|
||||
|
||||
import { ingest } from "./routes/pings";
|
||||
import { monitors } from "./routes/monitors";
|
||||
import { account } from "./routes/auth";
|
||||
import { internal } from "./routes/internal";
|
||||
import { migrate } from "./db";
|
||||
await migrate();
|
||||
import { SECURITY_HEADERS } from "../../shared/auth";
|
||||
|
||||
const SECURITY_HEADERS = {
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Strict-Transport-Security": "max-age=63072000; includeSubDomains",
|
||||
"X-XSS-Protection": "0",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
};
|
||||
await migrate();
|
||||
|
||||
const elysia = new Elysia()
|
||||
.get("/", () => ({
|
||||
|
||||
@@ -1,33 +1,13 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { createHmac, randomBytes } from "crypto";
|
||||
import sql from "../db";
|
||||
import { createRateLimiter } from "../utils/rate-limit";
|
||||
import { getPlanLimits } from "../utils/plans";
|
||||
import { createRateLimiter } from "../../../shared/rate-limit";
|
||||
import { getPlanLimits } from "../../../shared/plans";
|
||||
import { generateKey, hashEmail, resolveKey as sharedResolveKey, extractAuthKey, COOKIE_OPTS } from "../../../shared/auth";
|
||||
|
||||
// ── Per-IP rate limiting for auth endpoints ───────────────────────────
|
||||
const checkAuthRateLimit = createRateLimiter();
|
||||
|
||||
const EMAIL_HMAC_KEY = process.env.EMAIL_HMAC_KEY || "pingql-default-hmac-key";
|
||||
|
||||
function generateKey(): string {
|
||||
return randomBytes(32).toString("base64url");
|
||||
}
|
||||
|
||||
function hashEmail(email: string): string {
|
||||
return createHmac("sha256", EMAIL_HMAC_KEY).update(email.toLowerCase().trim()).digest("hex");
|
||||
}
|
||||
|
||||
async function resolveKey(key: string): Promise<{ accountId: string; keyId: string | null; plan: string } | null> {
|
||||
const [account] = await sql`SELECT id, plan FROM accounts WHERE key = ${key}`;
|
||||
if (account) return { accountId: account.id, keyId: null, plan: account.plan };
|
||||
|
||||
const [apiKey] = await sql`SELECT k.id, k.account_id, a.plan FROM api_keys k JOIN accounts a ON a.id = k.account_id WHERE k.key = ${key}`;
|
||||
if (apiKey) {
|
||||
sql`UPDATE api_keys SET last_used_at = now() WHERE id = ${apiKey.id}`.catch(() => {});
|
||||
return { accountId: apiKey.account_id, keyId: apiKey.id, plan: apiKey.plan };
|
||||
}
|
||||
|
||||
return null;
|
||||
async function resolveKey(key: string) {
|
||||
return sharedResolveKey(sql, key);
|
||||
}
|
||||
|
||||
export { resolveKey };
|
||||
@@ -35,11 +15,7 @@ export { resolveKey };
|
||||
export function requireAuth(app: Elysia) {
|
||||
return app
|
||||
.derive(async ({ headers, cookie, set }) => {
|
||||
const authHeader = headers["authorization"] ?? "";
|
||||
const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim();
|
||||
const cookieKey = cookie?.pingql_key?.value;
|
||||
|
||||
const key = bearer || cookieKey;
|
||||
const key = extractAuthKey(headers, cookie);
|
||||
if (!key) {
|
||||
set.status = 401;
|
||||
return { accountId: null as string | null, keyId: null as string | null, plan: "free" as string };
|
||||
@@ -59,15 +35,6 @@ export function requireAuth(app: Elysia) {
|
||||
});
|
||||
}
|
||||
|
||||
const COOKIE_OPTS = {
|
||||
httpOnly: true,
|
||||
secure: process.env.COOKIE_SECURE !== "false",
|
||||
sameSite: "none" as const,
|
||||
path: "/",
|
||||
domain: process.env.COOKIE_DOMAIN ?? ".pingql.com",
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
};
|
||||
|
||||
export const account = new Elysia({ prefix: "/account" })
|
||||
|
||||
.post("/login", async ({ body, cookie, set, request }) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { Elysia } from "elysia";
|
||||
import sql from "../db";
|
||||
import { safeTokenCompare } from "../utils/token";
|
||||
import { safeTokenCompare } from "../../../shared/auth";
|
||||
|
||||
export async function pruneOldPings(retentionDays = 90) {
|
||||
const result = await sql`DELETE FROM pings WHERE checked_at < now() - ${retentionDays + ' days'}::interval`;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Elysia, t } from "elysia";
|
||||
import { requireAuth } from "./auth";
|
||||
import sql from "../db";
|
||||
import { validateMonitorUrl } from "../utils/ssrf";
|
||||
import { getPlanLimits } from "../utils/plans";
|
||||
import { getPlanLimits } from "../../../shared/plans";
|
||||
|
||||
const MonitorBody = t.Object({
|
||||
name: t.String({ maxLength: 200, description: "Human-readable name" }),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import sql from "../db";
|
||||
import { resolveKey } from "./auth";
|
||||
import { safeTokenCompare } from "../utils/token";
|
||||
import { extractAuthKey, safeTokenCompare } from "../../../shared/auth";
|
||||
|
||||
// ── SSE bus ───────────────────────────────────────────────────────────────────
|
||||
type SSEController = ReadableStreamDefaultController<Uint8Array>;
|
||||
@@ -121,9 +121,7 @@ export const ingest = new Elysia()
|
||||
|
||||
// Fetch response body for a specific ping
|
||||
.get("/pings/:id/body", async ({ params, headers, cookie, set }) => {
|
||||
const authHeader = headers["authorization"] ?? "";
|
||||
const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim();
|
||||
const key = bearer ?? cookie?.pingql_key?.value;
|
||||
const key = extractAuthKey(headers, cookie);
|
||||
if (!key) { set.status = 401; return { error: "Unauthorized" }; }
|
||||
|
||||
const resolved = await resolveKey(key);
|
||||
@@ -143,10 +141,7 @@ export const ingest = new Elysia()
|
||||
|
||||
// SSE: single stream for all of the account's monitors
|
||||
.get("/account/stream", async ({ headers, cookie }) => {
|
||||
const authHeader = headers["authorization"] ?? "";
|
||||
const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim();
|
||||
const key = bearer ?? cookie?.pingql_key?.value;
|
||||
|
||||
const key = extractAuthKey(headers, cookie);
|
||||
if (!key) return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
|
||||
|
||||
const resolved = await resolveKey(key);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
export type Plan = "free" | "pro" | "pro2x" | "pro4x" | "lifetime";
|
||||
|
||||
export interface PlanLimits {
|
||||
maxMonitors: number;
|
||||
minIntervalS: number;
|
||||
maxRegions: number;
|
||||
}
|
||||
|
||||
const PLANS: Record<Plan, PlanLimits> = {
|
||||
free: { maxMonitors: 10, minIntervalS: 30, maxRegions: 1 },
|
||||
pro: { maxMonitors: 200, minIntervalS: 5, maxRegions: 99 },
|
||||
pro2x: { maxMonitors: 400, minIntervalS: 5, maxRegions: 99 },
|
||||
pro4x: { maxMonitors: 800, minIntervalS: 5, maxRegions: 99 },
|
||||
lifetime: { maxMonitors: 200, minIntervalS: 5, maxRegions: 99 },
|
||||
};
|
||||
|
||||
export function getPlanLimits(plan: string): PlanLimits {
|
||||
return PLANS[plan as Plan] || PLANS.free;
|
||||
}
|
||||
|
||||
// Display helpers
|
||||
export const PLAN_LABELS: Record<string, string> = {
|
||||
free: "Free", pro: "Pro", pro2x: "Pro 2x", pro4x: "Pro 4x", lifetime: "Lifetime",
|
||||
};
|
||||
|
||||
export const PRO_MULTIPLIERS = [
|
||||
{ plan: "pro", label: "1x", monitors: 200, interval: "5s", priceMultiplier: 1 },
|
||||
{ plan: "pro2x", label: "2x", monitors: 400, interval: "5s", priceMultiplier: 2 },
|
||||
{ plan: "pro4x", label: "4x", monitors: 800, interval: "5s", priceMultiplier: 4 },
|
||||
];
|
||||
|
||||
export const PRO_MONTHLY_USD = 12;
|
||||
export const LIFETIME_USD = 140;
|
||||
|
||||
// Tier ranking for plan stacking decisions
|
||||
const PLAN_RANK: Record<string, number> = {
|
||||
free: 0, pro: 1, lifetime: 1, pro2x: 2, pro4x: 3,
|
||||
};
|
||||
|
||||
export function planTier(plan: string): number {
|
||||
return PLAN_RANK[plan] ?? 0;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
export function createRateLimiter(windowMs = 60_000, cleanupIntervalMs = 5 * 60_000) {
|
||||
const map = new Map<string, { count: number; resetAt: number }>();
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of map) {
|
||||
if (now > entry.resetAt) map.delete(key);
|
||||
}
|
||||
}, cleanupIntervalMs);
|
||||
|
||||
return function check(key: string, max: number): boolean {
|
||||
const now = Date.now();
|
||||
const entry = map.get(key);
|
||||
if (!entry || now > entry.resetAt) {
|
||||
map.set(key, { count: 1, resetAt: now + windowMs });
|
||||
return true;
|
||||
}
|
||||
entry.count++;
|
||||
return entry.count <= max;
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { timingSafeEqual } from "crypto";
|
||||
|
||||
export function safeTokenCompare(a: string | undefined, b: string | undefined): boolean {
|
||||
if (!a || !b) return false;
|
||||
const bufA = Buffer.from(a);
|
||||
const bufB = Buffer.from(b);
|
||||
if (bufA.length !== bufB.length) return false;
|
||||
return timingSafeEqual(bufA, bufB);
|
||||
}
|
||||
Reference in New Issue
Block a user