securisation d'acces à la page
This commit is contained in:
parent
17d89c024d
commit
773cc71613
3 changed files with 286 additions and 96 deletions
|
|
@ -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>
|
||||
|
|
|
|||
364
index.html
364
index.html
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue