From 86951c69339d7a5803ca05396dd5c65fdf8a263c Mon Sep 17 00:00:00 2001 From: Bxio Date: Sat, 10 Oct 2026 18:23:43 +0100 Subject: [PATCH] Add local Discord owner login and XanaX dashboard --- .dockerignore | 4 +++ .env.example | 4 +++ .gitignore | 4 +++ Dockerfile | 9 +++++ README.md | 26 +++++++++++++++ auth.test.mjs | 43 ++++++++++++++++++++++++ compose.yaml | 20 ++++++++++++ package.json | 1 + public/app.js | 31 ++++++++++++++++++ public/favicon.ico | Bin 0 -> 102977 bytes public/index.html | 1 + public/logo.png | Bin 0 -> 26082 bytes public/style.css | 1 + server.mjs | 80 +++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 224 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 auth.test.mjs create mode 100644 compose.yaml create mode 100644 package.json create mode 100644 public/app.js create mode 100644 public/favicon.ico create mode 100644 public/index.html create mode 100644 public/logo.png create mode 100644 public/style.css create mode 100644 server.mjs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6fbc9ca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.env +data +node_modules diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cb15442 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +APP_URL=http://192.168.88.99:3080 +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +OWNER_DISCORD_ID=376768118535749633 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e1846d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +data/ +node_modules/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4513c4c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-bookworm-slim +WORKDIR /app +COPY --chown=node:node package.json server.mjs ./ +COPY --chown=node:node public ./public +RUN mkdir /app/data && chown node:node /app/data +USER node +ENV PORT=3080 DATA_DIR=/app/data +EXPOSE 3080 +CMD ["npm", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..da095fc --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# TS6 Botflow + +Primeira experiência local: login Discord, conta do dono, sessão persistente e painel XanaX. Logótipos copiados sem alterações do projeto existente. As pastas de origem permanecem intactas. + +O editor de flows e o motor de bots ainda não estão implementados nesta versão. Nenhuma ligação ao TeamSpeak é feita. + +## Docker / Portainer + +Usar `compose.yaml` a partir deste repositório. O serviço é construído pelo Dockerfile, porta 3080, volume persistente `botflow-data`. + +Configurar as quatro variáveis nas Environment variables da stack (ou copiar `.env.example` para `.env` quando executado por Docker Compose): + +- `APP_URL`: endereço usado no navegador, por exemplo `http://192.168.88.99:3080`. +- `DISCORD_CLIENT_ID`: ID da aplicação Discord. +- `DISCORD_CLIENT_SECRET`: segredo OAuth da aplicação, nunca o token de um bot. +- `OWNER_DISCORD_ID`: ID **numérico** da tua conta Discord, não o nome `bxio`. + +No Discord Developer Portal, OAuth2 → Redirects, adicionar exatamente `http://192.168.88.99:3080/auth/discord/callback` (ajustar se APP_URL mudar). É possível aproveitar a aplicação Discord existente adicionando esse redirect; manter os redirects antigos. + +Para copiar o ID: Discord → Definições → Avançadas → Modo de programador; clicar com o botão direito na tua conta → Copiar ID do utilizador. + +O navegador e o container precisam de Internet para o OAuth Discord. O painel não precisa de estar publicado na Internet. A rede local precisa de conseguir aceder à porta 3080. Uma conta diferente do OWNER_DISCORD_ID é recusada. Esta versão não tem gestão de outros perfis. + +Local: Node 22.15+, definir as mesmas variáveis e executar `npm start`. Testes: `npm test`. + +Sessões HttpOnly/SameSite, estado OAuth de uso único, credenciais no ambiente e armazenamento SQLite no volume. Em HTTP local os cookies não têm Secure; usar HTTPS quando houver publicação. Não adicionar `.env` nem dados ao Git. diff --git a/auth.test.mjs b/auth.test.mjs new file mode 100644 index 0000000..fd87506 --- /dev/null +++ b/auth.test.mjs @@ -0,0 +1,43 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createApp } from './server.mjs'; + +test('OAuth rejects strangers, invalid state and replay; owner session requires authentication', async () => { + const directory = mkdtempSync(join(tmpdir(), 'botflow-test-')); + let profileId = '999999999999999999'; + let requests = 0; + const server = createApp({ env: { DATA_DIR: directory, APP_URL: 'http://localhost:3080', DISCORD_CLIENT_ID: 'client', DISCORD_CLIENT_SECRET: 'secret', OWNER_DISCORD_ID: '111111111111111111' }, discordFetch: async (url) => { + requests++; + return { ok: true, json: async () => url.endsWith('/token') ? { access_token: 'temporary' } : { id: profileId, username: 'bxio' } }; + } }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const base = `http://127.0.0.1:${server.address().port}`; + const request = (path, options = {}) => fetch(base + path, { redirect: 'manual', ...options }); + const begin = async () => { const response = await request('/auth/discord'); const state = new URL(response.headers.get('location')).searchParams.get('state'); return { state, cookie: `oauth_state=${state}` }; }; + try { + assert.equal((await request('/')).status, 200); + assert.equal((await request('/api/dashboard')).status, 401); + assert.equal((await request('/auth/discord/callback?state=fake&code=code', { headers: { Cookie: 'oauth_state=fake' } })).headers.get('location'), '/?error=authentication'); + assert.equal(requests, 0); + const outsider = await begin(); + const denied = await request(`/auth/discord/callback?state=${outsider.state}&code=code`, { headers: { Cookie: outsider.cookie } }); + assert.equal(denied.headers.get('location'), '/?error=denied'); + profileId = '111111111111111111'; + const owner = await begin(); + const callback = `/auth/discord/callback?state=${owner.state}&code=code`; + const login = await request(callback, { headers: { Cookie: owner.cookie } }); + assert.equal(login.headers.get('location'), '/'); + const sessionCookie = login.headers.getSetCookie().find(v => v.startsWith('session=')).split(';')[0]; + assert.equal((await request('/api/dashboard', { headers: { Cookie: sessionCookie } })).status, 200); + assert.equal((await request(callback, { headers: { Cookie: owner.cookie } })).headers.get('location'), '/?error=authentication'); + assert.equal((await request('/auth/logout', { method: 'POST', headers: { Cookie: sessionCookie, Origin: 'http://evil.example' } })).status, 403); + assert.equal((await request('/auth/logout', { method: 'POST', headers: { Cookie: sessionCookie, Origin: 'http://localhost:3080' } })).status, 200); + assert.equal((await request('/api/dashboard', { headers: { Cookie: sessionCookie } })).status, 401); + } finally { + await new Promise(resolve => server.close(resolve)); + rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..6aa0653 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,20 @@ +services: + ts6-botflow: + build: . + ports: + - "3080:3080" + environment: + APP_URL: ${APP_URL:-http://192.168.88.99:3080} + DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID:?Define DISCORD_CLIENT_ID} + DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET:?Define DISCORD_CLIENT_SECRET} + OWNER_DISCORD_ID: ${OWNER_DISCORD_ID:-376768118535749633} + volumes: + - botflow-data:/app/data + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:3080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 30s + timeout: 5s + retries: 3 +volumes: + botflow-data: diff --git a/package.json b/package.json new file mode 100644 index 0000000..8e186f2 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{"name":"ts6-botflow","version":"0.1.0","private":true,"type":"module","scripts":{"start":"node --experimental-sqlite server.mjs","test":"node --experimental-sqlite --test"},"engines":{"node":">=22.15"}} diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..61c4537 --- /dev/null +++ b/public/app.js @@ -0,0 +1,31 @@ +const app = document.querySelector('#app'); +const errors = { denied: 'Esta conta ainda não tem acesso. Nesta fase, só o dono pode entrar.', authentication: 'O login expirou ou foi cancelado. Tenta novamente.', configuration: 'Falta configurar o Discord e o ID numérico do dono no Docker.' }; +async function boot() { + const response = await fetch('/api/me'); + if (!response.ok) throw new Error(); + const { user, configured } = await response.json(); + if (!user) { + app.innerHTML = `
`; + const message = errors[new URLSearchParams(location.search).get('error')] || (!configured && errors.configuration); + if (message) { const notice = document.querySelector('#notice'); notice.className = 'notice'; notice.textContent = message; } + return; + } + const data = await fetch('/api/dashboard'); if (!data.ok) throw new Error(); + const dashboard = await data.json(); + app.innerHTML = `
TS6 Botflow Local
XanaX · O teu centro de automações
`; + document.querySelector('#username').textContent = user.name; + const content = document.querySelector('#content'); + function page(name) { + document.querySelectorAll('.nav').forEach(b => b.classList.toggle('active', b.dataset.page === name)); + if (name === 'access') { + content.innerHTML = '

Acesso ao painel

A tua conta está autorizada como dono.

PerfilDono
ID Discord
Novos utilizadoresSem acesso
'; + document.querySelector('#discord-id').textContent = user.id; return; + } + if (name === 'bots') { content.innerHTML = '

Bots e flows

A primeira experiência valida o painel e o acesso.

◇Ainda não existem bots.

O editor de flows e a ligação ao TeamSpeak serão a próxima etapa.

'; return; } + content.innerHTML = `

