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>
169 lines
5.2 KiB
JavaScript
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}`);
|
|
});
|