Add full application: receipt scanning, auth, billing, and account deletion

Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Timo
2026-08-19 20:59:04 +02:00
parent 650a74da97
commit 84b9987c49
415 changed files with 96619 additions and 0 deletions

72
scripts/create-admin.ts Normal file
View File

@@ -0,0 +1,72 @@
import fs from "fs";
// Load .env.local into process.env (same pattern as other scripts) BEFORE the
// db module initialises, so it picks up the real DATABASE_URL (port 5436).
const dotenvContent = fs.readFileSync(".env.local", "utf8");
for (const l of dotenvContent.split("\n")) {
const trimmed = l.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const idx = trimmed.indexOf("=");
if (idx !== -1) {
process.env[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
}
}
async function main() {
const { db } = await import("../src/lib/db");
const { users } = await import("../src/lib/schema/db");
const { hashPassword } = await import("../src/lib/auth/password");
const { canonicaliseEmail, normaliseEmail } = await import("../src/lib/auth/email");
const { newId } = await import("../src/lib/auth/tokens");
const { eq } = await import("drizzle-orm");
const rawEmail = process.env.ADMIN_EMAILS?.split(",")[0]?.trim() ?? "";
const password = process.env.ADMIN_PASSWORD ?? "fiesta";
if (!rawEmail) throw new Error("ADMIN_EMAILS not set");
const email = canonicaliseEmail(rawEmail);
const emailKey = normaliseEmail(rawEmail);
if (!emailKey) throw new Error(`Invalid admin email: ${rawEmail}`);
const passwordHash = await hashPassword(password);
const existing = await db.select().from(users).where(eq(users.emailKey, emailKey)).limit(1);
if (existing.length > 0) {
await db
.update(users)
.set({
passwordHash,
emailVerifiedAt: new Date(),
isGuest: false,
updatedAt: new Date(),
})
.where(eq(users.id, existing[0].id));
console.log(`Updated admin ${email} (${existing[0].id}) — password reset, email verified`);
} else {
const row = (
await db
.insert(users)
.values({
id: newId("usr"),
email,
emailKey,
name: "Timo Knuth",
passwordHash,
emailVerifiedAt: new Date(),
isGuest: false,
plan: "free",
})
.returning()
)[0];
console.log(`Created admin ${email} (${row.id}) — email verified`);
}
process.exit(0);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});