We have a role concept
This commit is contained in:
@@ -0,0 +1,194 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bamort/database"
|
||||||
|
"bamort/logger"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListUsers returns all users (admin only)
|
||||||
|
func ListUsers(c *gin.Context) {
|
||||||
|
logger.Debug("Listing all users...")
|
||||||
|
|
||||||
|
var users []User
|
||||||
|
if err := database.DB.Find(&users).Error; err != nil {
|
||||||
|
logger.Error("Failed to fetch users: %s", err.Error())
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to fetch users")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove password hashes from response
|
||||||
|
for i := range users {
|
||||||
|
users[i].PasswordHash = ""
|
||||||
|
users[i].ResetPwHash = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Successfully fetched %d users", len(users))
|
||||||
|
c.JSON(http.StatusOK, users)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns a specific user by ID (admin only, or own profile)
|
||||||
|
func GetUser(c *gin.Context) {
|
||||||
|
logger.Debug("Fetching user by ID...")
|
||||||
|
|
||||||
|
userIDParam := c.Param("id")
|
||||||
|
targetUserID, err := strconv.ParseUint(userIDParam, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Invalid user ID: %s", userIDParam)
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Invalid user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get requesting user from context
|
||||||
|
requestingUserInterface, exists := c.Get("user")
|
||||||
|
if !exists {
|
||||||
|
logger.Error("User not found in context")
|
||||||
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestingUser, ok := requestingUserInterface.(*User)
|
||||||
|
if !ok {
|
||||||
|
logger.Error("Invalid user context")
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Invalid user context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow users to view their own profile, or admins to view any profile
|
||||||
|
if requestingUser.UserID != uint(targetUserID) && !requestingUser.IsAdmin() {
|
||||||
|
logger.Warn("User %s attempted to access user %d without permission", requestingUser.Username, targetUserID)
|
||||||
|
respondWithError(c, http.StatusForbidden, "Forbidden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err := user.FirstId(uint(targetUserID)); err != nil {
|
||||||
|
logger.Error("User not found: %d", targetUserID)
|
||||||
|
respondWithError(c, http.StatusNotFound, "User not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sensitive data
|
||||||
|
user.PasswordHash = ""
|
||||||
|
user.ResetPwHash = nil
|
||||||
|
|
||||||
|
logger.Info("Successfully fetched user: %s (ID: %d)", user.Username, user.UserID)
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserRole updates a user's role (admin only)
|
||||||
|
func UpdateUserRole(c *gin.Context) {
|
||||||
|
logger.Debug("Updating user role...")
|
||||||
|
|
||||||
|
userIDParam := c.Param("id")
|
||||||
|
targetUserID, err := strconv.ParseUint(userIDParam, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Invalid user ID: %s", userIDParam)
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Invalid user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input struct {
|
||||||
|
Role string `json:"role" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
logger.Error("Failed to parse role update data: %s", err.Error())
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Role is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate role
|
||||||
|
if !ValidateRole(input.Role) {
|
||||||
|
logger.Error("Invalid role: %s", input.Role)
|
||||||
|
respondWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid role. Must be one of: %s, %s, %s", RoleStandardUser, RoleMaintainer, RoleAdmin))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err := user.FirstId(uint(targetUserID)); err != nil {
|
||||||
|
logger.Error("User not found: %d", targetUserID)
|
||||||
|
respondWithError(c, http.StatusNotFound, "User not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get requesting user for logging
|
||||||
|
requestingUserInterface, _ := c.Get("user")
|
||||||
|
requestingUser, _ := requestingUserInterface.(*User)
|
||||||
|
|
||||||
|
oldRole := user.Role
|
||||||
|
user.Role = input.Role
|
||||||
|
|
||||||
|
if err := user.Save(); err != nil {
|
||||||
|
logger.Error("Failed to update user role for user %s: %s", user.Username, err.Error())
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to update user role")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("User role updated: %s (ID: %d) from %s to %s by %s", user.Username, user.UserID, oldRole, user.Role, requestingUser.Username)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "User role updated successfully",
|
||||||
|
"user": gin.H{
|
||||||
|
"id": user.UserID,
|
||||||
|
"username": user.Username,
|
||||||
|
"role": user.Role,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes a user (admin only)
|
||||||
|
func DeleteUser(c *gin.Context) {
|
||||||
|
logger.Debug("Deleting user...")
|
||||||
|
|
||||||
|
userIDParam := c.Param("id")
|
||||||
|
targetUserID, err := strconv.ParseUint(userIDParam, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Invalid user ID: %s", userIDParam)
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Invalid user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get requesting user
|
||||||
|
requestingUserInterface, exists := c.Get("user")
|
||||||
|
if !exists {
|
||||||
|
logger.Error("User not found in context")
|
||||||
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestingUser, ok := requestingUserInterface.(*User)
|
||||||
|
if !ok {
|
||||||
|
logger.Error("Invalid user context")
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Invalid user context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent self-deletion
|
||||||
|
if requestingUser.UserID == uint(targetUserID) {
|
||||||
|
logger.Warn("User %s attempted to delete themselves", requestingUser.Username)
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Cannot delete your own account")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err := user.FirstId(uint(targetUserID)); err != nil {
|
||||||
|
logger.Error("User not found: %d", targetUserID)
|
||||||
|
respondWithError(c, http.StatusNotFound, "User not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Delete(&user).Error; err != nil {
|
||||||
|
logger.Error("Failed to delete user %s: %s", user.Username, err.Error())
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to delete user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("User deleted: %s (ID: %d) by %s", user.Username, user.UserID, requestingUser.Username)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "User deleted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -60,6 +60,11 @@ func RegisterUser(c *gin.Context) {
|
|||||||
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
|
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
|
||||||
logger.Debug("Passwort-Hash erstellt für Benutzer: %s", user.Username)
|
logger.Debug("Passwort-Hash erstellt für Benutzer: %s", user.Username)
|
||||||
|
|
||||||
|
// Set default role for new users
|
||||||
|
if user.Role == "" {
|
||||||
|
user.Role = RoleStandardUser
|
||||||
|
}
|
||||||
|
|
||||||
//fmt.Printf("pwdh: %s", user.PasswordHash)
|
//fmt.Printf("pwdh: %s", user.PasswordHash)
|
||||||
if err := user.Create(); err != nil {
|
if err := user.Create(); err != nil {
|
||||||
logger.Error("Fehler beim Erstellen des Benutzers %s: %s", user.Username, err.Error())
|
logger.Error("Fehler beim Erstellen des Benutzers %s: %s", user.Username, err.Error())
|
||||||
@@ -217,6 +222,7 @@ func AuthMiddleware() gin.HandlerFunc {
|
|||||||
// Set user information in context
|
// Set user information in context
|
||||||
c.Set("userID", user.UserID)
|
c.Set("userID", user.UserID)
|
||||||
c.Set("username", user.Username)
|
c.Set("username", user.Username)
|
||||||
|
c.Set("user", user)
|
||||||
|
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
@@ -438,6 +444,7 @@ func GetUserProfile(c *gin.Context) {
|
|||||||
"id": user.UserID,
|
"id": user.UserID,
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
|
"role": user.Role,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequireRole is a middleware that checks if the user has the required role
|
||||||
|
func RequireRole(requiredRole string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get user from context (set by auth middleware)
|
||||||
|
userInterface, exists := c.Get("user")
|
||||||
|
if !exists {
|
||||||
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, ok := userInterface.(*User)
|
||||||
|
if !ok {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Invalid user context")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has required role
|
||||||
|
switch requiredRole {
|
||||||
|
case RoleAdmin:
|
||||||
|
if !user.IsAdmin() {
|
||||||
|
respondWithError(c, http.StatusForbidden, "Admin role required")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case RoleMaintainer:
|
||||||
|
if !user.IsMaintainer() {
|
||||||
|
respondWithError(c, http.StatusForbidden, "Maintainer role required")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case RoleStandardUser:
|
||||||
|
if !user.IsStandardUser() {
|
||||||
|
respondWithError(c, http.StatusForbidden, "Insufficient permissions")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Invalid role requirement")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireAdmin is a convenience middleware for admin-only endpoints
|
||||||
|
func RequireAdmin() gin.HandlerFunc {
|
||||||
|
return RequireRole(RoleAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireMaintainer is a convenience middleware for maintainer-or-higher endpoints
|
||||||
|
func RequireMaintainer() gin.HandlerFunc {
|
||||||
|
return RequireRole(RoleMaintainer)
|
||||||
|
}
|
||||||
@@ -8,11 +8,19 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Role constants
|
||||||
|
const (
|
||||||
|
RoleStandardUser = "standard"
|
||||||
|
RoleMaintainer = "maintainer"
|
||||||
|
RoleAdmin = "admin"
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
UserID uint `gorm:"primaryKey" json:"id"`
|
UserID uint `gorm:"primaryKey" json:"id"`
|
||||||
Username string `gorm:"unique" json:"username"`
|
Username string `gorm:"unique" json:"username"`
|
||||||
PasswordHash string `json:"password"`
|
PasswordHash string `json:"password"`
|
||||||
Email string `gorm:"unique" json:"email"`
|
Email string `gorm:"unique" json:"email"`
|
||||||
|
Role string `gorm:"default:standard" json:"role"`
|
||||||
ResetPwHash *string `gorm:"index" json:"-"` // Hash für Password-Reset (wird nicht serialisiert)
|
ResetPwHash *string `gorm:"index" json:"-"` // Hash für Password-Reset (wird nicht serialisiert)
|
||||||
ResetPwHashExpires *time.Time `json:"-"` // Ablaufzeit für Password-Reset-Hash
|
ResetPwHashExpires *time.Time `json:"-"` // Ablaufzeit für Password-Reset-Hash
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
@@ -120,3 +128,28 @@ func (object *User) IsResetHashValid(resetHash string) bool {
|
|||||||
}
|
}
|
||||||
return *object.ResetPwHash == resetHash && time.Now().Before(*object.ResetPwHashExpires)
|
return *object.ResetPwHash == resetHash && time.Now().Before(*object.ResetPwHashExpires)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasRole checks if the user has the specified role
|
||||||
|
func (u *User) HasRole(role string) bool {
|
||||||
|
return u.Role == role
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdmin checks if the user is an admin
|
||||||
|
func (u *User) IsAdmin() bool {
|
||||||
|
return u.Role == RoleAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMaintainer checks if the user is a maintainer or higher
|
||||||
|
func (u *User) IsMaintainer() bool {
|
||||||
|
return u.Role == RoleMaintainer || u.Role == RoleAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsStandardUser checks if the user is a standard user or higher
|
||||||
|
func (u *User) IsStandardUser() bool {
|
||||||
|
return u.Role == RoleStandardUser || u.IsMaintainer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRole checks if the given role is valid
|
||||||
|
func ValidateRole(role string) bool {
|
||||||
|
return role == RoleStandardUser || role == RoleMaintainer || role == RoleAdmin
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"bamort/database"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupRoleTestEnvironment sets up the test environment for role tests
|
||||||
|
func setupRoleTestEnvironment(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
database.SetupTestDB()
|
||||||
|
err := MigrateStructure()
|
||||||
|
require.NoError(t, err, "Should migrate user structure")
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUserRoleDefaults tests that new users get standard role by default
|
||||||
|
func TestUserRoleDefaults(t *testing.T) {
|
||||||
|
setupRoleTestEnvironment(t)
|
||||||
|
|
||||||
|
user := &User{
|
||||||
|
Username: "roletest_user",
|
||||||
|
PasswordHash: "hashedpw",
|
||||||
|
Email: "roletest@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := user.Create()
|
||||||
|
require.NoError(t, err, "Should create user")
|
||||||
|
assert.Equal(t, RoleStandardUser, user.Role, "New users should have standard role")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRoleValidation tests role validation
|
||||||
|
func TestRoleValidation(t *testing.T) {
|
||||||
|
assert.True(t, ValidateRole(RoleStandardUser), "standard should be valid")
|
||||||
|
assert.True(t, ValidateRole(RoleMaintainer), "maintainer should be valid")
|
||||||
|
assert.True(t, ValidateRole(RoleAdmin), "admin should be valid")
|
||||||
|
assert.False(t, ValidateRole("invalid"), "invalid should not be valid")
|
||||||
|
assert.False(t, ValidateRole(""), "empty should not be valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRoleHierarchy tests role hierarchy methods
|
||||||
|
func TestRoleHierarchy(t *testing.T) {
|
||||||
|
standardUser := &User{Role: RoleStandardUser}
|
||||||
|
maintainer := &User{Role: RoleMaintainer}
|
||||||
|
admin := &User{Role: RoleAdmin}
|
||||||
|
|
||||||
|
// Test IsStandardUser
|
||||||
|
assert.True(t, standardUser.IsStandardUser(), "standard user should pass IsStandardUser")
|
||||||
|
assert.True(t, maintainer.IsStandardUser(), "maintainer should pass IsStandardUser")
|
||||||
|
assert.True(t, admin.IsStandardUser(), "admin should pass IsStandardUser")
|
||||||
|
|
||||||
|
// Test IsMaintainer
|
||||||
|
assert.False(t, standardUser.IsMaintainer(), "standard user should not pass IsMaintainer")
|
||||||
|
assert.True(t, maintainer.IsMaintainer(), "maintainer should pass IsMaintainer")
|
||||||
|
assert.True(t, admin.IsMaintainer(), "admin should pass IsMaintainer")
|
||||||
|
|
||||||
|
// Test IsAdmin
|
||||||
|
assert.False(t, standardUser.IsAdmin(), "standard user should not pass IsAdmin")
|
||||||
|
assert.False(t, maintainer.IsAdmin(), "maintainer should not pass IsAdmin")
|
||||||
|
assert.True(t, admin.IsAdmin(), "admin should pass IsAdmin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListUsers tests listing all users (admin only)
|
||||||
|
func TestListUsers(t *testing.T) {
|
||||||
|
setupRoleTestEnvironment(t)
|
||||||
|
|
||||||
|
// Create test users
|
||||||
|
admin := &User{Username: "listadmin", PasswordHash: "hash", Email: "listadmin@test.com", Role: RoleAdmin}
|
||||||
|
require.NoError(t, admin.Create())
|
||||||
|
|
||||||
|
standardUser := &User{Username: "listuser", PasswordHash: "hash", Email: "listuser@test.com", Role: RoleStandardUser}
|
||||||
|
require.NoError(t, standardUser.Create())
|
||||||
|
|
||||||
|
// Test admin access
|
||||||
|
router := gin.Default()
|
||||||
|
router.GET("/users", func(c *gin.Context) {
|
||||||
|
c.Set("user", admin)
|
||||||
|
}, RequireAdmin(), ListUsers)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "/users", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code, "Admin should access list")
|
||||||
|
|
||||||
|
// Test standard user access (should fail)
|
||||||
|
router2 := gin.Default()
|
||||||
|
router2.GET("/users", func(c *gin.Context) {
|
||||||
|
c.Set("user", standardUser)
|
||||||
|
}, RequireAdmin(), ListUsers)
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("GET", "/users", nil)
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
router2.ServeHTTP(w2, req2)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w2.Code, "Standard user should not access list")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpdateUserRole tests updating user role (admin only)
|
||||||
|
func TestUpdateUserRole(t *testing.T) {
|
||||||
|
setupRoleTestEnvironment(t)
|
||||||
|
|
||||||
|
// Create admin and target user
|
||||||
|
admin := &User{Username: "roleadmin", PasswordHash: "hash", Email: "roleadmin@test.com", Role: RoleAdmin}
|
||||||
|
require.NoError(t, admin.Create())
|
||||||
|
|
||||||
|
targetUser := &User{Username: "roletarget", PasswordHash: "hash", Email: "roletarget@test.com", Role: RoleStandardUser}
|
||||||
|
require.NoError(t, targetUser.Create())
|
||||||
|
|
||||||
|
// Test valid role update
|
||||||
|
router := gin.Default()
|
||||||
|
router.PUT("/users/:id/role", func(c *gin.Context) {
|
||||||
|
c.Set("user", admin)
|
||||||
|
}, RequireAdmin(), UpdateUserRole)
|
||||||
|
|
||||||
|
updateData := map[string]string{"role": RoleMaintainer}
|
||||||
|
jsonData, _ := json.Marshal(updateData)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/users/%d/role", targetUser.UserID), bytes.NewBuffer(jsonData))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code, "Admin should update role")
|
||||||
|
|
||||||
|
// Verify role was updated
|
||||||
|
var updatedUser User
|
||||||
|
require.NoError(t, updatedUser.FirstId(targetUser.UserID))
|
||||||
|
assert.Equal(t, RoleMaintainer, updatedUser.Role, "Role should be updated to maintainer")
|
||||||
|
|
||||||
|
// Test invalid role update
|
||||||
|
invalidData := map[string]string{"role": "invalid"}
|
||||||
|
jsonData2, _ := json.Marshal(invalidData)
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("PUT", fmt.Sprintf("/users/%d/role", targetUser.UserID), bytes.NewBuffer(jsonData2))
|
||||||
|
req2.Header.Set("Content-Type", "application/json")
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w2, req2)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w2.Code, "Should reject invalid role")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeleteUser tests deleting a user (admin only)
|
||||||
|
func TestDeleteUser(t *testing.T) {
|
||||||
|
setupRoleTestEnvironment(t)
|
||||||
|
|
||||||
|
// Create admin and target user
|
||||||
|
admin := &User{Username: "deladmin", PasswordHash: "hash", Email: "deladmin@test.com", Role: RoleAdmin}
|
||||||
|
require.NoError(t, admin.Create())
|
||||||
|
|
||||||
|
targetUser := &User{Username: "deltarget", PasswordHash: "hash", Email: "deltarget@test.com", Role: RoleStandardUser}
|
||||||
|
require.NoError(t, targetUser.Create())
|
||||||
|
|
||||||
|
// Test deletion
|
||||||
|
router := gin.Default()
|
||||||
|
router.DELETE("/users/:id", func(c *gin.Context) {
|
||||||
|
c.Set("user", admin)
|
||||||
|
}, RequireAdmin(), DeleteUser)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/users/%d", targetUser.UserID), nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code, "Admin should delete user")
|
||||||
|
|
||||||
|
// Verify user was deleted
|
||||||
|
var deletedUser User
|
||||||
|
err := deletedUser.FirstId(targetUser.UserID)
|
||||||
|
assert.Error(t, err, "User should not exist after deletion")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMaintainerPermissions tests maintainer-specific permissions
|
||||||
|
func TestMaintainerPermissions(t *testing.T) {
|
||||||
|
setupRoleTestEnvironment(t)
|
||||||
|
|
||||||
|
standardUser := &User{Username: "permuser", PasswordHash: "hash", Email: "permuser@test.com", Role: RoleStandardUser}
|
||||||
|
require.NoError(t, standardUser.Create())
|
||||||
|
|
||||||
|
maintainer := &User{Username: "permmaintainer", PasswordHash: "hash", Email: "permmaintainer@test.com", Role: RoleMaintainer}
|
||||||
|
require.NoError(t, maintainer.Create())
|
||||||
|
|
||||||
|
admin := &User{Username: "permadmin", PasswordHash: "hash", Email: "permadmin@test.com", Role: RoleAdmin}
|
||||||
|
require.NoError(t, admin.Create())
|
||||||
|
|
||||||
|
router := gin.Default()
|
||||||
|
router.GET("/maintainer-only", func(c *gin.Context) {
|
||||||
|
c.Set("user", standardUser)
|
||||||
|
}, RequireMaintainer(), func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Standard user should fail
|
||||||
|
req, _ := http.NewRequest("GET", "/maintainer-only", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code, "Standard user should not access")
|
||||||
|
|
||||||
|
// Maintainer should succeed
|
||||||
|
router2 := gin.Default()
|
||||||
|
router2.GET("/maintainer-only", func(c *gin.Context) {
|
||||||
|
c.Set("user", maintainer)
|
||||||
|
}, RequireMaintainer(), func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("GET", "/maintainer-only", nil)
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
router2.ServeHTTP(w2, req2)
|
||||||
|
assert.Equal(t, http.StatusOK, w2.Code, "Maintainer should access")
|
||||||
|
|
||||||
|
// Admin should also succeed
|
||||||
|
router3 := gin.Default()
|
||||||
|
router3.GET("/maintainer-only", func(c *gin.Context) {
|
||||||
|
c.Set("user", admin)
|
||||||
|
}, RequireMaintainer(), func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
req3, _ := http.NewRequest("GET", "/maintainer-only", nil)
|
||||||
|
w3 := httptest.NewRecorder()
|
||||||
|
router3.ServeHTTP(w3, req3)
|
||||||
|
assert.Equal(t, http.StatusOK, w3.Code, "Admin should access maintainer endpoints")
|
||||||
|
}
|
||||||
@@ -13,4 +13,14 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
userGroup.PUT("/email", UpdateEmail)
|
userGroup.PUT("/email", UpdateEmail)
|
||||||
userGroup.PUT("/password", UpdatePassword)
|
userGroup.PUT("/password", UpdatePassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admin routes - require admin role
|
||||||
|
adminGroup := r.Group("/users")
|
||||||
|
adminGroup.Use(RequireAdmin())
|
||||||
|
{
|
||||||
|
adminGroup.GET("", ListUsers)
|
||||||
|
adminGroup.GET("/:id", GetUser)
|
||||||
|
adminGroup.PUT("/:id/role", UpdateUserRole)
|
||||||
|
adminGroup.DELETE("/:id", DeleteUser)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import API from '../utils/api'
|
import API from '../utils/api'
|
||||||
|
import { useUserStore } from '../stores/userStore'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -73,6 +74,11 @@ export default {
|
|||||||
password: this.password,
|
password: this.password,
|
||||||
})
|
})
|
||||||
localStorage.setItem('token', response.data.token)
|
localStorage.setItem('token', response.data.token)
|
||||||
|
|
||||||
|
// Fetch user profile to get role information
|
||||||
|
const userStore = useUserStore()
|
||||||
|
await userStore.fetchCurrentUser()
|
||||||
|
|
||||||
// Emit auth change event
|
// Emit auth change event
|
||||||
window.dispatchEvent(new Event('auth-changed'))
|
window.dispatchEvent(new Event('auth-changed'))
|
||||||
this.$router.push('/dashboard')
|
this.$router.push('/dashboard')
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
<li v-if="!isLoggedIn">
|
<li v-if="!isLoggedIn">
|
||||||
<router-link to="/register" active-class="active">{{ $t('menu.Register') }}</router-link>
|
<router-link to="/register" active-class="active">{{ $t('menu.Register') }}</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="isLoggedIn">
|
<li v-if="isLoggedIn && isMaintainer">
|
||||||
<router-link to="/maintenance" active-class="active">{{ $t('menu.Maintenance') }}</router-link>
|
<router-link to="/maintenance" active-class="active">{{ $t('menu.Maintenance') }}</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="isLoggedIn && isAdmin">
|
||||||
|
<router-link to="/users" active-class="active">{{ $t('menu.UserManagement') }}</router-link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="menu-right">
|
<div class="menu-right">
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
@@ -27,6 +30,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { isLoggedIn, logout } from "../utils/auth";
|
import { isLoggedIn, logout } from "../utils/auth";
|
||||||
import LanguageSwitcher from "./LanguageSwitcher.vue";
|
import LanguageSwitcher from "./LanguageSwitcher.vue";
|
||||||
|
import { useUserStore } from "../stores/userStore";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -34,18 +38,48 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
LanguageSwitcher,
|
LanguageSwitcher,
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
userStore: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.userStore = useUserStore()
|
||||||
|
if (isLoggedIn() && !this.userStore.currentUser) {
|
||||||
|
await this.userStore.fetchCurrentUser()
|
||||||
|
}
|
||||||
|
// Listen for auth changes to refresh user data
|
||||||
|
window.addEventListener('auth-changed', this.handleAuthChange)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('auth-changed', this.handleAuthChange)
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
return isLoggedIn();
|
return isLoggedIn();
|
||||||
},
|
},
|
||||||
|
isAdmin() {
|
||||||
|
return this.userStore?.isAdmin || false
|
||||||
|
},
|
||||||
|
isMaintainer() {
|
||||||
|
return this.userStore?.isMaintainer || false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
logout() {
|
logout() {
|
||||||
logout();
|
logout();
|
||||||
|
this.userStore.clearUser()
|
||||||
// Emit auth change event
|
// Emit auth change event
|
||||||
window.dispatchEvent(new Event('auth-changed'));
|
window.dispatchEvent(new Event('auth-changed'));
|
||||||
this.$router.push("/");
|
this.$router.push("/");
|
||||||
},
|
},
|
||||||
|
async handleAuthChange() {
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
await this.userStore.fetchCurrentUser()
|
||||||
|
} else {
|
||||||
|
this.userStore.clearUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export default {
|
|||||||
Logout:'Abmelden',
|
Logout:'Abmelden',
|
||||||
Register:'Registrieren',
|
Register:'Registrieren',
|
||||||
Maintenance:'Wartung',
|
Maintenance:'Wartung',
|
||||||
|
UserManagement:'Benutzerverwaltung',
|
||||||
},
|
},
|
||||||
Equipment:'Ausrüstung',
|
Equipment:'Ausrüstung',
|
||||||
equipment:{
|
equipment:{
|
||||||
@@ -444,6 +445,34 @@ export default {
|
|||||||
pleaseWait: 'Bitte warten, dies kann einen Moment dauern',
|
pleaseWait: 'Bitte warten, dies kann einen Moment dauern',
|
||||||
popupBlocked: 'Popup wurde blockiert. Bitte erlauben Sie Popups für diese Seite.'
|
popupBlocked: 'Popup wurde blockiert. Bitte erlauben Sie Popups für diese Seite.'
|
||||||
},
|
},
|
||||||
|
userManagement: {
|
||||||
|
title: 'Benutzerverwaltung',
|
||||||
|
loading: 'Lade Benutzer...',
|
||||||
|
loadError: 'Fehler beim Laden der Benutzer',
|
||||||
|
id: 'ID',
|
||||||
|
username: 'Benutzername',
|
||||||
|
email: 'E-Mail',
|
||||||
|
role: 'Rolle',
|
||||||
|
createdAt: 'Erstellt am',
|
||||||
|
actions: 'Aktionen',
|
||||||
|
changeRole: 'Rolle ändern',
|
||||||
|
delete: 'Löschen',
|
||||||
|
changeRoleTitle: 'Benutzerrolle ändern',
|
||||||
|
changeRoleFor: 'Rolle ändern für',
|
||||||
|
selectRole: 'Rolle auswählen',
|
||||||
|
save: 'Speichern',
|
||||||
|
cancel: 'Abbrechen',
|
||||||
|
deleteUserTitle: 'Benutzer löschen',
|
||||||
|
deleteConfirm: 'Möchten Sie wirklich den Benutzer löschen',
|
||||||
|
deleteWarning: 'Diese Aktion kann nicht rückgängig gemacht werden!',
|
||||||
|
updateError: 'Fehler beim Aktualisieren der Benutzerrolle',
|
||||||
|
deleteError: 'Fehler beim Löschen des Benutzers',
|
||||||
|
roles: {
|
||||||
|
standard: 'Standard-Benutzer',
|
||||||
|
maintainer: 'Maintainer',
|
||||||
|
admin: 'Administrator'
|
||||||
|
}
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
title: 'Benutzerprofil',
|
title: 'Benutzerprofil',
|
||||||
loading: 'Lade Profil...',
|
loading: 'Lade Profil...',
|
||||||
|
|||||||
+29
-2
@@ -42,8 +42,7 @@ export default {
|
|||||||
ImportData:'Import Data',
|
ImportData:'Import Data',
|
||||||
Logout:'Logout',
|
Logout:'Logout',
|
||||||
Register:'Register',
|
Register:'Register',
|
||||||
Maintenance:'Maintenance',
|
Maintenance:'Maintenance', UserManagement:'User Management', },
|
||||||
},
|
|
||||||
Equipment:'Equipment',
|
Equipment:'Equipment',
|
||||||
equipment:{
|
equipment:{
|
||||||
id:'ID',
|
id:'ID',
|
||||||
@@ -442,6 +441,34 @@ export default {
|
|||||||
pleaseWait: 'Please wait, this may take a moment',
|
pleaseWait: 'Please wait, this may take a moment',
|
||||||
popupBlocked: 'Popup was blocked. Please allow popups for this site.'
|
popupBlocked: 'Popup was blocked. Please allow popups for this site.'
|
||||||
},
|
},
|
||||||
|
userManagement: {
|
||||||
|
title: 'User Management',
|
||||||
|
loading: 'Loading users...',
|
||||||
|
loadError: 'Failed to load users',
|
||||||
|
id: 'ID',
|
||||||
|
username: 'Username',
|
||||||
|
email: 'Email',
|
||||||
|
role: 'Role',
|
||||||
|
createdAt: 'Created At',
|
||||||
|
actions: 'Actions',
|
||||||
|
changeRole: 'Change Role',
|
||||||
|
delete: 'Delete',
|
||||||
|
changeRoleTitle: 'Change User Role',
|
||||||
|
changeRoleFor: 'Change role for',
|
||||||
|
selectRole: 'Select Role',
|
||||||
|
save: 'Save',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
deleteUserTitle: 'Delete User',
|
||||||
|
deleteConfirm: 'Do you really want to delete user',
|
||||||
|
deleteWarning: 'This action cannot be undone!',
|
||||||
|
updateError: 'Failed to update user role',
|
||||||
|
deleteError: 'Failed to delete user',
|
||||||
|
roles: {
|
||||||
|
standard: 'Standard User',
|
||||||
|
maintainer: 'Maintainer',
|
||||||
|
admin: 'Administrator'
|
||||||
|
}
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
title: 'User Profile',
|
title: 'User Profile',
|
||||||
loading: 'Loading profile...',
|
loading: 'Loading profile...',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import AusruestungView from "../views/AusruestungView.vue";
|
|||||||
import MaintenanceView from "../views/MaintenanceView.vue";
|
import MaintenanceView from "../views/MaintenanceView.vue";
|
||||||
import FileUploadPage from "../views/FileUploadPage.vue";
|
import FileUploadPage from "../views/FileUploadPage.vue";
|
||||||
import UserProfileView from "../views/UserProfileView.vue";
|
import UserProfileView from "../views/UserProfileView.vue";
|
||||||
|
import UserManagementView from "../views/UserManagementView.vue";
|
||||||
|
|
||||||
import CharacterDetails from "@/components/CharacterDetails.vue";
|
import CharacterDetails from "@/components/CharacterDetails.vue";
|
||||||
import CharacterCreation from "@/components/CharacterCreation.vue";
|
import CharacterCreation from "@/components/CharacterCreation.vue";
|
||||||
@@ -23,6 +24,7 @@ const routes = [
|
|||||||
{ path: "/reset-password", name: "ResetPassword", component: ResetPasswordView },
|
{ path: "/reset-password", name: "ResetPassword", component: ResetPasswordView },
|
||||||
{ path: "/dashboard", name: "Dashboard", component: DashboardView, meta: { requiresAuth: true } },
|
{ path: "/dashboard", name: "Dashboard", component: DashboardView, meta: { requiresAuth: true } },
|
||||||
{ path: "/profile", name: "UserProfile", component: UserProfileView, meta: { requiresAuth: true } },
|
{ path: "/profile", name: "UserProfile", component: UserProfileView, meta: { requiresAuth: true } },
|
||||||
|
{ path: "/users", name: "UserManagement", component: UserManagementView, meta: { requiresAuth: true, requiresAdmin: true } },
|
||||||
{ path: "/ausruestung/:characterId", name: "Ausruestung", component: AusruestungView, meta: { requiresAuth: true } },
|
{ path: "/ausruestung/:characterId", name: "Ausruestung", component: AusruestungView, meta: { requiresAuth: true } },
|
||||||
{ path: "/maintenance", name: "Maintenance", component: MaintenanceView, meta: { requiresAuth: true } },
|
{ path: "/maintenance", name: "Maintenance", component: MaintenanceView, meta: { requiresAuth: true } },
|
||||||
{ path: "/upload", name: "FileUpload", component: FileUploadPage },
|
{ path: "/upload", name: "FileUpload", component: FileUploadPage },
|
||||||
@@ -39,10 +41,26 @@ const router = createRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Navigation guard
|
// Navigation guard
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
if (to.meta.requiresAuth && !isLoggedIn()) {
|
if (to.meta.requiresAuth && !isLoggedIn()) {
|
||||||
// Redirect to login if not authenticated
|
// Redirect to login if not authenticated
|
||||||
next({ name: "Login" });
|
next({ name: "Login" });
|
||||||
|
} else if (to.meta.requiresAdmin) {
|
||||||
|
// Check if route requires admin role
|
||||||
|
const { useUserStore } = await import('../stores/userStore')
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// Fetch user if not already loaded
|
||||||
|
if (!userStore.currentUser) {
|
||||||
|
await userStore.fetchCurrentUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userStore.isAdmin) {
|
||||||
|
// Redirect to dashboard if not admin
|
||||||
|
next({ name: "Dashboard" });
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
next(); // Allow navigation
|
next(); // Allow navigation
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import API from '../utils/api'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', {
|
||||||
|
state: () => ({
|
||||||
|
currentUser: null,
|
||||||
|
isLoading: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
isAuthenticated: (state) => !!state.currentUser,
|
||||||
|
userRole: (state) => state.currentUser?.role || 'standard',
|
||||||
|
isAdmin: (state) => state.currentUser?.role === 'admin',
|
||||||
|
isMaintainer: (state) => state.currentUser?.role === 'maintainer' || state.currentUser?.role === 'admin',
|
||||||
|
isStandardUser: (state) => !!state.currentUser
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchCurrentUser() {
|
||||||
|
this.isLoading = true
|
||||||
|
try {
|
||||||
|
const response = await API.get('/api/user/profile')
|
||||||
|
this.currentUser = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user profile:', error)
|
||||||
|
this.currentUser = null
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearUser() {
|
||||||
|
this.currentUser = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>{{ $t('userManagement.title') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoading" class="loading">{{ $t('userManagement.loading') }}</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="badge badge-danger">{{ error }}</div>
|
||||||
|
|
||||||
|
<div v-else class="card">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('userManagement.id') }}</th>
|
||||||
|
<th>{{ $t('userManagement.username') }}</th>
|
||||||
|
<th>{{ $t('userManagement.email') }}</th>
|
||||||
|
<th>{{ $t('userManagement.role') }}</th>
|
||||||
|
<th>{{ $t('userManagement.createdAt') }}</th>
|
||||||
|
<th>{{ $t('userManagement.actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="user in users" :key="user.id">
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>
|
||||||
|
<span :class="getRoleBadgeClass(user.role)">
|
||||||
|
{{ $t(`userManagement.roles.${user.role}`) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ formatDate(user.created_at) }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
@click="openRoleDialog(user)"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
:disabled="user.id === currentUser.id"
|
||||||
|
>
|
||||||
|
{{ $t('userManagement.changeRole') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="confirmDeleteUser(user)"
|
||||||
|
class="btn btn-danger btn-sm"
|
||||||
|
:disabled="user.id === currentUser.id"
|
||||||
|
>
|
||||||
|
{{ $t('userManagement.delete') }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role Change Dialog -->
|
||||||
|
<div v-if="showRoleDialog" class="modal-overlay" @click.self="showRoleDialog = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>{{ $t('userManagement.changeRoleTitle') }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{ $t('userManagement.changeRoleFor') }}: <strong>{{ selectedUser.username }}</strong></p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ $t('userManagement.selectRole') }}</label>
|
||||||
|
<select v-model="newRole" class="form-control">
|
||||||
|
<option value="standard">{{ $t('userManagement.roles.standard') }}</option>
|
||||||
|
<option value="maintainer">{{ $t('userManagement.roles.maintainer') }}</option>
|
||||||
|
<option value="admin">{{ $t('userManagement.roles.admin') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button @click="updateUserRole" class="btn btn-primary">
|
||||||
|
{{ $t('userManagement.save') }}
|
||||||
|
</button>
|
||||||
|
<button @click="showRoleDialog = false" class="btn btn-secondary">
|
||||||
|
{{ $t('userManagement.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<div v-if="showDeleteDialog" class="modal-overlay" @click.self="showDeleteDialog = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>{{ $t('userManagement.deleteUserTitle') }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{ $t('userManagement.deleteConfirm') }}: <strong>{{ selectedUser.username }}</strong>?</p>
|
||||||
|
<p class="badge badge-warning">{{ $t('userManagement.deleteWarning') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button @click="deleteUser" class="btn btn-danger">
|
||||||
|
{{ $t('userManagement.delete') }}
|
||||||
|
</button>
|
||||||
|
<button @click="showDeleteDialog = false" class="btn btn-secondary">
|
||||||
|
{{ $t('userManagement.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th,
|
||||||
|
.data-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-role-standard {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-role-maintainer {
|
||||||
|
background-color: #0dcaf0;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-role-admin {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import API from '../utils/api'
|
||||||
|
import { useUserStore } from '../stores/userStore'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UserManagementView',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
users: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
showRoleDialog: false,
|
||||||
|
showDeleteDialog: false,
|
||||||
|
selectedUser: null,
|
||||||
|
newRole: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentUser() {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
return userStore.currentUser
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.loadUsers()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadUsers() {
|
||||||
|
this.isLoading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const response = await API.get('/api/users')
|
||||||
|
this.users = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load users:', error)
|
||||||
|
this.error = this.$t('userManagement.loadError')
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openRoleDialog(user) {
|
||||||
|
this.selectedUser = user
|
||||||
|
this.newRole = user.role
|
||||||
|
this.showRoleDialog = true
|
||||||
|
},
|
||||||
|
async updateUserRole() {
|
||||||
|
try {
|
||||||
|
await API.put(`/api/users/${this.selectedUser.id}/role`, {
|
||||||
|
role: this.newRole
|
||||||
|
})
|
||||||
|
this.showRoleDialog = false
|
||||||
|
await this.loadUsers()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update user role:', error)
|
||||||
|
this.error = this.$t('userManagement.updateError')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmDeleteUser(user) {
|
||||||
|
this.selectedUser = user
|
||||||
|
this.showDeleteDialog = true
|
||||||
|
},
|
||||||
|
async deleteUser() {
|
||||||
|
try {
|
||||||
|
await API.delete(`/api/users/${this.selectedUser.id}`)
|
||||||
|
this.showDeleteDialog = false
|
||||||
|
await this.loadUsers()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete user:', error)
|
||||||
|
this.error = this.$t('userManagement.deleteError')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getRoleBadgeClass(role) {
|
||||||
|
return `badge-role-${role}`
|
||||||
|
},
|
||||||
|
formatDate(dateString) {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user