Allow additional Discord reader account
All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 17s
All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 17s
This commit is contained in:
parent
febc807ad9
commit
e9f1a90592
@ -2,3 +2,4 @@ APP_URL=http://192.168.88.99:3080
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
OWNER_DISCORD_ID=376768118535749633
|
||||
ALLOWED_DISCORD_IDS=723613831196311603
|
||||
|
||||
@ -21,7 +21,7 @@ No Discord Developer Portal, OAuth2 → Redirects, adicionar exatamente `http://
|
||||
|
||||
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.
|
||||
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. A conta OWNER_DISCORD_ID entra como Dono. O ID 723613831196311603 também está autorizado como Leitor por defeito. ALLOWED_DISCORD_IDS permite substituir a lista de leitores (IDs separados por vírgulas; valor vazio remove todos). Outras contas são recusadas. A gestão de utilizadores pelo painel ainda não está implementada.
|
||||
|
||||
Local: Node 22.15+, definir as mesmas variáveis e executar `npm start`. Testes: `npm test`.
|
||||
|
||||
|
||||
@ -25,12 +25,21 @@ 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';
|
||||
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'), '/');
|
||||
const readerCookie = readerLogin.headers.getSetCookie().find(v => v.startsWith('session=')).split(';')[0];
|
||||
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';
|
||||
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 (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(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);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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.' };
|
||||
const errors = { denied: 'Esta conta não está autorizada a entrar no painel.', 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();
|
||||
@ -18,11 +18,13 @@ async function boot() {
|
||||
function page(name) {
|
||||
document.querySelectorAll('.nav').forEach(b => b.classList.toggle('active', b.dataset.page === name));
|
||||
if (name === 'access') {
|
||||
content.innerHTML = '<h1 class="title">Acesso ao painel</h1><p>A tua conta está autorizada como dono.</p><div class="card details"><div><span>Perfil</span><strong>Dono</strong></div><div><span>ID Discord</span><strong id="discord-id"></strong></div><div><span>Novos utilizadores</span><strong>Sem acesso</strong></div></div>';
|
||||
document.querySelector('#discord-id').textContent = user.id; return;
|
||||
content.innerHTML = '<h1 class="title">Acesso ao painel</h1><p>A tua conta está autorizada a entrar no painel.</p><div class="card details"><div><span>Perfil</span><strong id="profile-role"></strong></div><div><span>ID Discord</span><strong id="discord-id"></strong></div><div><span>Novos utilizadores</span><strong>Sem acesso</strong></div></div>';
|
||||
document.querySelector('#discord-id').textContent = user.id;
|
||||
document.querySelector('#profile-role').textContent = user.role; return;
|
||||
}
|
||||
if (name === 'bots') { content.innerHTML = '<h1 class="title">Bots e flows</h1><p>A primeira experiência valida o painel e o acesso.</p><div class="card empty"><span class="empty-symbol">◇</span>Ainda não existem bots.<p>O editor de flows e a ligação ao TeamSpeak serão a próxima etapa.</p></div>'; return; }
|
||||
content.innerHTML = `<h1 class="title">Dashboard</h1><p>Bem-vindo ao teu centro de automações.</p><div class="stats">${[['Bots',dashboard.bots.length],['Flows',dashboard.flows.length],['Execuções',dashboard.executions.length],['Servidores',dashboard.connections.length]].map(([label,value])=>`<div class="card"><span>${label}</span><strong class="stat-value">${value}</strong></div>`).join('')}</div><div class="split"><div class="card"><h2>Atividade recente</h2><div class="empty"><span class="empty-symbol">◷</span>Ainda sem execuções.<p>A atividade dos bots aparecerá aqui.</p></div></div><div class="card"><h2>O teu espaço</h2><div class="details"><div><span>Acesso</span><span class="badge">Dono</span></div><div><span>Autenticação</span><strong>Discord</strong></div><div><span>Ligação TeamSpeak</span><strong>Por configurar</strong></div></div><p>Começamos por testar o login. Depois ligamos os bots e construímos os flows.</p></div></div>`;
|
||||
content.innerHTML = `<h1 class="title">Dashboard</h1><p>Bem-vindo ao teu centro de automações.</p><div class="stats">${[['Bots',dashboard.bots.length],['Flows',dashboard.flows.length],['Execuções',dashboard.executions.length],['Servidores',dashboard.connections.length]].map(([label,value])=>`<div class="card"><span>${label}</span><strong class="stat-value">${value}</strong></div>`).join('')}</div><div class="split"><div class="card"><h2>Atividade recente</h2><div class="empty"><span class="empty-symbol">◷</span>Ainda sem execuções.<p>A atividade dos bots aparecerá aqui.</p></div></div><div class="card"><h2>O teu espaço</h2><div class="details"><div><span>Acesso</span><span class="badge" id="dashboard-role"></span></div><div><span>Autenticação</span><strong>Discord</strong></div><div><span>Ligação TeamSpeak</span><strong>Por configurar</strong></div></div><p>Começamos por testar o login. Depois ligamos os bots e construímos os flows.</p></div></div>`;
|
||||
document.querySelector('#dashboard-role').textContent = user.role;
|
||||
}
|
||||
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('/'); };
|
||||
|
||||
14
server.mjs
14
server.mjs
@ -14,6 +14,11 @@ export function createApp({ env = process.env, discordFetch = fetch } = {}) {
|
||||
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 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));
|
||||
return allowed.includes(id) ? 'Leitor' : null;
|
||||
};
|
||||
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' : ''}`;
|
||||
@ -48,14 +53,17 @@ export function createApp({ env = process.env, discordFetch = fetch } = {}) {
|
||||
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 role = roleFor(user.id);
|
||||
if (!role) 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);
|
||||
db.prepare('INSERT INTO sessions VALUES (?, ?, ?)').run(hash(session), JSON.stringify({ id: user.id, name: user.global_name || user.username, role }), 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;
|
||||
const storedUser = session ? JSON.parse(session.user) : null;
|
||||
const currentRole = storedUser && roleFor(storedUser.id);
|
||||
const user = currentRole ? { ...storedUser, role: currentRole } : 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.' });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user