Login: Passwort-Auge, aenderbare Serveradresse, laenger stehende Fehlermeldung
All checks were successful
Build APK / build-apk (push) Has been skipped

- Auge-Symbol im Passwortfeld. Haben alle anderen AWL-Apps seit 06.07.2026; NetDiag
  war die letzte ohne. Grund: bei falschem Tastaturlayout tippt man sich sonst blind
  fest. Der Typ wird imperativ am Element gesetzt, weil Svelte 5 ein dynamisches
  type-Attribut an einem Feld mit bind:value verbietet; type="button" am Schalter ist
  Pflicht, sonst schickt er das Formular ab.
- Serveradresse laesst sich in den Einstellungen aendern. Bisher stand sie dort nur
  als Text — eine falsche Adresse liess sich nur durch Abmelden korrigieren. Ein
  Wechsel meldet bewusst ab, weil das Token nur beim bisherigen Server gilt.
- Login-Fehler bleiben 8 s stehen statt 3 s und zeigen die Servermeldung im Wortlaut.
  Bei 429 steht dort die Wartezeit der Brute-Force-Bremse, bei 403 der Grund.

Im Emulator (Pixel 6 / Android 14) gegen das Test-Dolibarr geprueft: Ein- und
Ausblenden des Passworts, Anmeldung ueber awlauth, Auftragsliste, Einstellungen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-08-14 18:23:43 +02:00
parent 3c95ff6b07
commit 4189448c56
2 changed files with 125 additions and 12 deletions

View file

