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:
@@ -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');
|
||||
|
||||
@@ -199,6 +199,30 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'last_update_ratings'
|
||||
},
|
||||
playwrightStorageState: {
|
||||
// Encrypted JSON blob: full Playwright browser storage state (cookies + localStorage).
|
||||
// Allows restoring a previous login session without a new CAPTCHA challenge.
|
||||
type: DataTypes.TEXT('long'),
|
||||
allowNull: true,
|
||||
field: 'playwright_storage_state',
|
||||
set(value) {
|
||||
if (value === null || value === undefined) {
|
||||
this.setDataValue('playwrightStorageState', null);
|
||||
} else {
|
||||
const jsonString = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
this.setDataValue('playwrightStorageState', encryptData(jsonString));
|
||||
}
|
||||
},
|
||||
get() {
|
||||
const encrypted = this.getDataValue('playwrightStorageState');
|
||||
if (!encrypted) return null;
|
||||
try {
|
||||
return JSON.parse(decryptData(encrypted));
|
||||
} catch (_e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
|
||||
@@ -60,7 +60,12 @@ class MyTischtennisService {
|
||||
loginResult = await myTischtennisClient.login(email, password);
|
||||
if (!loginResult.success && loginResult.requiresCaptcha) {
|
||||
console.log('[myTischtennisService.upsertAccount] CAPTCHA-Fehler, versuche Playwright-Fallback...');
|
||||
const playwrightResult = await myTischtennisClient.loginWithBrowserAutomation(email, password);
|
||||
// Load saved browser state so we can skip the CAPTCHA if the previous session is still valid.
|
||||
const existingAccount = await MyTischtennis.findOne({ where: { userId } });
|
||||
const savedStorageState = existingAccount?.playwrightStorageState ?? null;
|
||||
const playwrightResult = await myTischtennisClient.loginWithBrowserAutomation(
|
||||
email, password, { savedStorageState }
|
||||
);
|
||||
if (playwrightResult.success) {
|
||||
loginResult = playwrightResult;
|
||||
} else {
|
||||
@@ -106,6 +111,9 @@ class MyTischtennisService {
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
account.userData = loginResult.user;
|
||||
if (loginResult.storageState) {
|
||||
account.playwrightStorageState = loginResult.storageState;
|
||||
}
|
||||
|
||||
// Hole Club-ID und Federation
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
@@ -139,6 +147,9 @@ class MyTischtennisService {
|
||||
accountData.expiresAt = loginResult.expiresAt;
|
||||
accountData.cookie = loginResult.cookie;
|
||||
accountData.userData = loginResult.user;
|
||||
if (loginResult.storageState) {
|
||||
accountData.playwrightStorageState = loginResult.storageState;
|
||||
}
|
||||
|
||||
// Hole Club-ID
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
@@ -242,7 +253,10 @@ class MyTischtennisService {
|
||||
if (!effectiveLoginResult.success && effectiveLoginResult.requiresCaptcha) {
|
||||
console.log('[myTischtennisService.verifyLogin] CAPTCHA-Fehler, versuche Playwright-Fallback...');
|
||||
try {
|
||||
const playwrightResult = await myTischtennisClient.loginWithBrowserAutomation(account.email, password);
|
||||
const savedStorageState = account.playwrightStorageState ?? null;
|
||||
const playwrightResult = await myTischtennisClient.loginWithBrowserAutomation(
|
||||
account.email, password, { savedStorageState }
|
||||
);
|
||||
if (playwrightResult.success) {
|
||||
effectiveLoginResult = playwrightResult;
|
||||
} else {
|
||||
@@ -282,6 +296,9 @@ class MyTischtennisService {
|
||||
account.expiresAt = effectiveLoginResult.expiresAt;
|
||||
account.cookie = effectiveLoginResult.cookie;
|
||||
account.userData = effectiveLoginResult.user;
|
||||
if (effectiveLoginResult.storageState) {
|
||||
account.playwrightStorageState = effectiveLoginResult.storageState;
|
||||
}
|
||||
|
||||
// Hole Club-ID und Federation
|
||||
const profileResult = await myTischtennisClient.getUserProfile(effectiveLoginResult.cookie);
|
||||
|
||||
Reference in New Issue
Block a user