feat: implement login modal and altcha captcha for registration

- Added a login modal to the home page with mobile and password fields.
- Integrated altcha captcha for the registration process to enhance security.
- Updated the registration submission logic to include altcha payload.
- Improved error handling and user feedback for both login and registration processes.
This commit is contained in:
hamed
2026-07-10 10:55:37 +03:30
parent 10b0743d9a
commit aded526718
7 changed files with 923 additions and 709 deletions
+150 -2
View File
@@ -89,7 +89,7 @@
<svg viewbox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
</button>
<button class="btn btn-ghost" id="openRegModal" style="font-size:13px;padding:8px 20px">ثبت نام</button>
<a href="/admin" class="btn btn-blue" style="font-size:13px;padding:8px 20px;text-decoration:none">ورود به پنل</a>
<button class="btn btn-blue" id="openLoginModal" style="font-size:13px;padding:8px 20px">ورود به پنل</button>
</div>
</div>
</header>
@@ -557,6 +557,9 @@
</div>
</div>
<!-- ALTCHA captcha — proof-of-work در پس‌زمینه (بدون تعامل کاربر) -->
<altcha-widget id="regAltcha" challengeurl="/api/v1/altcha/challenge" auto="onload" style="display:block;margin-top:16px"></altcha-widget>
<!-- Error / Success -->
<div id="regMsg" style="display:none;margin-top:14px;padding:12px 16px;border-radius:10px;font-size:13px"></div>
@@ -593,6 +596,15 @@ var overlay = document.getElementById('regOverlay');
var submitBtn = document.getElementById('regSubmitBtn');
var selectedType = null;
// payload حل‌شده‌ی ALTCHA (یک‌بارمصرف؛ در prod الزامی است)
var regAltchaPayload = '';
var regAltchaEl = document.getElementById('regAltcha');
if (regAltchaEl)
regAltchaEl.addEventListener('statechange', function (e) {
if (e.detail && e.detail.state === 'verified')
regAltchaPayload = e.detail.payload;
});
function openRegModal() {
overlay.style.display = 'block';
document.body.style.overflow = 'hidden';
@@ -674,7 +686,8 @@ body: JSON.stringify(
type: selectedType,
name: name,
mobile: mobile,
info: info || null
info: info || null,
altcha: regAltchaPayload
}
)
}).then(function (r) {
@@ -692,15 +705,24 @@ showMsg(errMsg, 'error');
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
submitBtn.textContent = 'ارسال درخواست';
resetRegAltcha();
}
}).catch(function () {
showMsg('خطا در اتصال به سرور', 'error');
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
submitBtn.textContent = 'ارسال درخواست';
resetRegAltcha();
});
};
// challenge یک‌بارمصرف است؛ بعد از خطا یک challenge تازه بگیر.
function resetRegAltcha() {
regAltchaPayload = '';
if (regAltchaEl && typeof regAltchaEl.reset === 'function')
regAltchaEl.reset();
}
function showMsg(text, type) {
var msg = document.getElementById('regMsg');
msg.textContent = text;
@@ -709,6 +731,132 @@ msg.style.background = type === 'success' ? '#dcfce7' : '#fef2f2';
msg.style.color = type === 'success' ? '#16a34a' : '#ef4444';
msg.style.border = type === 'success' ? '1px solid #bbf7d0' : '1px solid #fecaca';
}
})();
</script>
<!-- ===================== Panel Login Modal ===================== -->
<div id="loginOverlay" style="display:none;position:fixed;inset:0;background:oklch(0.2 0.05 285 / 0.55);z-index:900;backdrop-filter:blur(4px);overflow-y:auto;padding:24px 16px" onclick="if(event.target===this)closeLoginModal()">
<div style="background:#fff;border-radius:20px;max-width:420px;margin:auto;padding:32px 28px;position:relative;box-shadow:0 24px 60px oklch(0.3 0.1 285 / 0.22)">
<button onclick="closeLoginModal()" aria-label="بستن" style="position:absolute;top:16px;left:20px;background:none;border:none;cursor:pointer;font-size:22px;color:var(--text-2);line-height:1">×</button>
<h2 style="margin:0 0 6px;font-size:20px;font-weight:800;color:var(--ink)">ورود به پنل</h2>
<p style="margin:0 0 24px;font-size:13px;color:var(--text-2)">با شماره موبایل و رمز عبور وارد شوید</p>
<div style="display:flex;flex-direction:column;gap:14px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;color:var(--ink)">شماره موبایل</label>
<input id="loginMobile" type="tel" placeholder="09xxxxxxxxx" dir="ltr" autocomplete="username" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;outline:none;box-sizing:border-box;color:var(--ink);text-align:right"/>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;color:var(--ink)">رمز عبور</label>
<input id="loginPass" type="password" placeholder="••••••••" autocomplete="current-password" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;outline:none;box-sizing:border-box;color:var(--ink)"/>
</div>
</div>
<div id="loginMsg" style="display:none;margin-top:14px;padding:12px 16px;border-radius:10px;font-size:13px"></div>
<button id="loginSubmitBtn" onclick="submitLogin()" style="margin-top:20px;width:100%;background:var(--blue);color:#fff;border:none;border-radius:12px;padding:13px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;transition:opacity .2s">
ورود به سیستم
</button>
<p style="text-align:center;font-size:12px;color:var(--text-2);margin:12px 0 0">حساب ندارید؟ از دکمه‌ی «ثبت نام» استفاده کنید</p>
</div>
</div>
<script>
(function () {
var loginOverlay = document.getElementById('loginOverlay');
var loginBtn = document.getElementById('loginSubmitBtn');
var openBtn = document.getElementById('openLoginModal');
if (openBtn)
openBtn.addEventListener('click', function () {
loginOverlay.style.display = 'block';
document.body.style.overflow = 'hidden';
});
window.closeLoginModal = function () {
loginOverlay.style.display = 'none';
document.body.style.overflow = '';
};
function loginMsg(text, type) {
var m = document.getElementById('loginMsg');
m.textContent = text;
m.style.display = 'block';
m.style.background = type === 'success' ? '#dcfce7' : '#fef2f2';
m.style.color = type === 'success' ? '#16a34a' : '#ef4444';
m.style.border = type === 'success' ? '1px solid #bbf7d0' : '1px solid #fecaca';
}
// state را دقیقاً به شکل store ادمین (کلید clinicpro-auth) می‌نویسیم تا SPA پس از ری‌دایرکت لاگین بماند.
function persistAuth(tokens, info) {
var d = (info && info.data) ? info.data : {};
var state = {
token: tokens.access_token,
refreshToken: tokens.refresh_token || null,
isAuthenticated: true,
userUuid: d.uuid || null,
userName: d.realName || null,
primaryRole: d.primary_role || null,
dbUuid: d.db_uuid || null,
dbKey: d.db_key || null,
doctorUuid: d.doctor_uuid || null,
context: d.context || null,
availableContexts: d.available_contexts || []
};
localStorage.setItem('clinicpro-auth', JSON.stringify({ state: state, version: 0 }));
}
window.submitLogin = function () {
var mobile = document.getElementById('loginMobile').value.trim();
var pass = document.getElementById('loginPass').value;
if (! /^09[0-9]{9}$/.test(mobile)) {
loginMsg('شماره موبایل معتبر نیست', 'error');
return;
}
if (! pass) {
loginMsg('رمز عبور را وارد کنید', 'error');
return;
}
loginBtn.disabled = true;
loginBtn.textContent = 'در حال ورود...';
fetch('/api/v1/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mobile_number: mobile, password: pass })
}).then(function (r) {
return r.json().then(function (j) { return { ok: r.ok, body: j }; });
}).then(function (res) {
if (! res.ok || ! res.body.access_token) {
var err = (res.body.errors && res.body.errors[0]) ? res.body.errors[0].message : 'نام کاربری یا رمز عبور اشتباه است';
loginMsg(err, 'error');
loginBtn.disabled = false;
loginBtn.textContent = 'ورود به سیستم';
return;
}
var tokens = res.body;
// اطلاعات کاربر را می‌گیریم تا state کامل ذخیره شود؛ اگر نشد، با state حداقلی ادامه بده.
fetch('/oauth/userinfo', { headers: { 'Authorization': 'Bearer ' + tokens.access_token } })
.then(function (r) { return r.json(); })
.catch(function () { return null; })
.then(function (info) {
persistAuth(tokens, info);
loginMsg('خوش آمدید، در حال انتقال به پنل...', 'success');
window.location.href = '/admin';
});
}).catch(function () {
loginMsg('خطا در اتصال به سرور', 'error');
loginBtn.disabled = false;
loginBtn.textContent = 'ورود به سیستم';
});
};
document.getElementById('loginPass').addEventListener('keydown', function (e) {
if (e.key === 'Enter') submitLogin();
});
})();
</script>