Compare commits
11 Commits
d23bfa0376
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ea0ebae1b | |||
| 2f2be739f2 | |||
| d1e9c2433c | |||
| f0c23cad35 | |||
| 39148bbb56 | |||
| e96fa8f367 | |||
| 97163becfa | |||
| 16043e2577 | |||
| e3a2ccb82d | |||
| dbb8049c7e | |||
| 24d97f6057 |
327
.gitignore
vendored
327
.gitignore
vendored
@@ -1,29 +1,342 @@
|
|||||||
|
# ============================================
|
||||||
|
# Certigo Addon - Comprehensive .gitignore
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Go / Backend
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Binaries and executables
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
backend/bin/
|
||||||
|
backend/myapp
|
||||||
|
backend/certigo-addon
|
||||||
|
backend/certigo-addon-*
|
||||||
|
/tmp/certigo-addon-*
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool
|
||||||
|
*.out
|
||||||
|
coverage.html
|
||||||
|
coverage.txt
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# Go module cache (optional, but recommended for CI/CD)
|
||||||
|
# .go/
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Node.js / Frontend
|
||||||
|
# ============================================
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
|
dist-ssr/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
backend/bin/
|
*.local
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
.vite/
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# ============================================
|
||||||
# Database
|
# Database
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# SQLite databases
|
||||||
*.db
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
backend/spaces.db
|
backend/spaces.db
|
||||||
|
backend/*.db
|
||||||
|
|
||||||
# Environment variables
|
# Database backups
|
||||||
.env
|
*.sql.backup
|
||||||
.env.local
|
*.db.backup
|
||||||
|
|
||||||
# IDE
|
# ============================================
|
||||||
|
# Uploads & User-generated Content
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# User uploads (avatars, files, etc.)
|
||||||
|
backend/uploads/
|
||||||
|
backend/uploads/**
|
||||||
|
!backend/uploads/.gitkeep
|
||||||
|
frontend/public/uploads/
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Logs
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
backend/*.log
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# IDE & Editors
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# VSCode
|
||||||
.vscode/
|
.vscode/
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# IntelliJ IDEA / WebStorm
|
||||||
.idea/
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Sublime Text
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Vim
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
*~
|
||||||
|
.vim/
|
||||||
|
|
||||||
# OS
|
# Emacs
|
||||||
|
*~
|
||||||
|
\#*\#
|
||||||
|
/.emacs.desktop
|
||||||
|
/.emacs.desktop.lock
|
||||||
|
*.elc
|
||||||
|
auto-save-list
|
||||||
|
tramp
|
||||||
|
.\#*
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# OS Files
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon
|
||||||
|
._*
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
*.stackdump
|
||||||
|
[Dd]esktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
*~
|
||||||
|
.fuse_hidden*
|
||||||
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Temporary & Cache Files
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.swp
|
||||||
|
*~.nib
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Cache directories
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
.node_repl_history
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Testing & Coverage
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
.nyc_output/
|
||||||
|
.coverage/
|
||||||
|
htmlcov/
|
||||||
|
.pytest_cache/
|
||||||
|
.tox/
|
||||||
|
|
||||||
|
# Jest
|
||||||
|
.jest/
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Build Tools & CI/CD
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
target/
|
||||||
|
.next/
|
||||||
|
.nuxt/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github/workflows/*.yml.local
|
||||||
|
.circleci/
|
||||||
|
.travis.yml.local
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Security & Secrets
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Secrets and credentials
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
*.cert
|
||||||
|
secrets/
|
||||||
|
.secrets/
|
||||||
|
*.secret
|
||||||
|
config/secrets.*
|
||||||
|
|
||||||
|
# API keys and tokens
|
||||||
|
.env.secret
|
||||||
|
.env.production
|
||||||
|
.env.staging
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Documentation Build
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Generated documentation
|
||||||
|
docs/_build/
|
||||||
|
site/
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Misc
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Package manager lock files (optional - uncomment if you want to ignore)
|
||||||
|
# package-lock.json
|
||||||
|
# yarn.lock
|
||||||
|
# pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Project-specific
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# OpenAPI generated files (if any)
|
||||||
|
backend/generated/
|
||||||
|
backend/api/
|
||||||
|
|
||||||
|
# Provider test outputs
|
||||||
|
backend/test-outputs/
|
||||||
|
|
||||||
|
# Script outputs
|
||||||
|
backend/scripts/output/
|
||||||
|
|
||||||
|
# Keep directory structure but ignore contents
|
||||||
|
!backend/uploads/.gitkeep
|
||||||
|
!backend/config/providers/.gitkeep
|
||||||
|
|||||||
74
PASSWORD_SECURITY_ANALYSIS.md
Normal file
74
PASSWORD_SECURITY_ANALYSIS.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Passwort-Speicherung Sicherheitsanalyse
|
||||||
|
|
||||||
|
## Aktuelle Implementierung
|
||||||
|
|
||||||
|
### Wie werden Passwörter gespeichert?
|
||||||
|
|
||||||
|
1. **Algorithmus**: `bcrypt` (golang.org/x/crypto/bcrypt)
|
||||||
|
2. **Cost Factor**: `bcrypt.DefaultCost` (Wert: **10**)
|
||||||
|
3. **Speicherung**:
|
||||||
|
- Feld: `password_hash TEXT NOT NULL` in SQLite
|
||||||
|
- Format: bcrypt Hash-String (enthält automatisch Salt + Hash)
|
||||||
|
- Beispiel: `$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy`
|
||||||
|
|
||||||
|
4. **Passwortrichtlinie**:
|
||||||
|
- Mindestens 8 Zeichen
|
||||||
|
- Großbuchstaben erforderlich
|
||||||
|
- Kleinbuchstaben erforderlich
|
||||||
|
- Zahlen erforderlich
|
||||||
|
- Sonderzeichen erforderlich
|
||||||
|
|
||||||
|
5. **Validierung**:
|
||||||
|
- Altes Passwort wird bei Änderung geprüft
|
||||||
|
- `bcrypt.CompareHashAndPassword()` für Login-Validierung
|
||||||
|
|
||||||
|
## Entspricht es aktuellen Sicherheitsstandards?
|
||||||
|
|
||||||
|
### ✅ **Gut implementiert:**
|
||||||
|
|
||||||
|
1. **bcrypt ist ein sicherer, bewährter Algorithmus**
|
||||||
|
- Speziell für Passwort-Hashing entwickelt
|
||||||
|
- Verlangsamt Brute-Force-Angriffe durch anpassbare Rechenzeit
|
||||||
|
- Wird von OWASP und anderen Sicherheitsorganisationen empfohlen
|
||||||
|
|
||||||
|
2. **Automatisches Salting**
|
||||||
|
- bcrypt generiert für jedes Passwort einen eindeutigen Salt
|
||||||
|
- Verhindert Rainbow-Table-Angriffe
|
||||||
|
- Salt wird im Hash-String mitgespeichert
|
||||||
|
|
||||||
|
3. **Passwörter werden nie im Klartext gespeichert**
|
||||||
|
- Nur gehashte Werte in der Datenbank
|
||||||
|
- Einweg-Hashing (nicht reversibel)
|
||||||
|
|
||||||
|
4. **Passwortrichtlinie vorhanden**
|
||||||
|
- Erzwingt starke Passwörter
|
||||||
|
- Mindestanforderungen erfüllt
|
||||||
|
|
||||||
|
### ⚠️ **Verbesserungspotenzial:**
|
||||||
|
|
||||||
|
1. **Cost Factor könnte erhöht werden**
|
||||||
|
- **Aktuell**: Cost 10 (DefaultCost)
|
||||||
|
- **Empfohlen 2024/2025**: Cost 12-14
|
||||||
|
- **Begründung**:
|
||||||
|
- Cost 10 war vor ~10 Jahren Standard
|
||||||
|
- Moderne Hardware ist schneller
|
||||||
|
- Cost 12-14 bietet besseren Schutz gegen Brute-Force
|
||||||
|
- Trade-off: Etwas langsamere Login-Zeit (~100-500ms), aber deutlich sicherer
|
||||||
|
|
||||||
|
2. **Fehlende Sicherheitsfeatures** (optional, aber empfohlen):
|
||||||
|
- ❌ Rate Limiting für Login-Versuche (verhindert Brute-Force)
|
||||||
|
- ❌ Passwort-Historie (verhindert Wiederverwendung)
|
||||||
|
- ❌ Passwort-Ablaufzeit
|
||||||
|
- ❌ Account-Lockout nach fehlgeschlagenen Versuchen
|
||||||
|
- ❌ 2FA/MFA Support
|
||||||
|
|
||||||
|
## Empfehlung
|
||||||
|
|
||||||
|
Die aktuelle Implementierung ist **grundsätzlich sicher** und entspricht **modernen Standards**, aber:
|
||||||
|
|
||||||
|
1. **Sofort umsetzbar**: Cost Factor von 10 auf 12-14 erhöhen
|
||||||
|
2. **Mittelfristig**: Rate Limiting für Login-Versuche implementieren
|
||||||
|
3. **Langfristig**: Zusätzliche Sicherheitsfeatures (2FA, Passwort-Historie)
|
||||||
|
|
||||||
|
Soll ich den Cost Factor erhöhen?
|
||||||
|
|
||||||
568
backend/main.go
568
backend/main.go
@@ -261,6 +261,8 @@ type User struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
IsAdmin bool `json:"isAdmin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
GroupIDs []string `json:"groupIds,omitempty"`
|
GroupIDs []string `json:"groupIds,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -306,6 +308,8 @@ type UpdateUserRequest struct {
|
|||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
OldPassword string `json:"oldPassword,omitempty"`
|
OldPassword string `json:"oldPassword,omitempty"`
|
||||||
|
IsAdmin *bool `json:"isAdmin,omitempty"`
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
GroupIDs []string `json:"groupIds,omitempty"`
|
GroupIDs []string `json:"groupIds,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +318,7 @@ type CreateUserRequest struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
IsAdmin bool `json:"isAdmin,omitempty"`
|
||||||
GroupIDs []string `json:"groupIds,omitempty"`
|
GroupIDs []string `json:"groupIds,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,6 +540,8 @@ func initDB() {
|
|||||||
username TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
email TEXT NOT NULL UNIQUE,
|
email TEXT NOT NULL UNIQUE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
|
is_admin INTEGER DEFAULT 0,
|
||||||
|
enabled INTEGER DEFAULT 1,
|
||||||
created_at DATETIME NOT NULL
|
created_at DATETIME NOT NULL
|
||||||
);`
|
);`
|
||||||
|
|
||||||
@@ -550,6 +557,22 @@ func initDB() {
|
|||||||
|
|
||||||
log.Println("Datenbank erfolgreich initialisiert")
|
log.Println("Datenbank erfolgreich initialisiert")
|
||||||
|
|
||||||
|
// Füge is_admin Spalte hinzu falls nicht vorhanden
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
_, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0")
|
||||||
|
cancel()
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
|
||||||
|
log.Printf("Hinweis: is_admin-Spalte könnte bereits existieren: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Füge enabled Spalte hinzu falls nicht vorhanden
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
_, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN enabled INTEGER DEFAULT 1")
|
||||||
|
cancel()
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
|
||||||
|
log.Printf("Hinweis: enabled-Spalte könnte bereits existieren: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Erstelle Audit-Log-Tabelle
|
// Erstelle Audit-Log-Tabelle
|
||||||
log.Println("Erstelle audit_logs-Tabelle...")
|
log.Println("Erstelle audit_logs-Tabelle...")
|
||||||
createAuditLogsTableSQL := `
|
createAuditLogsTableSQL := `
|
||||||
@@ -679,19 +702,26 @@ func createDefaultAdmin() {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Prüfe ob bereits ein Admin-User existiert
|
// Prüfe ob bereits ein Admin-User mit UID "admin" existiert
|
||||||
var count int
|
var count int
|
||||||
err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = 'admin'").Scan(&count)
|
err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE id = 'admin'").Scan(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Fehler beim Prüfen des Admin-Users: %v", err)
|
log.Printf("Fehler beim Prüfen des Admin-Users: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
log.Println("Admin-User existiert bereits")
|
log.Println("Admin-User mit UID 'admin' existiert bereits")
|
||||||
|
// Stelle sicher, dass der Admin-User als Admin markiert ist
|
||||||
|
_, err = db.ExecContext(ctx, "UPDATE users SET is_admin = 1 WHERE id = 'admin'")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warnung: Konnte Admin-Status nicht setzen: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Admin-User ist als Administrator markiert")
|
||||||
|
}
|
||||||
// Prüfe ob das Passwort noch "admin" ist (für Debugging)
|
// Prüfe ob das Passwort noch "admin" ist (für Debugging)
|
||||||
var storedHash string
|
var storedHash string
|
||||||
err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = 'admin'").Scan(&storedHash)
|
err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE id = 'admin'").Scan(&storedHash)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Teste ob das Passwort "admin" ist
|
// Teste ob das Passwort "admin" ist
|
||||||
testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin"))
|
testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin"))
|
||||||
@@ -704,7 +734,45 @@ func createDefaultAdmin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erstelle Default Admin-User
|
// Migration: Falls ein Admin-User mit username='admin' aber anderer UID existiert, migriere ihn
|
||||||
|
var existingAdminID string
|
||||||
|
err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = 'admin' AND id != 'admin' LIMIT 1").Scan(&existingAdminID)
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Migriere bestehenden Admin-User von UID '%s' zu UID 'admin'", existingAdminID)
|
||||||
|
// Hole alle Daten des alten Admin-Users
|
||||||
|
var oldUsername, oldEmail, oldPasswordHash string
|
||||||
|
var oldIsAdmin, oldEnabled int
|
||||||
|
var oldCreatedAt string
|
||||||
|
err = db.QueryRowContext(ctx, "SELECT username, email, password_hash, is_admin, enabled, created_at FROM users WHERE id = ?", existingAdminID).
|
||||||
|
Scan(&oldUsername, &oldEmail, &oldPasswordHash, &oldIsAdmin, &oldEnabled, &oldCreatedAt)
|
||||||
|
if err == nil {
|
||||||
|
// Erstelle neuen Admin-User mit UID 'admin'
|
||||||
|
_, err = db.ExecContext(ctx,
|
||||||
|
"INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"admin", oldUsername, oldEmail, oldPasswordHash, oldIsAdmin, oldEnabled, oldCreatedAt)
|
||||||
|
if err == nil {
|
||||||
|
// Migriere user_groups Zuweisungen
|
||||||
|
_, err = db.ExecContext(ctx, "UPDATE user_groups SET user_id = 'admin' WHERE user_id = ?", existingAdminID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warnung: Konnte user_groups nicht migrieren: %v", err)
|
||||||
|
}
|
||||||
|
// Lösche den alten User (CASCADE sollte user_groups automatisch löschen)
|
||||||
|
_, err = db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", existingAdminID)
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("✓ Admin-User erfolgreich zu UID 'admin' migriert")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Printf("Warnung: Konnte alten Admin-User nicht löschen: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Warnung: Konnte neuen Admin-User nicht erstellen: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Warnung: Konnte Daten des alten Admin-Users nicht lesen: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle Default Admin-User mit fester UID "admin"
|
||||||
adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden
|
adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -712,18 +780,18 @@ func createDefaultAdmin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
adminID := uuid.New().String()
|
adminID := "admin" // Feste UID statt UUID
|
||||||
createdAt := time.Now().Format(time.RFC3339)
|
createdAt := time.Now().Format(time.RFC3339)
|
||||||
|
|
||||||
_, err = db.ExecContext(ctx,
|
_, err = db.ExecContext(ctx,
|
||||||
"INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
adminID, "admin", "admin@certigo.local", string(hashedPassword), createdAt)
|
adminID, "admin", "admin@certigo.local", string(hashedPassword), 1, 1, createdAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Fehler beim Erstellen des Admin-Users: %v", err)
|
log.Printf("Fehler beim Erstellen des Admin-Users: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("✓ Default Admin-User erstellt: username='admin', password='admin'")
|
log.Println("✓ Default Admin-User erstellt: UID='admin', username='admin', password='admin'")
|
||||||
log.Printf(" User ID: %s", adminID)
|
log.Printf(" User ID: %s", adminID)
|
||||||
log.Printf(" Email: admin@certigo.local")
|
log.Printf(" Email: admin@certigo.local")
|
||||||
}
|
}
|
||||||
@@ -914,20 +982,34 @@ func createSpaceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe, ob der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung)
|
// Prüfe ob User Admin ist - Admins haben immer Vollzugriff
|
||||||
permissions, err := getUserPermissions(userID)
|
isAdmin, err := isUserAdmin(userID)
|
||||||
if err != nil || len(permissions.Groups) == 0 {
|
if err != nil {
|
||||||
http.Error(w, "Keine Berechtigung zum Erstellen von Spaces", http.StatusForbidden)
|
log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
|
||||||
|
http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hasFullAccess := false
|
// Prüfe, ob der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung)
|
||||||
|
permissions, err := getUserPermissions(userID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError)
|
||||||
|
log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin oder HasFullAccess erlaubt Space-Erstellung
|
||||||
|
hasFullAccess := isAdmin || permissions.HasFullAccess
|
||||||
|
|
||||||
|
// Wenn nicht Admin, prüfe auch Gruppen
|
||||||
|
if !isAdmin && len(permissions.Groups) > 0 {
|
||||||
for _, group := range permissions.Groups {
|
for _, group := range permissions.Groups {
|
||||||
if group.Permission == PermissionFullAccess {
|
if group.Permission == PermissionFullAccess {
|
||||||
hasFullAccess = true
|
hasFullAccess = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !hasFullAccess {
|
if !hasFullAccess {
|
||||||
http.Error(w, "Keine Berechtigung zum Erstellen von Spaces. Vollzugriff erforderlich.", http.StatusForbidden)
|
http.Error(w, "Keine Berechtigung zum Erstellen von Spaces. Vollzugriff erforderlich.", http.StatusForbidden)
|
||||||
@@ -1601,19 +1683,33 @@ func deleteAllFqdnsGlobalHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
permissions, err := getUserPermissions(userID)
|
// Prüfe ob User Admin ist - Admins haben immer Vollzugriff
|
||||||
if err != nil || len(permissions.Groups) == 0 {
|
isAdmin, err := isUserAdmin(userID)
|
||||||
http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
|
if err != nil {
|
||||||
|
log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
|
||||||
|
http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hasFullAccess := false
|
permissions, err := getUserPermissions(userID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError)
|
||||||
|
log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin oder HasFullAccess erlaubt Löschen aller FQDNs
|
||||||
|
hasFullAccess := isAdmin || permissions.HasFullAccess
|
||||||
|
|
||||||
|
// Wenn nicht Admin, prüfe auch Gruppen
|
||||||
|
if !isAdmin && len(permissions.Groups) > 0 {
|
||||||
for _, group := range permissions.Groups {
|
for _, group := range permissions.Groups {
|
||||||
if group.Permission == PermissionFullAccess {
|
if group.Permission == PermissionFullAccess {
|
||||||
hasFullAccess = true
|
hasFullAccess = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !hasFullAccess {
|
if !hasFullAccess {
|
||||||
http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
|
http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
|
||||||
@@ -1712,19 +1808,33 @@ func deleteAllCSRsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
permissions, err := getUserPermissions(userID)
|
// Prüfe ob User Admin ist - Admins haben immer Vollzugriff
|
||||||
if err != nil || len(permissions.Groups) == 0 {
|
isAdmin, err := isUserAdmin(userID)
|
||||||
http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden)
|
if err != nil {
|
||||||
|
log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
|
||||||
|
http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hasFullAccess := false
|
permissions, err := getUserPermissions(userID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError)
|
||||||
|
log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin oder HasFullAccess erlaubt Löschen aller CSRs
|
||||||
|
hasFullAccess := isAdmin || permissions.HasFullAccess
|
||||||
|
|
||||||
|
// Wenn nicht Admin, prüfe auch Gruppen
|
||||||
|
if !isAdmin && len(permissions.Groups) > 0 {
|
||||||
for _, group := range permissions.Groups {
|
for _, group := range permissions.Groups {
|
||||||
if group.Permission == PermissionFullAccess {
|
if group.Permission == PermissionFullAccess {
|
||||||
hasFullAccess = true
|
hasFullAccess = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !hasFullAccess {
|
if !hasFullAccess {
|
||||||
http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden)
|
http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden)
|
||||||
@@ -2765,6 +2875,13 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe ob User Admin ist
|
||||||
|
isAdmin, err := isUserAdmin(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
|
||||||
|
isAdmin = false
|
||||||
|
}
|
||||||
|
|
||||||
// Hole Berechtigungen
|
// Hole Berechtigungen
|
||||||
permissions, err := getUserPermissions(userID)
|
permissions, err := getUserPermissions(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2781,11 +2898,12 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"userId": userID,
|
"userId": userID,
|
||||||
"hasFullAccess": permissions.HasFullAccess,
|
"isAdmin": isAdmin,
|
||||||
|
"hasFullAccess": permissions.HasFullAccess || isAdmin,
|
||||||
"accessibleSpaces": []string{},
|
"accessibleSpaces": []string{},
|
||||||
"permissions": map[string]interface{}{
|
"permissions": map[string]interface{}{
|
||||||
"canCreateSpace": false,
|
"canCreateSpace": permissions.HasFullAccess || isAdmin,
|
||||||
"canDeleteSpace": false,
|
"canDeleteSpace": permissions.HasFullAccess || isAdmin,
|
||||||
"canCreateFqdn": canCreateFqdn,
|
"canCreateFqdn": canCreateFqdn,
|
||||||
"canDeleteFqdn": canDeleteFqdn,
|
"canDeleteFqdn": canDeleteFqdn,
|
||||||
"canUploadCSR": canUploadCSR,
|
"canUploadCSR": canUploadCSR,
|
||||||
@@ -2838,6 +2956,34 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe globale Berechtigungen (Space erstellen/löschen)
|
// Prüfe globale Berechtigungen (Space erstellen/löschen)
|
||||||
|
// Admins haben immer Vollzugriff
|
||||||
|
if isAdmin {
|
||||||
|
perms := response["permissions"].(map[string]interface{})
|
||||||
|
perms["canCreateSpace"] = true
|
||||||
|
perms["canDeleteSpace"] = true
|
||||||
|
// Alle Spaces sind zugänglich für Admins
|
||||||
|
spaceRows, err := db.QueryContext(ctx, "SELECT id FROM spaces")
|
||||||
|
if err == nil {
|
||||||
|
defer spaceRows.Close()
|
||||||
|
var allSpaceIDs []string
|
||||||
|
for spaceRows.Next() {
|
||||||
|
var spaceID string
|
||||||
|
if err := spaceRows.Scan(&spaceID); err == nil {
|
||||||
|
allSpaceIDs = append(allSpaceIDs, spaceID)
|
||||||
|
canCreateFqdn[spaceID] = true
|
||||||
|
canDeleteFqdn[spaceID] = true
|
||||||
|
canUploadCSR[spaceID] = true
|
||||||
|
canSignCSR[spaceID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spaceRows.Close()
|
||||||
|
response["accessibleSpaces"] = allSpaceIDs
|
||||||
|
perms["canCreateFqdn"] = canCreateFqdn
|
||||||
|
perms["canDeleteFqdn"] = canDeleteFqdn
|
||||||
|
perms["canUploadCSR"] = canUploadCSR
|
||||||
|
perms["canSignCSR"] = canSignCSR
|
||||||
|
}
|
||||||
|
} else {
|
||||||
hasFullAccessGlobal := false
|
hasFullAccessGlobal := false
|
||||||
for _, group := range permissions.Groups {
|
for _, group := range permissions.Groups {
|
||||||
if group.Permission == PermissionFullAccess {
|
if group.Permission == PermissionFullAccess {
|
||||||
@@ -2849,6 +2995,7 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
perms := response["permissions"].(map[string]interface{})
|
perms := response["permissions"].(map[string]interface{})
|
||||||
perms["canCreateSpace"] = hasFullAccessGlobal
|
perms["canCreateSpace"] = hasFullAccessGlobal
|
||||||
perms["canDeleteSpace"] = hasFullAccessGlobal
|
perms["canDeleteSpace"] = hasFullAccessGlobal
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(response)
|
json.NewEncoder(w).Encode(response)
|
||||||
@@ -2869,7 +3016,7 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Lade alle Benutzer
|
// Lade alle Benutzer
|
||||||
rows, err := db.QueryContext(ctx, "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC")
|
rows, err := db.QueryContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users ORDER BY created_at DESC")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError)
|
http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError)
|
||||||
log.Printf("Fehler beim Abrufen der Benutzer: %v", err)
|
log.Printf("Fehler beim Abrufen der Benutzer: %v", err)
|
||||||
@@ -2881,12 +3028,15 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var userIDs []string
|
var userIDs []string
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var user User
|
var user User
|
||||||
err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
|
var isAdmin, enabled int
|
||||||
|
err := rows.Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError)
|
http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError)
|
||||||
log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err)
|
log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
user.IsAdmin = isAdmin == 1
|
||||||
|
user.Enabled = enabled == 1
|
||||||
user.GroupIDs = []string{} // Initialisiere als leeres Array
|
user.GroupIDs = []string{} // Initialisiere als leeres Array
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
userIDs = append(userIDs, user.ID)
|
userIDs = append(userIDs, user.ID)
|
||||||
@@ -2946,8 +3096,9 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var user User
|
var user User
|
||||||
err := db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
|
var isAdmin, enabled int
|
||||||
Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
|
err := db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID).
|
||||||
|
Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
|
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
|
||||||
@@ -2957,6 +3108,8 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
|
log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
user.IsAdmin = isAdmin == 1
|
||||||
|
user.Enabled = enabled == 1
|
||||||
|
|
||||||
// Lade Gruppen-IDs für diesen Benutzer
|
// Lade Gruppen-IDs für diesen Benutzer
|
||||||
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
|
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
|
||||||
@@ -3022,9 +3175,18 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
isAdmin := 0
|
||||||
|
if req.IsAdmin {
|
||||||
|
isAdmin = 1
|
||||||
|
}
|
||||||
|
// Admin muss immer enabled sein
|
||||||
|
enabledValue := 1
|
||||||
|
if req.IsAdmin {
|
||||||
|
enabledValue = 1 // Admin immer enabled
|
||||||
|
}
|
||||||
_, err = db.ExecContext(ctx,
|
_, err = db.ExecContext(ctx,
|
||||||
"INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
userID, req.Username, req.Email, string(hashedPassword), createdAt)
|
userID, req.Username, req.Email, string(hashedPassword), isAdmin, enabledValue, createdAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||||
if strings.Contains(err.Error(), "username") {
|
if strings.Contains(err.Error(), "username") {
|
||||||
@@ -3058,6 +3220,8 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ID: userID,
|
ID: userID,
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
|
IsAdmin: req.IsAdmin,
|
||||||
|
Enabled: true,
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
}
|
}
|
||||||
@@ -3068,12 +3232,17 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Audit-Log: User erstellt
|
// Audit-Log: User erstellt
|
||||||
requestUserID, requestUsername := getUserFromRequest(r)
|
requestUserID, requestUsername := getUserFromRequest(r)
|
||||||
ipAddress, userAgent := getRequestInfo(r)
|
ipAddress, userAgent := getRequestInfo(r)
|
||||||
auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{
|
auditDetails := map[string]interface{}{
|
||||||
"username": req.Username,
|
"username": req.Username,
|
||||||
"email": req.Email,
|
"email": req.Email,
|
||||||
"groupIds": req.GroupIDs,
|
"groupIds": req.GroupIDs,
|
||||||
"message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email),
|
"message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email),
|
||||||
}, ipAddress, userAgent)
|
}
|
||||||
|
if req.IsAdmin {
|
||||||
|
auditDetails["isAdmin"] = true
|
||||||
|
auditDetails["message"] = fmt.Sprintf("Administrator erstellt: %s (%s)", req.Username, req.Email)
|
||||||
|
}
|
||||||
|
auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, auditDetails, ipAddress, userAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -3100,25 +3269,105 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Prüfe ob Benutzer existiert
|
// Prüfe ob Benutzer existiert
|
||||||
var exists bool
|
var isAdmin int
|
||||||
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists)
|
var currentUsername, currentEmail string
|
||||||
if err != nil || !exists {
|
err := db.QueryRowContext(ctx, "SELECT is_admin, username, email FROM users WHERE id = ?", userID).
|
||||||
|
Scan(&isAdmin, ¤tUsername, ¤tEmail)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
|
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError)
|
||||||
|
log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur der spezielle Admin-User mit UID "admin": Username und Email sind unveränderbar
|
||||||
|
// Andere Admin-User können ihre Daten ändern
|
||||||
|
if userID == "admin" {
|
||||||
|
if req.Username != "" && req.Username != currentUsername {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Email != "" && req.Email != currentEmail {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update Felder
|
// Update Felder
|
||||||
updates := []string{}
|
updates := []string{}
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
|
|
||||||
if req.Username != "" {
|
// Nur Username/Email updaten wenn nicht der spezielle Admin-User mit UID "admin"
|
||||||
|
// Andere Admin-User können ihre Daten ändern
|
||||||
|
if req.Username != "" && (userID != "admin" || req.Username == currentUsername) {
|
||||||
updates = append(updates, "username = ?")
|
updates = append(updates, "username = ?")
|
||||||
args = append(args, req.Username)
|
args = append(args, req.Username)
|
||||||
}
|
}
|
||||||
if req.Email != "" {
|
if req.Email != "" && (userID != "admin" || req.Email == currentEmail) {
|
||||||
updates = append(updates, "email = ?")
|
updates = append(updates, "email = ?")
|
||||||
args = append(args, req.Email)
|
args = append(args, req.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAdmin aktualisieren, falls angegeben
|
||||||
|
// UID 'admin' kann seinen Admin-Status nicht ändern
|
||||||
|
if req.IsAdmin != nil {
|
||||||
|
// UID 'admin' ist immer Admin und kann nicht geändert werden
|
||||||
|
if userID == "admin" {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Der Admin-Status des Users mit UID 'admin' kann nicht geändert werden"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adminValue := 0
|
||||||
|
if *req.IsAdmin {
|
||||||
|
adminValue = 1
|
||||||
|
}
|
||||||
|
updates = append(updates, "is_admin = ?")
|
||||||
|
args = append(args, adminValue)
|
||||||
|
|
||||||
|
// Wenn Admin aktiviert wird, entferne alle Gruppen-Zuweisungen
|
||||||
|
if *req.IsAdmin {
|
||||||
|
_, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen für Admin: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enabled aktualisieren, falls angegeben
|
||||||
|
// Nur der spezielle Admin-User mit UID "admin" kann enabled geändert werden
|
||||||
|
if req.Enabled != nil {
|
||||||
|
// Nur UID "admin" kann enabled geändert werden
|
||||||
|
if userID != "admin" {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Nur der Admin-User mit UID 'admin' kann aktiviert/deaktiviert werden"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob der anfragende User ein Admin ist (für Deaktivierung)
|
||||||
|
if !*req.Enabled {
|
||||||
|
requestUserID, _ := getUserFromRequest(r)
|
||||||
|
isRequestingAdmin, err := isUserAdmin(requestUserID)
|
||||||
|
if err != nil || !isRequestingAdmin {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren können den Admin-User mit UID 'admin' deaktivieren"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledValue := 0
|
||||||
|
if *req.Enabled {
|
||||||
|
enabledValue = 1
|
||||||
|
}
|
||||||
|
updates = append(updates, "enabled = ?")
|
||||||
|
args = append(args, enabledValue)
|
||||||
|
}
|
||||||
|
|
||||||
if req.Password != "" {
|
if req.Password != "" {
|
||||||
// Altes Passwort ist erforderlich, wenn Passwort geändert wird
|
// Altes Passwort ist erforderlich, wenn Passwort geändert wird
|
||||||
if req.OldPassword == "" {
|
if req.OldPassword == "" {
|
||||||
@@ -3185,7 +3434,20 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Aktualisiere Gruppen-Zuweisungen, falls angegeben
|
// Aktualisiere Gruppen-Zuweisungen, falls angegeben
|
||||||
|
// Nur wenn User nicht Admin ist oder Admin deaktiviert wird
|
||||||
if req.GroupIDs != nil {
|
if req.GroupIDs != nil {
|
||||||
|
// Prüfe ob User nach Update Admin ist
|
||||||
|
var willBeAdmin int
|
||||||
|
if req.IsAdmin != nil {
|
||||||
|
if *req.IsAdmin {
|
||||||
|
willBeAdmin = 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
willBeAdmin = isAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur Gruppen zuweisen wenn User nicht Admin ist
|
||||||
|
if willBeAdmin == 0 {
|
||||||
// Lösche alle bestehenden Gruppen-Zuweisungen
|
// Lösche alle bestehenden Gruppen-Zuweisungen
|
||||||
_, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
|
_, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3205,19 +3467,25 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lade aktualisierten Benutzer
|
// Lade aktualisierten Benutzer
|
||||||
var user User
|
var user User
|
||||||
err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
|
var isAdminUpdated, enabledUpdated int
|
||||||
Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
|
err = db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID).
|
||||||
|
Scan(&user.ID, &user.Username, &user.Email, &isAdminUpdated, &enabledUpdated, &user.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError)
|
http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError)
|
||||||
log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err)
|
log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
user.IsAdmin = isAdminUpdated == 1
|
||||||
|
user.Enabled = enabledUpdated == 1
|
||||||
|
|
||||||
// Lade Gruppen-IDs
|
// Lade Gruppen-IDs (nur wenn nicht Admin)
|
||||||
if req.GroupIDs != nil {
|
if user.IsAdmin {
|
||||||
|
user.GroupIDs = []string{} // Admins haben keine Gruppen
|
||||||
|
} else if req.GroupIDs != nil {
|
||||||
user.GroupIDs = req.GroupIDs
|
user.GroupIDs = req.GroupIDs
|
||||||
} else {
|
} else {
|
||||||
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
|
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
|
||||||
@@ -3231,6 +3499,8 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
groupRows.Close()
|
groupRows.Close()
|
||||||
user.GroupIDs = groupIDs
|
user.GroupIDs = groupIDs
|
||||||
|
} else {
|
||||||
|
user.GroupIDs = []string{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3249,6 +3519,22 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.Password != "" {
|
if req.Password != "" {
|
||||||
details["passwordChanged"] = true
|
details["passwordChanged"] = true
|
||||||
}
|
}
|
||||||
|
if req.IsAdmin != nil {
|
||||||
|
details["isAdmin"] = *req.IsAdmin
|
||||||
|
if *req.IsAdmin {
|
||||||
|
details["message"] = "Benutzer wurde zum Administrator ernannt"
|
||||||
|
} else {
|
||||||
|
details["message"] = "Administrator-Rechte wurden entfernt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Enabled != nil {
|
||||||
|
details["enabled"] = *req.Enabled
|
||||||
|
if *req.Enabled {
|
||||||
|
details["message"] = "Benutzer wurde aktiviert"
|
||||||
|
} else {
|
||||||
|
details["message"] = "Benutzer wurde deaktiviert"
|
||||||
|
}
|
||||||
|
}
|
||||||
if req.GroupIDs != nil {
|
if req.GroupIDs != nil {
|
||||||
details["groupIds"] = req.GroupIDs
|
details["groupIds"] = req.GroupIDs
|
||||||
}
|
}
|
||||||
@@ -3272,6 +3558,29 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
// Prüfe ob der zu löschende User der spezielle Admin-User mit UID "admin" ist
|
||||||
|
// Nur dieser User kann nicht gelöscht werden, andere Admin-User können gelöscht werden
|
||||||
|
if userID == "admin" {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"error": "Der Administrator-Benutzer mit UID 'admin' kann nicht gelöscht werden. Verwenden Sie stattdessen die Deaktivierungsfunktion.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob User existiert
|
||||||
|
var exists bool
|
||||||
|
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Fehler beim Prüfen des Benutzers", http.StatusInternalServerError)
|
||||||
|
log.Printf("Fehler beim Prüfen des Benutzers: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID)
|
result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Fehler beim Löschen des Benutzers", http.StatusInternalServerError)
|
http.Error(w, "Fehler beim Löschen des Benutzers", http.StatusInternalServerError)
|
||||||
@@ -3294,9 +3603,9 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(response)
|
json.NewEncoder(w).Encode(response)
|
||||||
|
|
||||||
// Audit-Log: User gelöscht
|
// Audit-Log: User gelöscht
|
||||||
userID, username := getUserFromRequest(r)
|
requestUserID, username := getUserFromRequest(r)
|
||||||
ipAddress, userAgent := getRequestInfo(r)
|
ipAddress, userAgent := getRequestInfo(r)
|
||||||
auditService.Track(r.Context(), "DELETE", "user", vars["id"], userID, username, map[string]interface{}{
|
auditService.Track(r.Context(), "DELETE", "user", vars["id"], requestUserID, username, map[string]interface{}{
|
||||||
"message": fmt.Sprintf("User gelöscht: %s", vars["id"]),
|
"message": fmt.Sprintf("User gelöscht: %s", vars["id"]),
|
||||||
}, ipAddress, userAgent)
|
}, ipAddress, userAgent)
|
||||||
}
|
}
|
||||||
@@ -3952,7 +4261,8 @@ func getUserFromRequest(r *http.Request) (userID, username string) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var id string
|
var id string
|
||||||
err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&id)
|
var enabled int
|
||||||
|
err = db.QueryRowContext(ctx, "SELECT id, enabled FROM users WHERE username = ?", username).Scan(&id, &enabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Logge Fehler nur wenn es nicht "no rows" ist
|
// Logge Fehler nur wenn es nicht "no rows" ist
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
@@ -3961,6 +4271,12 @@ func getUserFromRequest(r *http.Request) (userID, username string) {
|
|||||||
return "", username
|
return "", username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe ob User aktiviert ist
|
||||||
|
if enabled == 0 {
|
||||||
|
log.Printf("API-Zugriff für deaktivierten Benutzer: %s", username)
|
||||||
|
return "", username
|
||||||
|
}
|
||||||
|
|
||||||
return id, username
|
return id, username
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3978,6 +4294,27 @@ type PermissionGroupInfo struct {
|
|||||||
SpaceIDs []string // Leer bedeutet Zugriff auf alle Spaces
|
SpaceIDs []string // Leer bedeutet Zugriff auf alle Spaces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isUserAdmin prüft, ob ein Benutzer Admin ist
|
||||||
|
func isUserAdmin(userID string) (bool, error) {
|
||||||
|
if userID == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var isAdmin int
|
||||||
|
err := db.QueryRowContext(ctx, "SELECT is_admin FROM users WHERE id = ?", userID).Scan(&isAdmin)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAdmin == 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
// getUserPermissions ruft die Berechtigungen eines Benutzers ab
|
// getUserPermissions ruft die Berechtigungen eines Benutzers ab
|
||||||
func getUserPermissions(userID string) (*UserPermissionInfo, error) {
|
func getUserPermissions(userID string) (*UserPermissionInfo, error) {
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
@@ -3987,6 +4324,19 @@ func getUserPermissions(userID string) (*UserPermissionInfo, error) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
// Prüfe ob User Admin ist - Admins haben immer Vollzugriff
|
||||||
|
isAdmin, err := isUserAdmin(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
|
||||||
|
}
|
||||||
|
if isAdmin {
|
||||||
|
return &UserPermissionInfo{
|
||||||
|
UserID: userID,
|
||||||
|
Groups: []PermissionGroupInfo{},
|
||||||
|
HasFullAccess: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Hole alle Gruppen des Benutzers mit ihren Berechtigungen
|
// Hole alle Gruppen des Benutzers mit ihren Berechtigungen
|
||||||
query := `
|
query := `
|
||||||
SELECT pg.id, pg.permission
|
SELECT pg.id, pg.permission
|
||||||
@@ -4071,13 +4421,20 @@ func hasSpaceAccess(userID, spaceID string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admins haben immer Zugriff
|
||||||
|
isAdmin, err := isUserAdmin(userID)
|
||||||
|
if err == nil && isAdmin {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
permissions, err := getUserPermissions(userID)
|
permissions, err := getUserPermissions(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff
|
// Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keinen Zugriff
|
||||||
if len(permissions.Groups) == 0 {
|
// Admins haben immer Zugriff (wird bereits oben geprüft)
|
||||||
|
if !isAdmin && len(permissions.Groups) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4105,13 +4462,20 @@ func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) (
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admins haben immer alle Berechtigungen
|
||||||
|
isAdmin, err := isUserAdmin(userID)
|
||||||
|
if err == nil && isAdmin {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
permissions, err := getUserPermissions(userID)
|
permissions, err := getUserPermissions(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn der Benutzer keine Gruppen hat, hat er keine Berechtigung
|
// Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keine Berechtigung
|
||||||
if len(permissions.Groups) == 0 {
|
// Admins haben immer alle Berechtigungen (wird bereits oben geprüft)
|
||||||
|
if !isAdmin && len(permissions.Groups) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4164,12 +4528,36 @@ func getAccessibleSpaceIDs(userID string) ([]string, error) {
|
|||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe ob User Admin ist - Admins haben Zugriff auf alle Spaces
|
||||||
|
isAdmin, err := isUserAdmin(userID)
|
||||||
|
if err == nil && isAdmin {
|
||||||
|
// Hole alle Spaces für Admin
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
rows, err := db.QueryContext(ctx, "SELECT id FROM spaces")
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var spaceIDs []string
|
||||||
|
for rows.Next() {
|
||||||
|
var spaceID string
|
||||||
|
if err := rows.Scan(&spaceID); err == nil {
|
||||||
|
spaceIDs = append(spaceIDs, spaceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spaceIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
permissions, err := getUserPermissions(userID)
|
permissions, err := getUserPermissions(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff
|
// Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff
|
||||||
|
// (Admin wurde bereits oben behandelt)
|
||||||
if len(permissions.Groups) == 0 {
|
if len(permissions.Groups) == 0 {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
@@ -4605,7 +4993,8 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var storedHash string
|
var storedHash string
|
||||||
err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = ?", username).Scan(&storedHash)
|
var enabled int
|
||||||
|
err = db.QueryRowContext(ctx, "SELECT password_hash, enabled FROM users WHERE username = ?", username).Scan(&storedHash, &enabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
if !isAjaxRequest {
|
if !isAjaxRequest {
|
||||||
@@ -4623,6 +5012,17 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe ob User aktiviert ist
|
||||||
|
if enabled == 0 {
|
||||||
|
if !isAjaxRequest {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe Passwort
|
// Prüfe Passwort
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4640,6 +5040,37 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adminOnlyMiddleware prüft, ob der Benutzer ein Admin ist
|
||||||
|
func adminOnlyMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return basicAuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, _ := getUserFromRequest(r)
|
||||||
|
if userID == "" {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Nicht authentifiziert"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin, err := isUserAdmin(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Fehler beim Prüfen der Berechtigung"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAdmin {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren haben Zugriff auf diese Funktion"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück)
|
// Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück)
|
||||||
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -4697,8 +5128,9 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var user User
|
var user User
|
||||||
var storedHash string
|
var storedHash string
|
||||||
err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", username).
|
var enabled int
|
||||||
Scan(&user.ID, &user.Username, &user.Email, &storedHash, &user.CreatedAt)
|
err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, enabled, created_at FROM users WHERE username = ?", username).
|
||||||
|
Scan(&user.ID, &user.Username, &user.Email, &storedHash, &enabled, &user.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
log.Printf("Benutzer nicht gefunden: %s", username)
|
log.Printf("Benutzer nicht gefunden: %s", username)
|
||||||
@@ -4712,6 +5144,14 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe ob User aktiviert ist
|
||||||
|
if enabled == 0 {
|
||||||
|
log.Printf("Login-Versuch für deaktivierten Benutzer: %s", username)
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe Passwort
|
// Prüfe Passwort
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4780,22 +5220,22 @@ func main() {
|
|||||||
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(getCSRByFQDNHandler)).Methods("GET", "OPTIONS")
|
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(getCSRByFQDNHandler)).Methods("GET", "OPTIONS")
|
||||||
api.HandleFunc("/csrs", basicAuthMiddleware(deleteAllCSRsHandler)).Methods("DELETE", "OPTIONS")
|
api.HandleFunc("/csrs", basicAuthMiddleware(deleteAllCSRsHandler)).Methods("DELETE", "OPTIONS")
|
||||||
|
|
||||||
// User Routes
|
// User Routes (Admin only)
|
||||||
api.HandleFunc("/users", getUsersHandler).Methods("GET", "OPTIONS")
|
api.HandleFunc("/users", adminOnlyMiddleware(getUsersHandler)).Methods("GET", "OPTIONS")
|
||||||
api.HandleFunc("/users", createUserHandler).Methods("POST", "OPTIONS")
|
api.HandleFunc("/users", adminOnlyMiddleware(createUserHandler)).Methods("POST", "OPTIONS")
|
||||||
api.HandleFunc("/users/{id}", getUserHandler).Methods("GET", "OPTIONS")
|
api.HandleFunc("/users/{id}", adminOnlyMiddleware(getUserHandler)).Methods("GET", "OPTIONS")
|
||||||
api.HandleFunc("/users/{id}", updateUserHandler).Methods("PUT", "OPTIONS")
|
api.HandleFunc("/users/{id}", adminOnlyMiddleware(updateUserHandler)).Methods("PUT", "OPTIONS")
|
||||||
api.HandleFunc("/users/{id}", deleteUserHandler).Methods("DELETE", "OPTIONS")
|
api.HandleFunc("/users/{id}", adminOnlyMiddleware(deleteUserHandler)).Methods("DELETE", "OPTIONS")
|
||||||
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS")
|
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS")
|
||||||
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS")
|
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS")
|
||||||
api.HandleFunc("/user/permissions", basicAuthMiddleware(getUserPermissionsHandler)).Methods("GET", "OPTIONS")
|
api.HandleFunc("/user/permissions", basicAuthMiddleware(getUserPermissionsHandler)).Methods("GET", "OPTIONS")
|
||||||
|
|
||||||
// Permission Groups Routes (Protected)
|
// Permission Groups Routes (Admin only)
|
||||||
api.HandleFunc("/permission-groups", basicAuthMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS")
|
api.HandleFunc("/permission-groups", adminOnlyMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS")
|
||||||
api.HandleFunc("/permission-groups", basicAuthMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS")
|
api.HandleFunc("/permission-groups", adminOnlyMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS")
|
||||||
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS")
|
api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS")
|
||||||
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS")
|
api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS")
|
||||||
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS")
|
api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS")
|
||||||
|
|
||||||
// Provider Routes (Protected)
|
// Provider Routes (Protected)
|
||||||
api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS")
|
api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS")
|
||||||
|
|||||||
BIN
backend/myapp
BIN
backend/myapp
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1,6 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||||
|
import { PermissionsProvider } from './contexts/PermissionsContext'
|
||||||
|
import { usePermissions } from './hooks/usePermissions'
|
||||||
import Sidebar from './components/Sidebar'
|
import Sidebar from './components/Sidebar'
|
||||||
import Footer from './components/Footer'
|
import Footer from './components/Footer'
|
||||||
import Home from './pages/Home'
|
import Home from './pages/Home'
|
||||||
@@ -10,6 +12,7 @@ import Impressum from './pages/Impressum'
|
|||||||
import Profile from './pages/Profile'
|
import Profile from './pages/Profile'
|
||||||
import Users from './pages/Users'
|
import Users from './pages/Users'
|
||||||
import Permissions from './pages/Permissions'
|
import Permissions from './pages/Permissions'
|
||||||
|
import Providers from './pages/Providers'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import AuditLogs from './pages/AuditLogs'
|
import AuditLogs from './pages/AuditLogs'
|
||||||
|
|
||||||
@@ -34,6 +37,85 @@ const ProtectedRoute = ({ children }) => {
|
|||||||
return isAuthenticated ? children : <Navigate to="/login" replace />
|
return isAuthenticated ? children : <Navigate to="/login" replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admin Only Route Component
|
||||||
|
const AdminRoute = ({ children }) => {
|
||||||
|
const { isAuthenticated, loading } = useAuth()
|
||||||
|
const { isAdmin, loading: permissionsLoading } = usePermissions()
|
||||||
|
|
||||||
|
if (loading || permissionsLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<svg className="animate-spin h-12 w-12 text-blue-500 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p className="text-slate-300">Lade...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-red-400 text-xl font-semibold mb-2">Zugriff verweigert</p>
|
||||||
|
<p className="text-slate-300">Nur Administratoren haben Zugriff auf diese Seite.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group Required Route Component - User muss einer Berechtigungsgruppe zugewiesen sein
|
||||||
|
const GroupRequiredRoute = ({ children }) => {
|
||||||
|
const { isAuthenticated, loading } = useAuth()
|
||||||
|
const { isAdmin, hasFullAccess, accessibleSpaces, loading: permissionsLoading } = usePermissions()
|
||||||
|
|
||||||
|
if (loading || permissionsLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<svg className="animate-spin h-12 w-12 text-blue-500 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p className="text-slate-300">Lade...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin oder User mit Gruppen haben Zugriff
|
||||||
|
const hasGroups = isAdmin || hasFullAccess || (accessibleSpaces && accessibleSpaces.length > 0)
|
||||||
|
|
||||||
|
if (!hasGroups) {
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to="/"
|
||||||
|
replace
|
||||||
|
state={{
|
||||||
|
message: "Sie sind keiner Berechtigungsgruppe zugewiesen. Bitte kontaktieren Sie einen Administrator.",
|
||||||
|
type: "warning"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
// Public Route Component (redirects to home if already logged in)
|
// Public Route Component (redirects to home if already logged in)
|
||||||
const PublicRoute = ({ children }) => {
|
const PublicRoute = ({ children }) => {
|
||||||
const { isAuthenticated, loading } = useAuth()
|
const { isAuthenticated, loading } = useAuth()
|
||||||
@@ -67,13 +149,14 @@ const AppContent = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<PublicRoute><Login /></PublicRoute>} />
|
<Route path="/login" element={<PublicRoute><Login /></PublicRoute>} />
|
||||||
<Route path="/" element={<ProtectedRoute><Home /></ProtectedRoute>} />
|
<Route path="/" element={<ProtectedRoute><Home /></ProtectedRoute>} />
|
||||||
<Route path="/spaces" element={<ProtectedRoute><Spaces /></ProtectedRoute>} />
|
<Route path="/spaces" element={<GroupRequiredRoute><Spaces /></GroupRequiredRoute>} />
|
||||||
<Route path="/spaces/:id" element={<ProtectedRoute><SpaceDetail /></ProtectedRoute>} />
|
<Route path="/spaces/:id" element={<GroupRequiredRoute><SpaceDetail /></GroupRequiredRoute>} />
|
||||||
<Route path="/impressum" element={<ProtectedRoute><Impressum /></ProtectedRoute>} />
|
<Route path="/impressum" element={<GroupRequiredRoute><Impressum /></GroupRequiredRoute>} />
|
||||||
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
|
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
|
||||||
<Route path="/settings/users" element={<ProtectedRoute><Users /></ProtectedRoute>} />
|
<Route path="/settings/users" element={<AdminRoute><Users /></AdminRoute>} />
|
||||||
<Route path="/settings/permissions" element={<ProtectedRoute><Permissions /></ProtectedRoute>} />
|
<Route path="/settings/permissions" element={<AdminRoute><Permissions /></AdminRoute>} />
|
||||||
<Route path="/audit-logs" element={<ProtectedRoute><AuditLogs /></ProtectedRoute>} />
|
<Route path="/settings/providers" element={<AdminRoute><Providers /></AdminRoute>} />
|
||||||
|
<Route path="/audit-logs" element={<GroupRequiredRoute><AuditLogs /></GroupRequiredRoute>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
@@ -87,7 +170,9 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<PermissionsProvider>
|
||||||
<AppContent />
|
<AppContent />
|
||||||
|
</PermissionsProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
import { usePermissions } from '../hooks/usePermissions'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
const Sidebar = ({ isOpen, setIsOpen }) => {
|
const Sidebar = ({ isOpen, setIsOpen }) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
|
const { isAdmin, hasFullAccess, accessibleSpaces } = usePermissions()
|
||||||
const [expandedMenus, setExpandedMenus] = useState({})
|
const [expandedMenus, setExpandedMenus] = useState({})
|
||||||
|
|
||||||
|
// Prüfe ob User Berechtigungsgruppen hat
|
||||||
|
const hasGroups = isAdmin || hasFullAccess || (accessibleSpaces && accessibleSpaces.length > 0)
|
||||||
|
|
||||||
|
// Menüpunkte - Home ist immer sichtbar, andere nur mit Gruppen
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ path: '/', label: 'Home', icon: '🏠' },
|
{ path: '/', label: 'Home', icon: '🏠', alwaysVisible: true },
|
||||||
{ path: '/spaces', label: 'Spaces', icon: '📁' },
|
{ path: '/spaces', label: 'Spaces', icon: '📁', requiresGroups: true },
|
||||||
{ path: '/audit-logs', label: 'Audit Log', icon: '📋' },
|
{ path: '/audit-logs', label: 'Audit Log', icon: '📋', requiresGroups: true },
|
||||||
{ path: '/impressum', label: 'Impressum', icon: 'ℹ️' },
|
{ path: '/impressum', label: 'Impressum', icon: 'ℹ️', requiresGroups: true },
|
||||||
]
|
].filter(item => item.alwaysVisible || !item.requiresGroups || hasGroups)
|
||||||
|
|
||||||
// Settings mit Unterpunkten
|
// Settings mit Unterpunkten
|
||||||
const settingsMenu = {
|
const settingsMenu = {
|
||||||
@@ -23,6 +29,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
|
|||||||
subItems: [
|
subItems: [
|
||||||
{ path: '/settings/users', label: 'User', icon: '👥' },
|
{ path: '/settings/users', label: 'User', icon: '👥' },
|
||||||
{ path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' },
|
{ path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' },
|
||||||
|
{ path: '/settings/providers', label: 'SSL Provider', icon: '🔒' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +135,8 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Settings Menu mit Unterpunkten */}
|
{/* Settings Menu mit Unterpunkten - nur für Admins */}
|
||||||
|
{isAdmin && (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => isOpen && toggleMenu(settingsMenu.path)}
|
onClick={() => isOpen && toggleMenu(settingsMenu.path)}
|
||||||
@@ -184,6 +192,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{/* Profil-Eintrag und Logout am unteren Ende */}
|
{/* Profil-Eintrag und Logout am unteren Ende */}
|
||||||
<div className="mt-auto pt-2 border-t border-slate-700/50 space-y-2">
|
<div className="mt-auto pt-2 border-t border-slate-700/50 space-y-2">
|
||||||
|
|||||||
192
frontend/src/contexts/PermissionsContext.jsx
Normal file
192
frontend/src/contexts/PermissionsContext.jsx
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'
|
||||||
|
import { useAuth } from './AuthContext'
|
||||||
|
|
||||||
|
const PermissionsContext = createContext(null)
|
||||||
|
|
||||||
|
// Intervall für automatisches Neuladen der Permissions (30 Sekunden)
|
||||||
|
const PERMISSIONS_REFRESH_INTERVAL = 30000
|
||||||
|
|
||||||
|
export const PermissionsProvider = ({ children }) => {
|
||||||
|
const { authFetch, isAuthenticated } = useAuth()
|
||||||
|
const [permissions, setPermissions] = useState({
|
||||||
|
isAdmin: false,
|
||||||
|
hasFullAccess: false,
|
||||||
|
accessibleSpaces: [],
|
||||||
|
canCreateSpace: false,
|
||||||
|
canDeleteSpace: false,
|
||||||
|
canCreateFqdn: {},
|
||||||
|
canDeleteFqdn: {},
|
||||||
|
canUploadCSR: {},
|
||||||
|
canSignCSR: {},
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const intervalRef = useRef(null)
|
||||||
|
const isMountedRef = useRef(true)
|
||||||
|
|
||||||
|
const fetchPermissions = useCallback(async (isInitial = false) => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isInitial) {
|
||||||
|
setLoading(true)
|
||||||
|
}
|
||||||
|
const response = await authFetch('/api/user/permissions')
|
||||||
|
if (response.ok && isMountedRef.current) {
|
||||||
|
try {
|
||||||
|
const data = await response.json()
|
||||||
|
// Nur Permissions aktualisieren, wenn Daten erfolgreich geparst wurden
|
||||||
|
setPermissions({
|
||||||
|
isAdmin: data.isAdmin || false,
|
||||||
|
hasFullAccess: data.hasFullAccess || false,
|
||||||
|
accessibleSpaces: Array.isArray(data.accessibleSpaces) ? data.accessibleSpaces : [],
|
||||||
|
canCreateSpace: data.permissions?.canCreateSpace || false,
|
||||||
|
canDeleteSpace: data.permissions?.canDeleteSpace || false,
|
||||||
|
canCreateFqdn: data.permissions?.canCreateFqdn || {},
|
||||||
|
canDeleteFqdn: data.permissions?.canDeleteFqdn || {},
|
||||||
|
canUploadCSR: data.permissions?.canUploadCSR || {},
|
||||||
|
canSignCSR: data.permissions?.canSignCSR || {},
|
||||||
|
})
|
||||||
|
} catch (parseErr) {
|
||||||
|
console.error('Error parsing permissions response:', parseErr)
|
||||||
|
// Bei Parse-Fehler Permissions nicht zurücksetzen, nur loggen
|
||||||
|
}
|
||||||
|
} else if (response.status === 401 && isMountedRef.current) {
|
||||||
|
// Bei 401 Unauthorized werden Permissions zurückgesetzt (wird von AuthContext gehandelt)
|
||||||
|
console.log('Unauthorized - permissions will be cleared by auth context')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching permissions:', err)
|
||||||
|
// Bei Netzwerkfehlern etc. Permissions nicht zurücksetzen
|
||||||
|
} finally {
|
||||||
|
if (isInitial && isMountedRef.current) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, authFetch])
|
||||||
|
|
||||||
|
// Initiales Laden der Permissions
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
fetchPermissions(true)
|
||||||
|
} else {
|
||||||
|
setPermissions({
|
||||||
|
isAdmin: false,
|
||||||
|
hasFullAccess: false,
|
||||||
|
accessibleSpaces: [],
|
||||||
|
canCreateSpace: false,
|
||||||
|
canDeleteSpace: false,
|
||||||
|
canCreateFqdn: {},
|
||||||
|
canDeleteFqdn: {},
|
||||||
|
canUploadCSR: {},
|
||||||
|
canSignCSR: {},
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, fetchPermissions])
|
||||||
|
|
||||||
|
// Automatisches Neuladen der Permissions im Hintergrund
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starte Polling-Intervall
|
||||||
|
const startPolling = () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current)
|
||||||
|
}
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
if (isMountedRef.current && document.visibilityState === 'visible') {
|
||||||
|
fetchPermissions(false)
|
||||||
|
}
|
||||||
|
}, PERMISSIONS_REFRESH_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle visibility change - pausiere Polling wenn Tab versteckt ist
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
// Tab ist versteckt, stoppe Intervall
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current)
|
||||||
|
intervalRef.current = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tab ist sichtbar, lade Permissions sofort und starte Polling
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
fetchPermissions(false)
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starte initiales Polling
|
||||||
|
startPolling()
|
||||||
|
|
||||||
|
// Event Listener für visibility change
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current)
|
||||||
|
intervalRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, fetchPermissions])
|
||||||
|
|
||||||
|
// Cleanup beim Unmount
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current)
|
||||||
|
intervalRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const canCreateSpace = () => permissions.canCreateSpace
|
||||||
|
const canDeleteSpace = (spaceId) => permissions.canDeleteSpace
|
||||||
|
const canCreateFqdn = (spaceId) => permissions.canCreateFqdn[spaceId] === true
|
||||||
|
const canDeleteFqdn = (spaceId) => permissions.canDeleteFqdn[spaceId] === true
|
||||||
|
const canUploadCSR = (spaceId) => permissions.canUploadCSR[spaceId] === true
|
||||||
|
const canSignCSR = (spaceId) => permissions.canSignCSR[spaceId] === true
|
||||||
|
const hasAccessToSpace = (spaceId) => permissions.accessibleSpaces.includes(spaceId)
|
||||||
|
|
||||||
|
// refreshPermissions Funktion, die auch loading state setzt
|
||||||
|
const refreshPermissions = useCallback(async () => {
|
||||||
|
await fetchPermissions(true)
|
||||||
|
}, [fetchPermissions])
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
permissions,
|
||||||
|
loading,
|
||||||
|
refreshPermissions,
|
||||||
|
isAdmin: permissions.isAdmin,
|
||||||
|
hasFullAccess: permissions.hasFullAccess,
|
||||||
|
accessibleSpaces: permissions.accessibleSpaces,
|
||||||
|
canCreateSpace,
|
||||||
|
canDeleteSpace,
|
||||||
|
canCreateFqdn,
|
||||||
|
canDeleteFqdn,
|
||||||
|
canUploadCSR,
|
||||||
|
canSignCSR,
|
||||||
|
hasAccessToSpace,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PermissionsContext.Provider value={value}>{children}</PermissionsContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePermissions = () => {
|
||||||
|
const context = useContext(PermissionsContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('usePermissions muss innerhalb eines PermissionsProvider verwendet werden')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,86 +1 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
export { usePermissions } from '../contexts/PermissionsContext'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
|
||||||
|
|
||||||
export const usePermissions = () => {
|
|
||||||
const { authFetch, isAuthenticated } = useAuth()
|
|
||||||
const [permissions, setPermissions] = useState({
|
|
||||||
hasFullAccess: false,
|
|
||||||
accessibleSpaces: [],
|
|
||||||
canCreateSpace: false,
|
|
||||||
canDeleteSpace: false,
|
|
||||||
canCreateFqdn: {},
|
|
||||||
canDeleteFqdn: {},
|
|
||||||
canUploadCSR: {},
|
|
||||||
canSignCSR: {},
|
|
||||||
})
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
|
|
||||||
const fetchPermissions = useCallback(async () => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
const response = await authFetch('/api/user/permissions')
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
setPermissions({
|
|
||||||
hasFullAccess: data.hasFullAccess || false,
|
|
||||||
accessibleSpaces: data.accessibleSpaces || [],
|
|
||||||
canCreateSpace: data.permissions?.canCreateSpace || false,
|
|
||||||
canDeleteSpace: data.permissions?.canDeleteSpace || false,
|
|
||||||
canCreateFqdn: data.permissions?.canCreateFqdn || {},
|
|
||||||
canDeleteFqdn: data.permissions?.canDeleteFqdn || {},
|
|
||||||
canUploadCSR: data.permissions?.canUploadCSR || {},
|
|
||||||
canSignCSR: data.permissions?.canSignCSR || {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching permissions:', err)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, authFetch])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isAuthenticated) {
|
|
||||||
fetchPermissions()
|
|
||||||
} else {
|
|
||||||
setPermissions({
|
|
||||||
hasFullAccess: false,
|
|
||||||
accessibleSpaces: [],
|
|
||||||
canCreateSpace: false,
|
|
||||||
canDeleteSpace: false,
|
|
||||||
canCreateFqdn: {},
|
|
||||||
canDeleteFqdn: {},
|
|
||||||
canUploadCSR: {},
|
|
||||||
canSignCSR: {},
|
|
||||||
})
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, fetchPermissions])
|
|
||||||
|
|
||||||
const canCreateSpace = () => permissions.canCreateSpace
|
|
||||||
const canDeleteSpace = (spaceId) => permissions.canDeleteSpace
|
|
||||||
const canCreateFqdn = (spaceId) => permissions.canCreateFqdn[spaceId] === true
|
|
||||||
const canDeleteFqdn = (spaceId) => permissions.canDeleteFqdn[spaceId] === true
|
|
||||||
const canUploadCSR = (spaceId) => permissions.canUploadCSR[spaceId] === true
|
|
||||||
const canSignCSR = (spaceId) => permissions.canSignCSR[spaceId] === true
|
|
||||||
const hasAccessToSpace = (spaceId) => permissions.accessibleSpaces.includes(spaceId)
|
|
||||||
|
|
||||||
return {
|
|
||||||
permissions,
|
|
||||||
loading,
|
|
||||||
refreshPermissions: fetchPermissions,
|
|
||||||
canCreateSpace,
|
|
||||||
canDeleteSpace,
|
|
||||||
canCreateFqdn,
|
|
||||||
canDeleteFqdn,
|
|
||||||
canUploadCSR,
|
|
||||||
canSignCSR,
|
|
||||||
hasAccessToSpace,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import ProvidersSection from '../components/ProvidersSection'
|
import { usePermissions } from '../hooks/usePermissions'
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { authFetch } = useAuth()
|
const { authFetch } = useAuth()
|
||||||
|
const location = useLocation()
|
||||||
|
const { isAdmin, hasFullAccess, accessibleSpaces } = usePermissions()
|
||||||
const [data, setData] = useState(null)
|
const [data, setData] = useState(null)
|
||||||
const [stats, setStats] = useState(null)
|
const [stats, setStats] = useState(null)
|
||||||
const [loadingStats, setLoadingStats] = useState(true)
|
const [loadingStats, setLoadingStats] = useState(true)
|
||||||
@@ -11,6 +14,19 @@ const Home = () => {
|
|||||||
const intervalRef = useRef(null)
|
const intervalRef = useRef(null)
|
||||||
const isMountedRef = useRef(true)
|
const isMountedRef = useRef(true)
|
||||||
|
|
||||||
|
// Prüfe ob User Berechtigungsgruppen hat
|
||||||
|
const hasGroups = isAdmin || hasFullAccess || (accessibleSpaces && accessibleSpaces.length > 0)
|
||||||
|
const message = location.state?.message
|
||||||
|
const messageType = location.state?.type || 'info'
|
||||||
|
|
||||||
|
// Lösche location.state nach dem ersten Anzeigen
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.state?.message) {
|
||||||
|
// Entferne die Nachricht aus dem state nach dem ersten Render
|
||||||
|
window.history.replaceState({}, document.title, location.pathname)
|
||||||
|
}
|
||||||
|
}, [location.state, location.pathname])
|
||||||
|
|
||||||
// Fetch stats function
|
// Fetch stats function
|
||||||
const fetchStats = useCallback(async (isInitial = false) => {
|
const fetchStats = useCallback(async (isInitial = false) => {
|
||||||
try {
|
try {
|
||||||
@@ -188,7 +204,36 @@ const Home = () => {
|
|||||||
Dies ist die Startseite der Certigo Addon Anwendung.
|
Dies ist die Startseite der Certigo Addon Anwendung.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
{/* Warnung wenn User keine Berechtigungsgruppen hat */}
|
||||||
|
{(!hasGroups || message) && (
|
||||||
|
<div className={`mb-6 p-4 rounded-lg border ${
|
||||||
|
messageType === 'warning'
|
||||||
|
? 'bg-yellow-500/20 border-yellow-500/50'
|
||||||
|
: 'bg-blue-500/20 border-blue-500/50'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className={`text-2xl flex-shrink-0 ${
|
||||||
|
messageType === 'warning' ? 'text-yellow-400' : 'text-blue-400'
|
||||||
|
}`}>
|
||||||
|
{messageType === 'warning' ? '⚠️' : 'ℹ️'}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`font-semibold mb-1 ${
|
||||||
|
messageType === 'warning' ? 'text-yellow-300' : 'text-blue-300'
|
||||||
|
}`}>
|
||||||
|
{messageType === 'warning' ? 'Keine Berechtigungsgruppe' : 'Information'}
|
||||||
|
</p>
|
||||||
|
<p className={`text-sm ${
|
||||||
|
messageType === 'warning' ? 'text-yellow-200' : 'text-blue-200'
|
||||||
|
}`}>
|
||||||
|
{message || "Sie sind keiner Berechtigungsgruppe zugewiesen. Bitte kontaktieren Sie einen Administrator, um Zugriff auf die Anwendung zu erhalten."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
{/* Stats Dashboard */}
|
{/* Stats Dashboard */}
|
||||||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6">
|
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@@ -306,9 +351,6 @@ const Home = () => {
|
|||||||
<p className="text-slate-400">Lade Daten...</p>
|
<p className="text-slate-400">Lade Daten...</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SSL Certificate Providers */}
|
|
||||||
<ProvidersSection />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
import { usePermissions } from '../contexts/PermissionsContext'
|
||||||
|
|
||||||
const Permissions = () => {
|
const Permissions = () => {
|
||||||
const { authFetch } = useAuth()
|
const { authFetch } = useAuth()
|
||||||
|
const { refreshPermissions } = usePermissions()
|
||||||
const [groups, setGroups] = useState([])
|
const [groups, setGroups] = useState([])
|
||||||
const [spaces, setSpaces] = useState([])
|
const [spaces, setSpaces] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -112,6 +114,8 @@ const Permissions = () => {
|
|||||||
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
|
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
|
||||||
setShowForm(false)
|
setShowForm(false)
|
||||||
setEditingGroup(null)
|
setEditingGroup(null)
|
||||||
|
// Aktualisiere Berechtigungen nach Änderung an Berechtigungsgruppen
|
||||||
|
refreshPermissions()
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe')
|
setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe')
|
||||||
@@ -156,6 +160,8 @@ const Permissions = () => {
|
|||||||
setShowDeleteModal(false)
|
setShowDeleteModal(false)
|
||||||
setGroupToDelete(null)
|
setGroupToDelete(null)
|
||||||
setConfirmChecked(false)
|
setConfirmChecked(false)
|
||||||
|
// Aktualisiere Berechtigungen nach Löschen einer Berechtigungsgruppe
|
||||||
|
refreshPermissions()
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
|
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
|
||||||
alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe')
|
alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe')
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
import { usePermissions } from '../hooks/usePermissions'
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const { authFetch, user } = useAuth()
|
const { authFetch, user } = useAuth()
|
||||||
|
const { isAdmin } = usePermissions()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [showSuccessAnimation, setShowSuccessAnimation] = useState(false)
|
const [showSuccessAnimation, setShowSuccessAnimation] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -286,8 +288,10 @@ const Profile = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
...(formData.username && { username: formData.username }),
|
// Nur der spezielle Admin-User mit UID 'admin': Username und Email nicht ändern
|
||||||
...(formData.email && { email: formData.email }),
|
// Andere Admin-User können ihre Daten ändern
|
||||||
|
...(user?.id !== 'admin' && formData.username && { username: formData.username }),
|
||||||
|
...(user?.id !== 'admin' && formData.email && { email: formData.email }),
|
||||||
...(formData.password && {
|
...(formData.password && {
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
oldPassword: formData.oldPassword
|
oldPassword: formData.oldPassword
|
||||||
@@ -414,9 +418,15 @@ const Profile = () => {
|
|||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
disabled={user?.id === 'admin'}
|
||||||
|
className={`w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
|
user?.id === 'admin' ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
placeholder="Geben Sie Ihren Benutzernamen ein"
|
placeholder="Geben Sie Ihren Benutzernamen ein"
|
||||||
/>
|
/>
|
||||||
|
{user?.id === 'admin' && (
|
||||||
|
<p className="mt-1 text-xs text-slate-400">Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -430,9 +440,15 @@ const Profile = () => {
|
|||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
disabled={user?.id === 'admin'}
|
||||||
|
className={`w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
|
user?.id === 'admin' ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
placeholder="Geben Sie Ihre E-Mail-Adresse ein"
|
placeholder="Geben Sie Ihre E-Mail-Adresse ein"
|
||||||
/>
|
/>
|
||||||
|
{user?.id === 'admin' && (
|
||||||
|
<p className="mt-1 text-xs text-slate-400">Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 border-t border-slate-700/50">
|
<div className="pt-4 border-t border-slate-700/50">
|
||||||
|
|||||||
342
frontend/src/pages/Providers.jsx
Normal file
342
frontend/src/pages/Providers.jsx
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
|
||||||
|
const Providers = () => {
|
||||||
|
const { authFetch } = useAuth()
|
||||||
|
const [providers, setProviders] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [showConfigModal, setShowConfigModal] = useState(false)
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState(null)
|
||||||
|
const [configValues, setConfigValues] = useState({})
|
||||||
|
const [testing, setTesting] = useState(false)
|
||||||
|
const [testResult, setTestResult] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProviders()
|
||||||
|
}, [authFetch])
|
||||||
|
|
||||||
|
const fetchProviders = async () => {
|
||||||
|
try {
|
||||||
|
const response = await authFetch('/api/providers')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
// Definiere feste Reihenfolge der Provider
|
||||||
|
const providerOrder = ['dummy-ca', 'autodns', 'hetzner']
|
||||||
|
const sortedProviders = providerOrder
|
||||||
|
.map(id => data.find(p => p.id === id))
|
||||||
|
.filter(p => p !== undefined)
|
||||||
|
.concat(data.filter(p => !providerOrder.includes(p.id)))
|
||||||
|
setProviders(sortedProviders)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching providers:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleProvider = async (providerId, currentEnabled) => {
|
||||||
|
try {
|
||||||
|
const response = await authFetch(`/api/providers/${providerId}/enabled`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ enabled: !currentEnabled }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
fetchProviders()
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Ändern des Provider-Status')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling provider:', err)
|
||||||
|
alert('Fehler beim Ändern des Provider-Status')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenConfig = async (provider) => {
|
||||||
|
setSelectedProvider(provider)
|
||||||
|
setTestResult(null)
|
||||||
|
|
||||||
|
// Lade aktuelle Konfiguration
|
||||||
|
try {
|
||||||
|
const response = await authFetch(`/api/providers/${provider.id}`)
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
// Initialisiere Config-Werte
|
||||||
|
const initialValues = {}
|
||||||
|
provider.settings.forEach(setting => {
|
||||||
|
if (data.config && data.config[setting.name] !== undefined) {
|
||||||
|
// Wenn Wert "***" ist, bedeutet das, dass es ein Passwort ist - leer lassen
|
||||||
|
initialValues[setting.name] = data.config[setting.name] === '***' ? '' : data.config[setting.name]
|
||||||
|
} else {
|
||||||
|
initialValues[setting.name] = setting.default || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setConfigValues(initialValues)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching provider config:', err)
|
||||||
|
// Initialisiere mit leeren Werten
|
||||||
|
const initialValues = {}
|
||||||
|
provider.settings.forEach(setting => {
|
||||||
|
initialValues[setting.name] = setting.default || ''
|
||||||
|
})
|
||||||
|
setConfigValues(initialValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowConfigModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseConfig = () => {
|
||||||
|
setShowConfigModal(false)
|
||||||
|
setSelectedProvider(null)
|
||||||
|
setConfigValues({})
|
||||||
|
setTestResult(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfigChange = (name, value) => {
|
||||||
|
setConfigValues({
|
||||||
|
...configValues,
|
||||||
|
[name]: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTestConnection = async () => {
|
||||||
|
if (!selectedProvider) return
|
||||||
|
|
||||||
|
setTesting(true)
|
||||||
|
setTestResult(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authFetch(`/api/providers/${selectedProvider.id}/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ settings: configValues }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
setTestResult(result)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error testing connection:', err)
|
||||||
|
setTestResult({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Testen der Verbindung',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveConfig = async () => {
|
||||||
|
if (!selectedProvider) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authFetch(`/api/providers/${selectedProvider.id}/config`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ settings: configValues }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
handleCloseConfig()
|
||||||
|
fetchProviders()
|
||||||
|
} else {
|
||||||
|
const error = await response.text()
|
||||||
|
alert(`Fehler beim Speichern: ${error}`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving config:', err)
|
||||||
|
alert('Fehler beim Speichern der Konfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold text-white mb-4">SSL Certificate Providers</h1>
|
||||||
|
<p className="text-lg text-slate-200 mb-8">
|
||||||
|
Verwalten Sie Ihre SSL-Zertifikats-Provider und deren Konfiguration.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6">
|
||||||
|
<p className="text-slate-400">Lade Provider...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{providers.map((provider) => (
|
||||||
|
<div
|
||||||
|
key={provider.id}
|
||||||
|
className="bg-slate-700/50 rounded-lg p-4 border border-slate-600/50 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-1 transition-colors duration-300">
|
||||||
|
{provider.displayName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-300 mb-2 transition-colors duration-300">
|
||||||
|
{provider.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpenConfig(provider)}
|
||||||
|
className="p-2 text-slate-400 hover:text-white hover:bg-slate-700/50 rounded-lg transition-colors"
|
||||||
|
title="Konfiguration"
|
||||||
|
aria-label="Konfiguration"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={provider.enabled}
|
||||||
|
onChange={() => handleToggleProvider(provider.id, provider.enabled)}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-slate-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600 transition-all duration-300"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Configuration Modal */}
|
||||||
|
{showConfigModal && selectedProvider && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 transition-colors duration-300">
|
||||||
|
<div className="bg-slate-800 rounded-xl shadow-2xl border border-slate-600/50 max-w-2xl w-full p-6 transition-all duration-300">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-2xl font-bold text-white transition-colors duration-300">
|
||||||
|
{selectedProvider.displayName} - Konfiguration
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={handleCloseConfig}
|
||||||
|
className="p-2 text-slate-400 hover:text-white hover:bg-slate-700/50 rounded-lg transition-colors"
|
||||||
|
aria-label="Schließen"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
{selectedProvider.settings.length > 0 ? (
|
||||||
|
selectedProvider.settings.map((setting) => (
|
||||||
|
<div key={setting.name}>
|
||||||
|
<label className="block text-sm font-medium text-slate-200 mb-2 transition-colors duration-300">
|
||||||
|
{setting.label}
|
||||||
|
{setting.required && <span className="text-red-400 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
{setting.description && (
|
||||||
|
<p className="text-xs text-slate-400 mb-2 transition-colors duration-300">{setting.description}</p>
|
||||||
|
)}
|
||||||
|
{setting.type === 'password' ? (
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={configValues[setting.name] || ''}
|
||||||
|
onChange={(e) => handleConfigChange(setting.name, e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300"
|
||||||
|
placeholder={setting.label}
|
||||||
|
required={setting.required}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type={setting.type || 'text'}
|
||||||
|
value={configValues[setting.name] || ''}
|
||||||
|
onChange={(e) => handleConfigChange(setting.name, e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300"
|
||||||
|
placeholder={setting.label}
|
||||||
|
required={setting.required}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-slate-300 text-center py-4 transition-colors duration-300">
|
||||||
|
Dieser Provider benötigt keine Konfiguration.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testResult && (
|
||||||
|
<div
|
||||||
|
className={`mb-4 p-4 rounded-lg border ${
|
||||||
|
testResult.success
|
||||||
|
? 'bg-green-500/20 border-green-500/50'
|
||||||
|
: 'bg-red-500/20 border-red-500/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className={`text-sm ${
|
||||||
|
testResult.success ? 'text-green-300' : 'text-red-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{testResult.success ? '✅' : '❌'} {testResult.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{selectedProvider.settings.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleTestConnection}
|
||||||
|
disabled={testing}
|
||||||
|
className="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-slate-700 disabled:text-slate-500 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-all duration-200"
|
||||||
|
>
|
||||||
|
{testing ? 'Teste...' : 'Verbindung testen'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleSaveConfig}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-all duration-200"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCloseConfig}
|
||||||
|
className="px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Providers
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { usePermissions } from '../hooks/usePermissions'
|
import { usePermissions } from '../contexts/PermissionsContext'
|
||||||
|
|
||||||
const SpaceDetail = () => {
|
const SpaceDetail = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -232,18 +232,50 @@ const SpaceDetail = () => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const csr = await response.json()
|
const csr = await response.json()
|
||||||
|
|
||||||
// Füge den neuen CSR zur History hinzu (nur wenn der Bereich bereits geöffnet ist)
|
|
||||||
if (showCSRDropdown[fqdn.id]) {
|
|
||||||
const newCsrWithFqdnId = { ...csr, fqdnId: fqdn.id }
|
|
||||||
setCsrHistory(prev => {
|
|
||||||
const filtered = prev.filter(csrItem => csrItem.fqdnId !== fqdn.id)
|
|
||||||
// Füge den neuen CSR am Anfang hinzu (neuester zuerst)
|
|
||||||
return [newCsrWithFqdnId, ...filtered]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setCsrData(csr)
|
setCsrData(csr)
|
||||||
setSelectedFqdn(fqdn)
|
setSelectedFqdn(fqdn)
|
||||||
|
|
||||||
|
// Lade die komplette CSR History neu, um den neuen CSR anzuzeigen
|
||||||
|
// WICHTIG: Warte auf die History bevor der Dropdown geöffnet wird
|
||||||
|
try {
|
||||||
|
const historyResponse = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
|
||||||
|
if (historyResponse.ok) {
|
||||||
|
const history = await historyResponse.json()
|
||||||
|
|
||||||
|
// Stelle sicher, dass history ein Array ist
|
||||||
|
const historyArray = Array.isArray(history) ? history : []
|
||||||
|
|
||||||
|
// Füge fqdnId zu jedem CSR hinzu und stelle sicher dass sie immer gesetzt ist
|
||||||
|
// Auch wenn die API fqdnId bereits zurückgibt, überschreiben wir sie mit fqdn.id für Konsistenz
|
||||||
|
// Stelle sicher dass alle CSRs gültig sind und fqdnId gesetzt ist
|
||||||
|
const historyWithFqdnId = historyArray
|
||||||
|
.filter(csrItem => csrItem && csrItem.id) // Stelle sicher dass CSR gültig ist
|
||||||
|
.map(csrItem => ({
|
||||||
|
...csrItem,
|
||||||
|
fqdnId: String(fqdn.id) // Immer als String für konsistente Filterung
|
||||||
|
}))
|
||||||
|
|
||||||
|
setCsrHistory(prev => {
|
||||||
|
// Entferne alte CSRs für diesen FQDN und füge die neuen hinzu
|
||||||
|
// Verwende String-Vergleich für Robustheit
|
||||||
|
const filtered = prev.filter(csrItem => String(csrItem?.fqdnId) !== String(fqdn.id))
|
||||||
|
return [...filtered, ...historyWithFqdnId]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Öffne den CSR History Dropdown NACH dem Laden der History
|
||||||
|
setShowCSRDropdown(prev => ({ ...prev, [fqdn.id]: true }))
|
||||||
|
} else {
|
||||||
|
setCsrHistory(prev => {
|
||||||
|
// Bei Fehler, entferne nur CSRs für diesen FQDN, behalte andere
|
||||||
|
return prev.filter(csrItem => String(csrItem?.fqdnId) !== String(fqdn.id))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching CSR history after upload:', err)
|
||||||
|
// Bei Fehler, entferne nur CSRs für diesen FQDN
|
||||||
|
setCsrHistory(prev => prev.filter(csrItem => String(csrItem?.fqdnId) !== String(fqdn.id)))
|
||||||
|
}
|
||||||
|
|
||||||
setShowCSRModal(true)
|
setShowCSRModal(true)
|
||||||
|
|
||||||
// Aktualisiere die FQDN-Liste
|
// Aktualisiere die FQDN-Liste
|
||||||
@@ -298,14 +330,23 @@ const SpaceDetail = () => {
|
|||||||
const historyResponse = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
|
const historyResponse = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
|
||||||
if (historyResponse.ok) {
|
if (historyResponse.ok) {
|
||||||
const history = await historyResponse.json()
|
const history = await historyResponse.json()
|
||||||
setCsrHistory(Array.isArray(history) ? history : [])
|
const historyArray = Array.isArray(history) ? history : []
|
||||||
|
// Stelle sicher dass fqdnId gesetzt ist für konsistente Filterung
|
||||||
|
const historyWithFqdnId = historyArray
|
||||||
|
.filter(csr => csr && csr.id)
|
||||||
|
.map(csr => ({ ...csr, fqdnId: String(fqdn.id) }))
|
||||||
|
setCsrHistory(prev => {
|
||||||
|
const filtered = prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id))
|
||||||
|
return [...filtered, ...historyWithFqdnId]
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
setCsrHistory([])
|
setCsrHistory(prev => prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id)))
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching CSR:', err)
|
console.error('Error fetching CSR:', err)
|
||||||
setCsrData(null)
|
setCsrData(null)
|
||||||
setCsrHistory([])
|
// Entferne nur CSRs für diesen FQDN, behalte andere
|
||||||
|
setCsrHistory(prev => prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +360,7 @@ const SpaceDetail = () => {
|
|||||||
setSelectedFqdn(null)
|
setSelectedFqdn(null)
|
||||||
setCsrData(null)
|
setCsrData(null)
|
||||||
setCsrError('')
|
setCsrError('')
|
||||||
setCsrHistory([])
|
// csrHistory NICHT zurücksetzen - bleibt für Dropdown-Anzeige erhalten
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
@@ -423,33 +464,54 @@ const SpaceDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleViewCertificates = async (fqdn) => {
|
const handleViewCertificates = async (fqdn) => {
|
||||||
|
if (!fqdn || !fqdn.id) {
|
||||||
|
console.error('Invalid FQDN provided to handleViewCertificates')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedFqdn(fqdn)
|
setSelectedFqdn(fqdn)
|
||||||
setLoadingCertificates(true)
|
setLoadingCertificates(true)
|
||||||
setCertificates([])
|
setCertificates([])
|
||||||
|
setShowCertificatesModal(true) // Öffne Modal sofort, auch wenn noch geladen wird
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/certificates`)
|
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/certificates`)
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
try {
|
||||||
const certs = await response.json()
|
const certs = await response.json()
|
||||||
setCertificates(certs)
|
// Stelle sicher, dass certs ein Array ist
|
||||||
|
setCertificates(Array.isArray(certs) ? certs : [])
|
||||||
|
} catch (parseErr) {
|
||||||
|
console.error('Error parsing certificates response:', parseErr)
|
||||||
|
setCertificates([])
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Fehler beim Laden der Zertifikate')
|
// Bei Fehler-Response (404, 403, etc.) setze leeres Array
|
||||||
|
console.error('Fehler beim Laden der Zertifikate:', response.status, response.statusText)
|
||||||
|
setCertificates([])
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching certificates:', err)
|
console.error('Error fetching certificates:', err)
|
||||||
|
setCertificates([])
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingCertificates(false)
|
setLoadingCertificates(false)
|
||||||
setShowCertificatesModal(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefreshCertificate = async (cert) => {
|
const handleRefreshCertificate = async (cert) => {
|
||||||
|
if (!cert || !cert.id || !selectedFqdn || !selectedFqdn.id) {
|
||||||
|
console.error('Invalid certificate or FQDN for refresh')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setRefreshingCertificate(cert.id)
|
setRefreshingCertificate(cert.id)
|
||||||
try {
|
try {
|
||||||
const response = await authFetch(`/api/spaces/${id}/fqdns/${selectedFqdn.id}/certificates/${cert.id}/refresh`, {
|
const response = await authFetch(`/api/spaces/${id}/fqdns/${selectedFqdn.id}/certificates/${cert.id}/refresh`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
try {
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
// Aktualisiere Zertifikat in der Liste
|
// Aktualisiere Zertifikat in der Liste
|
||||||
setCertificates(prev => prev.map(c =>
|
setCertificates(prev => prev.map(c =>
|
||||||
@@ -457,6 +519,9 @@ const SpaceDetail = () => {
|
|||||||
? { ...c, certificatePEM: result.certificatePEM }
|
? { ...c, certificatePEM: result.certificatePEM }
|
||||||
: c
|
: c
|
||||||
))
|
))
|
||||||
|
} catch (parseErr) {
|
||||||
|
console.error('Error parsing refresh response:', parseErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error refreshing certificate:', err)
|
console.error('Error refreshing certificate:', err)
|
||||||
@@ -798,11 +863,12 @@ const SpaceDetail = () => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const history = await response.json()
|
const history = await response.json()
|
||||||
// Speichere History mit FQDN-ID als Key
|
// Speichere History mit FQDN-ID als Key
|
||||||
const historyWithFqdnId = Array.isArray(history)
|
const historyArray = Array.isArray(history) ? history : []
|
||||||
? history.map(csr => ({ ...csr, fqdnId: fqdn.id }))
|
const historyWithFqdnId = historyArray
|
||||||
: []
|
.filter(csr => csr && csr.id)
|
||||||
|
.map(csr => ({ ...csr, fqdnId: String(fqdn.id) }))
|
||||||
setCsrHistory(prev => {
|
setCsrHistory(prev => {
|
||||||
const filtered = prev.filter(csr => csr.fqdnId !== fqdn.id)
|
const filtered = prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id))
|
||||||
return [...filtered, ...historyWithFqdnId]
|
return [...filtered, ...historyWithFqdnId]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -866,12 +932,13 @@ const SpaceDetail = () => {
|
|||||||
<div className="border-t border-slate-600/50 bg-slate-800/50 p-4">
|
<div className="border-t border-slate-600/50 bg-slate-800/50 p-4">
|
||||||
<h5 className="text-sm font-semibold text-slate-300 mb-3">CSR History</h5>
|
<h5 className="text-sm font-semibold text-slate-300 mb-3">CSR History</h5>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
// Filtere CSRs für diesen FQDN - verwende String-Vergleich für Robustheit
|
||||||
const fqdnHistory = csrHistory
|
const fqdnHistory = csrHistory
|
||||||
.filter(csr => csr.fqdnId === fqdn.id)
|
.filter(csr => csr && String(csr.fqdnId) === String(fqdn.id))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// Sortiere nach created_at, neueste zuerst
|
// Sortiere nach created_at, neueste zuerst
|
||||||
const dateA = new Date(a.createdAt).getTime()
|
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0
|
||||||
const dateB = new Date(b.createdAt).getTime()
|
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0
|
||||||
return dateB - dateA
|
return dateB - dateA
|
||||||
})
|
})
|
||||||
return fqdnHistory.length > 0 ? (
|
return fqdnHistory.length > 0 ? (
|
||||||
@@ -1478,10 +1545,13 @@ const SpaceDetail = () => {
|
|||||||
<div className="bg-slate-800 rounded-lg border border-slate-600 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
<div className="bg-slate-800 rounded-lg border border-slate-600 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h2 className="text-2xl font-bold text-white">Zertifikate für {selectedFqdn.fqdn}</h2>
|
<h2 className="text-2xl font-bold text-white">
|
||||||
|
Zertifikate für {selectedFqdn?.fqdn || 'Unbekannter FQDN'}
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={closeCertificatesModal}
|
onClick={closeCertificatesModal}
|
||||||
className="text-slate-400 hover:text-white transition-colors"
|
className="text-slate-400 hover:text-white transition-colors"
|
||||||
|
aria-label="Schließen"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
@@ -1501,64 +1571,80 @@ const SpaceDetail = () => {
|
|||||||
{certificates.length} {certificates.length === 1 ? 'Zertifikat' : 'Zertifikate'} gefunden
|
{certificates.length} {certificates.length === 1 ? 'Zertifikat' : 'Zertifikate'} gefunden
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{certificates.map((cert, index) => (
|
{certificates.map((cert, index) => {
|
||||||
<div key={cert.id} className="bg-slate-700/30 rounded-lg p-4 border border-slate-600">
|
// Sicherstellen, dass cert ein gültiges Objekt ist
|
||||||
|
if (!cert || !cert.id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const certId = cert.id || 'unknown'
|
||||||
|
const certCertificateId = cert.certificateId || 'N/A'
|
||||||
|
const certCreatedAt = cert.createdAt ? new Date(cert.createdAt) : null
|
||||||
|
const certStatus = cert.status || 'unknown'
|
||||||
|
const certProviderId = cert.providerId
|
||||||
|
const certPEM = cert.certificatePEM
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={certId} className="bg-slate-700/30 rounded-lg p-4 border border-slate-600">
|
||||||
<div className="flex justify-between items-start mb-3">
|
<div className="flex justify-between items-start mb-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="text-xs font-semibold text-purple-400 bg-purple-500/20 px-2 py-1 rounded">
|
<span className="text-xs font-semibold text-purple-400 bg-purple-500/20 px-2 py-1 rounded">
|
||||||
#{certificates.length - index}
|
#{certificates.length - index}
|
||||||
</span>
|
</span>
|
||||||
<h4 className="text-white font-semibold">CA-Zertifikat-ID: {cert.certificateId}</h4>
|
<h4 className="text-white font-semibold">CA-Zertifikat-ID: {certCertificateId}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-slate-400 text-xs">
|
<p className="text-slate-400 text-xs">
|
||||||
<span className="font-semibold text-slate-300">Interne UID:</span>{' '}
|
<span className="font-semibold text-slate-300">Interne UID:</span>{' '}
|
||||||
<span className="font-mono text-xs">{cert.id}</span>
|
<span className="font-mono text-xs">{certId}</span>
|
||||||
</p>
|
</p>
|
||||||
|
{certCreatedAt && !isNaN(certCreatedAt.getTime()) && (
|
||||||
<p className="text-slate-400 text-sm">
|
<p className="text-slate-400 text-sm">
|
||||||
<span className="font-semibold text-slate-300">Erstellt:</span>{' '}
|
<span className="font-semibold text-slate-300">Erstellt:</span>{' '}
|
||||||
{new Date(cert.createdAt).toLocaleString('de-DE')}
|
{certCreatedAt.toLocaleString('de-DE')}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
<p className="text-slate-400 text-sm">
|
<p className="text-slate-400 text-sm">
|
||||||
<span className="font-semibold text-slate-300">Status:</span>{' '}
|
<span className="font-semibold text-slate-300">Status:</span>{' '}
|
||||||
<span className={`inline-block px-2 py-0.5 rounded text-xs ${
|
<span className={`inline-block px-2 py-0.5 rounded text-xs ${
|
||||||
cert.status === 'issued'
|
certStatus === 'issued'
|
||||||
? 'bg-green-500/20 text-green-400'
|
? 'bg-green-500/20 text-green-400'
|
||||||
: cert.status === 'pending'
|
: certStatus === 'pending'
|
||||||
? 'bg-yellow-500/20 text-yellow-400'
|
? 'bg-yellow-500/20 text-yellow-400'
|
||||||
: 'bg-red-500/20 text-red-400'
|
: 'bg-red-500/20 text-red-400'
|
||||||
}`}>
|
}`}>
|
||||||
{cert.status}
|
{certStatus}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
{cert.providerId && (
|
{certProviderId && (
|
||||||
<p className="text-slate-400 text-sm">
|
<p className="text-slate-400 text-sm">
|
||||||
<span className="font-semibold text-slate-300">Provider:</span>{' '}
|
<span className="font-semibold text-slate-300">Provider:</span>{' '}
|
||||||
{cert.providerId}
|
{certProviderId}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRefreshCertificate(cert)}
|
onClick={() => handleRefreshCertificate(cert)}
|
||||||
disabled={refreshingCertificate === cert.id}
|
disabled={refreshingCertificate === certId || !selectedFqdn}
|
||||||
className="ml-4 px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white text-sm rounded-lg transition-colors"
|
className="ml-4 px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white text-sm rounded-lg transition-colors"
|
||||||
title="Zertifikat von CA abrufen"
|
title="Zertifikat von CA abrufen"
|
||||||
>
|
>
|
||||||
{refreshingCertificate === cert.id ? 'Aktualisiere...' : 'Aktualisieren'}
|
{refreshingCertificate === certId ? 'Aktualisiere...' : 'Aktualisieren'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{cert.certificatePEM && (
|
{certPEM && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<h5 className="text-sm font-semibold text-slate-300 mb-2">Zertifikat (PEM):</h5>
|
<h5 className="text-sm font-semibold text-slate-300 mb-2">Zertifikat (PEM):</h5>
|
||||||
<pre className="text-xs text-slate-200 bg-slate-900/50 p-3 rounded overflow-auto max-h-60 font-mono">
|
<pre className="text-xs text-slate-200 bg-slate-900/50 p-3 rounded overflow-auto max-h-60 font-mono">
|
||||||
{cert.certificatePEM}
|
{certPEM}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { usePermissions } from '../hooks/usePermissions'
|
import { usePermissions } from '../contexts/PermissionsContext'
|
||||||
|
|
||||||
const Spaces = () => {
|
const Spaces = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
import { usePermissions } from '../contexts/PermissionsContext'
|
||||||
|
|
||||||
const Users = () => {
|
const Users = () => {
|
||||||
const { authFetch } = useAuth()
|
const { authFetch } = useAuth()
|
||||||
|
const { refreshPermissions } = usePermissions()
|
||||||
const [users, setUsers] = useState([])
|
const [users, setUsers] = useState([])
|
||||||
const [groups, setGroups] = useState([])
|
const [groups, setGroups] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -12,14 +14,20 @@ const Users = () => {
|
|||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
const [userToDelete, setUserToDelete] = useState(null)
|
const [userToDelete, setUserToDelete] = useState(null)
|
||||||
const [confirmChecked, setConfirmChecked] = useState(false)
|
const [confirmChecked, setConfirmChecked] = useState(false)
|
||||||
|
const [showToggleModal, setShowToggleModal] = useState(false)
|
||||||
|
const [userToToggle, setUserToToggle] = useState(null)
|
||||||
|
const [confirmToggleChecked, setConfirmToggleChecked] = useState(false)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
oldPassword: '',
|
oldPassword: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
|
isAdmin: false,
|
||||||
|
enabled: true,
|
||||||
groupIds: []
|
groupIds: []
|
||||||
})
|
})
|
||||||
|
const [showAdminWarning, setShowAdminWarning] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers()
|
fetchUsers()
|
||||||
@@ -81,18 +89,24 @@ const Users = () => {
|
|||||||
|
|
||||||
const body = editingUser
|
const body = editingUser
|
||||||
? {
|
? {
|
||||||
...(formData.username && { username: formData.username }),
|
// Username/Email nur setzen wenn nicht der spezielle Admin-User mit UID 'admin'
|
||||||
...(formData.email && { email: formData.email }),
|
...(formData.username && editingUser.id !== 'admin' && { username: formData.username }),
|
||||||
|
...(formData.email && editingUser.id !== 'admin' && { email: formData.email }),
|
||||||
...(formData.password && {
|
...(formData.password && {
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
oldPassword: formData.oldPassword
|
oldPassword: formData.oldPassword
|
||||||
}),
|
}),
|
||||||
|
// isAdmin nur setzen wenn nicht UID 'admin' (UID 'admin' ist immer Admin)
|
||||||
|
...(formData.isAdmin !== undefined && editingUser.id !== 'admin' && { isAdmin: formData.isAdmin }),
|
||||||
|
// enabled wird nicht über das Bearbeitungsformular geändert, nur über den Button in der Liste
|
||||||
...(formData.groupIds !== undefined && { groupIds: formData.groupIds })
|
...(formData.groupIds !== undefined && { groupIds: formData.groupIds })
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
|
isAdmin: formData.isAdmin || false,
|
||||||
|
enabled: true, // Neue User sind immer aktiviert, enabled kann nur für UID 'admin' geändert werden
|
||||||
groupIds: formData.groupIds || []
|
groupIds: formData.groupIds || []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +120,12 @@ const Users = () => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await fetchUsers()
|
await fetchUsers()
|
||||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
|
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
|
||||||
setShowForm(false)
|
setShowForm(false)
|
||||||
setEditingUser(null)
|
setEditingUser(null)
|
||||||
|
setShowAdminWarning(false)
|
||||||
|
// Aktualisiere Berechtigungen nach Änderung an Benutzern (Gruppen-Zuweisungen könnten sich geändert haben)
|
||||||
|
refreshPermissions()
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
setError(errorData.error || 'Fehler beim Speichern des Benutzers')
|
setError(errorData.error || 'Fehler beim Speichern des Benutzers')
|
||||||
@@ -129,6 +146,8 @@ const Users = () => {
|
|||||||
oldPassword: '',
|
oldPassword: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
|
isAdmin: user.isAdmin || false,
|
||||||
|
enabled: user.enabled !== undefined ? user.enabled : true, // Wird nicht im Formular angezeigt, nur für internen Zustand
|
||||||
groupIds: user.groupIds || []
|
groupIds: user.groupIds || []
|
||||||
})
|
})
|
||||||
setShowForm(true)
|
setShowForm(true)
|
||||||
@@ -140,6 +159,67 @@ const Users = () => {
|
|||||||
setConfirmChecked(false)
|
setConfirmChecked(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleToggleEnabled = (user) => {
|
||||||
|
if (user.id !== 'admin') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setUserToToggle(user)
|
||||||
|
setShowToggleModal(true)
|
||||||
|
setConfirmToggleChecked(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmToggle = async () => {
|
||||||
|
if (!confirmToggleChecked || !userToToggle) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEnabled = !userToToggle.enabled
|
||||||
|
const action = newEnabled ? 'aktivieren' : 'deaktivieren'
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const response = await authFetch(`/api/users/${userToToggle.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
enabled: newEnabled
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await fetchUsers()
|
||||||
|
setShowToggleModal(false)
|
||||||
|
setUserToToggle(null)
|
||||||
|
setConfirmToggleChecked(false)
|
||||||
|
// Aktualisiere Berechtigungen nach Änderung
|
||||||
|
refreshPermissions()
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json().catch(() => ({ error: `Fehler beim ${action}` }))
|
||||||
|
const errorMessage = errorData.error || `Fehler beim ${action} des Admin-Users`
|
||||||
|
setError(errorMessage)
|
||||||
|
// Schließe Modal bei Fehler
|
||||||
|
if (response.status === 403) {
|
||||||
|
setShowToggleModal(false)
|
||||||
|
setUserToToggle(null)
|
||||||
|
setConfirmToggleChecked(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error toggling enabled for admin user:`, err)
|
||||||
|
setError(`Fehler beim ${action} des Admin-Users`)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelToggle = () => {
|
||||||
|
setShowToggleModal(false)
|
||||||
|
setUserToToggle(null)
|
||||||
|
setConfirmToggleChecked(false)
|
||||||
|
}
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!confirmChecked || !userToDelete) {
|
if (!confirmChecked || !userToDelete) {
|
||||||
return
|
return
|
||||||
@@ -155,13 +235,23 @@ const Users = () => {
|
|||||||
setShowDeleteModal(false)
|
setShowDeleteModal(false)
|
||||||
setUserToDelete(null)
|
setUserToDelete(null)
|
||||||
setConfirmChecked(false)
|
setConfirmChecked(false)
|
||||||
|
// Aktualisiere Berechtigungen nach Löschen eines Benutzers
|
||||||
|
refreshPermissions()
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
|
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
|
||||||
alert(errorData.error || 'Fehler beim Löschen des Benutzers')
|
const errorMessage = errorData.error || 'Fehler beim Löschen des Benutzers'
|
||||||
|
// Zeige Fehlermeldung
|
||||||
|
setError(errorMessage)
|
||||||
|
// Wenn Admin-Löschung verhindert wurde, schließe Modal
|
||||||
|
if (response.status === 403) {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
setUserToDelete(null)
|
||||||
|
setConfirmChecked(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting user:', err)
|
console.error('Error deleting user:', err)
|
||||||
alert('Fehler beim Löschen des Benutzers')
|
setError('Fehler beim Löschen des Benutzers')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +279,20 @@ const Users = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAdminToggle = (e) => {
|
||||||
|
const isAdmin = e.target.checked
|
||||||
|
if (isAdmin && !showAdminWarning) {
|
||||||
|
setShowAdminWarning(true)
|
||||||
|
}
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
isAdmin,
|
||||||
|
// Wenn Admin aktiviert wird, entferne alle Gruppen und stelle sicher dass enabled=true
|
||||||
|
groupIds: isAdmin ? [] : prev.groupIds,
|
||||||
|
enabled: isAdmin ? true : (prev.enabled !== undefined ? prev.enabled : true) // Admin muss immer enabled sein
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
const getPermissionLabel = (permission) => {
|
const getPermissionLabel = (permission) => {
|
||||||
switch (permission) {
|
switch (permission) {
|
||||||
case 'READ':
|
case 'READ':
|
||||||
@@ -216,7 +320,8 @@ const Users = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowForm(!showForm)
|
setShowForm(!showForm)
|
||||||
setEditingUser(null)
|
setEditingUser(null)
|
||||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
|
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
|
||||||
|
setShowAdminWarning(false)
|
||||||
}}
|
}}
|
||||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200"
|
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200"
|
||||||
>
|
>
|
||||||
@@ -242,9 +347,15 @@ const Users = () => {
|
|||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={!editingUser}
|
required={!editingUser}
|
||||||
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
disabled={editingUser && editingUser.id === 'admin'}
|
||||||
|
className={`w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
|
editingUser && editingUser.id === 'admin' ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
placeholder="Geben Sie einen Benutzernamen ein"
|
placeholder="Geben Sie einen Benutzernamen ein"
|
||||||
/>
|
/>
|
||||||
|
{editingUser && editingUser.id === 'admin' && (
|
||||||
|
<p className="mt-1 text-xs text-slate-400">Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2">
|
<label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2">
|
||||||
@@ -257,9 +368,15 @@ const Users = () => {
|
|||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={!editingUser}
|
required={!editingUser}
|
||||||
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
disabled={editingUser && editingUser.id === 'admin'}
|
||||||
|
className={`w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
|
editingUser && editingUser.id === 'admin' ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
placeholder="Geben Sie eine E-Mail-Adresse ein"
|
placeholder="Geben Sie eine E-Mail-Adresse ein"
|
||||||
/>
|
/>
|
||||||
|
{editingUser && editingUser.id === 'admin' && (
|
||||||
|
<p className="mt-1 text-xs text-slate-400">Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{editingUser && (
|
{editingUser && (
|
||||||
<div>
|
<div>
|
||||||
@@ -344,21 +461,56 @@ const Users = () => {
|
|||||||
<p className="mt-1 text-xs text-green-400">✓ Passwörter stimmen überein</p>
|
<p className="mt-1 text-xs text-green-400">✓ Passwörter stimmen überein</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Checkbox - nicht für UID 'admin' */}
|
||||||
|
{(!editingUser || editingUser.id !== 'admin') && (
|
||||||
|
<div className="bg-slate-700/30 border border-slate-600/50 rounded-lg p-4">
|
||||||
|
<label className="flex items-start cursor-pointer group">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.isAdmin || false}
|
||||||
|
onChange={handleAdminToggle}
|
||||||
|
disabled={editingUser && editingUser.username === 'admin'} // Admin user kann seinen Status nicht ändern
|
||||||
|
className="mt-1 w-5 h-5 text-blue-600 bg-slate-700 border-slate-600 rounded focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-slate-800 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div className="ml-3 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-slate-200 font-semibold">Administrator</span>
|
||||||
|
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium">
|
||||||
|
VOLLZUGRIFF
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
|
Ein Administrator hat vollständigen Zugriff auf alle Funktionen und kann alle Einstellungen verwalten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Berechtigungsgruppen - ausgegraut wenn Admin oder UID 'admin' */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-200 mb-2">
|
<label className="block text-sm font-medium text-slate-200 mb-2">
|
||||||
Berechtigungsgruppen
|
Berechtigungsgruppen
|
||||||
|
{(formData.isAdmin || (editingUser && editingUser.id === 'admin')) && <span className="text-xs text-slate-400 ml-2">(nicht verfügbar für Administratoren)</span>}
|
||||||
</label>
|
</label>
|
||||||
<div className="bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto">
|
<div className={`bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto ${
|
||||||
|
formData.isAdmin || (editingUser && editingUser.id === 'admin') ? 'opacity-50 pointer-events-none' : ''
|
||||||
|
}`}>
|
||||||
{groups.length === 0 ? (
|
{groups.length === 0 ? (
|
||||||
<p className="text-slate-400 text-sm">Keine Berechtigungsgruppen vorhanden</p>
|
<p className="text-slate-400 text-sm">Keine Berechtigungsgruppen vorhanden</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{groups.map(group => (
|
{groups.map(group => (
|
||||||
<label key={group.id} className="flex items-start cursor-pointer hover:bg-slate-600/50 p-2 rounded">
|
<label key={group.id} className={`flex items-start p-2 rounded ${
|
||||||
|
formData.isAdmin || (editingUser && editingUser.id === 'admin') ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-slate-600/50'
|
||||||
|
}`}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.groupIds?.includes(group.id) || false}
|
checked={formData.groupIds?.includes(group.id) || false}
|
||||||
onChange={() => handleGroupToggle(group.id)}
|
onChange={() => handleGroupToggle(group.id)}
|
||||||
|
disabled={formData.isAdmin || (editingUser && editingUser.id === 'admin')}
|
||||||
className="w-4 h-4 text-blue-600 bg-slate-600 border-slate-500 rounded focus:ring-blue-500 mt-1"
|
className="w-4 h-4 text-blue-600 bg-slate-600 border-slate-500 rounded focus:ring-blue-500 mt-1"
|
||||||
/>
|
/>
|
||||||
<div className="ml-3 flex-1">
|
<div className="ml-3 flex-1">
|
||||||
@@ -396,7 +548,8 @@ const Users = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowForm(false)
|
setShowForm(false)
|
||||||
setEditingUser(null)
|
setEditingUser(null)
|
||||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
|
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
|
||||||
|
setShowAdminWarning(false)
|
||||||
setError('')
|
setError('')
|
||||||
}}
|
}}
|
||||||
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
||||||
@@ -437,11 +590,23 @@ const Users = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-xl font-semibold text-white mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<h3 className="text-xl font-semibold text-white">
|
||||||
{user.username}
|
{user.username}
|
||||||
</h3>
|
</h3>
|
||||||
|
{user.isAdmin && (
|
||||||
|
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium border border-red-500/30">
|
||||||
|
ADMIN
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{user.id === 'admin' && user.enabled === false && (
|
||||||
|
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium border border-red-500/30">
|
||||||
|
DEAKTIVIERT
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<p className="text-slate-300 mb-2">{user.email}</p>
|
<p className="text-slate-300 mb-2">{user.email}</p>
|
||||||
{user.groupIds && user.groupIds.length > 0 && (
|
{!user.isAdmin && user.groupIds && user.groupIds.length > 0 && (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<p className="text-sm text-slate-300 mb-1">Berechtigungsgruppen:</p>
|
<p className="text-sm text-slate-300 mb-1">Berechtigungsgruppen:</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -472,12 +637,26 @@ const Users = () => {
|
|||||||
>
|
>
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
</button>
|
</button>
|
||||||
|
{user.id === 'admin' ? (
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggleEnabled(user)}
|
||||||
|
className={`px-4 py-2 text-white text-sm rounded-lg transition-colors ${
|
||||||
|
user.enabled
|
||||||
|
? 'bg-yellow-600 hover:bg-yellow-700'
|
||||||
|
: 'bg-green-600 hover:bg-green-700'
|
||||||
|
}`}
|
||||||
|
title={user.enabled ? "Admin-User deaktivieren" : "Admin-User aktivieren"}
|
||||||
|
>
|
||||||
|
{user.enabled ? 'Deaktivieren' : 'Aktivieren'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(user)}
|
onClick={() => handleDelete(user)}
|
||||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors"
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -486,6 +665,66 @@ const Users = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Warning Modal */}
|
||||||
|
{showAdminWarning && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-slate-800 rounded-xl shadow-2xl border border-red-600/50 max-w-md w-full p-6">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<div className="flex-shrink-0 w-12 h-12 bg-red-500/20 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-red-400"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-white">
|
||||||
|
Administrator-Berechtigung
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<p className="text-slate-300 mb-3">
|
||||||
|
Sie sind dabei, diesem Benutzer <span className="font-semibold text-red-400">Administrator-Rechte</span> zu gewähren.
|
||||||
|
</p>
|
||||||
|
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3 mb-4">
|
||||||
|
<p className="text-sm font-semibold text-red-300 mb-2">⚠️ Mögliche Gefahren:</p>
|
||||||
|
<ul className="text-xs text-slate-300 space-y-1 list-disc list-inside">
|
||||||
|
<li>Vollständiger Zugriff auf alle Funktionen und Einstellungen</li>
|
||||||
|
<li>Möglichkeit, andere Benutzer zu erstellen, zu bearbeiten oder zu löschen</li>
|
||||||
|
<li>Zugriff auf alle Spaces, FQDNs und Zertifikate</li>
|
||||||
|
<li>Möglichkeit, Berechtigungsgruppen zu verwalten</li>
|
||||||
|
<li>Keine Einschränkungen durch Berechtigungsgruppen</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Möchten Sie wirklich fortfahren?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdminWarning(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdminWarning(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Verstanden, fortfahren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{showDeleteModal && userToDelete && (
|
{showDeleteModal && userToDelete && (
|
||||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
@@ -548,6 +787,99 @@ const Users = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Toggle Enabled Confirmation Modal */}
|
||||||
|
{showToggleModal && userToToggle && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
|
<div className={`bg-slate-800 rounded-xl shadow-2xl border max-w-md w-full p-6 ${
|
||||||
|
userToToggle.enabled ? 'border-yellow-600/50' : 'border-green-600/50'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<div className={`flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center mr-4 ${
|
||||||
|
userToToggle.enabled ? 'bg-yellow-500/20' : 'bg-green-500/20'
|
||||||
|
}`}>
|
||||||
|
{userToToggle.enabled ? (
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-yellow-400"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-green-400"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-white">
|
||||||
|
Admin-User {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<p className="text-slate-300 mb-4">
|
||||||
|
Möchten Sie den Admin-User <span className="font-semibold text-white">{userToToggle.username}</span> (UID: {userToToggle.id}) wirklich {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'}?
|
||||||
|
</p>
|
||||||
|
<p className={`text-sm mb-4 ${
|
||||||
|
userToToggle.enabled ? 'text-yellow-400' : 'text-green-400'
|
||||||
|
}`}>
|
||||||
|
{userToToggle.enabled
|
||||||
|
? 'Der Admin-User kann sich nach der Deaktivierung nicht mehr anmelden und keine API-Calls durchführen.'
|
||||||
|
: 'Der Admin-User kann sich nach der Aktivierung wieder anmelden und API-Calls durchführen.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label className="flex items-start cursor-pointer group">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={confirmToggleChecked}
|
||||||
|
onChange={(e) => setConfirmToggleChecked(e.target.checked)}
|
||||||
|
className={`mt-1 w-5 h-5 bg-slate-700 border-slate-600 rounded focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-800 cursor-pointer ${
|
||||||
|
userToToggle.enabled
|
||||||
|
? 'text-yellow-600 focus:ring-yellow-500'
|
||||||
|
: 'text-green-600 focus:ring-green-500'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="ml-3 text-sm text-slate-300 group-hover:text-white transition-colors">
|
||||||
|
Ich bestätige, dass ich den Admin-User {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'} möchte
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={confirmToggle}
|
||||||
|
disabled={!confirmToggleChecked}
|
||||||
|
className={`flex-1 px-4 py-2 disabled:bg-slate-700 disabled:text-slate-500 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors duration-200 ${
|
||||||
|
userToToggle.enabled
|
||||||
|
? 'bg-yellow-600 hover:bg-yellow-700'
|
||||||
|
: 'bg-green-600 hover:bg-green-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{userToToggle.enabled ? 'Deaktivieren' : 'Aktivieren'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={cancelToggle}
|
||||||
|
className="flex-1 px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user