Merge pull request #23 from Bardioc26/public_chars
Show Public chars to everyone users can change a char from private to public and vice versa user can share a private char with other users show own chars in left list and shared chars in right list ensure user can only change own chars
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -59,6 +70,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))
|
||||
@@ -101,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
|
||||
@@ -133,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")
|
||||
@@ -267,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 {
|
||||
@@ -357,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 {
|
||||
@@ -567,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 {
|
||||
@@ -1052,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 {
|
||||
@@ -1225,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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
package character
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"bamort/testutils"
|
||||
"bamort/user"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestOwnershipGuardsBlockMutations(t *testing.T) {
|
||||
testutils.SetupTestEnvironment(t)
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
database.SetupTestDB(true, true)
|
||||
t.Cleanup(database.ResetTestDB)
|
||||
|
||||
require.NoError(t, models.MigrateStructure())
|
||||
|
||||
t.Run("UpdateCharacter blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 101)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
originalName := char.Name
|
||||
|
||||
ctx, w := buildJSONContext(t, http.MethodPut, map[string]any{"name": "New Name"}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
UpdateCharacter(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
reloaded := reloadCharacter(t, char.ID)
|
||||
require.Equal(t, originalName, reloaded.Name)
|
||||
})
|
||||
|
||||
t.Run("DeleteCharacter blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 102)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
|
||||
ctx, w := buildJSONContext(t, http.MethodDelete, nil, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
DeleteCharacter(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
var exists models.Char
|
||||
err := database.DB.First(&exists, char.ID).Error
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("UpdateCharacterExperience blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 103)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
seedExperience(t, char, 25)
|
||||
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"experience_points": 50}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
UpdateCharacterExperience(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
reloaded := reloadCharacterWithPreloads(t, char.ID)
|
||||
require.Equal(t, 25, reloaded.Erfahrungsschatz.EP)
|
||||
})
|
||||
|
||||
t.Run("UpdateCharacterWealth blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 104)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
seedWealth(t, char, 5, 4, 3)
|
||||
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"goldstücke": 50}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
UpdateCharacterWealth(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
reloaded := reloadCharacterWithPreloads(t, char.ID)
|
||||
require.Equal(t, 5, reloaded.Vermoegen.Goldstuecke)
|
||||
require.Equal(t, 4, reloaded.Vermoegen.Silberstuecke)
|
||||
require.Equal(t, 3, reloaded.Vermoegen.Kupferstuecke)
|
||||
})
|
||||
|
||||
t.Run("LearnSkill blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 105)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
before := countSkills(t, char.ID)
|
||||
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"name": "Test Skill", "target_level": 1, "type": "skill"}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
LearnSkill(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
after := countSkills(t, char.ID)
|
||||
require.Equal(t, before, after)
|
||||
})
|
||||
|
||||
t.Run("ImproveSkill blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 106)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
seedSkill(t, char, "Athletik", 1, 0)
|
||||
before := countSkills(t, char.ID)
|
||||
|
||||
payload := map[string]any{
|
||||
"char_id": char.ID,
|
||||
"name": "Athletik",
|
||||
"current_level": 1,
|
||||
"target_level": 2,
|
||||
"type": "skill",
|
||||
"action": "improve",
|
||||
"reward": "default",
|
||||
}
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, nil)
|
||||
|
||||
ImproveSkill(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
after := countSkills(t, char.ID)
|
||||
require.Equal(t, before, after)
|
||||
})
|
||||
|
||||
t.Run("LearnSpell blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 107)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
before := countSpells(t, char.ID)
|
||||
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"name": "Test Spell"}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
LearnSpell(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
after := countSpells(t, char.ID)
|
||||
require.Equal(t, before, after)
|
||||
})
|
||||
|
||||
t.Run("UpdatePracticePoints blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 108)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
seedSkill(t, char, "Menschenkenntnis", 1, 2)
|
||||
before := fetchSkillPp(t, char.ID, "Menschenkenntnis")
|
||||
|
||||
payload := []map[string]any{{"skill_name": "Menschenkenntnis", "amount": 0}}
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
UpdatePracticePoints(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
after := fetchSkillPp(t, char.ID, "Menschenkenntnis")
|
||||
require.Equal(t, before, after)
|
||||
})
|
||||
|
||||
t.Run("AddPracticePoint blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 109)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
seedSkill(t, char, "Athletik", 1, 1)
|
||||
before := fetchSkillPp(t, char.ID, "Athletik")
|
||||
|
||||
payload := map[string]any{"skill_name": "Athletik", "amount": 3}
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
AddPracticePoint(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
after := fetchSkillPp(t, char.ID, "Athletik")
|
||||
require.Equal(t, before, after)
|
||||
})
|
||||
|
||||
t.Run("UsePracticePoint blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 110)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
seedSkill(t, char, "Athletik", 1, 2)
|
||||
before := fetchSkillPp(t, char.ID, "Athletik")
|
||||
|
||||
payload := map[string]any{"skill_name": "Athletik", "amount": 1}
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
UsePracticePoint(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
after := fetchSkillPp(t, char.ID, "Athletik")
|
||||
require.Equal(t, before, after)
|
||||
})
|
||||
|
||||
t.Run("UpdateCharacterShares blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 111)
|
||||
shareTarget := ensureUserExists(t, 112)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
|
||||
payload := map[string]any{"user_ids": []uint{shareTarget.UserID}}
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
UpdateCharacterShares(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
var shares []models.CharShare
|
||||
err := database.DB.Where("character_id = ?", char.ID).Find(&shares).Error
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, shares)
|
||||
})
|
||||
|
||||
t.Run("UpdateCharacterImage blocks non-owner", func(t *testing.T) {
|
||||
owner := ensureUserExists(t, 113)
|
||||
char := createCharacterOwnedBy(t, owner.UserID)
|
||||
char.Image = "initial.png"
|
||||
require.NoError(t, database.DB.Save(&char).Error)
|
||||
|
||||
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"image": "new.png"}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||||
|
||||
UpdateCharacterImage(ctx)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
reloaded := reloadCharacter(t, char.ID)
|
||||
require.Equal(t, "initial.png", reloaded.Image)
|
||||
})
|
||||
}
|
||||
|
||||
func ensureUserExists(t *testing.T, id uint) user.User {
|
||||
var existing user.User
|
||||
err := database.DB.First(&existing, "user_id = ?", id).Error
|
||||
if err == nil {
|
||||
return existing
|
||||
}
|
||||
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
newUser := user.User{
|
||||
UserID: id,
|
||||
Username: fmt.Sprintf("user-%d-%d", id, time.Now().UnixNano()),
|
||||
Email: fmt.Sprintf("user-%d@example.com", id),
|
||||
Role: user.RoleStandardUser,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&newUser).Error)
|
||||
return newUser
|
||||
}
|
||||
|
||||
func createCharacterOwnedBy(t *testing.T, ownerID uint) models.Char {
|
||||
char := models.Char{
|
||||
BamortBase: models.BamortBase{Name: fmt.Sprintf("Char-%d", time.Now().UnixNano())},
|
||||
UserID: ownerID,
|
||||
Typ: "Krieger",
|
||||
Rasse: "Mensch",
|
||||
Grad: 1,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&char).Error)
|
||||
return char
|
||||
}
|
||||
|
||||
func seedExperience(t *testing.T, char models.Char, ep int) {
|
||||
exp := models.Erfahrungsschatz{
|
||||
BamortCharTrait: models.BamortCharTrait{CharacterID: char.ID, UserID: char.UserID},
|
||||
EP: ep,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&exp).Error)
|
||||
}
|
||||
|
||||
func seedWealth(t *testing.T, char models.Char, gold, silver, copper int) {
|
||||
wealth := models.Vermoegen{
|
||||
BamortCharTrait: models.BamortCharTrait{CharacterID: char.ID, UserID: char.UserID},
|
||||
Goldstuecke: gold,
|
||||
Silberstuecke: silver,
|
||||
Kupferstuecke: copper,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&wealth).Error)
|
||||
}
|
||||
|
||||
func seedSkill(t *testing.T, char models.Char, name string, level, pp int) {
|
||||
skill := models.SkFertigkeit{
|
||||
BamortCharTrait: models.BamortCharTrait{
|
||||
BamortBase: models.BamortBase{Name: name},
|
||||
CharacterID: char.ID,
|
||||
UserID: char.UserID,
|
||||
},
|
||||
Fertigkeitswert: level,
|
||||
Pp: pp,
|
||||
Improvable: true,
|
||||
Category: "Test",
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&skill).Error)
|
||||
}
|
||||
|
||||
func countSkills(t *testing.T, charID uint) int {
|
||||
var skills []models.SkFertigkeit
|
||||
require.NoError(t, database.DB.Where("character_id = ?", charID).Find(&skills).Error)
|
||||
return len(skills)
|
||||
}
|
||||
|
||||
func countSpells(t *testing.T, charID uint) int {
|
||||
var spells []models.SkZauber
|
||||
require.NoError(t, database.DB.Where("character_id = ?", charID).Find(&spells).Error)
|
||||
return len(spells)
|
||||
}
|
||||
|
||||
func fetchSkillPp(t *testing.T, charID uint, name string) int {
|
||||
var skill models.SkFertigkeit
|
||||
err := database.DB.Where("character_id = ? AND name = ?", charID, name).First(&skill).Error
|
||||
require.NoError(t, err)
|
||||
return skill.Pp
|
||||
}
|
||||
|
||||
func reloadCharacter(t *testing.T, id uint) models.Char {
|
||||
var char models.Char
|
||||
require.NoError(t, database.DB.First(&char, id).Error)
|
||||
return char
|
||||
}
|
||||
|
||||
func reloadCharacterWithPreloads(t *testing.T, id uint) models.Char {
|
||||
var char models.Char
|
||||
require.NoError(t, database.DB.Preload("Erfahrungsschatz").Preload("Vermoegen").First(&char, id).Error)
|
||||
return char
|
||||
}
|
||||
|
||||
func buildJSONContext(t *testing.T, method string, body any, userID uint, params map[string]string) (*gin.Context, *httptest.ResponseRecorder) {
|
||||
var buf bytes.Buffer
|
||||
if body != nil {
|
||||
require.NoError(t, json.NewEncoder(&buf).Encode(body))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, "/", &buf)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = req
|
||||
ctx.Set("userID", userID)
|
||||
|
||||
for k, v := range params {
|
||||
ctx.Params = append(ctx.Params, gin.Param{Key: k, Value: v})
|
||||
}
|
||||
|
||||
return ctx, w
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -10,10 +10,17 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
||||
charGrp.POST("", CreateCharacter)
|
||||
charGrp.GET("/:id", GetCharacter)
|
||||
charGrp.PUT("/:id", UpdateCharacter)
|
||||
charGrp.PATCH("/:id", UpdateCharacter)
|
||||
|
||||
charGrp.DELETE("/:id", DeleteCharacter)
|
||||
charGrp.PUT("/:id/image", UpdateCharacterImage)
|
||||
charGrp.GET("/:id/datasheet-options", GetDatasheetOptions)
|
||||
|
||||
// Character Sharing
|
||||
charGrp.GET("/:id/shares", GetCharacterShares)
|
||||
charGrp.PUT("/:id/shares", UpdateCharacterShares)
|
||||
charGrp.GET("/:id/available-users", GetAvailableUsersForSharing)
|
||||
|
||||
// Erfahrung und Vermögen
|
||||
charGrp.GET("/:id/experience-wealth", GetCharacterExperienceAndWealth) // NewSystem
|
||||
charGrp.PUT("/:id/experience", UpdateCharacterExperience) // NewSystem
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package character
|
||||
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"bamort/user"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetCharacterShares returns the list of users a character is shared with
|
||||
func GetCharacterShares(c *gin.Context) {
|
||||
charID := c.Param("id")
|
||||
|
||||
var character models.Char
|
||||
if err := character.FirstID(charID); err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
if !checkCharacterOwnership(c, &character) {
|
||||
return
|
||||
}
|
||||
|
||||
var shares []models.CharShare
|
||||
if err := database.DB.Where("character_id = ?", character.ID).Find(&shares).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve shares")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user details for each share
|
||||
type ShareWithUser struct {
|
||||
models.CharShare
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
var sharesWithUsers []ShareWithUser
|
||||
for _, share := range shares {
|
||||
var u user.User
|
||||
if err := u.FirstId(share.UserID); err == nil {
|
||||
sharesWithUsers = append(sharesWithUsers, ShareWithUser{
|
||||
CharShare: share,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, sharesWithUsers)
|
||||
}
|
||||
|
||||
// UpdateCharacterShares updates the list of users a character is shared with
|
||||
func UpdateCharacterShares(c *gin.Context) {
|
||||
charID := c.Param("id")
|
||||
|
||||
var character models.Char
|
||||
if err := character.FirstID(charID); err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
if !checkCharacterOwnership(c, &character) {
|
||||
return
|
||||
}
|
||||
|
||||
type UpdateSharesRequest struct {
|
||||
UserIDs []uint `json:"user_ids" binding:"required"`
|
||||
}
|
||||
|
||||
var request UpdateSharesRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Delete existing shares
|
||||
if err := database.DB.Where("character_id = ?", character.ID).Delete(&models.CharShare{}).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to delete existing shares")
|
||||
return
|
||||
}
|
||||
|
||||
// Create new shares
|
||||
for _, userID := range request.UserIDs {
|
||||
// Don't share with yourself
|
||||
if userID == character.UserID {
|
||||
continue
|
||||
}
|
||||
|
||||
share := models.CharShare{
|
||||
CharacterID: character.ID,
|
||||
UserID: userID,
|
||||
Permission: "read",
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&share).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to create share")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Shares updated successfully"})
|
||||
}
|
||||
|
||||
// GetAvailableUsersForSharing returns a list of users (excluding the owner)
|
||||
func GetAvailableUsersForSharing(c *gin.Context) {
|
||||
charID := c.Param("id")
|
||||
|
||||
var character models.Char
|
||||
if err := character.FirstID(charID); err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
if !checkCharacterOwnership(c, &character) {
|
||||
return
|
||||
}
|
||||
|
||||
var users []user.User
|
||||
if err := database.DB.Where("user_id != ?", character.UserID).Find(&users).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve users")
|
||||
return
|
||||
}
|
||||
|
||||
// Remove sensitive data
|
||||
type UserInfo struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
var userInfos []UserInfo
|
||||
for _, u := range users {
|
||||
userInfos = append(userInfos, UserInfo{
|
||||
UserID: u.UserID,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, userInfos)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package config
|
||||
|
||||
// Version is the application version
|
||||
const Version = "0.2.0"
|
||||
const Version = "0.2.1"
|
||||
|
||||
var (
|
||||
// GitCommit will be set by build flags or detected at runtime
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ func TestCreateAusruestung(t *testing.T) {
|
||||
BamortBase: models.BamortBase{
|
||||
Name: "Test Sword",
|
||||
},
|
||||
CharacterID: 1,
|
||||
UserID: 1,
|
||||
CharacterID: 21,
|
||||
UserID: 4,
|
||||
},
|
||||
Magisch: models.Magisch{
|
||||
IstMagisch: false,
|
||||
@@ -111,13 +111,17 @@ func TestCreateAusruestung(t *testing.T) {
|
||||
{
|
||||
name: "Empty JSON",
|
||||
payload: map[string]interface{}{},
|
||||
expectedStatus: http.StatusCreated,
|
||||
expectedStatus: http.StatusNotFound,
|
||||
shouldContain: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// u := user.User{}
|
||||
// u.FirstId(1)
|
||||
// token := user.GenerateToken(&u)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
@@ -129,9 +133,12 @@ func TestCreateAusruestung(t *testing.T) {
|
||||
body = *bytes.NewBuffer(jsonData)
|
||||
}
|
||||
|
||||
c.Set("userID", uint(4)) // Simulate logged-in user with ID 4
|
||||
c.Request = httptest.NewRequest("POST", "/ausruestung", &body)
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
//c.Request.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
CreateAusruestung(c)
|
||||
|
||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||
@@ -235,8 +242,8 @@ func TestUpdateAusruestung(t *testing.T) {
|
||||
BamortBase: models.BamortBase{
|
||||
Name: "Original Equipment",
|
||||
},
|
||||
CharacterID: 123,
|
||||
UserID: 1,
|
||||
CharacterID: 21,
|
||||
UserID: 4,
|
||||
},
|
||||
Magisch: models.Magisch{
|
||||
IstMagisch: false,
|
||||
@@ -297,6 +304,8 @@ func TestUpdateAusruestung(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
c.Set("userID", uint(4))
|
||||
|
||||
c.Params = gin.Params{
|
||||
{Key: "ausruestung_id", Value: tt.ausruestungID},
|
||||
}
|
||||
@@ -332,8 +341,8 @@ func TestDeleteAusruestung(t *testing.T) {
|
||||
BamortBase: models.BamortBase{
|
||||
Name: "Equipment to Delete",
|
||||
},
|
||||
CharacterID: 123,
|
||||
UserID: 1,
|
||||
CharacterID: 21,
|
||||
UserID: 4,
|
||||
},
|
||||
Magisch: models.Magisch{
|
||||
IstMagisch: false,
|
||||
@@ -368,14 +377,14 @@ func TestDeleteAusruestung(t *testing.T) {
|
||||
{
|
||||
name: "Non-existent Ausruestung",
|
||||
ausruestungID: "999",
|
||||
expectedStatus: http.StatusOK, // GORM doesn't fail on deleting non-existent records
|
||||
shouldContain: "deleted successfully",
|
||||
expectedStatus: http.StatusNotFound, // GORM doesn't fail on deleting non-existent records
|
||||
shouldContain: "Ausruestung not found",
|
||||
},
|
||||
{
|
||||
name: "Invalid Ausruestung ID",
|
||||
ausruestungID: "invalid",
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
shouldContain: "error",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
shouldContain: "Ausruestung not found",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -383,11 +392,11 @@ func TestDeleteAusruestung(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Set("userID", uint(4))
|
||||
|
||||
c.Params = gin.Params{
|
||||
{Key: "ausruestung_id", Value: tt.ausruestungID},
|
||||
}
|
||||
|
||||
DeleteAusruestung(c)
|
||||
|
||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||
|
||||
+23
-21
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func SetupGin(r *gin.Engine) {
|
||||
r.Use(cors.New(cors.Config{
|
||||
//AllowOrigins: []string{"http://localhost:3000"}, // Replace with your frontend's URL
|
||||
AllowOrigins: allowedOrigins,
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Frontend Version Management
|
||||
|
||||
## Current Version: 0.2.0
|
||||
## Current Version: 0.2.1
|
||||
|
||||
The frontend version is managed independently from the backend.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bamort-frontend",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"type": "module",
|
||||
|
||||
@@ -210,12 +210,14 @@ a:focus {
|
||||
}
|
||||
|
||||
/* Global fullwidth page class for view components */
|
||||
/*
|
||||
.fullwidth-page {
|
||||
/*width: 100%;
|
||||
width: 100%;
|
||||
min-height: calc(100vh - 120px);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;*/
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Common Card Layouts */
|
||||
.card {
|
||||
@@ -2544,7 +2546,7 @@ a:focus {
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
position: absolute;
|
||||
/*position: absolute;*/
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
@@ -3999,10 +4001,15 @@ a:focus {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
row-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.dragon-container {
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dragon-image {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<button @click="showExportDialog = true" class="export-button-small" :title="$t('export.title')">
|
||||
📄
|
||||
</button>
|
||||
<button v-if="isOwner" @click="showVisibilityDialog = true" class="export-button-small" :title="$t('visibility.title')">
|
||||
{{ character.public ? '🌐' : '🔒' }}
|
||||
</button>
|
||||
<h2>{{ $t('char') }}: {{ character.name }} ({{ $t(currentView) }})</h2>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,9 +21,18 @@
|
||||
@export-success="handleExportSuccess"
|
||||
/>
|
||||
|
||||
<!-- Visibility Dialog -->
|
||||
<VisibilityDialog
|
||||
:characterId="id"
|
||||
:currentVisibility="character.public"
|
||||
:showDialog="showVisibilityDialog"
|
||||
@update:showDialog="showVisibilityDialog = $event"
|
||||
@visibility-updated="handleVisibilityUpdated"
|
||||
/>
|
||||
|
||||
<!-- Submenu Content -->
|
||||
<!-- <div class="character-aspect"> -->
|
||||
<component :is="currentView" :character="character" @character-updated="refreshCharacter"/>
|
||||
<component :is="currentView" :character="character" :isOwner="isOwner" @character-updated="refreshCharacter"/>
|
||||
<!-- </div> -->
|
||||
|
||||
<!-- Submenu -->
|
||||
@@ -52,7 +64,9 @@
|
||||
|
||||
<script>
|
||||
import API from '../utils/api'
|
||||
import { useUserStore } from '../stores/userStore'
|
||||
import ExportDialog from "./ExportDialog.vue";
|
||||
import VisibilityDialog from "./VisibilityDialog.vue";
|
||||
import DatasheetView from "./DatasheetView.vue"; // Component for character stats
|
||||
import SkillView from "./SkillView.vue"; // Component for character history
|
||||
import WeaponView from "./WeaponView.vue"; // Component for character history
|
||||
@@ -67,6 +81,7 @@ export default {
|
||||
props: ["id"], // Receive the route parameter as a prop
|
||||
components: {
|
||||
ExportDialog,
|
||||
VisibilityDialog,
|
||||
DatasheetView,
|
||||
SkillView,
|
||||
WeaponView,
|
||||
@@ -81,6 +96,7 @@ export default {
|
||||
currentView: "DatasheetView", // Default view
|
||||
lastView: "DatasheetView",
|
||||
showExportDialog: false,
|
||||
showVisibilityDialog: false,
|
||||
menus: [
|
||||
{ id: 1, name: "Datasheet", component: "DatasheetView" },
|
||||
{ id: 2, name: "Skill", component: "SkillView" },
|
||||
@@ -96,6 +112,12 @@ export default {
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isOwner() {
|
||||
const userStore = useUserStore()
|
||||
return userStore.currentUser && this.character.user_id === userStore.currentUser.id
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await API.get(`/api/characters/${this.id}`, {
|
||||
@@ -108,6 +130,12 @@ export default {
|
||||
console.log('PDF exported successfully')
|
||||
},
|
||||
|
||||
handleVisibilityUpdated(isPublic) {
|
||||
this.character.public = isPublic
|
||||
this.showVisibilityDialog = false
|
||||
console.log('Character visibility updated to:', isPublic ? 'public' : 'private')
|
||||
},
|
||||
|
||||
changeView(view) {
|
||||
this.lastView = this.currentView;
|
||||
this.currentView = view;
|
||||
|
||||
@@ -16,29 +16,50 @@
|
||||
@delete-session="handleDeleteSession"
|
||||
/>
|
||||
|
||||
<div v-if="characters.length === 0" class="empty-state">
|
||||
<div v-if="ownedCharacters.length === 0" class="empty-state">
|
||||
<h3>{{ $t('characters.list.no_characters') }}</h3>
|
||||
<p>{{ $t('characters.list.no_characters_description') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="list-container">
|
||||
<div v-for="character in characters" :key="character.character_id" class="list-item">
|
||||
<router-link :to="`/character/${character.id}`" class="list-item-content">
|
||||
<h4 class="list-item-title">{{ character.name }}</h4>
|
||||
<div class="list-item-details">
|
||||
{{ character.rasse }} <span class="list-item-separator">|</span>
|
||||
{{ character.typ }} <span class="list-item-separator">|</span>
|
||||
{{ $t('characters.list.grade') }}: {{ character.grad }} <span class="list-item-separator">|</span>
|
||||
{{ $t('characters.list.owner') }}: {{ character.owner }} <span class="list-item-separator">|</span>
|
||||
<span class="badge" :class="character.public ? 'badge-success' : 'badge-secondary'">
|
||||
{{ character.public ? $t('characters.list.public') : $t('characters.list.private') }}
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div v-else class="list-container horizontal-placement">
|
||||
<div class="charlist">
|
||||
<div class="charlist-header">{{ $t('characters.list.owned_characters_title') }}</div>
|
||||
<div v-for="character in ownedCharacters" :key="character.character_id" class="list-item">
|
||||
<router-link :to="`/character/${character.id}`" class="list-item-content">
|
||||
<h4 class="list-item-title">{{ character.name }}</h4>
|
||||
<div class="list-item-details">
|
||||
{{ character.rasse }} <span class="list-item-separator">|</span>
|
||||
{{ character.typ }} <span class="list-item-separator">|</span>
|
||||
{{ $t('characters.list.grade') }}: {{ character.grad }} <span class="list-item-separator">|</span>
|
||||
{{ $t('characters.list.owner') }}: {{ character.owner }} <span class="list-item-separator">|</span>
|
||||
<span class="badge" :class="character.public ? 'badge-success' : 'badge-secondary'">
|
||||
{{ character.public ? $t('characters.list.public') : $t('characters.list.private') }}
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="charlist">
|
||||
<div class="charlist-header">{{ $t('characters.list.shared_characters_title') }}</div>
|
||||
<div v-for="character in sharedCharacters" :key="character.character_id" class="list-item">
|
||||
<router-link :to="`/character/${character.id}`" class="list-item-content">
|
||||
<h4 class="list-item-title">{{ character.name }}</h4>
|
||||
<div class="list-item-details">
|
||||
{{ character.rasse }} <span class="list-item-separator">|</span>
|
||||
{{ character.typ }} <span class="list-item-separator">|</span>
|
||||
{{ $t('characters.list.grade') }}: {{ character.grad }} <span class="list-item-separator">|</span>
|
||||
{{ $t('characters.list.owner') }}: {{ character.owner }} <span class="list-item-separator">|</span>
|
||||
<span class="badge" :class="character.public ? 'badge-success' : 'badge-secondary'">
|
||||
{{ character.public ? $t('characters.list.public') : $t('characters.list.private') }}
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template><script>
|
||||
</template>
|
||||
<script>
|
||||
import API from '../utils/api'
|
||||
import { formatDate } from '@/utils/dateUtils'
|
||||
import CharacterCreationSessions from './CharacterCreationSessions.vue'
|
||||
@@ -49,7 +70,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
characters: [],
|
||||
ownedCharacters: [],
|
||||
sharedCharacters: [],
|
||||
creationSessions: [],
|
||||
}
|
||||
},
|
||||
@@ -64,7 +86,8 @@ export default {
|
||||
const response = await API.get('/api/characters', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
this.characters = response.data.self_owned
|
||||
this.ownedCharacters = response.data.self_owned
|
||||
this.sharedCharacters = response.data.others
|
||||
} catch (error) {
|
||||
console.error('Error loading characters:', error)
|
||||
}
|
||||
@@ -132,7 +155,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
/* All common styles moved to main.css */
|
||||
|
||||
.create-character-section {
|
||||
@@ -166,6 +189,32 @@ export default {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.horizontal-placement {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.charlist {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 15px - 300px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.charlist-header {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 12px 20px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.list-item {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<div class="cd-view">
|
||||
<p>Are you sure you want to delete {{ character.name }}?</p>
|
||||
<button @click="deleteCharacter" class="btn btn-danger">Yes, Delete</button>
|
||||
<button @click="$emit('cancel')" class="btn btn-secondary">Cancel</button>
|
||||
<div v-if="!isOwner" class="error-message">
|
||||
<p>{{ $t('deleteChar.notAuthorized') }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Are you sure you want to delete {{ character.name }}?</p>
|
||||
<button @click="deleteCharacter" class="btn btn-danger">Yes, Delete</button>
|
||||
<button @click="$emit('cancel')" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +21,10 @@ export default {
|
||||
character: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isOwner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="fullwidth-container">
|
||||
<div class="header-section">
|
||||
<h2>{{ $t('EquipmentView') }}</h2>
|
||||
<button @click="openAddEquipmentDialog" class="btn-add-equipment">
|
||||
<button v-if="isOwner" @click="openAddEquipmentDialog" class="btn-add-equipment">
|
||||
{{ $t('equipment.add') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@
|
||||
<th>{{ $t('equipment.amount') }}</th>
|
||||
<th>{{ $t('equipment.contained_in') }}</th>
|
||||
<th>{{ $t('equipment.bonus') }}</th>
|
||||
<th>{{ $t('equipment.actions') }}</th>
|
||||
<th v-if="isOwner">{{ $t('equipment.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -31,7 +31,7 @@
|
||||
<td>{{ equipment.anzahl || '-' }}</td>
|
||||
<td>{{ equipment.beinhaltet_in || '-' }}</td>
|
||||
<td>{{ equipment.bonus || '-' }}</td>
|
||||
<td class="action-cell">
|
||||
<td v-if="isOwner" class="action-cell">
|
||||
<button @click="deleteEquipment(equipment)" class="btn-delete" title="Löschen">
|
||||
🗑️
|
||||
</button>
|
||||
@@ -126,6 +126,24 @@
|
||||
.cd-table th {
|
||||
background-color: #1da766;
|
||||
}
|
||||
|
||||
/* Fix modal footer visibility */
|
||||
.modal-fullscreen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 50px);
|
||||
}
|
||||
|
||||
.modal-fullscreen .modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.modal-fullscreen .modal-header,
|
||||
.modal-fullscreen .modal-footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -137,6 +155,10 @@ name: "EquipmentView",
|
||||
character: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isOwner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="resource-amount">{{ character.erfahrungsschatz?.ep || 0 }} EP</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row control-row">
|
||||
<div v-if="isOwner" class="form-row control-row">
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model.number="experienceAmount"
|
||||
@@ -51,7 +51,7 @@
|
||||
<div class="resource-amount">{{ character.vermoegen?.goldstücke || 0 }} GS</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row control-row">
|
||||
<div v-if="isOwner" class="form-row control-row">
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model.number="goldAmount"
|
||||
@@ -147,6 +147,10 @@ export default {
|
||||
character: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isOwner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="table-wrapper-left">
|
||||
<div class="header-section">
|
||||
<!-- Lernmodus Toggle Button -->
|
||||
<div class="learning-mode-controls">
|
||||
<div v-if="isOwner" class="learning-mode-controls">
|
||||
<!-- Ressourcen-Anzeige (nur sichtbar wenn Lernmodus aktiv) -->
|
||||
<div v-if="learningMode" class="resources-display">
|
||||
<div class="resource-item">
|
||||
@@ -68,7 +68,7 @@
|
||||
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
||||
<td>{{ skill.bonus || '0' }}</td>
|
||||
<td class="pp-cell">
|
||||
<div class="pp-container">
|
||||
<div v-if="isOwner" class="pp-container">
|
||||
<button
|
||||
@click="decreasePP(skill)"
|
||||
class="pp-btn pp-btn-minus"
|
||||
@@ -86,6 +86,7 @@
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<span v-else>{{ skill.pp || '0' }}</span>
|
||||
</td>
|
||||
<td>{{ skill.bemerkung || '-' }}</td>
|
||||
<td v-if="learningMode" class="action-cell">
|
||||
@@ -109,7 +110,7 @@
|
||||
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
||||
<td>{{ skill.bonus || '0' }}</td>
|
||||
<td class="pp-cell">
|
||||
<div class="pp-container">
|
||||
<div v-if="isOwner" class="pp-container">
|
||||
<button
|
||||
@click="decreaseWeaponPP(skill)"
|
||||
class="pp-btn pp-btn-minus"
|
||||
@@ -127,6 +128,7 @@
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<span v-else>{{ skill.pp || '0' }}</span>
|
||||
</td>
|
||||
<td>{{ skill.bemerkung || '-' }}</td>
|
||||
<td v-if="learningMode" class="action-cell">
|
||||
@@ -290,6 +292,10 @@ export default {
|
||||
character: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isOwner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="fullwidth-container">
|
||||
<!-- Header mit Lernmodus-Kontrollen -->
|
||||
<div class="page-header header-section">
|
||||
<div v-if="isOwner" class="page-header header-section">
|
||||
<div class="learning-mode-controls">
|
||||
<!-- Lernmodus Toggle Button -->
|
||||
<button
|
||||
@@ -99,6 +99,10 @@ export default {
|
||||
character: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isOwner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
<template>
|
||||
<div v-if="showDialog" class="modal-overlay" @click.self="closeDialog">
|
||||
<div class="modal-content modal-large">
|
||||
<div class="modal-header">
|
||||
<h3>{{ $t('visibility.title') }}</h3>
|
||||
<button @click="closeDialog" class="close-button">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div v-if="isUpdating" class="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
<p>{{ $t('visibility.updating') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Visibility Options -->
|
||||
<div class="form-group">
|
||||
<p>{{ $t('visibility.description') }}</p>
|
||||
<div class="visibility-options">
|
||||
<label class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
:value="false"
|
||||
v-model="isPublic"
|
||||
:disabled="isUpdating"
|
||||
>
|
||||
<div class="option-content">
|
||||
<span class="option-label">{{ $t('visibility.private') }}</span>
|
||||
<span class="option-description">{{ $t('visibility.privateDescription') }}</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
:value="true"
|
||||
v-model="isPublic"
|
||||
:disabled="isUpdating"
|
||||
>
|
||||
<div class="option-content">
|
||||
<span class="option-label">{{ $t('visibility.public') }}</span>
|
||||
<span class="option-description">{{ $t('visibility.publicDescription') }}</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share with Specific Users Section -->
|
||||
<div class="form-group">
|
||||
<h4>{{ $t('visibility.shareWithUsers') }}</h4>
|
||||
<p class="section-description">{{ $t('visibility.shareDescription') }}</p>
|
||||
|
||||
<div class="share-sections-container">
|
||||
|
||||
<!-- Add Users Section -->
|
||||
<div class="add-users-section">
|
||||
<h5>{{ $t('visibility.addUsers') }}</h5>
|
||||
<div class="user-search">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="$t('visibility.searchUsers')"
|
||||
class="form-control"
|
||||
:disabled="isUpdating"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingUsers" class="loading">{{ $t('visibility.loadingUsers') }}</div>
|
||||
|
||||
<div v-else-if="filteredAvailableUsers.length > 0" class="available-users-list">
|
||||
<div
|
||||
v-for="user in filteredAvailableUsers"
|
||||
:key="user.user_id"
|
||||
class="user-item"
|
||||
@click="toggleUser(user.user_id)"
|
||||
>
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ user.username }}</span>
|
||||
<span class="user-email">{{ user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isLoadingUsers && availableUsers.length === 0" class="no-users">
|
||||
{{ $t('visibility.noOtherUsers') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="no-users">
|
||||
{{ $t('visibility.noMatchingUsers') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Currently Shared Users -->
|
||||
<div class="shared-users-list">
|
||||
<h5>{{ $t('visibility.currentlySharedWith') }}</h5>
|
||||
<div v-if="sharedUserIds.length > 0" class="shared-users-items">
|
||||
<div
|
||||
v-for="userId in sharedUserIds"
|
||||
:key="userId"
|
||||
class="user-item shared-user"
|
||||
>
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ getUserName(userId) }}</span>
|
||||
<span class="user-email">{{ getUserEmail(userId) }}</span>
|
||||
</div>
|
||||
<button @click="removeUser(userId)" class="remove-btn" :disabled="isUpdating">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-users">
|
||||
{{ $t('visibility.noSharedUsers') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="closeDialog" class="btn-cancel" :disabled="isUpdating">
|
||||
{{ $t('visibility.cancel') }}
|
||||
</button>
|
||||
<button @click="updateVisibilityAndShares" class="btn-primary" :disabled="isUpdating">
|
||||
<span v-if="!isUpdating">{{ $t('visibility.save') }}</span>
|
||||
<span v-else>{{ $t('visibility.saving') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import API from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: "VisibilityDialog",
|
||||
props: {
|
||||
characterId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
currentVisibility: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showDialog: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isPublic: false,
|
||||
isUpdating: false,
|
||||
isLoadingUsers: false,
|
||||
availableUsers: [],
|
||||
sharedUserIds: [],
|
||||
searchQuery: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredAvailableUsers() {
|
||||
let users = this.availableUsers.filter(user => !this.sharedUserIds.includes(user.user_id))
|
||||
|
||||
if (!this.searchQuery) {
|
||||
return users
|
||||
}
|
||||
const query = this.searchQuery.toLowerCase()
|
||||
return users.filter(user =>
|
||||
user.username.toLowerCase().includes(query) ||
|
||||
user.email.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentVisibility: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
this.isPublic = newValue
|
||||
}
|
||||
},
|
||||
showDialog(newValue) {
|
||||
if (newValue) {
|
||||
this.isPublic = this.currentVisibility
|
||||
this.loadAvailableUsers()
|
||||
this.loadCurrentShares()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadAvailableUsers() {
|
||||
this.isLoadingUsers = true
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await API.get(`/api/characters/${this.characterId}/available-users`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
this.availableUsers = response.data || []
|
||||
} catch (error) {
|
||||
console.error('Failed to load available users:', error)
|
||||
} finally {
|
||||
this.isLoadingUsers = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadCurrentShares() {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await API.get(`/api/characters/${this.characterId}/shares`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
this.sharedUserIds = (response.data || []).map(share => share.user_id)
|
||||
} catch (error) {
|
||||
console.error('Failed to load current shares:', error)
|
||||
this.sharedUserIds = []
|
||||
}
|
||||
},
|
||||
|
||||
toggleUser(userId) {
|
||||
const index = this.sharedUserIds.indexOf(userId)
|
||||
if (index > -1) {
|
||||
this.sharedUserIds.splice(index, 1)
|
||||
} else {
|
||||
this.sharedUserIds.push(userId)
|
||||
}
|
||||
},
|
||||
|
||||
removeUser(userId) {
|
||||
const index = this.sharedUserIds.indexOf(userId)
|
||||
if (index > -1) {
|
||||
this.sharedUserIds.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
getUserName(userId) {
|
||||
const user = this.availableUsers.find(u => u.user_id === userId)
|
||||
return user ? user.username : 'Unknown'
|
||||
},
|
||||
|
||||
getUserEmail(userId) {
|
||||
const user = this.availableUsers.find(u => u.user_id === userId)
|
||||
return user ? user.email : ''
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
if (!this.isUpdating) {
|
||||
this.searchQuery = ''
|
||||
this.$emit('update:showDialog', false)
|
||||
}
|
||||
},
|
||||
|
||||
async updateVisibilityAndShares() {
|
||||
this.isUpdating = true
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// Update visibility
|
||||
await API.patch(`/api/characters/${this.characterId}`,
|
||||
{ public: this.isPublic },
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
)
|
||||
|
||||
// Update shares
|
||||
await API.put(`/api/characters/${this.characterId}/shares`,
|
||||
{ user_ids: this.sharedUserIds },
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
)
|
||||
|
||||
this.$emit('visibility-updated', this.isPublic)
|
||||
this.closeDialog()
|
||||
} catch (error) {
|
||||
console.error('Failed to update character visibility/shares:', error)
|
||||
alert(this.$t('visibility.updateError') + ': ' + (error.response?.data?.error || error.message))
|
||||
} finally {
|
||||
this.isUpdating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.visibility-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 15px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.radio-option:hover {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.radio-option input[type="radio"] {
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.option-description {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.radio-option input[type="radio"]:checked + .option-content .option-label {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.radio-option:has(input[type="radio"]:checked) {
|
||||
border-color: #007bff;
|
||||
background-color: #e7f3ff;
|
||||
}
|
||||
|
||||
.modal-large {
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.share-sections-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.shared-users-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.shared-users-list h5 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.shared-users-items {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.user-item.shared-user {
|
||||
background: white;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.user-item.shared-user:hover {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.user-item.shared-user .remove-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #dc3545;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0 8px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-item.shared-user .remove-btn:hover {
|
||||
color: #a71d2a;
|
||||
}
|
||||
|
||||
.user-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-chip .remove-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #dc3545;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.user-chip .remove-btn:hover {
|
||||
color: #a71d2a;
|
||||
}
|
||||
|
||||
.add-users-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.add-users-section h5 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.user-search {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.available-users-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.user-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.user-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.user-item.selected {
|
||||
background: #e7f3ff;
|
||||
border-left: 3px solid #007bff;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #28a745;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.no-users {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="cd-view">
|
||||
<div class="header-section">
|
||||
<h2>{{ $t('WeaponView') }}</h2>
|
||||
<button @click="openAddWeaponDialog" class="btn-add-weapon">
|
||||
<button v-if="isOwner" @click="openAddWeaponDialog" class="btn-add-weapon">
|
||||
{{ $t('weapon.add') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@
|
||||
<th>{{ $t('weapon.amount') }}</th>
|
||||
<th>{{ $t('weapon.contained_in') }}</th>
|
||||
<th>{{ $t('weapon.bonus') }}</th>
|
||||
<th>{{ $t('weapon.actions') }}</th>
|
||||
<th v-if="isOwner">{{ $t('weapon.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -31,7 +31,7 @@
|
||||
<td>{{ weapon.anzahl || '-' }}</td>
|
||||
<td>{{ weapon.beinhaltet_in || '-' }}</td>
|
||||
<td>{{ weapon.anb || '-' }}/{{ weapon.abwb || '-' }}</td>
|
||||
<td class="action-cell">
|
||||
<td v-if="isOwner" class="action-cell">
|
||||
<button @click="editWeapon(weapon)" class="btn-edit" title="Bearbeiten">
|
||||
✏️
|
||||
</button>
|
||||
@@ -378,6 +378,10 @@ name: "WeaponView",
|
||||
character: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isOwner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
+28
-1
@@ -86,6 +86,9 @@ export default {
|
||||
Characters:'Charaktere',
|
||||
Admin:'Administration',
|
||||
},
|
||||
deleteChar: {
|
||||
notAuthorized: 'Sie sind nicht berechtigt, diesen Charakter zu löschen. Nur der Besitzer kann diese Aktion durchführen.'
|
||||
},
|
||||
landing:{
|
||||
title:'BaMoRT - Charakterverwaltung für mein Lieblingsrollenspielsystem',
|
||||
description:'BaMoRT ist ein Werkzeug zur Charakterverwaltung für Rollenspiele. Es bietet Funktionen zur Charaktererstellung, -entwicklung und -verwaltung mit Unterstützung für Fertigkeiten, Zauber, Ausrüstung und mehr. Viele Ausrüstungsteile, Fertikeiten und Zauber fehlen noch, da das Projekt noch in der Entwicklung ist.',
|
||||
@@ -313,7 +316,9 @@ export default {
|
||||
public: 'Öffentlich',
|
||||
private: 'Privat',
|
||||
grade: 'Grad',
|
||||
owner: 'Besitzer'
|
||||
owner: 'Besitzer',
|
||||
shared_characters_title: 'Geteilte Charaktere',
|
||||
owned_characters_title: 'Eigene Charaktere'
|
||||
},
|
||||
create: {
|
||||
spells: {
|
||||
@@ -511,6 +516,28 @@ 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',
|
||||
shareWithUsers: 'Mit bestimmten Benutzern teilen',
|
||||
shareDescription: 'Wählen Sie Benutzer aus, die diesen Charakter sehen können',
|
||||
currentlySharedWith: 'Derzeit geteilt mit',
|
||||
addUsers: 'Benutzer hinzufügen',
|
||||
searchUsers: 'Benutzer suchen...',
|
||||
loadingUsers: 'Lade Benutzer...',
|
||||
noOtherUsers: 'Keine anderen Benutzer verfügbar',
|
||||
noMatchingUsers: 'Keine Benutzer entsprechen Ihrer Suche',
|
||||
noSharedUsers: 'Noch nicht mit Benutzern geteilt'
|
||||
},
|
||||
userManagement: {
|
||||
title: 'Benutzerverwaltung',
|
||||
loading: 'Lade Benutzer...',
|
||||
|
||||
+28
-1
@@ -85,6 +85,9 @@ export default {
|
||||
Characters:'Characters',
|
||||
Admin:'Administration',
|
||||
},
|
||||
deleteChar: {
|
||||
notAuthorized: 'You are not authorized to delete this character. Only the owner can perform this action.'
|
||||
},
|
||||
landing:{
|
||||
title:'BaMoRT - Character Management for Role-Playing Games',
|
||||
description:'BaMoRT is a modern character management tool for role-playing games. It provides comprehensive features for character creation, development, and management with support for skills, spells, equipment, and more.',
|
||||
@@ -309,7 +312,9 @@ export default {
|
||||
public: 'Public',
|
||||
private: 'Private',
|
||||
grade: 'Grade',
|
||||
owner: 'Owner'
|
||||
owner: 'Owner',
|
||||
shared_characters_title: 'Geteilte Charaktere',
|
||||
owned_characters_title: 'Eigene Charaktere'
|
||||
},
|
||||
create: {
|
||||
spells: {
|
||||
@@ -507,6 +512,28 @@ 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',
|
||||
shareWithUsers: 'Share with specific users',
|
||||
shareDescription: 'Select users who can view this character',
|
||||
currentlySharedWith: 'Currently shared with',
|
||||
addUsers: 'Add users',
|
||||
searchUsers: 'Search users...',
|
||||
loadingUsers: 'Loading users...',
|
||||
noOtherUsers: 'No other users available',
|
||||
noMatchingUsers: 'No users match your search',
|
||||
noSharedUsers: 'Not shared with any users yet'
|
||||
},
|
||||
userManagement: {
|
||||
title: 'User Management',
|
||||
loading: 'Loading users...',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Frontend version information
|
||||
export const VERSION = '0.2.0'
|
||||
export const VERSION = '0.2.1'
|
||||
|
||||
// Git commit will be injected at build time or detected from env
|
||||
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
|
||||
|
||||
Reference in New Issue
Block a user