Bladeren bron

add admin panel

Daniel Bohry 2 weken geleden
bovenliggende
commit
b8e9527213

+ 1 - 1
src/main/java/com/danielbohry/authservice/App.java

@@ -6,7 +6,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 @SpringBootApplication
 public class App {
 
-    public static void main(String[] args) {
+    static void main(String[] args) {
         SpringApplication.run(App.class, args);
     }
 

+ 41 - 12
src/main/java/com/danielbohry/authservice/api/UserController.java

@@ -1,12 +1,11 @@
 package com.danielbohry.authservice.api;
 
-import com.danielbohry.authservice.api.dto.AuthenticationResponse;
-import com.danielbohry.authservice.api.dto.PasswordChangeRequest;
-import com.danielbohry.authservice.api.dto.PasswordResetRequest;
-import com.danielbohry.authservice.api.dto.ProfileUpdateRequest;
-import com.danielbohry.authservice.api.dto.UserResponse;
+import com.danielbohry.authservice.api.dto.*;
 import com.danielbohry.authservice.domain.ApplicationUser;
+import com.danielbohry.authservice.domain.Role;
 import com.danielbohry.authservice.service.auth.AuthService;
+import com.danielbohry.authservice.service.user.UserService;
+import com.mongodb.internal.bulk.UpdateRequest;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
@@ -14,6 +13,10 @@ import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
+import static com.danielbohry.authservice.domain.Role.ADMIN;
+import static java.time.Instant.now;
 import static org.springframework.http.HttpStatus.FORBIDDEN;
 import static org.springframework.http.HttpStatus.UNAUTHORIZED;
 
@@ -24,17 +27,43 @@ import static org.springframework.http.HttpStatus.UNAUTHORIZED;
 @RequestMapping("api/users")
 public class UserController {
 
-    private final AuthService service;
+    private final AuthService authService;
+    private final UserService userService;
+
+    @GetMapping
+    public ResponseEntity<List<UserResponse>> getAll() {
+        SecurityContext context = SecurityContextHolder.getContext();
+        Object principal = context.getAuthentication().getPrincipal();
+        if (principal instanceof ApplicationUser user && user.getRoles().contains(ADMIN)) {
+            return ResponseEntity.ok(userService.findAll().stream()
+                    .map(UserResponse::from)
+                    .toList());
+        }
+
+        return ResponseEntity.status(FORBIDDEN).build();
+    }
+
+    @PutMapping("{userId}")
+    public ResponseEntity<UserResponse> update(@PathVariable String userId, @RequestBody UserUpdateRequest request) {
+        SecurityContext context = SecurityContextHolder.getContext();
+        Object principal = context.getAuthentication().getPrincipal();
+        if (principal instanceof ApplicationUser user && user.getRoles().contains(ADMIN)) {
+            user.setActive(request.isActive());
+            user.setRoles(request.roles());
+            user.setEmail(request.email());
+            user.setUpdatedAt(now());
+            return ResponseEntity.ok(UserResponse.from(userService.update(userId, user)));
+        }
+
+        return ResponseEntity.status(FORBIDDEN).build();
+    }
 
     @GetMapping("current")
     public ResponseEntity<UserResponse> get() {
         SecurityContext context = SecurityContextHolder.getContext();
         Object principal = context.getAuthentication().getPrincipal();
         if (principal instanceof ApplicationUser user) {
-            return ResponseEntity.ok(new UserResponse(user.getId(), user.getUsername(), user.getRoles()
-                    .stream()
-                    .map(Enum::toString)
-                    .toList()));
+            return ResponseEntity.ok(UserResponse.from(user));
         }
 
         return ResponseEntity.status(FORBIDDEN).build();
@@ -47,7 +76,7 @@ public class UserController {
 
         if (principal instanceof ApplicationUser user) {
             log.info("Resetting password for user [{}]", user.getUsername());
-            var response = service.resetPassword(user.getId(), request.getNewPassword());
+            var response = authService.resetPassword(user.getId(), request.getNewPassword());
             return ResponseEntity.ok(response);
         }
 
@@ -61,7 +90,7 @@ public class UserController {
 
         if (principal instanceof ApplicationUser user) {
             log.info("Updating profile for user [{}]", user.getUsername());
-            var response = service.updateProfile(user.getId(), request.getCurrentPassword(), request.getNewPassword(), request.getEmail());
+            var response = authService.updateProfile(user.getId(), request.getCurrentPassword(), request.getNewPassword(), request.getEmail());
             return ResponseEntity.ok(response);
         }
 

+ 11 - 0
src/main/java/com/danielbohry/authservice/api/dto/UserResponse.java

@@ -1,5 +1,6 @@
 package com.danielbohry.authservice.api.dto;
 
+import com.danielbohry.authservice.domain.ApplicationUser;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
@@ -13,6 +14,16 @@ public class UserResponse {
 
     private String id;
     private String username;
+    private String email;
     private List<String> roles;
+    private boolean isActive;
+
+    public static UserResponse from(ApplicationUser user) {
+        return new UserResponse(user.getId(), user.getUsername(), user.getEmail(), user.getRoles()
+                .stream()
+                .map(Enum::toString)
+                .toList(),
+                user.isActive());
+    }
 
 }

+ 8 - 0
src/main/java/com/danielbohry/authservice/api/dto/UserUpdateRequest.java

@@ -0,0 +1,8 @@
+package com.danielbohry.authservice.api.dto;
+
+import com.danielbohry.authservice.domain.Role;
+
+import java.util.List;
+
+public record UserUpdateRequest(List<Role> roles, String email, boolean isActive) {
+}

+ 1 - 0
src/main/java/com/danielbohry/authservice/config/SecurityConfig.java

@@ -46,6 +46,7 @@ public class SecurityConfig {
                         .requestMatchers(
                                 "/",
                                 "/index.html",
+                                "/admin.html",
                                 "/css/**",
                                 "/js/**",
                                 "/img/**"

+ 4 - 0
src/main/java/com/danielbohry/authservice/domain/ApplicationUser.java

@@ -7,6 +7,7 @@ import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 
+import java.time.Instant;
 import java.util.Collection;
 import java.util.List;
 
@@ -21,6 +22,9 @@ public class ApplicationUser implements UserDetails {
     private String email;
     private List<Role> roles;
     private boolean active;
+    private Instant createdAt;
+    private Instant updatedAt;
+    private Instant lastLoginAt;
 
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {

+ 8 - 1
src/main/java/com/danielbohry/authservice/service/auth/AuthService.java

@@ -19,6 +19,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import static com.danielbohry.authservice.service.auth.UserConverter.convert;
+import static java.time.Instant.now;
 
 @Slf4j
 @Service
@@ -44,7 +45,11 @@ public class AuthService implements UserDetailsService {
 
     public AuthenticationResponse register(AuthenticationRequest request) {
         UserDetails user = buildUserDetails(request);
-        ApplicationUser saved = service.create(convert(user, request.getEmail()));
+        ApplicationUser toSave = convert(user, request.getEmail());
+        toSave.setCreatedAt(now());
+        toSave.setUpdatedAt(now());
+        toSave.setLastLoginAt(now());
+        ApplicationUser saved = service.create(toSave);
         Authentication authentication = jwtService.generateToken(saved);
         log.info("Username [{}] registered", request.getUsername());
         return buildResponse(saved, authentication);
@@ -57,6 +62,8 @@ public class AuthService implements UserDetailsService {
         ApplicationUser user = service.findByUsername(request.getUsername());
         Authentication authentication = jwtService.generateToken(user);
         log.info("Username [{}] authenticated", request.getUsername());
+        user.setLastLoginAt(now());
+        service.update(user.getId(), user);
         return buildResponse(user, authentication);
     }
 

+ 19 - 3
src/main/java/com/danielbohry/authservice/service/user/UserService.java

@@ -25,11 +25,20 @@ public class UserService {
             .orElseThrow(() -> new NotFoundException("User not found"));
     }
 
+    public List<ApplicationUser> findAll() {
+        return repository.findAll();
+    }
+
     public ApplicationUser findByUsername(String username) {
         return repository.findByUsernameAndActiveTrue(username)
             .orElseThrow(() -> new NotFoundException("User not found"));
     }
 
+    public ApplicationUser findById(String id) {
+        return repository.findById(id)
+                .orElseThrow(() -> new NotFoundException("User not found"));
+    }
+
     public ApplicationUser create(ApplicationUser applicationUser) {
         validateUsername(applicationUser);
 
@@ -41,9 +50,16 @@ public class UserService {
         return repository.save(applicationUser);
     }
 
-    public ApplicationUser update(ApplicationUser applicationUser) {
-        validateUsername(applicationUser);
-        return repository.save(applicationUser);
+    public ApplicationUser update(String id, ApplicationUser applicationUser) {
+        ApplicationUser current = findById(id);
+
+        current.setRoles(applicationUser.getRoles());
+        current.setEmail(applicationUser.getEmail());
+        current.setActive(applicationUser.isActive());
+        current.setUpdatedAt(applicationUser.getUpdatedAt());
+        current.setLastLoginAt(applicationUser.getLastLoginAt());
+
+        return repository.save(current);
     }
 
     public ApplicationUser changePassword(String userId, String currentPassword, String newPassword, PasswordEncoder passwordEncoder) {

+ 186 - 0
src/main/resources/static/admin.html

@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Auth Service - Admin Panel</title>
+    <link rel="stylesheet" href="css/main.css">
+    <link rel="stylesheet" href="css/admin.css">
+    <link rel="icon" href="img/favicon.png" type="image/png">
+</head>
+<body>
+    <div class="admin-container">
+        <div class="header">
+            <h1>Admin Panel</h1>
+            <p>Manage registered users</p>
+            <div class="header-controls">
+                <button class="header-btn" id="refreshBtn" onclick="loadUsers()" title="Refresh users">
+                    <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
+                        <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
+                    </svg>
+                </button>
+                <button class="header-btn" onclick="window.location.href='index.html'" title="Back to login">
+                    <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
+                        <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
+                    </svg>
+                </button>
+                <button class="theme-toggle" id="themeToggle" onclick="toggleTheme()" title="Switch theme">
+                    <svg class="theme-icon sun-icon" viewBox="0 0 24 24" fill="currentColor">
+                        <path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"/>
+                    </svg>
+                    <svg class="theme-icon moon-icon" viewBox="0 0 24 24" fill="currentColor">
+                        <path d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z"/>
+                    </svg>
+                </button>
+            </div>
+        </div>
+
+        <div class="admin-controls">
+            <div class="stats-container">
+                <div class="stat-card">
+                    <h3 id="totalUsers">-</h3>
+                    <p>Total Users</p>
+                </div>
+                <div class="stat-card">
+                    <h3 id="activeUsers">-</h3>
+                    <p>Active Today</p>
+                </div>
+                <div class="stat-card">
+                    <h3 id="adminUsers">-</h3>
+                    <p>Administrators</p>
+                </div>
+            </div>
+        </div>
+
+        <div class="users-section">
+            <div class="section-header">
+                <h2>Registered Users</h2>
+                <div class="search-container">
+                    <input type="text" id="searchInput" placeholder="Search users..." oninput="filterUsers()">
+                    <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
+                        <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
+                    </svg>
+                </div>
+            </div>
+
+            <div class="table-container">
+                <table class="users-table" id="usersTable">
+                    <thead>
+                        <tr>
+                            <th>Username</th>
+                            <th>Email</th>
+                            <th>Roles</th>
+                            <th>Status</th>
+                            <th>Last Login</th>
+                            <th>Actions</th>
+                        </tr>
+                    </thead>
+                    <tbody id="usersTableBody">
+                        <!-- Users will be loaded here -->
+                    </tbody>
+                </table>
+            </div>
+
+            <div id="loadingIndicator" class="loading-container" style="display: none;">
+                <div class="loading-spinner"></div>
+                <p>Loading users...</p>
+            </div>
+
+            <div id="emptyState" class="empty-state" style="display: none;">
+                <svg width="64" height="64" viewBox="0 0 24 24" fill="currentColor" opacity="0.5">
+                    <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
+                </svg>
+                <h3>No users found</h3>
+                <p>No users match your search criteria.</p>
+            </div>
+        </div>
+
+        <div id="messageContainer" class="message" style="display: none;"></div>
+    </div>
+
+    <!-- Edit User Modal -->
+    <div id="editUserModal" class="modal" style="display: none;">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h3>Edit User</h3>
+                <button class="close-btn" onclick="closeEditUserModal()">
+                    <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
+                        <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
+                    </svg>
+                </button>
+            </div>
+
+            <form id="editUserForm" onsubmit="handleEditUserSubmit(event)">
+                <div class="form-section">
+                    <div class="form-group">
+                        <label for="editUserEmail">Email Address</label>
+                        <input type="email" id="editUserEmail" name="email" placeholder="Enter email address">
+                    </div>
+                </div>
+
+                <div class="form-section">
+                    <label class="section-label">User Status</label>
+                    <div class="toggle-group">
+                        <label class="toggle-switch">
+                            <input type="checkbox" id="editUserActive" name="isActive">
+                            <span class="toggle-slider"></span>
+                            <span class="toggle-label">Active User</span>
+                        </label>
+                    </div>
+                </div>
+
+                <div class="form-section">
+                    <label class="section-label">User Roles</label>
+                    <div class="roles-grid">
+                        <label class="role-checkbox">
+                            <input type="checkbox" name="role" value="USER">
+                            <span class="role-checkmark"></span>
+                            <span class="role-name">USER</span>
+                        </label>
+                        <label class="role-checkbox">
+                            <input type="checkbox" name="role" value="ADMIN">
+                            <span class="role-checkmark"></span>
+                            <span class="role-name">ADMIN</span>
+                        </label>
+                        <label class="role-checkbox">
+                            <input type="checkbox" name="role" value="MODERATOR">
+                            <span class="role-checkmark"></span>
+                            <span class="role-name">MODERATOR</span>
+                        </label>
+                        <label class="role-checkbox">
+                            <input type="checkbox" name="role" value="SERVICE">
+                            <span class="role-checkmark"></span>
+                            <span class="role-name">SERVICE</span>
+                        </label>
+                        <label class="role-checkbox">
+                            <input type="checkbox" name="role" value="SYSTEM">
+                            <span class="role-checkmark"></span>
+                            <span class="role-name">SYSTEM</span>
+                        </label>
+                        <label class="role-checkbox">
+                            <input type="checkbox" name="role" value="VPN">
+                            <span class="role-checkmark"></span>
+                            <span class="role-name">VPN</span>
+                        </label>
+                        <label class="role-checkbox">
+                            <input type="checkbox" name="role" value="TESTER">
+                            <span class="role-checkmark"></span>
+                            <span class="role-name">TESTER</span>
+                        </label>
+                    </div>
+                </div>
+
+                <div class="modal-buttons">
+                    <button type="button" class="btn-secondary" onclick="closeEditUserModal()">Cancel</button>
+                    <button type="submit" class="btn-primary" id="saveUserBtn">Save Changes</button>
+                </div>
+
+                <div id="editUserMessage" class="modal-message" style="display: none;"></div>
+            </form>
+        </div>
+    </div>
+
+    <script src="js/main.js"></script>
+    <script src="js/admin.js"></script>
+</body>
+</html>

+ 779 - 0
src/main/resources/static/css/admin.css

@@ -0,0 +1,779 @@
+/* Admin Panel Specific Styles */
+
+/* Extend the container for admin panel to accommodate wider content */
+.admin-container {
+    background: var(--container-bg);
+    border-radius: 15px;
+    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
+    overflow: hidden;
+    width: 100%;
+    max-width: 1200px;
+    margin: 20px;
+    transition: background 0.3s ease;
+    min-height: 600px;
+}
+
+/* Header controls */
+.header-controls {
+    position: absolute;
+    top: 20px;
+    right: 20px;
+    display: flex;
+    gap: 8px;
+    align-items: center;
+}
+
+.header-btn {
+    background: rgba(255, 255, 255, 0.2);
+    border: 1px solid rgba(255, 255, 255, 0.3);
+    border-radius: 50%;
+    width: 40px;
+    height: 40px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    backdrop-filter: blur(10px);
+    color: var(--header-text);
+}
+
+.header-btn:hover {
+    background: rgba(255, 255, 255, 0.3);
+    transform: scale(1.1);
+}
+
+.header-btn svg {
+    transition: all 0.3s ease;
+}
+
+/* Admin controls section */
+.admin-controls {
+    padding: 20px;
+    background: var(--section-bg);
+    border-bottom: 1px solid var(--border-color);
+    transition: all 0.3s ease;
+}
+
+/* Stats cards */
+.stats-container {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+    gap: 15px;
+    margin-bottom: 20px;
+}
+
+.stat-card {
+    background: var(--user-details-bg);
+    padding: 20px;
+    border-radius: 10px;
+    text-align: center;
+    border: 2px solid transparent;
+    transition: all 0.3s ease;
+    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.stat-card:hover {
+    border-color: var(--tab-border);
+    transform: translateY(-2px);
+}
+
+.stat-card h3 {
+    font-size: 2rem;
+    font-weight: bold;
+    color: var(--tab-active-text);
+    margin: 0 0 5px 0;
+    transition: color 0.3s ease;
+}
+
+.stat-card p {
+    font-size: 0.9rem;
+    color: var(--text-secondary);
+    margin: 0;
+    transition: color 0.3s ease;
+}
+
+/* Control buttons */
+.control-buttons {
+    display: flex;
+    gap: 10px;
+    flex-wrap: wrap;
+}
+
+.action-btn.secondary {
+    background: var(--user-info-bg);
+    color: var(--text-primary);
+    border: 2px solid var(--border-color);
+}
+
+.action-btn.secondary:hover {
+    background: var(--tab-active-bg);
+    border-color: var(--tab-border);
+}
+
+.action-btn svg {
+    margin-right: 5px;
+    vertical-align: middle;
+}
+
+/* Users section */
+.users-section {
+    padding: 20px;
+}
+
+.section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+    flex-wrap: wrap;
+    gap: 15px;
+}
+
+.section-header h2 {
+    color: var(--text-primary);
+    margin: 0;
+    font-size: 1.5rem;
+    transition: color 0.3s ease;
+}
+
+/* Search container */
+.search-container {
+    position: relative;
+    min-width: 250px;
+}
+
+.search-container input {
+    width: 100%;
+    padding: 10px 40px 10px 15px;
+    border: 2px solid var(--input-border);
+    border-radius: 25px;
+    background: var(--input-bg);
+    color: var(--input-text);
+    font-size: 0.9rem;
+    transition: all 0.3s ease;
+}
+
+.search-container input:focus {
+    outline: none;
+    border-color: var(--input-focus-border);
+    box-shadow: 0 0 0 3px var(--input-focus-shadow);
+}
+
+.search-icon {
+    position: absolute;
+    right: 12px;
+    top: 50%;
+    transform: translateY(-50%);
+    color: var(--text-secondary);
+    pointer-events: none;
+    transition: color 0.3s ease;
+}
+
+/* Table styles */
+.table-container {
+    background: var(--user-details-bg);
+    border-radius: 10px;
+    overflow: hidden;
+    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+    transition: background 0.3s ease;
+}
+
+.users-table {
+    width: 100%;
+    border-collapse: collapse;
+    font-size: 0.9rem;
+}
+
+.users-table th,
+.users-table td {
+    padding: 15px 12px;
+    text-align: left;
+    border-bottom: 1px solid var(--border-color);
+    transition: border-color 0.3s ease;
+}
+
+.users-table th {
+    background: var(--section-bg);
+    font-weight: 600;
+    color: var(--text-primary);
+    text-transform: uppercase;
+    font-size: 0.8rem;
+    letter-spacing: 0.5px;
+    transition: all 0.3s ease;
+}
+
+.users-table tbody tr:hover {
+    background: var(--section-bg);
+    transition: background 0.3s ease;
+}
+
+.users-table tbody tr:last-child td {
+    border-bottom: none;
+}
+
+/* Table cell content */
+.users-table .user-info {
+    display: block !important;
+    padding: 0;
+    background: none;
+    border: none;
+}
+
+.username {
+    font-weight: 600;
+    color: var(--text-primary);
+    transition: color 0.3s ease;
+    display: inline !important;
+}
+
+.email {
+    color: var(--text-secondary);
+    font-size: 0.85rem;
+    transition: color 0.3s ease;
+}
+
+/* Role badges */
+.roles {
+    display: flex;
+    gap: 5px;
+    flex-wrap: wrap;
+}
+
+.role-badge {
+    display: inline-block;
+    padding: 3px 8px;
+    border-radius: 12px;
+    font-size: 0.7rem;
+    font-weight: 600;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+    transition: all 0.3s ease;
+}
+
+.role-badge.admin {
+    background: linear-gradient(135deg, #ff6b6b, #ee5a52);
+    color: white;
+}
+
+.role-badge.user {
+    background: var(--section-bg);
+    color: var(--text-secondary);
+    border: 1px solid var(--border-color);
+}
+
+.role-badge.moderator {
+    background: linear-gradient(135deg, #4ecdc4, #44a08d);
+    color: white;
+}
+
+.role-badge.service {
+    background: linear-gradient(135deg, #667eea, #764ba2);
+    color: white;
+}
+
+.role-badge.system {
+    background: linear-gradient(135deg, #2d3748, #4a5568);
+    color: white;
+}
+
+.role-badge.vpn {
+    background: linear-gradient(135deg, #48bb78, #38a169);
+    color: white;
+}
+
+.role-badge.tester {
+    background: linear-gradient(135deg, #ed8936, #dd6b20);
+    color: white;
+}
+
+/* Default/fallback style for any unknown roles */
+.role-badge:not(.admin):not(.user):not(.moderator):not(.service):not(.system):not(.vpn):not(.tester) {
+    background: linear-gradient(135deg, #718096, #4a5568);
+    color: white;
+    border: 1px solid var(--border-color);
+}
+
+/* Status badges */
+.status-badge {
+    display: inline-block;
+    padding: 4px 10px;
+    border-radius: 15px;
+    font-size: 0.75rem;
+    font-weight: 600;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+    transition: all 0.3s ease;
+}
+
+.status-badge.active {
+    background: var(--message-success-bg);
+    color: var(--message-success-text);
+    border: 1px solid var(--message-success-border);
+}
+
+.status-badge.inactive {
+    background: var(--message-error-bg);
+    color: var(--message-error-text);
+    border: 1px solid var(--message-error-border);
+}
+
+/* Last login */
+.last-login {
+    color: var(--text-secondary);
+    font-size: 0.8rem;
+    transition: color 0.3s ease;
+}
+
+/* Action buttons in table */
+.action-buttons {
+    display: flex;
+    gap: 5px;
+}
+
+.action-btn.small {
+    padding: 6px 8px;
+    font-size: 0.8rem;
+    min-width: auto;
+    width: auto;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.action-btn.small svg {
+    margin: 0;
+}
+
+/* Loading states */
+.loading-container {
+    text-align: center;
+    padding: 40px 20px;
+    color: var(--text-secondary);
+}
+
+.loading-spinner {
+    display: inline-block;
+    width: 32px;
+    height: 32px;
+    border: 3px solid var(--border-color);
+    border-radius: 50%;
+    border-top-color: var(--tab-border);
+    animation: spin 1s ease-in-out infinite;
+    margin-bottom: 15px;
+}
+
+/* Empty state */
+.empty-state {
+    text-align: center;
+    padding: 60px 20px;
+    color: var(--text-secondary);
+}
+
+.empty-state h3 {
+    color: var(--text-primary);
+    margin: 15px 0 10px 0;
+    transition: color 0.3s ease;
+}
+
+.empty-state p {
+    margin: 0;
+    font-size: 0.9rem;
+}
+
+.empty-state svg {
+    margin-bottom: 15px;
+}
+
+
+/* Responsive design */
+@media (max-width: 768px) {
+    .admin-container {
+        margin: 10px;
+        max-width: none;
+    }
+
+    .header-controls {
+        position: relative;
+        top: 15px;
+        right: auto;
+        justify-content: center;
+        margin-top: 15px;
+    }
+
+    .stats-container {
+        grid-template-columns: 1fr;
+        gap: 10px;
+    }
+
+    .section-header {
+        flex-direction: column;
+        align-items: stretch;
+    }
+
+    .search-container {
+        min-width: auto;
+    }
+
+    /* Make table horizontally scrollable on mobile */
+    .table-container {
+        overflow-x: auto;
+    }
+
+    .users-table {
+        min-width: 700px;
+    }
+
+    .users-table th,
+    .users-table td {
+        padding: 10px 8px;
+        white-space: nowrap;
+    }
+
+    .roles {
+        flex-direction: column;
+        gap: 3px;
+    }
+
+    .action-buttons {
+        flex-direction: column;
+        gap: 3px;
+    }
+}
+
+@media (max-width: 480px) {
+    .users-section {
+        padding: 15px;
+    }
+
+    .admin-controls {
+        padding: 15px;
+    }
+
+    .stat-card {
+        padding: 15px;
+    }
+
+    .stat-card h3 {
+        font-size: 1.5rem;
+    }
+}
+
+/* Edit User Modal */
+.modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.6);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 1000;
+    backdrop-filter: blur(4px);
+}
+
+.modal-content {
+    background: var(--container-bg);
+    border-radius: 15px;
+    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
+    width: 100%;
+    max-width: 500px;
+    max-height: 90vh;
+    overflow-y: auto;
+    margin: 20px;
+    transition: all 0.3s ease;
+}
+
+.modal-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 25px 30px 20px;
+    border-bottom: 1px solid var(--border-color);
+}
+
+.modal-header h3 {
+    margin: 0;
+    color: var(--text-primary);
+    font-size: 1.4rem;
+    transition: color 0.3s ease;
+}
+
+.close-btn {
+    background: none;
+    border: none;
+    color: var(--text-secondary);
+    cursor: pointer;
+    padding: 5px;
+    border-radius: 50%;
+    transition: all 0.3s ease;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.close-btn:hover {
+    background: var(--section-bg);
+    color: var(--text-primary);
+    transform: scale(1.1);
+}
+
+.form-section {
+    padding: 20px 30px;
+    border-bottom: 1px solid var(--border-color);
+}
+
+.form-section:last-of-type {
+    border-bottom: none;
+}
+
+.section-label {
+    display: block;
+    font-weight: 600;
+    color: var(--text-primary);
+    margin-bottom: 15px;
+    font-size: 1rem;
+    transition: color 0.3s ease;
+}
+
+/* Toggle Switch */
+.toggle-group {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.toggle-switch {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    cursor: pointer;
+    padding: 8px 0;
+}
+
+.toggle-switch input[type="checkbox"] {
+    display: none;
+}
+
+.toggle-slider {
+    position: relative;
+    width: 50px;
+    height: 24px;
+    background: var(--border-color);
+    border-radius: 24px;
+    transition: all 0.3s ease;
+}
+
+.toggle-slider::before {
+    content: '';
+    position: absolute;
+    top: 2px;
+    left: 2px;
+    width: 20px;
+    height: 20px;
+    background: white;
+    border-radius: 50%;
+    transition: all 0.3s ease;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.toggle-switch input[type="checkbox"]:checked + .toggle-slider {
+    background: var(--tab-border);
+}
+
+.toggle-switch input[type="checkbox"]:checked + .toggle-slider::before {
+    transform: translateX(26px);
+}
+
+.toggle-label {
+    color: var(--text-primary);
+    font-weight: 500;
+    transition: color 0.3s ease;
+}
+
+/* Role Checkboxes */
+.roles-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+    gap: 12px;
+}
+
+.role-checkbox {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    cursor: pointer;
+    padding: 12px;
+    border: 2px solid var(--border-color);
+    border-radius: 8px;
+    transition: all 0.3s ease;
+    background: var(--user-details-bg);
+}
+
+.role-checkbox:hover {
+    border-color: var(--tab-border);
+    background: var(--section-bg);
+}
+
+.role-checkbox input[type="checkbox"] {
+    display: none;
+}
+
+.role-checkmark {
+    width: 18px;
+    height: 18px;
+    border: 2px solid var(--border-color);
+    border-radius: 4px;
+    position: relative;
+    transition: all 0.3s ease;
+    flex-shrink: 0;
+}
+
+.role-checkmark::after {
+    content: '';
+    position: absolute;
+    top: 2px;
+    left: 5px;
+    width: 4px;
+    height: 8px;
+    border: solid white;
+    border-width: 0 2px 2px 0;
+    transform: rotate(45deg);
+    opacity: 0;
+    transition: opacity 0.3s ease;
+}
+
+.role-checkbox input[type="checkbox"]:checked + .role-checkmark {
+    background: var(--tab-border);
+    border-color: var(--tab-border);
+}
+
+.role-checkbox input[type="checkbox"]:checked + .role-checkmark::after {
+    opacity: 1;
+}
+
+.role-name {
+    font-size: 0.85rem;
+    font-weight: 500;
+    color: var(--text-primary);
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+    transition: color 0.3s ease;
+}
+
+/* Modal Buttons */
+.modal-buttons {
+    display: flex;
+    gap: 12px;
+    padding: 25px 30px;
+    justify-content: flex-end;
+}
+
+.btn-primary, .btn-secondary {
+    padding: 12px 24px;
+    border: none;
+    border-radius: 8px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    font-size: 0.9rem;
+}
+
+.btn-primary {
+    background: var(--button-gradient);
+    color: var(--button-text);
+}
+
+.btn-primary:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 5px 15px var(--button-hover-shadow);
+}
+
+.btn-primary:disabled {
+    background: var(--button-disabled-bg);
+    cursor: not-allowed;
+    transform: none;
+    box-shadow: none;
+}
+
+.btn-secondary {
+    background: var(--section-bg);
+    color: var(--text-primary);
+    border: 2px solid var(--border-color);
+}
+
+.btn-secondary:hover {
+    background: var(--tab-active-bg);
+    border-color: var(--tab-border);
+}
+
+/* Modal Message */
+.modal-message {
+    margin-top: 15px;
+    padding: 12px;
+    border-radius: 6px;
+    text-align: center;
+    font-weight: 500;
+    font-size: 0.9rem;
+}
+
+.modal-message.success {
+    background: var(--message-success-bg);
+    color: var(--message-success-text);
+    border: 1px solid var(--message-success-border);
+}
+
+.modal-message.error {
+    background: var(--message-error-bg);
+    color: var(--message-error-text);
+    border: 1px solid var(--message-error-border);
+}
+
+/* Mobile Modal Responsiveness */
+@media (max-width: 768px) {
+    .modal-content {
+        max-width: none;
+        margin: 10px;
+        max-height: 95vh;
+    }
+
+    .modal-header,
+    .form-section,
+    .modal-buttons {
+        padding-left: 20px;
+        padding-right: 20px;
+    }
+
+    .roles-grid {
+        grid-template-columns: 1fr 1fr;
+        gap: 8px;
+    }
+
+    .role-checkbox {
+        padding: 10px;
+        font-size: 0.8rem;
+    }
+
+    .modal-buttons {
+        flex-direction: column;
+    }
+}
+
+/* Dark theme adjustments */
+[data-theme="dark"] .modal {
+    background: rgba(0, 0, 0, 0.8);
+}
+
+[data-theme="dark"] .modal-content {
+    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
+}
+
+[data-theme="dark"] .table-container {
+    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
+}
+
+[data-theme="dark"] .stat-card {
+    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
+}

+ 79 - 2
src/main/resources/static/css/main.css

@@ -136,6 +136,13 @@ body {
     backdrop-filter: blur(10px);
 }
 
+/* When theme toggle is in header controls (admin page) */
+.header-controls .theme-toggle {
+    position: relative;
+    top: auto;
+    right: auto;
+}
+
 .theme-toggle:hover {
     background: rgba(255, 255, 255, 0.3);
     transform: scale(1.1);
@@ -278,7 +285,7 @@ body {
     transition: all 0.3s ease;
 }
 
-.user-info {
+#userSection .user-info {
     display: none;
     padding: 20px;
     background: var(--user-info-bg);
@@ -286,7 +293,7 @@ body {
     transition: all 0.3s ease;
 }
 
-.user-info.active {
+#userSection .user-info.active {
     display: block;
 }
 
@@ -421,4 +428,74 @@ body {
 .forgot-password-link a:hover {
     color: var(--input-focus-border);
     text-decoration: underline;
+}
+
+/* Action buttons section */
+.action-buttons {
+    display: flex;
+    gap: 10px;
+    justify-content: center;
+    margin-bottom: 15px;
+}
+
+.action-btn-circular {
+    background: var(--section-bg);
+    border: 2px solid var(--border-color);
+    border-radius: 50%;
+    width: 48px;
+    height: 48px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    color: var(--tab-active-text);
+}
+
+.action-btn-circular:hover {
+    background: var(--tab-border);
+    border-color: var(--tab-border);
+    color: white;
+    transform: scale(1.1);
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.action-icon {
+    transition: all 0.3s ease;
+}
+
+/* Admin button special styling */
+.admin-btn-circular {
+    background: linear-gradient(135deg, #ff6b6b, #ee5a52);
+    border-color: #ff6b6b;
+    color: white;
+}
+
+.admin-btn-circular:hover {
+    background: linear-gradient(135deg, #ee5a52, #dc3545);
+    border-color: #ee5a52;
+    color: white;
+    box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
+}
+
+/* Active state for edit profile button */
+.action-btn-circular.active {
+    background: var(--tab-border);
+    border-color: var(--tab-border);
+    color: white;
+    transform: scale(1.05);
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+/* Mobile responsiveness for action buttons */
+@media (max-width: 480px) {
+    .action-btn-circular {
+        width: 44px;
+        height: 44px;
+    }
+
+    .action-icon {
+        width: 16px;
+        height: 16px;
+    }
 }

+ 10 - 1
src/main/resources/static/index.html

@@ -94,7 +94,16 @@
             </div>
 
             <div class="action-buttons">
-                <button class="action-btn" id="showEditProfileBtn" onclick="toggleEditProfileForm()">Edit Profile</button>
+                <button class="action-btn-circular" id="showEditProfileBtn" onclick="toggleEditProfileForm()" title="Edit Profile">
+                    <svg class="action-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
+                        <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
+                    </svg>
+                </button>
+                <button class="action-btn-circular admin-btn-circular" id="adminPanelBtn" onclick="openAdminPanel()" style="display: none;" title="Admin Panel">
+                    <svg class="action-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
+                        <path d="M12 1L3 5V11C3 16.55 6.84 21.74 12 23C17.16 21.74 21 16.55 21 11V5L12 1ZM12 7C13.1 7 14 7.9 14 9S13.1 11 12 11 10 10.1 10 9 10.9 7 12 7ZM18 15C16 13 12 13 12 13S8 13 6 15V13.5C6 11 9 10 12 10S18 11 18 13.5V15Z"/>
+                    </svg>
+                </button>
             </div>
 
             <div class="change-password-section" id="editProfileSection" style="display: none;">

+ 433 - 0
src/main/resources/static/js/admin.js

@@ -0,0 +1,433 @@
+// Admin Panel JavaScript
+let currentUsers = [];
+let filteredUsers = [];
+
+// Check if user has admin role on page load
+document.addEventListener('DOMContentLoaded', function() {
+    initializeTheme();
+    checkAdminAccess();
+    loadUsers();
+});
+
+// Theme toggle functionality (reused from main.js)
+function toggleTheme() {
+    const currentTheme = document.documentElement.getAttribute('data-theme');
+    const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
+
+    document.documentElement.setAttribute('data-theme', newTheme);
+    localStorage.setItem('theme', newTheme);
+}
+
+function initializeTheme() {
+    const savedTheme = localStorage.getItem('theme') || 'light';
+    document.documentElement.setAttribute('data-theme', savedTheme);
+}
+
+// Check admin access
+function checkAdminAccess() {
+    const currentUser = getCurrentUser();
+
+    if (!currentUser || !hasAdminRole(currentUser.roles)) {
+        showMessage('Access denied. Admin privileges required.', 'error');
+        setTimeout(() => {
+            window.location.href = 'index.html';
+        }, 2000);
+        return false;
+    }
+    return true;
+}
+
+// Get current user from localStorage
+function getCurrentUser() {
+    try {
+        const userData = localStorage.getItem('userData');
+        if (!userData) return null;
+        return JSON.parse(userData);
+    } catch (e) {
+        console.error('Error parsing user data:', e);
+        return null;
+    }
+}
+
+// Check if user has admin role
+function hasAdminRole(roles) {
+    return Array.isArray(roles) && roles.includes('ADMIN');
+}
+
+// Load users from API
+async function loadUsers() {
+    showLoading(true);
+    hideEmptyState();
+
+    try {
+        const token = localStorage.getItem('authToken');
+        if (!token) {
+            throw new Error('No authentication token found');
+        }
+
+        const response = await fetch(API_BASE_URL + '/users', {
+            method: 'GET',
+            headers: {
+                'Authorization': `Bearer ${token}`,
+                'Content-Type': 'application/json'
+            }
+        });
+
+        if (!response.ok) {
+            if (response.status === 401 || response.status === 403) {
+                throw new Error('Access denied. Admin privileges required.');
+            }
+            throw new Error(`Failed to load users: ${response.status} ${response.statusText}`);
+        }
+
+        const users = await response.json();
+
+        // Transform API response to match our display format
+        const transformedUsers = users.map(user => ({
+            id: user.id,
+            username: user.username,
+            email: user.email,
+            roles: user.roles,
+            status: user.active ? 'active' : 'inactive',
+            lastLogin: null // Not provided by API
+        }));
+
+        currentUsers = transformedUsers;
+        filteredUsers = [...currentUsers];
+        displayUsers(filteredUsers);
+        updateStats(transformedUsers);
+        showMessage('Users loaded successfully', 'success');
+
+    } catch (error) {
+        console.error('Error loading users:', error);
+        if (error.message.includes('Access denied') || error.message.includes('Admin privileges')) {
+            showMessage('Access denied. Admin privileges required.', 'error');
+            setTimeout(() => {
+                window.location.href = 'index.html';
+            }, 2000);
+        } else {
+            showMessage('Failed to load users. Please try again.', 'error');
+        }
+        showEmptyState();
+    } finally {
+        showLoading(false);
+    }
+}
+
+// Display users in table
+function displayUsers(users) {
+    const tableBody = document.getElementById('usersTableBody');
+
+    if (users.length === 0) {
+        showEmptyState();
+        return;
+    }
+
+    tableBody.innerHTML = users.map(user => `
+        <tr class="user-row" data-user-id="${user.id}">
+            <td>
+                <div class="user-info">
+                    <strong class="username">${user.username}</strong>
+                </div>
+            </td>
+            <td>
+                <span class="email">${user.email || 'No email'}</span>
+            </td>
+            <td>
+                <div class="roles">
+                    ${user.roles.map(role => `<span class="role-badge ${role.toLowerCase()}">${role}</span>`).join('')}
+                </div>
+            </td>
+            <td>
+                <span class="status-badge ${user.status}">${user.status}</span>
+            </td>
+            <td>
+                <span class="last-login">${user.lastLogin ? formatDate(user.lastLogin) : 'Not available'}</span>
+            </td>
+            <td>
+                <div class="action-buttons">
+                    <button class="action-btn small" onclick="viewUser('${user.id}')" title="View Details">
+                        <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
+                            <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
+                        </svg>
+                    </button>
+                    <button class="action-btn small secondary" onclick="editUser('${user.id}')" title="Edit User">
+                        <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
+                            <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
+                        </svg>
+                    </button>
+                </div>
+            </td>
+        </tr>
+        `).join('');
+}
+
+// Update statistics
+function updateStats(users) {
+    const totalUsers = users.length;
+    const activeUsers = users.filter(u => u.status === 'active').length;
+    const adminUsers = users.filter(u => u.roles.includes('ADMIN')).length;
+
+    document.getElementById('totalUsers').textContent = totalUsers;
+    document.getElementById('activeUsers').textContent = activeUsers;
+    document.getElementById('adminUsers').textContent = adminUsers;
+}
+
+// Filter users based on search input
+function filterUsers() {
+    const searchTerm = document.getElementById('searchInput').value.toLowerCase();
+
+    filteredUsers = currentUsers.filter(user =>
+        user.username.toLowerCase().includes(searchTerm) ||
+        user.id.toLowerCase().includes(searchTerm) ||
+        (user.email && user.email.toLowerCase().includes(searchTerm)) ||
+        user.roles.some(role => role.toLowerCase().includes(searchTerm)) ||
+        user.status.toLowerCase().includes(searchTerm)
+    );
+
+    displayUsers(filteredUsers);
+}
+
+// User action functions
+function viewUser(userId) {
+    const user = currentUsers.find(u => u.id === userId);
+    if (user) {
+        const details = [
+            `ID: ${user.id}`,
+            `Username: ${user.username}`,
+            `Email: ${user.email || 'No email'}`,
+            `Roles: ${user.roles.join(', ')}`,
+            `Status: ${user.status}`,
+            `Last Login: ${user.lastLogin ? formatDate(user.lastLogin) : 'Not available'}`
+        ];
+        alert(`User Details:\n\n${details.join('\n')}`);
+    }
+}
+
+function editUser(userId) {
+    const user = currentUsers.find(u => u.id === userId);
+    if (user) {
+        openEditUserModal(user);
+    }
+}
+
+// Edit User Modal Functions
+let currentEditUserId = null;
+
+function openEditUserModal(user) {
+    currentEditUserId = user.id;
+    const modal = document.getElementById('editUserModal');
+
+    // Populate form with user data
+    document.getElementById('editUserEmail').value = user.email || '';
+    document.getElementById('editUserActive').checked = user.status === 'active';
+
+    // Clear all role checkboxes first
+    document.querySelectorAll('input[name="role"]').forEach(checkbox => {
+        checkbox.checked = false;
+    });
+
+    // Check the roles the user has
+    user.roles.forEach(role => {
+        const checkbox = document.querySelector(`input[name="role"][value="${role}"]`);
+        if (checkbox) {
+            checkbox.checked = true;
+        }
+    });
+
+    // Clear any previous messages
+    clearEditUserMessage();
+
+    // Show modal
+    modal.style.display = 'flex';
+
+    // Focus on email input
+    setTimeout(() => {
+        document.getElementById('editUserEmail').focus();
+    }, 100);
+}
+
+function closeEditUserModal() {
+    const modal = document.getElementById('editUserModal');
+    modal.style.display = 'none';
+    currentEditUserId = null;
+
+    // Reset form
+    document.getElementById('editUserForm').reset();
+    clearEditUserMessage();
+}
+
+function clearEditUserMessage() {
+    const messageEl = document.getElementById('editUserMessage');
+    messageEl.style.display = 'none';
+    messageEl.textContent = '';
+    messageEl.className = 'modal-message';
+}
+
+function showEditUserMessage(message, type) {
+    const messageEl = document.getElementById('editUserMessage');
+    messageEl.textContent = message;
+    messageEl.className = `modal-message ${type}`;
+    messageEl.style.display = 'block';
+}
+
+async function handleEditUserSubmit(event) {
+    event.preventDefault();
+
+    if (!currentEditUserId) {
+        showEditUserMessage('Error: No user selected for editing', 'error');
+        return;
+    }
+
+    const formData = new FormData(event.target);
+    const email = formData.get('email').trim();
+    const isActive = formData.has('isActive');
+
+    // Get selected roles
+    const selectedRoles = [];
+    document.querySelectorAll('input[name="role"]:checked').forEach(checkbox => {
+        selectedRoles.push(checkbox.value);
+    });
+
+    // Validation
+    if (selectedRoles.length === 0) {
+        showEditUserMessage('Please select at least one role', 'error');
+        return;
+    }
+
+    if (email && !isValidEmail(email)) {
+        showEditUserMessage('Please enter a valid email address', 'error');
+        return;
+    }
+
+    // Prepare request data
+    const requestData = {
+        roles: selectedRoles,
+        email: email || null,
+        isActive: isActive
+    };
+
+    // Update UI
+    const saveBtn = document.getElementById('saveUserBtn');
+    const originalText = saveBtn.textContent;
+    saveBtn.textContent = 'Saving...';
+    saveBtn.disabled = true;
+    clearEditUserMessage();
+
+    try {
+        const token = localStorage.getItem('authToken');
+        if (!token) {
+            throw new Error('No authentication token found');
+        }
+
+        const response = await fetch(`${API_BASE_URL}/users/${currentEditUserId}`, {
+            method: 'PUT',
+            headers: {
+                'Authorization': `Bearer ${token}`,
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(requestData)
+        });
+
+        if (!response.ok) {
+            if (response.status === 401 || response.status === 403) {
+                throw new Error('Access denied. Admin privileges required.');
+            }
+
+            let errorMessage;
+            try {
+                const errorData = await response.json();
+                errorMessage = errorData.message || `Update failed: ${response.status} ${response.statusText}`;
+            } catch (parseError) {
+                errorMessage = `Update failed: ${response.status} ${response.statusText}`;
+            }
+            throw new Error(errorMessage);
+        }
+
+        showEditUserMessage('User updated successfully!', 'success');
+
+        // Refresh the users list to show updated data
+        setTimeout(() => {
+            loadUsers();
+            closeEditUserModal();
+        }, 1500);
+
+    } catch (error) {
+        console.error('Error updating user:', error);
+        showEditUserMessage(error.message, 'error');
+    } finally {
+        saveBtn.textContent = originalText;
+        saveBtn.disabled = false;
+    }
+}
+
+function isValidEmail(email) {
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+    return emailRegex.test(email);
+}
+
+// Close modal when clicking outside of it
+document.addEventListener('click', function(event) {
+    const modal = document.getElementById('editUserModal');
+    if (event.target === modal) {
+        closeEditUserModal();
+    }
+});
+
+// Close modal with Escape key
+document.addEventListener('keydown', function(event) {
+    if (event.key === 'Escape') {
+        const modal = document.getElementById('editUserModal');
+        if (modal.style.display === 'flex') {
+            closeEditUserModal();
+        }
+    }
+});
+
+// Utility functions
+function formatUserId(id) {
+    // Format UUID for display (show first 8 characters)
+    return id.substring(0, 8) + '...';
+}
+
+function formatDate(dateString) {
+    if (!dateString) return 'Never';
+    const date = new Date(dateString);
+    return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
+}
+
+function showLoading(show) {
+    const indicator = document.getElementById('loadingIndicator');
+    indicator.style.display = show ? 'block' : 'none';
+}
+
+function showEmptyState() {
+    document.getElementById('emptyState').style.display = 'block';
+}
+
+function hideEmptyState() {
+    document.getElementById('emptyState').style.display = 'none';
+}
+
+function showMessage(message, type = 'info') {
+    const messageContainer = document.getElementById('messageContainer');
+    messageContainer.textContent = message;
+    messageContainer.className = `message ${type}`;
+    messageContainer.style.display = 'block';
+
+    // Auto-hide message after 3 seconds for success messages
+    if (type === 'success') {
+        setTimeout(() => {
+            messageContainer.style.display = 'none';
+        }, 3000);
+    }
+}
+
+// Export functions for potential use in other scripts
+window.adminAPI = {
+    loadUsers,
+    filterUsers,
+    viewUser,
+    editUser,
+    toggleTheme
+};

+ 120 - 24
src/main/resources/static/js/main.js

@@ -31,6 +31,13 @@ document.addEventListener('DOMContentLoaded', function() {
     // Initialize theme before anything else
     initializeTheme();
 
+    // Ensure user section is hidden by default
+    const userSection = document.getElementById('userSection');
+    if (userSection) {
+        userSection.classList.remove('active');
+        userSection.style.display = 'none';
+    }
+
     const urlInfo = parseURLParams();
 
     if (urlInfo.isResetMode) {
@@ -45,11 +52,29 @@ document.addEventListener('DOMContentLoaded', function() {
     if (token && userData) {
         try {
             currentUser = JSON.parse(userData);
-            showUserSection();
+            // Only show user section if we have valid user data
+            if (currentUser && currentUser.username) {
+                showUserSection();
+            } else {
+                throw new Error('Invalid user data structure');
+            }
         } catch (e) {
             console.error('Invalid user data in localStorage', e);
             localStorage.removeItem('authToken');
             localStorage.removeItem('userData');
+            currentUser = null;
+            // Make sure user section stays hidden
+            if (userSection) {
+                userSection.classList.remove('active');
+                userSection.style.display = 'none';
+            }
+        }
+    } else {
+        // No auth data, make sure user section is hidden
+        currentUser = null;
+        if (userSection) {
+            userSection.classList.remove('active');
+            userSection.style.display = 'none';
         }
     }
 
@@ -102,7 +127,10 @@ function setButtonLoading(buttonId, loading) {
     }
 }
 
-document.getElementById('loginForm').addEventListener('submit', async function(e) {
+// Only add event listeners if elements exist (for index.html)
+const loginForm = document.getElementById('loginForm');
+if (loginForm) {
+    loginForm.addEventListener('submit', async function(e) {
     e.preventDefault();
 
     const username = document.getElementById('loginUsername').value;
@@ -147,8 +175,11 @@ document.getElementById('loginForm').addEventListener('submit', async function(e
         setButtonLoading('loginBtn', false);
     }
 });
+}
 
-document.getElementById('registerForm').addEventListener('submit', async function(e) {
+const registerForm = document.getElementById('registerForm');
+if (registerForm) {
+    registerForm.addEventListener('submit', async function(e) {
     e.preventDefault();
 
     const username = document.getElementById('registerUsername').value;
@@ -203,8 +234,11 @@ document.getElementById('registerForm').addEventListener('submit', async functio
         setButtonLoading('registerBtn', false);
     }
 });
+}
 
-document.getElementById('resetPasswordForm').addEventListener('submit', async function(e) {
+const resetPasswordForm = document.getElementById('resetPasswordForm');
+if (resetPasswordForm) {
+    resetPasswordForm.addEventListener('submit', async function(e) {
     e.preventDefault();
 
     const newPassword = document.getElementById('resetNewPassword').value;
@@ -260,8 +294,11 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
         setButtonLoading('resetPasswordBtn', false);
     }
 });
+}
 
-document.getElementById('editProfileForm').addEventListener('submit', async function(e) {
+const editProfileForm = document.getElementById('editProfileForm');
+if (editProfileForm) {
+    editProfileForm.addEventListener('submit', async function(e) {
     e.preventDefault();
 
     const currentPassword = document.getElementById('currentPassword').value;
@@ -354,6 +391,7 @@ document.getElementById('editProfileForm').addEventListener('submit', async func
         setButtonLoading('editProfileBtn', false);
     }
 });
+}
 
 function clearEditProfileMessages() {
     const messageElement = document.getElementById('editProfileMessage');
@@ -368,16 +406,36 @@ function clearResetPasswordMessages() {
 }
 
 function updateUserDisplay() {
-    document.getElementById('currentUsername').textContent = currentUser.username;
-    document.getElementById('currentEmail').textContent = maskUserEmail(currentUser.email) || '-';
-    document.getElementById('currentUserId').textContent = maskUserId(currentUser.id);
-    document.getElementById('currentRoles').textContent = currentUser.roles.join(', ');
+    if (!currentUser) {
+        console.log('updateUserDisplay called but no currentUser');
+        return;
+    }
+
+    const usernameEl = document.getElementById('currentUsername');
+    const emailEl = document.getElementById('currentEmail');
+    const userIdEl = document.getElementById('currentUserId');
+    const rolesEl = document.getElementById('currentRoles');
+
+    if (usernameEl) usernameEl.textContent = currentUser.username || '';
+    if (emailEl) emailEl.textContent = maskUserEmail(currentUser.email) || '-';
+    if (userIdEl) userIdEl.textContent = maskUserId(currentUser.id) || '';
+    if (rolesEl) rolesEl.textContent = (currentUser.roles || []).join(', ');
+
+    // Show/hide admin panel button based on user role
+    updateAdminPanelVisibility();
 }
 
 function showUserSection() {
-    document.getElementById('authSection').style.display = 'none';
-    document.getElementById('userSection').classList.add('active');
-    document.querySelector('.header').style.display = 'block';
+    const authSection = document.getElementById('authSection');
+    const userSection = document.getElementById('userSection');
+    const header = document.querySelector('.header');
+
+    if (authSection) authSection.style.display = 'none';
+    if (userSection) {
+        userSection.classList.add('active');
+        userSection.style.display = 'block';
+    }
+    if (header) header.style.display = 'block';
 
     updateUserDisplay();
 }
@@ -418,11 +476,20 @@ function logout() {
     localStorage.removeItem('userData');
     currentUser = null;
 
-    document.getElementById('userSection').classList.remove('active');
-    document.getElementById('authSection').style.display = 'block';
-    document.querySelector('.header').style.display = 'block';
-    document.getElementById('loginForm').reset();
-    document.getElementById('registerForm').reset();
+    const userSection = document.getElementById('userSection');
+    const authSection = document.getElementById('authSection');
+    const header = document.querySelector('.header');
+    const loginForm = document.getElementById('loginForm');
+    const registerForm = document.getElementById('registerForm');
+
+    if (userSection) {
+        userSection.classList.remove('active');
+        userSection.style.display = 'none';
+    }
+    if (authSection) authSection.style.display = 'block';
+    if (header) header.style.display = 'block';
+    if (loginForm) loginForm.reset();
+    if (registerForm) registerForm.reset();
 
     hideEditProfileForm();
     clearMessages();
@@ -444,33 +511,62 @@ function showEditProfileForm() {
     const formSection = document.getElementById('editProfileSection');
     const actionBtn = document.getElementById('showEditProfileBtn');
 
-    formSection.style.display = 'block';
-    actionBtn.textContent = 'Hide Edit Profile';
+    if (formSection) formSection.style.display = 'block';
+
+    // Add active styling to button
+    if (actionBtn) actionBtn.classList.add('active');
 
     // Clear any previous messages
     clearEditProfileMessages();
 
     // Pre-populate email field with current user's email
     if (currentUser) {
-        document.getElementById('editEmail').value = currentUser.email || '';
+        const editEmail = document.getElementById('editEmail');
+        if (editEmail) editEmail.value = currentUser.email || '';
     }
 
     // Focus on email input first since that's the most common change
-    setTimeout(() => document.getElementById('editEmail').focus(), 100);
+    setTimeout(() => {
+        const editEmail = document.getElementById('editEmail');
+        if (editEmail) editEmail.focus();
+    }, 100);
 }
 
 function hideEditProfileForm() {
     const formSection = document.getElementById('editProfileSection');
     const actionBtn = document.getElementById('showEditProfileBtn');
 
-    formSection.style.display = 'none';
-    actionBtn.textContent = 'Edit Profile';
+    if (formSection) formSection.style.display = 'none';
+
+    // Remove active styling from button
+    if (actionBtn) actionBtn.classList.remove('active');
 
     // Clear form and messages
-    document.getElementById('editProfileForm').reset();
+    const editForm = document.getElementById('editProfileForm');
+    if (editForm) editForm.reset();
     clearEditProfileMessages();
 }
 
+// Admin panel functions
+function hasAdminRole() {
+    return currentUser && Array.isArray(currentUser.roles) && currentUser.roles.includes('ADMIN');
+}
+
+function updateAdminPanelVisibility() {
+    const adminBtn = document.getElementById('adminPanelBtn');
+    if (adminBtn) {
+        adminBtn.style.display = hasAdminRole() ? 'block' : 'none';
+    }
+}
+
+function openAdminPanel() {
+    if (!hasAdminRole()) {
+        alert('Access denied. Admin privileges required.');
+        return;
+    }
+    window.location.href = 'admin.html';
+}
+
 async function handleForgotPassword(e) {
     e.preventDefault();