292 lines
12 KiB
HTML
292 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
<title>Ativar conta — Ligbox Ops</title>
|
|
<link rel="stylesheet" href="/assets/styles.css"/>
|
|
</head>
|
|
<body class="login-page activate-page">
|
|
<div class="activate-card">
|
|
<header class="activate-header">
|
|
<div>
|
|
<h1>Ativar conta</h1>
|
|
<p class="activate-sub">Complete <strong>2 de 3</strong> fatores — escolha os que preferir</p>
|
|
</div>
|
|
<p id="activate-info" class="activate-account">Carregando…</p>
|
|
</header>
|
|
|
|
<div id="factor-progress" class="factor-progress" hidden>
|
|
<div class="factor-progress-bar"><span id="factor-progress-fill"></span></div>
|
|
<span id="factor-progress-text">0/2 fatores</span>
|
|
</div>
|
|
|
|
<form id="activate-form" class="activate-form" hidden>
|
|
<div class="factor-grid">
|
|
<section class="factor-tile" id="factor-email" data-factor="email">
|
|
<div class="factor-tile-head">
|
|
<span class="factor-num">1</span>
|
|
<div>
|
|
<strong>E-mail</strong>
|
|
<p class="factor-desc">Código de 6 dígitos</p>
|
|
</div>
|
|
<span class="factor-check" id="chk-email" hidden aria-label="validado">✓</span>
|
|
</div>
|
|
<button type="button" class="btn btn-ghost btn-sm btn-block" id="btn-email-otp">Enviar código</button>
|
|
<label class="factor-label">
|
|
Código
|
|
<input type="text" name="email_otp" inputmode="numeric" maxlength="6" placeholder="000000" autocomplete="one-time-code"/>
|
|
</label>
|
|
</section>
|
|
|
|
<section class="factor-tile" id="factor-phone" data-factor="phone">
|
|
<div class="factor-tile-head">
|
|
<span class="factor-num">2</span>
|
|
<div>
|
|
<strong>Telefone</strong>
|
|
<p class="factor-desc">Por e-mail (SMS em breve)</p>
|
|
</div>
|
|
<span class="factor-check" id="chk-phone" hidden aria-label="validado">✓</span>
|
|
</div>
|
|
<label class="factor-label">
|
|
Número
|
|
<input type="tel" id="act-phone" name="phone" placeholder="+5511999999999"/>
|
|
</label>
|
|
<button type="button" class="btn btn-ghost btn-sm btn-block" id="btn-phone-otp">Enviar código</button>
|
|
<label class="factor-label">
|
|
Código
|
|
<input type="text" name="phone_otp" inputmode="numeric" maxlength="6" placeholder="000000"/>
|
|
</label>
|
|
</section>
|
|
|
|
<section class="factor-tile factor-tile-totp" id="factor-totp" data-factor="totp">
|
|
<div class="factor-tile-head">
|
|
<span class="factor-num">3</span>
|
|
<div>
|
|
<strong>App autenticador</strong>
|
|
<p class="factor-desc">Google Authenticator / Authy</p>
|
|
</div>
|
|
<span class="factor-check" id="chk-totp" hidden aria-label="validado">✓</span>
|
|
</div>
|
|
<div class="qr-panel">
|
|
<p class="qr-label">Escaneie o QR code</p>
|
|
<div id="qr-wrap" class="qr-wrap">
|
|
<p id="qr-loading" class="qr-placeholder">Gerando QR…</p>
|
|
</div>
|
|
<p id="qr-error" class="qr-error" hidden>Não foi possível gerar o QR. Recarregue a página.</p>
|
|
</div>
|
|
<div id="ntfy-box" class="ntfy-box" hidden>
|
|
<p class="ntfy-title">Push opcional (ntfy)</p>
|
|
<a id="ntfy-link" class="ntfy-link" href="#" target="_blank" rel="noopener">Inscrever no tópico</a>
|
|
</div>
|
|
<label class="factor-label">
|
|
Código do app
|
|
<input type="text" name="totp_code" inputmode="numeric" maxlength="6" placeholder="000000"/>
|
|
</label>
|
|
</section>
|
|
</div>
|
|
|
|
<p id="activate-error" class="login-error activate-feedback" hidden></p>
|
|
<p id="activate-success" class="login-notice activate-feedback" hidden></p>
|
|
<button type="submit" class="btn btn-primary btn-block activate-submit">Ativar conta</button>
|
|
</form>
|
|
|
|
<p class="login-hint activate-footer">
|
|
<a href="/login.html">← Voltar ao login</a>
|
|
</p>
|
|
</div>
|
|
|
|
<script src="/assets/qrcode.min.js?v=20260610k"></script>
|
|
<script src="/assets/auth.js?v=20260610k"></script>
|
|
<script>
|
|
const params = new URLSearchParams(window.location.search);
|
|
const token = params.get('token');
|
|
const info = document.getElementById('activate-info');
|
|
const progress = document.getElementById('factor-progress');
|
|
const progressFill = document.getElementById('factor-progress-fill');
|
|
const progressText = document.getElementById('factor-progress-text');
|
|
const form = document.getElementById('activate-form');
|
|
const err = document.getElementById('activate-error');
|
|
const ok = document.getElementById('activate-success');
|
|
|
|
function setTileVerified(name, on) {
|
|
const tile = document.querySelector(`[data-factor="${name}"]`);
|
|
tile?.classList.toggle('factor-tile-done', on);
|
|
const chk = document.getElementById(`chk-${name}`);
|
|
if (chk) chk.hidden = !on;
|
|
}
|
|
|
|
function updateProgress(factors) {
|
|
if (!factors) return;
|
|
const n = factors.verified_count || 0;
|
|
const req = factors.required || 2;
|
|
const pct = Math.min(100, Math.round((n / req) * 100));
|
|
progressFill.style.width = `${pct}%`;
|
|
progressText.textContent = n >= req
|
|
? `${n}/${req} fatores — pronto para ativar`
|
|
: `${n}/${req} fatores — faltam ${req - n}`;
|
|
progress.hidden = false;
|
|
setTileVerified('email', !!factors.email);
|
|
setTileVerified('phone', !!factors.phone);
|
|
setTileVerified('totp', !!factors.totp);
|
|
}
|
|
|
|
function renderQr(uri) {
|
|
const wrap = document.getElementById('qr-wrap');
|
|
const loading = document.getElementById('qr-loading');
|
|
const qrErr = document.getElementById('qr-error');
|
|
if (!uri) {
|
|
loading.textContent = 'QR indisponível';
|
|
return;
|
|
}
|
|
if (typeof QRCode === 'undefined') {
|
|
loading.hidden = true;
|
|
qrErr.textContent = 'Biblioteca QR não carregou. Recarregue a página (Ctrl+Shift+R).';
|
|
qrErr.hidden = false;
|
|
return;
|
|
}
|
|
loading.textContent = 'Gerando QR…';
|
|
QRCode.toCanvas(uri, { width: 140, margin: 1 }, (e, canvas) => {
|
|
loading.hidden = true;
|
|
if (e || !canvas) {
|
|
qrErr.textContent = 'Erro ao gerar QR. Recarregue a página.';
|
|
qrErr.hidden = false;
|
|
console.error('QRCode:', e);
|
|
return;
|
|
}
|
|
wrap.innerHTML = '';
|
|
wrap.appendChild(canvas);
|
|
});
|
|
}
|
|
|
|
function setupNtfy(topic, subscribeUrl) {
|
|
if (!topic) return;
|
|
const box = document.getElementById('ntfy-box');
|
|
const link = document.getElementById('ntfy-link');
|
|
const url = subscribeUrl || `https://ntfy.sh/${topic}`;
|
|
link.href = url;
|
|
link.textContent = topic;
|
|
box.hidden = false;
|
|
}
|
|
|
|
async function refreshStatus() {
|
|
const r = await fetchWithTimeout(`/api/v1/auth/activate?token=${encodeURIComponent(token)}`);
|
|
const d = await r.json().catch(() => ({}));
|
|
if (!r.ok) throw new Error(d.detail || 'Token inválido');
|
|
updateProgress(d.factors);
|
|
return d;
|
|
}
|
|
|
|
if (!token) {
|
|
info.textContent = 'Link inválido — falta token de ativação.';
|
|
} else {
|
|
refreshStatus()
|
|
.then((d) => {
|
|
info.innerHTML = `<strong>${d.email}</strong> · ${d.role || 'técnico'}`;
|
|
form.hidden = false;
|
|
if (d.otpauth_uri) renderQr(d.otpauth_uri);
|
|
setupNtfy(d.ntfy_topic, d.ntfy_subscribe_url);
|
|
})
|
|
.catch((e) => { info.textContent = e.message; });
|
|
}
|
|
|
|
document.getElementById('btn-email-otp')?.addEventListener('click', async () => {
|
|
err.hidden = true;
|
|
try {
|
|
const r = await fetchWithTimeout(`/api/v1/auth/activate/send-email-otp?token=${encodeURIComponent(token)}`, { method: 'POST' });
|
|
const d = await r.json().catch(() => ({}));
|
|
if (!r.ok) throw new Error(d.detail || `Falha ao enviar (${r.status})`);
|
|
ok.textContent = d.message;
|
|
ok.hidden = false;
|
|
} catch (e) {
|
|
err.textContent = e.message;
|
|
err.hidden = false;
|
|
}
|
|
});
|
|
|
|
document.getElementById('btn-phone-otp')?.addEventListener('click', async () => {
|
|
err.hidden = true;
|
|
ok.hidden = true;
|
|
const phone = document.getElementById('act-phone')?.value?.trim();
|
|
if (!phone) {
|
|
err.textContent = 'Informe o telefone primeiro';
|
|
err.hidden = false;
|
|
return;
|
|
}
|
|
try {
|
|
const r = await fetchWithTimeout('/api/v1/auth/activate/send-phone-otp', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, phone }),
|
|
});
|
|
const d = await r.json().catch(() => ({}));
|
|
if (!r.ok) throw new Error(d.detail || `Falha ao enviar (${r.status})`);
|
|
ok.textContent = d.message;
|
|
ok.hidden = false;
|
|
} catch (e) {
|
|
err.textContent = e.message;
|
|
err.hidden = false;
|
|
}
|
|
});
|
|
|
|
form?.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
err.hidden = true;
|
|
ok.hidden = true;
|
|
const btn = form.querySelector('button[type="submit"]');
|
|
const body = { token };
|
|
const emailOtp = form.email_otp.value.trim();
|
|
const phoneOtp = form.phone_otp.value.trim();
|
|
const totpCode = form.totp_code.value.trim();
|
|
if (emailOtp) body.email_otp = emailOtp;
|
|
if (phoneOtp) body.phone_otp = phoneOtp;
|
|
if (totpCode) body.totp_code = totpCode;
|
|
const filled = [emailOtp, phoneOtp, totpCode].filter(Boolean).length;
|
|
if (filled < 2) {
|
|
err.textContent = 'Preencha códigos de pelo menos 2 fatores diferentes';
|
|
err.hidden = false;
|
|
return;
|
|
}
|
|
btn.disabled = true;
|
|
try {
|
|
const r = await fetchWithTimeout('/api/v1/auth/activate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const d = await r.json().catch(() => ({}));
|
|
if (!r.ok) {
|
|
await refreshStatus().catch(() => {});
|
|
throw new Error(d.detail || 'Ativação falhou');
|
|
}
|
|
if (d.backup_codes?.length) {
|
|
form.hidden = true;
|
|
progress.hidden = true;
|
|
const panel = document.createElement('div');
|
|
panel.className = 'backup-codes-panel';
|
|
panel.innerHTML = `
|
|
<p class="login-hint" style="text-align:left">
|
|
<strong>Guarde estes códigos de backup</strong><br/>
|
|
Use no login se perder o autenticador. Cada código é de uso único. Também enviamos por e-mail.
|
|
</p>
|
|
<ul class="backup-codes-list">${d.backup_codes.map((c) => `<li><code>${c}</code></li>`).join('')}</ul>
|
|
<button type="button" class="btn btn-primary btn-block" id="activate-go-login">Ir para o login</button>`;
|
|
document.querySelector('.activate-card')?.appendChild(panel);
|
|
panel.querySelector('#activate-go-login')?.addEventListener('click', () => {
|
|
window.location.href = '/login.html';
|
|
});
|
|
return;
|
|
}
|
|
ok.textContent = (d.message || 'Conta ativa') + ' Redirecionando…';
|
|
ok.hidden = false;
|
|
setTimeout(() => { window.location.href = '/login.html'; }, 2000);
|
|
} catch (ex) {
|
|
err.textContent = ex.message;
|
|
err.hidden = false;
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|