KI-AGENT: Erstzugriff und Mandanten-Grunddaten für Selfhosting ergänzen
This commit is contained in:
@@ -47,6 +47,15 @@ OPENAI_API_KEY=replace-this
|
||||
STIRLING_API_KEY=replace-this
|
||||
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
|
||||
|
||||
# Optionaler Erststart-Bootstrap. Wenn gesetzt, werden Admin, Mandant,
|
||||
# Admin-Rolle und Grunddaten beim Backend-Start idempotent angelegt.
|
||||
FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=change-this-admin-password
|
||||
FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
|
||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
||||
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
||||
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
||||
|
||||
# FEDEO Matrix-Kommunikation
|
||||
#
|
||||
# Diese Werte werden von docker-compose.yml gelesen, wenn das Profil "matrix"
|
||||
|
||||
11
README.md
11
README.md
@@ -184,8 +184,17 @@ OPENAI_API_KEY=replace-this
|
||||
STIRLING_API_KEY=replace-this
|
||||
|
||||
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
|
||||
|
||||
FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=change-this-admin-password
|
||||
FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
|
||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
||||
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
||||
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
||||
```
|
||||
|
||||
Die `FEDEO_BOOTSTRAP_*`-Werte sind für den ersten Start gedacht. Wenn `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` gesetzt sind, legt das Backend idempotent einen Admin-Benutzer, einen ersten Mandanten, eine Administrator-Rolle und grundlegende Stammdaten an. Nach erfolgreichem Erstzugriff solltest du das Bootstrap-Passwort aus der `.env` entfernen oder ändern.
|
||||
|
||||
## Docker Compose mit optionaler S3-MinIO-Option
|
||||
|
||||
Die Selfhost-Konfiguration liegt in `docker-compose.selfhost.yml`. Sie startet MinIO standardmäßig mit. Wenn du stattdessen AWS S3, Hetzner Object Storage, Backblaze B2 S3 oder einen anderen externen S3-Dienst nutzen willst, kannst du die Services `minio` und `createbuckets` entfernen und nur die entsprechenden S3-Umgebungsvariablen auf den externen Anbieter zeigen lassen.
|
||||
@@ -414,6 +423,8 @@ Erwartung:
|
||||
- Frontend liefert `200` oder `302`
|
||||
- Backend liefert JSON wie `{"status":"ok"}`
|
||||
|
||||
Wenn der Bootstrap aktiviert ist, kannst du dich danach mit `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` anmelden. Die Mandantensperre wird über `locked` gesteuert; `hasActiveLicense` wird nicht mehr für den Selfhost-Zugriff ausgewertet.
|
||||
|
||||
## Updates
|
||||
|
||||
Bei neuen Versionen:
|
||||
|
||||
@@ -56,6 +56,7 @@ import {sendMail} from "./utils/mailer";
|
||||
import {loadSecrets, secrets} from "./utils/secrets";
|
||||
import {initMailer} from "./utils/mailer"
|
||||
import {initS3} from "./utils/s3";
|
||||
import { runBootstrap } from "./modules/bootstrap.service";
|
||||
|
||||
|
||||
//Services
|
||||
@@ -80,6 +81,7 @@ async function main() {
|
||||
await app.register(dayjsPlugin);
|
||||
await app.register(dbPlugin);
|
||||
await app.register(servicesPlugin);
|
||||
await runBootstrap(app);
|
||||
|
||||
app.addHook('preHandler', (req, reply, done) => {
|
||||
console.log(req.method)
|
||||
|
||||
490
backend/src/modules/bootstrap.service.ts
Normal file
490
backend/src/modules/bootstrap.service.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { and, eq } from "drizzle-orm"
|
||||
import bcrypt from "bcrypt"
|
||||
|
||||
import {
|
||||
accounts,
|
||||
authProfiles,
|
||||
authRoles,
|
||||
authRolePermissions,
|
||||
authTenantUsers,
|
||||
authUserRoles,
|
||||
authUsers,
|
||||
branches,
|
||||
filetags,
|
||||
folders,
|
||||
productcategories,
|
||||
servicecategories,
|
||||
taxTypes,
|
||||
teams,
|
||||
tenants,
|
||||
texttemplates,
|
||||
units,
|
||||
} from "../../db/schema"
|
||||
|
||||
const adminPermissions = [
|
||||
"mcp.tokens.write",
|
||||
"staff.time.read_all",
|
||||
"masterdata.customers.read",
|
||||
"masterdata.vendors.read",
|
||||
"masterdata.contacts.read",
|
||||
"masterdata.products.read",
|
||||
"masterdata.services.read",
|
||||
"masterdata.cost_centres.read",
|
||||
"masterdata.branches.read",
|
||||
"masterdata.teams.read",
|
||||
"masterdata.vehicles.read",
|
||||
"masterdata.inventory_items.read",
|
||||
"masterdata.units.read",
|
||||
"accounting.outgoing_documents.read",
|
||||
"accounting.outgoing_documents.write",
|
||||
"accounting.accounts.read",
|
||||
"accounting.incoming_invoices.read",
|
||||
"accounting.incoming_invoices.write",
|
||||
"accounting.bank.read",
|
||||
"accounting.statement_allocations.read",
|
||||
"organisation.customers.read",
|
||||
"organisation.projects.read",
|
||||
"organisation.plants.read",
|
||||
"organisation.events.read",
|
||||
"organisation.tasks.read",
|
||||
"organisation.tasks.write",
|
||||
]
|
||||
|
||||
const defaultUnits = [
|
||||
{ name: "Stück", single: "Stück", multiple: "Stück", short: "Stk.", step: "1" },
|
||||
{ name: "Stunde", single: "Stunde", multiple: "Stunden", short: "Std.", step: "0.25" },
|
||||
{ name: "Pauschale", single: "Pauschale", multiple: "Pauschalen", short: "Psch.", step: "1" },
|
||||
{ name: "Meter", single: "Meter", multiple: "Meter", short: "m", step: "0.1" },
|
||||
]
|
||||
|
||||
const defaultTaxTypes = [
|
||||
{ label: "Umsatzsteuer 19%", percentage: 19 },
|
||||
{ label: "Umsatzsteuer 7%", percentage: 7 },
|
||||
{ label: "Steuerfrei", percentage: 0 },
|
||||
]
|
||||
|
||||
const defaultAccounts = [
|
||||
{ number: "8400", label: "Erlöse 19% USt", accountChart: "skr03" },
|
||||
{ number: "8300", label: "Erlöse 7% USt", accountChart: "skr03" },
|
||||
{ number: "1200", label: "Bank", accountChart: "skr03" },
|
||||
{ number: "1000", label: "Kasse", accountChart: "skr03" },
|
||||
{ number: "1400", label: "Forderungen aus Lieferungen und Leistungen", accountChart: "skr03" },
|
||||
{ number: "1600", label: "Verbindlichkeiten aus Lieferungen und Leistungen", accountChart: "skr03" },
|
||||
]
|
||||
|
||||
async function ensureGlobalDefaults(server: FastifyInstance, userId: string) {
|
||||
for (const unit of defaultUnits) {
|
||||
const existing = await server.db.select({ id: units.id }).from(units).where(eq(units.name, unit.name)).limit(1)
|
||||
if (!existing.length) await server.db.insert(units).values(unit)
|
||||
}
|
||||
|
||||
for (const taxType of defaultTaxTypes) {
|
||||
const existing = await server.db
|
||||
.select({ id: taxTypes.id })
|
||||
.from(taxTypes)
|
||||
.where(eq(taxTypes.percentage, taxType.percentage))
|
||||
.limit(1)
|
||||
|
||||
if (!existing.length) {
|
||||
await server.db.insert(taxTypes).values({
|
||||
...taxType,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: userId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const account of defaultAccounts) {
|
||||
const existing = await server.db
|
||||
.select({ id: accounts.id })
|
||||
.from(accounts)
|
||||
.where(and(eq(accounts.accountChart, account.accountChart), eq(accounts.number, account.number)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing.length) {
|
||||
await server.db.insert(accounts).values({
|
||||
...account,
|
||||
description: "FEDEO Standardkonto",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTenantFileDefaults(server: FastifyInstance, tenantId: number, userId: string) {
|
||||
const currentYear = new Date().getFullYear()
|
||||
const timestamp = new Date()
|
||||
|
||||
const tagDefaults = [
|
||||
{ name: "Rechnungen", color: "#16a34a", createdDocumentType: "invoices" },
|
||||
{ name: "Angebote", color: "#2563eb", createdDocumentType: "quotes" },
|
||||
{ name: "Auftragsbestätigungen", color: "#7c3aed", createdDocumentType: "confirmationOrders" },
|
||||
{ name: "Lieferscheine", color: "#ea580c", createdDocumentType: "deliveryNotes" },
|
||||
{ name: "Eingangsrechnungen", color: "#dc2626", incomingDocumentType: "invoices" },
|
||||
{ name: "Mahnungen", color: "#b91c1c", incomingDocumentType: "reminders" },
|
||||
]
|
||||
|
||||
for (const tag of tagDefaults) {
|
||||
const existing = await server.db
|
||||
.select({ id: filetags.id })
|
||||
.from(filetags)
|
||||
.where(and(eq(filetags.tenant, tenantId), eq(filetags.name, tag.name)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing.length) {
|
||||
await server.db.insert(filetags).values({
|
||||
tenant: tenantId,
|
||||
...tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const allTags = await server.db.select().from(filetags).where(eq(filetags.tenant, tenantId))
|
||||
const tagByCreatedType = new Map(allTags.map((tag) => [tag.createdDocumentType, tag.id]))
|
||||
const tagByIncomingType = new Map(allTags.map((tag) => [tag.incomingDocumentType, tag.id]))
|
||||
|
||||
const rootFolders = [
|
||||
{ name: "Ausgangsrechnungen", function: "yearSubCategory" as const, icon: "i-heroicons-document-text" },
|
||||
{ name: "Angebote", function: "yearSubCategory" as const, icon: "i-heroicons-document-duplicate" },
|
||||
{ name: "Auftragsbestätigungen", function: "yearSubCategory" as const, icon: "i-heroicons-clipboard-document-check" },
|
||||
{ name: "Lieferscheine", function: "yearSubCategory" as const, icon: "i-heroicons-truck" },
|
||||
{ name: "Eingangsrechnungen", function: "yearSubCategory" as const, icon: "i-heroicons-inbox-arrow-down" },
|
||||
{ name: "Belege Bankeinzahlung", function: "yearSubCategory" as const, icon: "i-heroicons-banknotes" },
|
||||
]
|
||||
|
||||
for (const folder of rootFolders) {
|
||||
const existing = await server.db
|
||||
.select({ id: folders.id })
|
||||
.from(folders)
|
||||
.where(and(eq(folders.tenant, tenantId), eq(folders.name, folder.name)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing.length) {
|
||||
await server.db.insert(folders).values({
|
||||
tenant: tenantId,
|
||||
name: folder.name,
|
||||
function: folder.function,
|
||||
icon: folder.icon,
|
||||
isSystemUsed: true,
|
||||
updatedAt: timestamp,
|
||||
updatedBy: userId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const allFolders = await server.db.select().from(folders).where(eq(folders.tenant, tenantId))
|
||||
const rootFolderByName = new Map(allFolders.filter((folder) => !folder.parent).map((folder) => [folder.name, folder.id]))
|
||||
|
||||
const yearFolders = [
|
||||
{
|
||||
parentName: "Ausgangsrechnungen",
|
||||
function: "invoices" as const,
|
||||
icon: "i-heroicons-document-text",
|
||||
standardFiletype: tagByCreatedType.get("invoices"),
|
||||
},
|
||||
{
|
||||
parentName: "Angebote",
|
||||
function: "quotes" as const,
|
||||
icon: "i-heroicons-document-duplicate",
|
||||
standardFiletype: tagByCreatedType.get("quotes"),
|
||||
},
|
||||
{
|
||||
parentName: "Auftragsbestätigungen",
|
||||
function: "confirmationOrders" as const,
|
||||
icon: "i-heroicons-clipboard-document-check",
|
||||
standardFiletype: tagByCreatedType.get("confirmationOrders"),
|
||||
},
|
||||
{
|
||||
parentName: "Lieferscheine",
|
||||
function: "deliveryNotes" as const,
|
||||
icon: "i-heroicons-truck",
|
||||
standardFiletype: tagByCreatedType.get("deliveryNotes"),
|
||||
},
|
||||
{
|
||||
parentName: "Eingangsrechnungen",
|
||||
function: "incomingInvoices" as const,
|
||||
icon: "i-heroicons-inbox-arrow-down",
|
||||
standardFiletype: tagByIncomingType.get("invoices"),
|
||||
},
|
||||
{
|
||||
parentName: "Belege Bankeinzahlung",
|
||||
function: "deposit" as const,
|
||||
icon: "i-heroicons-banknotes",
|
||||
},
|
||||
]
|
||||
|
||||
for (const folder of yearFolders) {
|
||||
const parent = rootFolderByName.get(folder.parentName)
|
||||
if (!parent) continue
|
||||
|
||||
const existing = await server.db
|
||||
.select({ id: folders.id })
|
||||
.from(folders)
|
||||
.where(and(eq(folders.tenant, tenantId), eq(folders.parent, parent), eq(folders.year, currentYear)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing.length) {
|
||||
await server.db.insert(folders).values({
|
||||
tenant: tenantId,
|
||||
name: String(currentYear),
|
||||
parent,
|
||||
function: folder.function,
|
||||
year: currentYear,
|
||||
icon: folder.icon,
|
||||
standardFiletype: folder.standardFiletype,
|
||||
standardFiletypeIsOptional: false,
|
||||
isSystemUsed: true,
|
||||
updatedAt: timestamp,
|
||||
updatedBy: userId,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureTenantBaseData(server: FastifyInstance, tenantId: number, adminUserId: string) {
|
||||
await ensureGlobalDefaults(server, adminUserId)
|
||||
await ensureTenantFileDefaults(server, tenantId, adminUserId)
|
||||
|
||||
const [adminRole] = await server.db
|
||||
.select({ id: authRoles.id })
|
||||
.from(authRoles)
|
||||
.where(and(eq(authRoles.tenant_id, tenantId), eq(authRoles.name, "Administrator")))
|
||||
.limit(1)
|
||||
|
||||
let adminRoleId = adminRole?.id
|
||||
if (!adminRoleId) {
|
||||
const [createdRole] = await server.db
|
||||
.insert(authRoles)
|
||||
.values({
|
||||
name: "Administrator",
|
||||
description: "Vollzugriff für die Administration dieses Mandanten",
|
||||
tenant_id: tenantId,
|
||||
created_by: adminUserId,
|
||||
})
|
||||
.returning({ id: authRoles.id })
|
||||
|
||||
adminRoleId = createdRole.id
|
||||
}
|
||||
|
||||
for (const permission of adminPermissions) {
|
||||
const existing = await server.db
|
||||
.select()
|
||||
.from(authRolePermissions)
|
||||
.where(and(eq(authRolePermissions.role_id, adminRoleId), eq(authRolePermissions.permission, permission)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing.length) {
|
||||
await server.db.insert(authRolePermissions).values({
|
||||
role_id: adminRoleId,
|
||||
permission,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const membership = await server.db
|
||||
.select()
|
||||
.from(authTenantUsers)
|
||||
.where(and(eq(authTenantUsers.tenant_id, tenantId), eq(authTenantUsers.user_id, adminUserId)))
|
||||
.limit(1)
|
||||
|
||||
if (!membership.length) {
|
||||
await server.db.insert(authTenantUsers).values({
|
||||
tenant_id: tenantId,
|
||||
user_id: adminUserId,
|
||||
created_by: adminUserId,
|
||||
})
|
||||
}
|
||||
|
||||
const roleAssignment = await server.db
|
||||
.select()
|
||||
.from(authUserRoles)
|
||||
.where(and(
|
||||
eq(authUserRoles.tenant_id, tenantId),
|
||||
eq(authUserRoles.user_id, adminUserId),
|
||||
eq(authUserRoles.role_id, adminRoleId),
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
if (!roleAssignment.length) {
|
||||
await server.db.insert(authUserRoles).values({
|
||||
tenant_id: tenantId,
|
||||
user_id: adminUserId,
|
||||
role_id: adminRoleId,
|
||||
created_by: adminUserId,
|
||||
})
|
||||
}
|
||||
|
||||
const profile = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(and(eq(authProfiles.tenant_id, tenantId), eq(authProfiles.user_id, adminUserId)))
|
||||
.limit(1)
|
||||
|
||||
if (!profile.length) {
|
||||
await server.db.insert(authProfiles).values({
|
||||
tenant_id: tenantId,
|
||||
user_id: adminUserId,
|
||||
first_name: process.env.FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME || "Admin",
|
||||
last_name: process.env.FEDEO_BOOTSTRAP_ADMIN_LAST_NAME || "Benutzer",
|
||||
email: process.env.FEDEO_BOOTSTRAP_ADMIN_EMAIL?.trim().toLowerCase(),
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const branch = await server.db
|
||||
.select({ id: branches.id })
|
||||
.from(branches)
|
||||
.where(and(eq(branches.tenant, tenantId), eq(branches.name, "Hauptstandort")))
|
||||
.limit(1)
|
||||
|
||||
let branchId = branch[0]?.id
|
||||
if (!branchId) {
|
||||
const [createdBranch] = await server.db.insert(branches).values({
|
||||
tenant: tenantId,
|
||||
name: "Hauptstandort",
|
||||
number: "001",
|
||||
description: "Standardstandort",
|
||||
updatedAt: new Date(),
|
||||
updatedBy: adminUserId,
|
||||
}).returning({ id: branches.id })
|
||||
branchId = createdBranch.id
|
||||
}
|
||||
|
||||
const team = await server.db
|
||||
.select({ id: teams.id })
|
||||
.from(teams)
|
||||
.where(and(eq(teams.tenant, tenantId), eq(teams.name, "Standardteam")))
|
||||
.limit(1)
|
||||
|
||||
if (!team.length) {
|
||||
await server.db.insert(teams).values({
|
||||
tenant: tenantId,
|
||||
name: "Standardteam",
|
||||
description: "Automatisch angelegtes Standardteam",
|
||||
branch: branchId,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: adminUserId,
|
||||
})
|
||||
}
|
||||
|
||||
const defaultProductCategory = await server.db
|
||||
.select({ id: productcategories.id })
|
||||
.from(productcategories)
|
||||
.where(and(eq(productcategories.tenant, tenantId), eq(productcategories.name, "Standard")))
|
||||
.limit(1)
|
||||
|
||||
if (!defaultProductCategory.length) {
|
||||
await server.db.insert(productcategories).values({
|
||||
tenant: tenantId,
|
||||
name: "Standard",
|
||||
description: "Standardkategorie",
|
||||
updatedAt: new Date(),
|
||||
updatedBy: adminUserId,
|
||||
})
|
||||
}
|
||||
|
||||
const defaultServiceCategory = await server.db
|
||||
.select({ id: servicecategories.id })
|
||||
.from(servicecategories)
|
||||
.where(and(eq(servicecategories.tenant, tenantId), eq(servicecategories.name, "Standard")))
|
||||
.limit(1)
|
||||
|
||||
if (!defaultServiceCategory.length) {
|
||||
await server.db.insert(servicecategories).values({
|
||||
tenant: tenantId,
|
||||
name: "Standard",
|
||||
description: "Standardkategorie",
|
||||
updated_at: new Date(),
|
||||
updated_by: adminUserId,
|
||||
})
|
||||
}
|
||||
|
||||
const templateDefaults = [
|
||||
{ name: "Standard Einleitung", pos: "startText" as const, text: "<p>vielen Dank für Ihre Anfrage.</p>" },
|
||||
{ name: "Standard Schluss", pos: "endText" as const, text: "<p>Mit freundlichen Grüßen</p>" },
|
||||
]
|
||||
|
||||
for (const template of templateDefaults) {
|
||||
const existing = await server.db
|
||||
.select({ id: texttemplates.id })
|
||||
.from(texttemplates)
|
||||
.where(and(eq(texttemplates.tenant, tenantId), eq(texttemplates.name, template.name)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing.length) {
|
||||
await server.db.insert(texttemplates).values({
|
||||
tenant: tenantId,
|
||||
name: template.name,
|
||||
text: template.text,
|
||||
pos: template.pos,
|
||||
default: true,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: adminUserId,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runBootstrap(server: FastifyInstance) {
|
||||
const email = process.env.FEDEO_BOOTSTRAP_ADMIN_EMAIL?.trim().toLowerCase()
|
||||
const password = process.env.FEDEO_BOOTSTRAP_ADMIN_PASSWORD
|
||||
|
||||
if (!email && !password) return
|
||||
if (!email || !password) {
|
||||
throw new Error("FEDEO_BOOTSTRAP_ADMIN_EMAIL und FEDEO_BOOTSTRAP_ADMIN_PASSWORD müssen gemeinsam gesetzt sein")
|
||||
}
|
||||
|
||||
const [existingUser] = await server.db
|
||||
.select()
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.email, email))
|
||||
.limit(1)
|
||||
|
||||
let adminUser = existingUser
|
||||
if (!adminUser) {
|
||||
const [createdUser] = await server.db.insert(authUsers).values({
|
||||
email,
|
||||
passwordHash: await bcrypt.hash(password, 10),
|
||||
is_admin: true,
|
||||
multiTenant: true,
|
||||
must_change_password: false,
|
||||
updatedAt: new Date(),
|
||||
}).returning()
|
||||
|
||||
adminUser = createdUser
|
||||
console.log(`✅ Bootstrap-Admin angelegt: ${email}`)
|
||||
} else if (!adminUser.is_admin) {
|
||||
const [updatedUser] = await server.db.update(authUsers).set({
|
||||
is_admin: true,
|
||||
updatedAt: new Date(),
|
||||
}).where(eq(authUsers.id, adminUser.id)).returning()
|
||||
|
||||
adminUser = updatedUser
|
||||
console.log(`✅ Bootstrap-Adminrechte gesetzt: ${email}`)
|
||||
}
|
||||
|
||||
const tenantName = process.env.FEDEO_BOOTSTRAP_TENANT_NAME?.trim() || "FEDEO"
|
||||
const tenantShort = process.env.FEDEO_BOOTSTRAP_TENANT_SHORT?.trim() || "FEDEO"
|
||||
|
||||
const [existingTenant] = await server.db
|
||||
.select()
|
||||
.from(tenants)
|
||||
.where(eq(tenants.short, tenantShort))
|
||||
.limit(1)
|
||||
|
||||
let tenant = existingTenant
|
||||
if (!tenant) {
|
||||
const [createdTenant] = await server.db.insert(tenants).values({
|
||||
name: tenantName,
|
||||
short: tenantShort,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: adminUser.id,
|
||||
}).returning()
|
||||
|
||||
tenant = createdTenant
|
||||
console.log(`✅ Bootstrap-Mandant angelegt: ${tenant.name}`)
|
||||
}
|
||||
|
||||
await ensureTenantBaseData(server, tenant.id, adminUser.id)
|
||||
console.log("✅ Bootstrap-Grunddaten geprüft")
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../../db/schema";
|
||||
import { generateRandomPassword, hashPassword } from "../utils/password";
|
||||
import { sendMail } from "../utils/mailer";
|
||||
import { ensureTenantBaseData } from "../modules/bootstrap.service";
|
||||
|
||||
export default async function adminRoutes(server: FastifyInstance) {
|
||||
const deriveNameFromEmail = (email: string) => {
|
||||
@@ -825,6 +826,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
});
|
||||
|
||||
await createTenantSeeds(createdTenant.id, currentUser.id);
|
||||
await ensureTenantBaseData(server, createdTenant.id, currentUser.id);
|
||||
|
||||
return { tenant: createdTenant };
|
||||
} catch (err) {
|
||||
|
||||
@@ -124,6 +124,12 @@ services:
|
||||
DOKUBOX_IMAP_PASSWORD: ${DOKUBOX_IMAP_PASSWORD}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
STIRLING_API_KEY: ${STIRLING_API_KEY}
|
||||
FEDEO_BOOTSTRAP_ADMIN_EMAIL: ${FEDEO_BOOTSTRAP_ADMIN_EMAIL:-}
|
||||
FEDEO_BOOTSTRAP_ADMIN_PASSWORD: ${FEDEO_BOOTSTRAP_ADMIN_PASSWORD:-}
|
||||
FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME: ${FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME:-Admin}
|
||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME: ${FEDEO_BOOTSTRAP_ADMIN_LAST_NAME:-Benutzer}
|
||||
FEDEO_BOOTSTRAP_TENANT_NAME: ${FEDEO_BOOTSTRAP_TENANT_NAME:-FEDEO}
|
||||
FEDEO_BOOTSTRAP_TENANT_SHORT: ${FEDEO_BOOTSTRAP_TENANT_SHORT:-FEDEO}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)
|
||||
|
||||
@@ -14,9 +14,7 @@ const tenantInitials = computed(() => {
|
||||
.join('') || 'M'
|
||||
})
|
||||
|
||||
const activeTenants = computed(() =>
|
||||
auth.tenants.filter((tenant) => tenant.hasActiveLicense)
|
||||
)
|
||||
const activeTenants = computed(() => auth.tenants)
|
||||
|
||||
const tenantItems = computed(() => [
|
||||
activeTenants.value.map((tenant) => ({
|
||||
|
||||
Reference in New Issue
Block a user