added language setting to the users profile

This commit is contained in:
2026-01-14 15:35:51 +01:00
parent 4972816c75
commit 4e58a3b4bc
8 changed files with 343 additions and 5 deletions
+51
View File
@@ -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,
})
}
+192
View File
@@ -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)
}
})
}
+1
View File
@@ -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"`
+1
View File
@@ -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
+20
View File
@@ -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
+6
View File
@@ -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',
+6
View File
@@ -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',
+62 -1
View File
@@ -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
}
} }
} }
} }