added an easy role concept
User roles
This commit is contained in:
+31
-26
@@ -1,39 +1,44 @@
|
||||
package gsmaster
|
||||
|
||||
import (
|
||||
"bamort/user"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoutes(r *gin.RouterGroup) {
|
||||
maintGrp := r.Group("/maintenance")
|
||||
maintGrp.GET("", GetMasterData)
|
||||
maintGrp.GET("/skills", GetMDSkills)
|
||||
maintGrp.GET("/skills/:id", GetMDSkill)
|
||||
maintGrp.PUT("/skills/:id", UpdateMDSkill)
|
||||
maintGrp.POST("/skills", AddSkill)
|
||||
maintGrp.DELETE("/skills/:id", DeleteMDSkill)
|
||||
maintGrp.Use(user.RequireMaintainer())
|
||||
{
|
||||
maintGrp.GET("", GetMasterData)
|
||||
maintGrp.GET("/skills", GetMDSkills)
|
||||
maintGrp.GET("/skills/:id", GetMDSkill)
|
||||
maintGrp.PUT("/skills/:id", UpdateMDSkill)
|
||||
maintGrp.POST("/skills", AddSkill)
|
||||
maintGrp.DELETE("/skills/:id", DeleteMDSkill)
|
||||
|
||||
maintGrp.GET("/weaponskills", GetMDWeaponSkills)
|
||||
maintGrp.GET("/weaponskills/:id", GetMDWeaponSkill)
|
||||
maintGrp.PUT("/weaponskills/:id", UpdateMDWeaponSkill)
|
||||
maintGrp.POST("/weaponskills", AddWeaponSkill)
|
||||
maintGrp.DELETE("/weaponskills/:id", DeleteMDWeaponSkill)
|
||||
maintGrp.GET("/weaponskills", GetMDWeaponSkills)
|
||||
maintGrp.GET("/weaponskills/:id", GetMDWeaponSkill)
|
||||
maintGrp.PUT("/weaponskills/:id", UpdateMDWeaponSkill)
|
||||
maintGrp.POST("/weaponskills", AddWeaponSkill)
|
||||
maintGrp.DELETE("/weaponskills/:id", DeleteMDWeaponSkill)
|
||||
|
||||
maintGrp.GET("/spells", GetMDSpells)
|
||||
maintGrp.GET("/spells/:id", GetMDSpell)
|
||||
maintGrp.PUT("/spells/:id", UpdateMDSpell)
|
||||
maintGrp.POST("/spells", AddSpell)
|
||||
maintGrp.DELETE("/spells/:id", DeleteMDSpell)
|
||||
maintGrp.GET("/spells", GetMDSpells)
|
||||
maintGrp.GET("/spells/:id", GetMDSpell)
|
||||
maintGrp.PUT("/spells/:id", UpdateMDSpell)
|
||||
maintGrp.POST("/spells", AddSpell)
|
||||
maintGrp.DELETE("/spells/:id", DeleteMDSpell)
|
||||
|
||||
maintGrp.GET("/equipment", GetMDEquipments)
|
||||
maintGrp.GET("/equipment/:id", GetMDEquipment)
|
||||
maintGrp.PUT("/equipment/:id", UpdateMDEquipment)
|
||||
maintGrp.POST("/equipment", AddEquipment)
|
||||
maintGrp.DELETE("/equipment/:id", DeleteMDEquipment)
|
||||
maintGrp.GET("/equipment", GetMDEquipments)
|
||||
maintGrp.GET("/equipment/:id", GetMDEquipment)
|
||||
maintGrp.PUT("/equipment/:id", UpdateMDEquipment)
|
||||
maintGrp.POST("/equipment", AddEquipment)
|
||||
maintGrp.DELETE("/equipment/:id", DeleteMDEquipment)
|
||||
|
||||
maintGrp.GET("/weapons", GetMDWeapons)
|
||||
maintGrp.GET("/weapons/:id", GetMDWeapon)
|
||||
maintGrp.PUT("/weapons/:id", UpdateMDWeapon)
|
||||
maintGrp.POST("/weapons", AddWeapon)
|
||||
maintGrp.DELETE("/weapons/:id", DeleteMDWeapon)
|
||||
maintGrp.GET("/weapons", GetMDWeapons)
|
||||
maintGrp.GET("/weapons/:id", GetMDWeapon)
|
||||
maintGrp.PUT("/weapons/:id", UpdateMDWeapon)
|
||||
maintGrp.POST("/weapons", AddWeapon)
|
||||
maintGrp.DELETE("/weapons/:id", DeleteMDWeapon)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"bamort/user"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoutes(r *gin.RouterGroup) {
|
||||
charGrp := r.Group("/maintenance")
|
||||
charGrp.GET("/setupcheck", SetupCheck)
|
||||
charGrp.GET("/setupcheck-dev", SetupCheckDev)
|
||||
charGrp.GET("/mktestdata", MakeTestdataFromLive)
|
||||
charGrp.GET("/reconndb", ReconnectDataBase) // Datenbank neu verbinden
|
||||
charGrp.GET("/reloadenv", ReloadENV)
|
||||
charGrp.POST("/transfer-sqlite-to-mariadb", TransferSQLiteToMariaDB) // Transfer data from SQLite to MariaDB
|
||||
/*
|
||||
//nur zur einmaligen Ausführung, um das Lernkosten-System zu initialisieren
|
||||
charGrp.POST("/initialize-learning-costs", InitializeLearningCosts)
|
||||
// Zur Überprüfung der Lernkosten-Daten
|
||||
charGrp.GET("/learning-costs-summary", gsmaster.GetLearningCostsSummaryHandler)
|
||||
*/
|
||||
|
||||
charGrp.Use(user.RequireMaintainer())
|
||||
{
|
||||
charGrp.GET("/setupcheck", SetupCheck)
|
||||
charGrp.GET("/setupcheck-dev", SetupCheckDev)
|
||||
charGrp.GET("/mktestdata", MakeTestdataFromLive)
|
||||
charGrp.GET("/reconndb", ReconnectDataBase) // Datenbank neu verbinden
|
||||
charGrp.GET("/reloadenv", ReloadENV)
|
||||
charGrp.POST("/transfer-sqlite-to-mariadb", TransferSQLiteToMariaDB) // Transfer data from SQLite to MariaDB
|
||||
/*
|
||||
//nur zur einmaligen Ausführung, um das Lernkosten-System zu initialisieren
|
||||
charGrp.POST("/initialize-learning-costs", InitializeLearningCosts)
|
||||
// Zur Überprüfung der Lernkosten-Daten
|
||||
charGrp.GET("/learning-costs-summary", gsmaster.GetLearningCostsSummaryHandler)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
||||
|
||||
// RegisterPublicRoutes registers public PDF routes (no authentication required)
|
||||
func RegisterPublicRoutes(r *gin.Engine) {
|
||||
// Get PDF file from xporttemp (public - for direct browser access)
|
||||
// Get PDF file from export_temp (public - for direct browser access)
|
||||
r.GET("/api/pdf/file/:filename", GetPDFFile)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/logger"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"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",
|
||||
})
|
||||
}
|
||||
|
||||
// ChangeUserPassword allows admin to change a user's password (admin only)
|
||||
func ChangeUserPassword(c *gin.Context) {
|
||||
logger.Debug("Admin changing user password...")
|
||||
|
||||
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 {
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
logger.Error("Failed to parse password data: %s", err.Error())
|
||||
respondWithError(c, http.StatusBadRequest, "New password (min 6 characters) is required")
|
||||
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)
|
||||
|
||||
// Hash new password using MD5 (same as registration)
|
||||
hashedPassword := md5.Sum([]byte(input.NewPassword))
|
||||
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
|
||||
|
||||
if err := user.Save(); err != nil {
|
||||
logger.Error("Failed to update password for user %s: %s", user.Username, err.Error())
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to update password")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Password changed for user %s (ID: %d) by admin %s", user.Username, user.UserID, requestingUser.Username)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Password updated successfully",
|
||||
})
|
||||
}
|
||||
@@ -60,6 +60,11 @@ func RegisterUser(c *gin.Context) {
|
||||
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
|
||||
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)
|
||||
if err := user.Create(); err != nil {
|
||||
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
|
||||
c.Set("userID", user.UserID)
|
||||
c.Set("username", user.Username)
|
||||
c.Set("user", user)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
@@ -438,6 +444,7 @@ func GetUserProfile(c *gin.Context) {
|
||||
"id": user.UserID,
|
||||
"username": user.Username,
|
||||
"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"
|
||||
)
|
||||
|
||||
// Role constants
|
||||
const (
|
||||
RoleStandardUser = "standard"
|
||||
RoleMaintainer = "maintainer"
|
||||
RoleAdmin = "admin"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
UserID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique" json:"username"`
|
||||
PasswordHash string `json:"password"`
|
||||
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)
|
||||
ResetPwHashExpires *time.Time `json:"-"` // Ablaufzeit für Password-Reset-Hash
|
||||
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)
|
||||
}
|
||||
|
||||
// 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,15 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
||||
userGroup.PUT("/email", UpdateEmail)
|
||||
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.PUT("/:id/password", ChangeUserPassword)
|
||||
adminGroup.DELETE("/:id", DeleteUser)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
<script>
|
||||
import API from '../utils/api'
|
||||
import { useUserStore } from '../stores/userStore'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -73,6 +74,11 @@ export default {
|
||||
password: this.password,
|
||||
})
|
||||
localStorage.setItem('token', response.data.token)
|
||||
|
||||
// Fetch user profile to get role information
|
||||
const userStore = useUserStore()
|
||||
await userStore.fetchCurrentUser()
|
||||
|
||||
// Emit auth change event
|
||||
window.dispatchEvent(new Event('auth-changed'))
|
||||
this.$router.push('/dashboard')
|
||||
|
||||
@@ -13,9 +13,12 @@
|
||||
<li v-if="!isLoggedIn">
|
||||
<router-link to="/register" active-class="active">{{ $t('menu.Register') }}</router-link>
|
||||
</li>
|
||||
<li v-if="isLoggedIn">
|
||||
<li v-if="isLoggedIn && isMaintainer">
|
||||
<router-link to="/maintenance" active-class="active">{{ $t('menu.Maintenance') }}</router-link>
|
||||
</li>
|
||||
<li v-if="isLoggedIn && isAdmin">
|
||||
<router-link to="/users" active-class="active">{{ $t('menu.UserManagement') }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="menu-right">
|
||||
<LanguageSwitcher />
|
||||
@@ -27,6 +30,7 @@
|
||||
<script>
|
||||
import { isLoggedIn, logout } from "../utils/auth";
|
||||
import LanguageSwitcher from "./LanguageSwitcher.vue";
|
||||
import { useUserStore } from "../stores/userStore";
|
||||
|
||||
|
||||
export default {
|
||||
@@ -34,18 +38,48 @@ export default {
|
||||
components: {
|
||||
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: {
|
||||
isLoggedIn() {
|
||||
return isLoggedIn();
|
||||
},
|
||||
isAdmin() {
|
||||
return this.userStore?.isAdmin || false
|
||||
},
|
||||
isMaintainer() {
|
||||
return this.userStore?.isMaintainer || false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
logout();
|
||||
this.userStore.clearUser()
|
||||
// Emit auth change event
|
||||
window.dispatchEvent(new Event('auth-changed'));
|
||||
this.$router.push("/");
|
||||
},
|
||||
async handleAuthChange() {
|
||||
if (isLoggedIn()) {
|
||||
await this.userStore.fetchCurrentUser()
|
||||
} else {
|
||||
this.userStore.clearUser()
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -43,6 +43,7 @@ export default {
|
||||
Logout:'Abmelden',
|
||||
Register:'Registrieren',
|
||||
Maintenance:'Wartung',
|
||||
UserManagement:'Benutzerverwaltung',
|
||||
},
|
||||
Equipment:'Ausrüstung',
|
||||
equipment:{
|
||||
@@ -444,12 +445,52 @@ export default {
|
||||
pleaseWait: 'Bitte warten, dies kann einen Moment dauern',
|
||||
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',
|
||||
changePassword: 'Passwort ä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',
|
||||
changePasswordTitle: 'Benutzerpasswort ändern',
|
||||
changePasswordFor: 'Passwort ändern für',
|
||||
newPassword: 'Neues Passwort',
|
||||
newPasswordPlaceholder: 'Mindestens 6 Zeichen',
|
||||
confirmPassword: 'Passwort bestätigen',
|
||||
confirmPasswordPlaceholder: 'Neues Passwort wiederholen',
|
||||
passwordRequired: 'Passwort ist erforderlich',
|
||||
passwordTooShort: 'Das Passwort muss mindestens 6 Zeichen lang sein',
|
||||
passwordMismatch: 'Die Passwörter stimmen nicht überein',
|
||||
passwordChangeError: 'Fehler beim Ändern des Passworts',
|
||||
roles: {
|
||||
standard: 'Standard-Benutzer',
|
||||
maintainer: 'Maintainer',
|
||||
admin: 'Administrator'
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
title: 'Benutzerprofil',
|
||||
loading: 'Lade Profil...',
|
||||
userInfo: 'Benutzerinformationen',
|
||||
username: 'Benutzername',
|
||||
currentEmail: 'Aktuelle E-Mail',
|
||||
role: 'Rolle',
|
||||
changeEmail: 'E-Mail ändern',
|
||||
newEmail: 'Neue E-Mail',
|
||||
emailPlaceholder: 'ihre.email@example.com',
|
||||
|
||||
+41
-2
@@ -42,8 +42,7 @@ export default {
|
||||
ImportData:'Import Data',
|
||||
Logout:'Logout',
|
||||
Register:'Register',
|
||||
Maintenance:'Maintenance',
|
||||
},
|
||||
Maintenance:'Maintenance', UserManagement:'User Management', },
|
||||
Equipment:'Equipment',
|
||||
equipment:{
|
||||
id:'ID',
|
||||
@@ -442,12 +441,52 @@ export default {
|
||||
pleaseWait: 'Please wait, this may take a moment',
|
||||
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',
|
||||
changePassword: 'Change Password',
|
||||
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',
|
||||
changePasswordTitle: 'Change User Password',
|
||||
changePasswordFor: 'Change password for',
|
||||
newPassword: 'New Password',
|
||||
newPasswordPlaceholder: 'At least 6 characters',
|
||||
confirmPassword: 'Confirm Password',
|
||||
confirmPasswordPlaceholder: 'Repeat new password',
|
||||
passwordRequired: 'Password is required',
|
||||
passwordTooShort: 'Password must be at least 6 characters long',
|
||||
passwordMismatch: 'Passwords do not match',
|
||||
passwordChangeError: 'Failed to change password',
|
||||
roles: {
|
||||
standard: 'Standard User',
|
||||
maintainer: 'Maintainer',
|
||||
admin: 'Administrator'
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
title: 'User Profile',
|
||||
loading: 'Loading profile...',
|
||||
userInfo: 'User Information',
|
||||
username: 'Username',
|
||||
currentEmail: 'Current Email',
|
||||
role: 'Role',
|
||||
changeEmail: 'Change Email',
|
||||
newEmail: 'New Email',
|
||||
emailPlaceholder: 'your.email@example.com',
|
||||
|
||||
@@ -9,6 +9,7 @@ import AusruestungView from "../views/AusruestungView.vue";
|
||||
import MaintenanceView from "../views/MaintenanceView.vue";
|
||||
import FileUploadPage from "../views/FileUploadPage.vue";
|
||||
import UserProfileView from "../views/UserProfileView.vue";
|
||||
import UserManagementView from "../views/UserManagementView.vue";
|
||||
|
||||
import CharacterDetails from "@/components/CharacterDetails.vue";
|
||||
import CharacterCreation from "@/components/CharacterCreation.vue";
|
||||
@@ -23,6 +24,7 @@ const routes = [
|
||||
{ path: "/reset-password", name: "ResetPassword", component: ResetPasswordView },
|
||||
{ path: "/dashboard", name: "Dashboard", component: DashboardView, 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: "/maintenance", name: "Maintenance", component: MaintenanceView, meta: { requiresAuth: true } },
|
||||
{ path: "/upload", name: "FileUpload", component: FileUploadPage },
|
||||
@@ -39,10 +41,26 @@ const router = createRouter({
|
||||
});
|
||||
|
||||
// Navigation guard
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (to.meta.requiresAuth && !isLoggedIn()) {
|
||||
// Redirect to login if not authenticated
|
||||
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 {
|
||||
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,368 @@
|
||||
<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="openPasswordDialog(user)"
|
||||
class="btn btn-sm"
|
||||
>
|
||||
{{ $t('userManagement.changePassword') }}
|
||||
</button>
|
||||
<button
|
||||
@click="confirmDeleteUser(user)"
|
||||
class="btn 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>
|
||||
|
||||
<!-- Change Password Dialog -->
|
||||
<div v-if="showPasswordDialog" class="modal-overlay" @click.self="showPasswordDialog = false">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>{{ $t('userManagement.changePasswordTitle') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ $t('userManagement.changePasswordFor') }}: <strong>{{ selectedUser.username }}</strong></p>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('userManagement.newPassword') }}</label>
|
||||
<input
|
||||
v-model="newPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
:placeholder="$t('userManagement.newPasswordPlaceholder')"
|
||||
minlength="6"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('userManagement.confirmPassword') }}</label>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
:placeholder="$t('userManagement.confirmPasswordPlaceholder')"
|
||||
minlength="6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="changeUserPassword" class="btn btn-primary">
|
||||
{{ $t('userManagement.save') }}
|
||||
</button>
|
||||
<button @click="showPasswordDialog = 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,
|
||||
showPasswordDialog: false,
|
||||
selectedUser: null,
|
||||
newRole: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
},
|
||||
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')
|
||||
}
|
||||
},
|
||||
openPasswordDialog(user) {
|
||||
this.selectedUser = user
|
||||
this.newPassword = ''
|
||||
this.confirmPassword = ''
|
||||
this.showPasswordDialog = true
|
||||
},
|
||||
async changeUserPassword() {
|
||||
if (!this.newPassword || !this.confirmPassword) {
|
||||
this.error = this.$t('userManagement.passwordRequired')
|
||||
return
|
||||
}
|
||||
if (this.newPassword.length < 6) {
|
||||
this.error = this.$t('userManagement.passwordTooShort')
|
||||
return
|
||||
}
|
||||
if (this.newPassword !== this.confirmPassword) {
|
||||
this.error = this.$t('userManagement.passwordMismatch')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await API.put(`/api/users/${this.selectedUser.id}/password`, {
|
||||
new_password: this.newPassword
|
||||
})
|
||||
this.showPasswordDialog = false
|
||||
this.error = null
|
||||
} catch (error) {
|
||||
console.error('Failed to change password:', error)
|
||||
this.error = this.$t('userManagement.passwordChangeError')
|
||||
}
|
||||
},
|
||||
getRoleBadgeClass(role) {
|
||||
return `badge-role-${role}`
|
||||
},
|
||||
formatDate(dateString) {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -19,6 +19,12 @@
|
||||
<label>{{ $t('profile.currentEmail') }}:</label>
|
||||
<span>{{ userProfile.email }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<label>{{ $t('profile.role') }}:</label>
|
||||
<span :class="getRoleBadgeClass(userProfile.role)">
|
||||
{{ $t(`userManagement.roles.${userProfile.role}`) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Email Section -->
|
||||
@@ -200,6 +206,33 @@ h1 {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.badge-role-standard {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-role-maintainer {
|
||||
background-color: #0dcaf0;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-role-admin {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -213,7 +246,8 @@ export default {
|
||||
isUpdating: false,
|
||||
userProfile: {
|
||||
username: '',
|
||||
email: ''
|
||||
email: '',
|
||||
role: 'standard'
|
||||
},
|
||||
emailForm: {
|
||||
newEmail: ''
|
||||
@@ -242,6 +276,9 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getRoleBadgeClass(role) {
|
||||
return `badge-role-${role}`
|
||||
},
|
||||
async updateEmail() {
|
||||
if (!this.emailForm.newEmail) {
|
||||
alert(this.$t('profile.emailRequired'))
|
||||
|
||||
Reference in New Issue
Block a user