diff --git a/backend/admin-service/handlers.go b/backend/admin-service/handlers.go new file mode 100644 index 0000000..f1aedf1 --- /dev/null +++ b/backend/admin-service/handlers.go @@ -0,0 +1,395 @@ +package adminservice + +import ( + "fmt" + "strconv" + + "github.com/gofiber/fiber/v2" + "github.com/nora-nak/backend/models" + "github.com/nora-nak/backend/utils" + "gorm.io/gorm" +) + +type AdminHandler struct { + DB *gorm.DB +} + +func NewAdminHandler(db *gorm.DB) *AdminHandler { + return &AdminHandler{DB: db} +} + +// GetDashboardStats returns statistics for the admin dashboard +// GET /v1/admin/stats +func (h *AdminHandler) GetDashboardStats(c *fiber.Ctx) error { + var userCount int64 + var customHourCount int64 + var examCount int64 + + if err := h.DB.Model(&models.User{}).Count(&userCount).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to count users", + }) + } + + if err := h.DB.Model(&models.CustomHour{}).Count(&customHourCount).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to count custom hours", + }) + } + + if err := h.DB.Model(&models.Exam{}).Count(&examCount).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to count exams", + }) + } + + return c.JSON(fiber.Map{ + "user_count": userCount, + "custom_hour_count": customHourCount, + "exam_count": examCount, + }) +} + +// GetUsers returns a list of users with pagination and search +// GET /v1/admin/users +func (h *AdminHandler) GetUsers(c *fiber.Ctx) error { + page, _ := strconv.Atoi(c.Query("page", "1")) + limit, _ := strconv.Atoi(c.Query("limit", "20")) + search := c.Query("search", "") + + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 20 + } + if limit > 100 { + limit = 100 + } + + offset := (page - 1) * limit + + var users []models.User + var total int64 + + query := h.DB.Model(&models.User{}) + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Where("first_name ILIKE ? OR last_name ILIKE ? OR mail ILIKE ?", searchTerm, searchTerm, searchTerm) + } + + if err := query.Count(&total).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to count users", + }) + } + + if err := query.Select("id, mail, created_at, verified, is_admin, uuid, first_name, last_name, initials, zenturien_id").Order("created_at DESC").Offset(offset).Limit(limit).Find(&users).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to fetch users", + }) + } + + return c.JSON(fiber.Map{ + "users": users, + "meta": fiber.Map{ + "total": total, + "page": page, + "limit": limit, + "pages": (total + int64(limit) - 1) / int64(limit), + }, + }) +} + +// verifyAdminPassword checks if the provided password matches the current admin user's password +func (h *AdminHandler) verifyAdminPassword(c *fiber.Ctx, password string) error { + // Get current user from context (set by AuthMiddleware) + user, ok := c.Locals("user").(*models.User) + if !ok || user == nil { + return fmt.Errorf("user not authenticated") + } + + if !user.IsAdmin { + return fmt.Errorf("user is not an admin") + } + + if password == "" { + return fmt.Errorf("password required") + } + + // Verify password + if !utils.CheckPasswordHash(password, user.PasswordHash) { + return fmt.Errorf("invalid password") + } + + return nil +} + +// PromoteToAdmin promotes a user to admin +// PUT /v1/admin/users/:id/promote +func (h *AdminHandler) PromoteToAdmin(c *fiber.Ctx) error { + var input struct { + AdminPassword string `json:"admin_password"` + } + + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid request body", + }) + } + + // Verify admin password + if err := h.verifyAdminPassword(c, input.AdminPassword); err != nil { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "detail": "Authentication failed: " + err.Error(), + }) + } + + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid user ID", + }) + } + + var user models.User + if err := h.DB.First(&user, id).Error; err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "detail": "User not found", + }) + } + + if user.IsAdmin { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "User is already an admin", + }) + } + + user.IsAdmin = true + if err := h.DB.Save(&user).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to promote user", + }) + } + + return c.JSON(fiber.Map{ + "message": fmt.Sprintf("User %s %s promoted to admin", user.FirstName, user.LastName), + "user": user, + }) +} + +// DeleteUser deletes a user +// DELETE /v1/admin/users/:id +func (h *AdminHandler) DeleteUser(c *fiber.Ctx) error { + var input struct { + AdminPassword string `json:"admin_password"` + } + + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid request body", + }) + } + + // Verify admin password + if err := h.verifyAdminPassword(c, input.AdminPassword); err != nil { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "detail": "Authentication failed: " + err.Error(), + }) + } + + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid user ID", + }) + } + + if err := h.DB.Delete(&models.User{}, id).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to delete user", + }) + } + + return c.JSON(fiber.Map{ + "message": "User deleted successfully", + }) +} + +// VerifyUser toggles user verification status +// PUT /v1/admin/users/:id/verify +func (h *AdminHandler) VerifyUser(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid user ID", + }) + } + + var input struct { + Verified bool `json:"verified"` + AdminPassword string `json:"admin_password"` + } + + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid request body", + }) + } + + // Verify admin password + if err := h.verifyAdminPassword(c, input.AdminPassword); err != nil { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "detail": "Authentication failed: " + err.Error(), + }) + } + + var user models.User + if err := h.DB.First(&user, id).Error; err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "detail": "User not found", + }) + } + + user.Verified = input.Verified + if err := h.DB.Save(&user).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to update user verification", + }) + } + + return c.JSON(fiber.Map{ + "message": "User verification updated", + "user": user, + }) +} + +// UpdateUser updates user details +// PUT /v1/admin/users/:id +func (h *AdminHandler) UpdateUser(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid user ID", + }) + } + + var input struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Mail string `json:"mail"` + Initials string `json:"initials"` + AdminPassword string `json:"admin_password"` + } + + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid request body", + }) + } + + // Verify admin password + if err := h.verifyAdminPassword(c, input.AdminPassword); err != nil { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "detail": "Authentication failed: " + err.Error(), + }) + } + + // Validate email domain + if len(input.Mail) < 18 || input.Mail[len(input.Mail)-17:] != "@nordakademie.de" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Email must end with @nordakademie.de", + }) + } + + var user models.User + if err := h.DB.First(&user, id).Error; err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "detail": "User not found", + }) + } + + user.FirstName = input.FirstName + user.LastName = input.LastName + user.Mail = input.Mail + user.Initials = input.Initials + + if err := h.DB.Save(&user).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to update user", + }) + } + + return c.JSON(fiber.Map{ + "message": "User updated successfully", + "user": user, + }) +} + +// ResetUserPassword resets a user's password +// POST /v1/admin/users/:id/reset-password +func (h *AdminHandler) ResetUserPassword(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid user ID", + }) + } + + var input struct { + NewPassword string `json:"new_password"` + AdminPassword string `json:"admin_password"` + } + + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Invalid request body", + }) + } + + // Verify admin password + if err := h.verifyAdminPassword(c, input.AdminPassword); err != nil { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "detail": "Authentication failed: " + err.Error(), + }) + } + + if len(input.NewPassword) < 8 { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Password must be at least 8 characters long", + }) + } + + var user models.User + if err := h.DB.First(&user, id).Error; err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "detail": "User not found", + }) + } + + // Hash new password + hashedPassword, err := utils.HashPassword(input.NewPassword) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to hash password", + }) + } + + user.PasswordHash = hashedPassword + if err := h.DB.Save(&user).Error; err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Failed to update password", + }) + } + + // Invalidate all sessions for this user + if err := h.DB.Where("user_id = ?", user.ID).Delete(&models.Session{}).Error; err != nil { + // Log error but don't fail the request as password is already reset + fmt.Printf("Failed to invalidate sessions for user %d: %v\n", user.ID, err) + } + + return c.JSON(fiber.Map{ + "message": "Password reset successfully", + }) +} diff --git a/backend/admin-service/routes.go b/backend/admin-service/routes.go new file mode 100644 index 0000000..ce503a5 --- /dev/null +++ b/backend/admin-service/routes.go @@ -0,0 +1,19 @@ +package adminservice + +import ( + "github.com/gofiber/fiber/v2" + "github.com/nora-nak/backend/middleware" + "gorm.io/gorm" +) + +func SetupRoutes(app fiber.Router, db *gorm.DB) { + adminHandler := NewAdminHandler(db) + admin := app.Group("/v1/admin", middleware.AuthMiddleware, middleware.AdminMiddleware) + admin.Get("/stats", adminHandler.GetDashboardStats) + admin.Get("/users", adminHandler.GetUsers) + admin.Put("/users/:id/promote", adminHandler.PromoteToAdmin) + admin.Delete("/users/:id", adminHandler.DeleteUser) + admin.Put("/users/:id/verify", adminHandler.VerifyUser) + admin.Put("/users/:id", adminHandler.UpdateUser) + admin.Post("/users/:id/reset-password", adminHandler.ResetUserPassword) +} diff --git a/backend/handlers/timetable.go b/backend/handlers/timetable.go index 7d9f8c7..c7c122a 100644 --- a/backend/handlers/timetable.go +++ b/backend/handlers/timetable.go @@ -18,8 +18,8 @@ type UserResponse struct { FirstName string `json:"first_name"` LastName string `json:"last_name"` SubscriptionUUID *string `json:"subscription_uuid,omitempty"` - Zenturie *string `json:"zenturie,omitempty"` - Year *string `json:"year,omitempty"` + Zenturie *string `json:"zenturie"` + Year *string `json:"year"` } // ZenturieResponse represents zenturie information diff --git a/backend/main.go b/backend/main.go index 2ff5c72..b824b51 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io" "log" "os" @@ -12,12 +13,16 @@ import ( "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/google/uuid" "github.com/joho/godotenv" + adminservice "github.com/nora-nak/backend/admin-service" "github.com/nora-nak/backend/config" "github.com/nora-nak/backend/handlers" "github.com/nora-nak/backend/middleware" + "github.com/nora-nak/backend/models" "github.com/nora-nak/backend/services" + "github.com/nora-nak/backend/utils" ) func main() { @@ -45,6 +50,11 @@ func main() { log.Fatal("Failed to run migrations:", err) } + // Seed initial admin user + if err := seedAdmin(); err != nil { + log.Printf("WARNING: Failed to seed admin user: %v", err) + } + // Start scheduler (run immediately on startup) if err := services.StartScheduler(true); err != nil { log.Printf("WARNING: Failed to start scheduler: %v", err) @@ -94,6 +104,9 @@ func main() { // Protected routes (requires authentication) setupProtectedRoutes(app) + // Admin routes + adminservice.SetupRoutes(app, config.DB) + // Start server port := config.AppConfig.ServerPort log.Printf("Server starting on port %s", port) @@ -249,3 +262,61 @@ func setupLogging() error { return nil } + +// seedAdmin creates the initial admin user if it doesn't exist +func seedAdmin() error { + adminEmail := os.Getenv("ADMIN_EMAIL") + if adminEmail == "" { + adminEmail = "nora.team@nordakademie.de" + } + + var user models.User + result := config.DB.Where("mail = ?", adminEmail).First(&user) + + if result.Error == nil { + // User exists, ensure is_admin is true + if !user.IsAdmin { + user.IsAdmin = true + if err := config.DB.Save(&user).Error; err != nil { + return fmt.Errorf("failed to update admin user: %w", err) + } + log.Println("Existing admin user updated with admin privileges") + } + return nil + } + + // User does not exist, create it + log.Println("Creating initial admin user...") + + adminPassword := os.Getenv("ADMIN_PASSWORD") + if adminPassword == "" { + adminPassword = "Start123!" + log.Println("WARNING: ADMIN_PASSWORD not set, using default password") + } + + passwordHash, err := utils.HashPassword(adminPassword) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + subscriptionUUID := uuid.New().String() + + newUser := models.User{ + Mail: adminEmail, + PasswordHash: passwordHash, + Verified: true, + IsAdmin: true, + FirstName: "NORA", + LastName: "Admin", + Initials: "NA", + UUID: uuid.New(), + SubscriptionUUID: &subscriptionUUID, + } + + if err := config.DB.Create(&newUser).Error; err != nil { + return fmt.Errorf("failed to create admin user: %w", err) + } + + log.Println("Initial admin user created successfully") + return nil +} diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index bbe6a4a..223e7b1 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -56,6 +56,23 @@ func AuthMiddleware(c *fiber.Ctx) error { return c.Next() } +func AdminMiddleware(c *fiber.Ctx) error { + user, ok := c.Locals("user").(*models.User) + if !ok || user == nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "detail": "User not authenticated", + }) + } + + if !user.IsAdmin { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "detail": "Admin access required", + }) + } + + return c.Next() +} + // GetCurrentUser retrieves the current user from context func GetCurrentUser(c *fiber.Ctx) *models.User { user, ok := c.Locals("user").(*models.User) diff --git a/backend/models/models.go b/backend/models/models.go index af548a2..afe693b 100644 --- a/backend/models/models.go +++ b/backend/models/models.go @@ -23,6 +23,7 @@ type User struct { PasswordHash string `gorm:"not null" json:"-"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` Verified bool `gorm:"default:false" json:"verified"` + IsAdmin bool `gorm:"default:false" json:"is_admin"` UUID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid()" json:"uuid"` VerificationCode *string `gorm:"size:6;index" json:"verification_code,omitempty"` // 6-digit verification code VerificationExpiry *time.Time `gorm:"index" json:"verification_expiry,omitempty"` // Expiry for email verification code diff --git a/frontend/admin.html b/frontend/admin.html new file mode 100644 index 0000000..dec0a97 --- /dev/null +++ b/frontend/admin.html @@ -0,0 +1,260 @@ + + + + + + + Admin Dashboard - NORA + + + + + + + + + + + + + +
+ +
+

