const $=id=>document.getElementById(id),page=document.body.dataset.page,params=new URLSearchParams(location.search); const roles={admin:'Admin',team:'Equipe Musiclin',general:'Geral'},statuses={pending:'Aguardando aprovação',approved:'Liberado',blocked:'Bloqueado',rejected:'Reprovado'}; let csrf='',session=null,users=[],report=null,settings={},reviewingUser=null; function showDemo(value){if(value){const banner=document.createElement('div');banner.className='demo-banner';banner.textContent='Demonstração · cadastros e acessos fictícios · e-mails não são enviados';document.body.prepend(banner);}} const node=(tag,text='',className='')=>{const el=document.createElement(tag);el.textContent=text;if(className)el.className=className;return el;}; const date=value=>value?new Date(value*1000).toLocaleString('pt-BR',{timeZone:'America/Sao_Paulo'}):'—'; const hours=seconds=>(seconds/3600).toLocaleString('pt-BR',{minimumFractionDigits:2,maximumFractionDigits:2}); function message(id,text,success=false){ const el=$(id);el.textContent=text;el.hidden=!text;el.classList.toggle('success',success); if(text&&!success&&id==='public-message')el.scrollIntoView?.({block:'nearest',behavior:'auto'}); } async function api(path,{body,method=body?'POST':'GET'}={}){ const response=await fetch(path,{method,credentials:'same-origin',cache:'no-store',headers:body?{'Content-Type':'application/json','X-CSRF-Token':csrf}:{},...(body?{body:JSON.stringify(body)}:{})}); const data=await response.json(); if(!response.ok){const error=new Error(data.error||'Não consegui concluir. Tente novamente.');error.status=response.status;error.code=data.code;throw error;} return data; } async function busy(button,action,messageId='private-message'){ button.disabled=true; try{message(messageId,'');await action();} catch(error){message(messageId,error.message);} finally{button.disabled=false;} } function formValues(form){return Object.fromEntries(new FormData(form));} function passwordConfirmation(form){ const values=formValues(form),confirmation=form.elements.confirmation; confirmation.removeAttribute('aria-invalid');confirmation.removeAttribute('aria-describedby'); if(values.password!==values.confirmation){ confirmation.closest('label').after($('public-message')); confirmation.setAttribute('aria-invalid','true');confirmation.setAttribute('aria-describedby','public-message'); confirmation.focus({preventScroll:true}); throw new Error('As senhas precisam ser iguais.'); } delete values.confirmation;return values; } function loginMode(){ const admin=$('admin-preview').checked; $('login-context').textContent=admin?'ACESSO ADMINISTRATIVO':'SEU ESPAÇO DE TRABALHO'; $('login-title').textContent=admin?'Entrar no painel admin':'Entrar no Miáudio'; $('login-description').textContent=admin?'Este painel é exclusivo do administrador do Miáudio.':'Use o e-mail e a senha do seu cadastro aprovado.'; $('login-submit').textContent=admin?'Entrar no painel admin':'Entrar no Miáudio'; $('welcome-title').textContent=admin?'Cuide dos acessos. Acompanhe o Miáudio.':'Porque editar áudios deve ser algo fácil e divertido!'; $('welcome-copy').textContent=admin?'Revise os pedidos, acompanhe a utilização e organize o acesso ao aplicativo.':'Corte, organize e ajuste seus áudios em um só lugar.'; $('welcome-note').textContent=admin?'Somente seu login de Admin pode acessar este painel.':'Seu acesso é pessoal e liberado pelo administrador.'; const features=admin?['Revise as solicitações de acesso','Consulte o uso semanal','Configure os avisos de cadastro']:['Organize suas pistas','Ajuste o tom e a velocidade','Prepare sua mixagem']; $('welcome-features').replaceChildren(...features.map(text=>node('li',text))); } async function publicView(){ const preview=params.get('admin')==='1';$('admin-preview').checked=preview; const meta=await api('/api/access/public');csrf=meta.csrf; showDemo(meta.demo); $('loading').hidden=true; const activeView=$(({setup:'setup-view',register:'register-view',forgot:'forgot-view',reset:'reset-view'})[page]||'login-view'); activeView.hidden=false; const activeForm=activeView.querySelector('form'); if(activeForm)activeForm.append($('public-message')); $('forgot-password-link').hidden=page!=='login'||!meta.passwordRecovery; if(meta.hosted)$('setup-prompt').textContent='O administrador está preparando o Miáudio. Os acessos serão abertos após a configuração inicial.'; if(page==='login'){ loginMode();$('admin-preview').addEventListener('change',loginMode); $('setup-prompt').hidden=!meta.setupRequired; $('login-submit').disabled=meta.setupRequired; $('login-form').addEventListener('submit',event=>{event.preventDefault();busy($('login-submit'),async()=>{ const result=await api('/api/access/login',{body:{...formValues(event.target),admin:$('admin-preview').checked,requestId:params.get('solicitacao')}});location.assign(result.next); },'public-message');}); }else if(page==='register'){ $('welcome-copy').textContent='Solicite seu acesso para começar a preparar suas músicas. Seu cadastro será analisado pelo administrador.';$('welcome-note').textContent='Enviar o cadastro não libera o acesso automaticamente.'; if(meta.setupRequired){message('public-message','O Admin ainda está preparando os cadastros. Tente novamente mais tarde.');$('register-form').querySelector('button').disabled=true;} $('register-form').addEventListener('submit',event=>{event.preventDefault();busy(event.submitter,async()=>{ await api('/api/access/register',{body:passwordConfirmation(event.target)});$('register-view').hidden=true;$('request-success').hidden=false;event.target.reset(); },'public-message');}); }else if(page==='forgot'){ $('welcome-copy').textContent='Recupere seu acesso com um link enviado para o e-mail do seu cadastro.'; $('forgot-form').addEventListener('submit',event=>{event.preventDefault();busy(event.submitter,async()=>{ await api('/api/access/forgot-password',{body:formValues(event.target)});event.target.reset();$('forgot-view').hidden=true;$('forgot-success').hidden=false; },'public-message');}); }else if(page==='reset'){ const token=location.hash.slice(1);history.replaceState(null,'',location.pathname); $('welcome-copy').textContent='Crie uma nova senha para continuar preparando seus áudios.'; if(!/^[A-Za-z0-9_-]{40,128}$/.test(token)){message('public-message','O link está incompleto. Solicite um novo link de recuperação.');$('reset-form').querySelector('button').disabled=true;} $('reset-form').addEventListener('submit',event=>{event.preventDefault();busy(event.submitter,async()=>{ await api('/api/access/reset-password',{body:{...passwordConfirmation(event.target),token}});event.target.reset();$('reset-view').hidden=true;$('reset-success').hidden=false; },'public-message');}); }else{ $('welcome-title').textContent='Organize os acessos ao Miáudio.';$('welcome-copy').textContent='Configure seu login de Admin para aprovar os cadastros e acompanhar o uso do aplicativo.';$('welcome-features').hidden=true;$('welcome-note').textContent='Este será o único cadastro com acesso administrativo.'; const key=location.hash.slice(1);history.replaceState(null,'',location.pathname); if(!key){message('public-message',meta.hosted?'Use o link privado de configuração do Admin, fornecido na instalação.':'Abra “Configurar Admin Miáudio” no computador do aplicativo. O atalho contém a autorização para criar seu Admin.');$('setup-form').querySelector('button').disabled=true;} $('setup-form').addEventListener('submit',event=>{event.preventDefault();busy(event.submitter,async()=>{ const result=await api('/api/access/setup',{body:{...passwordConfirmation(event.target),key}});location.assign(result.next); },'public-message');}); } } function badge(status){return node('span',statuses[status]||status,'badge '+status);} function showSection(section){ document.querySelectorAll('.dashboard-section').forEach(el=>el.hidden=el.id!=='section-'+section); document.querySelectorAll('[data-section]').forEach(button=>button.classList.toggle('selected',button.dataset.section===section)); const labels={overview:['Visão geral','Acompanhe os pedidos e o uso do Miáudio.'],users:['Cadastros','Somente o Admin libera o acesso ao Miáudio.'],report:['Acessos e uso','Consulte a semana e exporte a tabela.'],settings:['Configurações','Notificações, redes e identificação do IP.']}; $('page-title').textContent=labels[section][0];$('page-description').textContent=labels[section][1];message('private-message',''); window.scrollTo({top:0,behavior:'auto'}); } function openUser(user){ reviewingUser=user;const form=$('user-form');form.elements.id.value=user.id;form.elements.role.value=user.role;form.elements.status.value=user.status; $('user-dialog-title').textContent=user.name;$('user-dialog-email').textContent=user.email;message('user-dialog-message',''); userDecision(); $('user-dialog').showModal(); } function userDecision(){ const status=$('user-form').elements.status.value; const actions={approved:'Liberar acesso',pending:'Manter em análise',blocked:'Bloquear acesso',rejected:'Reprovar pedido'}; const descriptions={approved:'Ao confirmar, este usuário poderá entrar no Miáudio.',pending:'O pedido continuará aguardando sua aprovação.',blocked:'Ao confirmar, este usuário ficará sem acesso ao Miáudio.',rejected:'Ao confirmar, a solicitação será reprovada e o acesso continuará fechado.'}; $('save-user-access').textContent=status===reviewingUser?.status?'Salvar revisão':actions[status]; $('user-decision').textContent=descriptions[status]; } function renderUsers(){ const search=$('user-search').value.trim().toLocaleLowerCase('pt-BR'),filter=$('user-status').value; const list=users.filter(user=>(!filter||user.status===filter)&&(!search||[user.name,user.email,roles[user.role]].some(x=>x.toLocaleLowerCase('pt-BR').includes(search)))); const host=$('users-table');host.replaceChildren(); for(const user of list){const row=document.createElement('tr'),person=node('div','','person');person.append(node('strong',user.name),node('span',user.email));const nameCell=document.createElement('td');nameCell.append(person);row.append(nameCell,node('td',roles[user.role]));const status=node('td');status.append(badge(user.status));row.append(status);const action=document.createElement('td');if(user.role!=='admin'){const button=node('button',user.status==='pending'?'Revisar pedido':'Gerenciar','secondary');button.addEventListener('click',()=>openUser(user));action.append(button);}else action.append(node('span','Seu cadastro','badge'));row.append(action);host.append(row);} $('users-empty').hidden=!!list.length; const pending=users.filter(user=>user.status==='pending');$('pending-count').textContent=pending.length?String(pending.length):'';$('stat-pending').textContent=String(pending.length);$('stat-approved').textContent=String(users.filter(user=>user.status==='approved').length); const preview=$('pending-preview');preview.replaceChildren(); if(!pending.length)preview.append(node('p','Nenhuma solicitação aguardando aprovação.','empty')); for(const user of pending.slice(0,5)){const row=node('div','','request-row'),person=node('div','','person');person.append(node('strong',user.name),node('span',user.email));const button=node('button','Revisar pedido','secondary');button.addEventListener('click',()=>openUser(user));row.append(person,button);preview.append(row);} } function reportRows(){const search=$('report-search').value.trim().toLocaleLowerCase('pt-BR');return (report?.rows||[]).filter(row=>!search||[row.name,row.email,row.ip,row.city,row.network].some(v=>v.toLocaleLowerCase('pt-BR').includes(search)));} function networkCell(row,kind){if(row.network==='unclassified')return 'Não classificada';return row[kind]?'Sim':'Não';} function renderReport(){ const host=$('report-table');host.replaceChildren();const rows=reportRows(); for(const row of rows){const tr=document.createElement('tr');for(const value of [row.name,row.email,row.ip,row.city,networkCell(row,'musiclin'),networkCell(row,'house'),networkCell(row,'other'),hours(row.seconds),String(row.accesses),date(row.lastAccess)])tr.append(node('td',value));host.append(tr);} $('report-empty').hidden=!!rows.length;$('stat-hours').textContent=hours(report?.totalSeconds||0)+' h'; } async function loadReport(){report=await api('/api/access/admin/report'+($('report-week').value?'?week='+encodeURIComponent($('report-week').value):''));$('report-week').value=report.week;renderReport();} function downloadReport(){ const safe=value=>{value=String(value??'');if(/^[=+@\-\t\r]/.test(value))value="'"+value;return '"'+value.replaceAll('"','""')+'"';}; const headers=['Nome','E-mail','IP','Cidade do IP','Musiclin','House','Outras redes','Uso semanal (horas)','Acessos','Último acesso']; const lines=[headers,...reportRows().map(row=>[row.name,row.email,row.ip,row.city,networkCell(row,'musiclin'),networkCell(row,'house'),networkCell(row,'other'),hours(row.seconds),row.accesses,date(row.lastAccess)])]; const url=URL.createObjectURL(new Blob(['\ufeff'+lines.map(row=>row.map(safe).join(';')).join('\r\n')],{type:'text/csv;charset=utf-8'})); const anchor=document.createElement('a');anchor.href=url;anchor.download='Miáudio - acessos - '+report.week+'.csv';anchor.click();setTimeout(()=>URL.revokeObjectURL(url),5000); } function fillSettings(){const form=$('settings-form'); for(const input of form.elements){if(!input.name||input.name==='smtp_password')continue;const value=settings[input.name];if(value!==undefined)input.value=Array.isArray(value)?value.join('\n'):value;} $('smtp-secret-state').textContent=settings.smtpPasswordConfigured?'Senha já configurada. Deixe vazio para mantê-la.':'Informe a senha do servidor de e-mail.'; $('geoip-state').textContent=settings.geoipAvailable?'Base de cidades disponível.':'Sem uma base válida, a cidade aparece como não identificada.'; const configured=settings.notification_email&&settings.smtp_host&&settings.smtp_from; $('overview-notification').hidden=!!configured;$('overview-notification').textContent='Os pedidos já aparecem neste painel. Configure o servidor de e-mail para receber as notificações; os avisos pendentes ficam na fila.'; } async function loadNotifications(){const {notifications}=await api('/api/access/admin/notifications');const host=$('notification-list');host.replaceChildren(); if(!notifications.length)host.append(node('p','Nenhuma notificação na fila.','empty')); for(const notification of notifications){const row=node('div','','notification-row'),person=node('div',notification.name+' · '+notification.email+(notification.kind==='reset'?' · Recuperação de senha':''));person.append(node('small',notification.last_error||date(notification.created_at)));const labels={sent:'Enviado ao servidor',queued:'Na fila',cancelled:'Encerrado'};row.append(person,node('span',labels[notification.status]||notification.status,'badge '+(notification.status==='sent'?'approved':notification.status==='queued'?'pending':'')));host.append(row);} } async function privateView(){ const data=await api('/api/access/session');session=data.user;csrf=data.csrf; showDemo(data.demo); if(session.role!=='admin'){location.replace('/');return;} $('public-view').hidden=true;$('private-view').hidden=false;$('viewer-name').textContent=session.name;$('viewer-role').textContent=roles[session.role]; $('logout').addEventListener('click',()=>busy($('logout'),async()=>{await api('/api/access/logout',{body:{}});location.assign('/login');})); const loaded=await Promise.all([api('/api/access/admin/users'),api('/api/access/admin/settings')]);users=loaded[0].users;settings=loaded[1]; renderUsers();fillSettings();await Promise.all([loadReport(),loadNotifications()]); document.querySelectorAll('[data-section]').forEach(button=>button.addEventListener('click',()=>{showSection(button.dataset.section);if(button.dataset.section==='report')loadReport().catch(error=>message('private-message',error.message));})); document.querySelectorAll('[data-go]').forEach(button=>button.addEventListener('click',()=>showSection(button.dataset.go))); $('user-search').addEventListener('input',renderUsers);$('user-status').addEventListener('change',renderUsers);$('report-search').addEventListener('input',renderReport); $('refresh-report').addEventListener('click',()=>busy($('refresh-report'),loadReport));$('report-week').addEventListener('change',()=>loadReport().catch(error=>message('private-message',error.message)));$('download-report').addEventListener('click',downloadReport); $('close-user-dialog').addEventListener('click',()=>$('user-dialog').close()); $('user-form').elements.status.addEventListener('change',userDecision); $('user-form').addEventListener('submit',event=>{event.preventDefault();busy(event.submitter,async()=>{ const values=formValues(event.target);await api('/api/access/admin/users/'+values.id,{body:values});$('user-dialog').close();users=(await api('/api/access/admin/users')).users;renderUsers();message('private-message','Cadastro atualizado. O acesso ao Miáudio foi aplicado.',true); },'user-dialog-message');}); $('settings-form').addEventListener('submit',event=>{event.preventDefault();busy(event.submitter,async()=>{ const values=formValues(event.target);for(const key of ['musiclin_ranges','house_ranges','trusted_proxies'])values[key]=values[key].split(/[\n,;]+/).map(x=>x.trim()).filter(Boolean); settings=await api('/api/access/admin/settings',{body:values});event.target.elements.smtp_password.value='';fillSettings();await loadNotifications();message('private-message','Configurações salvas.',true); });}); $('retry-mail').addEventListener('click',()=>busy($('retry-mail'),async()=>{const result=await api('/api/access/admin/retry-mail',{body:{}});await loadNotifications();message('private-message',result.message,true);})); const target=params.get('solicitacao'),requested=users.find(user=>user.id===target);if(requested&&requested.role!=='admin'){showSection('users');openUser(requested);} } try{if(page==='admin')await privateView();else await publicView();} catch(error){if(error.status===401&&page==='admin'){location.replace('/login?admin=1');}else{const id=page==='admin'?'private-message':'public-message';$('loading').hidden=true;message(id,error.message);}}