@ -2,7 +2,8 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import AppHeader from '$lib/components/AppHeader.svelte'; import AppHeader from '$lib/components/AppHeader.svelte';
import { auth } from '$lib/auth.svelte'; import { auth } from '$lib/auth.svelte';
import { getServerUrl } from '$lib/api'; import { getServerUrl, setServerUrl } from '$lib/api';
import { Capacitor } from '@capacitor/core';
import { sync } from '$lib/sync.svelte'; import { sync } from '$lib/sync.svelte';
import { toast } from '$lib/toast.svelte'; import { toast } from '$lib/toast.svelte';
import { APP_VERSION, checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater'; import { APP_VERSION, checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater';
@ -62,6 +63,33 @@
sync.stop(); sync.stop();
goto('/login/'); goto('/login/');
} }
/* --- Server-Adresse ändern ------------------------------------------- */
// Nur auf dem Gerät sinnvoll: im Browser läuft alles über den Vite-Proxy.
const canEditServer = Capacitor.isNativePlatform();
let editServer = $state(false);
let serverInput = $state(getServerUrl());
/**
* Neue Server-Adresse übernehmen. Das Token gilt nur beim alten Server,
* deshalb wird bewusst abgemeldet statt so zu tun, als liefe alles weiter.
*/
async function saveServer() {
const url = serverInput.trim();
if (!url) {
toast.show('Server-Adresse fehlt', 'error');
return;
}
if (!/^https?:\/\//i.test(url)) {
toast.show('Adresse muss mit http:// oder https:// beginnen', 'error', 5000);
return;
}
await setServerUrl(url);
await auth.logout();
sync.stop();
toast.show('Server geändert — bitte neu anmelden', 'info', 5000);
goto('/login/');
}
</script> </script>
<AppHeader title="Einstellungen" back /> <AppHeader title="Einstellungen" back />
@ -113,9 +141,54 @@
<section class="rounded-lg bg-zinc-900 p-4"> <section class="rounded-lg bg-zinc-900 p-4">
<h2 class="mb-2 text-sm font-semibold text-zinc-300">Synchronisierung</h2> <h2 class="mb-2 text-sm font-semibold text-zinc-300">Synchronisierung</h2>
<p class="text-sm text-zinc-300">{syncLabel}</p> <p class="text-sm text-zinc-300">{syncLabel}</p>
{#if editServer}
<div class="mt-2 flex flex-col gap-2">
<input
class="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm"
type="url"
inputmode="url"
autocapitalize="off"
autocorrect="off"
placeholder="https://dolibarr.example.de"
bind:value={serverInput}
/>
<div class="flex gap-2">
<button
class="flex-1 rounded bg-zinc-800 px-3 py-2 text-sm active:bg-zinc-700"
onclick={() => {
editServer = false;
serverInput = getServerUrl();
}}
>
Abbrechen
</button>
<button
class="flex-1 rounded bg-sky-700 px-3 py-2 text-sm font-semibold text-white active:bg-sky-800"
onclick={saveServer}
>
Speichern
</button>
</div>
<p class="text-xs text-zinc-500">
Ein Wechsel meldet ab — das Token gilt nur beim bisherigen Server.
</p>
</div>
{:else}
<p class="mt-0.5 break-all text-xs text-zinc-500"> <p class="mt-0.5 break-all text-xs text-zinc-500">
Server: {getServerUrl() || '(Browser-Proxy)'} Server: {getServerUrl() || '(Browser-Proxy)'}
{#if canEditServer}
<button
class="ml-1 text-sky-400 underline active:text-sky-300"
onclick={() => {
serverInput = getServerUrl();
editServer = true;
}}
>
ändern
</button>
{/if}
</p> </p>
{/if}
<button <button
class="mt-3 w-full rounded bg-zinc-800 px-3 py-2 text-sm active:bg-zinc-700 disabled:opacity-50" class="mt-3 w-full rounded bg-zinc-800 px-3 py-2 text-sm active:bg-zinc-700 disabled:opacity-50"
onclick={() => sync.syncManual()} onclick={() => sync.syncManual()}

View file

@ -5,6 +5,7 @@
import { getServerUrl, setServerUrl, ApiError } from '$lib/api'; import { getServerUrl, setServerUrl, ApiError } from '$lib/api';
import { toast } from '$lib/toast.svelte'; import { toast } from '$lib/toast.svelte';
import { Capacitor } from '@capacitor/core'; import { Capacitor } from '@capacitor/core';
import { Eye, EyeOff } from 'lucide-svelte';
let server = $state(getServerUrl()); let server = $state(getServerUrl());
let loginName = $state(''); let loginName = $state('');
@ -14,6 +15,21 @@
// Server-URL nur auf dem Gerät nötig (im Browser läuft der Vite-Proxy) // Server-URL nur auf dem Gerät nötig (im Browser läuft der Vite-Proxy)
const needsServer = Capacitor.isNativePlatform(); const needsServer = Capacitor.isNativePlatform();
/**
* Passwort ein-/ausblenden.
*
* Der Typ wird imperativ am Element gesetzt: Svelte 5 verbietet ein
* dynamisches `type` an einem Feld mit `bind:value`. Grund für den Schalter:
* bei falschem Tastaturlayout tippt man sich sonst blind fest.
*/
let pwEl: HTMLInputElement;
let pwVisible = $state(false);
function togglePw() {
pwVisible = !pwVisible;
if (pwEl) pwEl.type = pwVisible ? 'text' : 'password';
}
async function submit(e: Event) { async function submit(e: Event) {
e.preventDefault(); e.preventDefault();
if (busy) return; if (busy) return;
@ -28,7 +44,14 @@
toast.show('Angemeldet', 'success'); toast.show('Angemeldet', 'success');
goto('/auftraege/'); goto('/auftraege/');
} catch (err) { } catch (err) {
toast.show(err instanceof ApiError ? err.message : 'Anmeldung fehlgeschlagen', 'error'); // Servermeldung im Wortlaut zeigen und stehen lassen — bei 429 steht dort
// die Wartezeit, bei 403 der Grund. Eine Meldung, die nach 3 s verschwindet,
// ist auf der Baustelle wertlos.
toast.show(
err instanceof ApiError ? err.message : 'Anmeldung fehlgeschlagen',
'error',
8000,
);
} finally { } finally {
busy = false; busy = false;
} }
@ -66,13 +89,30 @@
</label> </label>
<label class="flex flex-col gap-1 text-sm"> <label class="flex flex-col gap-1 text-sm">
<span class="text-zinc-400">Passwort</span> <span class="text-zinc-400">Passwort</span>
<div class="relative">
<input <input
class="rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2" class="w-full rounded-lg border border-zinc-700 bg-zinc-800 py-2 pl-3 pr-12"
type="password" type="password"
bind:this={pwEl}
bind:value={password} bind:value={password}
autocomplete="current-password" autocomplete="current-password"
required required
/> />
<!-- type="button" ist Pflicht: sonst schickt der Schalter das Formular ab -->
<button
type="button"
class="absolute right-0 top-0 flex h-full w-12 items-center justify-center text-zinc-400 active:text-zinc-200"
onclick={togglePw}
aria-label={pwVisible ? 'Passwort verbergen' : 'Passwort anzeigen'}
aria-pressed={pwVisible}
>
{#if pwVisible}
<EyeOff size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</label> </label>
<button <button
class="mt-2 rounded-lg bg-sky-600 py-2.5 font-semibold text-white active:bg-sky-700 disabled:opacity-50" class="mt-2 rounded-lg bg-sky-600 py-2.5 font-semibold text-white active:bg-sky-700 disabled:opacity-50"