const express = require('express'); const { execFile } = require('child_process'); const { fetchAlexaCookieAndSave } = require('./auth4-core'); const app = express(); const PORT = process.env.AUTH4_WEB_PORT || 3678; const LOCAL_ENV_PATH = __dirname + '/.env'; const DEFAULT_SSH_TARGET = process.env.ALEXA_DEPLOY_SSH_TARGET || 'keinafarm'; const DEFAULT_REMOTE_UPLOAD_PATH = process.env.ALEXA_DEPLOY_REMOTE_UPLOAD_PATH || '/tmp/alexa-api.env'; const DEFAULT_DEPLOY_COMMAND = process.env.ALEXA_DEPLOY_COMMAND || 'sudo /usr/local/bin/alexa-cookie-deploy.sh /tmp/alexa-api.env'; app.use(express.urlencoded({ extended: false })); function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderPage(options) { const opts = options || {}; const email = escapeHtml(opts.email || ''); const logs = Array.isArray(opts.logs) ? opts.logs : []; const result = opts.result || null; const error = opts.error || ''; const statusClass = result ? 'ok' : (error ? 'error' : ''); const deploy = opts.deploy || {}; const deployTarget = escapeHtml(deploy.target || DEFAULT_SSH_TARGET); const deployUploadPath = escapeHtml(deploy.remoteUploadPath || DEFAULT_REMOTE_UPLOAD_PATH); const deployCommand = escapeHtml(deploy.deployCommand || DEFAULT_DEPLOY_COMMAND); const deployLogs = Array.isArray(deploy.logs) ? deploy.logs : []; const deployError = deploy.error || ''; const deployResult = deploy.result || null; const messageHtml = result ? `
Cookie を更新しました。
保存先: ${escapeHtml(result.envPath)}
Cookie 長さ: ${escapeHtml(result.cookieLength)}
` : (error ? `
${escapeHtml(error).replace(/\n/g, '
')}
` : ''); const logHtml = logs.length ? `
${escapeHtml(logs.join('\n'))}
` : '
ここにログが表示されます。
'; const deployMessageHtml = deployResult ? `
サーバー反映が完了しました。
転送先: ${deployTarget}:${deployUploadPath}
` : (deployError ? `
${escapeHtml(deployError).replace(/\n/g, '
')}
` : ''); const deployLogHtml = deployLogs.length ? `
${escapeHtml(deployLogs.join('\n'))}
` : '
ここに転送と再起動のログが表示されます。
'; return ` Alexa Cookie 更新

Alexa Cookie 更新

メールアドレスとパスワードをその場で入力して、auth4.js と同じ認証フローで .env を更新します。入力値は保存しません。

${messageHtml}
ローカル専用の簡易 GUI です。CAPTCHA や MFA が出た場合は失敗ログを表示します。
${logHtml}

サーバー反映

更新済みの .env を一時パスへ転送し、sudoers で許可した専用スクリプトだけを実行します。SSH の接続先は必要に応じて変えてください。

${deployMessageHtml}
このPCの SSH 設定では keinafarm が使えます。初回はサーバー側に /usr/local/bin/alexa-cookie-deploy.shsudoers 設定が必要です。
${deployLogHtml}
`; } function runCommand(command, args) { return new Promise((resolve, reject) => { execFile(command, args, { maxBuffer: 1024 * 1024 }, function(error, stdout, stderr) { if (error) { error.stdout = stdout || ''; error.stderr = stderr || ''; reject(error); return; } resolve({ stdout: stdout || '', stderr: stderr || '', }); }); }); } function buildDeployState(body, extra) { const payload = body || {}; const override = extra || {}; return { target: override.target || payload.target || DEFAULT_SSH_TARGET, remoteUploadPath: override.remoteUploadPath || payload.remoteUploadPath || DEFAULT_REMOTE_UPLOAD_PATH, deployCommand: override.deployCommand || payload.deployCommand || DEFAULT_DEPLOY_COMMAND, logs: override.logs || [], error: override.error || '', result: override.result || null, }; } app.get('/', function(req, res) { res.type('html').send(renderPage({ deploy: buildDeployState({}), })); }); app.post('/login', async function(req, res) { const email = (req.body && req.body.email) || ''; const password = (req.body && req.body.password) || ''; const logs = []; const logger = function(message) { logs.push(message); }; if (!email || !password) { res.status(400).type('html').send(renderPage({ email, error: 'メールアドレスとパスワードは必須です。', logs, deploy: buildDeployState({}), })); return; } try { const result = await fetchAlexaCookieAndSave({ email, password, logger }); logs.push(''); logs.push('=============================================='); logs.push('認証成功'); logs.push(`保存先: ${result.envPath}`); logs.push(`Cookie 長さ: ${result.cookieLength}`); res.type('html').send(renderPage({ email, result, logs, deploy: buildDeployState({}), })); } catch (error) { logs.push(''); logs.push('[ERROR] ' + error.message); res.status(500).type('html').send(renderPage({ email, error: error.message, logs, deploy: buildDeployState({}), })); } }); app.post('/deploy', async function(req, res) { const deploy = buildDeployState(req.body); const logs = []; if (!deploy.target || !deploy.remoteUploadPath || !deploy.deployCommand) { res.status(400).type('html').send(renderPage({ deploy: buildDeployState(req.body, { logs, error: 'SSH 接続先、一時アップロード先、専用コマンドは必須です。', }), })); return; } try { logs.push('[1] .env をサーバーの一時パスへ転送中...'); logs.push(`scp ${LOCAL_ENV_PATH} ${deploy.target}:${deploy.remoteUploadPath}`); const scpResult = await runCommand('scp', [LOCAL_ENV_PATH, `${deploy.target}:${deploy.remoteUploadPath}`]); if (scpResult.stdout.trim()) logs.push(scpResult.stdout.trim()); if (scpResult.stderr.trim()) logs.push(scpResult.stderr.trim()); logs.push(''); logs.push('[2] 専用デプロイスクリプトを実行中...'); logs.push(`ssh ${deploy.target} ${deploy.deployCommand}`); const sshResult = await runCommand('ssh', [deploy.target, deploy.deployCommand]); if (sshResult.stdout.trim()) logs.push(sshResult.stdout.trim()); if (sshResult.stderr.trim()) logs.push(sshResult.stderr.trim()); logs.push(''); logs.push('反映完了'); res.type('html').send(renderPage({ deploy: buildDeployState(req.body, { logs, result: { ok: true }, }), })); } catch (error) { if (error.stdout && error.stdout.trim()) logs.push(error.stdout.trim()); if (error.stderr && error.stderr.trim()) logs.push(error.stderr.trim()); logs.push(''); logs.push('[ERROR] ' + error.message); res.status(500).type('html').send(renderPage({ deploy: buildDeployState(req.body, { logs, error: error.stderr || error.message, }), })); } }); app.listen(PORT, '127.0.0.1', function() { console.log('[INFO] auth4-web listening on http://127.0.0.1:' + PORT); });