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})
|
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) {
|
func ListCharacters(c *gin.Context) {
|
||||||
logger.Debug("ListCharacters aufgerufen")
|
logger.Debug("ListCharacters aufgerufen")
|
||||||
|
|
||||||
@@ -59,6 +70,14 @@ func ListCharacters(c *gin.Context) {
|
|||||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve public characters")
|
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve public characters")
|
||||||
return
|
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
|
allCharacters.Others = listPublic
|
||||||
|
|
||||||
logger.Info("Charakterliste erfolgreich geladen: %d Charaktere", len(listOfChars))
|
logger.Info("Charakterliste erfolgreich geladen: %d Charaktere", len(listOfChars))
|
||||||
@@ -101,6 +120,11 @@ func UpdateCharacter(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Store the original ID to preserve it
|
// Store the original ID to preserve it
|
||||||
originalID := character.ID
|
originalID := character.ID
|
||||||
originalGameSystem := character.GameSystem
|
originalGameSystem := character.GameSystem
|
||||||
@@ -133,6 +157,12 @@ func DeleteCharacter(c *gin.Context) {
|
|||||||
respondWithError(c, http.StatusNotFound, "Character not found")
|
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = character.Delete()
|
err = character.Delete()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondWithError(c, http.StatusInternalServerError, "Failed to delete character")
|
respondWithError(c, http.StatusInternalServerError, "Failed to delete character")
|
||||||
@@ -267,6 +297,11 @@ func UpdateCharacterExperience(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Parse Request
|
// Parse Request
|
||||||
var req UpdateExperienceRequest
|
var req UpdateExperienceRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -357,6 +392,11 @@ func UpdateCharacterWealth(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Parse Request
|
// Parse Request
|
||||||
var req UpdateWealthRequest
|
var req UpdateWealthRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -567,6 +607,11 @@ func LearnSkill(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Verwende gsmaster.LernCostRequest direkt
|
// Verwende gsmaster.LernCostRequest direkt
|
||||||
var request gsmaster.LernCostRequest
|
var request gsmaster.LernCostRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
@@ -1052,6 +1097,11 @@ func ImproveSkill(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, char) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Skill validieren und Level ermitteln
|
// 2. Skill validieren und Level ermitteln
|
||||||
characterClass, skillInfo, currentLevel, err := validateSkillForImprovement(char, &request)
|
characterClass, skillInfo, currentLevel, err := validateSkillForImprovement(char, &request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1225,6 +1275,18 @@ func LearnSpell(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
charID := uint(charIDInt)
|
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
|
var lernRequest gsmaster.LernCostRequest
|
||||||
if err := c.ShouldBindJSON(&lernRequest); err != nil {
|
if err := c.ShouldBindJSON(&lernRequest); err != nil {
|
||||||
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
|
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ func UpdateCharacterImage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var request ImageUpdateRequest
|
var request ImageUpdateRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error("Invalid request data: %s", err.Error())
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Request-Parameter abrufen
|
// Request-Parameter abrufen
|
||||||
var practicePoints []PracticePointResponse
|
var practicePoints []PracticePointResponse
|
||||||
if err := c.ShouldBindJSON(&practicePoints); err != nil {
|
if err := c.ShouldBindJSON(&practicePoints); err != nil {
|
||||||
@@ -120,6 +125,11 @@ func AddPracticePoint(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Request-Parameter abrufen
|
// Request-Parameter abrufen
|
||||||
type AddPPRequest struct {
|
type AddPPRequest struct {
|
||||||
SkillName string `json:"skill_name" binding:"required"`
|
SkillName string `json:"skill_name" binding:"required"`
|
||||||
@@ -218,6 +228,11 @@ func UsePracticePoint(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkCharacterOwnership(c, &character) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Request-Parameter abrufen
|
// Request-Parameter abrufen
|
||||||
type UsePPRequest struct {
|
type UsePPRequest struct {
|
||||||
SkillName string `json:"skill_name" binding:"required"`
|
SkillName string `json:"skill_name" binding:"required"`
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
charGrp.POST("", CreateCharacter)
|
charGrp.POST("", CreateCharacter)
|
||||||
charGrp.GET("/:id", GetCharacter)
|
charGrp.GET("/:id", GetCharacter)
|
||||||
charGrp.PUT("/:id", UpdateCharacter)
|
charGrp.PUT("/:id", UpdateCharacter)
|
||||||
|
charGrp.PATCH("/:id", UpdateCharacter)
|
||||||
|
|
||||||
charGrp.DELETE("/:id", DeleteCharacter)
|
charGrp.DELETE("/:id", DeleteCharacter)
|
||||||
charGrp.PUT("/:id/image", UpdateCharacterImage)
|
charGrp.PUT("/:id/image", UpdateCharacterImage)
|
||||||
charGrp.GET("/:id/datasheet-options", GetDatasheetOptions)
|
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
|
// Erfahrung und Vermögen
|
||||||
charGrp.GET("/:id/experience-wealth", GetCharacterExperienceAndWealth) // NewSystem
|
charGrp.GET("/:id/experience-wealth", GetCharacterExperienceAndWealth) // NewSystem
|
||||||
charGrp.PUT("/:id/experience", UpdateCharacterExperience) // 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
|
package config
|
||||||
|
|
||||||
// Version is the application version
|
// Version is the application version
|
||||||
const Version = "0.2.0"
|
const Version = "0.2.1"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// GitCommit will be set by build flags or detected at runtime
|
// 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})
|
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) {
|
func CreateAusruestung(c *gin.Context) {
|
||||||
var ausruestung models.EqAusruestung
|
var ausruestung models.EqAusruestung
|
||||||
if err := c.ShouldBindJSON(&ausruestung); err != nil {
|
if err := c.ShouldBindJSON(&ausruestung); err != nil {
|
||||||
@@ -26,6 +41,11 @@ func CreateAusruestung(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkEquipmentOwnership(c, ausruestung.CharacterID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := database.DB.Create(&ausruestung).Error; err != nil {
|
if err := database.DB.Create(&ausruestung).Error; err != nil {
|
||||||
respondWithError(c, http.StatusInternalServerError, "Failed to create Ausruestung")
|
respondWithError(c, http.StatusInternalServerError, "Failed to create Ausruestung")
|
||||||
return
|
return
|
||||||
@@ -55,6 +75,11 @@ func UpdateAusruestung(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkEquipmentOwnership(c, ausruestung.CharacterID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&ausruestung); err != nil {
|
if err := c.ShouldBindJSON(&ausruestung); err != nil {
|
||||||
respondWithError(c, http.StatusBadRequest, err.Error())
|
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
@@ -70,7 +95,19 @@ func UpdateAusruestung(c *gin.Context) {
|
|||||||
|
|
||||||
func DeleteAusruestung(c *gin.Context) {
|
func DeleteAusruestung(c *gin.Context) {
|
||||||
ausruestungID := c.Param("ausruestung_id")
|
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")
|
respondWithError(c, http.StatusInternalServerError, "Failed to delete Ausruestung")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -89,6 +126,11 @@ func CreateWaffe(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkEquipmentOwnership(c, waffe.CharacterID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := database.DB.Create(&waffe).Error; err != nil {
|
if err := database.DB.Create(&waffe).Error; err != nil {
|
||||||
respondWithError(c, http.StatusInternalServerError, "Failed to create Waffe")
|
respondWithError(c, http.StatusInternalServerError, "Failed to create Waffe")
|
||||||
return
|
return
|
||||||
@@ -118,6 +160,11 @@ func UpdateWaffe(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if !checkEquipmentOwnership(c, waffe.CharacterID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&waffe); err != nil {
|
if err := c.ShouldBindJSON(&waffe); err != nil {
|
||||||
respondWithError(c, http.StatusBadRequest, err.Error())
|
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
@@ -133,7 +180,19 @@ func UpdateWaffe(c *gin.Context) {
|
|||||||
|
|
||||||
func DeleteWaffe(c *gin.Context) {
|
func DeleteWaffe(c *gin.Context) {
|
||||||
waffeID := c.Param("waffe_id")
|
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")
|
respondWithError(c, http.StatusInternalServerError, "Failed to delete Waffe")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ func TestCreateAusruestung(t *testing.T) {
|
|||||||
BamortBase: models.BamortBase{
|
BamortBase: models.BamortBase{
|
||||||
Name: "Test Sword",
|
Name: "Test Sword",
|
||||||
},
|
},
|
||||||
CharacterID: 1,
|
CharacterID: 21,
|
||||||
UserID: 1,
|
UserID: 4,
|
||||||
},
|
},
|
||||||
Magisch: models.Magisch{
|
Magisch: models.Magisch{
|
||||||
IstMagisch: false,
|
IstMagisch: false,
|
||||||
@@ -111,13 +111,17 @@ func TestCreateAusruestung(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Empty JSON",
|
name: "Empty JSON",
|
||||||
payload: map[string]interface{}{},
|
payload: map[string]interface{}{},
|
||||||
expectedStatus: http.StatusCreated,
|
expectedStatus: http.StatusNotFound,
|
||||||
shouldContain: "",
|
shouldContain: "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// u := user.User{}
|
||||||
|
// u.FirstId(1)
|
||||||
|
// token := user.GenerateToken(&u)
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := gin.CreateTestContext(w)
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
|
||||||
@@ -129,9 +133,12 @@ func TestCreateAusruestung(t *testing.T) {
|
|||||||
body = *bytes.NewBuffer(jsonData)
|
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 = httptest.NewRequest("POST", "/ausruestung", &body)
|
||||||
c.Request.Header.Set("Content-Type", "application/json")
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
//c.Request.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
CreateAusruestung(c)
|
CreateAusruestung(c)
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||||
@@ -235,8 +242,8 @@ func TestUpdateAusruestung(t *testing.T) {
|
|||||||
BamortBase: models.BamortBase{
|
BamortBase: models.BamortBase{
|
||||||
Name: "Original Equipment",
|
Name: "Original Equipment",
|
||||||
},
|
},
|
||||||
CharacterID: 123,
|
CharacterID: 21,
|
||||||
UserID: 1,
|
UserID: 4,
|
||||||
},
|
},
|
||||||
Magisch: models.Magisch{
|
Magisch: models.Magisch{
|
||||||
IstMagisch: false,
|
IstMagisch: false,
|
||||||
@@ -297,6 +304,8 @@ func TestUpdateAusruestung(t *testing.T) {
|
|||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := gin.CreateTestContext(w)
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Set("userID", uint(4))
|
||||||
|
|
||||||
c.Params = gin.Params{
|
c.Params = gin.Params{
|
||||||
{Key: "ausruestung_id", Value: tt.ausruestungID},
|
{Key: "ausruestung_id", Value: tt.ausruestungID},
|
||||||
}
|
}
|
||||||
@@ -332,8 +341,8 @@ func TestDeleteAusruestung(t *testing.T) {
|
|||||||
BamortBase: models.BamortBase{
|
BamortBase: models.BamortBase{
|
||||||
Name: "Equipment to Delete",
|
Name: "Equipment to Delete",
|
||||||
},
|
},
|
||||||
CharacterID: 123,
|
CharacterID: 21,
|
||||||
UserID: 1,
|
UserID: 4,
|
||||||
},
|
},
|
||||||
Magisch: models.Magisch{
|
Magisch: models.Magisch{
|
||||||
IstMagisch: false,
|
IstMagisch: false,
|
||||||
@@ -368,14 +377,14 @@ func TestDeleteAusruestung(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Non-existent Ausruestung",
|
name: "Non-existent Ausruestung",
|
||||||
ausruestungID: "999",
|
ausruestungID: "999",
|
||||||
expectedStatus: http.StatusOK, // GORM doesn't fail on deleting non-existent records
|
expectedStatus: http.StatusNotFound, // GORM doesn't fail on deleting non-existent records
|
||||||
shouldContain: "deleted successfully",
|
shouldContain: "Ausruestung not found",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid Ausruestung ID",
|
name: "Invalid Ausruestung ID",
|
||||||
ausruestungID: "invalid",
|
ausruestungID: "invalid",
|
||||||
expectedStatus: http.StatusInternalServerError,
|
expectedStatus: http.StatusNotFound,
|
||||||
shouldContain: "error",
|
shouldContain: "Ausruestung not found",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,11 +392,11 @@ func TestDeleteAusruestung(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := gin.CreateTestContext(w)
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Set("userID", uint(4))
|
||||||
|
|
||||||
c.Params = gin.Params{
|
c.Params = gin.Params{
|
||||||
{Key: "ausruestung_id", Value: tt.ausruestungID},
|
{Key: "ausruestung_id", Value: tt.ausruestungID},
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteAusruestung(c)
|
DeleteAusruestung(c)
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||||
|
|||||||
+23
-21
@@ -8,50 +8,52 @@ import (
|
|||||||
|
|
||||||
func RegisterRoutes(r *gin.RouterGroup) {
|
func RegisterRoutes(r *gin.RouterGroup) {
|
||||||
maintGrp := r.Group("/maintenance")
|
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.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.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/:id", UpdateMDSkill)
|
||||||
maintGrp.PUT("/skills-enhanced/:id", UpdateEnhancedMDSkill) // New enhanced endpoint
|
maintGrp.PUT("/skills-enhanced/:id", UpdateEnhancedMDSkill) // New enhanced endpoint
|
||||||
maintGrp.POST("/skills", AddSkill)
|
maintGrp.POST("/skills", AddSkill)
|
||||||
maintGrp.DELETE("/skills/:id", DeleteMDSkill)
|
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/:id", UpdateMDWeaponSkill)
|
||||||
maintGrp.PUT("/weaponskills-enhanced/:id", UpdateEnhancedMDWeaponSkill) // New enhanced endpoint
|
maintGrp.PUT("/weaponskills-enhanced/:id", UpdateEnhancedMDWeaponSkill) // New enhanced endpoint
|
||||||
maintGrp.POST("/weaponskills", AddWeaponSkill)
|
maintGrp.POST("/weaponskills", AddWeaponSkill)
|
||||||
maintGrp.DELETE("/weaponskills/:id", DeleteMDWeaponSkill)
|
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/:id", UpdateMDSpell)
|
||||||
maintGrp.PUT("/spells-enhanced/:id", UpdateEnhancedMDSpell) // New enhanced endpoint
|
maintGrp.PUT("/spells-enhanced/:id", UpdateEnhancedMDSpell) // New enhanced endpoint
|
||||||
maintGrp.POST("/spells", AddSpell)
|
maintGrp.POST("/spells", AddSpell)
|
||||||
maintGrp.DELETE("/spells/:id", DeleteMDSpell)
|
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/:id", UpdateMDEquipment)
|
||||||
maintGrp.PUT("/equipment-enhanced/:id", UpdateEnhancedMDEquipmentItem) // New enhanced endpoint
|
maintGrp.PUT("/equipment-enhanced/:id", UpdateEnhancedMDEquipmentItem) // New enhanced endpoint
|
||||||
maintGrp.POST("/equipment", AddEquipment)
|
maintGrp.POST("/equipment", AddEquipment)
|
||||||
maintGrp.DELETE("/equipment/:id", DeleteMDEquipment)
|
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/:id", UpdateMDWeapon)
|
||||||
maintGrp.PUT("/weapons-enhanced/:id", UpdateEnhancedMDWeapon) // New enhanced endpoint
|
maintGrp.PUT("/weapons-enhanced/:id", UpdateEnhancedMDWeapon) // New enhanced endpoint
|
||||||
maintGrp.POST("/weapons", AddWeapon)
|
maintGrp.POST("/weapons", AddWeapon)
|
||||||
|
|||||||
@@ -295,6 +295,9 @@ func copyMariaDBToSQLite(mariaDB, sqliteDB *gorm.DB) error {
|
|||||||
// Audit Logging (abhängig von Char)
|
// Audit Logging (abhängig von Char)
|
||||||
&models.AuditLogEntry{},
|
&models.AuditLogEntry{},
|
||||||
|
|
||||||
|
// Char Shares (abhängig von Char und User)
|
||||||
|
&models.CharShare{},
|
||||||
|
|
||||||
// View-Strukturen ohne eigene Tabellen werden nicht kopiert:
|
// View-Strukturen ohne eigene Tabellen werden nicht kopiert:
|
||||||
// SkillLearningInfo, SpellLearningInfo, CharList, FeChar, etc.
|
// SkillLearningInfo, SpellLearningInfo, CharList, FeChar, etc.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ func MigrateStructure(db ...*gorm.DB) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gameSystemMigrateStructure(db ...*gorm.DB) error {
|
func gameSystemMigrateStructure(db ...*gorm.DB) error {
|
||||||
// Use provided DB or default to database.DB
|
// Use provided DB or default to database.DB
|
||||||
var targetDB *gorm.DB
|
var targetDB *gorm.DB
|
||||||
@@ -66,6 +67,7 @@ func gameSystemMigrateStructure(db ...*gorm.DB) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gsMasterMigrateStructure(db ...*gorm.DB) error {
|
func gsMasterMigrateStructure(db ...*gorm.DB) error {
|
||||||
// Use provided DB or default to database.DB
|
// Use provided DB or default to database.DB
|
||||||
var targetDB *gorm.DB
|
var targetDB *gorm.DB
|
||||||
@@ -112,6 +114,7 @@ func characterMigrateStructure(db ...*gorm.DB) error {
|
|||||||
&Bennies{},
|
&Bennies{},
|
||||||
&Vermoegen{},
|
&Vermoegen{},
|
||||||
&CharacterCreationSession{},
|
&CharacterCreationSession{},
|
||||||
|
&CharShare{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -255,6 +255,21 @@ func (object *Char) FindByUserID(userID uint) ([]Char, error) {
|
|||||||
return chars, nil
|
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) {
|
func FindPublicCharList() ([]CharList, error) {
|
||||||
var chars []CharList
|
var chars []CharList
|
||||||
gs := GetGameSystem(0, "midgard")
|
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{
|
r.Use(cors.New(cors.Config{
|
||||||
//AllowOrigins: []string{"http://localhost:3000"}, // Replace with your frontend's URL
|
//AllowOrigins: []string{"http://localhost:3000"}, // Replace with your frontend's URL
|
||||||
AllowOrigins: allowedOrigins,
|
AllowOrigins: allowedOrigins,
|
||||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
||||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||||
ExposeHeaders: []string{"Content-Length"},
|
ExposeHeaders: []string{"Content-Length"},
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Frontend Version Management
|
# Frontend Version Management
|
||||||
|
|
||||||
## Current Version: 0.2.0
|
## Current Version: 0.2.1
|
||||||
|
|
||||||
The frontend version is managed independently from the backend.
|
The frontend version is managed independently from the backend.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bamort-frontend",
|
"name": "bamort-frontend",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -210,12 +210,14 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Global fullwidth page class for view components */
|
/* Global fullwidth page class for view components */
|
||||||
|
/*
|
||||||
.fullwidth-page {
|
.fullwidth-page {
|
||||||
/*width: 100%;
|
width: 100%;
|
||||||
min-height: calc(100vh - 120px);
|
min-height: calc(100vh - 120px);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;*/
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/* Common Card Layouts */
|
/* Common Card Layouts */
|
||||||
.card {
|
.card {
|
||||||
@@ -2544,7 +2546,7 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remove-btn {
|
.remove-btn {
|
||||||
position: absolute;
|
/*position: absolute;*/
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
@@ -3999,10 +4001,15 @@ a:focus {
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
row-gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragon-container {
|
.dragon-container {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragon-image {
|
.dragon-image {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
<button @click="showExportDialog = true" class="export-button-small" :title="$t('export.title')">
|
<button @click="showExportDialog = true" class="export-button-small" :title="$t('export.title')">
|
||||||
📄
|
📄
|
||||||
</button>
|
</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>
|
<h2>{{ $t('char') }}: {{ character.name }} ({{ $t(currentView) }})</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,9 +21,18 @@
|
|||||||
@export-success="handleExportSuccess"
|
@export-success="handleExportSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Visibility Dialog -->
|
||||||
|
<VisibilityDialog
|
||||||
|
:characterId="id"
|
||||||
|
:currentVisibility="character.public"
|
||||||
|
:showDialog="showVisibilityDialog"
|
||||||
|
@update:showDialog="showVisibilityDialog = $event"
|
||||||
|
@visibility-updated="handleVisibilityUpdated"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Submenu Content -->
|
<!-- Submenu Content -->
|
||||||
<!-- <div class="character-aspect"> -->
|
<!-- <div class="character-aspect"> -->
|
||||||
<component :is="currentView" :character="character" @character-updated="refreshCharacter"/>
|
<component :is="currentView" :character="character" :isOwner="isOwner" @character-updated="refreshCharacter"/>
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
|
|
||||||
<!-- Submenu -->
|
<!-- Submenu -->
|
||||||
@@ -52,7 +64,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import API from '../utils/api'
|
import API from '../utils/api'
|
||||||
|
import { useUserStore } from '../stores/userStore'
|
||||||
import ExportDialog from "./ExportDialog.vue";
|
import ExportDialog from "./ExportDialog.vue";
|
||||||
|
import VisibilityDialog from "./VisibilityDialog.vue";
|
||||||
import DatasheetView from "./DatasheetView.vue"; // Component for character stats
|
import DatasheetView from "./DatasheetView.vue"; // Component for character stats
|
||||||
import SkillView from "./SkillView.vue"; // Component for character history
|
import SkillView from "./SkillView.vue"; // Component for character history
|
||||||
import WeaponView from "./WeaponView.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
|
props: ["id"], // Receive the route parameter as a prop
|
||||||
components: {
|
components: {
|
||||||
ExportDialog,
|
ExportDialog,
|
||||||
|
VisibilityDialog,
|
||||||
DatasheetView,
|
DatasheetView,
|
||||||
SkillView,
|
SkillView,
|
||||||
WeaponView,
|
WeaponView,
|
||||||
@@ -81,6 +96,7 @@ export default {
|
|||||||
currentView: "DatasheetView", // Default view
|
currentView: "DatasheetView", // Default view
|
||||||
lastView: "DatasheetView",
|
lastView: "DatasheetView",
|
||||||
showExportDialog: false,
|
showExportDialog: false,
|
||||||
|
showVisibilityDialog: false,
|
||||||
menus: [
|
menus: [
|
||||||
{ id: 1, name: "Datasheet", component: "DatasheetView" },
|
{ id: 1, name: "Datasheet", component: "DatasheetView" },
|
||||||
{ id: 2, name: "Skill", component: "SkillView" },
|
{ 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() {
|
async created() {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
const response = await API.get(`/api/characters/${this.id}`, {
|
const response = await API.get(`/api/characters/${this.id}`, {
|
||||||
@@ -108,6 +130,12 @@ export default {
|
|||||||
console.log('PDF exported successfully')
|
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) {
|
changeView(view) {
|
||||||
this.lastView = this.currentView;
|
this.lastView = this.currentView;
|
||||||
this.currentView = view;
|
this.currentView = view;
|
||||||
|
|||||||
@@ -16,29 +16,50 @@
|
|||||||
@delete-session="handleDeleteSession"
|
@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>
|
<h3>{{ $t('characters.list.no_characters') }}</h3>
|
||||||
<p>{{ $t('characters.list.no_characters_description') }}</p>
|
<p>{{ $t('characters.list.no_characters_description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="list-container">
|
<div v-else class="list-container horizontal-placement">
|
||||||
<div v-for="character in characters" :key="character.character_id" class="list-item">
|
<div class="charlist">
|
||||||
<router-link :to="`/character/${character.id}`" class="list-item-content">
|
<div class="charlist-header">{{ $t('characters.list.owned_characters_title') }}</div>
|
||||||
<h4 class="list-item-title">{{ character.name }}</h4>
|
<div v-for="character in ownedCharacters" :key="character.character_id" class="list-item">
|
||||||
<div class="list-item-details">
|
<router-link :to="`/character/${character.id}`" class="list-item-content">
|
||||||
{{ character.rasse }} <span class="list-item-separator">|</span>
|
<h4 class="list-item-title">{{ character.name }}</h4>
|
||||||
{{ character.typ }} <span class="list-item-separator">|</span>
|
<div class="list-item-details">
|
||||||
{{ $t('characters.list.grade') }}: {{ character.grad }} <span class="list-item-separator">|</span>
|
{{ character.rasse }} <span class="list-item-separator">|</span>
|
||||||
{{ $t('characters.list.owner') }}: {{ character.owner }} <span class="list-item-separator">|</span>
|
{{ character.typ }} <span class="list-item-separator">|</span>
|
||||||
<span class="badge" :class="character.public ? 'badge-success' : 'badge-secondary'">
|
{{ $t('characters.list.grade') }}: {{ character.grad }} <span class="list-item-separator">|</span>
|
||||||
{{ character.public ? $t('characters.list.public') : $t('characters.list.private') }}
|
{{ $t('characters.list.owner') }}: {{ character.owner }} <span class="list-item-separator">|</span>
|
||||||
</span>
|
<span class="badge" :class="character.public ? 'badge-success' : 'badge-secondary'">
|
||||||
</div>
|
{{ character.public ? $t('characters.list.public') : $t('characters.list.private') }}
|
||||||
</router-link>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template><script>
|
</template>
|
||||||
|
<script>
|
||||||
import API from '../utils/api'
|
import API from '../utils/api'
|
||||||
import { formatDate } from '@/utils/dateUtils'
|
import { formatDate } from '@/utils/dateUtils'
|
||||||
import CharacterCreationSessions from './CharacterCreationSessions.vue'
|
import CharacterCreationSessions from './CharacterCreationSessions.vue'
|
||||||
@@ -49,7 +70,8 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
characters: [],
|
ownedCharacters: [],
|
||||||
|
sharedCharacters: [],
|
||||||
creationSessions: [],
|
creationSessions: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -64,7 +86,8 @@ export default {
|
|||||||
const response = await API.get('/api/characters', {
|
const response = await API.get('/api/characters', {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
this.characters = response.data.self_owned
|
this.ownedCharacters = response.data.self_owned
|
||||||
|
this.sharedCharacters = response.data.others
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading characters:', error)
|
console.error('Error loading characters:', error)
|
||||||
}
|
}
|
||||||
@@ -132,7 +155,7 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
/* All common styles moved to main.css */
|
/* All common styles moved to main.css */
|
||||||
|
|
||||||
.create-character-section {
|
.create-character-section {
|
||||||
@@ -166,6 +189,32 @@ export default {
|
|||||||
color: #007bff;
|
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 */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.list-item {
|
.list-item {
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cd-view">
|
<div class="cd-view">
|
||||||
<p>Are you sure you want to delete {{ character.name }}?</p>
|
<div v-if="!isOwner" class="error-message">
|
||||||
<button @click="deleteCharacter" class="btn btn-danger">Yes, Delete</button>
|
<p>{{ $t('deleteChar.notAuthorized') }}</p>
|
||||||
<button @click="$emit('cancel')" class="btn btn-secondary">Cancel</button>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -16,6 +21,10 @@ export default {
|
|||||||
character: {
|
character: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isOwner: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="fullwidth-container">
|
<div class="fullwidth-container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
<h2>{{ $t('EquipmentView') }}</h2>
|
<h2>{{ $t('EquipmentView') }}</h2>
|
||||||
<button @click="openAddEquipmentDialog" class="btn-add-equipment">
|
<button v-if="isOwner" @click="openAddEquipmentDialog" class="btn-add-equipment">
|
||||||
{{ $t('equipment.add') }}
|
{{ $t('equipment.add') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<th>{{ $t('equipment.amount') }}</th>
|
<th>{{ $t('equipment.amount') }}</th>
|
||||||
<th>{{ $t('equipment.contained_in') }}</th>
|
<th>{{ $t('equipment.contained_in') }}</th>
|
||||||
<th>{{ $t('equipment.bonus') }}</th>
|
<th>{{ $t('equipment.bonus') }}</th>
|
||||||
<th>{{ $t('equipment.actions') }}</th>
|
<th v-if="isOwner">{{ $t('equipment.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<td>{{ equipment.anzahl || '-' }}</td>
|
<td>{{ equipment.anzahl || '-' }}</td>
|
||||||
<td>{{ equipment.beinhaltet_in || '-' }}</td>
|
<td>{{ equipment.beinhaltet_in || '-' }}</td>
|
||||||
<td>{{ equipment.bonus || '-' }}</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 @click="deleteEquipment(equipment)" class="btn-delete" title="Löschen">
|
||||||
🗑️
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
@@ -126,6 +126,24 @@
|
|||||||
.cd-table th {
|
.cd-table th {
|
||||||
background-color: #1da766;
|
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>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -137,6 +155,10 @@ name: "EquipmentView",
|
|||||||
character: {
|
character: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isOwner: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="resource-amount">{{ character.erfahrungsschatz?.ep || 0 }} EP</div>
|
<div class="resource-amount">{{ character.erfahrungsschatz?.ep || 0 }} EP</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row control-row">
|
<div v-if="isOwner" class="form-row control-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input
|
<input
|
||||||
v-model.number="experienceAmount"
|
v-model.number="experienceAmount"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<div class="resource-amount">{{ character.vermoegen?.goldstücke || 0 }} GS</div>
|
<div class="resource-amount">{{ character.vermoegen?.goldstücke || 0 }} GS</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row control-row">
|
<div v-if="isOwner" class="form-row control-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input
|
<input
|
||||||
v-model.number="goldAmount"
|
v-model.number="goldAmount"
|
||||||
@@ -147,6 +147,10 @@ export default {
|
|||||||
character: {
|
character: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isOwner: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="table-wrapper-left">
|
<div class="table-wrapper-left">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
<!-- Lernmodus Toggle Button -->
|
<!-- Lernmodus Toggle Button -->
|
||||||
<div class="learning-mode-controls">
|
<div v-if="isOwner" class="learning-mode-controls">
|
||||||
<!-- Ressourcen-Anzeige (nur sichtbar wenn Lernmodus aktiv) -->
|
<!-- Ressourcen-Anzeige (nur sichtbar wenn Lernmodus aktiv) -->
|
||||||
<div v-if="learningMode" class="resources-display">
|
<div v-if="learningMode" class="resources-display">
|
||||||
<div class="resource-item">
|
<div class="resource-item">
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
||||||
<td>{{ skill.bonus || '0' }}</td>
|
<td>{{ skill.bonus || '0' }}</td>
|
||||||
<td class="pp-cell">
|
<td class="pp-cell">
|
||||||
<div class="pp-container">
|
<div v-if="isOwner" class="pp-container">
|
||||||
<button
|
<button
|
||||||
@click="decreasePP(skill)"
|
@click="decreasePP(skill)"
|
||||||
class="pp-btn pp-btn-minus"
|
class="pp-btn pp-btn-minus"
|
||||||
@@ -86,6 +86,7 @@
|
|||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-else>{{ skill.pp || '0' }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ skill.bemerkung || '-' }}</td>
|
<td>{{ skill.bemerkung || '-' }}</td>
|
||||||
<td v-if="learningMode" class="action-cell">
|
<td v-if="learningMode" class="action-cell">
|
||||||
@@ -109,7 +110,7 @@
|
|||||||
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
||||||
<td>{{ skill.bonus || '0' }}</td>
|
<td>{{ skill.bonus || '0' }}</td>
|
||||||
<td class="pp-cell">
|
<td class="pp-cell">
|
||||||
<div class="pp-container">
|
<div v-if="isOwner" class="pp-container">
|
||||||
<button
|
<button
|
||||||
@click="decreaseWeaponPP(skill)"
|
@click="decreaseWeaponPP(skill)"
|
||||||
class="pp-btn pp-btn-minus"
|
class="pp-btn pp-btn-minus"
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-else>{{ skill.pp || '0' }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ skill.bemerkung || '-' }}</td>
|
<td>{{ skill.bemerkung || '-' }}</td>
|
||||||
<td v-if="learningMode" class="action-cell">
|
<td v-if="learningMode" class="action-cell">
|
||||||
@@ -290,6 +292,10 @@ export default {
|
|||||||
character: {
|
character: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isOwner: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fullwidth-container">
|
<div class="fullwidth-container">
|
||||||
<!-- Header mit Lernmodus-Kontrollen -->
|
<!-- Header mit Lernmodus-Kontrollen -->
|
||||||
<div class="page-header header-section">
|
<div v-if="isOwner" class="page-header header-section">
|
||||||
<div class="learning-mode-controls">
|
<div class="learning-mode-controls">
|
||||||
<!-- Lernmodus Toggle Button -->
|
<!-- Lernmodus Toggle Button -->
|
||||||
<button
|
<button
|
||||||
@@ -99,6 +99,10 @@ export default {
|
|||||||
character: {
|
character: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isOwner: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
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="cd-view">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
<h2>{{ $t('WeaponView') }}</h2>
|
<h2>{{ $t('WeaponView') }}</h2>
|
||||||
<button @click="openAddWeaponDialog" class="btn-add-weapon">
|
<button v-if="isOwner" @click="openAddWeaponDialog" class="btn-add-weapon">
|
||||||
{{ $t('weapon.add') }}
|
{{ $t('weapon.add') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<th>{{ $t('weapon.amount') }}</th>
|
<th>{{ $t('weapon.amount') }}</th>
|
||||||
<th>{{ $t('weapon.contained_in') }}</th>
|
<th>{{ $t('weapon.contained_in') }}</th>
|
||||||
<th>{{ $t('weapon.bonus') }}</th>
|
<th>{{ $t('weapon.bonus') }}</th>
|
||||||
<th>{{ $t('weapon.actions') }}</th>
|
<th v-if="isOwner">{{ $t('weapon.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<td>{{ weapon.anzahl || '-' }}</td>
|
<td>{{ weapon.anzahl || '-' }}</td>
|
||||||
<td>{{ weapon.beinhaltet_in || '-' }}</td>
|
<td>{{ weapon.beinhaltet_in || '-' }}</td>
|
||||||
<td>{{ weapon.anb || '-' }}/{{ weapon.abwb || '-' }}</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 @click="editWeapon(weapon)" class="btn-edit" title="Bearbeiten">
|
||||||
✏️
|
✏️
|
||||||
</button>
|
</button>
|
||||||
@@ -378,6 +378,10 @@ name: "WeaponView",
|
|||||||
character: {
|
character: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isOwner: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
+28
-1
@@ -86,6 +86,9 @@ export default {
|
|||||||
Characters:'Charaktere',
|
Characters:'Charaktere',
|
||||||
Admin:'Administration',
|
Admin:'Administration',
|
||||||
},
|
},
|
||||||
|
deleteChar: {
|
||||||
|
notAuthorized: 'Sie sind nicht berechtigt, diesen Charakter zu löschen. Nur der Besitzer kann diese Aktion durchführen.'
|
||||||
|
},
|
||||||
landing:{
|
landing:{
|
||||||
title:'BaMoRT - Charakterverwaltung für mein Lieblingsrollenspielsystem',
|
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.',
|
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',
|
public: 'Öffentlich',
|
||||||
private: 'Privat',
|
private: 'Privat',
|
||||||
grade: 'Grad',
|
grade: 'Grad',
|
||||||
owner: 'Besitzer'
|
owner: 'Besitzer',
|
||||||
|
shared_characters_title: 'Geteilte Charaktere',
|
||||||
|
owned_characters_title: 'Eigene Charaktere'
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
spells: {
|
spells: {
|
||||||
@@ -511,6 +516,28 @@ export default {
|
|||||||
pleaseWait: 'Bitte warten, dies kann einen Moment dauern',
|
pleaseWait: 'Bitte warten, dies kann einen Moment dauern',
|
||||||
popupBlocked: 'Popup wurde blockiert. Bitte erlauben Sie Popups für diese Seite.'
|
popupBlocked: 'Popup wurde blockiert. Bitte erlauben Sie Popups für diese Seite.'
|
||||||
},
|
},
|
||||||
|
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: {
|
userManagement: {
|
||||||
title: 'Benutzerverwaltung',
|
title: 'Benutzerverwaltung',
|
||||||
loading: 'Lade Benutzer...',
|
loading: 'Lade Benutzer...',
|
||||||
|
|||||||
+28
-1
@@ -85,6 +85,9 @@ export default {
|
|||||||
Characters:'Characters',
|
Characters:'Characters',
|
||||||
Admin:'Administration',
|
Admin:'Administration',
|
||||||
},
|
},
|
||||||
|
deleteChar: {
|
||||||
|
notAuthorized: 'You are not authorized to delete this character. Only the owner can perform this action.'
|
||||||
|
},
|
||||||
landing:{
|
landing:{
|
||||||
title:'BaMoRT - Character Management for Role-Playing Games',
|
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.',
|
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',
|
public: 'Public',
|
||||||
private: 'Private',
|
private: 'Private',
|
||||||
grade: 'Grade',
|
grade: 'Grade',
|
||||||
owner: 'Owner'
|
owner: 'Owner',
|
||||||
|
shared_characters_title: 'Geteilte Charaktere',
|
||||||
|
owned_characters_title: 'Eigene Charaktere'
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
spells: {
|
spells: {
|
||||||
@@ -507,6 +512,28 @@ export default {
|
|||||||
pleaseWait: 'Please wait, this may take a moment',
|
pleaseWait: 'Please wait, this may take a moment',
|
||||||
popupBlocked: 'Popup was blocked. Please allow popups for this site.'
|
popupBlocked: 'Popup was blocked. Please allow popups for this site.'
|
||||||
},
|
},
|
||||||
|
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: {
|
userManagement: {
|
||||||
title: 'User Management',
|
title: 'User Management',
|
||||||
loading: 'Loading users...',
|
loading: 'Loading users...',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Frontend version information
|
// 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
|
// Git commit will be injected at build time or detected from env
|
||||||
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
|
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
|
||||||
|
|||||||
Reference in New Issue
Block a user