mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 18:36:55 +00:00
feat(secubox-toolbox): proper R3 CA + multi-OS install page + wiki (ref #496)
User reported (live screenshot from Android Chrome on kbin) :
- 'Ce certificat émis par null doit être installé via les paramètres'
- Signal passthrough OK (whitelist works ✓)
- But surf + banner KO because CA install impossible from Chrome
## Root cause
mitm 11 auto-generated its own CA at first start with subject
'CN=mitmproxy, O=mitmproxy' — minimal DN, ugly to Chrome's installer
(no C, ST, L, OU → renders as 'null' in the dialog).
Also : even with a perfect CA, Android 11+ blocks CA install from
Chrome — the user MUST go through Settings → Security → Encryption &
credentials → Install a certificate → CA certificate. The R3 install
page didn't explain this.
## Fix 1 : pre-generated CA with full DN
secubox-toolbox-wg-provision now generates BEFORE mitm starts a proper
CA with full subject :
C=FR, ST=Auvergne-Rhone-Alpes, L=Notre-Dame-du-Cruet,
O=CyberMind Gondwana, OU=ToolBoX R3 WireGuard,
CN=Gondwana ToolBoX R3 CA
Valid 10 years (was 1y), with proper v3_ca extensions :
basicConstraints critical CA:TRUE
keyUsage critical keyCertSign,cRLSign
subjectKeyIdentifier + authorityKeyIdentifier
subjectAltName DNS:Gondwana ToolBoX R3 CA, DNS:cabine.gondwana.local
Combined key+cert is written to <confdir>/mitmproxy-ca.pem (the file
mitm 11 reads on startup) so mitm uses OUR cert instead of generating
its own. Also writes mitmproxy-ca-cert.pem (public PEM) and
mitmproxy-ca-cert.cer (DER for Android picky installers).
Idempotent : keeps existing CA if mitmproxy-ca.pem already present
(SHA1 fingerprint preserved across reinstalls).
Live verification :
subject = C=FR, ST=Auvergne-Rhone-Alpes, L=Notre-Dame-du-Cruet,
O=CyberMind Gondwana, OU=ToolBoX R3 WireGuard,
CN=Gondwana ToolBoX R3 CA
sha1 Fingerprint = 96:74:E5:66:8E:F9:47:D1:F6:7E:C5:BB:73:7F:A4:2F:DC:9A:53:C4
## Fix 2 : /wg/r3-install with per-OS tabs
The install page (step 2) now has 5 OS tabs :
🍎 iOS / iPad — .mobileconfig 1-tap + Trust Settings
🤖 Android — Settings → Security path + WARNING about Chrome
🐧 Linux — Debian/Ubuntu, Fedora/RHEL, Arch + Firefox/Chrome NSS
💻 macOS — Keychain GUI + security CLI command
🪟 Windows — PowerShell Import-Certificate + GUI fallback
Each tab has copy-paste-ready commands.
Step 1 (WG profile) also gained WG client download links for all OS.
Footer links to the full wiki page.
## Fix 3 : Wiki page
Created https://github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install
with the exhaustive guide :
- Architecture diagram
- Step-by-step install for every OS
- Verify steps via /landing cert autocheck
- Whitelist explanation + apps that bypass
- Troubleshooting section
Live verification :
/wg/r3-install via kbin : HTTP 200 10015b
tabs : iOS=2 Android=4 Linux=3 macOS=2 Windows=1
commands : update-ca-certificates, update-ca-trust, trust anchor,
Cert:\LocalMachine, System.keychain — all present
This commit is contained in:
parent
b865636aa3
commit
645ce57251
|
|
@ -86,18 +86,62 @@ nft add rule inet filter input udp dport ${WG_PORT} accept 2>/dev/null || true
|
|||
echo "net.ipv4.ip_forward = 1" > /etc/sysctl.d/99-secubox-wg.conf
|
||||
sysctl -p /etc/sysctl.d/99-secubox-wg.conf >/dev/null 2>&1 || true
|
||||
|
||||
# ── CA WG dedicated root (separate from transparent mitm CA) ──
|
||||
if [ ! -f "$CA_WG_DIR/ca.pem" ]; then
|
||||
log "generating dedicated WG CA root"
|
||||
# ── CA WG : mitmproxy-readable combined PEM (key+cert) ──
|
||||
# mitm 11 looks for <confdir>/mitmproxy-ca.pem at startup. If present, it
|
||||
# uses our pre-generated CA instead of auto-generating an ugly one with
|
||||
# CN=mitmproxy. We MUST pre-generate it with a full DN so Android/Chrome
|
||||
# don't show "Ce certificat émis par null" (Chrome can't render certs
|
||||
# without a complete subject).
|
||||
if [ ! -f "$CA_WG_DIR/mitmproxy-ca.pem" ]; then
|
||||
log "generating dedicated WG CA (full DN, 10y)"
|
||||
cd "$CA_WG_DIR"
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out ca.pem \
|
||||
-days 365 -subj "/CN=Gondwana ToolBoX WG CA/O=CyberMind Gondwana/C=FR" \
|
||||
2>/dev/null
|
||||
chmod 0600 key.pem
|
||||
chmod 0644 ca.pem
|
||||
log "WG CA root created at $CA_WG_DIR/ca.pem"
|
||||
|
||||
# OpenSSL config — full DN, CA extensions, SKI/AKI
|
||||
cat > _ca.cnf <<'CNF'
|
||||
[req]
|
||||
distinguished_name = dn
|
||||
prompt = no
|
||||
x509_extensions = v3_ca
|
||||
|
||||
[dn]
|
||||
C = FR
|
||||
ST = Auvergne-Rhone-Alpes
|
||||
L = Notre-Dame-du-Cruet
|
||||
O = CyberMind Gondwana
|
||||
OU = ToolBoX R3 WireGuard
|
||||
CN = Gondwana ToolBoX R3 CA
|
||||
|
||||
[v3_ca]
|
||||
basicConstraints = critical, CA:TRUE
|
||||
keyUsage = critical, keyCertSign, cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
subjectAltName = DNS:Gondwana ToolBoX R3 CA, DNS:cabine.gondwana.local
|
||||
CNF
|
||||
|
||||
openssl genrsa -out _key.pem 2048 2>/dev/null
|
||||
openssl req -new -x509 -days 3650 -key _key.pem -out _cert.pem \
|
||||
-config _ca.cnf 2>/dev/null
|
||||
|
||||
# Combined key+cert for mitm (mitmproxy-ca.pem)
|
||||
cat _key.pem _cert.pem > mitmproxy-ca.pem
|
||||
chmod 0600 mitmproxy-ca.pem
|
||||
chown root:secubox-toolbox mitmproxy-ca.pem
|
||||
|
||||
# Public cert only (what we serve via /wg/ca.pem)
|
||||
cp _cert.pem mitmproxy-ca-cert.pem
|
||||
chmod 0644 mitmproxy-ca-cert.pem
|
||||
chown root:secubox-toolbox mitmproxy-ca-cert.pem
|
||||
|
||||
# DER fallback for Android (some installers prefer .cer DER)
|
||||
openssl x509 -in _cert.pem -outform DER -out mitmproxy-ca-cert.cer 2>/dev/null
|
||||
chmod 0644 mitmproxy-ca-cert.cer
|
||||
chown root:secubox-toolbox mitmproxy-ca-cert.cer
|
||||
|
||||
rm -f _key.pem _cert.pem _ca.cnf
|
||||
log "WG CA generated : $(openssl x509 -in mitmproxy-ca-cert.pem -noout -subject 2>&1 | head -1)"
|
||||
else
|
||||
log "WG CA root exists, keeping"
|
||||
log "WG CA exists, keeping ($(openssl x509 -in $CA_WG_DIR/mitmproxy-ca-cert.pem -noout -subject 2>&1 | head -1))"
|
||||
fi
|
||||
|
||||
# ── Enable wg-quick service ──
|
||||
|
|
|
|||
|
|
@ -409,20 +409,20 @@ async def webapp_manifest(request: Request) -> dict:
|
|||
|
||||
@router.get("/wg/r3-install", response_class=HTMLResponse)
|
||||
async def wg_r3_install(request: Request) -> HTMLResponse:
|
||||
"""R3 install page — simplified 3-step install : QR scan / CA R3 / GO.
|
||||
"""R3 install page — 3-step install with per-OS instructions tabs.
|
||||
|
||||
Optimized for first-time portable setup from anywhere (kbin.gk2.secubox.in).
|
||||
No more wall-of-text — large QR, 3 numbered steps, one big GO button.
|
||||
Step 1 = WG profile, Step 2 = CA install (per-OS), Step 3 = GO.
|
||||
"""
|
||||
html = """<!DOCTYPE html><html lang=fr><head><meta charset=UTF-8>
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<meta name=apple-mobile-web-app-capable content=yes>
|
||||
<link rel=manifest href=/manifest.json>
|
||||
<title>R3 — Tunnel portable Gondwana</title>
|
||||
<style>:root{--bg:#0a0a0f;--gold:#c9a84c;--phos:#00dd44;--phos-hot:#00ff55;--dim:#006622;--text:#e8e6d9;--purple:#9e76ff;--orange:#ffb347;--err:#e63946}
|
||||
<title>R3 — Tunnel portable Gondwana (install all-OS)</title>
|
||||
<style>:root{--bg:#0a0a0f;--gold:#c9a84c;--phos:#00dd44;--phos-hot:#00ff55;--dim:#006622;--text:#e8e6d9;--purple:#9e76ff;--orange:#ffb347;--err:#e63946;--blue:#5fa8ff}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{background:var(--bg);color:var(--text);font-family:'Courier New',Menlo,monospace;line-height:1.5}
|
||||
body{padding:1.2rem;max-width:600px;margin:auto}
|
||||
body{padding:1.2rem;max-width:720px;margin:auto}
|
||||
h1{color:var(--gold);text-align:center;font-size:1.4rem;margin-bottom:0.2rem;letter-spacing:2px}
|
||||
.lead{color:var(--phos);text-align:center;font-size:0.8rem;margin-bottom:1.4rem;opacity:0.85}
|
||||
.lead b{color:var(--phos-hot)}
|
||||
|
|
@ -432,52 +432,167 @@ h1{color:var(--gold);text-align:center;font-size:1.4rem;margin-bottom:0.2rem;let
|
|||
.qr{text-align:center;background:white;padding:14px;border-radius:8px;margin:0.6rem 0;box-shadow:0 2px 10px rgba(158,118,255,0.25)}
|
||||
.qr img{max-width:300px;width:100%;height:auto}
|
||||
.btn{display:block;text-align:center;padding:0.85rem 1rem;color:#0a0a0f;text-decoration:none;border-radius:6px;font-weight:bold;margin:0.5rem 0;font-size:0.95rem;transition:all 0.15s}
|
||||
.btn-pri{background:linear-gradient(90deg,var(--purple),#7e56df)}
|
||||
.btn-go{background:linear-gradient(90deg,var(--phos),var(--phos-hot));color:#0a0a0f;font-size:1.1rem;padding:1rem;letter-spacing:1px;text-shadow:0 0 6px rgba(0,255,85,0.5)}
|
||||
.btn-warn{background:linear-gradient(90deg,var(--orange),#dd7e0a)}
|
||||
.btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.4)}
|
||||
.btn-small{display:inline-block;padding:0.4rem 0.8rem;background:var(--orange);color:#0a0a0f;border-radius:4px;font-weight:bold;font-size:0.78rem;text-decoration:none;margin:0.2rem 0.3rem 0.2rem 0}
|
||||
.btn-small.blue{background:var(--blue)}
|
||||
.btn:hover,.btn-small:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.4)}
|
||||
.alt{font-size:0.72rem;color:#909090;text-align:center;margin-top:0.4rem}
|
||||
.alt a{color:var(--orange);text-decoration:underline}
|
||||
.tip{font-size:0.72rem;color:#a0d8a8;background:rgba(0,221,68,0.05);border-left:2px solid var(--phos);padding:0.5rem 0.7rem;margin-top:0.5rem;border-radius:0 3px 3px 0}
|
||||
.warn{font-size:0.78rem;color:#ffd6a0;background:rgba(255,179,71,0.08);border-left:3px solid var(--orange);padding:0.7rem;margin:0.4rem 0;border-radius:0 4px 4px 0}
|
||||
.back{display:block;text-align:center;margin-top:1.5rem;color:#666;text-decoration:none;font-size:0.85rem}
|
||||
.back:hover{color:var(--phos)}
|
||||
.tabs{display:flex;gap:2px;border-bottom:1px solid #2a2a3f;margin-bottom:0.6rem;flex-wrap:wrap}
|
||||
.tab{background:transparent;border:0;color:#888;padding:0.5rem 0.9rem;font-family:inherit;font-size:0.82rem;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}
|
||||
.tab.active{color:var(--purple);border-bottom-color:var(--purple);background:rgba(158,118,255,0.06)}
|
||||
.tab:hover{color:var(--text)}
|
||||
.tab-content{display:none;padding:0.4rem 0.2rem}
|
||||
.tab-content.active{display:block}
|
||||
.tab-content h3{color:var(--gold);font-size:0.85rem;margin-bottom:0.3rem}
|
||||
.tab-content ol{padding-left:1.4rem;font-size:0.82rem;line-height:1.7}
|
||||
.tab-content code{background:#1a1a25;color:var(--phos-hot);padding:0.15rem 0.4rem;border-radius:3px;font-size:0.78rem;display:inline-block;word-break:break-all}
|
||||
pre{background:#1a1a25;color:var(--phos-hot);padding:0.6rem 0.8rem;border-radius:4px;font-size:0.75rem;overflow-x:auto;margin:0.4rem 0;line-height:1.5}
|
||||
</style></head><body>
|
||||
|
||||
<h1>🌐 R3 — TUNNEL PORTABLE</h1>
|
||||
<p class=lead>3 étapes pour activer le mode <b>R3</b> — surveillance partout, sur 4G/WiFi tiers</p>
|
||||
<p class=lead>3 étapes · install <b>multi-OS</b> · marche partout (4G / WiFi tiers)</p>
|
||||
|
||||
<div class=step>
|
||||
<h2><span class=num>1</span> 📲 Scanner le QR profil</h2>
|
||||
<p style="font-size:0.85rem;margin-bottom:0.4rem">Installe l'app gratuite <b>WireGuard</b> (App Store / Play Store) puis scanne :</p>
|
||||
<h2><span class=num>1</span> 📲 Profil WireGuard</h2>
|
||||
<p style="font-size:0.85rem;margin-bottom:0.4rem">Installe l'app <b>WireGuard</b> (gratuite, tous OS) puis scanne ce QR :</p>
|
||||
<div class=qr><img src="/wg/qr.png" alt="QR profil WireGuard"/></div>
|
||||
<p class=alt>📥 Pas de scanner ? <a href="/wg/profile/new">Télécharger le .conf</a> (iPhone : touch → ouvrir dans WireGuard)</p>
|
||||
<p class=alt>📥 Pas de scanner ? <a href="/wg/profile/new">Télécharger village3b-toolbox.conf</a></p>
|
||||
<div class=tip>
|
||||
🌐 <b>WireGuard download :</b>
|
||||
<a class=btn-small href="https://apps.apple.com/app/wireguard/id1441195209">iOS</a>
|
||||
<a class=btn-small href="https://play.google.com/store/apps/details?id=com.wireguard.android">Android</a>
|
||||
<a class=btn-small href="https://www.wireguard.com/install/">Linux/Win/Mac</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=step>
|
||||
<h2><span class=num>2</span> 🔐 Installer le certificat R3</h2>
|
||||
<p style="font-size:0.85rem;margin-bottom:0.4rem">Certif <b>spécifique R3</b> (≠ R1/R2). Sans lui, les sites HTTPS cassent.</p>
|
||||
<a href="/wg/ca.mobileconfig" class="btn btn-warn">📲 iPhone : 1-tap install</a>
|
||||
<a href="/wg/ca.pem" class="btn btn-warn">🤖 Android / PC : .pem</a>
|
||||
<h2><span class=num>2</span> 🔐 Certificat R3 (par OS)</h2>
|
||||
<p style="font-size:0.85rem;margin-bottom:0.5rem">Le CA <b>« Gondwana ToolBoX R3 CA »</b> doit être ajouté à la racine de confiance.</p>
|
||||
|
||||
<div class=tabs>
|
||||
<button class="tab active" data-tab=ios>🍎 iOS / iPad</button>
|
||||
<button class="tab" data-tab=android>🤖 Android</button>
|
||||
<button class="tab" data-tab=linux>🐧 Linux</button>
|
||||
<button class="tab" data-tab=mac>💻 macOS</button>
|
||||
<button class="tab" data-tab=win>🪟 Windows</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" data-content=ios>
|
||||
<a href="/wg/ca.mobileconfig" class="btn btn-warn">📲 Installer .mobileconfig (1-tap)</a>
|
||||
<h3>Puis :</h3>
|
||||
<ol>
|
||||
<li>Réglages → Général → <b>VPN et gestion d'appareils</b> → installer le profil</li>
|
||||
<li>Réglages → Général → Information → <b>Réglages de confiance des certificats</b></li>
|
||||
<li>Activer le toggle <b>« Gondwana ToolBoX R3 CA »</b> 🟢</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" data-content=android>
|
||||
<a href="/wg/ca.pem" class="btn btn-warn">📥 Télécharger ca.pem</a>
|
||||
<div class=warn>
|
||||
<b>iPhone seulement :</b> après l'install, va aussi dans<br>
|
||||
Réglages → Général → Information → <b>Réglages de confiance</b><br>
|
||||
et active <b>« Gondwana ToolBoX WG CA »</b>
|
||||
⚠ Chrome ne peut PAS installer un CA directement (sécurité Android 11+).
|
||||
Tu DOIS passer par les <b>Paramètres système</b>.
|
||||
</div>
|
||||
<h3>Procédure (toutes versions Android) :</h3>
|
||||
<ol>
|
||||
<li>Télécharge le .pem (bouton ci-dessus, sauvé dans Téléchargements)</li>
|
||||
<li>Ouvre <b>Paramètres système</b> (pas Chrome)</li>
|
||||
<li>→ <b>Sécurité et confidentialité</b> (ou « Sécurité »)</li>
|
||||
<li>→ <b>Chiffrement et authentifiants</b> (ou « Identifiants »)</li>
|
||||
<li>→ <b>Installer un certificat</b> → <b>Certificat d'autorité</b></li>
|
||||
<li>Sélectionne <code>ca.pem</code> dans Téléchargements</li>
|
||||
<li>Confirme le PIN/empreinte du téléphone si demandé</li>
|
||||
</ol>
|
||||
<h3>Vérification :</h3>
|
||||
<pre>Paramètres → Sécurité → Identifiants
|
||||
de confiance → onglet UTILISATEUR
|
||||
→ « Gondwana ToolBoX R3 CA » présent ✓</pre>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" data-content=linux>
|
||||
<a href="/wg/ca.pem" class="btn btn-warn">📥 Télécharger ca.pem</a>
|
||||
<h3>Debian / Ubuntu :</h3>
|
||||
<pre>sudo cp ca.pem /usr/local/share/ca-certificates/gondwana-toolbox-r3.crt
|
||||
sudo update-ca-certificates</pre>
|
||||
<h3>Fedora / RHEL :</h3>
|
||||
<pre>sudo cp ca.pem /etc/pki/ca-trust/source/anchors/gondwana-toolbox-r3.crt
|
||||
sudo update-ca-trust</pre>
|
||||
<h3>Arch :</h3>
|
||||
<pre>sudo trust anchor --store ca.pem</pre>
|
||||
<h3>Vérif Firefox / Chrome :</h3>
|
||||
<pre># Firefox utilise son propre store
|
||||
# Chrome utilise NSS :
|
||||
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," \\
|
||||
-n "Gondwana ToolBoX R3 CA" -i ca.pem</pre>
|
||||
<h3>Tunnel rapide (Linux) :</h3>
|
||||
<pre>wget /wg/profile/new -O village3b.conf
|
||||
sudo wg-quick up ./village3b.conf
|
||||
# stop :
|
||||
sudo wg-quick down ./village3b.conf</pre>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" data-content=mac>
|
||||
<a href="/wg/ca.pem" class="btn btn-warn">📥 Télécharger ca.pem</a>
|
||||
<h3>Install (Keychain Access) :</h3>
|
||||
<ol>
|
||||
<li>Double-clic <code>ca.pem</code> → s'ouvre dans <b>Trousseaux d'accès</b></li>
|
||||
<li>Choisir trousseau <b>Système</b> (pas Login)</li>
|
||||
<li>Authentifier avec ton mot de passe macOS</li>
|
||||
<li>Double-clic le cert installé → <b>Approuver</b> → « Toujours approuver »</li>
|
||||
</ol>
|
||||
<h3>Ou en CLI :</h3>
|
||||
<pre>sudo security add-trusted-cert -d -r trustRoot \\
|
||||
-k /Library/Keychains/System.keychain ca.pem</pre>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" data-content=win>
|
||||
<a href="/wg/ca.pem" class="btn btn-warn">📥 Télécharger ca.pem</a>
|
||||
<h3>Install (PowerShell admin) :</h3>
|
||||
<pre>Import-Certificate -FilePath .\\ca.pem `
|
||||
-CertStoreLocation Cert:\\LocalMachine\\Root</pre>
|
||||
<h3>Ou GUI :</h3>
|
||||
<ol>
|
||||
<li>Double-clic <code>ca.pem</code> → <b>Installer le certificat</b></li>
|
||||
<li>Emplacement : <b>Ordinateur local</b> (pas Utilisateur courant)</li>
|
||||
<li>Magasin : <b>Autorités de certification racines de confiance</b></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=step>
|
||||
<h2><span class=num>3</span> 🚀 Activer le tunnel</h2>
|
||||
<p style="font-size:0.85rem;margin-bottom:0.5rem">Dans l'app WireGuard, glisse le toggle <b>ON</b>. L'icône VPN apparaît dans la barre d'état.</p>
|
||||
<p style="font-size:0.85rem;margin-bottom:0.5rem">App WireGuard → toggle <b>ON</b> sur le profil <code>village3b-toolbox</code>. L'icône VPN apparaît dans la barre d'état.</p>
|
||||
<a href="/report/me/html" class="btn btn-go">📊 GO — Voir mon rapport live</a>
|
||||
<div class=tip>
|
||||
✓ Trafic chiffré entre iPhone et la cabine (jamais en clair sur Internet)<br>
|
||||
✓ Bandeau sur toutes les pages HTTPS<br>
|
||||
✓ FaceTime / iCloud / Signal restent fluides (bypass intelligent)
|
||||
✓ Trafic chiffré entre device et cabine (jamais en clair sur Internet)<br>
|
||||
✓ Bandeau apparaît sur toutes les pages HTTPS<br>
|
||||
✓ Signal / WhatsApp / Telegram / iCloud / banques → <b>passthrough</b> (jamais déchiffrés)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/" class=back>← Retour splash</a>
|
||||
<p style="text-align:center;font-size:0.78rem;color:#666;margin-top:1.5rem">
|
||||
📖 Wiki complet : <a href="https://github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install" style="color:var(--purple)">github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install</a>
|
||||
</p>
|
||||
|
||||
<a href="/landing" class=back>← Retour landing</a>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.tab').forEach(function(t){
|
||||
t.addEventListener('click', function(){
|
||||
var tn = this.dataset.tab;
|
||||
document.querySelectorAll('.tab').forEach(function(x){x.classList.remove('active');});
|
||||
this.classList.add('active');
|
||||
document.querySelectorAll('.tab-content').forEach(function(x){x.classList.remove('active');});
|
||||
document.querySelector('[data-content='+tn+']').classList.add('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>"""
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user