Replace hardcoded .env configuration with database-backed settings
manageable through the Admin web interface. This reduces .env to
bootstrap-only variables (DB, Keycloak, encryption keys).
Backend:
- Add SystemSetting Prisma model with category, valueType, isSecret
- Add system-settings NestJS module (CRUD, 60s cache, encryption)
- Refactor all 7 connectors to lazy-load credentials from DB via
CredentialsService.findActiveByType() instead of ConfigService
- Add event-driven credential reload (@nestjs/event-emitter)
- Dynamic CORS origins and conditional Swagger from DB settings
- Fix JWT strategy: use Keycloak JWKS (RS256) instead of symmetric secret
- Add SYSTEM_SETTINGS_VIEW/MANAGE permissions
- Seed 13 default settings (sync intervals, features, branding, CORS)
- Add env-to-db migration script (prisma/migrate-env-to-db.ts)
Frontend:
- Add use-credentials hook (full CRUD for integration credentials)
- Add use-system-settings hook (read/update system settings)
- Wire admin-integrations page to real API (create/update/test/toggle)
- Add admin system-settings page with 4 tabs (Branding, CORS, Sync, Features)
- Fix sidebar double-highlighting with exactMatch flag
- Fix integration detail fallback when API unavailable
- Fix API client to unwrap backend's {success, data} envelope
- Update NEXT_PUBLIC_API_URL to include /v1 version prefix
- Fix activity-widget hydration error
- Add i18n keys for systemSettings (de + en)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
255 lines
7.7 KiB
TypeScript
255 lines
7.7 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
async function main() {
|
|
console.log('Seeding database...');
|
|
|
|
// Create default roles
|
|
const roles = await Promise.all([
|
|
prisma.role.upsert({
|
|
where: { name: 'admin' },
|
|
update: {},
|
|
create: {
|
|
name: 'admin',
|
|
description: 'Administrator with full system access',
|
|
permissions: JSON.stringify([
|
|
'users:read',
|
|
'users:write',
|
|
'users:delete',
|
|
'employees:read',
|
|
'employees:write',
|
|
'employees:delete',
|
|
'departments:read',
|
|
'departments:write',
|
|
'departments:delete',
|
|
'roles:read',
|
|
'roles:write',
|
|
'roles:delete',
|
|
'settings:read',
|
|
'settings:write',
|
|
]),
|
|
isSystem: true,
|
|
},
|
|
}),
|
|
prisma.role.upsert({
|
|
where: { name: 'hr-manager' },
|
|
update: {},
|
|
create: {
|
|
name: 'hr-manager',
|
|
description: 'HR Manager with access to employee data',
|
|
permissions: JSON.stringify([
|
|
'users:read',
|
|
'users:write',
|
|
'employees:read',
|
|
'employees:write',
|
|
'departments:read',
|
|
'absences:read',
|
|
'absences:write',
|
|
'time-entries:read',
|
|
'time-entries:write',
|
|
'reviews:read',
|
|
'reviews:write',
|
|
]),
|
|
isSystem: true,
|
|
},
|
|
}),
|
|
prisma.role.upsert({
|
|
where: { name: 'team-lead' },
|
|
update: {},
|
|
create: {
|
|
name: 'team-lead',
|
|
description: 'Team leader with access to team data',
|
|
permissions: JSON.stringify([
|
|
'users:read',
|
|
'employees:read',
|
|
'departments:read',
|
|
'absences:read',
|
|
'absences:approve',
|
|
'time-entries:read',
|
|
'reviews:read',
|
|
'reviews:write',
|
|
'skills:read',
|
|
'skills:write',
|
|
]),
|
|
isSystem: true,
|
|
},
|
|
}),
|
|
prisma.role.upsert({
|
|
where: { name: 'employee' },
|
|
update: {},
|
|
create: {
|
|
name: 'employee',
|
|
description: 'Regular employee with basic access',
|
|
permissions: JSON.stringify([
|
|
'profile:read',
|
|
'profile:write',
|
|
'absences:read',
|
|
'absences:request',
|
|
'time-entries:read',
|
|
'time-entries:write',
|
|
'skills:read',
|
|
]),
|
|
isSystem: true,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
console.log(`Created ${roles.length} roles`);
|
|
|
|
// Create default departments
|
|
const departments = await Promise.all([
|
|
prisma.department.upsert({
|
|
where: { code: 'MGMT' },
|
|
update: {},
|
|
create: {
|
|
name: 'Management',
|
|
code: 'MGMT',
|
|
},
|
|
}),
|
|
prisma.department.upsert({
|
|
where: { code: 'HR' },
|
|
update: {},
|
|
create: {
|
|
name: 'Human Resources',
|
|
code: 'HR',
|
|
},
|
|
}),
|
|
prisma.department.upsert({
|
|
where: { code: 'ENG' },
|
|
update: {},
|
|
create: {
|
|
name: 'Engineering',
|
|
code: 'ENG',
|
|
},
|
|
}),
|
|
prisma.department.upsert({
|
|
where: { code: 'PROD' },
|
|
update: {},
|
|
create: {
|
|
name: 'Production',
|
|
code: 'PROD',
|
|
},
|
|
}),
|
|
prisma.department.upsert({
|
|
where: { code: 'QA' },
|
|
update: {},
|
|
create: {
|
|
name: 'Quality Assurance',
|
|
code: 'QA',
|
|
},
|
|
}),
|
|
]);
|
|
|
|
console.log(`Created ${departments.length} departments`);
|
|
|
|
// Create admin user (for development)
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
const adminRole = roles.find((r) => r.name === 'admin');
|
|
const mgmtDept = departments.find((d) => d.code === 'MGMT');
|
|
|
|
if (adminRole && mgmtDept) {
|
|
const adminUser = await prisma.user.upsert({
|
|
where: { email: 'admin@tos.local' },
|
|
update: {},
|
|
create: {
|
|
email: 'admin@tos.local',
|
|
firstName: 'System',
|
|
lastName: 'Administrator',
|
|
departmentId: mgmtDept.id,
|
|
roles: {
|
|
create: {
|
|
roleId: adminRole.id,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
console.log(`Created admin user: ${adminUser.email}`);
|
|
}
|
|
}
|
|
|
|
// Create default skills
|
|
const skills = await Promise.all([
|
|
prisma.skill.upsert({
|
|
where: { name_departmentId: { name: 'Communication', departmentId: null as unknown as string } },
|
|
update: {},
|
|
create: {
|
|
name: 'Communication',
|
|
description: 'Effective verbal and written communication',
|
|
category: 'Soft Skills',
|
|
},
|
|
}),
|
|
prisma.skill.upsert({
|
|
where: { name_departmentId: { name: 'Problem Solving', departmentId: null as unknown as string } },
|
|
update: {},
|
|
create: {
|
|
name: 'Problem Solving',
|
|
description: 'Analytical thinking and problem resolution',
|
|
category: 'Soft Skills',
|
|
},
|
|
}),
|
|
prisma.skill.upsert({
|
|
where: { name_departmentId: { name: 'Team Collaboration', departmentId: null as unknown as string } },
|
|
update: {},
|
|
create: {
|
|
name: 'Team Collaboration',
|
|
description: 'Working effectively in a team environment',
|
|
category: 'Soft Skills',
|
|
},
|
|
}),
|
|
]);
|
|
|
|
console.log(`Created ${skills.length} skills`);
|
|
|
|
// Seed default system settings
|
|
const defaultSettings = [
|
|
// Sync intervals
|
|
{ key: 'sync.interval.plentyone', value: '15', category: 'sync', valueType: 'number', description: 'PlentyONE Sync-Intervall (Minuten)' },
|
|
{ key: 'sync.interval.zulip', value: '5', category: 'sync', valueType: 'number', description: 'Zulip Sync-Intervall (Minuten)' },
|
|
{ key: 'sync.interval.todoist', value: '10', category: 'sync', valueType: 'number', description: 'Todoist Sync-Intervall (Minuten)' },
|
|
{ key: 'sync.interval.freescout', value: '10', category: 'sync', valueType: 'number', description: 'FreeScout Sync-Intervall (Minuten)' },
|
|
{ key: 'sync.interval.nextcloud', value: '30', category: 'sync', valueType: 'number', description: 'Nextcloud Sync-Intervall (Minuten)' },
|
|
{ key: 'sync.interval.ecodms', value: '60', category: 'sync', valueType: 'number', description: 'ecoDMS Sync-Intervall (Minuten)' },
|
|
{ key: 'sync.interval.gembadocs', value: '30', category: 'sync', valueType: 'number', description: 'GembaDocs Sync-Intervall (Minuten)' },
|
|
// Feature flags
|
|
{ key: 'feature.syncJobs.enabled', value: 'false', category: 'feature', valueType: 'boolean', description: 'Hintergrund-Sync-Jobs aktivieren' },
|
|
{ key: 'feature.swagger.enabled', value: 'true', category: 'feature', valueType: 'boolean', description: 'Swagger API-Dokumentation aktivieren' },
|
|
// CORS
|
|
{ key: 'cors.origins', value: 'http://localhost:3000', category: 'cors', valueType: 'string', description: 'Erlaubte CORS Origins (kommagetrennt)' },
|
|
// Branding
|
|
{ key: 'branding.appName', value: 'tOS', category: 'branding', valueType: 'string', description: 'Anwendungsname' },
|
|
{ key: 'branding.companyName', value: '', category: 'branding', valueType: 'string', description: 'Firmenname' },
|
|
{ key: 'branding.logoUrl', value: '', category: 'branding', valueType: 'string', description: 'Logo-URL' },
|
|
];
|
|
|
|
await Promise.all(
|
|
defaultSettings.map((setting) =>
|
|
prisma.systemSetting.upsert({
|
|
where: { key: setting.key },
|
|
update: {},
|
|
create: {
|
|
key: setting.key,
|
|
value: setting.value,
|
|
category: setting.category,
|
|
valueType: setting.valueType,
|
|
description: setting.description,
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
|
|
console.log(`Seeded ${defaultSettings.length} system settings`);
|
|
|
|
console.log('Database seeding completed!');
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error('Error seeding database:', e);
|
|
process.exit(1);
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
});
|