Postgres
This commit is contained in:
@@ -86,6 +86,9 @@ export function CreateOrgForm() {
|
||||
}
|
||||
|
||||
const [isHeroUploading, setIsHeroUploading] = useState(false)
|
||||
const appBaseUrl = (typeof window !== 'undefined'
|
||||
? window.location.origin
|
||||
: (process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3010')).replace(/\/$/, '')
|
||||
|
||||
const handleHeroUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
@@ -182,7 +185,7 @@ export function CreateOrgForm() {
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Kurzbezeichnung (Slug)</label>
|
||||
<input type="text" name="slug" required value={formData.slug} onChange={handleChange} placeholder="z.B. tischler-berlin" pattern="^[a-z0-9\-]+$" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300" />
|
||||
<p className="text-[11px] text-gray-400 mt-2 leading-relaxed">Landingpage unter: <span className="text-[#E63946] font-medium">{formData.slug ? `${formData.slug}.localhost:3032` : 'ihr-slug.localhost:3032'}</span></p>
|
||||
<p className="text-[11px] text-gray-400 mt-2 leading-relaxed">Landingpage unter: <span className="text-[#E63946] font-medium">{formData.slug ? `${appBaseUrl}/${formData.slug}` : `${appBaseUrl}/ihr-slug`}</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Planungs-Modell</label>
|
||||
@@ -407,8 +410,8 @@ export function CreateOrgForm() {
|
||||
|
||||
<div className="bg-[#F8FEFB] p-6 rounded-2xl border border-[#E1F5EA] text-left mb-8">
|
||||
<p className="text-[10px] font-bold text-[#8CAB99] uppercase tracking-[0.15em] mb-4">Ihre neue Landingpage (Localhost) / Subdomain</p>
|
||||
<a href={`http://${formData.slug}.localhost:3032`} target="_blank" rel="noreferrer" className="text-[#E63946] font-bold text-lg hover:underline block break-all">
|
||||
{formData.slug}.localhost:3032
|
||||
<a href={`${appBaseUrl}/${formData.slug}`} target="_blank" rel="noreferrer" className="text-[#E63946] font-bold text-lg hover:underline block break-all">
|
||||
{appBaseUrl}/{formData.slug}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use server'
|
||||
|
||||
import { prisma } from '@innungsapp/shared'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { prisma, Prisma } from '@innungsapp/shared'
|
||||
import { auth, getSanitizedHeaders } from '@/lib/auth'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { headers } from 'next/headers'
|
||||
import { z } from 'zod'
|
||||
import { sendAdminCredentialsEmail } from '@/lib/email'
|
||||
// @ts-ignore
|
||||
@@ -14,6 +13,14 @@ function normalizeEmail(email: string | null | undefined): string {
|
||||
return (email ?? '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
function toJsonbText(value: string | undefined): Prisma.InputJsonValue | Prisma.NullableJsonNullValueInput {
|
||||
if (!value) {
|
||||
return Prisma.DbNull
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a credential (email+password) account for a user.
|
||||
* Uses direct DB write with better-auth's hashPassword for compatibility.
|
||||
@@ -39,7 +46,7 @@ async function setCredentialPassword(userId: string, password: string) {
|
||||
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
const session = await auth.api.getSession({ headers: await getSanitizedHeaders() })
|
||||
const superAdminEmail = process.env.SUPERADMIN_EMAIL || 'superadmin@innungsapp.de'
|
||||
|
||||
// An admin is either specifically the superadmin email OR has the 'admin' role from better-auth admin plugin
|
||||
@@ -165,8 +172,8 @@ export async function createOrganization(prevState: any, formData: FormData) {
|
||||
landingPageHeroImage: validatedData.landingPageHeroImage || null,
|
||||
// @ts-ignore
|
||||
landingPageHeroOverlayOpacity: validatedData.landingPageHeroOverlayOpacity,
|
||||
landingPageFeatures: validatedData.landingPageFeatures || null,
|
||||
landingPageFooter: validatedData.landingPageFooter || null,
|
||||
landingPageFeatures: toJsonbText(validatedData.landingPageFeatures),
|
||||
landingPageFooter: toJsonbText(validatedData.landingPageFooter),
|
||||
landingPageSectionTitle: validatedData.landingPageSectionTitle || null,
|
||||
landingPageButtonText: validatedData.landingPageButtonText || null,
|
||||
appStoreUrl: validatedData.appStoreUrl || null,
|
||||
@@ -221,7 +228,7 @@ export async function createOrganization(prevState: any, formData: FormData) {
|
||||
adminName: user.name || validatedData.adminEmail.split('@')[0],
|
||||
orgName: org.name,
|
||||
password: validatedData.adminPassword,
|
||||
loginUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3032',
|
||||
loginUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3010',
|
||||
})
|
||||
} catch (emailError) {
|
||||
console.error('E-Mail konnte nicht gesendet werden:', emailError)
|
||||
@@ -276,8 +283,8 @@ export async function updateOrganization(id: string, prevState: any, formData: F
|
||||
landingPageTitle: validatedData.landingPageTitle || null,
|
||||
landingPageText: validatedData.landingPageText || null,
|
||||
landingPageHeroImage: validatedData.landingPageHeroImage || null,
|
||||
landingPageFeatures: validatedData.landingPageFeatures || null,
|
||||
landingPageFooter: validatedData.landingPageFooter || null,
|
||||
landingPageFeatures: toJsonbText(validatedData.landingPageFeatures),
|
||||
landingPageFooter: toJsonbText(validatedData.landingPageFooter),
|
||||
landingPageSectionTitle: validatedData.landingPageSectionTitle || null,
|
||||
landingPageButtonText: validatedData.landingPageButtonText || null,
|
||||
appStoreUrl: validatedData.appStoreUrl || null,
|
||||
@@ -383,7 +390,7 @@ export async function createAdmin(prevState: any, formData: FormData) {
|
||||
adminName: validatedData.name,
|
||||
orgName: org?.name || 'Ihre Innung',
|
||||
password: validatedData.password,
|
||||
loginUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3032',
|
||||
loginUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3010',
|
||||
})
|
||||
} catch (emailError) {
|
||||
console.error('E-Mail konnte nicht gesendet werden (Admin wurde trotzdem angelegt):', emailError)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { auth } from '@/lib/auth'
|
||||
import { headers } from 'next/headers'
|
||||
import { auth, getSanitizedHeaders } from '@/lib/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
@@ -8,7 +7,7 @@ export default async function SuperAdminLayout({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
const session = await auth.api.getSession({ headers: await getSanitizedHeaders() })
|
||||
|
||||
if (!session?.user) {
|
||||
redirect('/login')
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
import { useActionState, useState } from 'react'
|
||||
import { updateOrganization } from '../../actions'
|
||||
|
||||
function jsonToText(value: unknown): string {
|
||||
if (value == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => (typeof item === 'string' ? item : JSON.stringify(item)))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
org: {
|
||||
id: string
|
||||
@@ -18,8 +36,8 @@ interface Props {
|
||||
landingPageButtonText: string | null
|
||||
landingPageHeroImage: string | null
|
||||
landingPageHeroOverlayOpacity: number | null
|
||||
landingPageFeatures: string | null
|
||||
landingPageFooter: string | null
|
||||
landingPageFeatures: unknown
|
||||
landingPageFooter: unknown
|
||||
appStoreUrl: string | null
|
||||
playStoreUrl: string | null
|
||||
}
|
||||
@@ -36,19 +54,8 @@ export function EditOrgForm({ org }: Props) {
|
||||
const [themeColor, setThemeColor] = useState(org.primaryColor || '#E63946')
|
||||
const [secondaryColor, setSecondaryColor] = useState(org.secondaryColor || '#FFFFFF')
|
||||
|
||||
let initialFeatures = ''
|
||||
try {
|
||||
if (org.landingPageFeatures) {
|
||||
const parsed = JSON.parse(org.landingPageFeatures)
|
||||
if (Array.isArray(parsed)) {
|
||||
initialFeatures = parsed.join('\n')
|
||||
} else {
|
||||
initialFeatures = org.landingPageFeatures
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
initialFeatures = org.landingPageFeatures || ''
|
||||
}
|
||||
const initialFeatures = jsonToText(org.landingPageFeatures)
|
||||
const initialFooter = jsonToText(org.landingPageFooter)
|
||||
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>, type: 'logo' | 'hero') => {
|
||||
const file = e.target.files?.[0]
|
||||
@@ -321,7 +328,7 @@ export function EditOrgForm({ org }: Props) {
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Footer Text</label>
|
||||
<textarea
|
||||
name="landingPageFooter"
|
||||
defaultValue={org.landingPageFooter ?? ''}
|
||||
defaultValue={initialFooter}
|
||||
rows={2}
|
||||
placeholder="© 2024 Innung. Alle Rechte vorbehalten."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
|
||||
Reference in New Issue
Block a user