✨ feat(doodstream): add OTP input support for web login
When Doodstream requires 2FA, the account modal now dynamically shows an OTP input field so the user can enter the code from their email and complete the login without restarting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f077868cc
commit
beba96c21b
@ -78,7 +78,7 @@ class DoodstreamUploader {
|
|||||||
/**
|
/**
|
||||||
* Login to DoodStream via web form
|
* Login to DoodStream via web form
|
||||||
*/
|
*/
|
||||||
async login(username, password) {
|
async login(username, password, otp) {
|
||||||
// GET homepage first to collect cookies
|
// GET homepage first to collect cookies
|
||||||
const homeRes = await this._fetch(BASE_URL);
|
const homeRes = await this._fetch(BASE_URL);
|
||||||
await homeRes.text();
|
await homeRes.text();
|
||||||
@ -88,7 +88,7 @@ class DoodstreamUploader {
|
|||||||
op: 'login_ajax',
|
op: 'login_ajax',
|
||||||
login: username,
|
login: username,
|
||||||
password: password,
|
password: password,
|
||||||
loginotp: ''
|
loginotp: otp || ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use raw fetch with redirect: 'manual' to detect success redirects
|
// Use raw fetch with redirect: 'manual' to detect success redirects
|
||||||
@ -122,6 +122,11 @@ class DoodstreamUploader {
|
|||||||
|
|
||||||
if (json && json.status === 'success') {
|
if (json && json.status === 'success') {
|
||||||
// Explicit success response
|
// Explicit success response
|
||||||
|
} else if (json && json.message && /otp/i.test(json.message)) {
|
||||||
|
// OTP required — signal caller to collect OTP from user
|
||||||
|
const err = new Error(`Doodstream Login: ${json.message}`);
|
||||||
|
err.otpRequired = true;
|
||||||
|
throw err;
|
||||||
} else if (json && json.status === 'fail') {
|
} else if (json && json.status === 'fail') {
|
||||||
throw new Error(`Doodstream Login: ${json.message || 'Login fehlgeschlagen'}`);
|
throw new Error(`Doodstream Login: ${json.message || 'Login fehlgeschlagen'}`);
|
||||||
} else if (body.includes('Dashboard')) {
|
} else if (body.includes('Dashboard')) {
|
||||||
|
|||||||
15
main.js
15
main.js
@ -188,7 +188,7 @@ function buildUploadTasksFromJobs(config, jobs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkDoodstreamHealth(hosterConfig) {
|
async function checkDoodstreamHealth(hosterConfig, otp) {
|
||||||
const username = hosterConfig && hosterConfig.username
|
const username = hosterConfig && hosterConfig.username
|
||||||
? String(hosterConfig.username).trim()
|
? String(hosterConfig.username).trim()
|
||||||
: '';
|
: '';
|
||||||
@ -199,7 +199,14 @@ async function checkDoodstreamHealth(hosterConfig) {
|
|||||||
// Login-based check (preferred)
|
// Login-based check (preferred)
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
const uploader = new DoodstreamUploader();
|
const uploader = new DoodstreamUploader();
|
||||||
await uploader.login(username, password);
|
try {
|
||||||
|
await uploader.login(username, password, otp || undefined);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.otpRequired) {
|
||||||
|
return { status: 'otp_required', message: err.message || 'OTP erforderlich' };
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
return { status: 'ok', message: 'Login ok, Upload-Seite bereit' };
|
return { status: 'ok', message: 'Login ok, Upload-Seite bereit' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +409,7 @@ async function runHosterHealthCheck(config, requestedChecks) {
|
|||||||
checks = requestedChecks;
|
checks = requestedChecks;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(checks.map(async ({ hoster, accountId }) => {
|
const results = await Promise.all(checks.map(async ({ hoster, accountId, otp }) => {
|
||||||
if (!allowed.includes(hoster)) {
|
if (!allowed.includes(hoster)) {
|
||||||
return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
||||||
}
|
}
|
||||||
@ -414,7 +421,7 @@ async function runHosterHealthCheck(config, requestedChecks) {
|
|||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (hoster === 'doodstream.com') {
|
if (hoster === 'doodstream.com') {
|
||||||
result = await withTimeout(checkDoodstreamHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Doodstream-Check');
|
result = await withTimeout(checkDoodstreamHealth(hosterConfig, otp), HEALTH_CHECK_TIMEOUT, 'Doodstream-Check');
|
||||||
} else if (hoster === 'vidmoly.me') {
|
} else if (hoster === 'vidmoly.me') {
|
||||||
result = await withTimeout(checkVidmolyHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Vidmoly-Check');
|
result = await withTimeout(checkVidmolyHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Vidmoly-Check');
|
||||||
} else if (hoster === 'voe.sx') {
|
} else if (hoster === 'voe.sx') {
|
||||||
|
|||||||
@ -2627,6 +2627,7 @@ function openAccountModal(editAccountId) {
|
|||||||
|
|
||||||
function closeAccountModal() {
|
function closeAccountModal() {
|
||||||
document.getElementById('accountModal').style.display = 'none';
|
document.getElementById('accountModal').style.display = 'none';
|
||||||
|
_hideOtpField();
|
||||||
editingAccountId = null;
|
editingAccountId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2734,15 +2735,30 @@ async function saveAccount() {
|
|||||||
renderHosterModal();
|
renderHosterModal();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
|
|
||||||
// Run health check for this specific account
|
// Check if OTP was entered (for retry after OTP prompt)
|
||||||
|
const otpInput = document.getElementById('accField_otp');
|
||||||
|
const otp = otpInput ? otpInput.value.trim() : '';
|
||||||
|
|
||||||
|
// Run health check for this specific account (include OTP if provided)
|
||||||
|
const checkPayload = { hoster: hosterName, accountId };
|
||||||
|
if (otp) checkPayload.otp = otp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.api.runHealthCheck({ hosters: [{ hoster: hosterName, accountId }] });
|
const result = await window.api.runHealthCheck({ hosters: [checkPayload] });
|
||||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||||
const row = rows.find(r => r.accountId === accountId);
|
const row = rows.find(r => r.accountId === accountId);
|
||||||
if (row && (row.status === 'ok' || row.status === 'warn')) {
|
if (row && row.status === 'otp_required') {
|
||||||
|
// Show OTP input field if not already visible
|
||||||
|
accountStatuses[accountId] = { status: 'error', message: row.message || 'OTP erforderlich' };
|
||||||
|
statusEl.textContent = row.message || 'OTP wurde an deine E-Mail gesendet.';
|
||||||
|
statusEl.className = 'account-modal-status error';
|
||||||
|
_showOtpField();
|
||||||
|
saveBtn.textContent = 'OTP bestätigen';
|
||||||
|
} else if (row && (row.status === 'ok' || row.status === 'warn')) {
|
||||||
accountStatuses[accountId] = { status: row.status || 'ok', message: row.message || '' };
|
accountStatuses[accountId] = { status: row.status || 'ok', message: row.message || '' };
|
||||||
statusEl.textContent = row.status === 'warn' ? row.message || 'Prüfung mit Warnung abgeschlossen.' : 'Login erfolgreich!';
|
statusEl.textContent = row.status === 'warn' ? row.message || 'Prüfung mit Warnung abgeschlossen.' : 'Login erfolgreich!';
|
||||||
statusEl.className = 'account-modal-status ok';
|
statusEl.className = 'account-modal-status ok';
|
||||||
|
_hideOtpField();
|
||||||
setTimeout(() => closeAccountModal(), 1200);
|
setTimeout(() => closeAccountModal(), 1200);
|
||||||
} else {
|
} else {
|
||||||
const msg = (row && row.message) || 'Login fehlgeschlagen';
|
const msg = (row && row.message) || 'Login fehlgeschlagen';
|
||||||
@ -2764,6 +2780,24 @@ async function saveAccount() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _showOtpField() {
|
||||||
|
if (document.getElementById('accField_otp')) return; // already visible
|
||||||
|
const container = document.getElementById('accountCredsFields');
|
||||||
|
const otpHtml = `
|
||||||
|
<div class="settings-row" id="otpFieldRow">
|
||||||
|
<label>OTP Code</label>
|
||||||
|
<input type="text" class="key-input" id="accField_otp" placeholder="6-stelliger Code aus E-Mail" autocomplete="one-time-code" inputmode="numeric" maxlength="10">
|
||||||
|
</div>`;
|
||||||
|
container.insertAdjacentHTML('beforeend', otpHtml);
|
||||||
|
// Auto-focus the OTP field
|
||||||
|
setTimeout(() => document.getElementById('accField_otp')?.focus(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hideOtpField() {
|
||||||
|
const row = document.getElementById('otpFieldRow');
|
||||||
|
if (row) row.remove();
|
||||||
|
}
|
||||||
|
|
||||||
// --- History ---
|
// --- History ---
|
||||||
async function loadHistory() {
|
async function loadHistory() {
|
||||||
const history = await window.api.getHistory();
|
const history = await window.api.getHistory();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user