Give both configured Discord accounts owner access
All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 18s

This commit is contained in:
Bxio 2026-10-10 19:31:36 +01:00
parent e2c0e703ea
commit 8c430b9486
6 changed files with 25 additions and 13 deletions

View File

@ -1,5 +1,5 @@
APP_URL=http://192.168.88.99:3080 APP_URL=http://192.168.88.99:3080
DISCORD_CLIENT_ID= DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET= DISCORD_CLIENT_SECRET=
OWNER_DISCORD_ID=376768118535749633 OWNER_DISCORD_ID=376768118535749633,723613831196311603
ALLOWED_DISCORD_IDS=723613831196311603 ALLOWED_DISCORD_IDS=

View File

@ -27,8 +27,8 @@ Variáveis de ambiente:
- `APP_URL`: endereço do navegador, por exemplo `http://192.168.88.99:3080`. - `APP_URL`: endereço do navegador, por exemplo `http://192.168.88.99:3080`.
- `DISCORD_CLIENT_ID`: ID da aplicação Discord. - `DISCORD_CLIENT_ID`: ID da aplicação Discord.
- `DISCORD_CLIENT_SECRET`: segredo OAuth da aplicação. - `DISCORD_CLIENT_SECRET`: segredo OAuth da aplicação.
- `OWNER_DISCORD_ID`: ID numérico da conta do dono. - `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, `723613831196311603`; valor vazio remove todos os leitores. - `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. 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.

View File

@ -5,11 +5,11 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { createApp } from './server.mjs'; 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-')); const directory = mkdtempSync(join(tmpdir(), 'botflow-test-'));
let profileId = '999999999999999999'; let profileId = '999999999999999999';
let requests = 0; 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++; requests++;
return { ok: true, json: async () => url.endsWith('/token') ? { access_token: 'temporary' } : { id: profileId, username: 'bxio' } }; 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 outsider = await begin();
const denied = await request(`/auth/discord/callback?state=${outsider.state}&code=code`, { headers: { Cookie: outsider.cookie } }); const denied = await request(`/auth/discord/callback?state=${outsider.state}&code=code`, { headers: { Cookie: outsider.cookie } });
assert.equal(denied.headers.get('location'), '/?error=denied'); assert.equal(denied.headers.get('location'), '/?error=denied');
profileId = '723613831196311603'; profileId = '222222222222222222';
const reader = await begin(); const reader = await begin();
const readerLogin = await request(`/auth/discord/callback?state=${reader.state}&code=code`, { headers: { Cookie: reader.cookie } }); const readerLogin = await request(`/auth/discord/callback?state=${reader.state}&code=code`, { headers: { Cookie: reader.cookie } });
assert.equal(readerLogin.headers.get('location'), '/'); 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(); const readerProfile = await (await request('/api/me', { headers: { Cookie: readerCookie } })).json();
assert.equal(readerProfile.user.role, 'Leitor'); assert.equal(readerProfile.user.role, 'Leitor');
assert.equal((await request('/api/dashboard', { headers: { Cookie: readerCookie } })).status, 200); 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 owner = await begin();
const callback = `/auth/discord/callback?state=${owner.state}&code=code`; const callback = `/auth/discord/callback?state=${owner.state}&code=code`;
const login = await request(callback, { headers: { Cookie: owner.cookie } }); 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]; 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 (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/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(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://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('/auth/logout', { method: 'POST', headers: { Cookie: sessionCookie, Origin: 'http://localhost:3080' } })).status, 200);

View File

@ -7,7 +7,8 @@ services:
APP_URL: ${APP_URL:-http://192.168.88.99:3080} APP_URL: ${APP_URL:-http://192.168.88.99:3080}
DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID:?Define DISCORD_CLIENT_ID} DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID:?Define DISCORD_CLIENT_ID}
DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET:?Define DISCORD_CLIENT_SECRET} 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: volumes:
- botflow-data:/app/data - botflow-data:/app/data
restart: unless-stopped restart: unless-stopped

View File

@ -12,7 +12,7 @@ services:
APP_URL: http://192.168.88.99:3080 APP_URL: http://192.168.88.99:3080
DISCORD_CLIENT_ID: "477999574804594708" DISCORD_CLIENT_ID: "477999574804594708"
DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET:?Define DISCORD_CLIENT_SECRET} DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET:?Define DISCORD_CLIENT_SECRET}
OWNER_DISCORD_ID: "376768118535749633" OWNER_DISCORD_ID: "376768118535749633,723613831196311603"
volumes: volumes:
- ts6_botflow_data:/app/data - ts6_botflow_data:/app/data
restart: unless-stopped restart: unless-stopped

View File

@ -15,10 +15,12 @@ export function createApp({ env = process.env, discordFetch = fetch } = {}) {
const studio = createStudio(db, dataDir); const studio = createStudio(db, dataDir);
const pending = new Map(); const pending = new Map();
const root = fileURLToPath(new URL('./public/', import.meta.url)); 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 => { const roleFor = id => {
if (id === env.OWNER_DISCORD_ID) return 'Dono'; if (owners.includes(id)) return 'Dono';
const allowed = (env.ALLOWED_DISCORD_IDS ?? '723613831196311603').split(',').map(value => value.trim()).filter(value => /^\d{17,20}$/.test(value)); const allowed = ids(env.ALLOWED_DISCORD_IDS ?? '');
return allowed.includes(id) ? 'Leitor' : null; return allowed.includes(id) ? 'Leitor' : null;
}; };
const token = () => randomBytes(32).toString('hex'); const token = () => randomBytes(32).toString('hex');