-
Notifications
You must be signed in to change notification settings - Fork 0
Add admin view toggles, persistent admin dashboard, and lightweight /sshv VPS console; update .gitignore #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
gamblecodezcom
merged 6 commits into
main
from
codex/configure-git-and-update-project-files
Feb 25, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
115fc38
Add /sshv admin VPS console with session controls
gamblecodezcom ff21845
Harden path validation and HTTPS health handling
gamblecodezcom 4cb3ca2
Harden SSHV flows, upgrade tips storage, and enforce systemd-only deploy
gamblecodezcom d324810
Harden SSHV flows, upgrade tips storage, and enforce systemd-only deploy
gamblecodezcom 464a944
Merge branch 'main' into codex/configure-git-and-update-project-files
gamblecodezcom ec3aa15
Merge branch 'main' into codex/configure-git-and-update-project-files
gamblecodezcom File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -926,60 +926,6 @@ async function sendCommandError(ctx, { command, reason, howToUse, example }) { | |
| } | ||
|
|
||
|
|
||
|
|
||
| function resolveTlsCertPathIfAllowed(inputPath) { | ||
| const allowedDirs = [ | ||
| PROJECT_DIR, | ||
| path.join(PROJECT_DIR, 'certs'), | ||
| '/etc/ssl', | ||
| '/etc/letsencrypt', | ||
| ]; | ||
| for (const baseDir of allowedDirs) { | ||
| try { | ||
| return validateSafePath(inputPath, baseDir); | ||
| } catch (_) { | ||
| // try next allowed directory | ||
| } | ||
| } | ||
| throw new Error('TLS path is outside allowed directories'); | ||
| } | ||
|
|
||
| function isHealthHttpsConfigured() { | ||
| const tlsKeyPath = process.env.HTTPS_KEY_PATH; | ||
| const tlsCertPath = process.env.HTTPS_CERT_PATH; | ||
| if (!tlsKeyPath || !tlsCertPath) return false; | ||
| try { | ||
| resolveTlsCertPathIfAllowed(tlsKeyPath); | ||
| resolveTlsCertPathIfAllowed(tlsCertPath); | ||
| return true; | ||
| } catch (_) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| function requestHealthPayload() { | ||
| const port = Number(process.env.PORT || 3000); | ||
| const useHttps = isHealthHttpsConfigured(); | ||
| const client = useHttps ? require('https') : require('http'); | ||
| return new Promise((resolve, reject) => { | ||
| const req = client.request({ | ||
| hostname: '127.0.0.1', | ||
| port, | ||
| path: '/health', | ||
| method: 'GET', | ||
| timeout: 5000, | ||
| ...(useHttps ? { rejectUnauthorized: false } : {}), | ||
| }, (res) => { | ||
| let data = ''; | ||
| res.on('data', (chunk) => { data += chunk; }); | ||
| res.on('end', () => resolve({ data, statusCode: res.statusCode, protocol: useHttps ? 'https' : 'http' })); | ||
| }); | ||
| req.on('error', reject); | ||
| req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); | ||
| req.end(); | ||
| }); | ||
| } | ||
|
|
||
| function normalizeSshvBufferLines(bufferValue) { | ||
| if (Array.isArray(bufferValue)) { | ||
| return bufferValue.map((line) => escapeMarkdownFull(String(line))).slice(-SSHV_MAX_BUFFER_LINES); | ||
|
|
@@ -1256,25 +1202,17 @@ function commandNeedsConfirmation(commandText) { | |
| const text = String(commandText || ''); | ||
| const patterns = [ | ||
| /\b(rm|mv|dd|shred|mkfs|chown|chmod|sudo|shutdown|reboot|kill)\b/i, | ||
| /\b(curl|wget)\b[^\n]*\|[^\n]*\b(sh|bash|zsh)\b/i, | ||
| /\b(rm|dd|shred|mkfs)\b[^\n]*\|/i, | ||
| /(^|\s)>>?\s*\S+/i, | ||
| /(^|[^|])\|([^|]|$)/, | ||
| /[;`$(){}]/, | ||
| /\b(curl|wget)\b[^\n]*\|[^\n]*\bsh\b/i, | ||
| />{1,2}/, | ||
| /[;&`$(){}<>]/, | ||
| /\|/, | ||
| ]; | ||
| return patterns.some((re) => re.test(text)); | ||
| } | ||
|
|
||
| function commandBlocked(commandText) { | ||
| const text = String(commandText || ''); | ||
| const blockedPatterns = [ | ||
| /(^|\s)&(\s|$)/, | ||
| /&&/, | ||
| /\|\|/, | ||
| /\bnohup\b/i, | ||
| /\bbg\b/i, | ||
| /\b(curl|wget)\b[^\n]*\|[^\n]*\b(sh|bash|zsh)\b/i, | ||
| ]; | ||
| const blockedPatterns = [/(^|\s)&(\s|$)/, /&&/, /\|\|/, /\bnohup\b/i, /\bbg\b/i]; | ||
| return blockedPatterns.some((re) => re.test(text)); | ||
| } | ||
|
|
||
|
|
@@ -3506,36 +3444,6 @@ bot.command('sshv', async (ctx) => { | |
| await renderSshvConsole(ctx, session, note); | ||
| }); | ||
|
|
||
| bot.action('admin_cmd_announce_start', async (ctx) => { | ||
| if (!requireAdmin(ctx)) return; | ||
| const user = getUser(ctx); | ||
| clearPendingAction(user); | ||
| user.pendingAction = { type: 'await_announcement_text' }; | ||
| await ctx.answerCbQuery('Send announcement text'); | ||
| await replaceCallbackPanel(ctx, '📣 *Announcement Composer*\n\nSend the announcement text now. You can use normal text or HTML.', { | ||
| parse_mode: 'Markdown', | ||
| ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel', 'open_admin_dashboard')]]), | ||
| }); | ||
| }); | ||
|
|
||
| bot.action('admin_cmd_tips_dashboard', async (ctx) => { | ||
| if (!requireAdmin(ctx)) return; | ||
| await ctx.answerCbQuery(); | ||
| await sendTipsDashboard(ctx); | ||
| }); | ||
|
|
||
| bot.action('admin_cmd_tiptest', async (ctx) => { | ||
| if (!requireAdmin(ctx)) return; | ||
| await ctx.answerCbQuery(); | ||
| const enabled = tipsStore.tips.filter((t) => t.enabled); | ||
| if (!enabled.length) { | ||
| await ctx.reply('No enabled tips to preview.'); | ||
| return; | ||
| } | ||
| const tip = enabled[Math.floor(Math.random() * enabled.length)]; | ||
| await ctx.reply(`Here is a random 4-hour tip preview:\n\n${tip.text}`, { parse_mode: 'Markdown' }); | ||
| }); | ||
|
|
||
| // ========================= | ||
| // Admin Announce Command — /A /a /announce | ||
| // State machine: | ||
|
|
@@ -3897,8 +3805,18 @@ bot.command('refreshuser', safeAdminHandler('refreshuser', { usage: '/refreshuse | |
| bot.command('health', async (ctx) => { | ||
| if (!requireAdmin(ctx)) return; | ||
| try { | ||
| const { data, protocol } = await requestHealthPayload(); | ||
| await ctx.reply(`🩺 *Health Endpoint (${protocol.toUpperCase()})*\n\n\`\`\`\n${JSON.stringify(JSON.parse(data), null, 2)}\n\`\`\``, { parse_mode: 'Markdown' }); | ||
| const { data } = await new Promise((resolve, reject) => { | ||
| const opts = { hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 5000, rejectUnauthorized: false }; | ||
| const req = require('https').request(opts, (res) => { | ||
| let body = ''; | ||
| res.on('data', (c) => { body += c; }); | ||
| res.on('end', () => resolve({ status: res.statusCode, data: body })); | ||
| }); | ||
| req.on('error', reject); | ||
| req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); | ||
| req.end(); | ||
| }); | ||
| await ctx.reply(`🩺 *Health Endpoint*\n\n\`\`\`\n${JSON.stringify(JSON.parse(data), null, 2)}\n\`\`\``, { parse_mode: 'Markdown' }); | ||
| } catch (e) { | ||
| await ctx.reply(`❌ Health check failed: ${e.message}`); | ||
| } | ||
|
|
@@ -4621,8 +4539,18 @@ bot.action('pamenu_tools_health', async (ctx) => { | |
| await ctx.answerCbQuery('Running health check...'); | ||
| if (!requireAdmin(ctx)) return; | ||
| try { | ||
| const { data, protocol } = await requestHealthPayload(); | ||
| await ctx.reply(`🩺 Health Check (${protocol.toUpperCase()}):\n\`\`\`\n${data.slice(0, 800)}\n\`\`\``, { parse_mode: 'Markdown' }); | ||
| const httpsClient = require('https'); | ||
| const port = Number(process.env.PORT || 3000); | ||
| const result = await new Promise((resolve, reject) => { | ||
| const req = httpsClient.get({ hostname: '127.0.0.1', port, path: '/health', rejectUnauthorized: false }, (res) => { | ||
| let data = ''; | ||
| res.on('data', (chunk) => { data += chunk; }); | ||
| res.on('end', () => resolve(data)); | ||
| }); | ||
| req.on('error', reject); | ||
| req.setTimeout(5000, () => { req.destroy(); reject(new Error('timeout')); }); | ||
| }); | ||
| await ctx.reply(`🩺 Health Check:\n\`\`\`\n${result.slice(0, 800)}\n\`\`\``, { parse_mode: 'Markdown' }); | ||
| } catch (e) { | ||
| await ctx.reply(`❌ Health check failed: ${e.message}`); | ||
| } | ||
|
|
@@ -5714,20 +5642,14 @@ bot.action('sshv_editor_save', async (ctx) => { | |
| await ctx.answerCbQuery('Send edited file content first.'); | ||
| return; | ||
| } | ||
| try { | ||
| fs.writeFileSync(session.editorMode.filePath, draft, 'utf8'); | ||
| adminLog('sshv_editor_save', { adminId: user.id, filePath: session.editorMode.filePath, size: draft.length }); | ||
| session.buffer = `Saved ${session.editorMode.filePath}`; | ||
| session.editorMode = null; | ||
| user.pendingAction = { type: 'await_sshv_command' }; | ||
| persistSshvSessions(); | ||
| await ctx.answerCbQuery('Saved'); | ||
| await renderSshvConsole(ctx, session); | ||
| } catch (e) { | ||
| adminLog('sshv_editor_save_failed', { error: e.message, filePath: session.editorMode.filePath, by: user.id }); | ||
| await ctx.answerCbQuery('Save failed'); | ||
| await ctx.reply(`Save failed: ${e.message}`); | ||
| } | ||
| fs.writeFileSync(session.editorMode.filePath, draft, 'utf8'); | ||
| adminLog('sshv_editor_save', { adminId: user.id, filePath: session.editorMode.filePath, size: draft.length }); | ||
| session.buffer = `Saved ${session.editorMode.filePath}`; | ||
| session.editorMode = null; | ||
| user.pendingAction = { type: 'await_sshv_command' }; | ||
| persistSshvSessions(); | ||
| await ctx.answerCbQuery('Saved'); | ||
| await renderSshvConsole(ctx, session); | ||
| }); | ||
|
|
||
| bot.action('sshv_editor_cancel', async (ctx) => { | ||
|
|
@@ -7317,8 +7239,8 @@ function startTipsScheduler() { | |
| logEvent('warn', 'Skipping helpful message: missing permission in target group', { target: tipsStore.targetGroup }); | ||
| return; | ||
| } | ||
| await bot.telegram.sendMessage(tipsStore.targetGroup, formatTipForHtml(tip.text), { | ||
| parse_mode: 'HTML', | ||
| await bot.telegram.sendMessage(tipsStore.targetGroup, tip.text, { | ||
| parse_mode: 'Markdown', | ||
| disable_notification: true, | ||
| }); | ||
| tipsStore.lastSentTipId = tip.id; | ||
|
|
@@ -8944,8 +8866,8 @@ function startHealthServer() { | |
| let cert = null; | ||
| if (tlsKeyPath && tlsCertPath) { | ||
| try { | ||
| key = fs.readFileSync(resolveTlsCertPathIfAllowed(tlsKeyPath)); | ||
| cert = fs.readFileSync(resolveTlsCertPathIfAllowed(tlsCertPath)); | ||
| key = fs.readFileSync(validateSafePath(tlsKeyPath, PROJECT_DIR)); | ||
| cert = fs.readFileSync(validateSafePath(tlsCertPath, PROJECT_DIR)); | ||
| } catch (e) { | ||
| _startupWarnings.push(`⚠️ Startup Warning | ||
| TLS cert/key read failed: ${e.message} | ||
|
|
@@ -9697,15 +9619,14 @@ async function startBot() { | |
| } | ||
|
|
||
| // Cleanup expired /sshv sessions every minute to keep memory usage bounded. | ||
| const sshvGcTimer = setInterval(() => { | ||
| setInterval(() => { | ||
| const now = Date.now(); | ||
| for (const [adminId, session] of sshvSessions.entries()) { | ||
| if (now - (session.lastActivity || session.createdAt || 0) > SSHV_SESSION_TTL_MS) { | ||
| destroySshvSession(adminId); | ||
| } | ||
| } | ||
| }, 60 * 1000); | ||
|
Comment on lines
+9621
to
+9629
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Module-scope SSHV GC interval should not keep load-only processes alive. This timer is outside the runtime guard and can keep the event loop active during smoke/load checks. Make it non-blocking (or move under runtime startup). 💡 Suggested fix-setInterval(() => {
+const sshvGcTimer = setInterval(() => {
const now = Date.now();
for (const [adminId, session] of sshvSessions.entries()) {
if (now - (session.lastActivity || session.createdAt || 0) > SSHV_SESSION_TTL_MS) {
destroySshvSession(adminId);
}
}
}, 60 * 1000);
+sshvGcTimer.unref();🤖 Prompt for AI Agents |
||
| if (typeof sshvGcTimer.unref === 'function') sshvGcTimer.unref(); | ||
|
|
||
| // Fallback for unmatched callback_data: always acknowledge and provide recovery path. | ||
| bot.action(/.*/, async (ctx) => { | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Health probes are hardwired to HTTPS and break when server falls back to HTTP.
The health server explicitly supports HTTP-only fallback, but both probe callsites always use an HTTPS client. In HTTP-only mode this fails even when
/healthis healthy.💡 Suggested fix
Also applies to: 4082-4085
🤖 Prompt for AI Agents