Compare commits

..

4 Commits

2 changed files with 42 additions and 15 deletions

View File

@ -123,7 +123,7 @@
.spark-svg { width:100%;height:100%;overflow:visible; } .spark-svg { width:100%;height:100%;overflow:visible; }
/* services */ /* services */
.service-grid { display:grid;grid-template-columns:repeat(2,1fr);grid-auto-rows:1fr;gap:8px;overflow-y:auto;flex:1; } .service-grid { display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:1fr;gap:8px;overflow-y:auto;flex:1; }
.service-card { background:var(--surface2);border:1px solid var(--border2);border-radius:11px;padding:12px;display:flex;flex-direction:column;gap:6px; } .service-card { background:var(--surface2);border:1px solid var(--border2);border-radius:11px;padding:12px;display:flex;flex-direction:column;gap:6px; }
.service-card-top { display:flex;align-items:center;justify-content:space-between; } .service-card-top { display:flex;align-items:center;justify-content:space-between; }
.service-name { font-size:13px;font-weight:500; } .service-name { font-size:13px;font-weight:500; }
@ -348,12 +348,13 @@ const HEALTH_URL = '/api/health';
const REFRESH_SEC = 30; const REFRESH_SEC = 30;
const HISTORY_MAX = 20; const HISTORY_MAX = 20;
const SERVICES = [ const SERVICES = [
{id:'posimai-dev',name:'posimai-dev', desc:'ブラウザターミナル + Claude Code',url:HEALTH_URL, isHealth:true}, {id:'posimai-dev',name:'posimai-dev', desc:'ブラウザターミナル + Claude Code', url:HEALTH_URL, isHealth:true},
{id:'posimai-api',name:'Posimai API',desc:'Node.js / Express — VPS 本番', url:'/api/vps-health', isHealth:false, proxy:true}, {id:'posimai-api',name:'Posimai API', desc:'Node.js / Express — VPS 本番', url:'/api/vps-health', isHealth:false, proxy:true},
{id:'gitea', name:'Gitea', desc:'ローカル Git バックアップ', url:'/api/check?url=http://100.76.7.3:3000', isHealth:false, proxy:true}, {id:'gitea', name:'Gitea', desc:'ローカル Git バックアップ (NAS)', url:'/api/check?url=http://100.76.7.3:3000', isHealth:false, proxy:true},
{id:'syncthing', name:'Syncthing', desc:'ファイル同期 GUI', url:'/api/check?url=http://100.77.11.43:8384', isHealth:false, proxy:true}, {id:'uptime-kuma',name:'Uptime Kuma', desc:'死活監視 — NAS Docker', url:'/api/check?url=http://100.76.7.3:3001', isHealth:false, proxy:true},
{id:'vercel', name:'Vercel', desc:'PWA ホスティング (27本)', url:'https://vercel.com', isHealth:false}, {id:'syncthing', name:'Syncthing', desc:'ファイル同期 GUI', url:'/api/check?url=http://100.77.11.43:8384', isHealth:false, proxy:true},
{id:'github', name:'GitHub', desc:'ソースコード管理', url:'https://github.com/posimai', isHealth:false}, {id:'vercel', name:'Vercel', desc:'PWA ホスティング (27本)', url:'https://vercel.com', isHealth:false},
{id:'github', name:'GitHub', desc:'ソースコード管理', url:'https://github.com/posimai', isHealth:false},
]; ];
const hist = {cpu:[], load:[]}; const hist = {cpu:[], load:[]};
const svcHist = {}; const svcHist = {};

View File

@ -105,11 +105,23 @@ async function createSessionJWT(userId) {
`INSERT INTO auth_sessions (id, user_id, token_hash, expires_at) VALUES ($1, $2, $3, $4)`, `INSERT INTO auth_sessions (id, user_id, token_hash, expires_at) VALUES ($1, $2, $3, $4)`,
[sessionId, userId, tokenHash, expiresAt] [sessionId, userId, tokenHash, expiresAt]
); );
return jwt.sign({ userId, sid: sessionId }, JWT_SECRET, { expiresIn: JWT_TTL_SECONDS }); // plan を JWT に含める(各アプリがプレミアム判定できるよう)
let plan = 'free';
try {
const r = await pool.query(`SELECT plan FROM users WHERE user_id = $1`, [userId]);
plan = r.rows[0]?.plan || 'free';
} catch (_) {}
return jwt.sign({ userId, sid: sessionId, plan }, JWT_SECRET, { expiresIn: JWT_TTL_SECONDS });
} }
const app = express(); const app = express();
app.use(express.json({ limit: '10mb' })); // Stripe Webhook は raw body が必要なため、webhook パスのみ json パースをスキップ
app.use((req, res, next) => {
if (req.path === '/brain/api/stripe/webhook' || req.path === '/api/stripe/webhook') {
return next();
}
express.json({ limit: '10mb' })(req, res, next);
});
// ── CORS ────────────────────────────────── // ── CORS ──────────────────────────────────
// ALLOWED_ORIGINS 環境変数(カンマ区切り)+ 開発用ローカル // ALLOWED_ORIGINS 環境変数(カンマ区切り)+ 開発用ローカル
@ -136,8 +148,21 @@ app.use((req, res, next) => {
next(); next();
}); });
// /health はサーバー間プロキシ経由で origin なしリクエストが来るため先に CORS * で通す
app.use((req, res, next) => {
if (req.path === '/brain/api/health' || req.path === '/api/health') {
res.setHeader('Access-Control-Allow-Origin', '*');
}
next();
});
app.use(cors({ app.use(cors({
origin: (origin, cb) => { origin: (origin, cb) => {
if (!origin) {
// origin なし = サーバー間リクエストcurl / Node fetch 等)。/health のみ通過させる
// それ以外のエンドポイントはCSRF対策で拒否
return cb(null, false);
}
if (isAllowedOrigin(origin)) cb(null, true); if (isAllowedOrigin(origin)) cb(null, true);
else cb(new Error('CORS not allowed')); else cb(new Error('CORS not allowed'));
}, },
@ -847,19 +872,20 @@ function buildRouter() {
} }
}); });
// GET /api/auth/session/verify — check current JWT + purchase status // GET /api/auth/session/verify — check current JWT + plan
r.get('/auth/session/verify', authMiddleware, async (req, res) => { r.get('/auth/session/verify', authMiddleware, async (req, res) => {
if (req.authType === 'apikey') { if (req.authType === 'apikey') {
return res.json({ ok: true, userId: req.userId, authType: req.authType, purchased: true }); return res.json({ ok: true, userId: req.userId, authType: req.authType, plan: 'premium', purchased: true });
} }
try { try {
const result = await pool.query( const result = await pool.query(
`SELECT purchased_at FROM users WHERE user_id = $1`, [req.userId] `SELECT plan, purchased_at FROM users WHERE user_id = $1`, [req.userId]
); );
const purchased = !!(result.rows[0]?.purchased_at); const plan = result.rows[0]?.plan || 'free';
res.json({ ok: true, userId: req.userId, authType: req.authType, purchased }); const purchased = plan === 'premium';
res.json({ ok: true, userId: req.userId, authType: req.authType, plan, purchased });
} catch (e) { } catch (e) {
res.json({ ok: true, userId: req.userId, authType: req.authType, purchased: false }); res.json({ ok: true, userId: req.userId, authType: req.authType, plan: 'free', purchased: false });
} }
}); });