added feature to reauthenticate on pageload
authorPhiTux <redacted>
Thu, 11 Sep 2025 15:08:39 +0000 (17:08 +0200)
committerPhiTux <redacted>
Thu, 11 Sep 2025 15:08:39 +0000 (17:08 +0200)
12 files changed:
backend/handlers/users.go
backend/main.go
frontend/src/lib/Datepicker.svelte
frontend/src/lib/Sidenav.svelte
frontend/src/lib/assets/GitHub-Logo.png [new file with mode: 0644]
frontend/src/lib/assets/bmc-button.png [new file with mode: 0644]
frontend/src/lib/helpers.js
frontend/src/routes/(authed)/+layout.svelte
frontend/src/routes/(authed)/read/+page.svelte
frontend/src/routes/(authed)/write/+page.svelte
frontend/src/routes/login/+page.svelte
frontend/src/routes/reauth/+page.svelte [new file with mode: 0644]

index 3b98b20b78c4e00e85b2942e451d328f09d65aed..1cd6b612fffe05f9938640e35a954424ebbda1ac 100644 (file)
@@ -439,6 +439,7 @@ func GetDefaultSettings() map[string]any {
                "monochromeBackgroundColor":  "#ececec",
                "checkForUpdates":            true,
                "includeTestVersions":        false,
+               "requirePasswordOnPageLoad":  false,
        }
 }
 
@@ -1145,3 +1146,33 @@ func ChangeUsername(w http.ResponseWriter, r *http.Request) {
                "available_backup_codes": availableBackupCodes,
        })
 }
