feat: initial workflow portal - multi-tenant architecture
- Backend: Node.js + Express + TypeScript + Prisma + PostgreSQL - Frontend: React + TypeScript + Tailwind CSS + Vite - Multi-tenant with role-based access (SystemAdmin, Technician, Customer) - Workflow editor with drag-and-drop steps - JWT authentication with refresh tokens - German UI throughout
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Tenant {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
slug String @unique
|
||||
logo String?
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
users User[]
|
||||
workflows Workflow[]
|
||||
categories Category[]
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
passwordHash String
|
||||
firstName String
|
||||
lastName String
|
||||
role Role
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id])
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdWorkflows Workflow[] @relation("CreatedBy")
|
||||
updatedWorkflows Workflow[] @relation("UpdatedBy")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
SYSTEM_ADMIN
|
||||
TECHNICIAN
|
||||
CUSTOMER_ADMIN
|
||||
CUSTOMER_USER
|
||||
}
|
||||
|
||||
model Category {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
icon String?
|
||||
tenantId String?
|
||||
tenant Tenant? @relation(fields: [tenantId], references: [id])
|
||||
workflows Workflow[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Workflow {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String?
|
||||
tenantId String
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
categoryId String?
|
||||
category Category? @relation(fields: [categoryId], references: [id])
|
||||
status WorkflowStatus @default(DRAFT)
|
||||
version Int @default(1)
|
||||
isTemplate Boolean @default(false)
|
||||
tags String[]
|
||||
createdById String
|
||||
createdBy User @relation("CreatedBy", fields: [createdById], references: [id])
|
||||
updatedById String?
|
||||
updatedBy User? @relation("UpdatedBy", fields: [updatedById], references: [id])
|
||||
steps WorkflowStep[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
enum WorkflowStatus {
|
||||
DRAFT
|
||||
REVIEW
|
||||
PUBLISHED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
model WorkflowStep {
|
||||
id String @id @default(uuid())
|
||||
workflowId String
|
||||
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
|
||||
order Int
|
||||
title String
|
||||
content String
|
||||
type StepType @default(INSTRUCTION)
|
||||
isRequired Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([workflowId])
|
||||
}
|
||||
|
||||
enum StepType {
|
||||
INSTRUCTION
|
||||
CHECKLIST
|
||||
WARNING
|
||||
INFO
|
||||
COMMAND
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { PrismaClient, Role, WorkflowStatus, StepType } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('Seeding database...');
|
||||
|
||||
// Create default tenant "Systemhaus"
|
||||
const systemhausTenant = await prisma.tenant.upsert({
|
||||
where: { slug: 'systemhaus' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Systemhaus',
|
||||
slug: 'systemhaus',
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
console.log('Created tenant:', systemhausTenant.name);
|
||||
|
||||
// Create sample tenant "Musterkunde GmbH"
|
||||
const musterkundeTenant = await prisma.tenant.upsert({
|
||||
where: { slug: 'musterkunde' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Musterkunde GmbH',
|
||||
slug: 'musterkunde',
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
console.log('Created tenant:', musterkundeTenant.name);
|
||||
|
||||
// Create system admin user
|
||||
const adminPasswordHash = await bcrypt.hash('admin123', 12);
|
||||
const systemAdmin = await prisma.user.upsert({
|
||||
where: { email: 'admin@systemhaus.de' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@systemhaus.de',
|
||||
passwordHash: adminPasswordHash,
|
||||
firstName: 'System',
|
||||
lastName: 'Administrator',
|
||||
role: Role.SYSTEM_ADMIN,
|
||||
tenantId: systemhausTenant.id,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
console.log('Created user:', systemAdmin.email, '(SYSTEM_ADMIN)');
|
||||
|
||||
// Create technician user
|
||||
const technicianPasswordHash = await bcrypt.hash('admin123', 12);
|
||||
const technician = await prisma.user.upsert({
|
||||
where: { email: 'techniker@systemhaus.de' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'techniker@systemhaus.de',
|
||||
passwordHash: technicianPasswordHash,
|
||||
firstName: 'Max',
|
||||
lastName: 'Techniker',
|
||||
role: Role.TECHNICIAN,
|
||||
tenantId: systemhausTenant.id,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
console.log('Created user:', technician.email, '(TECHNICIAN)');
|
||||
|
||||
// Create customer admin user
|
||||
const customerAdminPasswordHash = await bcrypt.hash('admin123', 12);
|
||||
const customerAdmin = await prisma.user.upsert({
|
||||
where: { email: 'admin@musterkunde.de' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@musterkunde.de',
|
||||
passwordHash: customerAdminPasswordHash,
|
||||
firstName: 'Maria',
|
||||
lastName: 'Mustermann',
|
||||
role: Role.CUSTOMER_ADMIN,
|
||||
tenantId: musterkundeTenant.id,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
console.log('Created user:', customerAdmin.email, '(CUSTOMER_ADMIN)');
|
||||
|
||||
// Create customer user
|
||||
const customerUserPasswordHash = await bcrypt.hash('admin123', 12);
|
||||
const customerUser = await prisma.user.upsert({
|
||||
where: { email: 'user@musterkunde.de' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'user@musterkunde.de',
|
||||
passwordHash: customerUserPasswordHash,
|
||||
firstName: 'Hans',
|
||||
lastName: 'Benutzer',
|
||||
role: Role.CUSTOMER_USER,
|
||||
tenantId: musterkundeTenant.id,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
console.log('Created user:', customerUser.email, '(CUSTOMER_USER)');
|
||||
|
||||
// Create categories (global, no tenant)
|
||||
const categories = [
|
||||
{ name: 'Benutzerverwaltung', icon: 'users' },
|
||||
{ name: 'Netzwerk', icon: 'network' },
|
||||
{ name: 'VPN', icon: 'shield' },
|
||||
{ name: 'Backup', icon: 'database' },
|
||||
{ name: 'Server', icon: 'server' },
|
||||
];
|
||||
|
||||
const createdCategories: Record<string, string> = {};
|
||||
for (const cat of categories) {
|
||||
const existing = await prisma.category.findFirst({
|
||||
where: { name: cat.name, tenantId: null },
|
||||
});
|
||||
if (existing) {
|
||||
createdCategories[cat.name] = existing.id;
|
||||
console.log('Category already exists:', cat.name);
|
||||
} else {
|
||||
const created = await prisma.category.create({
|
||||
data: {
|
||||
name: cat.name,
|
||||
icon: cat.icon,
|
||||
tenantId: null,
|
||||
},
|
||||
});
|
||||
createdCategories[cat.name] = created.id;
|
||||
console.log('Created category:', cat.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Create sample workflow: "Neuen Benutzer anlegen"
|
||||
const existingWorkflow = await prisma.workflow.findFirst({
|
||||
where: {
|
||||
title: 'Neuen Benutzer anlegen',
|
||||
tenantId: musterkundeTenant.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingWorkflow) {
|
||||
const workflow = await prisma.workflow.create({
|
||||
data: {
|
||||
title: 'Neuen Benutzer anlegen',
|
||||
description: 'Schritt-fuer-Schritt Anleitung zum Anlegen eines neuen Benutzers im Active Directory und allen relevanten Systemen.',
|
||||
tenantId: musterkundeTenant.id,
|
||||
categoryId: createdCategories['Benutzerverwaltung'],
|
||||
status: WorkflowStatus.PUBLISHED,
|
||||
version: 1,
|
||||
isTemplate: false,
|
||||
tags: ['benutzer', 'onboarding', 'active-directory'],
|
||||
createdById: systemAdmin.id,
|
||||
steps: {
|
||||
create: [
|
||||
{
|
||||
order: 0,
|
||||
title: 'Anforderung pruefen',
|
||||
content: 'Pruefen Sie die Benutzeranforderung auf Vollstaendigkeit:\n- Vor- und Nachname\n- Abteilung\n- Vorgesetzter\n- Benoetigte Berechtigungen\n- Startdatum',
|
||||
type: StepType.CHECKLIST,
|
||||
isRequired: true,
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
title: 'Active Directory Benutzer erstellen',
|
||||
content: 'Oeffnen Sie die Active Directory Benutzer und Computer Konsole und erstellen Sie einen neuen Benutzer:\n\n```powershell\nNew-ADUser -Name "Vorname Nachname" -GivenName "Vorname" -Surname "Nachname" -SamAccountName "v.nachname" -UserPrincipalName "v.nachname@domain.de" -Path "OU=Users,DC=domain,DC=de" -AccountPassword (ConvertTo-SecureString "TempPasswort123!" -AsPlainText -Force) -Enabled $true -ChangePasswordAtLogon $true\n```',
|
||||
type: StepType.COMMAND,
|
||||
isRequired: true,
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
title: 'Gruppenmitgliedschaften konfigurieren',
|
||||
content: 'Fuegen Sie den Benutzer den entsprechenden Sicherheitsgruppen hinzu basierend auf Abteilung und Rolle:\n- Abteilungsgruppe\n- VPN-Zugang (falls erforderlich)\n- Drucker-Gruppen\n- Anwendungs-Gruppen',
|
||||
type: StepType.INSTRUCTION,
|
||||
isRequired: true,
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
title: 'E-Mail Postfach einrichten',
|
||||
content: 'WICHTIG: Warten Sie nach der AD-Erstellung mindestens 15 Minuten auf die Exchange-Synchronisation. Pruefen Sie anschliessend, ob das Postfach automatisch erstellt wurde. Falls nicht, erstellen Sie es manuell im Exchange Admin Center.',
|
||||
type: StepType.WARNING,
|
||||
isRequired: true,
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
title: 'Dokumentation und Uebergabe',
|
||||
content: 'Dokumentieren Sie die Zugangsdaten und informieren Sie den Vorgesetzten:\n- Benutzername\n- Initiales Passwort (sicher uebermitteln!)\n- E-Mail Adresse\n- VPN-Zugangsdaten (falls zutreffend)\n\nHinweis: Verwenden Sie einen sicheren Kanal fuer die Passwort-Uebermittlung.',
|
||||
type: StepType.INFO,
|
||||
isRequired: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log('Created workflow:', workflow.title, 'with 5 steps');
|
||||
} else {
|
||||
console.log('Workflow already exists:', existingWorkflow.title);
|
||||
}
|
||||
|
||||
console.log('\nSeed complete!');
|
||||
console.log('\nTest Accounts:');
|
||||
console.log(' System Admin: admin@systemhaus.de / admin123');
|
||||
console.log(' Technician: techniker@systemhaus.de / admin123');
|
||||
console.log(' Customer Admin: admin@musterkunde.de / admin123');
|
||||
console.log(' Customer User: user@musterkunde.de / admin123');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('Seed error:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user