new: usr: implement the 2FA authentication (TOTP and backup codes) #4
This commit is contained in:
54
templates/Security/2fa.html.twig
Normal file
54
templates/Security/2fa.html.twig
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends 'Game/index.html.twig' %}
|
||||
|
||||
{% block title %} - Two-Factor Authentication{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-title">Two-Factor Authentication</h2>
|
||||
<p class="auth-sub">Enter the 6-digit code from your authenticator app</p>
|
||||
|
||||
{% if authenticationError is defined and authenticationError %}
|
||||
<div class="auth-error">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
{{ authenticationError|trans({}, 'SchebTwoFactorBundle') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form class="auth-form" method="post" action="{{ path('2fa_login_check') }}">
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="auth-code" class="auth-label">Authentication Code</label>
|
||||
<div class="auth-input-wrap">
|
||||
<i class="fa fa-shield auth-input-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="auth-code"
|
||||
name="_auth_code"
|
||||
class="auth-input auth-input--code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxlength="8"
|
||||
autocomplete="one-time-code"
|
||||
autofocus
|
||||
required
|
||||
placeholder="000000"
|
||||
/>
|
||||
</div>
|
||||
<p class="auth-field-hint">Or enter one of your backup codes.</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-submit">
|
||||
<i class="fa fa-check"></i> Verify
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ path('MineSeekerBundle_logout') }}" class="auth-cancel-standalone">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('logout') }}"/>
|
||||
<button type="submit" class="auth-cancel auth-cancel--block">
|
||||
<i class="fa fa-sign-out"></i> Cancel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
63
templates/Security/2fa_setup.html.twig
Normal file
63
templates/Security/2fa_setup.html.twig
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends 'Game/index.html.twig' %}
|
||||
|
||||
{% block title %} - Enable Two-Factor Authentication{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="auth-page">
|
||||
<div class="auth-card auth-card--wide">
|
||||
<h2 class="auth-title">Enable Two-Factor Authentication</h2>
|
||||
<p class="auth-sub">Scan the QR code with your authenticator app</p>
|
||||
|
||||
<div class="twofa-setup">
|
||||
<div class="twofa-setup__qr">
|
||||
<img
|
||||
src="{{ path('MineSeekerBundle_2fa_qr_code') }}"
|
||||
alt="TOTP QR Code"
|
||||
width="220"
|
||||
height="220"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="twofa-setup__manual">
|
||||
<p class="twofa-setup__manual-label">Can't scan? Enter this key manually:</p>
|
||||
<code class="twofa-setup__secret">{{ pending_secret }}</code>
|
||||
</div>
|
||||
|
||||
<form class="auth-form" method="post" action="{{ path('MineSeekerBundle_2fa_enable') }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('2fa_enable') }}"/>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="auth-code" class="auth-label">Verification Code</label>
|
||||
<div class="auth-input-wrap">
|
||||
<i class="fa fa-shield auth-input-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="auth-code"
|
||||
name="_auth_code"
|
||||
class="auth-input auth-input--code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
autofocus
|
||||
required
|
||||
placeholder="000000"
|
||||
/>
|
||||
</div>
|
||||
<p class="auth-field-hint">Enter the 6-digit code from your app to confirm setup.</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-submit">
|
||||
<i class="fa fa-check"></i> Activate 2FA
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="auth-back">
|
||||
<a href="{{ path('MineSeekerBundle_profile_security') }}">
|
||||
<i class="fa fa-chevron-left"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -45,12 +45,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<a href="{{ path('MineSeekerBundle_profile_security') }}" class="profile-action-btn">
|
||||
<i class="fa fa-lock"></i> Security Settings
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if recent|length > 0 %}
|
||||
<div class="profile-section">
|
||||
<h2 class="profile-section__title">
|
||||
|
||||
@@ -4,18 +4,6 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="profile-page">
|
||||
<div class="profile-header">
|
||||
<div class="profile-avatar">
|
||||
{{ app.user.username|slice(0, 2)|upper }}
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<h1 class="profile-name">{{ app.user.username }}</h1>
|
||||
<p class="profile-role">
|
||||
<i class="fa fa-lock"></i> Security Settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<a href="{{ path('MineSeekerBundle_profile') }}" class="profile-action-btn">
|
||||
<i class="fa fa-chevron-left"></i> Back to Profile
|
||||
@@ -39,6 +27,83 @@
|
||||
}|json_encode|e('html') }}"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<h2 class="profile-section__title">
|
||||
<i class="fa fa-shield"></i> Two-Factor Authentication
|
||||
</h2>
|
||||
<p class="profile-section__description">
|
||||
Add an extra layer of security by requiring a one-time code from your authenticator app each time you sign in
|
||||
with a password.
|
||||
</p>
|
||||
|
||||
{% set newBackupCodes = app.flashes('2fa_backup_codes') %}
|
||||
|
||||
{% if isTotpEnabled %}
|
||||
<div class="twofa-status twofa-status--enabled">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
Two-factor authentication is <strong>active</strong>.
|
||||
</div>
|
||||
|
||||
{% if newBackupCodes|length > 0 %}
|
||||
<div class="twofa-backup-reveal">
|
||||
<p class="twofa-backup-reveal__warning">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
Save these backup codes now — they will not be shown again.
|
||||
</p>
|
||||
<div class="twofa-backup-reveal__grid">
|
||||
{% for code in newBackupCodes[0] %}
|
||||
<code class="twofa-backup-code">{{ code }}</code>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="twofa-actions">
|
||||
<form method="post" action="{{ path('MineSeekerBundle_2fa_disable') }}" class="twofa-actions__form">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('2fa_disable') }}"/>
|
||||
<button type="submit" class="btn btn--danger btn--sm">
|
||||
<i class="fa fa-times"></i> Disable 2FA
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="twofa-backup-meta">
|
||||
<span class="twofa-backup-meta__count">
|
||||
<i class="fa fa-life-ring"></i>
|
||||
{{ backupCodesCount }} backup code{{ backupCodesCount != 1 ? 's' : '' }} remaining
|
||||
</span>
|
||||
<form method="post" action="{{ path('MineSeekerBundle_2fa_backup_regenerate') }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('2fa_backup_regen') }}"/>
|
||||
<button type="submit" class="btn btn--secondary btn--sm">
|
||||
<i class="fa fa-refresh"></i> Regenerate codes
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="twofa-status twofa-status--disabled">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
Two-factor authentication is <strong>not enabled</strong>.
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ path('MineSeekerBundle_2fa_prepare') }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('2fa_prepare') }}"/>
|
||||
<button type="submit" class="btn btn--primary">
|
||||
<i class="fa fa-shield"></i> Enable 2FA
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<h2 class="profile-section__title">
|
||||
<i class="fa fa-key"></i> Password changing
|
||||
</h2>
|
||||
<p class="profile-section__description">
|
||||
The password changing only possible through the Forgot password functionality on the Login page.
|
||||
The system would log you out anyway after requesting a new password, so this feature wasn't implemented here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user