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

@@ -14,8 +14,10 @@
"@better-auth/expo": "catalog:",
"@haushaltsApp/db": "workspace:*",
"@haushaltsApp/env": "workspace:*",
"@types/nodemailer": "^7.0.11",
"better-auth": "catalog:",
"dotenv": "catalog:",
"nodemailer": "^8.0.2",
"zod": "catalog:"
},
"devDependencies": {

View File

@@ -4,11 +4,23 @@ import * as schema from "@haushaltsApp/db/schema/auth";
import { env } from "@haushaltsApp/env/server";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { bearer, organization } from "better-auth/plugins";
import nodemailer from "nodemailer";
function createTransport() {
return nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT,
secure: env.SMTP_PORT === 465,
...(env.SMTP_USER && env.SMTP_PASSWORD
? { auth: { user: env.SMTP_USER, pass: env.SMTP_PASSWORD } }
: {}),
});
}
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: schema,
}),
trustedOrigins: [
@@ -20,7 +32,71 @@ export const auth = betterAuth({
],
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
async sendResetPassword(data) {
await createTransport().sendMail({
from: env.SMTP_FROM,
to: data.user.email,
subject: "Passwort zurücksetzen HausApp",
html: `
<p>Hallo ${data.user.name ?? ""},</p>
<p>Klicke auf den Link, um dein Passwort zurückzusetzen:</p>
<p><a href="${data.url}" style="background:#2563EB;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;">Passwort zurücksetzen</a></p>
<p style="color:#9ca3af;font-size:12px;">Oder kopiere diesen Link: ${data.url}</p>
<p style="color:#9ca3af;font-size:12px;">Der Link ist 1 Stunde gültig.</p>
`,
});
},
},
emailVerification: {
sendVerificationEmail: async (data) => {
await createTransport().sendMail({
from: env.SMTP_FROM,
to: data.user.email,
subject: "E-Mail bestätigen HausApp",
html: `
<p>Hallo ${data.user.name ?? ""},</p>
<p>Bitte bestätige deine E-Mail-Adresse:</p>
<p><a href="${data.url}" style="background:#2563EB;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;">E-Mail bestätigen</a></p>
<p style="color:#9ca3af;font-size:12px;">Oder kopiere diesen Link: ${data.url}</p>
<p style="color:#9ca3af;font-size:12px;">Der Link ist 1 Stunde gültig.</p>
`,
});
},
},
socialProviders: {
...(env.APPLE_CLIENT_ID && env.APPLE_PRIVATE_KEY
? {
apple: {
clientId: env.APPLE_CLIENT_ID,
clientSecret: env.APPLE_PRIVATE_KEY,
appBundleIdentifier: env.APPLE_CLIENT_ID,
},
}
: {}),
},
plugins: [
expo(),
bearer(),
organization({
allowUserToCreateOrganization: true,
async sendInvitationEmail(data) {
const inviteUrl = `${env.MOBILE_APP_SCHEME}invite?invitationId=${data.invitation.id}`;
await createTransport().sendMail({
from: env.SMTP_FROM,
to: data.email,
subject: `Einladung zu ${data.organization.name} HausApp`,
html: `
<p>Hallo,</p>
<p><strong>${data.inviter.user.name}</strong> hat dich eingeladen, dem Haushalt <strong>${data.organization.name}</strong> beizutreten.</p>
<p>Klicke auf den Link, um die Einladung anzunehmen:</p>
<p><a href="${inviteUrl}" style="background:#2563EB;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;">Einladung annehmen</a></p>
<p style="color:#9ca3af;font-size:12px;">Oder kopiere diesen Link: ${inviteUrl}</p>
`,
});
},
}),
],
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
advanced: {
@@ -30,5 +106,4 @@ export const auth = betterAuth({
httpOnly: true,
},
},
plugins: [expo()],
});