+ Admin Dashboard +

+

Verwaltung und Statistiken

+
+ + +
+ +
+
+
+ + + +
+
+

0

+

Registrierte Nutzer

+
+ + +
+
+
+ + + +
+
+

0

+

Eigene Stunden

+
+ + +
+
+
+ + + +
+
+

0

+

Eingetragene Klausuren

+
+
+ + +
+
+

Nutzerverwaltung

+
+ + + + +
+
+ +
+ + + + + + + + + + + + + +
+ Name + Email + Status + Registriert am + Aktionen
+
+ + +
+
+ Seite 1 von 1 +
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/js/admin.js b/frontend/js/admin.js new file mode 100644 index 0000000..0fdc7ab --- /dev/null +++ b/frontend/js/admin.js @@ -0,0 +1,375 @@ +/** + * Admin Dashboard JavaScript + */ + +(function () { + let currentPage = 1; + let totalPages = 1; + let currentSearch = ''; + + // Initialize + document.addEventListener('DOMContentLoaded', async () => { + if (!(await checkAuth())) return; + + // Check if user is admin + let userData = JSON.parse(localStorage.getItem('userData') || '{}'); + + // If not admin in local storage, try to fetch fresh profile to be sure + if (!userData.is_admin) { + try { + console.log('Checking admin status from server...'); + userData = await UserAPI.getProfile(); + localStorage.setItem('userData', JSON.stringify(userData)); + } catch (e) { + console.error('Failed to fetch profile:', e); + } + } + + if (!userData.is_admin) { + console.log('User is not admin, redirecting...'); + window.location.href = 'dashboard.html'; + return; + } + + // Fix: Set user initials in navbar + if (userData.initials && typeof setUserInitials === 'function') { + setUserInitials(userData.initials); + } + + // Load data + loadStats(); + loadUsers(); + + // Event listeners + document.getElementById('userSearch').addEventListener('input', debounce((e) => { + currentSearch = e.target.value; + currentPage = 1; + loadUsers(); + }, 500)); + + document.getElementById('prevPage').addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + loadUsers(); + } + }); + + document.getElementById('nextPage').addEventListener('click', () => { + if (currentPage < totalPages) { + currentPage++; + loadUsers(); + } + }); + + // Modal forms + document.getElementById('editUserForm').addEventListener('submit', handleEditUserSubmit); + document.getElementById('resetPasswordForm').addEventListener('submit', handleResetPasswordSubmit); + }); + + async function loadStats() { + try { + const stats = await AdminAPI.getStats(); + document.getElementById('totalUsers').textContent = stats.user_count; + document.getElementById('totalCustomHours').textContent = stats.custom_hour_count; + document.getElementById('totalExams').textContent = stats.exam_count; + } catch (error) { + console.error('Error loading stats:', error); + showToast('Fehler beim Laden der Statistiken', 'error'); + } + } + + async function loadUsers() { + try { + const data = await AdminAPI.getUsers(currentPage, 20, currentSearch); + renderUsers(data.users); + updatePagination(data.meta); + } catch (error) { + console.error('Error loading users:', error); + showToast('Fehler beim Laden der Nutzer', 'error'); + } + } + + function renderUsers(users) { + const tbody = document.getElementById('usersTableBody'); + tbody.innerHTML = users.map(user => ` + + +
+
+ ${user.initials} +
+
+
${user.first_name} ${user.last_name}
+
+
+ + +
${user.mail}
+ + +
+ + ${user.is_admin ? 'Admin' : 'User'} + + + ${user.verified ? 'Verifiziert' : 'Nicht verifiziert'} + +
+ + + ${new Date(user.created_at).toLocaleDateString()} + + +
+ + + + + + + + + + + ${!user.is_admin ? ` + + ` : ''} + + + +
+ + + `).join(''); + } + + function updatePagination(meta) { + currentPage = meta.page; + totalPages = meta.pages; + document.getElementById('currentPage').textContent = currentPage; + document.getElementById('totalPages').textContent = totalPages; + document.getElementById('prevPage').disabled = currentPage === 1; + document.getElementById('nextPage').disabled = currentPage === totalPages; + } + + // Password Prompt Logic + let passwordPromptCallback = null; + let passwordPromptCancelCallback = null; + + window.showPasswordPrompt = function (callback, cancelCallback = null) { + passwordPromptCallback = callback; + passwordPromptCancelCallback = cancelCallback; + document.getElementById('promptPassword').value = ''; + document.getElementById('passwordPromptModal').classList.remove('hidden'); + document.getElementById('promptPassword').focus(); + }; + + window.closePasswordPromptModal = function (isConfirmed = false) { + document.getElementById('passwordPromptModal').classList.add('hidden'); + + if (!isConfirmed && passwordPromptCancelCallback) { + passwordPromptCancelCallback(); + } + + passwordPromptCallback = null; + passwordPromptCancelCallback = null; + }; + + document.getElementById('passwordPromptForm').addEventListener('submit', function (e) { + e.preventDefault(); + const password = document.getElementById('promptPassword').value; + if (passwordPromptCallback) { + passwordPromptCallback(password); + } + closePasswordPromptModal(true); + }); + + // Global Functions for UI Actions + + window.promoteUser = function (userId, userName) { + showConfirmDialog(`Möchtest du ${userName} wirklich zum Admin befördern?`, () => { + showPasswordPrompt(async (password) => { + try { + await AdminAPI.promoteUser(userId, password); + showToast(`${userName} wurde zum Admin befördert`, 'success'); + loadUsers(); + } catch (error) { + console.error('Error promoting user:', error); + showToast(error.message || 'Fehler beim Befördern', 'error'); + } + }); + }); + }; + + window.deleteUser = function (userId, userName) { + showConfirmDialog(`Möchtest du ${userName} wirklich löschen? Dies kann nicht rückgängig gemacht werden.`, () => { + showPasswordPrompt(async (password) => { + try { + await AdminAPI.deleteUser(userId, password); + showToast(`${userName} wurde gelöscht`, 'success'); + loadUsers(); + loadStats(); // Update stats too + } catch (error) { + console.error('Error deleting user:', error); + showToast(error.message || 'Fehler beim Löschen', 'error'); + } + }); + }); + }; + + window.verifyUser = async function (userId, verified, userName) { + const action = verified ? 'verifizieren' : 'die Verifizierung entziehen'; + showConfirmDialog(`Möchtest du ${userName} wirklich ${action}?`, () => { + showPasswordPrompt(async (password) => { + try { + await AdminAPI.verifyUser(userId, verified, password); + showToast(`Status für ${userName} aktualisiert`, 'success'); + loadUsers(); + } catch (error) { + console.error('Error verifying user:', error); + showToast(error.message || 'Fehler beim Aktualisieren', 'error'); + } + }); + }); + }; + + // Edit User Modal Logic + window.openEditUserModal = function (user) { + document.getElementById('editUserId').value = user.id; + document.getElementById('editFirstName').value = user.first_name; + document.getElementById('editLastName').value = user.last_name; + document.getElementById('editEmail').value = user.mail; + document.getElementById('editInitials').value = user.initials; + + document.getElementById('editUserModal').classList.remove('hidden'); + }; + + window.closeEditUserModal = function () { + document.getElementById('editUserModal').classList.add('hidden'); + }; + + async function handleEditUserSubmit(e) { + e.preventDefault(); + const userId = document.getElementById('editUserId').value; + const data = { + first_name: document.getElementById('editFirstName').value, + last_name: document.getElementById('editLastName').value, + mail: document.getElementById('editEmail').value, + initials: document.getElementById('editInitials').value + }; + + // Hide edit modal immediately + closeEditUserModal(); + + showConfirmDialog('Möchtest du die Änderungen an diesem Nutzer wirklich speichern?', + // On Confirm + () => { + showPasswordPrompt(async (password) => { + try { + await AdminAPI.updateUser(userId, data, password); + showToast('Nutzer erfolgreich aktualisiert', 'success'); + loadUsers(); + // Modal remains closed on success + } catch (error) { + console.error('Error updating user:', error); + showToast(error.message || 'Fehler beim Aktualisieren', 'error'); + // Re-open modal on error so user can fix inputs + openEditUserModal({ + id: userId, + first_name: data.first_name, + last_name: data.last_name, + mail: data.mail, + initials: data.initials + }); + } + }, + // On Password Cancel (we need to update showPasswordPrompt to support this) + () => { + openEditUserModal({ + id: userId, + first_name: data.first_name, + last_name: data.last_name, + mail: data.mail, + initials: data.initials + }); + }); + }, + // On Confirm Cancel + () => { + openEditUserModal({ + id: userId, + first_name: data.first_name, + last_name: data.last_name, + mail: data.mail, + initials: data.initials + }); + } + ); + } + + // Reset Password Modal Logic + window.openResetPasswordModal = function (userId, userName) { + document.getElementById('resetUserId').value = userId; + document.getElementById('newPassword').value = ''; + document.getElementById('resetPasswordModal').classList.remove('hidden'); + }; + + window.closeResetPasswordModal = function () { + document.getElementById('resetPasswordModal').classList.add('hidden'); + }; + + async function handleResetPasswordSubmit(e) { + e.preventDefault(); + const userId = document.getElementById('resetUserId').value; + const newPassword = document.getElementById('newPassword').value; + + showConfirmDialog('Möchtest du das Passwort für diesen Nutzer wirklich ändern?', () => { + showPasswordPrompt(async (adminPassword) => { + try { + await AdminAPI.resetUserPassword(userId, newPassword, adminPassword); + showToast('Passwort erfolgreich geändert', 'success'); + closeResetPasswordModal(); + } catch (error) { + console.error('Error resetting password:', error); + showToast(error.message || 'Fehler beim Ändern des Passworts', 'error'); + } + }); + }); + } + + // Helper for debounce + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + +})(); diff --git a/frontend/js/navbar.js b/frontend/js/navbar.js index 0087f2d..2813745 100644 --- a/frontend/js/navbar.js +++ b/frontend/js/navbar.js @@ -3,8 +3,8 @@ * Renders navbar dynamically with preloader */ -(function() { -// Local reference to storage (exported by storage-manager.js to window.storage) +(function () { + // Local reference to storage (exported by storage-manager.js to window.storage) const storage = window.storage; /** @@ -34,6 +34,9 @@ Raumplan + @@ -127,6 +130,12 @@