From 8c430b948690c293d61762aa8e62178c7de08208 Mon Sep 17 00:00:00 2001 From: Bxio Date: Sat, 10 Oct 2026 19:31:36 +0100 Subject: [PATCH] Give both configured Discord accounts owner access --- .env.example | 4 ++-- README.md | 4 ++-- auth.test.mjs | 17 +++++++++++++---- compose.yaml | 3 ++- deploy/bot-stack.yaml | 2 +- server.mjs | 8 +++++--- 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 6489a18..6ec136d 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ APP_URL=http://192.168.88.99:3080 DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= -OWNER_DISCORD_ID=376768118535749633 -ALLOWED_DISCORD_IDS=723613831196311603 +OWNER_DISCORD_ID=376768118535749633,723613831196311603 +ALLOWED_DISCORD_IDS= diff --git a/README.md b/README.md index c1a8971..56aa49a 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Variáveis de ambiente: - `APP_URL`: endereço do 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. -- `OWNER_DISCORD_ID`: ID numérico da conta do dono. -- `ALLOWED_DISCORD_IDS`: IDs de leitores separados por vírgulas. Por defeito, `723613831196311603`; valor vazio remove todos os leitores. +- `OWNER_DISCORD_ID`: IDs numéricos dos donos, separados por vírgulas. A stack inclui `376768118535749633,723613831196311603`. Um único ID continua a ser aceite. +- `ALLOWED_DISCORD_IDS`: IDs de leitores separados por vírgulas. Por defeito, não há leitores. Um ID presente nas duas listas recebe o perfil Dono. No Discord Developer Portal, adicionar o redirect exato `http://192.168.88.99:3080/auth/discord/callback`, ajustando ao `APP_URL`. O navegador e o container precisam de Internet para o OAuth; o site pode continuar apenas na rede local. diff --git a/auth.test.mjs b/auth.test.mjs index f09146f..fef6e2c 100644 --- a/auth.test.mjs +++ b/auth.test.mjs @@ -5,11 +5,11 @@ 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 () => { +test('OAuth rejects strangers, invalid state and replay; both owners can write and readers cannot', 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) => { + const server = createApp({ env: { DATA_DIR: directory, APP_URL: 'http://localhost:3080', DISCORD_CLIENT_ID: 'client', DISCORD_CLIENT_SECRET: 'secret', OWNER_DISCORD_ID: '376768118535749633, 723613831196311603', ALLOWED_DISCORD_IDS: '222222222222222222,723613831196311603' }, discordFetch: async (url) => { requests++; return { ok: true, json: async () => url.endsWith('/token') ? { access_token: 'temporary' } : { id: profileId, username: 'bxio' } }; } }); @@ -25,7 +25,7 @@ test('OAuth rejects strangers, invalid state and replay; owner session requires 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 = '723613831196311603'; + profileId = '222222222222222222'; const reader = await begin(); const readerLogin = await request(`/auth/discord/callback?state=${reader.state}&code=code`, { headers: { Cookie: reader.cookie } }); assert.equal(readerLogin.headers.get('location'), '/'); @@ -33,7 +33,15 @@ test('OAuth rejects strangers, invalid state and replay; owner session requires const readerProfile = await (await request('/api/me', { headers: { Cookie: readerCookie } })).json(); assert.equal(readerProfile.user.role, 'Leitor'); assert.equal((await request('/api/dashboard', { headers: { Cookie: readerCookie } })).status, 200); - profileId = '111111111111111111'; + assert.equal((await request('/api/bots', { method: 'POST', headers: { Cookie: readerCookie, Origin: 'http://localhost:3080', 'Content-Type': 'application/json' }, body: JSON.stringify({name:'Reader bot'}) })).status, 403); + profileId = '723613831196311603'; + const secondOwner = await begin(); + const secondLogin = await request(`/auth/discord/callback?state=${secondOwner.state}&code=code`, { headers: { Cookie: secondOwner.cookie } }); + assert.equal(secondLogin.headers.get('location'), '/'); + const secondCookie = secondLogin.headers.getSetCookie().find(v => v.startsWith('session=')).split(';')[0]; + assert.equal((await (await request('/api/me', { headers: { Cookie: secondCookie } })).json()).user.role, 'Dono'); + assert.equal((await request('/api/bots', { method: 'POST', headers: { Cookie: secondCookie, Origin: 'http://localhost:3080', 'Content-Type': 'application/json' }, body: JSON.stringify({name:'Second owner bot'}) })).status, 201); + profileId = '376768118535749633'; const owner = await begin(); const callback = `/auth/discord/callback?state=${owner.state}&code=code`; const login = await request(callback, { headers: { Cookie: owner.cookie } }); @@ -41,6 +49,7 @@ test('OAuth rejects strangers, invalid state and replay; owner session requires const sessionCookie = login.headers.getSetCookie().find(v => v.startsWith('session=')).split(';')[0]; assert.equal((await (await request('/api/me', { headers: { Cookie: sessionCookie } })).json()).user.role, 'Dono'); assert.equal((await request('/api/dashboard', { headers: { Cookie: sessionCookie } })).status, 200); + assert.equal((await request('/api/bots', { method: 'POST', headers: { Cookie: sessionCookie, Origin: 'http://localhost:3080', 'Content-Type': 'application/json' }, body: JSON.stringify({name:'First owner bot'}) })).status, 201); 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); diff --git a/compose.yaml b/compose.yaml index 6aa0653..4dd8749 100644 --- a/compose.yaml +++ b/compose.yaml @@ -7,7 +7,8 @@ services: 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} + OWNER_DISCORD_ID: ${OWNER_DISCORD_ID:-376768118535749633,723613831196311603} + ALLOWED_DISCORD_IDS: ${ALLOWED_DISCORD_IDS:-} volumes: - botflow-data:/app/data restart: unless-stopped diff --git a/deploy/bot-stack.yaml b/deploy/bot-stack.yaml index b94c5d1..ffdd5d4 100644 --- a/deploy/bot-stack.yaml +++ b/deploy/bot-stack.yaml @@ -12,7 +12,7 @@ services: APP_URL: http://192.168.88.99:3080 DISCORD_CLIENT_ID: "477999574804594708" DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET:?Define DISCORD_CLIENT_SECRET} - OWNER_DISCORD_ID: "376768118535749633" + OWNER_DISCORD_ID: "376768118535749633,723613831196311603" volumes: - ts6_botflow_data:/app/data restart: unless-stopped diff --git a/server.mjs b/server.mjs index 3b64024..9d47873 100644 --- a/server.mjs +++ b/server.mjs @@ -15,10 +15,12 @@ export function createApp({ env = process.env, discordFetch = fetch } = {}) { const studio = createStudio(db, dataDir); 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 ids = value => (value || '').split(',').map(id => id.trim()).filter(id => /^\d{17,20}$/.test(id)); + const owners = ids(env.OWNER_DISCORD_ID); + const configured = Boolean(env.DISCORD_CLIENT_ID && env.DISCORD_CLIENT_SECRET && owners.length); const roleFor = id => { - if (id === env.OWNER_DISCORD_ID) return 'Dono'; - const allowed = (env.ALLOWED_DISCORD_IDS ?? '723613831196311603').split(',').map(value => value.trim()).filter(value => /^\d{17,20}$/.test(value)); + if (owners.includes(id)) return 'Dono'; + const allowed = ids(env.ALLOWED_DISCORD_IDS ?? ''); return allowed.includes(id) ? 'Leitor' : null; }; const token = () => randomBytes(32).toString('hex');