Dashboard

Bem-vindo ao teu centro de automações.

${[['Bots',dashboard.bots.length],['Flows',dashboard.flows.length],['Execuções',dashboard.executions.length],['Servidores',dashboard.connections.length]].map(([label,value])=>`
${label}${value}
`).join('')}

Atividade recente

◷Ainda sem execuções.

A atividade dos bots aparecerá aqui.

O teu espaço

AcessoDono
AutenticaçãoDiscord
Ligação TeamSpeakPor configurar

Começamos por testar o login. Depois ligamos os bots e construímos os flows.

`; + } + document.querySelectorAll('.nav').forEach(b => b.onclick = () => page(b.dataset.page)); + document.querySelector('#logout').onclick = async () => { const r = await fetch('/auth/logout', { method: 'POST' }); if (r.ok) location.assign('/'); }; + page('dashboard'); +} +boot().catch(() => { app.textContent = 'Não foi possível carregar o painel. Atualiza a página para tentar novamente.'; }); diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3867cd26d724def1655af0a97f6f1334dd4b0988 GIT binary patch literal 102977 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 literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..706ec50 --- /dev/null +++ b/public/index.html @@ -0,0 +1 @@ +XanaX · Botflow

A carregar…

diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5d24abfa23d4c73a36c830741f9d44db00488792 GIT binary patch literal 26082 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 MQqxy0Rk4rwKf5X!jsO4v literal 0 HcmV?d00001 diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..55947f5 --- /dev/null +++ b/public/style.css @@ -0,0 +1 @@ +:root{color-scheme:dark;font-family:'Public Sans',system-ui,sans-serif;background:#25293c;color:#cfcde4;--surface:#2f3349;--purple:#7367f0}*{box-sizing:border-box}body{margin:0}button,a{font:inherit}button,.button{display:inline-flex;align-items:center;justify-content:center;gap:10px;border:0;border-radius:6px;padding:12px 20px;background:var(--purple);color:white;text-decoration:none;cursor:pointer}button:hover,.button:hover{background:#8479f5}button:focus-visible,a:focus-visible{outline:2px solid white;outline-offset:4px}.login{min-height:100vh;display:grid;place-items:center;padding:24px}.login-card{width:min(440px,100%);padding:40px;background:var(--surface);border-radius:10px;box-shadow:0 8px 32px #13162440}.logo{width:190px;height:auto;object-fit:contain}.login h1{font-size:24px;margin-top:34px;color:#e6e5f2}p{line-height:1.7;color:#aaa8bf}.login .button{width:100%;margin-top:16px}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:1.4px;color:#9591ab}.notice{padding:14px;background:#ff9f4318;border:1px solid #ff9f4340;border-radius:6px;font-size:13px;color:#ffd5aa}.shell{display:grid;grid-template-columns:240px 1fr;min-height:100vh}.sidebar{background:var(--surface);padding:30px 18px}.sidebar .logo{width:180px;margin-bottom:40px}.nav{display:block;width:100%;text-align:left;padding:13px 16px;margin:8px 0;background:transparent;color:#cfcde4}.nav.active{background:var(--purple)}.workspace{padding:28px 36px;max-width:1500px;width:100%;margin:auto}.topbar{display:flex;align-items:center;justify-content:space-between;background:var(--surface);padding:18px 24px;border-radius:8px;margin-bottom:36px}.muted{font-size:13px;color:#aaa8bf}.quiet{background:#7367f019;color:#b5adff;padding:8px 12px}.title{font-size:27px;color:#e6e5f2;margin-bottom:6px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;margin:28px 0}.card{background:var(--surface);padding:24px;border-radius:8px}.stat-value{display:block;font-size:30px;color:#e6e5f2;margin-top:10px}.split{display:grid;grid-template-columns:1.6fr 1fr;gap:22px}h2{font-size:18px;font-weight:600;margin:0 0 18px}.empty{text-align:center;padding:42px 20px;color:#aaa8bf}.empty-symbol{display:block;font-size:36px;color:#7367f0;margin-bottom:15px}.badge{padding:5px 9px;border-radius:4px;background:#28c76f18;color:#50d895;font-size:12px}.footer{margin-top:32px;font-size:12px;color:#9591ab}.details{display:grid;gap:18px;font-size:14px}.details div{display:flex;justify-content:space-between;gap:16px}@media(max-width:900px){.shell{grid-template-columns:1fr}.sidebar{padding:18px}.sidebar .logo{margin-bottom:12px}.sidebar nav{display:flex;flex-wrap:wrap}.nav{width:auto}.workspace{padding:18px}.stats{grid-template-columns:repeat(2,1fr)}.split{grid-template-columns:1fr}}@media(max-width:480px){.login-card{padding:26px}.topbar{padding:14px;gap:12px}.stats{gap:12px}.card{padding:18px}} diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..171f018 --- /dev/null +++ b/server.mjs @@ -0,0 +1,80 @@ +import http from 'node:http'; +import { randomBytes, createHash } from 'node:crypto'; +import { mkdirSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; +import { fileURLToPath } from 'node:url'; + +export function createApp({ env = process.env, discordFetch = fetch } = {}) { + const appUrl = new URL(env.APP_URL || 'http://localhost:3080'); + const dataDir = resolve(env.DATA_DIR || 'data'); + mkdirSync(dataDir, { recursive: true }); + const db = new DatabaseSync(resolve(dataDir, 'botflow.sqlite')); + db.exec('CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, user TEXT NOT NULL, expires INTEGER NOT NULL)'); + const pending = new Map(); + const root = fileURLToPath(new URL('./public/', import.meta.url)); + const configured = Boolean(env.DISCORD_CLIENT_ID && env.DISCORD_CLIENT_SECRET && /^\d{17,20}$/.test(env.OWNER_DISCORD_ID || '')); + const token = () => randomBytes(32).toString('hex'); + const hash = value => createHash('sha256').update(value).digest('hex'); + const cookie = (name, value, age) => `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${age}${appUrl.protocol === 'https:' ? '; Secure' : ''}`; + const server = http.createServer(async (req, res) => { + res.setHeader('Cache-Control', 'no-store'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Referrer-Policy', 'no-referrer'); + res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self'; style-src 'self'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"); + const url = new URL(req.url, appUrl); + const cookies = Object.fromEntries((req.headers.cookie || '').split(';').map(v => v.trim().split('='))); + const json = (status, body) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); }; + const redirect = (location, cookies) => { res.writeHead(302, { Location: location, ...(cookies ? { 'Set-Cookie': cookies } : {}) }); res.end(); }; + try { + if (req.method === 'GET' && url.pathname === '/health') return json(200, { status: 'ok' }); + if (req.method === 'GET' && url.pathname === '/auth/discord') { + if (!configured) return redirect('/?error=configuration'); + for (const [key, expiry] of pending) if (expiry < Date.now()) pending.delete(key); + if (pending.size >= 1000) return json(429, { error: 'Tenta novamente mais tarde.' }); + const state = token(); pending.set(state, Date.now() + 600000); + const auth = new URL('https://discord.com/oauth2/authorize'); + auth.search = new URLSearchParams({ client_id: env.DISCORD_CLIENT_ID, redirect_uri: `${appUrl.origin}/auth/discord/callback`, response_type: 'code', scope: 'identify', state }).toString(); + return redirect(auth.href, cookie('oauth_state', state, 600)); + } + if (req.method === 'GET' && url.pathname === '/auth/discord/callback') { + const state = url.searchParams.get('state'); + const valid = configured && state && state === cookies.oauth_state && pending.get(state) > Date.now(); + if (state && state === cookies.oauth_state) pending.delete(state); + if (!valid || !url.searchParams.get('code')) return redirect('/?error=authentication', cookie('oauth_state', '', 0)); + const response = await discordFetch('https://discord.com/api/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: env.DISCORD_CLIENT_ID, client_secret: env.DISCORD_CLIENT_SECRET, grant_type: 'authorization_code', code: url.searchParams.get('code'), redirect_uri: `${appUrl.origin}/auth/discord/callback` }), signal: AbortSignal.timeout(15000) }); + if (!response.ok) throw new Error('oauth'); + const credentials = await response.json(); + const profile = await discordFetch('https://discord.com/api/v10/users/@me', { headers: { Authorization: `Bearer ${credentials.access_token}` }, signal: AbortSignal.timeout(15000) }); + if (!profile.ok) throw new Error('profile'); + const user = await profile.json(); + if (user.id !== env.OWNER_DISCORD_ID) return redirect('/?error=denied', cookie('oauth_state', '', 0)); + const session = token(); + db.prepare('DELETE FROM sessions WHERE expires < ?').run(Date.now()); + db.prepare('INSERT INTO sessions VALUES (?, ?, ?)').run(hash(session), JSON.stringify({ id: user.id, name: user.global_name || user.username, role: 'Dono' }), Date.now() + 86400000); + return redirect('/', [cookie('session', session, 86400), cookie('oauth_state', '', 0)]); + } + const session = cookies.session && db.prepare('SELECT user FROM sessions WHERE id = ? AND expires > ?').get(hash(cookies.session), Date.now()); + const user = session ? JSON.parse(session.user) : null; + if (req.method === 'GET' && url.pathname === '/api/me') return json(200, { user, configured }); + if (req.method === 'POST' && url.pathname === '/auth/logout') { + if (req.headers.origin !== appUrl.origin) return json(403, { error: 'Origem inválida.' }); + if (cookies.session) db.prepare('DELETE FROM sessions WHERE id = ?').run(hash(cookies.session)); + res.setHeader('Set-Cookie', cookie('session', '', 0)); return json(200, { ok: true }); + } + if (url.pathname.startsWith('/api/')) { + if (!user) return json(401, { error: 'Autenticação necessária.' }); + if (req.method === 'GET' && url.pathname === '/api/dashboard') return json(200, { bots: [], flows: [], executions: [], connections: [] }); + return json(404, { error: 'Não encontrado.' }); + } + const files = { '/': ['index.html', 'text/html; charset=utf-8'], '/app.js': ['app.js', 'text/javascript; charset=utf-8'], '/style.css': ['style.css', 'text/css'], '/logo.png': ['logo.png', 'image/png'], '/favicon.ico': ['favicon.ico', 'image/x-icon'] }; + if (req.method !== 'GET' || !files[url.pathname]) return json(404, { error: 'Não encontrado.' }); + const [file, type] = files[url.pathname]; res.writeHead(200, { 'Content-Type': type }); res.end(readFileSync(resolve(root, file))); + } catch { json(502, { error: 'Não foi possível concluir o login Discord. Tenta novamente.' }); } + }); + server.on('close', () => db.close()); + return server; +} +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + createApp().listen(Number(process.env.PORT || 3080), '0.0.0.0', () => console.log(`TS6 Botflow disponível na porta ${process.env.PORT || 3080}`)); +}