112 lines
4.5 KiB
TypeScript
112 lines
4.5 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { Customer, CustomerRepo } from '../auth';
|
|
|
|
export class SqliteCustomerRepo implements CustomerRepo {
|
|
constructor(private db: Database.Database, seed: Customer[] = []) {
|
|
this.db.exec(`
|
|
CREATE TABLE IF NOT EXISTS customers (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
tier_id TEXT NOT NULL,
|
|
api_key TEXT NOT NULL UNIQUE,
|
|
stripe_customer_id TEXT,
|
|
multiplier_override REAL
|
|
)
|
|
`);
|
|
// Idempotent migrations for pre-existing databases.
|
|
this.ensureColumn('billing_type', `billing_type TEXT NOT NULL DEFAULT 'stripe'`);
|
|
this.ensureColumn('email', 'email TEXT');
|
|
this.ensureColumn('password_hash', 'password_hash TEXT');
|
|
this.ensureColumn('totp_secret', 'totp_secret TEXT');
|
|
this.ensureColumn('totp_enabled', 'totp_enabled INTEGER NOT NULL DEFAULT 0');
|
|
this.ensureColumn('balance_cents', 'balance_cents INTEGER NOT NULL DEFAULT 0');
|
|
this.ensureColumn('email_invoicing', 'email_invoicing INTEGER NOT NULL DEFAULT 0');
|
|
const { n } = this.db.prepare('SELECT COUNT(*) AS n FROM customers').get() as { n: number };
|
|
if (n === 0) {
|
|
for (const c of seed) this.save(c);
|
|
}
|
|
}
|
|
|
|
private ensureColumn(name: string, ddl: string): void {
|
|
const cols = this.db.prepare('PRAGMA table_info(customers)').all() as { name: string }[];
|
|
if (!cols.some((c) => c.name === name)) {
|
|
this.db.exec(`ALTER TABLE customers ADD COLUMN ${ddl}`);
|
|
}
|
|
}
|
|
|
|
findByApiKey(apiKey: string): Customer | undefined {
|
|
const r = this.db.prepare('SELECT * FROM customers WHERE api_key = ?').get(apiKey) as
|
|
| Record<string, unknown>
|
|
| undefined;
|
|
return r ? toCustomer(r) : undefined;
|
|
}
|
|
|
|
findByEmail(email: string): Customer | undefined {
|
|
const r = this.db
|
|
.prepare('SELECT * FROM customers WHERE lower(email) = lower(?)')
|
|
.get(email) as Record<string, unknown> | undefined;
|
|
return r ? toCustomer(r) : undefined;
|
|
}
|
|
|
|
list(): Customer[] {
|
|
const rows = this.db.prepare('SELECT * FROM customers ORDER BY id').all() as Record<
|
|
string,
|
|
unknown
|
|
>[];
|
|
return rows.map(toCustomer);
|
|
}
|
|
|
|
save(customer: Customer): void {
|
|
this.db
|
|
.prepare(
|
|
`INSERT INTO customers (id, name, tier_id, api_key, stripe_customer_id, multiplier_override, billing_type, email, password_hash, totp_secret, totp_enabled, balance_cents, email_invoicing)
|
|
VALUES (@id, @name, @tierId, @apiKey, @stripeCustomerId, @multiplierOverride, @billingType, @email, @passwordHash, @totpSecret, @totpEnabled, @balanceCents, @emailInvoicing)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
name = @name,
|
|
tier_id = @tierId,
|
|
api_key = @apiKey,
|
|
stripe_customer_id = @stripeCustomerId,
|
|
multiplier_override = @multiplierOverride,
|
|
billing_type = @billingType,
|
|
email = @email,
|
|
password_hash = @passwordHash,
|
|
totp_secret = @totpSecret,
|
|
totp_enabled = @totpEnabled,
|
|
balance_cents = @balanceCents,
|
|
email_invoicing = @emailInvoicing`,
|
|
)
|
|
.run({
|
|
id: customer.id,
|
|
name: customer.name,
|
|
tierId: customer.tierId,
|
|
apiKey: customer.apiKey,
|
|
stripeCustomerId: customer.stripeCustomerId ?? null,
|
|
multiplierOverride: customer.multiplierOverride ?? null,
|
|
billingType: customer.billingType ?? 'stripe',
|
|
email: customer.email ?? null,
|
|
passwordHash: customer.passwordHash ?? null,
|
|
totpSecret: customer.totpSecret ?? null,
|
|
totpEnabled: customer.totpEnabled ? 1 : 0,
|
|
balanceCents: customer.balanceCents ?? 0,
|
|
emailInvoicing: customer.emailInvoicing ? 1 : 0,
|
|
});
|
|
}
|
|
}
|
|
|
|
function toCustomer(r: Record<string, unknown>): Customer {
|
|
return {
|
|
id: r.id as string,
|
|
name: r.name as string,
|
|
tierId: r.tier_id as string,
|
|
apiKey: r.api_key as string,
|
|
stripeCustomerId: (r.stripe_customer_id as string | null) ?? undefined,
|
|
multiplierOverride: (r.multiplier_override as number | null) ?? undefined,
|
|
billingType: ((r.billing_type as string | null) ?? 'stripe') as Customer['billingType'],
|
|
email: (r.email as string | null) ?? undefined,
|
|
passwordHash: (r.password_hash as string | null) ?? undefined,
|
|
totpSecret: (r.totp_secret as string | null) ?? undefined,
|
|
totpEnabled: ((r.totp_enabled as number | null) ?? 0) === 1,
|
|
balanceCents: (r.balance_cents as number | null) ?? 0,
|
|
emailInvoicing: ((r.email_invoicing as number | null) ?? 0) === 1,
|
|
};
|
|
}
|