Compare commits
9 Commits
dbb8049c7e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ea0ebae1b | |||
| 2f2be739f2 | |||
| d1e9c2433c | |||
| f0c23cad35 | |||
| 39148bbb56 | |||
| e96fa8f367 | |||
| 97163becfa | |||
| 16043e2577 | |||
| e3a2ccb82d |
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
|
||||
node_modules/
|
||||
frontend/node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
dist-ssr/
|
||||
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
|
||||
# ============================================
|
||||
|
||||
# SQLite databases
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
backend/spaces.db
|
||||
backend/*.db
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
# Database backups
|
||||
*.sql.backup
|
||||
*.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/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# IntelliJ IDEA / WebStorm
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
out/
|
||||
|
||||
# Sublime Text
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vim/
|
||||
|
||||
# OS
|
||||
# Emacs
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# ============================================
|
||||
# OS Files
|
||||
# ============================================
|
||||
|
||||
# macOS
|
||||
.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?
|
||||
|
||||
126
backend/main.go
126
backend/main.go
@@ -982,18 +982,32 @@ func createSpaceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prüfe, ob der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung)
|
||||
permissions, err := getUserPermissions(userID)
|
||||
if err != nil || len(permissions.Groups) == 0 {
|
||||
http.Error(w, "Keine Berechtigung zum Erstellen von Spaces", http.StatusForbidden)
|
||||
// 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)
|
||||
http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
hasFullAccess := false
|
||||
for _, group := range permissions.Groups {
|
||||
if group.Permission == PermissionFullAccess {
|
||||
hasFullAccess = true
|
||||
break
|
||||
// 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 {
|
||||
if group.Permission == PermissionFullAccess {
|
||||
hasFullAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1669,17 +1683,31 @@ func deleteAllFqdnsGlobalHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
permissions, err := getUserPermissions(userID)
|
||||
if err != nil || len(permissions.Groups) == 0 {
|
||||
http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
|
||||
// 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)
|
||||
http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
hasFullAccess := false
|
||||
for _, group := range permissions.Groups {
|
||||
if group.Permission == PermissionFullAccess {
|
||||
hasFullAccess = true
|
||||
break
|
||||
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 {
|
||||
if group.Permission == PermissionFullAccess {
|
||||
hasFullAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1780,17 +1808,31 @@ func deleteAllCSRsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
permissions, err := getUserPermissions(userID)
|
||||
if err != nil || len(permissions.Groups) == 0 {
|
||||
http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden)
|
||||
// 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)
|
||||
http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
hasFullAccess := false
|
||||
for _, group := range permissions.Groups {
|
||||
if group.Permission == PermissionFullAccess {
|
||||
hasFullAccess = true
|
||||
break
|
||||
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 {
|
||||
if group.Permission == PermissionFullAccess {
|
||||
hasFullAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4390,8 +4432,9 @@ func hasSpaceAccess(userID, spaceID string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff
|
||||
if len(permissions.Groups) == 0 {
|
||||
// Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keinen Zugriff
|
||||
// Admins haben immer Zugriff (wird bereits oben geprüft)
|
||||
if !isAdmin && len(permissions.Groups) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -4430,8 +4473,9 @@ func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) (
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Wenn der Benutzer keine Gruppen hat, hat er keine Berechtigung
|
||||
if len(permissions.Groups) == 0 {
|
||||
// Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keine Berechtigung
|
||||
// Admins haben immer alle Berechtigungen (wird bereits oben geprüft)
|
||||
if !isAdmin && len(permissions.Groups) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -4484,12 +4528,36 @@ func getAccessibleSpaceIDs(userID string) ([]string, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
// Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff
|
||||
// (Admin wurde bereits oben behandelt)
|
||||
if len(permissions.Groups) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
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: 90 KiB |
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||
import { PermissionsProvider, usePermissions } from './contexts/PermissionsContext'
|
||||
import { PermissionsProvider } from './contexts/PermissionsContext'
|
||||
import { usePermissions } from './hooks/usePermissions'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import Footer from './components/Footer'
|
||||
import Home from './pages/Home'
|
||||
@@ -11,6 +12,7 @@ import Impressum from './pages/Impressum'
|
||||
import Profile from './pages/Profile'
|
||||
import Users from './pages/Users'
|
||||
import Permissions from './pages/Permissions'
|
||||
import Providers from './pages/Providers'
|
||||
import Login from './pages/Login'
|
||||
import AuditLogs from './pages/AuditLogs'
|
||||
|
||||
@@ -72,6 +74,48 @@ const AdminRoute = ({ children }) => {
|
||||
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)
|
||||
const PublicRoute = ({ children }) => {
|
||||
const { isAuthenticated, loading } = useAuth()
|
||||
@@ -105,13 +149,14 @@ const AppContent = () => {
|
||||
<Routes>
|
||||
<Route path="/login" element={<PublicRoute><Login /></PublicRoute>} />
|
||||
<Route path="/" element={<ProtectedRoute><Home /></ProtectedRoute>} />
|
||||
<Route path="/spaces" element={<ProtectedRoute><Spaces /></ProtectedRoute>} />
|
||||
<Route path="/spaces/:id" element={<ProtectedRoute><SpaceDetail /></ProtectedRoute>} />
|
||||
<Route path="/impressum" element={<ProtectedRoute><Impressum /></ProtectedRoute>} />
|
||||
<Route path="/spaces" element={<GroupRequiredRoute><Spaces /></GroupRequiredRoute>} />
|
||||
<Route path="/spaces/:id" element={<GroupRequiredRoute><SpaceDetail /></GroupRequiredRoute>} />
|
||||
<Route path="/impressum" element={<GroupRequiredRoute><Impressum /></GroupRequiredRoute>} />
|
||||
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
|
||||
<Route path="/settings/users" element={<AdminRoute><Users /></AdminRoute>} />
|
||||
<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>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { usePermissions } from '../contexts/PermissionsContext'
|
||||
import { usePermissions } from '../hooks/usePermissions'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const Sidebar = ({ isOpen, setIsOpen }) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuth()
|
||||
const { isAdmin } = usePermissions()
|
||||
const { isAdmin, hasFullAccess, accessibleSpaces } = usePermissions()
|
||||
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 = [
|
||||
{ path: '/', label: 'Home', icon: '🏠' },
|
||||
{ path: '/spaces', label: 'Spaces', icon: '📁' },
|
||||
{ path: '/audit-logs', label: 'Audit Log', icon: '📋' },
|
||||
{ path: '/impressum', label: 'Impressum', icon: 'ℹ️' },
|
||||
]
|
||||
{ path: '/', label: 'Home', icon: '🏠', alwaysVisible: true },
|
||||
{ path: '/spaces', label: 'Spaces', icon: '📁', requiresGroups: true },
|
||||
{ path: '/audit-logs', label: 'Audit Log', icon: '📋', requiresGroups: true },
|
||||
{ path: '/impressum', label: 'Impressum', icon: 'ℹ️', requiresGroups: true },
|
||||
].filter(item => item.alwaysVisible || !item.requiresGroups || hasGroups)
|
||||
|
||||
// Settings mit Unterpunkten
|
||||
const settingsMenu = {
|
||||
@@ -25,6 +29,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
|
||||
subItems: [
|
||||
{ path: '/settings/users', label: 'User', icon: '👥' },
|
||||
{ path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' },
|
||||
{ path: '/settings/providers', label: 'SSL Provider', icon: '🔒' },
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
||||
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({
|
||||
@@ -17,40 +20,57 @@ export const PermissionsProvider = ({ children }) => {
|
||||
canSignCSR: {},
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const intervalRef = useRef(null)
|
||||
const isMountedRef = useRef(true)
|
||||
|
||||
const fetchPermissions = useCallback(async () => {
|
||||
const fetchPermissions = useCallback(async (isInitial = false) => {
|
||||
if (!isAuthenticated) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
if (isInitial) {
|
||||
setLoading(true)
|
||||
}
|
||||
const response = await authFetch('/api/user/permissions')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setPermissions({
|
||||
isAdmin: data.isAdmin || false,
|
||||
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 || {},
|
||||
})
|
||||
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 {
|
||||
setLoading(false)
|
||||
if (isInitial && isMountedRef.current) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, authFetch])
|
||||
|
||||
// Initiales Laden der Permissions
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
fetchPermissions()
|
||||
fetchPermissions(true)
|
||||
} else {
|
||||
setPermissions({
|
||||
isAdmin: false,
|
||||
@@ -67,6 +87,69 @@ export const PermissionsProvider = ({ children }) => {
|
||||
}
|
||||
}, [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
|
||||
@@ -75,11 +158,18 @@ export const PermissionsProvider = ({ children }) => {
|
||||
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: fetchPermissions,
|
||||
refreshPermissions,
|
||||
isAdmin: permissions.isAdmin,
|
||||
hasFullAccess: permissions.hasFullAccess,
|
||||
accessibleSpaces: permissions.accessibleSpaces,
|
||||
canCreateSpace,
|
||||
canDeleteSpace,
|
||||
canCreateFqdn,
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
// Re-export from PermissionsContext for backward compatibility
|
||||
export { usePermissions } from '../contexts/PermissionsContext'
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import ProvidersSection from '../components/ProvidersSection'
|
||||
import { usePermissions } from '../hooks/usePermissions'
|
||||
|
||||
const Home = () => {
|
||||
const { authFetch } = useAuth()
|
||||
const location = useLocation()
|
||||
const { isAdmin, hasFullAccess, accessibleSpaces } = usePermissions()
|
||||
const [data, setData] = useState(null)
|
||||
const [stats, setStats] = useState(null)
|
||||
const [loadingStats, setLoadingStats] = useState(true)
|
||||
@@ -11,6 +14,19 @@ const Home = () => {
|
||||
const intervalRef = useRef(null)
|
||||
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
|
||||
const fetchStats = useCallback(async (isInitial = false) => {
|
||||
try {
|
||||
@@ -188,7 +204,36 @@ const Home = () => {
|
||||
Dies ist die Startseite der Certigo Addon Anwendung.
|
||||
</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 */}
|
||||
<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">
|
||||
@@ -306,9 +351,6 @@ const Home = () => {
|
||||
<p className="text-slate-400">Lade Daten...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SSL Certificate Providers */}
|
||||
<ProvidersSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { usePermissions } from '../hooks/usePermissions'
|
||||
import { usePermissions } from '../contexts/PermissionsContext'
|
||||
|
||||
const Permissions = () => {
|
||||
const { authFetch } = useAuth()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { usePermissions } from '../contexts/PermissionsContext'
|
||||
import { usePermissions } from '../hooks/usePermissions'
|
||||
|
||||
const Profile = () => {
|
||||
const { authFetch, user } = useAuth()
|
||||
|
||||
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 { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { usePermissions } from '../hooks/usePermissions'
|
||||
import { usePermissions } from '../contexts/PermissionsContext'
|
||||
|
||||
const SpaceDetail = () => {
|
||||
const { id } = useParams()
|
||||
@@ -232,18 +232,50 @@ const SpaceDetail = () => {
|
||||
if (response.ok) {
|
||||
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)
|
||||
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)
|
||||
|
||||
// Aktualisiere die FQDN-Liste
|
||||
@@ -298,14 +330,23 @@ const SpaceDetail = () => {
|
||||
const historyResponse = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
|
||||
if (historyResponse.ok) {
|
||||
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 {
|
||||
setCsrHistory([])
|
||||
setCsrHistory(prev => prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id)))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching CSR:', err)
|
||||
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)
|
||||
setCsrData(null)
|
||||
setCsrError('')
|
||||
setCsrHistory([])
|
||||
// csrHistory NICHT zurücksetzen - bleibt für Dropdown-Anzeige erhalten
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
@@ -423,40 +464,64 @@ const SpaceDetail = () => {
|
||||
}
|
||||
|
||||
const handleViewCertificates = async (fqdn) => {
|
||||
if (!fqdn || !fqdn.id) {
|
||||
console.error('Invalid FQDN provided to handleViewCertificates')
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedFqdn(fqdn)
|
||||
setLoadingCertificates(true)
|
||||
setCertificates([])
|
||||
setShowCertificatesModal(true) // Öffne Modal sofort, auch wenn noch geladen wird
|
||||
|
||||
try {
|
||||
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/certificates`)
|
||||
|
||||
if (response.ok) {
|
||||
const certs = await response.json()
|
||||
setCertificates(certs)
|
||||
try {
|
||||
const certs = await response.json()
|
||||
// Stelle sicher, dass certs ein Array ist
|
||||
setCertificates(Array.isArray(certs) ? certs : [])
|
||||
} catch (parseErr) {
|
||||
console.error('Error parsing certificates response:', parseErr)
|
||||
setCertificates([])
|
||||
}
|
||||
} 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) {
|
||||
console.error('Error fetching certificates:', err)
|
||||
setCertificates([])
|
||||
} finally {
|
||||
setLoadingCertificates(false)
|
||||
setShowCertificatesModal(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshCertificate = async (cert) => {
|
||||
if (!cert || !cert.id || !selectedFqdn || !selectedFqdn.id) {
|
||||
console.error('Invalid certificate or FQDN for refresh')
|
||||
return
|
||||
}
|
||||
|
||||
setRefreshingCertificate(cert.id)
|
||||
try {
|
||||
const response = await authFetch(`/api/spaces/${id}/fqdns/${selectedFqdn.id}/certificates/${cert.id}/refresh`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
// Aktualisiere Zertifikat in der Liste
|
||||
setCertificates(prev => prev.map(c =>
|
||||
c.id === cert.id
|
||||
? { ...c, certificatePEM: result.certificatePEM }
|
||||
: c
|
||||
))
|
||||
try {
|
||||
const result = await response.json()
|
||||
// Aktualisiere Zertifikat in der Liste
|
||||
setCertificates(prev => prev.map(c =>
|
||||
c.id === cert.id
|
||||
? { ...c, certificatePEM: result.certificatePEM }
|
||||
: c
|
||||
))
|
||||
} catch (parseErr) {
|
||||
console.error('Error parsing refresh response:', parseErr)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error refreshing certificate:', err)
|
||||
@@ -798,11 +863,12 @@ const SpaceDetail = () => {
|
||||
if (response.ok) {
|
||||
const history = await response.json()
|
||||
// Speichere History mit FQDN-ID als Key
|
||||
const historyWithFqdnId = Array.isArray(history)
|
||||
? history.map(csr => ({ ...csr, fqdnId: fqdn.id }))
|
||||
: []
|
||||
const historyArray = Array.isArray(history) ? history : []
|
||||
const historyWithFqdnId = historyArray
|
||||
.filter(csr => csr && csr.id)
|
||||
.map(csr => ({ ...csr, fqdnId: String(fqdn.id) }))
|
||||
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]
|
||||
})
|
||||
}
|
||||
@@ -866,12 +932,13 @@ const SpaceDetail = () => {
|
||||
<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>
|
||||
{(() => {
|
||||
// Filtere CSRs für diesen FQDN - verwende String-Vergleich für Robustheit
|
||||
const fqdnHistory = csrHistory
|
||||
.filter(csr => csr.fqdnId === fqdn.id)
|
||||
.filter(csr => csr && String(csr.fqdnId) === String(fqdn.id))
|
||||
.sort((a, b) => {
|
||||
// Sortiere nach created_at, neueste zuerst
|
||||
const dateA = new Date(a.createdAt).getTime()
|
||||
const dateB = new Date(b.createdAt).getTime()
|
||||
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0
|
||||
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0
|
||||
return dateB - dateA
|
||||
})
|
||||
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="p-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
|
||||
onClick={closeCertificatesModal}
|
||||
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">
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
{certificates.map((cert, index) => (
|
||||
<div key={cert.id} 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-1">
|
||||
<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">
|
||||
#{certificates.length - index}
|
||||
</span>
|
||||
<h4 className="text-white font-semibold">CA-Zertifikat-ID: {cert.certificateId}</h4>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-slate-400 text-xs">
|
||||
<span className="font-semibold text-slate-300">Interne UID:</span>{' '}
|
||||
<span className="font-mono text-xs">{cert.id}</span>
|
||||
</p>
|
||||
<p className="text-slate-400 text-sm">
|
||||
<span className="font-semibold text-slate-300">Erstellt:</span>{' '}
|
||||
{new Date(cert.createdAt).toLocaleString('de-DE')}
|
||||
</p>
|
||||
<p className="text-slate-400 text-sm">
|
||||
<span className="font-semibold text-slate-300">Status:</span>{' '}
|
||||
<span className={`inline-block px-2 py-0.5 rounded text-xs ${
|
||||
cert.status === 'issued'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: cert.status === 'pending'
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
: 'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
{cert.status}
|
||||
{certificates.map((cert, index) => {
|
||||
// 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-1">
|
||||
<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">
|
||||
#{certificates.length - index}
|
||||
</span>
|
||||
</p>
|
||||
{cert.providerId && (
|
||||
<p className="text-slate-400 text-sm">
|
||||
<span className="font-semibold text-slate-300">Provider:</span>{' '}
|
||||
{cert.providerId}
|
||||
<h4 className="text-white font-semibold">CA-Zertifikat-ID: {certCertificateId}</h4>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-slate-400 text-xs">
|
||||
<span className="font-semibold text-slate-300">Interne UID:</span>{' '}
|
||||
<span className="font-mono text-xs">{certId}</span>
|
||||
</p>
|
||||
)}
|
||||
{certCreatedAt && !isNaN(certCreatedAt.getTime()) && (
|
||||
<p className="text-slate-400 text-sm">
|
||||
<span className="font-semibold text-slate-300">Erstellt:</span>{' '}
|
||||
{certCreatedAt.toLocaleString('de-DE')}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-slate-400 text-sm">
|
||||
<span className="font-semibold text-slate-300">Status:</span>{' '}
|
||||
<span className={`inline-block px-2 py-0.5 rounded text-xs ${
|
||||
certStatus === 'issued'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: certStatus === 'pending'
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
: 'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
{certStatus}
|
||||
</span>
|
||||
</p>
|
||||
{certProviderId && (
|
||||
<p className="text-slate-400 text-sm">
|
||||
<span className="font-semibold text-slate-300">Provider:</span>{' '}
|
||||
{certProviderId}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRefreshCertificate(cert)}
|
||||
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"
|
||||
title="Zertifikat von CA abrufen"
|
||||
>
|
||||
{refreshingCertificate === certId ? 'Aktualisiere...' : 'Aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRefreshCertificate(cert)}
|
||||
disabled={refreshingCertificate === cert.id}
|
||||
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"
|
||||
>
|
||||
{refreshingCertificate === cert.id ? 'Aktualisiere...' : 'Aktualisieren'}
|
||||
</button>
|
||||
{certPEM && (
|
||||
<div className="mt-3">
|
||||
<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">
|
||||
{certPEM}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{cert.certificatePEM && (
|
||||
<div className="mt-3">
|
||||
<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">
|
||||
{cert.certificatePEM}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { usePermissions } from '../hooks/usePermissions'
|
||||
import { usePermissions } from '../contexts/PermissionsContext'
|
||||
|
||||
const Spaces = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { usePermissions } from '../hooks/usePermissions'
|
||||
import { usePermissions } from '../contexts/PermissionsContext'
|
||||
|
||||
const Users = () => {
|
||||
const { authFetch } = useAuth()
|
||||
|
||||
Reference in New Issue
Block a user