refactor: improve maintainability by reducing LOC/reuse
This commit is contained in:
+2
-49
@@ -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: 10,
|
||||
@@ -9,53 +10,5 @@ const sql = postgres(process.env.DATABASE_URL ?? "postgres://pingql:pingql@local
|
||||
export default sql;
|
||||
|
||||
export async function migrate() {
|
||||
// Plan columns on accounts (may already exist from API/web migrations)
|
||||
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 TABLE IF NOT EXISTS payments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
plan TEXT NOT NULL,
|
||||
months INTEGER,
|
||||
amount_usd NUMERIC(10,2) NOT NULL,
|
||||
coin TEXT NOT NULL,
|
||||
amount_crypto TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
derivation_index INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
paid_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
txid TEXT
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS amount_received TEXT NOT NULL DEFAULT '0'`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS payment_txs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
payment_id BIGINT NOT NULL REFERENCES payments(id) ON DELETE CASCADE,
|
||||
txid TEXT NOT NULL,
|
||||
amount TEXT NOT NULL,
|
||||
confirmed BOOLEAN NOT NULL DEFAULT false,
|
||||
detected_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(payment_id, txid)
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payment_txs_payment ON payment_txs(payment_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payment_txs_txid ON payment_txs(txid)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payments_account ON payments(account_id)`;
|
||||
|
||||
await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS receipt_html TEXT`;
|
||||
|
||||
// Derivation index should be unique per coin, not globally
|
||||
await sql`DROP INDEX IF EXISTS payments_derivation_index_key`;
|
||||
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_payments_coin_derivation ON payments(coin, derivation_index)`;
|
||||
|
||||
console.log("Pay DB ready");
|
||||
await sharedMigrate(sql);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@ import { checkPayments, expireProPlans } from "./monitor";
|
||||
|
||||
await migrate();
|
||||
|
||||
const SECURITY_HEADERS = {
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Strict-Transport-Security": "max-age=63072000; includeSubDomains",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
};
|
||||
import { SECURITY_HEADERS } from "../../shared/auth";
|
||||
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGINS?.split(",") ?? ["https://pingql.com"];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/// States: pending → underpaid → confirming → paid | expired
|
||||
import sql from "./db";
|
||||
import { getAddressInfo, getAddressInfoBulk } from "./freedom";
|
||||
import { COINS } from "./plans";
|
||||
import { COINS, planTier } from "../../shared/plans";
|
||||
import { generateReceipt } from "./receipt";
|
||||
|
||||
const SOCK_API = process.env.FREEDOM_SOCK ?? "https://sock-v1.freedom.st";
|
||||
@@ -226,9 +226,6 @@ interface StackEntry { plan: string; remaining_days: number | null }
|
||||
interface AccountState { plan: string; plan_expires_at: Date | null; plan_stack: StackEntry[] }
|
||||
interface AccountUpdate { plan: string; plan_expires_at: Date | null; plan_stack: StackEntry[] }
|
||||
|
||||
const PLAN_RANK: Record<string, number> = { free: 0, pro: 1, lifetime: 1, pro2x: 2, pro4x: 3 };
|
||||
function planTier(plan: string): number { return PLAN_RANK[plan] ?? 0; }
|
||||
|
||||
export function insertIntoStack(stack: StackEntry[], entry: StackEntry): StackEntry[] {
|
||||
const result = stack.slice();
|
||||
// Merge if same plan already exists
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Pro pricing — base $12/mo, multiplied for 2x/4x
|
||||
export const PRO_MONTHLY_USD = 12;
|
||||
export const LIFETIME_USD = 140;
|
||||
|
||||
export const PLANS: Record<string, { label: string; monthlyUsd?: number; priceUsd?: number }> = {
|
||||
pro: { label: "Pro", monthlyUsd: PRO_MONTHLY_USD },
|
||||
pro2x: { label: "Pro 2x", monthlyUsd: PRO_MONTHLY_USD * 2 },
|
||||
pro4x: { label: "Pro 4x", monthlyUsd: PRO_MONTHLY_USD * 4 },
|
||||
lifetime: { label: "Lifetime", priceUsd: LIFETIME_USD },
|
||||
};
|
||||
|
||||
export const COINS: Record<string, { label: string; ticker: string; confirmations: number; uri: string }> = {
|
||||
btc: { label: "Bitcoin", ticker: "BTC", confirmations: 1, uri: "bitcoin" },
|
||||
ltc: { label: "Litecoin", ticker: "LTC", confirmations: 1, uri: "litecoin" },
|
||||
doge: { label: "Dogecoin", ticker: "DOGE", confirmations: 1, uri: "dogecoin" },
|
||||
dash: { label: "Dash", ticker: "DASH", confirmations: 1, uri: "dash" },
|
||||
bch: { label: "Bitcoin Cash", ticker: "BCH", confirmations: 0, uri: "bitcoincash" },
|
||||
xec: { label: "eCash", ticker: "XEC", confirmations: 0, uri: "ecash" },
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import sql from "./db";
|
||||
import { COINS } from "./plans";
|
||||
import { COINS } from "../../shared/plans";
|
||||
|
||||
export async function generateReceipt(paymentId: number): Promise<string> {
|
||||
const [payment] = await sql`SELECT * FROM payments WHERE id = ${paymentId}`;
|
||||
|
||||
+4
-17
@@ -2,33 +2,20 @@ import { Elysia, t } from "elysia";
|
||||
import sql from "./db";
|
||||
import { derive } from "./address";
|
||||
import { getExchangeRates, getAvailableCoins, fetchQrBase64 } from "./freedom";
|
||||
import { PLANS, COINS } from "./plans";
|
||||
import { PLAN_PRICING as PLANS, COINS } from "../../shared/plans";
|
||||
import { generateReceipt } from "./receipt";
|
||||
import { watchPayment } from "./monitor";
|
||||
|
||||
// Resolve account from key (same logic as API/web apps)
|
||||
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) return { accountId: apiKey.account_id, keyId: apiKey.id, plan: apiKey.plan };
|
||||
|
||||
return null;
|
||||
}
|
||||
import { resolveKey as sharedResolveKey, extractAuthKey } from "../../shared/auth";
|
||||
|
||||
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" };
|
||||
}
|
||||
const resolved = await resolveKey(key);
|
||||
const resolved = await sharedResolveKey(sql, key, { trackUsage: false });
|
||||
if (resolved) return resolved;
|
||||
set.status = 401;
|
||||
return { accountId: null as string | null, keyId: null as string | null, plan: "free" };
|
||||
|
||||
Reference in New Issue
Block a user