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

View File

@@ -0,0 +1,98 @@
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { eq } from "drizzle-orm";
import { getCurrentUser } from "@/lib/auth/session";
import { requireCsrf } from "@/lib/auth/csrf";
import { db, isDatabaseAvailable, schema } from "@/lib/db";
const stripe = process.env.STRIPE_SECRET_KEY
? new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2025-01-27.acacia" as any,
maxNetworkRetries: 2,
})
: null;
export const runtime = "nodejs";
export async function POST(req: NextRequest) {
try {
const csrfBlocked = requireCsrf(req);
if (csrfBlocked) return csrfBlocked;
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const hasDb = await isDatabaseAvailable().catch(() => false);
// A recurring Stripe subscription is cancelled at period end, so Pro
// access (and the Pro badge) continues for whatever the user already
// paid for. A plan with no subscription behind it (lifetime, or a plan
// granted without Stripe) has no period to run out, so it downgrades
// immediately instead.
if (user.stripeSubscriptionId && stripe) {
try {
await stripe.subscriptions.update(user.stripeSubscriptionId, {
cancel_at_period_end: true,
});
} catch (stripeErr: any) {
console.warn("Could not schedule Stripe subscription cancellation (may already be cancelled):", stripeErr?.message);
}
if (hasDb) {
try {
// Optimistic UI flag — the authoritative sync happens via the
// customer.subscription.updated webhook, which may lag behind.
await db
.update(schema.users)
.set({
cancelAtPeriodEnd: true,
updatedAt: new Date(),
})
.where(eq(schema.users.id, user.id));
} catch (dbErr) {
console.error("Database error while scheduling subscription cancellation:", dbErr);
}
}
return NextResponse.json({
success: true,
scheduled: true,
expiresAt: user.expiresAt ? user.expiresAt.toISOString() : null,
message: "Subscription will cancel at the end of the current billing period.",
});
}
// No Stripe subscription behind the plan — downgrade immediately.
if (hasDb) {
try {
await db
.update(schema.users)
.set({
plan: "free",
stripeSubscriptionId: null,
expiresAt: null,
cancelAtPeriodEnd: false,
updatedAt: new Date(),
})
.where(eq(schema.users.id, user.id));
} catch (dbErr) {
console.error("Database error while cancelling subscription:", dbErr);
}
}
return NextResponse.json({
success: true,
scheduled: false,
plan: "free",
message: "Subscription cancelled and downgraded to Free successfully.",
});
} catch (error: any) {
console.error("Subscription cancel error:", error);
return NextResponse.json(
{ error: error?.message || "Fehler beim Kündigen des Abonnements." },
{ status: 500 }
);
}
}