From 28a6e82bfc5ab94e96b00a20758459e91168c5c3 Mon Sep 17 00:00:00 2001 From: hyzen Date: Sun, 7 Jun 2026 00:14:06 +0530 Subject: [PATCH] Add: otp verification --- docs/css/auth-additions.css | 107 +++++++++++++ docs/signup/index.html | 16 +- layouts/signup/single.html | 289 ++++++++++++++++++++++++++++++---- static/css/auth-additions.css | 107 +++++++++++++ 4 files changed, 487 insertions(+), 32 deletions(-) diff --git a/docs/css/auth-additions.css b/docs/css/auth-additions.css index acc279b..f6e4094 100644 --- a/docs/css/auth-additions.css +++ b/docs/css/auth-additions.css @@ -198,3 +198,110 @@ [data-theme='light'] .auth-form__submit { color: #fff; } + +/* ── OTP step indicator ─────────────────────────────────────────────────── */ + +.otp-steps { + display: flex; + align-items: center; + margin-bottom: 1.6rem; +} + +.otp-steps__line { + flex: 1; + height: 2px; + background: var(--background-color1, #ccc); + transition: background 0.25s ease; +} + +.otp-steps__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; +} + +.otp-steps__num { + width: 1.8rem; + height: 1.8rem; + border-radius: 50%; + border: 2px solid var(--background-color1, #ccc); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.78rem; + font-weight: 700; + color: var(--foreground-color3, #888); + background: var(--background-color); + transition: + border-color 0.25s ease, + color 0.25s ease, + background 0.25s ease; +} + +.otp-steps__label { + font-size: 0.7rem; + color: var(--foreground-color3, #888); + white-space: nowrap; + transition: color 0.25s ease; +} + +/* Active step */ +.otp-steps__item--active .otp-steps__num { + border-color: var(--accent-color); + color: var(--accent-color); +} + +.otp-steps__item--active .otp-steps__label { + color: var(--accent-color); + font-weight: 600; +} + +/* Completed step */ +.otp-steps__item--done .otp-steps__num { + border-color: var(--accent-color); + background: var(--accent-color); + color: #fff; +} + +.otp-steps__item--done .otp-steps__label { + color: var(--accent-color); +} + +/* ── OTP panel ───────────────────────────────────────────────────────────── */ + +.otp-panel__hint { + font-size: 0.87rem; + line-height: 1.5; + color: var(--foreground-color); + margin: 0; +} + +.otp-panel__input { + letter-spacing: 0.25em; + font-size: 1.15rem; + text-align: center; +} + +.otp-panel__resend { + font-size: 0.82rem; + color: var(--foreground-color3, #888); + text-align: center; + margin: 0; +} + +.otp-panel__resend-btn { + background: none; + border: none; + padding: 0; + font: inherit; + font-size: 0.82rem; + color: var(--accent-color); + cursor: pointer; + font-weight: 600; + text-decoration: underline; +} + +.otp-panel__resend-btn:hover { + opacity: 0.8; +} diff --git a/docs/signup/index.html b/docs/signup/index.html index 4a8c03d..7bf0661 100644 --- a/docs/signup/index.html +++ b/docs/signup/index.html @@ -5,7 +5,10 @@ Sign Up
-

Sign Up

+

Sign Up

1 +Details
2 +Verify email
3 +Done
(3-32 characters; letters, numbers, _ and - only)
@@ -16,5 +19,12 @@ (at least 8 characters)
\ No newline at end of file +Send verification code +
\ No newline at end of file diff --git a/layouts/signup/single.html b/layouts/signup/single.html index 7cb90fa..0119454 100644 --- a/layouts/signup/single.html +++ b/layouts/signup/single.html @@ -5,6 +5,25 @@
+ +
+
+ 1 + Details +
+
+
+ 2 + Verify email +
+
+
+ 3 + Done +
+
+ +
@@ -95,8 +114,9 @@ />
+
+ +

+ Didn't get it? + + +

+
+ + @@ -124,12 +194,59 @@ (function () { var BACKEND = 'https://backend.freedoms4.org/auth.php'; - var form = document.getElementById('signup-form'); - var msgBox = document.getElementById('auth-message'); - var submitBtn = document.getElementById('signup-submit'); - var pwdInput = document.getElementById('signup-password'); - var eyeBtn = form.querySelector('.auth-form__eye'); + // ── State ────────────────────────────────────────────────────── + var state = { + username: '', + email: '', + password: '', + otpToken: null, + }; + // ── DOM refs ─────────────────────────────────────────────────── + var msgBox = document.getElementById('auth-message'); + var signupForm = document.getElementById('signup-form'); + var signupSubmit = document.getElementById('signup-submit'); + var otpPanel = document.getElementById('otp-panel'); + var otpEmailDisp = document.getElementById('otp-email-display'); + var otpInput = document.getElementById('otp-input'); + var otpSubmit = document.getElementById('otp-submit'); + var otpResendBtn = document.getElementById('otp-resend'); + var otpResendTimer = document.getElementById('otp-resend-timer'); + var pwdInput = document.getElementById('signup-password'); + var eyeBtn = signupForm.querySelector('.auth-form__eye'); + + var stepInds = [ + document.getElementById('step-ind-1'), + document.getElementById('step-ind-2'), + document.getElementById('step-ind-3'), + ]; + + // ── Helpers ──────────────────────────────────────────────────── + function showMsg(text, type) { + msgBox.textContent = text; + msgBox.className = 'auth-message auth-message--' + type; + msgBox.style.display = 'block'; + } + function hideMsg() { + msgBox.style.display = 'none'; + } + + function setLoading(btn, loading) { + btn.disabled = loading; + btn.querySelector('.auth-form__submit-text').style.display = loading ? 'none' : ''; + btn.querySelector('.auth-form__submit-loader').style.display = loading + ? 'inline-flex' + : 'none'; + } + + function setStep(n) { + stepInds.forEach(function (el, i) { + el.classList.toggle('otp-steps__item--active', i + 1 === n); + el.classList.toggle('otp-steps__item--done', i + 1 < n); + }); + } + + // ── Password eye toggle ──────────────────────────────────────── eyeBtn.addEventListener('click', function () { var isText = pwdInput.type === 'text'; pwdInput.type = isText ? 'password' : 'text'; @@ -137,15 +254,29 @@ eyeBtn.querySelector('.eye-hide').style.display = isText ? 'none' : ''; }); - function showMsg(text, type) { - msgBox.textContent = text; - msgBox.className = 'auth-message auth-message--' + type; - msgBox.style.display = 'block'; + // ── Resend cooldown ──────────────────────────────────────────── + var resendTimeout = null; + function startResendCooldown(seconds) { + otpResendBtn.style.display = 'none'; + otpResendTimer.style.display = ''; + var remaining = seconds; + otpResendTimer.textContent = 'Resend in ' + remaining + 's'; + resendTimeout = setInterval(function () { + remaining--; + if (remaining <= 0) { + clearInterval(resendTimeout); + otpResendTimer.style.display = 'none'; + otpResendBtn.style.display = ''; + } else { + otpResendTimer.textContent = 'Resend in ' + remaining + 's'; + } + }, 1000); } - form.addEventListener('submit', function (e) { + // ── STEP 1 submit — validate + send_otp ─────────────────────── + signupForm.addEventListener('submit', function (e) { e.preventDefault(); - msgBox.style.display = 'none'; + hideMsg(); var username = document.getElementById('signup-username').value.trim(); var email = document.getElementById('signup-email').value.trim(); @@ -172,43 +303,143 @@ return; } - submitBtn.disabled = true; - submitBtn.querySelector('.auth-form__submit-text').style.display = 'none'; - submitBtn.querySelector('.auth-form__submit-loader').style.display = 'inline-flex'; + state.username = username; + state.email = email; + state.password = password; + + setLoading(signupSubmit, true); fetch(BACKEND, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', - body: JSON.stringify({ - action: 'signup', - username: username, - email: email, - password: password, - }), + body: JSON.stringify({ action: 'send_otp', email: email }), }) .then(function (r) { return r.json(); }) .then(function (data) { if (data.success) { - showMsg('Account created! Redirecting to login\u2026', 'success'); - setTimeout(function () { - window.location.href = '/login/'; - }, 1500); + // Switch to OTP panel + signupForm.style.display = 'none'; + otpEmailDisp.textContent = email; + otpPanel.style.display = ''; + otpInput.value = ''; + otpInput.focus(); + setStep(2); + startResendCooldown(60); + showMsg('Code sent! Check your inbox (and spam folder).', 'success'); } else { - showMsg(data.message || 'Sign-up failed. Please try again.', 'error'); + showMsg(data.message || 'Failed to send code. Please try again.', 'error'); } }) .catch(function () { showMsg('Network error. Please try again.', 'error'); }) .finally(function () { - submitBtn.disabled = false; - submitBtn.querySelector('.auth-form__submit-text').style.display = ''; - submitBtn.querySelector('.auth-form__submit-loader').style.display = 'none'; + setLoading(signupSubmit, false); }); }); + + // ── Resend button ────────────────────────────────────────────── + otpResendBtn.addEventListener('click', function () { + hideMsg(); + otpResendBtn.disabled = true; + + fetch(BACKEND, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'send_otp', email: state.email }), + }) + .then(function (r) { + return r.json(); + }) + .then(function (data) { + if (data.success) { + showMsg('New code sent!', 'success'); + startResendCooldown(60); + } else { + showMsg(data.message || 'Could not resend. Please try again.', 'error'); + otpResendBtn.disabled = false; + } + }) + .catch(function () { + showMsg('Network error. Please try again.', 'error'); + otpResendBtn.disabled = false; + }); + }); + + // ── STEP 2 submit — verify_otp then signup ──────────────────── + otpSubmit.addEventListener('click', function () { + hideMsg(); + var otp = otpInput.value.trim(); + + if (!/^\d{6}$/.test(otp)) { + showMsg('Please enter the 6-digit code from your email.', 'error'); + return; + } + + setLoading(otpSubmit, true); + + // 2a. Verify OTP → get token + fetch(BACKEND, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ action: 'verify_otp', email: state.email, otp: otp }), + }) + .then(function (r) { + return r.json(); + }) + .then(function (data) { + if (!data.success) { + showMsg(data.message || 'Invalid or expired code.', 'error'); + setLoading(otpSubmit, false); + return; + } + state.otpToken = data.otp_token; + + // 2b. Create account + return fetch(BACKEND, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + action: 'signup', + username: state.username, + email: state.email, + password: state.password, + otp_token: state.otpToken, + }), + }) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + if (d.success) { + setStep(3); + otpPanel.style.display = 'none'; + showMsg('Account created! Redirecting to login\u2026', 'success'); + setTimeout(function () { + window.location.href = '/login/'; + }, 1800); + } else { + showMsg(d.message || 'Sign-up failed. Please try again.', 'error'); + setLoading(otpSubmit, false); + } + }); + }) + .catch(function () { + showMsg('Network error. Please try again.', 'error'); + setLoading(otpSubmit, false); + }); + }); + + // Allow pressing Enter in OTP box to submit + otpInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter') otpSubmit.click(); + }); })(); {{ end }} diff --git a/static/css/auth-additions.css b/static/css/auth-additions.css index acc279b..f6e4094 100644 --- a/static/css/auth-additions.css +++ b/static/css/auth-additions.css @@ -198,3 +198,110 @@ [data-theme='light'] .auth-form__submit { color: #fff; } + +/* ── OTP step indicator ─────────────────────────────────────────────────── */ + +.otp-steps { + display: flex; + align-items: center; + margin-bottom: 1.6rem; +} + +.otp-steps__line { + flex: 1; + height: 2px; + background: var(--background-color1, #ccc); + transition: background 0.25s ease; +} + +.otp-steps__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; +} + +.otp-steps__num { + width: 1.8rem; + height: 1.8rem; + border-radius: 50%; + border: 2px solid var(--background-color1, #ccc); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.78rem; + font-weight: 700; + color: var(--foreground-color3, #888); + background: var(--background-color); + transition: + border-color 0.25s ease, + color 0.25s ease, + background 0.25s ease; +} + +.otp-steps__label { + font-size: 0.7rem; + color: var(--foreground-color3, #888); + white-space: nowrap; + transition: color 0.25s ease; +} + +/* Active step */ +.otp-steps__item--active .otp-steps__num { + border-color: var(--accent-color); + color: var(--accent-color); +} + +.otp-steps__item--active .otp-steps__label { + color: var(--accent-color); + font-weight: 600; +} + +/* Completed step */ +.otp-steps__item--done .otp-steps__num { + border-color: var(--accent-color); + background: var(--accent-color); + color: #fff; +} + +.otp-steps__item--done .otp-steps__label { + color: var(--accent-color); +} + +/* ── OTP panel ───────────────────────────────────────────────────────────── */ + +.otp-panel__hint { + font-size: 0.87rem; + line-height: 1.5; + color: var(--foreground-color); + margin: 0; +} + +.otp-panel__input { + letter-spacing: 0.25em; + font-size: 1.15rem; + text-align: center; +} + +.otp-panel__resend { + font-size: 0.82rem; + color: var(--foreground-color3, #888); + text-align: center; + margin: 0; +} + +.otp-panel__resend-btn { + background: none; + border: none; + padding: 0; + font: inherit; + font-size: 0.82rem; + color: var(--accent-color); + cursor: pointer; + font-weight: 600; + text-decoration: underline; +} + +.otp-panel__resend-btn:hover { + opacity: 0.8; +}