393 lines
19 KiB
HTML
393 lines
19 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}User Profile - DosVault{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold mb-2 text-primary">User Profile</h1>
|
|
<p class="text-secondary">Manage your account settings and preferences</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<!-- Profile Information Card -->
|
|
<div class="lg:col-span-1">
|
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
|
<div class="flex items-center mb-6">
|
|
<div class="w-16 h-16 bg-accent rounded-full flex items-center justify-center mr-4">
|
|
<span class="text-2xl font-bold text-white">{{ current_user.username[0].upper() }}</span>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-primary">{{ current_user.username }}</h2>
|
|
<p class="text-secondary">{{ current_user.email }}</p>
|
|
<span class="px-2 py-1 rounded text-xs text-white mt-1 inline-block
|
|
{% if current_user.role == 'super' %}bg-danger-color
|
|
{% elif current_user.role == 'normal' %}bg-accent
|
|
{% else %}bg-warning-color{% endif %}">
|
|
{{ current_user.role.upper() }} USER
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-secondary">Joined:</span>
|
|
<span class="text-primary">{{ current_user.created_at.strftime('%B %d, %Y') if current_user.created_at else 'Unknown' }}</span>
|
|
</div>
|
|
|
|
{% if current_user.last_login %}
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-secondary">Last Login:</span>
|
|
<span class="text-primary">{{ current_user.last_login.strftime('%B %d, %Y at %I:%M %p') }}</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-secondary">Account Status:</span>
|
|
<span class="{% if current_user.is_active %}text-green-400{% else %}text-red-400{% endif %}">
|
|
{% if current_user.is_active %}Active{% else %}Inactive{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account Management -->
|
|
<div class="lg:col-span-2">
|
|
<div class="space-y-6">
|
|
<!-- Profile Information Edit Section (Super users only) -->
|
|
{% if current_user.role == "super" %}
|
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-6 h-6 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-primary">Edit Profile Information</h3>
|
|
</div>
|
|
<p class="text-secondary text-sm mb-4">Update your username and email address.</p>
|
|
|
|
<form onsubmit="updateProfile(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-secondary mb-1">Username</label>
|
|
<input type="text" id="edit-username" value="{{ current_user.username }}" required
|
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
|
<p class="text-xs text-secondary mt-1">Choose a unique username for your account.</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-secondary mb-1">Email Address</label>
|
|
<input type="email" id="edit-email" value="{{ current_user.email }}" required
|
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
|
<p class="text-xs text-secondary mt-1">A valid email address for account recovery.</p>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-4">
|
|
<button type="submit" id="profile-btn"
|
|
class="px-6 py-2 bg-green-600 hover:bg-green-700 rounded font-medium text-white transition-colors">
|
|
<span class="profile-text">Update Profile</span>
|
|
<span class="profile-loading hidden">Updating...</span>
|
|
</button>
|
|
<div id="profile-status" class="text-sm"></div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Password Change Section -->
|
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-6 h-6 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h2m0 0V7a2 2 0 012-2"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-primary">Change Password</h3>
|
|
</div>
|
|
<p class="text-secondary text-sm mb-4">Update your password to keep your account secure.</p>
|
|
|
|
<form onsubmit="changePassword(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-secondary mb-1">Current Password</label>
|
|
<input type="password" id="current-password" required
|
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-secondary mb-1">New Password</label>
|
|
<input type="password" id="new-password" required minlength="6"
|
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
|
<p class="text-xs text-secondary mt-1">Password must be at least 6 characters long.</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-secondary mb-1">Confirm New Password</label>
|
|
<input type="password" id="confirm-password" required
|
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-4">
|
|
<button type="submit" id="password-btn"
|
|
class="px-6 py-2 bg-accent hover:bg-accent-hover rounded font-medium text-white transition-colors">
|
|
<span class="password-text">Update Password</span>
|
|
<span class="password-loading hidden">Updating...</span>
|
|
</button>
|
|
<div id="password-status" class="text-sm"></div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Account Statistics (for non-demo users) -->
|
|
{% if current_user.role != "demo" %}
|
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-6 h-6 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-primary">Your Statistics</h3>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="user-stats">
|
|
<div class="bg-tertiary p-4 rounded-lg border border-theme">
|
|
<div class="flex items-center">
|
|
<div class="text-2xl text-accent mr-3">❤️</div>
|
|
<div>
|
|
<p class="text-lg font-bold text-primary" id="favorites-count">{{ current_user.favorites|length }}</p>
|
|
<p class="text-secondary text-sm">Favorite Games</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-tertiary p-4 rounded-lg border border-theme">
|
|
<div class="flex items-center">
|
|
<div class="text-2xl text-accent mr-3">⬇️</div>
|
|
<div>
|
|
<p class="text-lg font-bold text-primary">N/A</p>
|
|
<p class="text-secondary text-sm">Downloads</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<a href="/favorites" class="inline-flex items-center px-4 py-2 bg-accent hover:bg-accent-hover rounded font-medium text-white transition-colors">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
|
|
</svg>
|
|
View My Favorites
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-6 h-6 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-primary">Quick Actions</h3>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<a href="/" class="flex items-center p-3 bg-tertiary hover:bg-accent rounded-lg transition-colors group">
|
|
<div class="text-2xl mr-3 group-hover:text-white">🎮</div>
|
|
<div>
|
|
<p class="font-medium text-primary group-hover:text-white">Browse Games</p>
|
|
<p class="text-sm text-secondary group-hover:text-gray-200">Explore the library</p>
|
|
</div>
|
|
</a>
|
|
|
|
{% if current_user.role != "demo" %}
|
|
<a href="/favorites" class="flex items-center p-3 bg-tertiary hover:bg-accent rounded-lg transition-colors group">
|
|
<div class="text-2xl mr-3 group-hover:text-white">❤️</div>
|
|
<div>
|
|
<p class="font-medium text-primary group-hover:text-white">My Favorites</p>
|
|
<p class="text-sm text-secondary group-hover:text-gray-200">View saved games</p>
|
|
</div>
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% if current_user.role == "super" %}
|
|
<a href="/admin" class="flex items-center p-3 bg-tertiary hover:bg-accent rounded-lg transition-colors group">
|
|
<div class="text-2xl mr-3 group-hover:text-white">⚙️</div>
|
|
<div>
|
|
<p class="font-medium text-primary group-hover:text-white">Admin Panel</p>
|
|
<p class="text-sm text-secondary group-hover:text-gray-200">System management</p>
|
|
</div>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<button onclick="logout()" class="flex items-center p-3 bg-red-600 hover:bg-red-700 rounded-lg transition-colors group">
|
|
<div class="text-2xl mr-3 text-white">🚪</div>
|
|
<div>
|
|
<p class="font-medium text-white">Sign Out</p>
|
|
<p class="text-sm text-red-200">End your session</p>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function getCookie(name) {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
return null;
|
|
}
|
|
|
|
async function changePassword(event) {
|
|
event.preventDefault();
|
|
|
|
const currentPassword = document.getElementById('current-password').value;
|
|
const newPassword = document.getElementById('new-password').value;
|
|
const confirmPassword = document.getElementById('confirm-password').value;
|
|
|
|
const btn = document.getElementById('password-btn');
|
|
const status = document.getElementById('password-status');
|
|
const passwordText = btn.querySelector('.password-text');
|
|
const passwordLoading = btn.querySelector('.password-loading');
|
|
|
|
// Client-side validation
|
|
if (newPassword !== confirmPassword) {
|
|
status.textContent = 'New passwords do not match';
|
|
status.className = 'text-sm text-red-400';
|
|
return;
|
|
}
|
|
|
|
if (newPassword.length < 6) {
|
|
status.textContent = 'Password must be at least 6 characters long';
|
|
status.className = 'text-sm text-red-400';
|
|
return;
|
|
}
|
|
|
|
// Disable form and show loading
|
|
btn.disabled = true;
|
|
passwordText.classList.add('hidden');
|
|
passwordLoading.classList.remove('hidden');
|
|
status.textContent = '';
|
|
|
|
try {
|
|
const response = await fetch('/profile/change-password', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${getCookie('auth_token')}`
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
current_password: currentPassword,
|
|
new_password: newPassword
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
status.textContent = 'Password updated successfully!';
|
|
status.className = 'text-sm text-green-400';
|
|
|
|
// Clear form
|
|
document.getElementById('current-password').value = '';
|
|
document.getElementById('new-password').value = '';
|
|
document.getElementById('confirm-password').value = '';
|
|
|
|
} else {
|
|
status.textContent = result.detail || 'Failed to update password';
|
|
status.className = 'text-sm text-red-400';
|
|
}
|
|
} catch (error) {
|
|
console.error('Password change error:', error);
|
|
status.textContent = 'Network error. Please try again.';
|
|
status.className = 'text-sm text-red-400';
|
|
} finally {
|
|
// Re-enable form
|
|
btn.disabled = false;
|
|
passwordText.classList.remove('hidden');
|
|
passwordLoading.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
async function updateProfile(event) {
|
|
event.preventDefault();
|
|
|
|
const username = document.getElementById('edit-username').value.trim();
|
|
const email = document.getElementById('edit-email').value.trim();
|
|
|
|
const btn = document.getElementById('profile-btn');
|
|
const status = document.getElementById('profile-status');
|
|
const profileText = btn.querySelector('.profile-text');
|
|
const profileLoading = btn.querySelector('.profile-loading');
|
|
|
|
// Basic client-side validation
|
|
if (!username || !email) {
|
|
status.textContent = 'Username and email are required';
|
|
status.className = 'text-sm text-red-400';
|
|
return;
|
|
}
|
|
|
|
if (username.length < 3) {
|
|
status.textContent = 'Username must be at least 3 characters long';
|
|
status.className = 'text-sm text-red-400';
|
|
return;
|
|
}
|
|
|
|
// Disable form and show loading
|
|
btn.disabled = true;
|
|
profileText.classList.add('hidden');
|
|
profileLoading.classList.remove('hidden');
|
|
status.textContent = '';
|
|
|
|
try {
|
|
const response = await fetch('/profile/update-info', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${getCookie('auth_token')}`
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
username: username,
|
|
email: email
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
status.textContent = 'Profile updated successfully! Refreshing page...';
|
|
status.className = 'text-sm text-green-400';
|
|
|
|
// Refresh page after a short delay to show the updated info
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
|
|
} else {
|
|
status.textContent = result.detail || 'Failed to update profile';
|
|
status.className = 'text-sm text-red-400';
|
|
}
|
|
} catch (error) {
|
|
console.error('Profile update error:', error);
|
|
status.textContent = 'Network error. Please try again.';
|
|
status.className = 'text-sm text-red-400';
|
|
} finally {
|
|
// Re-enable form
|
|
btn.disabled = false;
|
|
profileText.classList.remove('hidden');
|
|
profileLoading.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// Logout function (if not already defined in base template)
|
|
async function logout() {
|
|
try {
|
|
await fetch('/logout', { method: 'POST' });
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
}
|
|
localStorage.removeItem('authToken');
|
|
window.location.href = '/';
|
|
}
|
|
</script>
|
|
{% endblock %}
|