{% extends "base.twig" %}
{% block title %}{{ t('ui.register.title', {}, 'common') }} - {{ parent() }}{% endblock %}
{% block content %}
{% if register_splash %}
<div class="row justify-content-center mb-3">
<div class="col-md-6 col-lg-6">
<div class="card">
<div class="card-body prose">
{{ register_splash|raw }}
</div>
</div>
</div>
</div>
{% endif %}
<div class="row justify-content-center">
<div class="col-md-6 col-lg-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-user-plus"></i>
{{ t('ui.register.create_account', {}, 'common') }}
</h4>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>{{ t('ui.register.approval_required', {}, 'common') }}</strong> {{ t('ui.register.approval_required_help', {}, 'common') }}
{{ t('ui.register.subject_to_rules', {}, 'common') }} <a href="#" data-bs-toggle="modal" data-bs-target="#houseRulesModal">{{ t('ui.about.house_rules', {}, 'common') }}</a>.
<br><small class="mt-1 d-inline-block"><i class="fas fa-envelope me-1"></i>{{ t('ui.register.email_notification_note', {}, 'common') }}</small>
</div>
<div id="alert-container"></div>
<form id="registerForm">
{# Anti-spam honeypot field - hidden from humans, visible to bots #}
<div style="position: absolute; left: -9999px; top: -9999px;" aria-hidden="true">
<label for="website">{{ t('ui.register.honeypot_website_leave_blank', {}, 'common') }}</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<div class="mb-3">
<label for="username" class="form-label">{{ t('ui.register.username', {}, 'common') }}</label>
<input type="text" class="form-control" id="username" name="username" required
pattern="[a-zA-Z0-9_]{3,20}" title="{{ t('ui.register.username_help', {}, 'common') }}">
<div class="form-text">{{ t('ui.register.username_help', {}, 'common') }}</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ t('ui.register.password', {}, 'common') }}</label>
<input type="password" class="form-control" id="password" name="password" required
minlength="8">
<div class="password-strength-meter mt-2" id="passwordStrengthMeter" style="display: none;">
<div class="progress" style="height: 8px;">
<div class="progress-bar" id="passwordStrengthBar" role="progressbar"
style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<small class="form-text" id="passwordStrengthText"></small>
</div>
<div class="form-text">{{ t('ui.register.password_help', {}, 'common') }}</div>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">{{ t('ui.register.confirm_password', {}, 'common') }}</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">{{ t('ui.register.email_address', {}, 'common') }}</label>
<input type="email" class="form-control" id="email" name="email">
<div class="form-text">{{ t('ui.register.email_help', {}, 'common') }}</div>
</div>
<div class="mb-3">
<label for="realName" class="form-label">{{ t('ui.register.real_name', {}, 'common') }}</label>
<input type="text" class="form-control" id="realName" name="real_name" required>
<div class="form-text">{{ t('ui.register.real_name_help', {}, 'common') }}</div>
</div>
<div class="mb-3">
<label for="location" class="form-label">{{ t('ui.register.location', {}, 'common') }}</label>
<input type="text" class="form-control" id="location" name="location" placeholder="{{ t('ui.register.location_placeholder', {}, 'common') }}">
<div class="form-text">{{ t('ui.register.location_help', {}, 'common') }}</div>
</div>
<div class="mb-3">
<label for="reason" class="form-label">{{ t('ui.register.reason_for_joining', {}, 'common') }}</label>
<textarea class="form-control" id="reason" name="reason" rows="3"
placeholder="{{ t('ui.register.reason_placeholder', {}, 'common') }}"></textarea>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus"></i>
{{ t('ui.register.submit_registration', {}, 'common') }}
</button>
</div>
</form>
</div>
</div>
<div class="text-center mt-3">
<small class="text-muted">
{{ t('ui.register.already_have_account', {}, 'common') }} <a href="/login" class="text-decoration-none">{{ t('ui.register.sign_in', {}, 'common') }}</a>
</small>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
return payload && payload.error ? payload.error : fallback;
}
function showAlert(message, type) {
const icon = type === 'success' ? 'check-circle' : type === 'danger' ? 'exclamation-triangle' : 'info-circle';
const alert = $(`
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
<i class="fas fa-${icon} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('#alert-container').append(alert);
}
// Password strength meter
function calculatePasswordStrength(password) {
if (!password) return { score: 0, text: '', color: '', width: 0 };
let score = 0;
// Length scoring
if (password.length >= 8) score += 20;
if (password.length >= 12) score += 20;
if (password.length >= 16) score += 10;
// Character variety
if (/[a-z]/.test(password)) score += 10; // lowercase
if (/[A-Z]/.test(password)) score += 10; // uppercase
if (/[0-9]/.test(password)) score += 10; // numbers
if (/[^a-zA-Z0-9]/.test(password)) score += 20; // special characters
// Bonus for good combinations
const hasLower = /[a-z]/.test(password);
const hasUpper = /[A-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[^a-zA-Z0-9]/.test(password);
const varietyCount = [hasLower, hasUpper, hasNumber, hasSpecial].filter(Boolean).length;
if (varietyCount >= 3) score += 10;
// Penalize common patterns
if (/^[a-z]+$/.test(password)) score -= 10; // all lowercase
if (/^[A-Z]+$/.test(password)) score -= 10; // all uppercase
if (/^[0-9]+$/.test(password)) score -= 10; // all numbers
if (/(.)\1{2,}/.test(password)) score -= 10; // repeated characters (aaa, 111)
if (/^(abc|123|qwerty|password|admin)/i.test(password)) score -= 20; // common sequences
// Cap score at 100
score = Math.max(0, Math.min(100, score));
// Determine strength level
let text, color;
if (score < 30) {
text = uiT('ui.register.password_strength.weak', 'Weak - Add more variety');
color = 'bg-danger';
} else if (score < 60) {
text = uiT('ui.register.password_strength.fair', 'Fair - Could be stronger');
color = 'bg-warning';
} else if (score < 80) {
text = uiT('ui.register.password_strength.good', 'Good - Nice password!');
color = 'bg-info';
} else {
text = uiT('ui.register.password_strength.strong', 'Strong - Excellent!');
color = 'bg-success';
}
return { score, text, color, width: score };
}
function updatePasswordStrength() {
const password = $('#password').val();
const meter = $('#passwordStrengthMeter');
const bar = $('#passwordStrengthBar');
const text = $('#passwordStrengthText');
if (!password) {
meter.hide();
return;
}
meter.show();
const strength = calculatePasswordStrength(password);
// Update progress bar
bar.removeClass('bg-danger bg-warning bg-info bg-success')
.addClass(strength.color)
.css('width', strength.width + '%')
.attr('aria-valuenow', strength.score);
// Update text
text.text(strength.text);
}
// Update strength meter on password input
$('#password').on('input', updatePasswordStrength);
$('#registerForm').on('submit', function(e) {
e.preventDefault();
// Clear previous alerts
$('#alert-container').empty();
// Anti-spam: Check honeypot field
if ($('#website').val() !== '') {
// Silent rejection - don't tell bots why they failed
return false;
}
// Validate passwords match
if ($('#password').val() !== $('#confirmPassword').val()) {
showAlert(uiT('ui.register.passwords_do_not_match', 'Passwords do not match.'), 'danger');
return;
}
// Disable submit button
const submitBtn = $('button[type="submit"]');
const originalText = submitBtn.html();
submitBtn.prop('disabled', true).html(`<i class="fas fa-spinner fa-spin"></i> ${uiT('ui.register.submitting', 'Submitting...')}`);
// Submit registration
$.ajax({
url: '/api/register',
method: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function(response) {
if (response.success) {
const successMessage = window.getApiMessage
? window.getApiMessage(response, uiT('ui.register.submitted_success', 'Registration submitted successfully! You will be notified when your account is approved.'))
: uiT('ui.register.submitted_success', 'Registration submitted successfully! You will be notified when your account is approved.');
showAlert(successMessage, 'success');
// Hide the form after successful registration
$('#registerForm').hide();
$('.card-body .alert-info').hide();
// Show link to login
$('#alert-container').append(
'<div class="mt-3 text-center">' +
`<a href="/login" class="btn btn-primary"><i class="fas fa-sign-in-alt"></i> ${uiT('ui.register.go_to_login', 'Go to Login')}</a>` +
'</div>'
);
} else {
showAlert(apiError(response, uiT('errors.register.failed', 'Registration failed. Please try again.')), 'danger');
}
},
error: function(xhr) {
const errorMsg = apiError(xhr.responseJSON, uiT('errors.register.failed', 'Registration failed. Please try again.'));
showAlert(errorMsg, 'danger');
},
complete: function() {
submitBtn.prop('disabled', false).html(originalText);
}
});
});
});
</script>
{% endblock %}
|