first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block title %}Forgot Password - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Forgot Password</h1>
<p class="page-subtitle">Recover your account access</p>
<div class="decorative-line"></div>
<!-- Message display area -->
<div id="forgot-password-messages"></div>
<form
id="forgot-password-form"
hx-post="{{ api_base_url }}/api/v1/auth/forgot-password"
hx-ext="json-enc"
hx-target="#forgot-password-messages"
hx-swap="innerHTML"
>
<div class="form-group">
<label class="form-label" for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="adventurer@realm.com"
required
autocomplete="email"
>
<span id="email-error" class="field-error"></span>
</div>
<button type="submit" class="btn btn-primary">
Send Reset Link
<span class="htmx-indicator loading-spinner"></span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.location.href='{{ url_for('auth_views.login') }}'">
Back to Login
</button>
</form>
<div class="form-links">
<div class="divider">or</div>
<a href="{{ url_for('auth_views.register') }}" class="form-link">Don't have an account? Register here</a>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Handle form submission response
document.getElementById('forgot-password-form').addEventListener('htmx:afterSwap', function(event) {
const response = event.detail.xhr.responseText;
try {
const data = JSON.parse(response);
// Check if request was successful
if (data.status === 200) {
// Show success message
document.getElementById('forgot-password-messages').innerHTML =
'<div class="success-message">' +
'If an account exists with this email, you will receive a password reset link shortly. ' +
'Please check your inbox and spam folder.' +
'</div>';
// Clear form
document.getElementById('forgot-password-form').reset();
} else {
// Display error message
if (data.error && data.error.details && data.error.details.email) {
document.getElementById('email-error').textContent = data.error.details.email;
} else if (data.error && data.error.message) {
document.getElementById('forgot-password-messages').innerHTML =
'<div class="error-message">' + data.error.message + '</div>';
}
}
} catch (e) {
console.error('Error parsing response:', e);
}
});
// Clear errors when user starts typing
document.getElementById('email').addEventListener('input', function() {
document.getElementById('email-error').textContent = '';
document.getElementById('forgot-password-messages').innerHTML = '';
});
</script>
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Login - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Login</h1>
<p class="page-subtitle">Enter the realm, brave adventurer</p>
<div class="decorative-line"></div>
<!-- Error display area -->
{% if error %}
<div class="error-message">{{ error }}</div>
{% endif %}
<form
id="login-form"
method="POST"
action="{{ url_for('auth_views.login') }}"
>
<div class="form-group">
<label class="form-label" for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="adventurer@realm.com"
required
autocomplete="email"
>
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Enter your secret passphrase"
required
autocomplete="current-password"
>
</div>
<div class="checkbox-group">
<input type="checkbox" id="remember_me" name="remember_me" class="checkbox-input">
<label for="remember_me" class="checkbox-label">Remember me for 30 days</label>
</div>
<button type="submit" class="btn btn-primary">
Enter the Realm
<span class="htmx-indicator loading-spinner"></span>
</button>
</form>
<div class="form-links">
<a href="{{ url_for('auth_views.forgot_password') }}" class="form-link">Forgot your password?</a>
<div class="divider">or</div>
<a href="{{ url_for('auth_views.register') }}" class="form-link">Don't have an account? Register here</a>
</div>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

View File

