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:
@@ -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": {
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user