Mit AWL-Konto anmelden: awlauth-Seite im Chrome-Tab statt Passwort in der App [apk]
All checks were successful
Build APK / build-apk (push) Successful in 6m6s
All checks were successful
Build APK / build-apk (push) Successful in 6m6s
Der Knopf oeffnet die awlauth-Anmeldeseite im Chrome-Tab (@capacitor/browser), der die Anmeldung mit Chrome teilt - wer im Handy-Browser angemeldet ist, ist ohne Eingabe zurueck in der App. Ruecksprung ueber das eigene Schema de.data-it-solution.netdiag:/sso (Intent-Filter), abgesichert mit state und PKCE; verarbeitet im Layout (appUrlOpen + getLaunchUrl). Passwort und Fingerabdruck bleiben. Mitgenommen: die bisher nicht committete README-Ergaenzung vom 19.08. (Plugin-Dateien, "Was noch offen ist"). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
3f29a373ac
commit
ad5a102565
8 changed files with 320 additions and 9 deletions
51
README.md
51
README.md
|
|
@ -38,6 +38,7 @@ die APK in die Package Registry (`netdiag-apk`). Siehe `.forgejo/workflows/build
|
|||
src/lib/
|
||||
api.ts JSON-API-Client (JWT, 401-Refresh, Timeout)
|
||||
auth.svelte.ts Anmelde-Status
|
||||
sso.ts Anmeldung per AWL-Konto (Chrome-Tab, PKCE, Rücksprung)
|
||||
db.ts Offline-Speicher (SQLite nativ / localStorage Browser)
|
||||
sync.svelte.ts Sync-Queue -> Dolibarr (idempotent über clientUuid)
|
||||
scanner.ts Brücke zum nativen Plugin (+ Browser-Mock)
|
||||
|
|
@ -69,6 +70,13 @@ src/routes/
|
|||
Werkzeuge mit Live-Anzeige — eigene Seite statt
|
||||
Dialog, weil sie über Minuten/Stunden laufen
|
||||
android/ natives Android-Projekt inkl. Kotlin-Plugin (einzige Quelle)
|
||||
NetDiagScannerPlugin.kt alle Plugin-Methoden (Scan, Monitor, WLAN, SIP, Update,
|
||||
Biometrie) — @PluginMethod-Namen = JS-Aufrufe
|
||||
Dns.kt / Snmp.kt / Sip.kt eigene Protokoll-Clients (UDP/TCP), je ein
|
||||
Ergebnistyp statt null — ein nicht erreichbarer Dienst
|
||||
darf nie wie ein fehlerfreier aussehen
|
||||
Biometrie.kt BiometricPrompt + Keystore (siehe „Anmeldung")
|
||||
MonitorService.kt Foreground-Service für Dauertest und Geräte-Monitor
|
||||
native-plugin/ nur noch Anleitung/Hinweise zum Plugin
|
||||
tools/ Prüfwerkzeuge ohne Gerät (siehe unten)
|
||||
```
|
||||
|
|
@ -137,12 +145,31 @@ die im Feld nicht gilt (KB #1086).
|
|||
## Anmeldung
|
||||
|
||||
Die App hängt am zentralen Auth-Modul **awlauth** (7. AWL-App). Das Token kommt aus
|
||||
`auth.php` und geht bei jeder Anfrage als `Authorization: Bearer` mit — auch beim
|
||||
`auth.php` (Passwort) oder `sso.php` (AWL-Konto, siehe unten) und geht bei jeder Anfrage als
|
||||
`Authorization: Bearer` mit — auch beim
|
||||
APK-Download und beim PDF-Abruf. **Nie in der Adresse:** ein Token in der URL steht in
|
||||
jedem Zugriffs- und Proxy-Log und gilt sieben Tage für die gesamte Kunden-API.
|
||||
(Einzige, befristete Ausnahme: `update.php?download=1` für App-Fassungen im Feld —
|
||||
siehe ChangeLog des Moduls 1.3.0.)
|
||||
|
||||
### Mit AWL-Konto anmelden (seit Modul 1.4.0 / awlauth 1.6.0)
|
||||
|
||||
Knopf „Mit AWL-Konto anmelden" öffnet die awlauth-Anmeldeseite im **Chrome-Tab**
|
||||
(`@capacitor/browser`). Der Tab teilt sich die Anmeldung mit Chrome: wer im Handy-Browser
|
||||
schon angemeldet ist (PWA, ERP), ist ohne Eingabe zurück in der App. Sonst Passwort oder
|
||||
Fingerabdruck auf der awlauth-Seite, danach „Zurück zu NetDiag".
|
||||
|
||||
- Rücksprung über das eigene Schema `de.data-it-solution.netdiag:/sso?code=…&state=…` —
|
||||
Intent-Filter im `AndroidManifest.xml`, Konstante `SSO_REDIRECT` in `src/lib/sso.ts`.
|
||||
**Beide müssen übereinstimmen**, und awlauth vergleicht die Adresse Zeichen für Zeichen.
|
||||
- PKCE (S256) + state: Geheimnis und state liegen bis zum Rücksprung in den Preferences
|
||||
(Android darf die App beenden, während der Chrome-Tab vorne ist), gelten einmal und
|
||||
höchstens zehn Minuten.
|
||||
- Der Rücksprung wird im Layout verarbeitet (`appUrlOpen` + `getLaunchUrl`), nicht auf der
|
||||
Login-Seite.
|
||||
- Fragt `sso.php` (GET) `available:false` zurück (awlauth zu alt, App dort abgeschaltet),
|
||||
zeigt die App den Grund; das Passwortfeld bleibt immer.
|
||||
|
||||
### Fingerabdruck statt Passwort
|
||||
|
||||
Einschaltbar unter Einstellungen → Konto. Was dabei passiert:
|
||||
|
|
@ -174,7 +201,7 @@ verfügbar (KB #855).
|
|||
|
||||
## Bedienung
|
||||
|
||||
1. **Anmelden** (Dolibarr-Zugang; auf dem Gerät zusätzlich Server-URL).
|
||||
1. **Anmelden** — „Mit AWL-Konto anmelden" oder Dolibarr-Zugang (auf dem Gerät zusätzlich Server-URL).
|
||||
2. **Aufträge** — aktive direkt sichtbar, abgeschlossene per Checkbox, Suche.
|
||||
Alternativ über **Kunden** suchen.
|
||||
3. Auftrag/Kunde antippen → Diagnose-Protokoll öffnet sich.
|
||||
|
|
@ -188,6 +215,26 @@ verfügbar (KB #855).
|
|||
Selten gebrauchte Aktionen (Protokoll löschen) liegen hinter den drei Punkten
|
||||
in der Kopfzeile — bewusst nicht neben dem grünen Abschließen-Knopf.
|
||||
|
||||
## Was noch offen ist
|
||||
|
||||
Die Arbeitsdateien `ROADMAP.md` / `ROADMAP_UMSETZUNG.md` sind abgearbeitet und entfernt
|
||||
(19.08.2026). Was aus ihnen **nicht** erledigt war, steht hier — damit es nicht
|
||||
mit den Dateien verschwindet:
|
||||
|
||||
- **WLAN-Kanalanalyse auf echtem Empfang gegenprüfen.** Der Emulator hat keine
|
||||
WLAN-Hardware und liefert grundsätzlich 0 Netze; alle Diagramme sind bisher nur mit
|
||||
Demodaten geprüft. Auf dem Handy nachfahren: Kanal am Router wechseln, zwei
|
||||
Momentaufnahmen vergleichen, Zeitverlauf ansehen.
|
||||
- **Ortung-aus-Zweig (Android < 13).** Auf einem API-33-AVD nicht erreichbar, deshalb
|
||||
nur per Codereview geprüft — die Meldung „Ortung ausschalten verhindert den WLAN-Scan"
|
||||
samt Sprung in die Systemeinstellung ist am echten Altgerät noch nie gelaufen.
|
||||
- **Vorschlags-Chips im ToolDialog.** Im Kopfbereich erledigt (`inputmode`), die Chips
|
||||
für häufige Werte (Gateway, letzte Ziele) fehlen weiterhin.
|
||||
|
||||
Dazu einer, der beim Modul liegt: die befristete `?jwt=`-Ausnahme in `update.php` fällt,
|
||||
sobald die APK-Fassung ≥ 20260819-2141 überall installiert ist (Hinweis steht dort im
|
||||
Code und im ChangeLog 1.3.0).
|
||||
|
||||
## Ohne Gerät prüfen
|
||||
|
||||
- **Browser** (`npm run dev`): Mock-Scanner liefert Beispieldaten.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,19 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!--
|
||||
Rücksprung von der awlauth-Anmeldeseite (Anmeldung per AWL-Konto):
|
||||
de.data-it-solution.netdiag:/sso?code=…&state=… — muss zu
|
||||
SSO_REDIRECT in src/lib/sso.ts passen. Ein fremdes Schema kann sich
|
||||
jede App registrieren; einen abgefangenen Code macht PKCE wertlos.
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="de.data-it-solution.netdiag" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "^6.0.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/browser": "^6.0.0",
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/network": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -162,6 +162,34 @@ export async function login(loginName: string, password: string): Promise<LoginR
|
|||
return res;
|
||||
}
|
||||
|
||||
/** Antwort von sso.php (GET): ist die Anmeldung per AWL-Konto eingerichtet? */
|
||||
export interface SsoKonfig {
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
clientId: string;
|
||||
redirectUri: string;
|
||||
/** Pfad der awlauth-Anmeldeseite, relativ zur Server-Adresse */
|
||||
authorizePath: string;
|
||||
}
|
||||
|
||||
/** Fragt ab, ob der Server die Anmeldung per AWL-Konto anbietet (siehe sso.ts) */
|
||||
export function ssoKonfig(): Promise<SsoKonfig> {
|
||||
return request<SsoKonfig>('sso.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Einmal-Code der awlauth-Seite einlösen — speichert das Token bei Erfolg.
|
||||
* Antwortform wie bei `login()`.
|
||||
*/
|
||||
export async function ssoEinloesen(code: string, verifier: string): Promise<LoginResult> {
|
||||
const res = await request<LoginResult>('sso.php', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code, code_verifier: verifier }),
|
||||
});
|
||||
await setToken(res.token);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mit einem bereits vorhandenen Token anmelden (Fingerabdruck-Weg).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import {
|
|||
loginMitToken,
|
||||
setAuthFailureHandler,
|
||||
tokenNurImSpeicher,
|
||||
type LoginResult,
|
||||
} from './api';
|
||||
import { ssoAbschliessen } from './sso';
|
||||
import { scanner } from './scanner';
|
||||
import type { NetUser } from './types';
|
||||
|
||||
|
|
@ -112,7 +114,21 @@ class AuthState {
|
|||
|
||||
/** Anmelden */
|
||||
async login(loginName: string, password: string): Promise<void> {
|
||||
const res = await apiLogin(loginName, password);
|
||||
await this.uebernehmen(await apiLogin(loginName, password));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rücksprung von der awlauth-Anmeldeseite verarbeiten (siehe sso.ts).
|
||||
* Liefert false, wenn gar keine Anmeldung offen war — dann ist nichts passiert.
|
||||
*/
|
||||
async loginMitSso(url: string): Promise<boolean> {
|
||||
const res = await ssoAbschliessen(url);
|
||||
if (!res) return false;
|
||||
await this.uebernehmen(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async uebernehmen(res: LoginResult): Promise<void> {
|
||||
this.user = res.user;
|
||||
this.loggedIn = true;
|
||||
await Preferences.set({ key: 'user', value: JSON.stringify(res.user) });
|
||||
|
|
|
|||
128
src/lib/sso.ts
Normal file
128
src/lib/sso.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* Anmeldung über die awlauth-Seite (SSO) statt Passwort in der App.
|
||||
*
|
||||
* Ablauf:
|
||||
* 1. `ssoStarten()` fragt den Server (netdiag/api/sso.php), ob der Weg
|
||||
* eingerichtet ist, erzeugt state + PKCE-Geheimnis und öffnet die
|
||||
* awlauth-Seite im Chrome-Tab (`@capacitor/browser`).
|
||||
* 2. Ist der Benutzer im Handy-Browser schon angemeldet (z. B. aus einer der
|
||||
* PWAs), springt awlauth sofort zurück — sonst nach Passwort oder
|
||||
* Fingerabdruck auf der awlauth-Seite. Rücksprung über das eigene Schema
|
||||
* `de.data-it-solution.netdiag:/sso?code=…&state=…` (Intent-Filter im
|
||||
* AndroidManifest).
|
||||
* 3. `ssoAbschliessen(url)` prüft state, schließt den Tab und löst den Code
|
||||
* mit dem PKCE-Geheimnis beim Server ein.
|
||||
*
|
||||
* Der Chrome-Tab und nicht der WebView: nur der Tab teilt sich den
|
||||
* Cookie-Speicher mit Chrome — der WebView der App hat einen eigenen, dort
|
||||
* käme eine bestehende Anmeldung nie an (KB #1451).
|
||||
*
|
||||
* state + Geheimnis liegen in den Preferences, nicht nur im Speicher: Android
|
||||
* darf die App beenden, während der Benutzer im Chrome-Tab tippt. Der
|
||||
* Rücksprung startet sie dann neu, und ohne die Ablage wäre die Anmeldung
|
||||
* verloren. Der Eintrag gilt genau einmal und höchstens zehn Minuten.
|
||||
*/
|
||||
|
||||
import { Browser } from '@capacitor/browser';
|
||||
import { Preferences } from '@capacitor/preferences';
|
||||
import { ApiError, getServerUrl, ssoEinloesen, ssoKonfig, type LoginResult } from './api';
|
||||
|
||||
/** Rücksprungadresse — muss zum Intent-Filter im AndroidManifest passen */
|
||||
export const SSO_REDIRECT = 'de.data-it-solution.netdiag:/sso';
|
||||
|
||||
const OFFEN_KEY = 'sso_offen';
|
||||
const MAX_ALTER_MS = 10 * 60_000;
|
||||
|
||||
interface OffeneAnmeldung {
|
||||
state: string;
|
||||
verifier: string;
|
||||
seit: number;
|
||||
}
|
||||
|
||||
/** Base64url ohne Auffüllzeichen (RFC 7636 Anhang A) */
|
||||
function b64url(bytes: Uint8Array): string {
|
||||
let s = '';
|
||||
for (const b of bytes) s += String.fromCharCode(b);
|
||||
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
function zufall(laenge: number): string {
|
||||
const a = new Uint8Array(laenge);
|
||||
crypto.getRandomValues(a);
|
||||
return b64url(a);
|
||||
}
|
||||
|
||||
/** PKCE-Prüfwert (S256) zum Geheimnis */
|
||||
export async function pkceChallenge(verifier: string): Promise<string> {
|
||||
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
||||
return b64url(new Uint8Array(hash));
|
||||
}
|
||||
|
||||
/** Ist diese Adresse ein Rücksprung von der awlauth-Seite? */
|
||||
export function istSsoRuecksprung(url: string): boolean {
|
||||
return url.startsWith(`${SSO_REDIRECT}?`);
|
||||
}
|
||||
|
||||
/**
|
||||
* awlauth-Seite im Chrome-Tab öffnen. Wirft mit Klartextgrund, wenn der
|
||||
* Server den Weg nicht anbietet (awlauth zu alt, App dort abgeschaltet …).
|
||||
*/
|
||||
export async function ssoStarten(): Promise<void> {
|
||||
const k = await ssoKonfig();
|
||||
if (!k.available) {
|
||||
throw new ApiError(503, k.reason || 'Anmeldung per AWL-Konto ist auf dem Server nicht eingerichtet');
|
||||
}
|
||||
if (k.redirectUri !== SSO_REDIRECT) {
|
||||
throw new ApiError(0, 'Server und App passen nicht zusammen (Rücksprungadresse) — App aktualisieren');
|
||||
}
|
||||
|
||||
const verifier = zufall(32); // 43 Zeichen
|
||||
const state = zufall(16);
|
||||
const challenge = await pkceChallenge(verifier);
|
||||
const offen: OffeneAnmeldung = { state, verifier, seit: Date.now() };
|
||||
await Preferences.set({ key: OFFEN_KEY, value: JSON.stringify(offen) });
|
||||
|
||||
const q = new URLSearchParams({
|
||||
client_id: k.clientId,
|
||||
redirect_uri: SSO_REDIRECT,
|
||||
state,
|
||||
code_challenge: challenge,
|
||||
code_challenge_method: 'S256',
|
||||
});
|
||||
await Browser.open({ url: `${getServerUrl()}${k.authorizePath}?${q.toString()}` });
|
||||
}
|
||||
|
||||
/**
|
||||
* Rücksprung verarbeiten.
|
||||
*
|
||||
* Liefert `null`, wenn gar keine Anmeldung offen ist — etwa weil derselbe
|
||||
* Rücksprung beim Kaltstart einmal über `getLaunchUrl()` und einmal über das
|
||||
* Ereignis ankommt. Das ist kein Fehler und wird still übergangen.
|
||||
*/
|
||||
export async function ssoAbschliessen(url: string): Promise<LoginResult | null> {
|
||||
const roh = (await Preferences.get({ key: OFFEN_KEY })).value;
|
||||
if (!roh) return null;
|
||||
// Einmalig: sofort entfernen, auch wenn das Einlösen gleich scheitert
|
||||
await Preferences.remove({ key: OFFEN_KEY });
|
||||
|
||||
// Tab schließen, sobald die App wieder vorne ist — er hat seine Arbeit getan
|
||||
Browser.close().catch(() => {});
|
||||
|
||||
let offen: OffeneAnmeldung;
|
||||
try {
|
||||
offen = JSON.parse(roh) as OffeneAnmeldung;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!offen.verifier || Date.now() - offen.seit > MAX_ALTER_MS) {
|
||||
throw new ApiError(0, 'Die Anmeldung hat zu lange gedauert — bitte erneut versuchen');
|
||||
}
|
||||
|
||||
const params = new URL(url).searchParams;
|
||||
const code = params.get('code') ?? '';
|
||||
// state schützt davor, eine fremde Anmeldung untergeschoben zu bekommen
|
||||
if (!code || params.get('state') !== offen.state) {
|
||||
throw new ApiError(0, 'Ungültige Rückmeldung der Anmeldeseite — bitte erneut versuchen');
|
||||
}
|
||||
return ssoEinloesen(code, offen.verifier);
|
||||
}
|
||||
|
|
@ -12,6 +12,9 @@
|
|||
import { closeTopOverlay } from '$lib/overlay.svelte';
|
||||
import { checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater';
|
||||
import { initDebugLog } from '$lib/debuglog.svelte';
|
||||
import { istSsoRuecksprung } from '$lib/sso';
|
||||
import { App } from '@capacitor/app';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import Toast from '$lib/components/Toast.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
|
@ -36,6 +39,24 @@
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Rücksprung von der awlauth-Anmeldeseite (Anmeldung per AWL-Konto).
|
||||
* Kommt je nach Lage als Ereignis (App lief im Hintergrund) oder als
|
||||
* Start-Adresse (Android hatte die App beendet, während der Chrome-Tab vorne
|
||||
* war). Ist keine Anmeldung offen, passiert nichts.
|
||||
*/
|
||||
async function ssoRuecksprung(url: string) {
|
||||
if (!istSsoRuecksprung(url)) return;
|
||||
try {
|
||||
if (!(await auth.loginMitSso(url))) return;
|
||||
await sync.start();
|
||||
toast.show('Angemeldet', 'success');
|
||||
goto(HOME);
|
||||
} catch (e) {
|
||||
toast.show(e instanceof Error ? e.message : 'Anmeldung fehlgeschlagen', 'error', 8000);
|
||||
}
|
||||
}
|
||||
|
||||
// Update herunterladen und Installer öffnen — Fortschritt im Banner
|
||||
async function runUpdate() {
|
||||
if (!updateInfo || updateBusy) return;
|
||||
|
|
@ -56,6 +77,14 @@
|
|||
await initDb();
|
||||
if (auth.loggedIn) await sync.start();
|
||||
|
||||
// Anmeldung per AWL-Konto: Rücksprung aus dem Chrome-Tab (erst nach
|
||||
// auth.init(), sonst fehlt die Server-Adresse fürs Einlösen)
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
void App.addListener('appUrlOpen', ({ url }) => void ssoRuecksprung(url));
|
||||
const start = await App.getLaunchUrl();
|
||||
if (start?.url) void ssoRuecksprung(start.url);
|
||||
}
|
||||
|
||||
// Hardware-Backbutton (Modul-Scope, Single-Instance — KB #480/#549)
|
||||
registerBackListener({
|
||||
handleOverlay: () => closeTopOverlay(),
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@
|
|||
import { getServerUrl, setServerUrl, ApiError } from '$lib/api';
|
||||
import { toast } from '$lib/toast.svelte';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { Eye, EyeOff, Fingerprint } from 'lucide-svelte';
|
||||
import { Eye, EyeOff, Fingerprint, KeyRound } from 'lucide-svelte';
|
||||
import { ssoStarten } from '$lib/sso';
|
||||
|
||||
let server = $state(getServerUrl());
|
||||
let loginName = $state('');
|
||||
let password = $state('');
|
||||
let busy = $state(false);
|
||||
let fingerBusy = $state(false);
|
||||
let ssoBusy = $state(false);
|
||||
|
||||
/**
|
||||
* Fingerabdruck-Anmeldung. Erscheint nur, wenn auf diesem Gerät auch
|
||||
|
|
@ -54,15 +56,41 @@
|
|||
if (pwEl) pwEl.type = pwVisible ? 'text' : 'password';
|
||||
}
|
||||
|
||||
/** Server-Adresse übernehmen — gilt für Passwort und AWL-Konto gleichermaßen */
|
||||
async function serverUebernehmen() {
|
||||
if (!needsServer) return;
|
||||
if (!server.trim()) throw new ApiError(0, 'Server-Adresse fehlt');
|
||||
await setServerUrl(server.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Anmeldung per AWL-Konto: öffnet die awlauth-Seite im Chrome-Tab. Das
|
||||
* Ergebnis kommt als Rücksprung im Layout an (+layout.svelte, ssoRuecksprung),
|
||||
* nicht hier — die Seite kann bis dahin längst neu aufgebaut worden sein.
|
||||
*/
|
||||
async function mitAwlKonto() {
|
||||
if (ssoBusy) return;
|
||||
ssoBusy = true;
|
||||
try {
|
||||
await serverUebernehmen();
|
||||
await ssoStarten();
|
||||
} catch (err) {
|
||||
toast.show(
|
||||
err instanceof ApiError ? err.message : 'Anmeldeseite konnte nicht geöffnet werden',
|
||||
'error',
|
||||
8000,
|
||||
);
|
||||
} finally {
|
||||
ssoBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(e: Event) {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
if (needsServer) {
|
||||
if (!server.trim()) throw new ApiError(0, 'Server-Adresse fehlt');
|
||||
await setServerUrl(server.trim());
|
||||
}
|
||||
await serverUebernehmen();
|
||||
await auth.login(loginName.trim(), password);
|
||||
await sync.start();
|
||||
toast.show('Angemeldet', 'success');
|
||||
|
|
@ -101,6 +129,25 @@
|
|||
/>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{#if needsServer}
|
||||
<!-- Anmeldung über die awlauth-Seite: ohne Eingabe, wenn im Handy-Browser schon angemeldet -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center gap-2 rounded-lg bg-sky-600 py-2.5 font-semibold text-white active:bg-sky-700 disabled:opacity-50"
|
||||
onclick={mitAwlKonto}
|
||||
disabled={ssoBusy}
|
||||
>
|
||||
<KeyRound size={20} />
|
||||
{ssoBusy ? 'Anmeldeseite wird geöffnet …' : 'Mit AWL-Konto anmelden'}
|
||||
</button>
|
||||
<div class="flex items-center gap-3 text-xs text-zinc-500">
|
||||
<span class="h-px flex-1 bg-zinc-700"></span>
|
||||
oder mit Passwort
|
||||
<span class="h-px flex-1 bg-zinc-700"></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="text-zinc-400">Benutzer</span>
|
||||
<input
|
||||
|
|
@ -139,7 +186,9 @@
|
|||
</div>
|
||||
</label>
|
||||
<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 py-2.5 font-semibold disabled:opacity-50 {needsServer
|
||||
? 'border border-zinc-700 text-zinc-200 active:bg-zinc-800'
|
||||
: 'bg-sky-600 text-white active:bg-sky-700'}"
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
>
|
||||
|
|
|
|||
Loading…
Reference in a new issue