feat: Implement client authentication and pre-fill functionality
- Added AuthController for client login and token management. - Introduced AuthGate for handling authentication state and login UI. - Updated API to support fetching client data for pre-filling forms. - Modified App.vue to pre-fill client information if logged in. - Enhanced client model to include username and password fields. - Created migration for adding username and password to clients table. - Added utility for generating secure usernames and passwords. - Updated frontend to handle client tokens for pre-filling checkout forms. - Removed old CSS file and updated HTML references to new assets. - Added StatsOverview widget for displaying client and quote statistics. - Improved error handling and user feedback in the login process.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
// Base URL dell'API Laravel: impostata in build (import.meta.env.VITE_PREVENTIVI_API_BASE, da
|
||||
// .env/.env.production — vedi root del repo), sovrascrivibile a runtime con window.PREVENTIVI_API_BASE
|
||||
// senza dover rebuildare (es. per un hotfix). Nessuna delle due impostata → fallback dev locale.
|
||||
export const API_BASE =
|
||||
(typeof window.PREVENTIVI_API_BASE === 'string' && window.PREVENTIVI_API_BASE) ||
|
||||
import.meta.env.VITE_PREVENTIVI_API_BASE ||
|
||||
'http://localhost:8000/api';
|
||||
+2
-2
@@ -150,7 +150,7 @@ var APP = {
|
||||
modello.visible = false;
|
||||
}
|
||||
|
||||
this.viewFrontSide = function () {
|
||||
this.viewFrontSide = function (fillFactor) {
|
||||
camera.position.set(0, 0, 0.7);
|
||||
const box = new THREE.Box3().setFromObject(scene);
|
||||
|
||||
@@ -160,7 +160,7 @@ var APP = {
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
|
||||
const fov = camera.fov * (Math.PI / 180);
|
||||
const cameraZ = Math.abs(maxDim / (2 * Math.tan(fov / 2))) / 0.4;
|
||||
const cameraZ = Math.abs(maxDim / (2 * Math.tan(fov / 2))) / (fillFactor || 0.4);
|
||||
|
||||
const direction = new THREE.Vector3().subVectors(camera.position, center).normalize();
|
||||
camera.position.copy(center).add(direction.multiplyScalar(cameraZ));
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { API_BASE } from './api_base.js';
|
||||
|
||||
const TOKEN_KEY = 'kreios_auth_token';
|
||||
const EXPIRES_KEY = 'kreios_auth_expires';
|
||||
// sessionStorage: dura finché la finestra/scheda resta aperta, così cambiare
|
||||
// prodotto nel configuratore (nuova navigazione con ?type=...) non richiede
|
||||
// di riaggiungere ?test manualmente.
|
||||
const TEST_BYPASS_KEY = 'kreios_auth_test_bypass';
|
||||
|
||||
const getStoredAuth = () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const expiresAt = localStorage.getItem(EXPIRES_KEY);
|
||||
if (!token || !expiresAt) return null;
|
||||
if (new Date(expiresAt).getTime() <= Date.now()) return null;
|
||||
return { token, expiresAt };
|
||||
};
|
||||
|
||||
const storeAuth = (token, expiresAt) => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
localStorage.setItem(EXPIRES_KEY, expiresAt);
|
||||
};
|
||||
|
||||
const clearAuth = () => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(EXPIRES_KEY);
|
||||
};
|
||||
|
||||
// Verifica col backend che il token non sia stato revocato. Se il backend non è
|
||||
// raggiungibile non blocchiamo una sessione locale ancora nei termini di validità.
|
||||
const verifyToken = async (token) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/me`, {
|
||||
headers: { Accept: 'application/json', Authorization: `Bearer ${token}` },
|
||||
});
|
||||
return res.ok;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const injectStyles = () => {
|
||||
if (document.getElementById('auth-gate-styles')) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'auth-gate-styles';
|
||||
style.textContent = `
|
||||
#auth-gate-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20000;
|
||||
background: #12373C;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: inherit;
|
||||
}
|
||||
#auth-gate-form {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 32px 28px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
#auth-gate-form h1 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 20px;
|
||||
color: #12373C;
|
||||
text-align: center;
|
||||
}
|
||||
#auth-gate-form label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: #12373C;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
#auth-gate-form input {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
padding: 9px 10px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #c7d2d3;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#auth-gate-form button {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: #12373C;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#auth-gate-form button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
#auth-gate-form .auth-gate-error {
|
||||
margin-bottom: 14px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
color: #7a1f1f;
|
||||
background: #fbe4e4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
#auth-gate-form .d-none {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
const renderLoginForm = () => new Promise((resolve) => {
|
||||
injectStyles();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'auth-gate-overlay';
|
||||
overlay.innerHTML = `
|
||||
<form id="auth-gate-form" autocomplete="on">
|
||||
<h1>Accedi al configuratore</h1>
|
||||
<div class="auth-gate-error d-none"></div>
|
||||
<label>Nome utente
|
||||
<input type="text" name="username" required autocomplete="username" autofocus />
|
||||
</label>
|
||||
<label>Password
|
||||
<input type="password" name="password" required autocomplete="current-password" />
|
||||
</label>
|
||||
<button type="submit">Accedi</button>
|
||||
</form>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const form = overlay.querySelector('#auth-gate-form');
|
||||
const errorBox = overlay.querySelector('.auth-gate-error');
|
||||
const submitBtn = form.querySelector('button[type=submit]');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
errorBox.classList.add('d-none');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Accesso in corso...';
|
||||
|
||||
const username = form.username.value.trim();
|
||||
const password = form.password.value;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
errorBox.textContent = data.message || 'Accesso non riuscito.';
|
||||
errorBox.classList.remove('d-none');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Accedi';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
storeAuth(data.token, data.expires_at);
|
||||
overlay.remove();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
errorBox.textContent = 'Impossibile contattare il server. Riprova.';
|
||||
errorBox.classList.remove('d-none');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Accedi';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const AuthGate = {
|
||||
// Token del cliente loggato (o null se non autenticato/scaduto/bypass ?test).
|
||||
// Usato per passare l'identità al checkout (app "cliente") e precompilare l'anagrafica.
|
||||
getToken() {
|
||||
return getStoredAuth()?.token ?? null;
|
||||
},
|
||||
|
||||
// Risolve quando l'utente è autenticato (o quando l'accesso è bypassato con ?test).
|
||||
async ensure() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.has('test')) {
|
||||
sessionStorage.setItem(TEST_BYPASS_KEY, '1');
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionStorage.getItem(TEST_BYPASS_KEY) === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
const stored = getStoredAuth();
|
||||
if (stored) {
|
||||
const valid = await verifyToken(stored.token);
|
||||
if (valid) return;
|
||||
clearAuth();
|
||||
}
|
||||
|
||||
await renderLoginForm();
|
||||
},
|
||||
};
|
||||
|
||||
export default AuthGate;
|
||||
+13
-9
@@ -12,6 +12,8 @@ import './price_calculator.js';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||
import Configurator from './configurator.js';
|
||||
import AuthGate from './auth_gate.js';
|
||||
import { API_BASE as PREVENTIVI_API_BASE } from './api_base.js';
|
||||
|
||||
|
||||
window.$ = $;
|
||||
@@ -20,13 +22,6 @@ window.OrbitControls = OrbitControls; // Used by APP Scripts.
|
||||
window.GLTFLoader = GLTFLoader; // Used by APP Scripts.
|
||||
|
||||
// --- Integrazione backend preventivi ---
|
||||
// Base URL dell'API Laravel: impostata in build (import.meta.env.VITE_PREVENTIVI_API_BASE, da
|
||||
// .env/.env.production — vedi root del repo), sovrascrivibile a runtime con window.PREVENTIVI_API_BASE
|
||||
// senza dover rebuildare (es. per un hotfix). Nessuna delle due impostata → fallback dev locale.
|
||||
const PREVENTIVI_API_BASE =
|
||||
(typeof window.PREVENTIVI_API_BASE === 'string' && window.PREVENTIVI_API_BASE) ||
|
||||
import.meta.env.VITE_PREVENTIVI_API_BASE ||
|
||||
'http://localhost:8000/api';
|
||||
const CART_TOKEN_KEY = 'kreios_cart_token';
|
||||
|
||||
// URL dell'app cliente (checkout): richiesto all'API invece di essere indovinato, perché la
|
||||
@@ -59,7 +54,14 @@ window.mostraLinkPreventivo = (token, count) => {
|
||||
// diverse in dev/prod): lo passiamo qui come parametro, così il link "torna al configuratore"
|
||||
// nella pagina di conferma può essere generato dinamicamente invece di essere hardcoded.
|
||||
const ritorno = encodeURIComponent(window.location.href);
|
||||
link.href = `${appClienteUrl}/?token=${encodeURIComponent(token)}&ritorno=${ritorno}`;
|
||||
let href = `${appClienteUrl}/?token=${encodeURIComponent(token)}&ritorno=${ritorno}`;
|
||||
// Se il cliente è loggato nel configuratore, passiamo il suo token anche al checkout
|
||||
// così può precompilare l'anagrafica già salvata (vedi AuthGate.getToken()).
|
||||
const clienteToken = AuthGate.getToken();
|
||||
if (clienteToken) {
|
||||
href += `&cliente_token=${encodeURIComponent(clienteToken)}`;
|
||||
}
|
||||
link.href = href;
|
||||
link.textContent = count > 0 ? `Vai al preventivo (${count})` : 'Vai al preventivo';
|
||||
link.classList.remove('d-none');
|
||||
};
|
||||
@@ -256,12 +258,14 @@ const start = async () => {
|
||||
});
|
||||
|
||||
window.generaImagine = async () => {
|
||||
playerOff.viewFrontSide(0.6);
|
||||
playerOff.render();
|
||||
$('#canvas-offscreen').removeClass('d-none');
|
||||
const canvas = $('#canvas-offscreen canvas')[0];
|
||||
const dataUrl = await canvas.toDataURL('image/png');
|
||||
|
||||
$('#canvas-offscreen').addClass('d-none');
|
||||
playerOff.viewFrontSide();
|
||||
return dataUrl;
|
||||
const a = document.createElement('a');
|
||||
a.href = dataUrl;
|
||||
@@ -437,4 +441,4 @@ const start = async () => {
|
||||
}
|
||||
|
||||
|
||||
start();
|
||||
AuthGate.ensure().then(start);
|
||||
Reference in New Issue
Block a user