Production deployment setup + feature complete

- Dockerfile + deploy.sh for Hetzner server
- Email verification via Better Auth + Resend
- Invite code flow (6-digit OTP, generate/join)
- Settlement share percent fix (payer vs debtor)
- OCR scanner fixes (date display, retry, viewfinder)
- app.json icon/splash/adaptive-icon configured
- iOS deployment target 15.5 (ML Kit requirement)
- DB migration 0014: household_invitations table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
René Schober
2026-03-20 11:54:22 +01:00
parent 4e34270786
commit 9ddc7c6d7a
194 changed files with 55961 additions and 305 deletions

View File

@@ -1,8 +1,11 @@
import { auth } from "@haushaltsApp/auth";
import { env } from "@haushaltsApp/env/server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { registerRoutes } from "./routes";
import { shoppingWsHandlers } from "./ws/shopping-ws";
import { db, eq } from "@haushaltsApp/db";
import { session as sessionTable } from "@haushaltsApp/db/schema";
const app = new Hono();
@@ -11,16 +14,53 @@ app.use(
"/*",
cors({
origin: env.CORS_ORIGIN,
allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization", "x-household-id"],
credentials: true,
}),
);
app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw));
registerRoutes(app);
app.get("/", (c) => {
return c.text("OK");
});
// When running under Bun directly (not imported as a module for tests),
// start Bun.serve with WebSocket support.
if (typeof Bun !== "undefined" && !process.env.BUN_TEST) {
Bun.serve({
port: 3000,
hostname: "0.0.0.0",
websocket: shoppingWsHandlers,
async fetch(req: Request, server) {
const url = new URL(req.url);
if (url.pathname === "/api/shopping-lists/ws") {
const token = url.searchParams.get("token") ?? "";
const householdId = url.searchParams.get("householdId") ?? "";
if (!householdId) {
return new Response("Missing householdId", { status: 400 });
}
const rawToken = token.includes(".") ? token.split(".")[0] : token;
if (!rawToken) return new Response("Unauthorized", { status: 401 });
const sessionRow = await db.query.session.findFirst({
where: eq(sessionTable.token, rawToken),
with: { user: true },
});
if (!sessionRow?.user || sessionRow.expiresAt < new Date()) {
return new Response("Unauthorized", { status: 401 });
}
const upgraded = server.upgrade(req, {
data: { householdId, userId: sessionRow.user.id },
});
if (upgraded) return undefined as unknown as Response;
return new Response("WebSocket upgrade failed", { status: 400 });
}
return app.fetch(req);
},
});
}
export default app;