feat(myTischtennis): implement session restoration for browser automation login

- Enhanced the loginWithBrowserAutomation method to accept an options parameter for restoring a saved Playwright session, allowing users to bypass CAPTCHA if the session is still valid.
- Added a new playwrightStorageState field in the MyTischtennis model to store the encrypted browser storage state, facilitating session persistence.
- Updated myTischtennisService to utilize the saved storage state during login attempts, improving user experience by reducing unnecessary CAPTCHA challenges.
This commit is contained in:
Torsten Schulz (local)
2026-03-04 14:26:27 +01:00
parent 637bacf70f
commit 27665a45df
3 changed files with 103 additions and 5 deletions

View File

@@ -352,11 +352,62 @@ class MyTischtennisClient {
* Browser-based fallback login for CAPTCHA flows.
* @param {string} email
* @param {string} password
* @returns {Promise<Object>} Login response with token and session data
* @param {Object} [options]
* @param {Object} [options.savedStorageState] - Playwright storage state from a previous session.
* If provided and the stored auth cookie is still valid, returns immediately without a new login.
* @returns {Promise<Object>} Login response with token, session data, and `storageState` for persistence.
*/
async loginWithBrowserAutomation(email, password) {
async loginWithBrowserAutomation(email, password, options = {}) {
const { savedStorageState } = options;
let browser = null;
let context = null;
// --- Fast path: restore a saved Playwright session ---
if (savedStorageState) {
try {
browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] });
context = await browser.newContext({ storageState: savedStorageState });
const cookies = await context.cookies('https://www.mytischtennis.de');
const authCookie = cookies.find((c) => c.name === 'sb-10-auth-token' || /^sb-\d+-auth-token$/.test(c.name));
if (authCookie?.value) {
const tokenMatch = String(authCookie.value).match(/^base64-(.+)$/);
if (tokenMatch) {
const tokenData = JSON.parse(Buffer.from(tokenMatch[1], 'base64').toString('utf-8'));
const nowSec = Math.floor(Date.now() / 1000);
// Accept if not expired (with 5-minute safety buffer)
if (tokenData.expires_at && tokenData.expires_at > nowSec + 300) {
console.log('[myTischtennisClient.playwright] Restored session from saved state (no CAPTCHA needed)');
const storageState = await context.storageState();
await context.close();
await browser.close();
browser = null; context = null;
return {
success: true,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresAt: tokenData.expires_at,
expiresIn: tokenData.expires_in,
user: tokenData.user,
cookie: `sb-10-auth-token=${authCookie.value}`,
storageState,
restoredFromCache: true
};
}
}
}
// Cookie absent or expired → close and fall through to full login
console.log('[myTischtennisClient.playwright] Saved session expired or invalid, starting full login');
await context.close();
await browser.close();
browser = null; context = null;
} catch (restoreErr) {
console.warn('[myTischtennisClient.playwright] Session restore failed, starting full login:', restoreErr.message);
try { if (context) await context.close(); } catch (_e) { /* ignore */ }
try { if (browser) await browser.close(); } catch (_e) { /* ignore */ }
browser = null; context = null;
}
}
try {
console.log('[myTischtennisClient.playwright] Start browser login flow');
browser = await chromium.launch({
@@ -686,6 +737,11 @@ class MyTischtennisClient {
}
const cookie = `sb-10-auth-token=${authCookieObj.value}`;
// Persist the full browser storage state so future calls can skip the CAPTCHA flow.
let storageState = null;
try { storageState = await context.storageState(); } catch (_e) { /* ignore */ }
console.log('[myTischtennisClient.playwright] Browser login successful');
return {
success: true,
@@ -694,7 +750,8 @@ class MyTischtennisClient {
expiresAt: tokenData.expires_at,
expiresIn: tokenData.expires_in,
user: tokenData.user,
cookie
cookie,
storageState
};
} catch (error) {
const rawMessage = String(error?.message || error || 'Playwright-Login fehlgeschlagen');