We have a role concept

This commit is contained in:
2025-12-30 08:00:04 +01:00
parent aa82b95d9e
commit bc948fcad4
13 changed files with 985 additions and 4 deletions
+194
View File
@@ -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",
})
}
+7
View File
@@ -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,
}) })
} }
+65
View File
@@ -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)
}
+33
View File
@@ -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
}
+233
View File
@@ -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")
}
+10
View File
@@ -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)
}
} }
+6
View File
@@ -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')
+35 -1
View File
@@ -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>
+29
View File
@@ -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
View File
@@ -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...',
+19 -1
View File
@@ -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
} }
+36
View File
@@ -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
}
}
})
+289
View File
@@ -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>