This commit is contained in:
Timo Knuth
2026-02-27 15:19:24 +01:00
parent b7f8221095
commit 253c3c1c6d
134 changed files with 11188 additions and 1871 deletions

View File

@@ -4,6 +4,8 @@ import { magicLink } from 'better-auth/plugins'
import { admin as adminPlugin } from 'better-auth/plugins'
import { prisma } from '@innungsapp/shared'
import { sendMagicLinkEmail } from './email'
import { headers } from 'next/headers'
export const auth = betterAuth({
database: prismaAdapter(prisma, {
@@ -17,10 +19,25 @@ export const auth = betterAuth({
trustedOrigins: [
process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3032',
process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3032',
'http://192.168.178.115:3032',
'http://localhost:8081', // Expo dev client
'http://192.168.178.115:8081',
'http://localhost:3000',
'http://localhost:3001',
'http://localhost:3032',
'http://localhost:8081',
'http://*.localhost:3032',
'http://*.localhost:3000',
'https://*.innungsapp.de',
'https://*.innungsapp.com',
// Additional origins from env (comma-separated)
...(process.env.BETTER_AUTH_TRUSTED_ORIGINS ?? '').split(',').map((o) => o.trim()).filter(Boolean),
],
user: {
additionalFields: {
mustChangePassword: {
type: 'boolean',
defaultValue: false,
},
},
},
plugins: [
magicLink({
sendMagicLink: async ({ email, url }) => {
@@ -38,3 +55,19 @@ export const auth = betterAuth({
})
export type Auth = typeof auth
export async function getSanitizedHeaders() {
const allHeaders = await headers()
const sanitizedHeaders = new Headers(allHeaders)
// Avoid ENOTFOUND by forcing host to localhost for internal better-auth fetches
// We use the host defined in BETTER_AUTH_URL
try {
const betterAuthUrl = new URL(process.env.BETTER_AUTH_URL || 'http://localhost:3032')
sanitizedHeaders.set('host', betterAuthUrl.host)
} catch (e) {
sanitizedHeaders.set('host', 'localhost:3032')
}
return sanitizedHeaders
}

View File

@@ -88,3 +88,51 @@ export async function sendInviteEmail({
`,
})
}
export async function sendAdminCredentialsEmail({
to,
adminName,
orgName,
password,
loginUrl,
}: {
to: string
adminName: string
orgName: string
password: string
loginUrl: string
}) {
await transporter.sendMail({
from: process.env.EMAIL_FROM ?? 'noreply@innungsapp.de',
to,
subject: `Admin-Zugang für — ${orgName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #111827; padding: 24px; border-radius: 8px 8px 0 0;">
<h1 style="color: white; margin: 0; font-size: 24px;">InnungsApp Admin</h1>
</div>
<div style="background: #fff; padding: 32px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px;">
<h2 style="color: #111827; margin-top: 0;">Hallo ${adminName},</h2>
<p style="color: #4b5563;">
Sie wurden als Administrator für die <strong>${orgName}</strong> in der InnungsApp freigeschaltet.
</p>
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; margin: 24px 0;">
<p style="margin-top: 0; font-size: 14px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em;">Ihre Zugangsdaten</p>
<p style="margin: 8px 0; font-size: 16px; color: #111827;"><strong>E-Mail:</strong> ${to}</p>
<p style="margin: 8px 0; font-size: 16px; color: #111827;"><strong>Passwort:</strong> <code style="background: #eee; padding: 2px 4px; rounded: 4px;">${password}</code></p>
</div>
<p style="color: #4b5563;">Klicken Sie auf den Button, um sich im Verwaltungsportal anzumelden. Sie werden aufgefordert, Ihr Passwort nach dem ersten Login zu ändern.</p>
<a href="${loginUrl}/login?email=${encodeURIComponent(to)}"
style="display: inline-block; background: #111827; color: white; padding: 12px 24px;
border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0;">
Zum Admin-Portal
</a>
<hr style="border-color: #e5e7eb; margin: 24px 0;" />
<p style="color: #9ca3af; font-size: 12px; margin: 0;">
InnungsApp · Administrative Portal
</p>
</div>
</div>
`,
})
}

View File

@@ -0,0 +1,20 @@
import { headers } from 'next/headers'
const RESERVED_SUBDOMAINS = ['www', 'app', 'admin', 'localhost', 'superadmin', 'api']
export async function getTenantSlug() {
const host = (await headers()).get('host') || ''
const domainParts = host.split(':')[0].split('.')
if (
domainParts.length > 2 ||
(domainParts.length === 2 && domainParts[1] === 'localhost')
) {
const slug = domainParts[0]
if (!RESERVED_SUBDOMAINS.includes(slug)) {
return slug
}
}
return null
}