@@ -0,0 +1,230 @@
{% extends "base.html" %}
{% block title %}Register - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Register</h1>
<p class="page-subtitle">Begin your epic journey</p>
<div class="decorative-line"></div>
<!-- Error display area for HTMX responses -->
<div id="register-errors"></div>
<form
id="register-form"
hx-post="{{ api_base_url }}/api/v1/auth/register"
hx-ext="json-enc"
hx-target="#register-errors"
hx-swap="innerHTML"
>
<div class="form-group">
<label class="form-label" for="name">Character Name</label>
<input
type="text"
id="name"
name="name"
class="form-input"
placeholder="Enter your hero's name"
required
minlength="3"
maxlength="50"
autocomplete="name"
>
<span id="name-error" class="field-error"></span>
</div>
<div class="form-group">
<label class="form-label" for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="adventurer@realm.com"
required
autocomplete="email"
>
<span id="email-error" class="field-error"></span>
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Create a strong passphrase"
required
autocomplete="new-password"
>
<div class="password-strength">
<div class="strength-bar">
<div id="strength-fill" class="strength-fill"></div>
</div>
<span id="strength-text" class="strength-text">Enter a password to see strength</span>
</div>
<span id="password-error" class="field-error"></span>
</div>
<div class="form-group">
<label class="form-label" for="confirm-password">Confirm Password</label>
<input
type="password"
id="confirm-password"
name="confirm_password"
class="form-input"
placeholder="Re-enter your passphrase"
required
autocomplete="new-password"
>
<span id="confirm-password-error" class="field-error"></span>
</div>
<button type="submit" class="btn btn-primary">
Begin Adventure
<span class="htmx-indicator loading-spinner"></span>
</button>
</form>
<div class="form-links">
<a href="{{ url_for('auth_views.login') }}" class="form-link">Already have an account? Login here</a>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Password strength indicator
function updatePasswordStrength(password) {
const fill = document.getElementById('strength-fill');
const text = document.getElementById('strength-text');
if (!password) {
fill.className = 'strength-fill';
text.textContent = 'Enter a password to see strength';
text.style.color = 'var(--text-muted)';
return;
}
let strength = 0;
// Check length
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
// Check for uppercase
if (/[A-Z]/.test(password)) strength++;
// Check for lowercase
if (/[a-z]/.test(password)) strength++;
// Check for numbers
if (/[0-9]/.test(password)) strength++;
// Check for special characters
if (/[^A-Za-z0-9]/.test(password)) strength++;
if (strength <= 2) {
fill.className = 'strength-fill strength-weak';
text.textContent = 'Weak - Add more complexity';
text.style.color = 'var(--accent-red)';
} else if (strength <= 4) {
fill.className = 'strength-fill strength-medium';
text.textContent = 'Medium - Almost there';
text.style.color = 'var(--accent-gold)';
} else {
fill.className = 'strength-fill strength-strong';
text.textContent = 'Strong - Excellent password';
text.style.color = 'var(--accent-green)';
}
}
// Attach password strength checker
document.getElementById('password').addEventListener('input', function() {
updatePasswordStrength(this.value);
});
// Validate password confirmation
document.getElementById('confirm-password').addEventListener('input', function() {
const password = document.getElementById('password').value;
const confirmPassword = this.value;
const errorSpan = document.getElementById('confirm-password-error');
if (confirmPassword && password !== confirmPassword) {
errorSpan.textContent = 'Passwords do not match';
} else {
errorSpan.textContent = '';
}
});
// Handle HTMX response
document.body.addEventListener('htmx:afterRequest', function(event) {
// Only handle register form
if (!event.detail.elt || event.detail.elt.id !== 'register-form') return;
try {
const xhr = event.detail.xhr;
// Check if registration was successful
if (xhr.status === 201) {
const data = JSON.parse(xhr.responseText);
// Show success message
document.getElementById('register-errors').innerHTML = '<div class="success-message">' +
'Registration successful! Please check your email to verify your account.' +
'</div>';
// Clear form
document.getElementById('register-form').reset();
updatePasswordStrength('');
// Redirect to login after delay
setTimeout(function() {
window.location.href = '{{ url_for("auth_views.login") }}';
}, 2000);
} else {
// Handle error
const data = JSON.parse(xhr.responseText);
let errorHtml = '<div class="error-message">';
if (data.error && data.error.details) {
for (const [field, message] of Object.entries(data.error.details)) {
errorHtml += `<strong>${field}:</strong> ${message}<br>`;
// Also show inline error
const errorSpan = document.getElementById(field + '-error');
if (errorSpan) {
errorSpan.textContent = message;
}
}
} else if (data.error && data.error.message) {
errorHtml += data.error.message;
} else {
errorHtml += 'An error occurred. Please try again.';
}
errorHtml += '</div>';
document.getElementById('register-errors').innerHTML = errorHtml;
}
} catch (e) {
console.error('Error parsing response:', e);
document.getElementById('register-errors').innerHTML =
'<div class="error-message">An unexpected error occurred.</div>';
}
});
// Clear errors when user starts typing
document.querySelectorAll('.form-input').forEach(input => {
input.addEventListener('input', function() {
const fieldName = this.name;
const errorSpan = document.getElementById(fieldName + '-error');
if (errorSpan) {
errorSpan.textContent = '';
}
// Clear general errors
document.getElementById('register-errors').innerHTML = '';
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,203 @@
{% extends "base.html" %}
{% block title %}Reset Password - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Reset Password</h1>
<p class="page-subtitle">Create a new password for your account</p>
<div class="decorative-line"></div>
<!-- Message display area -->
<div id="reset-password-messages"></div>
<form
id="reset-password-form"
hx-post="{{ api_base_url }}/api/v1/auth/reset-password"
hx-ext="json-enc"
hx-target="#reset-password-messages"
hx-swap="innerHTML"
>
<!-- Hidden fields for user_id and secret from URL -->
<input type="hidden" name="user_id" value="{{ user_id }}">
<input type="hidden" name="secret" value="{{ secret }}">
<div class="form-group">
<label class="form-label" for="password">New Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Create a strong passphrase"
required
autocomplete="new-password"
>
<div class="password-strength">
<div class="strength-bar">
<div id="strength-fill" class="strength-fill"></div>
</div>
<span id="strength-text" class="strength-text">Enter a password to see strength</span>
</div>
<span id="password-error" class="field-error"></span>
</div>
<div class="form-group">
<label class="form-label" for="confirm-password">Confirm New Password</label>
<input
type="password"
id="confirm-password"
name="confirm_password"
class="form-input"
placeholder="Re-enter your passphrase"
required
autocomplete="new-password"
>
<span id="confirm-password-error" class="field-error"></span>
</div>
<button type="submit" class="btn btn-primary">
Reset Password
<span class="htmx-indicator loading-spinner"></span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.location.href='{{ url_for('auth_views.login') }}'">
Back to Login
</button>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
// Password strength indicator
function updatePasswordStrength(password) {
const fill = document.getElementById('strength-fill');
const text = document.getElementById('strength-text');
if (!password) {
fill.className = 'strength-fill';
text.textContent = 'Enter a password to see strength';
text.style.color = 'var(--text-muted)';
return;
}
let strength = 0;
// Check length
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
// Check for uppercase
if (/[A-Z]/.test(password)) strength++;
// Check for lowercase
if (/[a-z]/.test(password)) strength++;
// Check for numbers
if (/[0-9]/.test(password)) strength++;
// Check for special characters
if (/[^A-Za-z0-9]/.test(password)) strength++;
if (strength <= 2) {
fill.className = 'strength-fill strength-weak';
text.textContent = 'Weak - Add more complexity';
text.style.color = 'var(--accent-red)';
} else if (strength <= 4) {
fill.className = 'strength-fill strength-medium';
text.textContent = 'Medium - Almost there';
text.style.color = 'var(--accent-gold)';
} else {
fill.className = 'strength-fill strength-strong';
text.textContent = 'Strong - Excellent password';
text.style.color = 'var(--accent-green)';
}
}
// Attach password strength checker
document.getElementById('password').addEventListener('input', function() {
updatePasswordStrength(this.value);
});
// Validate password confirmation
document.getElementById('confirm-password').addEventListener('input', function() {
const password = document.getElementById('password').value;
const confirmPassword = this.value;
const errorSpan = document.getElementById('confirm-password-error');
if (confirmPassword && password !== confirmPassword) {
errorSpan.textContent = 'Passwords do not match';
} else {
errorSpan.textContent = '';
}
});
// Validate before submit
document.getElementById('reset-password-form').addEventListener('submit', function(e) {
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm-password').value;
if (password !== confirmPassword) {
e.preventDefault();
document.getElementById('confirm-password-error').textContent = 'Passwords do not match';
return false;
}
});
// Handle form submission response
document.getElementById('reset-password-form').addEventListener('htmx:afterSwap', function(event) {
const response = event.detail.xhr.responseText;
try {
const data = JSON.parse(response);
// Check if reset was successful
if (data.status === 200) {
// Show success message
document.getElementById('reset-password-messages').innerHTML =
'<div class="success-message">' +
'Password reset successful! Redirecting to login...' +
'</div>';
// Redirect to login after delay
setTimeout(function() {
window.location.href = '{{ url_for("auth_views.login") }}';
}, 2000);
} else {
// Display error message
if (data.error && data.error.details) {
let errorHtml = '<div class="error-message">';
for (const [field, message] of Object.entries(data.error.details)) {
errorHtml += `<strong>${field}:</strong> ${message}<br>`;
const errorSpan = document.getElementById(field + '-error');
if (errorSpan) {
errorSpan.textContent = message;
}
}
errorHtml += '</div>';
document.getElementById('reset-password-messages').innerHTML = errorHtml;
} else if (data.error && data.error.message) {
document.getElementById('reset-password-messages').innerHTML =
'<div class="error-message">' + data.error.message + '</div>';
}
}
} catch (e) {
console.error('Error parsing response:', e);
}
});
// Clear errors when user starts typing
document.querySelectorAll('.form-input').forEach(input => {
input.addEventListener('input', function() {
const fieldName = this.name;
const errorSpan = document.getElementById(fieldName + '-error');
if (errorSpan) {
errorSpan.textContent = '';
}
document.getElementById('reset-password-messages').innerHTML = '';
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Verify Email - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Email Verification</h1>
<p class="page-subtitle">Confirming your email address</p>
<div class="decorative-line"></div>
<div class="text-center">
<div class="success-message">
Your email has been verified successfully!
</div>
<p class="mt-2 mb-2" style="color: var(--text-secondary);">
You can now log in to your account and begin your adventure.
</p>
<button class="btn btn-primary" onclick="window.location.href='{{ url_for('auth_views.login') }}'">
Go to Login
</button>
</div>
</div>
{% endblock %}