From 7f95a5ed54ffad458ddea9a02556a4ef15c9e91d Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 1 Feb 2026 18:15:38 +0100 Subject: [PATCH 01/10] added shared chars to list --- frontend/src/components/CharacterList.vue | 74 +++++++++++++++++------ frontend/src/locales/de | 4 +- frontend/src/locales/en | 4 +- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/CharacterList.vue b/frontend/src/components/CharacterList.vue index 441dcec..9c7c83d 100644 --- a/frontend/src/components/CharacterList.vue +++ b/frontend/src/components/CharacterList.vue @@ -16,29 +16,50 @@ @delete-session="handleDeleteSession" /> -
+

{{ $t('characters.list.no_characters') }}

{{ $t('characters.list.no_characters_description') }}

-
-
- -

{{ character.name }}

-
- {{ character.rasse }} | - {{ character.typ }} | - {{ $t('characters.list.grade') }}: {{ character.grad }} | - {{ $t('characters.list.owner') }}: {{ character.owner }} | - - {{ character.public ? $t('characters.list.public') : $t('characters.list.private') }} - -
-
+
+
+ {{ $t('characters.list.owned_characters_title') }} +
+ +

{{ character.name }}

+
+ {{ character.rasse }} | + {{ character.typ }} | + {{ $t('characters.list.grade') }}: {{ character.grad }} | + {{ $t('characters.list.owner') }}: {{ character.owner }} | + + {{ character.public ? $t('characters.list.public') : $t('characters.list.private') }} + +
+
+
+
+
+ {{ $t('characters.list.shared_characters_title') }} +
+ +

{{ character.name }}

+
+ {{ character.rasse }} | + {{ character.typ }} | + {{ $t('characters.list.grade') }}: {{ character.grad }} | + {{ $t('characters.list.owner') }}: {{ character.owner }} | + + {{ character.public ? $t('characters.list.public') : $t('characters.list.private') }} + +
+
+
- - diff --git a/frontend/src/locales/de b/frontend/src/locales/de index ef61c2f..d0ac7f5 100644 --- a/frontend/src/locales/de +++ b/frontend/src/locales/de @@ -513,6 +513,19 @@ export default { pleaseWait: 'Bitte warten, dies kann einen Moment dauern', popupBlocked: 'Popup wurde blockiert. Bitte erlauben Sie Popups für diese Seite.' }, + visibility: { + title: 'Sichtbarkeit ändern', + description: 'Legen Sie fest, wer diesen Charakter sehen kann.', + private: 'Privat', + privateDescription: 'Nur Sie können diesen Charakter sehen', + public: 'Öffentlich', + publicDescription: 'Alle Benutzer können diesen Charakter sehen', + cancel: 'Abbrechen', + save: 'Speichern', + saving: 'Wird gespeichert...', + updating: 'Sichtbarkeit wird aktualisiert...', + updateError: 'Fehler beim Aktualisieren der Sichtbarkeit' + }, userManagement: { title: 'Benutzerverwaltung', loading: 'Lade Benutzer...', diff --git a/frontend/src/locales/en b/frontend/src/locales/en index 07217b0..0812e43 100644 --- a/frontend/src/locales/en +++ b/frontend/src/locales/en @@ -509,6 +509,19 @@ export default { pleaseWait: 'Please wait, this may take a moment', popupBlocked: 'Popup was blocked. Please allow popups for this site.' }, + visibility: { + title: 'Change Visibility', + description: 'Set who can see this character.', + private: 'Private', + privateDescription: 'Only you can see this character', + public: 'Public', + publicDescription: 'All users can see this character', + cancel: 'Cancel', + save: 'Save', + saving: 'Saving...', + updating: 'Updating visibility...', + updateError: 'Failed to update visibility' + }, userManagement: { title: 'User Management', loading: 'Loading users...', From 321339861fd2a8c372070f3ab4ac84ce0f5f5f58 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 1 Feb 2026 21:32:48 +0100 Subject: [PATCH 04/10] Now we can share a char to other users only in DB no UI yet --- backend/character/handlers.go | 8 +++++++ backend/maintenance/handlers.go | 3 +++ backend/models/database.go | 3 +++ backend/models/model_character.go | 15 ++++++++++++ backend/models/model_character_share.go | 32 +++++++++++++++++++++++++ 5 files changed, 61 insertions(+) create mode 100644 backend/models/model_character_share.go diff --git a/backend/character/handlers.go b/backend/character/handlers.go index 37794c9..bf3b6dd 100644 --- a/backend/character/handlers.go +++ b/backend/character/handlers.go @@ -59,6 +59,14 @@ func ListCharacters(c *gin.Context) { respondWithError(c, http.StatusInternalServerError, "Failed to retrieve public characters") return } + listShared, err := models.FindSharedCharList(c.GetUint("userID")) + if err != nil { + logger.Error("Fehler beim Laden der geteilten Charaktere: %s", err.Error()) + respondWithError(c, http.StatusInternalServerError, "Failed to retrieve shared characters") + return + } + listPublic = append(listPublic, listShared...) + allCharacters.Others = listPublic logger.Info("Charakterliste erfolgreich geladen: %d Charaktere", len(listOfChars)) diff --git a/backend/maintenance/handlers.go b/backend/maintenance/handlers.go index c43974c..b151d38 100644 --- a/backend/maintenance/handlers.go +++ b/backend/maintenance/handlers.go @@ -295,6 +295,9 @@ func copyMariaDBToSQLite(mariaDB, sqliteDB *gorm.DB) error { // Audit Logging (abhängig von Char) &models.AuditLogEntry{}, + // Char Shares (abhängig von Char und User) + &models.CharShare{}, + // View-Strukturen ohne eigene Tabellen werden nicht kopiert: // SkillLearningInfo, SpellLearningInfo, CharList, FeChar, etc. } diff --git a/backend/models/database.go b/backend/models/database.go index 2f81806..662e087 100644 --- a/backend/models/database.go +++ b/backend/models/database.go @@ -49,6 +49,7 @@ func MigrateStructure(db ...*gorm.DB) error { return nil } + func gameSystemMigrateStructure(db ...*gorm.DB) error { // Use provided DB or default to database.DB var targetDB *gorm.DB @@ -66,6 +67,7 @@ func gameSystemMigrateStructure(db ...*gorm.DB) error { } return nil } + func gsMasterMigrateStructure(db ...*gorm.DB) error { // Use provided DB or default to database.DB var targetDB *gorm.DB @@ -112,6 +114,7 @@ func characterMigrateStructure(db ...*gorm.DB) error { &Bennies{}, &Vermoegen{}, &CharacterCreationSession{}, + &CharShare{}, ) if err != nil { return err diff --git a/backend/models/model_character.go b/backend/models/model_character.go index 36cfc4c..2ae3775 100644 --- a/backend/models/model_character.go +++ b/backend/models/model_character.go @@ -255,6 +255,21 @@ func (object *Char) FindByUserID(userID uint) ([]Char, error) { return chars, nil } +func FindSharedCharList(userID uint) ([]CharList, error) { + var chars []CharList + gs := GetGameSystem(0, "midgard") + err := database.DB.Table("char_chars"). + Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, users.username as owner"). + Joins("LEFT JOIN users ON char_chars.user_id = users.user_id"). + Joins("INNER JOIN char_shares ON char_shares.character_id = char_chars.id"). + Where("char_shares.user_id = ? AND (char_chars.game_system = ? OR char_chars.game_system_id = ?)", userID, gs.Name, gs.ID). + Find(&chars).Error + if err != nil { + return nil, err + } + return chars, nil +} + func FindPublicCharList() ([]CharList, error) { var chars []CharList gs := GetGameSystem(0, "midgard") diff --git a/backend/models/model_character_share.go b/backend/models/model_character_share.go new file mode 100644 index 0000000..d09f30d --- /dev/null +++ b/backend/models/model_character_share.go @@ -0,0 +1,32 @@ +package models + +import ( + "bamort/database" + "fmt" +) + +type CharShare struct { + ID uint `gorm:"primaryKey" json:"id"` + CharacterID uint `gorm:"index" json:"character_id"` // ID of the character being shared + UserID uint `gorm:"index" json:"user_id"` // ID of the user with whom the character is shared + Permission string `json:"permission"` // Permission level (e.g., "read", "write") +} + +func (object *CharShare) TableName() string { + dbPrefix := "char" + return dbPrefix + "_" + "shares" +} + +func (object *CharShare) FirstByChar(id uint) error { + if id == 0 { + return fmt.Errorf("invalid character ID") + } + return database.DB.First(object, "character_id = ?", id).Error +} + +func (object *CharShare) FirstByUser(id uint) error { + if id == 0 { + return fmt.Errorf("invalid user ID") + } + return database.DB.First(object, "user_id = ?", id).Error +} From 9bf36599fb6cad9f5031eb9693c357fb70121457 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 1 Feb 2026 22:48:07 +0100 Subject: [PATCH 05/10] remove edit elements if user is not owner --- backend/character/handlers.go | 54 +++++++++++++++++ backend/character/image_handler.go | 5 ++ backend/character/practice_points_handler.go | 15 +++++ backend/equipment/handlers.go | 63 +++++++++++++++++++- backend/gsmaster/routes.go | 44 +++++++------- frontend/src/components/CharacterDetails.vue | 11 +++- frontend/src/components/DeleteCharView.vue | 15 ++++- frontend/src/components/EquipmentView.vue | 28 ++++++++- frontend/src/components/ExperianceView.vue | 8 ++- frontend/src/components/SkillView.vue | 12 +++- frontend/src/components/SpellView.vue | 6 +- frontend/src/components/WeaponView.vue | 10 +++- frontend/src/locales/de | 3 + frontend/src/locales/en | 3 + 14 files changed, 237 insertions(+), 40 deletions(-) diff --git a/backend/character/handlers.go b/backend/character/handlers.go index bf3b6dd..66855fb 100644 --- a/backend/character/handlers.go +++ b/backend/character/handlers.go @@ -30,6 +30,17 @@ func respondWithError(c *gin.Context, status int, message string) { c.JSON(status, gin.H{"error": message}) } +// checkCharacterOwnership verifies that the logged-in user owns the character +func checkCharacterOwnership(c *gin.Context, character *models.Char) bool { + userID := c.GetUint("userID") + if character.UserID != userID { + logger.Warn("Unauthorized access attempt: user %d tried to modify character %d owned by user %d", userID, character.ID, character.UserID) + respondWithError(c, http.StatusForbidden, "You are not authorized to modify this character") + return false + } + return true +} + func ListCharacters(c *gin.Context) { logger.Debug("ListCharacters aufgerufen") @@ -109,6 +120,11 @@ func UpdateCharacter(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + // Store the original ID to preserve it originalID := character.ID originalGameSystem := character.GameSystem @@ -141,6 +157,12 @@ func DeleteCharacter(c *gin.Context) { respondWithError(c, http.StatusNotFound, "Character not found") return } + + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + err = character.Delete() if err != nil { respondWithError(c, http.StatusInternalServerError, "Failed to delete character") @@ -275,6 +297,11 @@ func UpdateCharacterExperience(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + // Parse Request var req UpdateExperienceRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -365,6 +392,11 @@ func UpdateCharacterWealth(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + // Parse Request var req UpdateWealthRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -575,6 +607,11 @@ func LearnSkill(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + // Verwende gsmaster.LernCostRequest direkt var request gsmaster.LernCostRequest if err := c.ShouldBindJSON(&request); err != nil { @@ -1060,6 +1097,11 @@ func ImproveSkill(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, char) { + return + } + // 2. Skill validieren und Level ermitteln characterClass, skillInfo, currentLevel, err := validateSkillForImprovement(char, &request) if err != nil { @@ -1233,6 +1275,18 @@ func LearnSpell(c *gin.Context) { } charID := uint(charIDInt) + // Load character to check ownership + var character models.Char + if err := character.FirstID(char_ID); err != nil { + respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden") + return + } + + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + var lernRequest gsmaster.LernCostRequest if err := c.ShouldBindJSON(&lernRequest); err != nil { respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error()) diff --git a/backend/character/image_handler.go b/backend/character/image_handler.go index b88b6da..f79bc86 100644 --- a/backend/character/image_handler.go +++ b/backend/character/image_handler.go @@ -25,6 +25,11 @@ func UpdateCharacterImage(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + var request ImageUpdateRequest if err := c.ShouldBindJSON(&request); err != nil { logger.Error("Invalid request data: %s", err.Error()) diff --git a/backend/character/practice_points_handler.go b/backend/character/practice_points_handler.go index 5c3d41a..714dca0 100644 --- a/backend/character/practice_points_handler.go +++ b/backend/character/practice_points_handler.go @@ -63,6 +63,11 @@ func UpdatePracticePoints(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + // Request-Parameter abrufen var practicePoints []PracticePointResponse if err := c.ShouldBindJSON(&practicePoints); err != nil { @@ -120,6 +125,11 @@ func AddPracticePoint(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + // Request-Parameter abrufen type AddPPRequest struct { SkillName string `json:"skill_name" binding:"required"` @@ -218,6 +228,11 @@ func UsePracticePoint(c *gin.Context) { return } + // Check ownership + if !checkCharacterOwnership(c, &character) { + return + } + // Request-Parameter abrufen type UsePPRequest struct { SkillName string `json:"skill_name" binding:"required"` diff --git a/backend/equipment/handlers.go b/backend/equipment/handlers.go index be68e2f..abe4e16 100644 --- a/backend/equipment/handlers.go +++ b/backend/equipment/handlers.go @@ -19,6 +19,21 @@ func respondWithError(c *gin.Context, status int, message string) { c.JSON(status, gin.H{"error": message}) } +// checkEquipmentOwnership verifies that the logged-in user owns the equipment's character +func checkEquipmentOwnership(c *gin.Context, characterID uint) bool { + userID := c.GetUint("userID") + var character models.Char + if err := database.DB.Select("id", "user_id").First(&character, characterID).Error; err != nil { + respondWithError(c, http.StatusNotFound, "Character not found") + return false + } + if character.UserID != userID { + respondWithError(c, http.StatusForbidden, "You are not authorized to modify this character's equipment") + return false + } + return true +} + func CreateAusruestung(c *gin.Context) { var ausruestung models.EqAusruestung if err := c.ShouldBindJSON(&ausruestung); err != nil { @@ -26,6 +41,11 @@ func CreateAusruestung(c *gin.Context) { return } + // Check ownership + if !checkEquipmentOwnership(c, ausruestung.CharacterID) { + return + } + if err := database.DB.Create(&ausruestung).Error; err != nil { respondWithError(c, http.StatusInternalServerError, "Failed to create Ausruestung") return @@ -55,6 +75,11 @@ func UpdateAusruestung(c *gin.Context) { return } + // Check ownership + if !checkEquipmentOwnership(c, ausruestung.CharacterID) { + return + } + if err := c.ShouldBindJSON(&ausruestung); err != nil { respondWithError(c, http.StatusBadRequest, err.Error()) return @@ -70,7 +95,19 @@ func UpdateAusruestung(c *gin.Context) { func DeleteAusruestung(c *gin.Context) { ausruestungID := c.Param("ausruestung_id") - if err := database.DB.Delete(&models.EqAusruestung{}, ausruestungID).Error; err != nil { + + var ausruestung models.EqAusruestung + if err := database.DB.First(&ausruestung, ausruestungID).Error; err != nil { + respondWithError(c, http.StatusNotFound, "Ausruestung not found") + return + } + + // Check ownership + if !checkEquipmentOwnership(c, ausruestung.CharacterID) { + return + } + + if err := database.DB.Delete(&ausruestung).Error; err != nil { respondWithError(c, http.StatusInternalServerError, "Failed to delete Ausruestung") return } @@ -89,6 +126,11 @@ func CreateWaffe(c *gin.Context) { return } + // Check ownership + if !checkEquipmentOwnership(c, waffe.CharacterID) { + return + } + if err := database.DB.Create(&waffe).Error; err != nil { respondWithError(c, http.StatusInternalServerError, "Failed to create Waffe") return @@ -118,6 +160,11 @@ func UpdateWaffe(c *gin.Context) { return } + // Check ownership + if !checkEquipmentOwnership(c, waffe.CharacterID) { + return + } + if err := c.ShouldBindJSON(&waffe); err != nil { respondWithError(c, http.StatusBadRequest, err.Error()) return @@ -133,7 +180,19 @@ func UpdateWaffe(c *gin.Context) { func DeleteWaffe(c *gin.Context) { waffeID := c.Param("waffe_id") - if err := database.DB.Delete(&models.EqWaffe{}, waffeID).Error; err != nil { + + var waffe models.EqWaffe + if err := database.DB.First(&waffe, waffeID).Error; err != nil { + respondWithError(c, http.StatusNotFound, "Waffe not found") + return + } + + // Check ownership + if !checkEquipmentOwnership(c, waffe.CharacterID) { + return + } + + if err := database.DB.Delete(&waffe).Error; err != nil { respondWithError(c, http.StatusInternalServerError, "Failed to delete Waffe") return } diff --git a/backend/gsmaster/routes.go b/backend/gsmaster/routes.go index 02d8e5a..be9c905 100644 --- a/backend/gsmaster/routes.go +++ b/backend/gsmaster/routes.go @@ -8,50 +8,52 @@ import ( func RegisterRoutes(r *gin.RouterGroup) { maintGrp := r.Group("/maintenance") + + maintGrp.GET("", GetMasterData) + maintGrp.GET("/skills", GetMDSkills) + maintGrp.GET("/skills-enhanced", GetEnhancedMDSkills) // New enhanced endpoint + maintGrp.GET("/skills/:id", GetMDSkill) + maintGrp.GET("/skills-enhanced/:id", GetEnhancedMDSkill) // New enhanced endpoint + maintGrp.GET("/weaponskills", GetMDWeaponSkills) + maintGrp.GET("/weaponskills-enhanced", GetEnhancedMDWeaponSkills) // New enhanced endpoint + maintGrp.GET("/weaponskills/:id", GetMDWeaponSkill) + maintGrp.GET("/weaponskills-enhanced/:id", GetEnhancedMDWeaponSkill) // New enhanced endpoint + maintGrp.GET("/spells", GetMDSpells) + maintGrp.GET("/spells-enhanced", GetEnhancedMDSpells) // New enhanced endpoint + maintGrp.GET("/spells/:id", GetMDSpell) + maintGrp.GET("/spells-enhanced/:id", GetEnhancedMDSpell) // New enhanced endpoint + maintGrp.GET("/equipment", GetMDEquipments) + maintGrp.GET("/equipment-enhanced", GetEnhancedMDEquipment) // New enhanced endpoint + maintGrp.GET("/equipment/:id", GetMDEquipment) + maintGrp.GET("/equipment-enhanced/:id", GetEnhancedMDEquipmentItem) // New enhanced endpoint + maintGrp.GET("/weapons", GetMDWeapons) + maintGrp.GET("/weapons-enhanced", GetEnhancedMDWeapons) // New enhanced endpoint + maintGrp.GET("/weapons/:id", GetMDWeapon) + maintGrp.GET("/weapons-enhanced/:id", GetEnhancedMDWeapon) // New enhanced endpoint + maintGrp.Use(user.RequireMaintainer()) { - maintGrp.GET("", GetMasterData) - maintGrp.GET("/skills", GetMDSkills) - maintGrp.GET("/skills-enhanced", GetEnhancedMDSkills) // New enhanced endpoint maintGrp.POST("/skills-enhanced", CreateEnhancedMDSkill) // Create new skill - maintGrp.GET("/skills/:id", GetMDSkill) - maintGrp.GET("/skills-enhanced/:id", GetEnhancedMDSkill) // New enhanced endpoint maintGrp.PUT("/skills/:id", UpdateMDSkill) maintGrp.PUT("/skills-enhanced/:id", UpdateEnhancedMDSkill) // New enhanced endpoint maintGrp.POST("/skills", AddSkill) maintGrp.DELETE("/skills/:id", DeleteMDSkill) - maintGrp.GET("/weaponskills", GetMDWeaponSkills) - maintGrp.GET("/weaponskills-enhanced", GetEnhancedMDWeaponSkills) // New enhanced endpoint - maintGrp.GET("/weaponskills/:id", GetMDWeaponSkill) - maintGrp.GET("/weaponskills-enhanced/:id", GetEnhancedMDWeaponSkill) // New enhanced endpoint maintGrp.PUT("/weaponskills/:id", UpdateMDWeaponSkill) maintGrp.PUT("/weaponskills-enhanced/:id", UpdateEnhancedMDWeaponSkill) // New enhanced endpoint maintGrp.POST("/weaponskills", AddWeaponSkill) maintGrp.DELETE("/weaponskills/:id", DeleteMDWeaponSkill) - maintGrp.GET("/spells", GetMDSpells) - maintGrp.GET("/spells-enhanced", GetEnhancedMDSpells) // New enhanced endpoint - maintGrp.GET("/spells/:id", GetMDSpell) - maintGrp.GET("/spells-enhanced/:id", GetEnhancedMDSpell) // New enhanced endpoint maintGrp.PUT("/spells/:id", UpdateMDSpell) maintGrp.PUT("/spells-enhanced/:id", UpdateEnhancedMDSpell) // New enhanced endpoint maintGrp.POST("/spells", AddSpell) maintGrp.DELETE("/spells/:id", DeleteMDSpell) - maintGrp.GET("/equipment", GetMDEquipments) - maintGrp.GET("/equipment-enhanced", GetEnhancedMDEquipment) // New enhanced endpoint - maintGrp.GET("/equipment/:id", GetMDEquipment) - maintGrp.GET("/equipment-enhanced/:id", GetEnhancedMDEquipmentItem) // New enhanced endpoint maintGrp.PUT("/equipment/:id", UpdateMDEquipment) maintGrp.PUT("/equipment-enhanced/:id", UpdateEnhancedMDEquipmentItem) // New enhanced endpoint maintGrp.POST("/equipment", AddEquipment) maintGrp.DELETE("/equipment/:id", DeleteMDEquipment) - maintGrp.GET("/weapons", GetMDWeapons) - maintGrp.GET("/weapons-enhanced", GetEnhancedMDWeapons) // New enhanced endpoint - maintGrp.GET("/weapons/:id", GetMDWeapon) - maintGrp.GET("/weapons-enhanced/:id", GetEnhancedMDWeapon) // New enhanced endpoint maintGrp.PUT("/weapons/:id", UpdateMDWeapon) maintGrp.PUT("/weapons-enhanced/:id", UpdateEnhancedMDWeapon) // New enhanced endpoint maintGrp.POST("/weapons", AddWeapon) diff --git a/frontend/src/components/CharacterDetails.vue b/frontend/src/components/CharacterDetails.vue index 3ae6682..275f1b0 100644 --- a/frontend/src/components/CharacterDetails.vue +++ b/frontend/src/components/CharacterDetails.vue @@ -6,7 +6,7 @@ -

{{ $t('char') }}: {{ character.name }} ({{ $t(currentView) }})

@@ -32,7 +32,7 @@ - + @@ -64,6 +64,7 @@