Files
DosVault/templates/user_profile.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 %}