ken_nogi/NodeSrv/apps/app-portal/src/index.js
Kenichiro NOGI ce58cb4be4 初回コミット: dev配下(NodeSrv/Pleasanter等)をGitea管理下に統合
GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。
NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは
別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter
インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:37:06 +09:00

169 lines
5.2 KiB
JavaScript

const express = require('express');
const path = require('path');
const { createAllowlistMiddleware } = require('./allowlist');
const { renderDashboard } = require('./dashboard');
const { getComposeEnv, getContainers, readLogs, startCompose, stopCompose, restartContainer } = require('./dokployClient');
const { extractPortalMeta } = require('./portalMeta');
const { getFilePath: getAllowlistFilePath, ensureFile, readEmails, addEmail, removeEmail } = require('./allowlistStore');
const {
verifyMasterKey,
createSessionToken,
verifySessionToken,
parseCookies,
buildSessionCookieHeader,
buildLogoutCookieHeader,
SESSION_COOKIE_NAME,
} = require('./adminAuth');
const { renderLoginPage, renderAdminPage } = require('./adminView');
const app = express();
const PORT = process.env.PORT || 3000;
const ENVIRONMENT_ID = process.env.DOKPLOY_ENVIRONMENT_ID;
const PORTAL_SECRET = process.env.PORTAL_SECRET;
const MASTER_KEY = process.env.PORTAL_MASTER_KEY;
const ALLOWLIST_FILE = getAllowlistFilePath();
ensureFile(ALLOWLIST_FILE, process.env.PORTAL_ALLOWED_EMAILS);
app.use(express.static(path.join(__dirname, '..', 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy' });
});
function requireAdminSession(req, res, next) {
const cookies = parseCookies(req.headers.cookie);
if (!verifySessionToken(cookies[SESSION_COOKIE_NAME], MASTER_KEY)) {
res.redirect('/admin/login');
return;
}
next();
}
app.get('/admin/login', (req, res) => {
res.set('Content-Type', 'text/html; charset=utf-8').send(renderLoginPage());
});
app.post('/admin/login', (req, res) => {
if (!verifyMasterKey(req.body.masterKey, MASTER_KEY)) {
res.set('Content-Type', 'text/html; charset=utf-8').send(renderLoginPage('Master Keyが正しくありません'));
return;
}
res.set('Set-Cookie', buildSessionCookieHeader(createSessionToken(MASTER_KEY)));
res.redirect('/admin');
});
app.post('/admin/logout', (req, res) => {
res.set('Set-Cookie', buildLogoutCookieHeader());
res.redirect('/admin/login');
});
app.get('/admin', requireAdminSession, (req, res) => {
res.set('Content-Type', 'text/html; charset=utf-8').send(renderAdminPage(readEmails(ALLOWLIST_FILE)));
});
app.post('/admin/allowlist/add', requireAdminSession, (req, res) => {
try {
addEmail(ALLOWLIST_FILE, req.body.email);
} catch (err) {
res.set('Content-Type', 'text/html; charset=utf-8').send(renderAdminPage(readEmails(ALLOWLIST_FILE), err.message));
return;
}
res.redirect('/admin');
});
app.post('/admin/allowlist/remove', requireAdminSession, (req, res) => {
removeEmail(ALLOWLIST_FILE, req.body.email);
res.redirect('/admin');
});
app.use(createAllowlistMiddleware(ALLOWLIST_FILE));
app.get('/', async (req, res) => {
try {
const html = await renderDashboard(ENVIRONMENT_ID);
res.set('Content-Type', 'text/html; charset=utf-8').send(html);
} catch (err) {
console.error('dashboard render failed', err.message);
res.status(500).send('ダッシュボード取得に失敗しました');
}
});
app.post('/api/compose/:composeId/start', async (req, res) => {
try {
await startCompose(req.params.composeId);
res.sendStatus(202);
} catch (err) {
console.error('start failed', err.message);
res.sendStatus(502);
}
});
app.post('/api/compose/:composeId/stop', async (req, res) => {
try {
await stopCompose(req.params.composeId);
res.sendStatus(202);
} catch (err) {
console.error('stop failed', err.message);
res.sendStatus(502);
}
});
app.post('/api/compose/:composeId/restart', async (req, res) => {
try {
const { appName } = await getComposeEnv(req.params.composeId);
const containers = await getContainers(appName);
const containerId = containers[0]?.containerId;
if (!containerId) {
res.sendStatus(404);
return;
}
await restartContainer(containerId);
res.sendStatus(202);
} catch (err) {
console.error('restart failed', err.message);
res.sendStatus(502);
}
});
app.get('/api/compose/:composeId/logs', async (req, res) => {
try {
const { appName } = await getComposeEnv(req.params.composeId);
const containers = await getContainers(appName);
const containerId = containers[0]?.containerId;
if (!containerId) {
res.sendStatus(404);
return;
}
const logs = await readLogs(req.params.composeId, containerId, 200);
res.json({ logs });
} catch (err) {
console.error('logs failed', err.message);
res.sendStatus(502);
}
});
app.post('/api/compose/:composeId/trigger', async (req, res) => {
try {
const { env } = await getComposeEnv(req.params.composeId);
const meta = extractPortalMeta(env);
if (!meta || meta.appType !== 'batch' || !meta.url || !meta.triggerPath) {
res.sendStatus(400);
return;
}
const triggerRes = await fetch(`${meta.url}${meta.triggerPath}`, {
method: 'POST',
headers: { 'X-Portal-Secret': PORTAL_SECRET },
});
res.sendStatus(triggerRes.status === 202 ? 202 : 502);
} catch (err) {
console.error('trigger failed', err.message);
res.status(502).json({ error: '応答なし' });
}
});
app.listen(PORT, () => {
console.log(`app-portal listening on port ${PORT}`);
});