added language setting to the users profile
This commit is contained in:
@@ -445,6 +445,7 @@ func GetUserProfile(c *gin.Context) {
|
|||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
"role": user.Role,
|
"role": user.Role,
|
||||||
|
"preferred_language": user.PreferredLanguage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,3 +555,53 @@ func UpdatePassword(c *gin.Context) {
|
|||||||
"message": "Password updated successfully",
|
"message": "Password updated successfully",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateLanguage Handler to update user's preferred language
|
||||||
|
func UpdateLanguage(c *gin.Context) {
|
||||||
|
logger.Debug("Starte Sprach-Aktualisierung...")
|
||||||
|
|
||||||
|
// Get user ID from context
|
||||||
|
userID, exists := c.Get("userID")
|
||||||
|
if !exists {
|
||||||
|
logger.Error("Benutzer-ID nicht im Context gefunden")
|
||||||
|
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input struct {
|
||||||
|
Language string `json:"language" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
logger.Error("Fehler beim Parsen der Sprach-Daten: %s", err.Error())
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Language is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate language (only de and en supported)
|
||||||
|
if input.Language != "de" && input.Language != "en" {
|
||||||
|
logger.Warn("Ungültige Sprache angefordert: %s", input.Language)
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Invalid language. Supported languages: de, en")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err := user.FirstId(userID.(uint)); err != nil {
|
||||||
|
logger.Error("Benutzer mit ID %v nicht gefunden: %s", userID, err.Error())
|
||||||
|
respondWithError(c, http.StatusNotFound, "User not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PreferredLanguage = input.Language
|
||||||
|
if err := user.Save(); err != nil {
|
||||||
|
logger.Error("Fehler beim Speichern der Sprache für Benutzer %s: %s", user.Username, err.Error())
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to update language")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Sprache erfolgreich aktualisiert für Benutzer: %s (ID: %d) - Neue Sprache: %s", user.Username, user.UserID, input.Language)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Language updated successfully",
|
||||||
|
"language": user.PreferredLanguage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -461,3 +461,195 @@ func TestUpdatePassword(t *testing.T) {
|
|||||||
assert.Equal(t, expectedHash, updatedUser.PasswordHash)
|
assert.Equal(t, expectedHash, updatedUser.PasswordHash)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestUpdateLanguage tests the UpdateLanguage handler
|
||||||
|
func TestUpdateLanguage(t *testing.T) {
|
||||||
|
setupHandlerTestEnvironment(t)
|
||||||
|
|
||||||
|
t.Run("Success - Update language to en", func(t *testing.T) {
|
||||||
|
testUser := createTestUser(t, "languser1", "password123", "languser1@test.com")
|
||||||
|
|
||||||
|
requestData := map[string]interface{}{
|
||||||
|
"language": "en",
|
||||||
|
}
|
||||||
|
requestBody, _ := json.Marshal(requestData)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/user/language", bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
c.Set("userID", testUser.UserID)
|
||||||
|
|
||||||
|
UpdateLanguage(c)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Language updated successfully", response["message"])
|
||||||
|
assert.Equal(t, "en", response["language"])
|
||||||
|
|
||||||
|
// Verify language was actually updated in database
|
||||||
|
var updatedUser User
|
||||||
|
err = updatedUser.FirstId(testUser.UserID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "en", updatedUser.PreferredLanguage)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Success - Update language to de", func(t *testing.T) {
|
||||||
|
testUser := createTestUser(t, "languser2", "password123", "languser2@test.com")
|
||||||
|
|
||||||
|
requestData := map[string]interface{}{
|
||||||
|
"language": "de",
|
||||||
|
}
|
||||||
|
requestBody, _ := json.Marshal(requestData)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/user/language", bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
c.Set("userID", testUser.UserID)
|
||||||
|
|
||||||
|
UpdateLanguage(c)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "de", response["language"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Failure - Invalid language", func(t *testing.T) {
|
||||||
|
testUser := createTestUser(t, "languser3", "password123", "languser3@test.com")
|
||||||
|
|
||||||
|
requestData := map[string]interface{}{
|
||||||
|
"language": "fr",
|
||||||
|
}
|
||||||
|
requestBody, _ := json.Marshal(requestData)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/user/language", bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
c.Set("userID", testUser.UserID)
|
||||||
|
|
||||||
|
UpdateLanguage(c)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Invalid language. Supported languages: de, en", response["error"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Failure - No user ID in context", func(t *testing.T) {
|
||||||
|
requestData := map[string]interface{}{
|
||||||
|
"language": "en",
|
||||||
|
}
|
||||||
|
requestBody, _ := json.Marshal(requestData)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/user/language", bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
|
||||||
|
UpdateLanguage(c)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Unauthorized", response["error"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Failure - Empty language", func(t *testing.T) {
|
||||||
|
testUser := createTestUser(t, "languser4", "password123", "languser4@test.com")
|
||||||
|
|
||||||
|
requestData := map[string]interface{}{
|
||||||
|
"language": "",
|
||||||
|
}
|
||||||
|
requestBody, _ := json.Marshal(requestData)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/user/language", bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
c.Set("userID", testUser.UserID)
|
||||||
|
|
||||||
|
UpdateLanguage(c)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Language is required", response["error"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetUserProfileWithLanguage tests that GetUserProfile returns the language field
|
||||||
|
func TestGetUserProfileWithLanguage(t *testing.T) {
|
||||||
|
setupHandlerTestEnvironment(t)
|
||||||
|
|
||||||
|
t.Run("Success - Profile includes preferred language", func(t *testing.T) {
|
||||||
|
testUser := createTestUser(t, "langprofileuser", "password123", "langprofile@test.com")
|
||||||
|
|
||||||
|
// Set language to en
|
||||||
|
testUser.PreferredLanguage = "en"
|
||||||
|
err := testUser.Save()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "/api/user/profile", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
c.Set("userID", testUser.UserID)
|
||||||
|
|
||||||
|
GetUserProfile(c)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "en", response["preferred_language"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Success - New user has default language de", func(t *testing.T) {
|
||||||
|
testUser := createTestUser(t, "newlanguser", "password123", "newlang@test.com")
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "/api/user/profile", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
c.Set("userID", testUser.UserID)
|
||||||
|
|
||||||
|
GetUserProfile(c)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that preferred_language is present and defaults to "de"
|
||||||
|
lang, ok := response["preferred_language"]
|
||||||
|
assert.True(t, ok, "preferred_language should be present in response")
|
||||||
|
if lang == "" || lang == nil {
|
||||||
|
// If empty, GORM default should be "de"
|
||||||
|
assert.Equal(t, "de", testUser.PreferredLanguage)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, "de", lang)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type User struct {
|
|||||||
PasswordHash string `json:"password"`
|
PasswordHash string `json:"password"`
|
||||||
Email string `gorm:"unique" json:"email"`
|
Email string `gorm:"unique" json:"email"`
|
||||||
Role string `gorm:"default:standard" json:"role"`
|
Role string `gorm:"default:standard" json:"role"`
|
||||||
|
PreferredLanguage string `gorm:"default:de" json:"preferred_language"`
|
||||||
ResetPwHash *string `gorm:"index" json:"-"` // Hash für Password-Reset (wird nicht serialisiert)
|
ResetPwHash *string `gorm:"index" json:"-"` // Hash für Password-Reset (wird nicht serialisiert)
|
||||||
ResetPwHashExpires *time.Time `json:"-"` // Ablaufzeit für Password-Reset-Hash
|
ResetPwHashExpires *time.Time `json:"-"` // Ablaufzeit für Password-Reset-Hash
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
userGroup.GET("/profile", GetUserProfile)
|
userGroup.GET("/profile", GetUserProfile)
|
||||||
userGroup.PUT("/email", UpdateEmail)
|
userGroup.PUT("/email", UpdateEmail)
|
||||||
userGroup.PUT("/password", UpdatePassword)
|
userGroup.PUT("/password", UpdatePassword)
|
||||||
|
userGroup.PUT("/language", UpdateLanguage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin routes - require admin role
|
// Admin routes - require admin role
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Environment variables for Bamort production environment
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
API_URL=https://bamort-api.trokan.de
|
||||||
|
VITE_API_URL=https://bamort-api.trokan.de
|
||||||
|
|
||||||
|
# Database Configuration Backend
|
||||||
|
DATABASE_TYPE=mysql
|
||||||
|
#DATABASE_URL=bamort:bG4)efozrc@tcp(mariadb:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local
|
||||||
|
|
||||||
|
# MariaDB Configuration
|
||||||
|
MARIADB_ROOT_PASSWORD=moam-bmrt-2.6
|
||||||
|
MARIADB_PASSWORD=bG4)efozrc
|
||||||
|
MARIADB_DATABASE=bamort
|
||||||
|
MARIADB_USER=bamort
|
||||||
|
|
||||||
|
API_PORT=8180
|
||||||
|
BASE_URL=https://bamort.trokan.de
|
||||||
|
TEMPLATES_DIR=./templates
|
||||||
|
EXPORT_TEMP_DIR=./export_temp
|
||||||
@@ -530,6 +530,12 @@ export default {
|
|||||||
username: 'Benutzername',
|
username: 'Benutzername',
|
||||||
currentEmail: 'Aktuelle E-Mail',
|
currentEmail: 'Aktuelle E-Mail',
|
||||||
role: 'Rolle',
|
role: 'Rolle',
|
||||||
|
language: 'Sprache',
|
||||||
|
changeLanguage: 'Sprache ändern',
|
||||||
|
selectLanguage: 'Sprache auswählen',
|
||||||
|
updateLanguage: 'Sprache aktualisieren',
|
||||||
|
languageUpdateSuccess: 'Sprache erfolgreich aktualisiert',
|
||||||
|
languageUpdateError: 'Fehler beim Aktualisieren der Sprache',
|
||||||
changeEmail: 'E-Mail ändern',
|
changeEmail: 'E-Mail ändern',
|
||||||
newEmail: 'Neue E-Mail',
|
newEmail: 'Neue E-Mail',
|
||||||
emailPlaceholder: 'ihre.email@example.com',
|
emailPlaceholder: 'ihre.email@example.com',
|
||||||
|
|||||||
@@ -526,6 +526,12 @@ export default {
|
|||||||
username: 'Username',
|
username: 'Username',
|
||||||
currentEmail: 'Current Email',
|
currentEmail: 'Current Email',
|
||||||
role: 'Role',
|
role: 'Role',
|
||||||
|
language: 'Language',
|
||||||
|
changeLanguage: 'Change Language',
|
||||||
|
selectLanguage: 'Select Language',
|
||||||
|
updateLanguage: 'Update Language',
|
||||||
|
languageUpdateSuccess: 'Language updated successfully',
|
||||||
|
languageUpdateError: 'Failed to update language',
|
||||||
changeEmail: 'Change Email',
|
changeEmail: 'Change Email',
|
||||||
newEmail: 'New Email',
|
newEmail: 'New Email',
|
||||||
emailPlaceholder: 'your.email@example.com',
|
emailPlaceholder: 'your.email@example.com',
|
||||||
|
|||||||
@@ -25,6 +25,32 @@
|
|||||||
{{ $t(`userManagement.roles.${userProfile.role}`) }}
|
{{ $t(`userManagement.roles.${userProfile.role}`) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<label>{{ $t('profile.language') }}:</label>
|
||||||
|
<span>{{ userProfile.preferred_language === 'de' ? 'Deutsch' : 'English' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Change Language Section -->
|
||||||
|
<div class="profile-section">
|
||||||
|
<h2>{{ $t('profile.changeLanguage') }}</h2>
|
||||||
|
<form @submit.prevent="updateLanguage" class="profile-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="language">{{ $t('profile.selectLanguage') }}:</label>
|
||||||
|
<select
|
||||||
|
id="language"
|
||||||
|
v-model="languageForm.selectedLanguage"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="de">Deutsch</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" :disabled="isUpdating" class="btn-primary">
|
||||||
|
<span v-if="!isUpdating">{{ $t('profile.updateLanguage') }}</span>
|
||||||
|
<span v-else>{{ $t('profile.updating') }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change Email Section -->
|
<!-- Change Email Section -->
|
||||||
@@ -206,7 +232,11 @@ export default {
|
|||||||
userProfile: {
|
userProfile: {
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
role: 'standard'
|
role: 'standard',
|
||||||
|
preferred_language: 'de'
|
||||||
|
},
|
||||||
|
languageForm: {
|
||||||
|
selectedLanguage: 'de'
|
||||||
},
|
},
|
||||||
emailForm: {
|
emailForm: {
|
||||||
newEmail: ''
|
newEmail: ''
|
||||||
@@ -228,6 +258,7 @@ export default {
|
|||||||
const response = await API.get('/api/user/profile')
|
const response = await API.get('/api/user/profile')
|
||||||
this.userProfile = response.data
|
this.userProfile = response.data
|
||||||
this.emailForm.newEmail = this.userProfile.email
|
this.emailForm.newEmail = this.userProfile.email
|
||||||
|
this.languageForm.selectedLanguage = this.userProfile.preferred_language || 'de'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load profile:', error)
|
console.error('Failed to load profile:', error)
|
||||||
alert(this.$t('profile.loadError') + ': ' + (error.response?.data?.error || error.message))
|
alert(this.$t('profile.loadError') + ': ' + (error.response?.data?.error || error.message))
|
||||||
@@ -315,6 +346,36 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
this.isUpdating = false
|
this.isUpdating = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async updateLanguage() {
|
||||||
|
if (!this.languageForm.selectedLanguage) {
|
||||||
|
alert(this.$t('profile.selectLanguage'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUpdating = true
|
||||||
|
try {
|
||||||
|
const response = await API.put('/api/user/language', {
|
||||||
|
language: this.languageForm.selectedLanguage
|
||||||
|
})
|
||||||
|
|
||||||
|
this.userProfile.preferred_language = response.data.language
|
||||||
|
|
||||||
|
// Update i18n language
|
||||||
|
this.$i18n.locale = response.data.language
|
||||||
|
localStorage.setItem('language', response.data.language)
|
||||||
|
|
||||||
|
alert(this.$t('profile.languageUpdateSuccess'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update language:', error)
|
||||||
|
let errorMsg = this.$t('profile.languageUpdateError')
|
||||||
|
if (error.response?.data?.error) {
|
||||||
|
errorMsg += ': ' + error.response.data.error
|
||||||
|
}
|
||||||
|
alert(errorMsg)
|
||||||
|
} finally {
|
||||||
|
this.isUpdating = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user