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/
|
src/lib/
|
||||||
api.ts JSON-API-Client (JWT, 401-Refresh, Timeout)
|
api.ts JSON-API-Client (JWT, 401-Refresh, Timeout)
|
||||||
auth.svelte.ts Anmelde-Status
|
auth.svelte.ts Anmelde-Status
|
||||||
|
sso.ts Anmeldung per AWL-Konto (Chrome-Tab, PKCE, Rücksprung)
|
||||||
db.ts Offline-Speicher (SQLite nativ / localStorage Browser)
|
db.ts Offline-Speicher (SQLite nativ / localStorage Browser)
|
||||||
sync.svelte.ts Sync-Queue -> Dolibarr (idempotent über clientUuid)
|
sync.svelte.ts Sync-Queue -> Dolibarr (idempotent über clientUuid)
|
||||||
scanner.ts Brücke zum nativen Plugin (+ Browser-Mock)
|
scanner.ts Brücke zum nativen Plugin (+ Browser-Mock)
|
||||||
|
|
@ -69,6 +70,13 @@ src/routes/
|
||||||
Werkzeuge mit Live-Anzeige — eigene Seite statt
|
Werkzeuge mit Live-Anzeige — eigene Seite statt
|
||||||
Dialog, weil sie über Minuten/Stunden laufen
|
Dialog, weil sie über Minuten/Stunden laufen
|
||||||
android/ natives Android-Projekt inkl. Kotlin-Plugin (einzige Quelle)
|
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
|
native-plugin/ nur noch Anleitung/Hinweise zum Plugin
|
||||||
tools/ Prüfwerkzeuge ohne Gerät (siehe unten)
|
tools/ Prüfwerkzeuge ohne Gerät (siehe unten)
|
||||||
```
|
```
|
||||||
|
|
@ -137,12 +145,31 @@ die im Feld nicht gilt (KB #1086).
|
||||||
## Anmeldung
|
## Anmeldung
|
||||||
|
|
||||||
Die App hängt am zentralen Auth-Modul **awlauth** (7. AWL-App). Das Token kommt aus
|
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
|
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.
|
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 —
|
(Einzige, befristete Ausnahme: `update.php?download=1` für App-Fassungen im Feld —
|
||||||
siehe ChangeLog des Moduls 1.3.0.)
|
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
|
### Fingerabdruck statt Passwort
|
||||||
|
|
||||||
Einschaltbar unter Einstellungen → Konto. Was dabei passiert:
|
Einschaltbar unter Einstellungen → Konto. Was dabei passiert:
|
||||||
|
|
@ -174,7 +201,7 @@ verfügbar (KB #855).
|
||||||
|
|
||||||
## Bedienung
|
## 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.
|
2. **Aufträge** — aktive direkt sichtbar, abgeschlossene per Checkbox, Suche.
|
||||||
Alternativ über **Kunden** suchen.
|
Alternativ über **Kunden** suchen.
|
||||||
3. Auftrag/Kunde antippen → Diagnose-Protokoll öffnet sich.
|
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
|
Selten gebrauchte Aktionen (Protokoll löschen) liegen hinter den drei Punkten
|
||||||
in der Kopfzeile — bewusst nicht neben dem grünen Abschließen-Knopf.
|
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
|
## Ohne Gerät prüfen
|
||||||
|
|
||||||
- **Browser** (`npm run dev`): Mock-Scanner liefert Beispieldaten.
|
- **Browser** (`npm run dev`): Mock-Scanner liefert Beispieldaten.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,19 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/sqlite": "^6.0.0",
|
"@capacitor-community/sqlite": "^6.0.0",
|
||||||
"@capacitor/app": "^6.0.0",
|
"@capacitor/app": "^6.0.0",
|
||||||
|
"@capacitor/browser": "^6.0.0",
|
||||||
"@capacitor/core": "^6.0.0",
|
"@capacitor/core": "^6.0.0",
|
||||||
"@capacitor/filesystem": "^6.0.0",
|
"@capacitor/filesystem": "^6.0.0",
|
||||||
"@capacitor/network": "^6.0.0",
|
"@capacitor/network": "^6.0.0",
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,34 @@ export async function login(loginName: string, password: string): Promise<LoginR
|
||||||
return res;
|
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).
|
* Mit einem bereits vorhandenen Token anmelden (Fingerabdruck-Weg).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ import {
|
||||||
loginMitToken,
|
loginMitToken,
|
||||||
setAuthFailureHandler,
|
setAuthFailureHandler,
|
||||||
tokenNurImSpeicher,
|
tokenNurImSpeicher,
|
||||||
|
type LoginResult,
|
||||||
} from './api';
|
} from './api';
|
||||||
|
import { ssoAbschliessen } from './sso';
|
||||||
import { scanner } from './scanner';
|
import { scanner } from './scanner';
|
||||||
import type { NetUser } from './types';
|
import type { NetUser } from './types';
|
||||||
|
|
||||||
|
|
@ -112,7 +114,21 @@ class AuthState {
|
||||||
|
|
||||||
/** Anmelden */
|
/** Anmelden */
|
||||||
async login(loginName: string, password: string): Promise<void> {
|
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.user = res.user;
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
await Preferences.set({ key: 'user', value: JSON.stringify(res.user) });
|
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 { closeTopOverlay } from '$lib/overlay.svelte';
|
||||||
import { checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater';
|
import { checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater';
|
||||||
import { initDebugLog } from '$lib/debuglog.svelte';
|
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';
|
import Toast from '$lib/components/Toast.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
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
|
// Update herunterladen und Installer öffnen — Fortschritt im Banner
|
||||||
async function runUpdate() {
|
async function runUpdate() {
|
||||||
if (!updateInfo || updateBusy) return;
|
if (!updateInfo || updateBusy) return;
|
||||||
|
|
@ -56,6 +77,14 @@
|
||||||
await initDb();
|
await initDb();
|
||||||
if (auth.loggedIn) await sync.start();
|
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)
|
// Hardware-Backbutton (Modul-Scope, Single-Instance — KB #480/#549)
|
||||||
registerBackListener({
|
registerBackListener({
|
||||||
handleOverlay: () => closeTopOverlay(),
|
handleOverlay: () => closeTopOverlay(),
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@
|
||||||
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, Fingerprint } from 'lucide-svelte';
|
import { Eye, EyeOff, Fingerprint, KeyRound } from 'lucide-svelte';
|
||||||
|
import { ssoStarten } from '$lib/sso';
|
||||||
|
|
||||||
let server = $state(getServerUrl());
|
let server = $state(getServerUrl());
|
||||||
let loginName = $state('');
|
let loginName = $state('');
|
||||||
let password = $state('');
|
let password = $state('');
|
||||||
let busy = $state(false);
|
let busy = $state(false);
|
||||||
let fingerBusy = $state(false);
|
let fingerBusy = $state(false);
|
||||||
|
let ssoBusy = $state(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fingerabdruck-Anmeldung. Erscheint nur, wenn auf diesem Gerät auch
|
* Fingerabdruck-Anmeldung. Erscheint nur, wenn auf diesem Gerät auch
|
||||||
|
|
@ -54,15 +56,41 @@
|
||||||
if (pwEl) pwEl.type = pwVisible ? 'text' : 'password';
|
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) {
|
async function submit(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
if (needsServer) {
|
await serverUebernehmen();
|
||||||
if (!server.trim()) throw new ApiError(0, 'Server-Adresse fehlt');
|
|
||||||
await setServerUrl(server.trim());
|
|
||||||
}
|
|
||||||
await auth.login(loginName.trim(), password);
|
await auth.login(loginName.trim(), password);
|
||||||
await sync.start();
|
await sync.start();
|
||||||
toast.show('Angemeldet', 'success');
|
toast.show('Angemeldet', 'success');
|
||||||
|
|
@ -101,6 +129,25 @@
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/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">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-zinc-400">Benutzer</span>
|
<span class="text-zinc-400">Benutzer</span>
|
||||||
<input
|
<input
|
||||||
|
|
@ -139,7 +186,9 @@
|
||||||
</div>
|
</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 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"
|
type="submit"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue