securisation d'acces à la page

This commit is contained in:
LIABEUF, Stephane (External) 2026-06-24 07:06:24 +02:00
parent 17d89c024d
commit 773cc71613
3 changed files with 286 additions and 96 deletions

View file

@ -702,8 +702,13 @@
<script>
const cv = new URLSearchParams(window.location.search).get('cv');
if (cv) document.getElementById('backBtn').href = `index.html?cv=${cv}`;
if (typeof umami !== 'undefined') umami.track('ecosystem-view');
const validCodes = ['f3b9c7a8d5e4b2d1c0a9b8d7', 'e2d1c0b9a8f7e6d5c4b3a2f1', 'a1b2c3d4e5f6a7b8c9d0e1f2'];
if (!cv || !validCodes.includes(cv)) {
window.location.href = 'index.html';
} else {
document.getElementById('backBtn').href = `index.html?cv=${cv}`;
if (typeof umami !== 'undefined') umami.track('ecosystem-view');
}
</script>
</body>
</html>

View file

@ -3485,107 +3485,287 @@
// Sélection du fichier JSON selon le paramètre ?cv=
const cvParam = new URLSearchParams(window.location.search).get('cv');
const cvAllowed = ['ux', 'dsi'];
const cvFile = cvAllowed.includes(cvParam) ? `cv-data-${cvParam}.json` : 'cv-data.json';
fetch(cvFile)
.then(r => {
if (!r.ok) throw new Error('Impossible de charger ' + cvFile + ' (code ' + r.status + ')');
return r.json();
})
.then(data => {
render(data);
injectSEO(data);
// Mapping des codes de 24 caractères vers les fichiers de CV correspondants
const cvMap = {
'f3b9c7a8d5e4b2d1c0a9b8d7': 'cv-data.json', // Version Générale (Directeur Conseil Service)
'e2d1c0b9a8f7e6d5c4b3a2f1': 'cv-data-ux.json', // Version UX & Digital Manager
'a1b2c3d4e5f6a7b8c9d0e1f2': 'cv-data-dsi.json' // Version Directeur SI & Transformation
};
// Mettre à jour le lien infra APRÈS render (l'élément existe maintenant dans le DOM)
const infraBtn = document.getElementById('infraBtn');
if (infraBtn && cvParam) infraBtn.href = `infrastructure.html?cv=${cvParam}`;
const ecosystemBtn = document.getElementById('ecosystemBtn');
if (ecosystemBtn && cvParam) ecosystemBtn.href = `ecosystem.html?cv=${cvParam}`;
if (!cvParam || !cvMap[cvParam]) {
// Masquer l'écran de chargement
const ld = document.getElementById('loading');
if (ld) ld.remove();
// Lang dynamique depuis le JSON (défaut : fr)
document.documentElement.lang = data.identity.lang || 'fr';
// Masquer les contrôles globaux non pertinents pour l'écran de verrouillage
const burger = document.getElementById('burgerBtn');
if (burger) burger.style.display = 'none';
const readProgress = document.getElementById('read-progress');
if (readProgress) readProgress.style.display = 'none';
// Tracking version CV
if (typeof umami !== 'undefined') umami.track('cv-view', { version: cvParam || 'master' });
// Restaurer le thème sauvegardé
const saved = localStorage.getItem('cv-theme');
if (saved && ['cyberpunk', 'bd', 'itpro', 'cgi'].includes(saved)) setTheme(saved);
// Scroll reveal — fix: sections already in viewport au chargement
document.querySelectorAll('section').forEach(s => {
const rect = s.getBoundingClientRect();
if (rect.top < window.innerHeight) s.classList.add('visible');
});
// Scroll reveal
const obs = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
if (entry.target.id === 'competences') {
setTimeout(() => {
document.querySelectorAll('.skill-row').forEach(r => {
r.querySelector('.skill-fill').style.width = r.dataset.level + '%';
});
}, 300);
}
}
});
}, { threshold: 0.08 });
document.querySelectorAll('section').forEach(s => obs.observe(s));
// Typing effect hero badge
const badge = document.getElementById('heroBadge');
if (badge) {
const txt = badge.textContent;
badge.textContent = '';
let i = 0;
setTimeout(() => {
const t = setInterval(() => {
badge.textContent = txt.slice(0, i++);
if (i > txt.length) clearInterval(t);
}, 40);
}, 500);
// Injecter le CSS pour la page privée
const style = document.createElement('style');
style.textContent = `
.private-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #02050a;
color: #f4f7fb;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
padding: 20px;
box-sizing: border-box;
position: fixed;
inset: 0;
z-index: 9999;
}
.private-card {
background: rgba(7, 18, 29, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(0, 215, 255, 0.2);
border-radius: 16px;
padding: 40px 30px;
max-width: 440px;
width: 100%;
text-align: center;
box-shadow: 0 0 40px rgba(0, 215, 255, 0.08);
}
.lock-icon {
color: #00d8ff;
margin-bottom: 24px;
animation: pulse-glow 3s infinite ease-in-out;
display: inline-flex;
justify-content: center;
align-items: center;
}
@keyframes pulse-glow {
0%, 100% {
transform: scale(1);
filter: drop-shadow(0 0 8px rgba(0, 216, 255, 0.4));
}
50% {
transform: scale(1.05);
filter: drop-shadow(0 0 20px rgba(0, 216, 255, 0.8));
}
}
.private-card h1 {
font-family: 'Orbitron', 'Arial Black', sans-serif;
font-size: 22px;
letter-spacing: 0.15em;
margin-bottom: 16px;
color: #00d8ff;
text-shadow: 0 0 10px rgba(0, 216, 255, 0.3);
}
.private-card p {
color: #9aa8b8;
font-size: 14px;
line-height: 1.6;
margin-bottom: 30px;
}
.input-group {
position: relative;
margin-bottom: 20px;
}
.private-card input {
width: 100%;
padding: 14px 16px;
background: rgba(2, 5, 10, 0.8);
border: 1px solid rgba(0, 215, 255, 0.15);
border-radius: 8px;
color: #fff;
font-family: 'Share Tech Mono', 'Courier New', monospace;
font-size: 15px;
letter-spacing: 0.05em;
text-align: center;
transition: all 0.3s ease;
box-sizing: border-box;
}
.private-card input:focus {
outline: none;
border-color: #00d8ff;
box-shadow: 0 0 15px rgba(0, 216, 255, 0.25);
}
.private-card button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #00d8ff 0%, #228cff 100%);
border: none;
border-radius: 8px;
color: #02050a;
font-family: 'Orbitron', sans-serif;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.1em;
cursor: pointer;
transition: all 0.3s ease;
}
.private-card button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 216, 255, 0.4);
}
.private-card button:active {
transform: translateY(0);
}
.error-message {
color: #ff3c74;
font-size: 12px;
margin-top: 15px;
font-family: 'Share Tech Mono', monospace;
min-height: 18px;
}
`;
document.head.appendChild(style);
// ── Progress bar ──
const progressBar = document.getElementById('read-progress');
window.addEventListener('scroll', () => {
const scrolled = window.scrollY;
const total = document.documentElement.scrollHeight - window.innerHeight;
progressBar.style.width = (total > 0 ? (scrolled / total) * 100 : 0) + '%';
}, { passive: true });
// Injecter le HTML de la page privée dans #app
const app = document.getElementById('app');
app.innerHTML = `
<div class="private-container">
<div class="private-card">
<div class="lock-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<h1>PAGE PRIVÉE</h1>
<p>Ce curriculum vitæ est confidentiel. Veuillez saisir votre code d'accès de 24 caractères pour y accéder.</p>
<form id="accessForm">
<div class="input-group">
<input type="text" id="accessCode" placeholder="Code d'accès (24 caractères)" maxlength="24" autocomplete="off" required>
</div>
<button type="submit">VALIDER</button>
</form>
<div id="errorMessage" class="error-message"></div>
</div>
</div>
`;
// ── Nav active (Intersection Observer) ──
const navObs = new IntersectionObserver(entries => {
entries.forEach(entry => {
const link = document.querySelector(`nav a[data-section="${entry.target.id}"]`);
if (link) link.classList.toggle('nav-active', entry.isIntersecting);
});
}, { threshold: 0.3 });
['experience', 'competences', 'secteurs', 'formation'].forEach(id => {
const el = document.getElementById(id);
if (el) navObs.observe(el);
});
// Se focaliser sur le champ de saisie
const accessCodeInput = document.getElementById('accessCode');
if (accessCodeInput) accessCodeInput.focus();
// Cacher le loading
const ld = document.getElementById('loading');
ld.style.opacity = '0';
setTimeout(() => ld.remove(), 400);
})
.catch(err => {
document.getElementById('loading').innerHTML = `
<div style="font-family:'Orbitron',monospace;color:#ff00aa;font-size:13px;letter-spacing:.1em;text-align:center;padding:40px">
⚠️ ERREUR DE CHARGEMENT<br><br>
<span style="font-family:'Courier New';font-size:11px;color:#7a9ab8">${err.message}</span><br><br>
<span style="font-size:11px;color:#4a6080">
Assurez-vous que <strong style="color:#00f5ff">cv-data.json</strong> est dans le même dossier<br>
et que la page est servie via un serveur web (pas file://).
</span>
</div>`;
// Écouter la soumission du formulaire
document.getElementById('accessForm').addEventListener('submit', (e) => {
e.preventDefault();
const code = accessCodeInput.value.trim();
if (cvMap[code]) {
// Rediriger avec le bon paramètre d'URL
window.location.search = `?cv=${code}`;
} else {
// Afficher le message d'erreur
const errEl = document.getElementById('errorMessage');
if (errEl) {
errEl.textContent = '❌ Code d\'accès invalide';
accessCodeInput.style.borderColor = '#ff3c74';
accessCodeInput.style.boxShadow = '0 0 15px rgba(255, 60, 116, 0.25)';
}
}
});
} else {
const cvFile = cvMap[cvParam];
fetch(cvFile)
.then(r => {
if (!r.ok) throw new Error('Impossible de charger ' + cvFile + ' (code ' + r.status + ')');
return r.json();
})
.then(data => {
render(data);
injectSEO(data);
// Mettre à jour le lien infra APRÈS render (l'élément existe maintenant dans le DOM)
const infraBtn = document.getElementById('infraBtn');
if (infraBtn && cvParam) infraBtn.href = `infrastructure.html?cv=${cvParam}`;
const ecosystemBtn = document.getElementById('ecosystemBtn');
if (ecosystemBtn && cvParam) ecosystemBtn.href = `ecosystem.html?cv=${cvParam}`;
// Lang dynamique depuis le JSON (défaut : fr)
document.documentElement.lang = data.identity.lang || 'fr';
// Tracking version CV
if (typeof umami !== 'undefined') umami.track('cv-view', { version: cvParam || 'master' });
// Restaurer le thème sauvegardé
const saved = localStorage.getItem('cv-theme');
if (saved && ['cyberpunk', 'bd', 'itpro', 'cgi'].includes(saved)) setTheme(saved);
// Scroll reveal — fix: sections already in viewport au chargement
document.querySelectorAll('section').forEach(s => {
const rect = s.getBoundingClientRect();
if (rect.top < window.innerHeight) s.classList.add('visible');
});
// Scroll reveal
const obs = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
if (entry.target.id === 'competences') {
setTimeout(() => {
document.querySelectorAll('.skill-row').forEach(r => {
r.querySelector('.skill-fill').style.width = r.dataset.level + '%';
});
}, 300);
}
}
});
}, { threshold: 0.08 });
document.querySelectorAll('section').forEach(s => obs.observe(s));
// Typing effect hero badge
const badge = document.getElementById('heroBadge');
if (badge) {
const txt = badge.textContent;
badge.textContent = '';
let i = 0;
setTimeout(() => {
const t = setInterval(() => {
badge.textContent = txt.slice(0, i++);
if (i > txt.length) clearInterval(t);
}, 40);
}, 500);
}
// ── Progress bar ──
const progressBar = document.getElementById('read-progress');
window.addEventListener('scroll', () => {
const scrolled = window.scrollY;
const total = document.documentElement.scrollHeight - window.innerHeight;
progressBar.style.width = (total > 0 ? (scrolled / total) * 100 : 0) + '%';
}, { passive: true });
// ── Nav active (Intersection Observer) ──
const navObs = new IntersectionObserver(entries => {
entries.forEach(entry => {
const link = document.querySelector(`nav a[data-section="${entry.target.id}"]`);
if (link) link.classList.toggle('nav-active', entry.isIntersecting);
});
}, { threshold: 0.3 });
['experience', 'competences', 'secteurs', 'formation'].forEach(id => {
const el = document.getElementById(id);
if (el) navObs.observe(el);
});
// Cacher le loading
const ld = document.getElementById('loading');
ld.style.opacity = '0';
setTimeout(() => ld.remove(), 400);
})
.catch(err => {
document.getElementById('loading').innerHTML = `
<div style="font-family:'Orbitron',monospace;color:#ff00aa;font-size:13px;letter-spacing:.1em;text-align:center;padding:40px">
⚠️ ERREUR DE CHARGEMENT<br><br>
<span style="font-family:'Courier New';font-size:11px;color:#7a9ab8">${err.message}</span><br><br>
<span style="font-size:11px;color:#4a6080">
Assurez-vous que <strong style="color:#00f5ff">cv-data.json</strong> est dans le même dossier<br>
et que la page est servie via un serveur web (pas file://).
</span>
</div>`;
});
}
// ══════════════════════════════════════════════
// TÉLÉCHARGEMENT PDF

View file

@ -980,7 +980,12 @@
</main>
<script>
const cv = new URLSearchParams(window.location.search).get('cv');
if (cv) document.getElementById('backBtn').href = `index.html?cv=${cv}`;
const validCodes = ['f3b9c7a8d5e4b2d1c0a9b8d7', 'e2d1c0b9a8f7e6d5c4b3a2f1', 'a1b2c3d4e5f6a7b8c9d0e1f2'];
if (!cv || !validCodes.includes(cv)) {
window.location.href = 'index.html';
} else {
document.getElementById('backBtn').href = `index.html?cv=${cv}`;
}
</script>
</body>