+
+// ValidatePasswordRequest represents the validate password request body
+type ValidatePasswordRequest struct {
+       Password string `json:"password"`
+}
+
+// ValidatePassword validates the user's password for re-authentication
+func ValidatePassword(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse the request body
+       var req ValidatePasswordRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Validate password using the same method as login
+       derived_key, available_backup_codes, _ := utils.CheckPasswordForUser(userID, req.Password)
+
+       utils.JSONResponse(w, http.StatusOK, map[string]any{
+               "valid":                  derived_key != "",
+               "available_backup_codes": available_backup_codes,
+       })
+}
index c7aae836f311e4407362a3b48e759f4773e48c3b..630215e6d3f43ec760f813900f9857904d5c6a9c 100644 (file)
@@ -76,6 +76,7 @@ func main() {
        mux.HandleFunc("POST /users/changeUsername", middleware.RequireAuth(handlers.ChangeUsername))
        mux.HandleFunc("POST /users/deleteAccount", middleware.RequireAuth(handlers.DeleteAccount))
        mux.HandleFunc("POST /users/createBackupCodes", middleware.RequireAuth(handlers.CreateBackupCodes))
+       mux.HandleFunc("POST /users/validatePassword", middleware.RequireAuth(handlers.ValidatePassword))
        mux.HandleFunc("GET /users/statistics", middleware.RequireAuth(handlers.GetStatistics))
 
        mux.HandleFunc("POST /logs/saveLog", middleware.RequireAuth(handlers.SaveLog))
index 0d0e6377b1ad0fc8d79855c95d70daec771f7b41..96a5c995268be0ee25ca156ba62c48924cc6e126 100644 (file)
@@ -3,7 +3,7 @@
        import { onMount } from 'svelte';
        import { fly } from 'svelte/transition';
        import * as bootstrap from 'bootstrap';
-       import { offcanvasIsOpen, sameDate } from '$lib/helpers.js';
+       import { offcanvasIsOpen, sameDate, isAuthenticated } from '$lib/helpers.js';
        import { getTranslate } from '@tolgee/svelte';
 
        const { t } = getTranslate();
@@ -88,7 +88,7 @@
        };
 
        $effect(() => {
-               if (window.location.href) {
+               if ($isAuthenticated && window.location.href) {
                        setTimeout(() => {
                                oc = document.querySelector('.offcanvas');
                                oc.addEventListener('hidden.bs.offcanvas', () => {
index 2ae549bad0cc65f3c45b36d0f26597e91969b431..7a1649d27d5ff9aaa149cecc62afd0315396a7ca 100644 (file)
@@ -8,7 +8,7 @@
        import { onMount } from 'svelte';
        import * as bootstrap from 'bootstrap';
        import Tag from './Tag.svelte';
-       import { offcanvasIsOpen, sameDate } from '$lib/helpers.js';
+       import { isAuthenticated, offcanvasIsOpen, sameDate } from '$lib/helpers.js';
        import { API_URL } from '$lib/APIurl.js';
        import axios from 'axios';
        import { cal } from '$lib/calendarStore.js';
@@ -58,7 +58,7 @@
        }
 
        $effect(() => {
-               if (window.location.href) {
+               if ($isAuthenticated && window.location.href) {
                        setTimeout(() => {
                                oc = document.querySelector('.offcanvas');
                                oc.addEventListener('hidden.bs.offcanvas', () => {
diff --git a/frontend/src/lib/assets/GitHub-Logo.png b/frontend/src/lib/assets/GitHub-Logo.png
new file mode 100644 (file)
index 0000000..84ed908
Binary files /dev/null and b/frontend/src/lib/assets/GitHub-Logo.png differ
diff --git a/frontend/src/lib/assets/bmc-button.png b/frontend/src/lib/assets/bmc-button.png
new file mode 100644 (file)
index 0000000..464bfd9
Binary files /dev/null and b/frontend/src/lib/assets/bmc-button.png differ
index 62136089af13957d5852fd277fef8b7236cd6811..ad37747dbd6650868bacedea0ba8a365c847879c 100644 (file)
@@ -21,6 +21,26 @@ function sameDate(date1, date2) {
        );
 }
 
+export const isAuthenticated = writable(false);
+
+// Function to check if page load authentication is required
+function needsReauthentication() {
+       isAuthenticated.subscribe((value) => {
+               if (value) return false;
+       })
+
+       if (typeof window === 'undefined') return false;
+
+       // Check localStorage for re-auth requirement
+       const requireReauth = localStorage.getItem('requirePasswordOnPageLoad');
+       
+       if (requireReauth !== 'true') {
+               isAuthenticated.set(true);
+       }
+
+       return requireReauth === 'true';
+}
+
 function generateNeonMesh(dark) {
        /* const baseColors = ['#ff00ff', '#00ffff', '#ffea00', '#ff0080', '#00ff80', '#ff4500']; */
        const baseColors = ["#ff00ff", "#00ffff", "#ffea00", "#ff0080", "#00ff80", "#ff4500",
@@ -56,7 +76,7 @@ function loadFlagEmoji(language) {
        return json[language] || '';
 }
 
-export { formatBytes, sameDate, loadFlagEmoji, generateNeonMesh };
+export { formatBytes, sameDate, needsReauthentication, generateNeonMesh, loadFlagEmoji };
 
 export let alwaysShowSidenav = writable(true);
 
index 5a5e3875864706b391115630fc383f9c8855c3b2..b0838f3940cbdadc7019ce930ef3c67ad3fdf1d8 100644 (file)
@@ -2,7 +2,7 @@
        import * as bootstrap from 'bootstrap';
        import Fa from 'svelte-fa';
        import { goto } from '$app/navigation';
-       import { onMount } from 'svelte';
+       import { onDestroy, onMount } from 'svelte';
        import {
                readingMode,
                settings,
        import { API_URL } from '$lib/APIurl.js';
        import { tags } from '$lib/tagStore.js';
        import TagModal from '$lib/TagModal.svelte';
-       import { alwaysShowSidenav, generateNeonMesh, loadFlagEmoji } from '$lib/helpers.js';
+       import {
+               alwaysShowSidenav,
+               generateNeonMesh,
+               loadFlagEmoji,
+               needsReauthentication,
+               isAuthenticated
+       } from '$lib/helpers.js';
        import { templates } from '$lib/templateStore';
        import {
                faRightFromBracket,
                }
        });
 
+       onDestroy(() => {
+               $isAuthenticated = false;
+       });
+
        onMount(() => {
+               let needsReauth = needsReauthentication();
+
+               // Check if re-authentication is needed FIRST
+               if (!$isAuthenticated && needsReauth) {
+                       // Save current route for return after reauth
+                       localStorage.setItem('returnAfterReauth', window.location.pathname);
+                       goto('/reauth');
+                       return; // Stop further initialization
+               }
+
+               // Normal initialization only if authenticated
                getUserSettings();
                getTemplates();
                getVersionInfo();
                                $settings = response.data;
                                $tempSettings = JSON.parse(JSON.stringify($settings));
                                aLookBackYears = $settings.aLookBackYears.toString();
+
+                               // Save re-auth setting to localStorage for immediate availability
+                               localStorage.setItem(
+                                       'requirePasswordOnPageLoad',
+                                       $settings.requirePasswordOnPageLoad.toString()
+                               );
+
                                updateLanguage();
 
                                // set background
                                if (response.data.success) {
                                        $settings = $tempSettings;
 
+                                       // Save re-auth setting to localStorage for immediate availability
+                                       localStorage.setItem(
+                                               'requirePasswordOnPageLoad',
+                                               $tempSettings.requirePasswordOnPageLoad.toString()
+                                       );
+
                                        // update language
                                        updateLanguage();
 
                                                                                </div>
                                                                        </div>
                                                                        <div id="loginonreload">
-                                                                               <h5>Login bei Reload</h5>
-                                                                               Bla<br />
-                                                                               blub <br />
-                                                                               bla <br />
-                                                                               blub <br />
+                                                                               {#if $tempSettings.requirePasswordOnPageLoad !== $settings.requirePasswordOnPageLoad}
+                                                                                       {@render unsavedChanges()}
+                                                                               {/if}
+
+                                                                               <h5>🔒 {$t('settings.reauth.title')}</h5>
+                                                                               {$t('settings.reauth.description')}
+
+                                                                               <div class="form-check form-switch mt-2">
+                                                                                       <input
+                                                                                               class="form-check-input"
+                                                                                               bind:checked={$tempSettings.requirePasswordOnPageLoad}
+                                                                                               type="checkbox"
+                                                                                               role="switch"
+                                                                                               id="requirePasswordOnPageLoadSwitch"
+                                                                                       />
+                                                                                       <label class="form-check-label" for="requirePasswordOnPageLoadSwitch">
+                                                                                               {$t('settings.reauth.label')}
+                                                                                       </label>
+                                                                               </div>
                                                                        </div>
                                                                </div>
 
index acf180d5eb4a6caf69ff805cd93ffe3dcbb8f983..b694ff6e008c0d32a5fd26d0a7020b9540e708e4 100644 (file)
@@ -13,7 +13,7 @@
        import { faCloudArrowDown } from '@fortawesome/free-solid-svg-icons';
        import { Fa } from 'svelte-fa';
        import ImageViewer from '$lib/ImageViewer.svelte';
-       import { alwaysShowSidenav } from '$lib/helpers.js';
+       import { alwaysShowSidenav, needsReauthentication, isAuthenticated } from '$lib/helpers.js';
        import { getTranslate } from '@tolgee/svelte';
 
        const { t } = getTranslate();
        let observer;
 
        onMount(() => {
+               /* if (!$isAuthenticated && needsReauthentication()) {
+                       return;
+               } */
+
                loadMonthForReading();
 
                // Highlights automatically the day in the calendar, when the log is in the viewport
index c352e1b4c41a54d71aeab6212d53151a1fcff0ba..0f378d4227ca7b6060d0741129c8e71175017527 100644 (file)
@@ -32,7 +32,7 @@
        import { formatBytes, alwaysShowSidenav, sameDate } from '$lib/helpers.js';
        import ImageViewer from '$lib/ImageViewer.svelte';
        import TemplateDropdown from '$lib/TemplateDropdown.svelte';
-       import { templates, insertTemplate } from '$lib/templateStore';
+       import { insertTemplate } from '$lib/templateStore';
        import ALookBack from '$lib/ALookBack.svelte';
        import { marked } from 'marked';
        import { T, getTranslate } from '@tolgee/svelte';
        let aLookBack = $state([]);
 
        function getALookBack() {
-               if (!$settings.useALookBack) {
+               // Skip if settings not loaded yet
+               if (!$settings || $settings.useALookBack === undefined || !$settings.useALookBack) {
                        aLookBack = [];
                        return;
                }
                        });
        }
 
+       // Re-trigger aLookBack when settings are loaded/changed
+       $effect(() => {
+               if ($settings && $settings.useALookBack !== undefined) {
+                       getALookBack();
+               }
+       });
+
        const imageExtensions = ['jpeg', 'jpg', 'gif', 'png', 'webp'];
        //TODO: support svg? -> minsize is necessary...
 
index 406b13c55bbf165fec4201023780e9ca3c0ba187..d7a54906b9a6ac2cab04c8b16512c184f6fc95fd 100644 (file)
@@ -6,7 +6,7 @@
        import { goto } from '$app/navigation';
        import { API_URL } from '$lib/APIurl.js';
        import { getTranslate, getTolgee } from '@tolgee/svelte';
-       import { loadFlagEmoji } from '$lib/helpers.js';
+       import { isAuthenticated, loadFlagEmoji } from '$lib/helpers.js';
 
        const { t } = getTranslate();
        const tolgee = getTolgee(['language']);
 
                                        handleMigrationProgress(response.data.username);
                                } else {
+                                       $isAuthenticated = true;
                                        localStorage.setItem('user', response.data.username);
                                        goto('/write');
                                }
diff --git a/frontend/src/routes/reauth/+page.svelte b/frontend/src/routes/reauth/+page.svelte
new file mode 100644 (file)
index 0000000..bc50b75
--- /dev/null
@@ -0,0 +1,144 @@
+<script>
+       import { goto } from '$app/navigation';
+       import { onMount } from 'svelte';
+       import axios from 'axios';
+       import { API_URL } from '$lib/APIurl.js';
+       import Fa from 'svelte-fa';
+       import { faLock } from '@fortawesome/free-solid-svg-icons';
+       import { generateNeonMesh, isAuthenticated } from '$lib/helpers';
+       import { getTranslate } from '@tolgee/svelte';
+       import logo from '$lib/assets/locked_heart_with_keyhole.svg';
+
+       const { t } = getTranslate();
+
+       let password = $state('');
+       let isValidating = $state(false);
+       let error = $state('');
+
+       onMount(() => {
+               // get browser default-light/dark-mode settings
+               const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+               generateNeonMesh(prefersDark);
+
+               // Focus password input
+               const passwordInput = document.getElementById('reauth-password');
+               if (passwordInput) {
+                       passwordInput.focus();
+               }
+       });
+
+       async function validatePassword() {
+               if (isValidating || !password.trim()) return;
+
+               isValidating = true;
+               error = '';
+
+               try {
+                       const response = await axios.post(API_URL + '/users/validatePassword', {
+                               password: password
+                       });
+
+                       if (response.data.valid) {
+                               $isAuthenticated = true;
+
+                               // Authentication successful - return to original route
+                               const returnPath = localStorage.getItem('returnAfterReauth') || '/write';
+                               localStorage.removeItem('returnAfterReauth');
+                               goto(returnPath);
+                       } else {
+                               error = $t('reauth.wrong_password');
+                               password = '';
+
+                               // Focus password input
+                               const passwordInput = document.getElementById('reauth-password');
+                               if (passwordInput) {
+                                       passwordInput.focus();
+                               }
+                       }
+               } catch (err) {
+                       console.error('Password validation error:', err);
+                       error = $t('reauth.authentication_error');
+               } finally {
+                       isValidating = false;
+               }
+       }
+
+       function handleKeydown(event) {
+               if (event.key === 'Enter') {
+                       validatePassword();
+               }
+       }
+</script>
+
+<div class="background container-fluid h-100 d-flex align-items-center justify-content-center">
+       <div class="card shadow-lg" style="max-width: 400px; width: 100%;">
+               <div class="card-header text-center bg-primary text-white">
+                       <div class="d-flex justify-content-center align-items-center mb-3">
+                               <img src={logo} alt="" width="50px" />
+                               <span class="dailytxt ms-2">DailyTxT</span>
+                       </div>
+                       <h3 class="mb-0">{$t('reauth.title')}</h3>
+               </div>
+               <div class="card-body p-4">
+                       <p class="text-muted text-center mb-4">
+                               {$t('reauth.description')}
+                       </p>
+
+                       <form onsubmit={validatePassword}>
+                               <div class="mb-3 position-relative">
+                                       <input
+                                               id="reauth-password"
+                                               type="password"
+                                               bind:value={password}
+                                               onkeydown={handleKeydown}
+                                               class="form-control form-control-lg pe-5"
+                                               placeholder={$t('login.password')}
+                                               disabled={isValidating}
+                                               required
+                                       />
+                               </div>
+
+                               {#if error}
+                                       <div class="alert alert-danger">{error}</div>
+                               {/if}
+
+                               <button
+                                       type="submit"
+                                       class="btn btn-primary btn-lg w-100"
+                                       disabled={isValidating || !password.trim()}
+                               >
+                                       {#if isValidating}
+                                               <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"
+                                               ></span>
+                                       {/if}
+                                       {$t('reauth.confirmButton')}
+                               </button>
+                       </form>
+               </div>
+       </div>
+</div>
+
+<style scoped>
+       .dailytxt {
+               color: #f57c00;
+               font-size: 2rem;
+               font-weight: 500;
+       }
+
+       .container-fluid {
+               /* background: linear-gradient(135deg, #899cf1 0%, #a24b4b 100%); */
+               min-height: 100vh;
+       }
+
+       .card {
+               backdrop-filter: blur(10px);
+               background: rgba(255, 255, 255, 0.5);
+               border: none;
+               border-radius: 10px;
+       }
+
+       .card-header {
+               border-top-left-radius: 10px;
+               border-top-right-radius: 10px;
+       }
+</style>
git clone https://git.99rst.org/PROJECT