Skip to content

Instantly share code, notes, and snippets.

@Cloud-Dark
Created May 19, 2025 09:30
Show Gist options
  • Save Cloud-Dark/8e76c75598d5a47e71d5a623be9287a4 to your computer and use it in GitHub Desktop.
Save Cloud-Dark/8e76c75598d5a47e71d5a623be9287a4 to your computer and use it in GitHub Desktop.
2fa generator
<div class="container">
<h2>TOTP Generator & QR Code</h2>
<input type="text" id="secretInput" placeholder="Enter Base32 Secret Token" autocomplete="off" />
<div class="btn-group">
<button id="btnGenerateQR">Generate QR Code</button>
<button id="btnGenerateToken">Generate Token</button>
<button id="btnCopyToken">Copy Token</button>
</div>
<p id="code">Waiting...</p>
<canvas id="qr"></canvas>
<input type="text" id="userCodeInput" placeholder="Enter Code to Validate" autocomplete="off"/>
<button id="btnValidateCode">Validate Code</button>
<p id="validationResult"></p>
</div>
let totp = null;
const secretInput = document.getElementById('secretInput');
const codeDisplay = document.getElementById('code');
const qrCanvas = document.getElementById('qr');
const userCodeInput = document.getElementById('userCodeInput');
const validationResult = document.getElementById('validationResult');
document.getElementById('btnGenerateToken').addEventListener('click', () => {
const secret = secretInput.value.trim();
if (!secret) return alert('Please enter a Base32 secret token!');
try {
totp = new OTPAuth.TOTP({
issuer: 'MyApp',
label: '[email protected]',
algorithm: 'SHA1',
digits: 6,
period: 30,
secret: OTPAuth.Secret.fromBase32(secret),
});
} catch (e) {
alert(e);
return;
}
const token = totp.generate();
codeDisplay.textContent = token;
validationResult.textContent = '';
// Trigger pulse animation
codeDisplay.classList.remove('pulse');
void codeDisplay.offsetWidth; // reflow to restart animation
codeDisplay.classList.add('pulse');
});
document.getElementById('btnGenerateQR').addEventListener('click', () => {
const secret = secretInput.value.trim();
if (!secret) return alert('Please enter a Base32 secret token!');
try {
const tempTotp = new OTPAuth.TOTP({
issuer: 'MyApp',
label: '[email protected]',
secret: OTPAuth.Secret.fromBase32(secret),
});
const uri = tempTotp.toString();
QRCode.toCanvas(qrCanvas, uri, { width: 200 }, (error) => {
if (error) {
alert('Failed to generate QR code.');
console.error(error);
}
});
} catch (e) {
alert('Invalid secret format.');
}
});
document.getElementById('btnCopyToken').addEventListener('click', () => {
const code = codeDisplay.textContent;
if (!code || code === 'Waiting...') return alert('No token to copy!');
navigator.clipboard.writeText(code)
.then(() => alert('Token copied to clipboard!'))
.catch(() => alert('Failed to copy token.'));
});
document.getElementById('btnValidateCode').addEventListener('click', () => {
if (!totp) return alert('Generate the token first to enable validation.');
const inputCode = userCodeInput.value.trim();
if (!inputCode) return alert('Please enter a code to validate.');
const delta = totp.validate({ token: inputCode, window: 1 });
if (delta !== null) {
validationResult.textContent = '✅ Code is valid!';
validationResult.style.color = 'green';
} else {
validationResult.textContent = '❌ Code is invalid!';
validationResult.style.color = 'red';
}
// Trigger blink animation on validation result
validationResult.classList.remove('blink');
void validationResult.offsetWidth; // reflow
validationResult.classList.add('blink');
});
<script src="https://cdn.jsdelivr.net/npm/otpauth/dist/otpauth.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js"></script>
/* Reset margin/padding dan atur box-sizing */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1d2b64, #f8cdda);
color: #222;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 30px;
margin: 0;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(29, 43, 100, 0.3);
padding: 30px 40px;
max-width: 600px;
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto auto;
gap: 20px 30px;
align-items: center;
/* Animasi fade-in */
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.8s ease forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
h2 {
grid-column: 1 / -1;
text-align: center;
margin: 0 0 10px;
font-weight: 700;
color: #1d2b64;
letter-spacing: 1.2px;
}
input[type="text"] {
font-size: 1rem;
padding: 12px 15px;
border-radius: 8px;
border: 1.5px solid #ccc;
outline: none;
width: 100%;
transition: border-color 0.3s;
text-align: center;
}
input[type="text"]:focus {
border-color: #007bff;
box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
}
/* Buttons container spanning two columns */
.btn-group {
grid-column: 1 / -1;
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
button {
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 8px;
padding: 12px 22px;
font-size: 1rem;
font-weight: 600;
transition: background-color 0.3s, box-shadow 0.3s, transform 0.2s;
flex: 1 1 130px;
max-width: 180px;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
button:hover {
background-color: #0056b3;
box-shadow: 0 6px 15px rgba(0, 86, 179, 0.5);
transform: scale(1.05);
}
#code {
grid-column: 1 / -1;
font-size: 3rem;
background: #f1f3f6;
color: #333;
border-radius: 16px;
padding: 20px 0;
user-select: all;
letter-spacing: 10px;
text-align: center;
box-shadow: inset 0 0 8px #ddd;
margin: 0;
/* Animasi pulse */
}
#code.pulse {
animation: pulse 1.2s ease-in-out;
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 8px 3px rgba(0, 123, 255, 0.5);
}
50% {
box-shadow: 0 0 12px 6px rgba(0, 123, 255, 0.9);
}
}
#qr {
grid-column: 1 / 2;
justify-self: center;
border-radius: 16px;
background: white;
box-shadow: 0 6px 20px rgba(29, 43, 100, 0.15);
width: 200px;
height: 200px;
}
#userCodeInput {
grid-column: 2 / 3;
justify-self: stretch;
}
#btnValidateCode {
grid-column: 2 / 3;
justify-self: center;
max-width: 180px;
}
#validationResult {
grid-column: 1 / -1;
text-align: center;
font-weight: 700;
font-size: 1.3rem;
min-height: 1.5rem;
margin-top: 10px;
user-select: none;
}
#validationResult.blink {
animation: blink 1s ease-in-out 2;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
/* Responsive for smaller screens */
@media (max-width: 480px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto auto auto auto;
}
#qr,
#userCodeInput,
#btnValidateCode {
grid-column: 1 / 2 !important;
justify-self: center !important;
width: 200px;
max-width: 100%;
}
.btn-group {
flex-direction: column;
}
button {
max-width: 100%;
flex: none;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment