Release v2.2: admin auth settings, Passkey RP ID, Cloudflare and Google captcha

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-07 02:57:49 +03:00
parent 0a51001791
commit 0584ebdc74
18 changed files with 458 additions and 4 deletions
+53 -1
View File
@@ -2,7 +2,7 @@
{% block title %}Настройки — Админка{% endblock %}
{% block admin_title %}Настройки системы{% endblock %}
{% block admin_subtitle %}<p class="admin-main__subtitle">S3, SFTP, FTP, SMTP и лимиты загрузки</p>{% endblock %}
{% block admin_subtitle %}<p class="admin-main__subtitle">Авторизация, captcha, S3, SFTP, FTP, SMTP и лимиты загрузки</p>{% endblock %}
{% block admin_content %}
<form method="post" class="settings-form">
@@ -16,6 +16,58 @@
</div>
</div>
<div class="admin-panel" style="margin-top:24px">
<h2 class="admin-panel__title">Регистрация и авторизация</h2>
<label class="form-checkbox"><input type="checkbox" name="registration_enabled" {% if settings.registration_enabled %}checked{% endif %}><span>Разрешить регистрацию</span></label>
<label class="form-checkbox"><input type="checkbox" name="password_login_enabled" {% if settings.password_login_enabled %}checked{% endif %}><span>Вход по паролю</span></label>
<label class="form-checkbox"><input type="checkbox" name="passkey_enabled" {% if settings.passkey_enabled %}checked{% endif %}><span>Passkey (WebAuthn)</span></label>
</div>
<div class="admin-panel" style="margin-top:24px">
<h2 class="admin-panel__title">Passkey — RP ID и Origin</h2>
<p class="folder-hint">Для production укажите домен сайта. Значения из админки имеют приоритет над <code>.env</code>.</p>
<div class="settings-grid">
<div class="form-group"><label>RP ID (домен)</label><input type="text" name="webauthn_rp_id" value="{{ settings.webauthn_rp_id or '' }}" placeholder="example.com"></div>
<div class="form-group"><label>RP Name</label><input type="text" name="webauthn_rp_name" value="{{ settings.webauthn_rp_name or 'PhotoHost' }}"></div>
<div class="form-group"><label>Origin (полный URL)</label><input type="text" name="webauthn_origin" value="{{ settings.webauthn_origin or '' }}" placeholder="https://example.com"></div>
</div>
</div>
<div class="admin-panel" style="margin-top:24px">
<h2 class="admin-panel__title">Captcha</h2>
<div class="form-group">
<label for="captcha_provider">Провайдер</label>
<select id="captcha_provider" name="captcha_provider">
<option value="none" {% if settings.captcha_provider == 'none' %}selected{% endif %}>Отключена</option>
<option value="turnstile" {% if settings.captcha_provider == 'turnstile' %}selected{% endif %}>Cloudflare Turnstile</option>
<option value="recaptcha_v2" {% if settings.captcha_provider == 'recaptcha_v2' %}selected{% endif %}>Google reCAPTCHA v2</option>
<option value="recaptcha_v3" {% if settings.captcha_provider == 'recaptcha_v3' %}selected{% endif %}>Google reCAPTCHA v3</option>
</select>
</div>
<label class="form-checkbox"><input type="checkbox" name="captcha_on_login" {% if settings.captcha_on_login %}checked{% endif %}><span>На странице входа</span></label>
<label class="form-checkbox"><input type="checkbox" name="captcha_on_register" {% if settings.captcha_on_register %}checked{% endif %}><span>На странице регистрации</span></label>
<label class="form-checkbox"><input type="checkbox" name="captcha_on_forgot_password" {% if settings.captcha_on_forgot_password %}checked{% endif %}><span>На сбросе пароля</span></label>
<h3 class="admin-panel__subtitle" style="margin-top:16px">Cloudflare Turnstile</h3>
<div class="settings-grid">
<div class="form-group"><label>Site Key</label><input type="text" name="turnstile_site_key" value="{{ settings.turnstile_site_key or '' }}"></div>
<div class="form-group"><label>Secret Key</label><input type="password" name="turnstile_secret_key" placeholder="оставьте пустым, если не меняете"></div>
</div>
<h3 class="admin-panel__subtitle" style="margin-top:16px">Google reCAPTCHA v2</h3>
<div class="settings-grid">
<div class="form-group"><label>Site Key</label><input type="text" name="recaptcha_v2_site_key" value="{{ settings.recaptcha_v2_site_key or '' }}"></div>
<div class="form-group"><label>Secret Key</label><input type="password" name="recaptcha_v2_secret_key" placeholder="оставьте пустым, если не меняете"></div>
</div>
<h3 class="admin-panel__subtitle" style="margin-top:16px">Google reCAPTCHA v3</h3>
<div class="settings-grid">
<div class="form-group"><label>Site Key</label><input type="text" name="recaptcha_v3_site_key" value="{{ settings.recaptcha_v3_site_key or '' }}"></div>
<div class="form-group"><label>Secret Key</label><input type="password" name="recaptcha_v3_secret_key" placeholder="оставьте пустым, если не меняете"></div>
<div class="form-group"><label>Мин. score (01)</label><input type="number" step="0.1" min="0" max="1" name="recaptcha_v3_min_score" value="{{ settings.recaptcha_v3_min_score or 0.5 }}"></div>
</div>
</div>
<div class="admin-panel" style="margin-top:24px">
<h2 class="admin-panel__title">Amazon S3 / совместимое хранилище</h2>
<label class="form-checkbox"><input type="checkbox" name="s3_enabled" {% if settings.s3_enabled %}checked{% endif %}><span>Включить S3</span></label>
+5
View File
@@ -14,6 +14,7 @@
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email">
</div>
{% include "partials/captcha.html" %}
<button type="submit" class="btn btn--primary btn--full">Отправить ссылку</button>
</form>
<p class="auth-card__footer"><a href="{{ url_for('auth.login') }}">← Вернуться ко входу</a></p>
@@ -21,3 +22,7 @@
</div>
</section>
{% endblock %}
{% block scripts %}
{% include "partials/captcha_scripts.html" %}
{% endblock %}
+14 -1
View File
@@ -11,6 +11,7 @@
{% include "partials/alerts.html" %}
{% if auth_settings.password_login_enabled %}
<form method="post" class="auth-form">
<div class="form-group">
<label for="login">Логин или email</label>
@@ -24,16 +25,25 @@
<input type="checkbox" name="remember">
<span>Запомнить меня</span>
</label>
{% include "partials/captcha.html" %}
<button type="submit" class="btn btn--primary btn--full">Войти</button>
</form>
{% endif %}
{% if auth_settings.passkey_enabled %}
<button type="button" class="btn btn--ghost btn--full" id="passkeyLoginBtn" style="margin-top:12px">
Войти с Passkey
</button>
{% endif %}
<p class="auth-card__footer">
<a href="{{ url_for('auth.forgot_password') }}">Забыли пароль?</a> ·
{% if auth_settings.password_login_enabled %}
<a href="{{ url_for('auth.forgot_password') }}">Забыли пароль?</a>
{% if auth_settings.registration_enabled %} · {% endif %}
{% endif %}
{% if auth_settings.registration_enabled %}
<a href="{{ url_for('auth.register') }}">Зарегистрироваться</a>
{% endif %}
</p>
</div>
</div>
@@ -41,5 +51,8 @@
{% endblock %}
{% block scripts %}
{% include "partials/captcha_scripts.html" %}
{% if auth_settings.passkey_enabled %}
<script src="{{ url_for('static', filename='js/passkey.js') }}"></script>
{% endif %}
{% endblock %}
+5
View File
@@ -28,6 +28,7 @@
<label for="password2">Подтверждение пароля</label>
<input type="password" id="password2" name="password2" required minlength="6" autocomplete="new-password" placeholder="повторите пароль">
</div>
{% include "partials/captcha.html" %}
<button type="submit" class="btn btn--primary btn--full">Создать аккаунт</button>
</form>
@@ -38,3 +39,7 @@
</div>
</section>
{% endblock %}
{% block scripts %}
{% include "partials/captcha_scripts.html" %}
{% endblock %}
+2
View File
@@ -32,8 +32,10 @@
<a href="{{ url_for('auth.logout') }}" class="nav__link">Выйти</a>
{% else %}
<a href="{{ url_for('auth.login') }}" class="nav__link">Вход</a>
{% if auth_settings.registration_enabled %}
<a href="{{ url_for('auth.register') }}" class="nav__link nav__link--accent">Регистрация</a>
{% endif %}
{% endif %}
</nav>
</div>
</header>
+7
View File
@@ -54,6 +54,7 @@
</div>
<div class="auth-card auth-card--wide profile-card">
{% if auth_settings.passkey_enabled %}
<h2 class="profile-card__title">Passkey</h2>
<p class="profile-card__hint">Вход без пароля через Face ID, Touch ID, Windows Hello или ключ безопасности.</p>
@@ -82,6 +83,10 @@
<input type="text" id="passkeyName" value="Моё устройство" maxlength="120">
</div>
<button type="button" class="btn btn--ghost" id="addPasskeyBtn">Добавить passkey</button>
{% else %}
<h2 class="profile-card__title">Passkey</h2>
<p class="profile-card__hint">Passkey отключён администратором сайта.</p>
{% endif %}
</div>
<div class="auth-card auth-card--wide profile-card">
@@ -149,5 +154,7 @@
{% endblock %}
{% block scripts %}
{% if auth_settings.passkey_enabled %}
<script src="{{ url_for('static', filename='js/passkey.js') }}"></script>
{% endif %}
{% endblock %}
+12
View File
@@ -0,0 +1,12 @@
{% if captcha_config %}
<div class="form-group captcha-widget">
{% if captcha_config.provider == 'turnstile' %}
<div class="cf-turnstile" data-sitekey="{{ captcha_config.site_key }}"></div>
{% elif captcha_config.provider == 'recaptcha_v2' %}
<div class="g-recaptcha" data-sitekey="{{ captcha_config.site_key }}"></div>
{% elif captcha_config.provider == 'recaptcha_v3' %}
<input type="hidden" name="g-recaptcha-response" id="recaptchaV3Token" value="">
<p class="folder-hint">Защита reCAPTCHA v3 активна</p>
{% endif %}
</div>
{% endif %}
@@ -0,0 +1,36 @@
{% if captcha_config %}
{% if captcha_config.provider == 'turnstile' %}
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
{% elif captcha_config.provider == 'recaptcha_v2' %}
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
{% elif captcha_config.provider == 'recaptcha_v3' %}
<script src="https://www.google.com/recaptcha/api.js?render={{ captcha_config.site_key }}"></script>
<script>
(function () {
function bindRecaptchaV3() {
const form = document.querySelector(".auth-form");
const tokenInput = document.getElementById("recaptchaV3Token");
if (!form || !tokenInput || !window.grecaptcha) return;
form.addEventListener("submit", function (event) {
if (tokenInput.value) return;
event.preventDefault();
grecaptcha.ready(function () {
grecaptcha.execute("{{ captcha_config.site_key }}", { action: "{{ captcha_config.action }}" })
.then(function (token) {
tokenInput.value = token;
form.submit();
});
});
});
}
if (window.grecaptcha) {
bindRecaptchaV3();
} else {
window.addEventListener("load", bindRecaptchaV3);
}
})();
</script>
{% endif %}
{% endif %}