added changing password
authorPhiTux <redacted>
Thu, 17 Jul 2025 15:36:11 +0000 (17:36 +0200)
committerPhiTux <redacted>
Thu, 17 Jul 2025 15:36:11 +0000 (17:36 +0200)
backend/handlers/users.go
backend/main.go
backend/utils/file_handling.go
backend/utils/migration.go
backend/utils/security.go
frontend/src/routes/+layout.svelte

index 6cf551cd3b29ce8a61a2e254fd05f3db90d070ac..8f2f75d7353e4082971f60108ee7afb95ac0118b 100644 (file)
@@ -1,6 +1,7 @@
 package handlers
 
 import (
+       "crypto/rand"
        "encoding/base64"
        "encoding/json"
        "fmt"
@@ -191,7 +192,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
        }
 
        // Verify password
-       if !utils.VerifyPassword(req.Password, hashedPassword, salt) {
+       if !utils.VerifyPassword(req.Password, hashedPassword) {
                utils.Logger.Printf("Login failed. Password for user '%s' is incorrect", req.Username)
                http.Error(w, "User/Password combination not found", http.StatusNotFound)
                return
@@ -291,20 +292,28 @@ func Register(username string, password string) (bool, error) {
        }
 
        // Create new user data
-       hashedPassword, salt, err := utils.HashPassword(password)
+       hashedPassword, err := utils.HashPassword(password)
        if err != nil {
                return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError)
        }
 
+       // Generate a random salt
+       salt := make([]byte, 16)
+       if _, err := rand.Read(salt); err != nil {
+               return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError)
+       }
+       // Convert salt to base64
+       saltBase64 := base64.StdEncoding.EncodeToString(salt)
+
        // Create encryption key
-       derivedKey, err := utils.DeriveKeyFromPassword(password, salt)
+       derivedKey, err := utils.DeriveKeyFromPassword(password, saltBase64)
        if err != nil {
                return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError)
        }
 
        // Generate a new random encryption key
        encryptionKey := make([]byte, 32)
-       if _, err := utils.RandRead(encryptionKey); err != nil {
+       if _, err := rand.Read(encryptionKey); err != nil {
                return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError)
        }
 
@@ -315,7 +324,7 @@ func Register(username string, password string) (bool, error) {
        }
 
        nonce := make([]byte, aead.NonceSize())
-       if _, err := utils.RandRead(nonce); err != nil {
+       if _, err := rand.Read(nonce); err != nil {
                return false, fmt.Errorf("internal Server Error: %d", http.StatusInternalServerError)
        }
 
@@ -631,3 +640,219 @@ func GetMigrationProgress(w http.ResponseWriter, r *http.Request) {
                "progress":              progress,
        })
 }
+
+// ChangePasswordRequest represents the change password request body
+type ChangePasswordRequest struct {
+       OldPassword string `json:"old_password"`
+       NewPassword string `json:"new_password"`
+}
+
+// ChangePassword changes the user's password
+func ChangePassword(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               utils.JSONResponse(w, http.StatusOK, map[string]any{
+                       "success": false,
+                       "message": "User not authenticated",
+               })
+               return
+       }
+
+       // Get derived key from context
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               utils.JSONResponse(w, http.StatusOK, map[string]any{
+                       "success": false,
+                       "message": "User not authenticated",
+               })
+               return
+       }
+
+       // Parse request body
+       var req ChangePasswordRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+                       "success": false,
+                       "message": "Invalid request body",
+               })
+               return
+       }
+
+       // Get user data
+       users, err := utils.GetUsers()
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error retrieving users: %v", err),
+               })
+               return
+       }
+
+       usersList, ok := users["users"].([]any)
+       if !ok {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": "Users data is not in the correct format",
+               })
+               return
+       }
+
+       var user map[string]any
+       for _, u := range usersList {
+               uMap, ok := u.(map[string]any)
+               if !ok {
+                       continue
+               }
+               if id, ok := uMap["user_id"].(float64); ok && int(id) == userID {
+                       user = uMap
+                       break
+               }
+       }
+
+       if user == nil {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "User not found",
+               })
+               return
+       }
+
+       currentPassword, ok := user["password"].(string)
+       if !ok {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": "Current hashed password not found for user",
+               })
+               return
+       }
+
+       if !utils.VerifyPassword(req.OldPassword, currentPassword) {
+               utils.JSONResponse(w, http.StatusOK, map[string]any{
+                       "success":            false,
+                       "message":            "Old password is incorrect",
+                       "password_incorrect": true,
+               })
+               return
+       }
+
+       newHashedPassword, err := utils.HashPassword(req.NewPassword)
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error hashing new password: %v", err),
+               })
+               return
+       }
+
+       // Update user data
+       user["password"] = newHashedPassword
+
+       // Decrypt the existing encryption key
+       // Get encryption key
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error getting encryption key: %v", err),
+               })
+               return
+       }
+       encKeyBytes, err := base64.URLEncoding.DecodeString(encKey)
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error decoding encryption key: %v", err),
+               })
+               return
+       }
+
+       // Re-Encrypt the encryption key with the new password
+       // Generate a random salt
+       salt := make([]byte, 16)
+       _, err = rand.Read(salt)
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error generating salt: %v", err),
+               })
+               return
+       }
+
+       saltBase64 := base64.StdEncoding.EncodeToString(salt)
+       newDerivedKey, err := utils.DeriveKeyFromPassword(req.NewPassword, saltBase64)
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error deriving new key from password: %v", err),
+               })
+               return
+       }
+
+       // Encrypt the encryption key with the new derived key
+       aead, err := utils.CreateAEAD(newDerivedKey)
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error creating AEAD: %v", err),
+               })
+               return
+       }
+       nonce := make([]byte, aead.NonceSize())
+       if _, err := rand.Read(nonce); err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error generating nonce: %v", err),
+               })
+               return
+       }
+       encryptedKey := aead.Seal(nonce, nonce, encKeyBytes, nil)
+       encEncKey := base64.StdEncoding.EncodeToString(encryptedKey)
+
+       // Update user data with new salt and encrypted key
+       user["salt"] = saltBase64
+       user["enc_enc_key"] = encEncKey
+
+       // Update users data
+       for i, u := range usersList {
+               if uMap, ok := u.(map[string]any); ok && uMap["user_id"] == userID {
+                       usersList[i] = user
+                       break
+               }
+       }
+       users["users"] = usersList
+       // Write updated users data to file
+       if err := utils.WriteUsers(users); err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error writing users data: %v", err),
+               })
+               return
+       }
+
+       // create new JWT token with updated derived key
+       token, err := utils.GenerateToken(userID, user["username"].(string), base64.StdEncoding.EncodeToString(newDerivedKey))
+       if err != nil {
+               utils.JSONResponse(w, http.StatusInternalServerError, map[string]any{
+                       "success": false,
+                       "message": fmt.Sprintf("Error generating new token: %v", err),
+               })
+               return
+       }
+
+       // Set new token cookie
+       http.SetCookie(w, &http.Cookie{
+               Name:     "token",
+               Value:    token,
+               HttpOnly: true,
+               SameSite: http.SameSiteLaxMode,
+               Path:     "/",
+               Expires:  time.Now().Add(time.Duration(utils.Settings.LogoutAfterDays) * 24 * time.Hour),
+       })
+
+       // Return success and return a new cookie
+       utils.JSONResponse(w, http.StatusOK, map[string]any{
+               "success": true,
+               "message": "Password changed successfully",
+       })
+}
index 238032edff76c5b1e37f2d75fd113e5da8d0211a..16c5ab02c51c04fb9daf4dd2418e04dec0d50a1b 100644 (file)
@@ -39,6 +39,7 @@ func main() {
        mux.HandleFunc("GET /users/check", middleware.RequireAuth(handlers.CheckLogin))
        mux.HandleFunc("GET /users/getUserSettings", middleware.RequireAuth(handlers.GetUserSettings))
        mux.HandleFunc("POST /users/saveUserSettings", middleware.RequireAuth(handlers.SaveUserSettings))
+       mux.HandleFunc("POST /users/changePassword", middleware.RequireAuth(handlers.ChangePassword))
 
        mux.HandleFunc("POST /logs/saveLog", middleware.RequireAuth(handlers.SaveLog))
        mux.HandleFunc("GET /logs/getLog", middleware.RequireAuth(handlers.GetLog))
index 098063a46191db08436e96e9b122e25f94fed73c..07a0e1fd7b9769a8824e120850ba8f89e69ff882 100644 (file)
@@ -1,7 +1,6 @@
 package utils
 
 import (
-       "crypto/rand"
        "encoding/json"
        "fmt"
        "io"
@@ -208,11 +207,6 @@ func WriteTags(userID int, content map[string]any) error {
        return nil
 }
 
-// RandRead is a helper function for reading random bytes
-func RandRead(b []byte) (int, error) {
-       return rand.Read(b)
-}
-
 // GetUserSettings retrieves the settings for a specific user
 func GetUserSettings(userID int) (string, error) {
        userSettingsMutex.RLock()
index 685ff02379db1fe5983ca94d523640cbea23b05e..da69b731b1449ee07fe813e48b0ad4410c3d5d93 100644 (file)
@@ -313,7 +313,7 @@ func MigrateUserData(username, password string, registerFunc RegisterUserFunc, p
                                newUserID = int(id)
 
                                // Verify password
-                               if !VerifyPassword(password, u["password"].(string), u["salt"].(string)) {
+                               if !VerifyPassword(password, u["password"].(string)) {
                                        return handleError(fmt.Sprintf("Login failed. Password for user '%s' is incorrect", username), nil)
                                }
 
index 8f720926f5d84a45d25d7e7c0c129b12063044f4..4c497696cb804b347df54b55658a0b1a781ee25e 100644 (file)
@@ -3,9 +3,12 @@ package utils
 import (
        "crypto/cipher"
        "crypto/rand"
+       "crypto/subtle"
        "encoding/base64"
        "fmt"
        "io"
+       "runtime"
+       "strings"
        "time"
 
        "github.com/golang-jwt/jwt/v5"
@@ -73,42 +76,107 @@ func ValidateToken(tokenString string) (*Claims, error) {
        return claims, nil
 }
 
+type Argon2Configuration struct {
+       HashRaw    []byte
+       Salt       []byte
+       TimeCost   uint32
+       MemoryCost uint32
+       Threads    uint8
+       KeyLength  uint32
+}
+
 // HashPassword hashes a password using Argon2
-func HashPassword(password string) (string, string, error) {
+func HashPassword(password string) (string, error) {
+       config := &Argon2Configuration{
+               TimeCost:   5,
+               MemoryCost: 64 * 1024,
+               Threads:    uint8(runtime.NumCPU()),
+               KeyLength:  32,
+       }
+
        // Generate a random salt
        salt := make([]byte, 16)
-       if _, err := io.ReadFull(rand.Reader, salt); err != nil {
-               return "", "", err
+       _, err := rand.Read(salt)
+       if err != nil {
+               return "", err
        }
+       config.Salt = salt
 
        // Hash password
-       hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 4, 32)
+       config.HashRaw = argon2.IDKey([]byte(password), salt, config.TimeCost, config.MemoryCost, config.Threads, config.KeyLength)
+
+       // Generate standardized hash format
+       encodedHash := fmt.Sprintf(
+               "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
+               argon2.Version,
+               config.MemoryCost,
+               config.TimeCost,
+               config.Threads,
+               base64.RawStdEncoding.EncodeToString(config.Salt),
+               base64.RawStdEncoding.EncodeToString(config.HashRaw),
+       )
+
+       return encodedHash, nil
+}
 
-       // Encode salt and hash to base64
-       saltBase64 := base64.StdEncoding.EncodeToString(salt)
-       hashBase64 := base64.StdEncoding.EncodeToString(hash)
+func parseArgon2Hash(encodedHash string) (*Argon2Configuration, error) {
+       components := strings.Split(encodedHash, "$")
+       if len(components) != 6 {
+               return nil, fmt.Errorf("invalid hash format structure")
+       }
 
-       return hashBase64, saltBase64, nil
-}
+       // Validate algorithm identifier
+       if !strings.HasPrefix(components[1], "argon2id") {
+               return nil, fmt.Errorf("unsupported algorithm variant")
+       }
 
-// VerifyPassword verifies if a password matches a hash
-func VerifyPassword(password, hashBase64, saltBase64 string) bool {
-       // Decode salt and hash
-       salt, err := base64.StdEncoding.DecodeString(saltBase64)
+       // Extract version information
+       var version int
+       fmt.Sscanf(components[2], "v=%d", &version)
+
+       // Parse configuration parameters
+       config := &Argon2Configuration{}
+       fmt.Sscanf(components[3], "m=%d,t=%d,p=%d",
+               &config.MemoryCost, &config.TimeCost, &config.Threads)
+
+       // Decode salt component
+       salt, err := base64.RawStdEncoding.DecodeString(components[4])
        if err != nil {
-               return false
+               return nil, fmt.Errorf("salt decoding failed: %w", err)
        }
+       config.Salt = salt
 
-       _, err = base64.StdEncoding.DecodeString(hashBase64)
+       // Decode hash component
+       hash, err := base64.RawStdEncoding.DecodeString(components[5])
        if err != nil {
-               return false
+               return nil, fmt.Errorf("hash decoding failed: %w", err)
        }
+       config.HashRaw = hash
+       config.KeyLength = uint32(len(hash))
 
-       // Hash the provided password with the same salt
-       hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 4, 32)
+       return config, nil
+}
+
+// VerifyPassword verifies if a password matches a hash
+func VerifyPassword(password, hashBase64 string) bool {
+       // Parse stored hash parameters
+       config, err := parseArgon2Hash(hashBase64)
+       if err != nil {
+               return false
+       }
 
-       // Compare hashes
-       return base64.StdEncoding.EncodeToString(hash) == hashBase64
+       // Generate hash using identical parameters
+       computedHash := argon2.IDKey(
+               []byte(password),
+               config.Salt,
+               config.TimeCost,
+               config.MemoryCost,
+               config.Threads,
+               config.KeyLength,
+       )
+
+       // Perform constant-time comparison to prevent timing attacks
+       return subtle.ConstantTimeCompare(config.HashRaw, computedHash) == 1
 }
 
 // DeriveKeyFromPassword derives a key from a password and salt
index beb7d3e0d519954c9fb6640e68ece161a753f29d..4df7f677ab57b93b9f6a9597d2ff0465c8dc54aa 100644 (file)
 
                                        settingsModal.hide();
                                } else {
-                                       throw new Error('Error saving settings');
+                                       console.error('Error saving settings');
                                }
                        })
                        .catch((error) => {
 
                localStorage.setItem('autoLoadImagesThisDevice', $autoLoadImagesThisDevice);
        });
+
+       let currentPassword = $state('');
+       let newPassword = $state('');
+       let confirmNewPassword = $state('');
+       let changePasswordNotEqual = $state(false);
+       let isChangingPassword = $state(false);
+       let changingPasswordSuccess = $state(false);
+       let changingPasswordError = $state(false);
+       let changingPasswordIncorrect = $state(false);
+
+       function changePassword() {
+               changePasswordNotEqual = false;
+               changingPasswordSuccess = false;
+               changingPasswordError = false;
+               changingPasswordIncorrect = false;
+
+               if (newPassword !== confirmNewPassword) {
+                       changePasswordNotEqual = true;
+                       return;
+               }
+
+               if (isChangingPassword) return;
+               isChangingPassword = true;
+
+               axios
+                       .post(API_URL + '/users/changePassword', {
+                               old_password: currentPassword,
+                               new_password: newPassword
+                       })
+                       .then((response) => {
+                               if (response.data.success) {
+                                       changingPasswordSuccess = true;
+                               } else {
+                                       changingPasswordError = true;
+                                       console.error('Error changing password');
+                                       if (response.data.password_incorrect) {
+                                               changingPasswordIncorrect = true;
+                                       }
+                               }
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                               console.log('Error on Changing password:', error.response.data.message);
+                               changingPasswordError = true;
+                       })
+                       .finally(() => {
+                               isChangingPassword = false;
+                       });
+       }
 </script>
 
 <main class="d-flex flex-column">
 
                                                                <div id="security">
                                                                        <h3 class="text-primary">🔒 Sicherheit</h3>
-                                                                       <div id="password"><h5>Password ändern</h5></div>
+                                                                       <div id="password">
+                                                                               <h5>Password ändern</h5>
+                                                                               <form onsubmit={changePassword}>
+                                                                                       <div class="form-floating mb-3">
+                                                                                               <input
+                                                                                                       type="password"
+                                                                                                       class="form-control"
+                                                                                                       id="currentPassword"
+                                                                                                       placeholder="Aktuelles Passwort"
+                                                                                                       bind:value={currentPassword}
+                                                                                               />
+                                                                                               <label for="currentPassword">Aktuelles Passwort</label>
+                                                                                       </div>
+                                                                                       <div class="form-floating mb-3">
+                                                                                               <input
+                                                                                                       type="password"
+                                                                                                       class="form-control"
+                                                                                                       id="newPassword"
+                                                                                                       placeholder="Neues Passwort"
+                                                                                                       bind:value={newPassword}
+                                                                                               />
+                                                                                               <label for="newPassword">Neues Passwort</label>
+                                                                                       </div>
+                                                                                       <div class="form-floating mb-3">
+                                                                                               <input
+                                                                                                       type="password"
+                                                                                                       class="form-control"
+                                                                                                       id="confirmNewPassword"
+                                                                                                       placeholder="Neues Passwort bestätigen"
+                                                                                                       bind:value={confirmNewPassword}
+                                                                                               />
+                                                                                               <label for="confirmNewPassword">Neues Passwort bestätigen</label>
+                                                                                       </div>
+                                                                                       <button class="btn btn-primary" onclick={changePassword}>
+                                                                                               {#if isChangingPassword}
+                                                                                                       <!-- svelte-ignore a11y_no_static_element_interactions -->
+                                                                                                       <div class="spinner-border" role="status">
+                                                                                                               <span class="visually-hidden">Loading...</span>
+                                                                                                       </div>
+                                                                                               {/if}
+                                                                                               Passwort ändern
+                                                                                       </button>
+                                                                               </form>
+                                                                               {#if changePasswordNotEqual}
+                                                                                       <div class="alert alert-danger mt-2" role="alert" transition:slide>
+                                                                                               Die neuen Passwörter stimmen nicht überein!
+                                                                                       </div>
+                                                                               {/if}
+                                                                               {#if changingPasswordSuccess}
+                                                                                       <div class="alert alert-success mt-2" role="alert" transition:slide>
+                                                                                               Das Passwort wurde erfolgreich geändert!<br />
+                                                                                               Backup-Keys wurden ungültig gemacht (sofern vorhanden), und müssen neu erstellt
+                                                                                               werden.
+                                                                                       </div>
+                                                                               {/if}
+                                                                               {#if changingPasswordIncorrect}
+                                                                                       <div class="alert alert-danger mt-2" role="alert" transition:slide>
+                                                                                               Das aktuelle Passwort ist falsch!
+                                                                                       </div>
+                                                                               {:else if changingPasswordError}
+                                                                                       <div class="alert alert-danger mt-2" role="alert" transition:slide>
+                                                                                               Fehler beim Ändern des Passworts!
+                                                                                       </div>
+                                                                               {/if}
+                                                                       </div>
                                                                        <div id="backupkeys"><h5>Backup-Keys</h5></div>
                                                                        <div id="username"><h5>Username ändern</h5></div>
                                                                        <div id="deleteaccount"><h5>Konto löschen</h5></div>
                                        </button>
                                </div>
                        </div>
+               </div>
+       </div>
 
-                       <div class="toast-container position-fixed bottom-0 end-0 p-3">
-                               <div
-                                       id="toastSuccessEditTag"
-                                       class="toast align-items-center text-bg-success"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Änderungen wurden gespeichert!</div>
-                                       </div>
-                               </div>
+       <div class="toast-container position-fixed bottom-0 end-0 p-3">
+               <div
+                       id="toastSuccessEditTag"
+                       class="toast align-items-center text-bg-success"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Änderungen wurden gespeichert!</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastErrorEditTag"
-                                       class="toast align-items-center text-bg-danger"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Fehler beim Speichern der Änderungen!</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastErrorEditTag"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Fehler beim Speichern der Änderungen!</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastErrorDeleteTag"
-                                       class="toast align-items-center text-bg-danger"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Fehler beim Löschen des Tags!</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastErrorDeleteTag"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Fehler beim Löschen des Tags!</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastSuccessSaveSettings"
-                                       class="toast align-items-center text-bg-success"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Einstellungen gespeichert!</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastSuccessSaveSettings"
+                       class="toast align-items-center text-bg-success"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Einstellungen gespeichert!</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastErrorSaveSettings"
-                                       class="toast align-items-center text-bg-danger"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Fehler beim Speichern der Einstellungen!</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastErrorSaveSettings"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Fehler beim Speichern der Einstellungen!</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastErrorInvalidTemplateEmpty"
-                                       class="toast align-items-center text-bg-danger"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Name oder Inhalt einer Vorlage dürfen nicht leer sein!</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastErrorInvalidTemplateEmpty"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Name oder Inhalt einer Vorlage dürfen nicht leer sein!</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastErrorInvalidTemplateDouble"
-                                       class="toast align-items-center text-bg-danger"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Name der Vorlage existiert bereits</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastErrorInvalidTemplateDouble"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Name der Vorlage existiert bereits</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastSuccessSaveTemplate"
-                                       class="toast align-items-center text-bg-success"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Vorlage gespeichert</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastSuccessSaveTemplate"
+                       class="toast align-items-center text-bg-success"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Vorlage gespeichert</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastErrorDeletingTemplate"
-                                       class="toast align-items-center text-bg-danger"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Fehler beim Löschen der Vorlage</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastErrorDeletingTemplate"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Fehler beim Löschen der Vorlage</div>
+                       </div>
+               </div>
 
-                               <div
-                                       id="toastSuccessDeletingTemplate"
-                                       class="toast align-items-center text-bg-success"
-                                       role="alert"
-                                       aria-live="assertive"
-                                       aria-atomic="true"
-                               >
-                                       <div class="d-flex">
-                                               <div class="toast-body">Vorlage gelöscht</div>
-                                       </div>
-                               </div>
+               <div
+                       id="toastSuccessDeletingTemplate"
+                       class="toast align-items-center text-bg-success"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Vorlage gelöscht</div>
                        </div>
                </div>
        </div>
git clone https://git.99rst.org/PROJECT