feat(verifactu): arquitectura token — front llama a MidAPI directamente
- verifactu.js: token en memoria (no localStorage), initVeriFactuStatus/reset - sendVeriFactuInvoice y cancelVeriFactuInvoice llaman a MidAPI con Bearer token - VERIFACTU_URL desde variable de entorno VITE_VERIFACTU_URL - session.js: initVeriFactuStatus en login, resetVeriFactuStatus en logout - SettingsPage.js: estado del cert consultado al BFF, sin botón borrar token Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7a06bd8963
commit
114e64d1fd
|
|
@ -2,7 +2,7 @@ import { isDarkTheme, toggleTheme } from '../services/theme.js';
|
||||||
import { icons } from '../services/icons.js';
|
import { icons } from '../services/icons.js';
|
||||||
import { apiGet, apiPut } from '../services/apiClient.js';
|
import { apiGet, apiPut } from '../services/apiClient.js';
|
||||||
import { showToast } from '../services/toast.js';
|
import { showToast } from '../services/toast.js';
|
||||||
import { encryptVeriFactuPassword, formatVeriFactuError, getVeriFactuFormats, getVeriFactuHealth, registerVeriFactuCertificate, saveVeriFactuToken, getVeriFactuToken, clearVeriFactuToken } from '../services/verifactu.js';
|
import { encryptVeriFactuPassword, formatVeriFactuError, getVeriFactuFormats, getVeriFactuHealth, getVeriFactuTokenStatus, initVeriFactuStatus, registerVeriFactuCertificate } from '../services/verifactu.js';
|
||||||
|
|
||||||
function decodeTokenPayload(token) {
|
function decodeTokenPayload(token) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -155,8 +155,7 @@ export function renderSettingsPage() {
|
||||||
<div style="display:flex;gap:0.75rem;justify-content:flex-end;flex-wrap:wrap">
|
<div style="display:flex;gap:0.75rem;justify-content:flex-end;flex-wrap:wrap">
|
||||||
<button type="button" class="btn-cancel-compact" id="verifactu-health-btn">Comprobar API</button>
|
<button type="button" class="btn-cancel-compact" id="verifactu-health-btn">Comprobar API</button>
|
||||||
<button type="button" class="btn-cancel-compact" id="verifactu-formats-btn">Ver formatos</button>
|
<button type="button" class="btn-cancel-compact" id="verifactu-formats-btn">Ver formatos</button>
|
||||||
<button type="button" class="btn-cancel-compact" id="verifactu-clear-token-btn" style="display:none">Borrar token</button>
|
<button type="button" class="btn-submit-compact" id="verifactu-register-btn">Registrar certificado</button>
|
||||||
<button type="button" class="btn-submit-compact" id="verifactu-register-btn">Registrar certificado</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="verifactu-status" style="font-size:0.8rem;color:var(--text-secondary);min-height:1.2em"></p>
|
<p id="verifactu-status" style="font-size:0.8rem;color:var(--text-secondary);min-height:1.2em"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -203,27 +202,19 @@ export function renderSettingsPage() {
|
||||||
const verifactuCertName = container.querySelector('#verifactu-cert-name');
|
const verifactuCertName = container.querySelector('#verifactu-cert-name');
|
||||||
const verifactuCertFile = container.querySelector('#verifactu-cert-file');
|
const verifactuCertFile = container.querySelector('#verifactu-cert-file');
|
||||||
const verifactuCertPassword = container.querySelector('#verifactu-cert-password');
|
const verifactuCertPassword = container.querySelector('#verifactu-cert-password');
|
||||||
const clearTokenBtn = container.querySelector('#verifactu-clear-token-btn');
|
async function renderVeriFactuTokenStatus() {
|
||||||
|
try {
|
||||||
const renderVeriFactuTokenStatus = () => {
|
const status = await getVeriFactuTokenStatus();
|
||||||
const token = getVeriFactuToken();
|
verifactuStatus.textContent = status?.active
|
||||||
if (token) {
|
? 'Certificado activo. El sistema está listo para facturar.'
|
||||||
verifactuStatus.textContent = `Token activo: ${token.slice(0, 8)}...`;
|
: 'Sin certificado registrado.';
|
||||||
clearTokenBtn.style.display = '';
|
} catch {
|
||||||
} else {
|
verifactuStatus.textContent = '';
|
||||||
clearTokenBtn.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
renderVeriFactuTokenStatus();
|
renderVeriFactuTokenStatus();
|
||||||
|
|
||||||
clearTokenBtn.addEventListener('click', () => {
|
|
||||||
clearVeriFactuToken();
|
|
||||||
verifactuStatus.textContent = 'Token eliminado.';
|
|
||||||
clearTokenBtn.style.display = 'none';
|
|
||||||
showToast('Token de VeriFactu eliminado', 'success');
|
|
||||||
});
|
|
||||||
|
|
||||||
verifactuCertFile.addEventListener('change', () => {
|
verifactuCertFile.addEventListener('change', () => {
|
||||||
const file = verifactuCertFile.files?.[0];
|
const file = verifactuCertFile.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
@ -354,13 +345,9 @@ export function renderSettingsPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.token) {
|
verifactuStatus.textContent = 'Certificado registrado. El sistema está listo para facturar.';
|
||||||
saveVeriFactuToken(result.token);
|
|
||||||
renderVeriFactuTokenStatus();
|
|
||||||
}
|
|
||||||
const tokenHint = result.token ? ` Token: ${result.token.slice(0, 8)}...` : '';
|
|
||||||
verifactuStatus.textContent = `Certificado registrado.${tokenHint}`;
|
|
||||||
showToast('Certificado registrado en VeriFactu', 'success');
|
showToast('Certificado registrado en VeriFactu', 'success');
|
||||||
|
initVeriFactuStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = formatVeriFactuError(error);
|
const errorMessage = formatVeriFactuError(error);
|
||||||
verifactuStatus.textContent = `Error al registrar certificado: ${errorMessage}`;
|
verifactuStatus.textContent = `Error al registrar certificado: ${errorMessage}`;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { auth } from './auth.js';
|
import { auth } from './auth.js';
|
||||||
|
import { initVeriFactuStatus, resetVeriFactuStatus } from './verifactu.js';
|
||||||
|
|
||||||
const SESSION_TIMEOUT_MS = Number(import.meta.env.VITE_SESSION_TIMEOUT_MS || 15 * 60 * 1000);
|
const SESSION_TIMEOUT_MS = Number(import.meta.env.VITE_SESSION_TIMEOUT_MS || 15 * 60 * 1000);
|
||||||
const SESSION_WARNING_MS = Number(import.meta.env.VITE_SESSION_WARNING_MS || 60 * 1000);
|
const SESSION_WARNING_MS = Number(import.meta.env.VITE_SESSION_WARNING_MS || 60 * 1000);
|
||||||
|
|
@ -166,11 +167,13 @@ function bindAuthListeners() {
|
||||||
window.addEventListener('auth:login', () => {
|
window.addEventListener('auth:login', () => {
|
||||||
removeWarningOverlay();
|
removeWarningOverlay();
|
||||||
resetSessionTimers();
|
resetSessionTimers();
|
||||||
|
initVeriFactuStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('auth:logout', () => {
|
window.addEventListener('auth:logout', () => {
|
||||||
removeWarningOverlay();
|
removeWarningOverlay();
|
||||||
clearTimers();
|
clearTimers();
|
||||||
|
resetVeriFactuStatus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
import forge from 'node-forge';
|
import forge from 'node-forge';
|
||||||
import { apiGet, apiPost } from './apiClient.js';
|
import { apiGet, apiPost, apiRequest } from './apiClient.js';
|
||||||
|
|
||||||
|
const VERIFACTU_URL = import.meta.env.VITE_VERIFACTU_URL ?? 'http://localhost:6789';
|
||||||
|
|
||||||
let cachedPublicKeyPem = null;
|
let cachedPublicKeyPem = null;
|
||||||
|
let verifactuActive = false;
|
||||||
|
let verifactuToken = null;
|
||||||
|
|
||||||
const VERIFACTU_TOKEN_KEY = 'verifactu_cert_token';
|
export function isVeriFactuActive() {
|
||||||
|
return verifactuActive;
|
||||||
|
}
|
||||||
|
|
||||||
export function saveVeriFactuToken(token) {
|
export async function initVeriFactuStatus() {
|
||||||
if (token) {
|
try {
|
||||||
localStorage.setItem(VERIFACTU_TOKEN_KEY, token);
|
const data = await apiGet('/api/VeriFactu/token');
|
||||||
|
verifactuActive = data?.active === true;
|
||||||
|
verifactuToken = data?.token ?? null;
|
||||||
|
} catch {
|
||||||
|
verifactuActive = false;
|
||||||
|
verifactuToken = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getVeriFactuToken() {
|
export function resetVeriFactuStatus() {
|
||||||
return localStorage.getItem(VERIFACTU_TOKEN_KEY) || null;
|
verifactuActive = false;
|
||||||
}
|
verifactuToken = null;
|
||||||
|
|
||||||
export function clearVeriFactuToken() {
|
|
||||||
localStorage.removeItem(VERIFACTU_TOKEN_KEY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERIFACTU_ERROR_MESSAGES = {
|
const VERIFACTU_ERROR_MESSAGES = {
|
||||||
|
|
@ -101,10 +109,26 @@ export function formatVeriFactuError(errorLike) {
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getVeriFactuTokenStatus() {
|
||||||
|
return apiGet('/api/VeriFactu/token-status');
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendVeriFactuInvoice(payload) {
|
export async function sendVeriFactuInvoice(payload) {
|
||||||
return apiPost('/api/VeriFactu/facturas', payload);
|
if (!verifactuToken) throw new Error('No hay token de VeriFactu activo');
|
||||||
|
return apiRequest(`${VERIFACTU_URL}/api/v1/facturas`, {
|
||||||
|
method: 'POST',
|
||||||
|
auth: false,
|
||||||
|
headers: { Authorization: `Bearer ${verifactuToken}` },
|
||||||
|
body: payload
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelVeriFactuInvoice(payload) {
|
export async function cancelVeriFactuInvoice(payload) {
|
||||||
return apiPost('/api/VeriFactu/facturas/anular', payload);
|
if (!verifactuToken) throw new Error('No hay token de VeriFactu activo');
|
||||||
|
return apiRequest(`${VERIFACTU_URL}/api/v1/facturas/anular`, {
|
||||||
|
method: 'POST',
|
||||||
|
auth: false,
|
||||||
|
headers: { Authorization: `Bearer ${verifactuToken}` },
|
||||||
|
body: payload
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue