Implement comprehensive account management system

- Create dedicated Account page (/account) for users and admins
- Add password change functionality with current password verification
- Implement token top-up system with predefined packages and custom amounts
- Add payment method selection (Credit Card, PayPal, Crypto) with UI
- Create account balance display with prominent token counter
- Add navigation links to Account page across all templates
- Implement backend routes for password changes and token purchases
- Add update_user_password function to database operations
- Include demo token purchase functionality (no real payments)
- Maintain consistent navigation experience across the application
- Ensure proper access control for account management features
parent 82e0f6a0
<!DOCTYPE html>
<html>
<head>
<title>Account - VidAI</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: #f8fafc; }
.header { background: white; padding: 1rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.header-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; }
.logo { font-size: 1.5rem; font-weight: 700; color: #667eea; }
.nav { display: flex; gap: 2rem; }
.nav a { text-decoration: none; color: #64748b; font-weight: 500; }
.nav a.active { color: #667eea; }
.user-menu { display: flex; align-items: center; gap: 1rem; }
.container { max-width: 1000px; margin: 2rem auto; padding: 0 2rem; }
.account-card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 2rem; }
.card-header { margin-bottom: 1.5rem; }
.card-header h3 { margin: 0; color: #1e293b; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input, .form-group select { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; }
.btn { padding: 0.75rem 2rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn:hover { background: #5a67d8; }
.btn-danger { background: #dc2626; }
.btn-danger:hover { background: #b91c1c; }
.btn-success { background: #059669; }
.btn-success:hover { background: #047857; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
.token-balance { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; border-radius: 12px; text-align: center; margin-bottom: 2rem; }
.balance-amount { font-size: 3rem; font-weight: 700; margin-bottom: 0.5rem; }
.balance-label { font-size: 1.2rem; opacity: 0.9; }
.pricing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.pricing-card { border: 2px solid #e0e0e0; border-radius: 12px; padding: 1.5rem; text-align: center; transition: all 0.3s ease; cursor: pointer; }
.pricing-card:hover { border-color: #667eea; transform: translateY(-2px); box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2); }
.pricing-card.selected { border-color: #667eea; background: #f0f4ff; }
.pricing-name { font-size: 1.2rem; font-weight: 600; margin-bottom: 0.5rem; color: #1e293b; }
.pricing-tokens { font-size: 2rem; font-weight: 700; color: #667eea; margin-bottom: 0.5rem; }
.pricing-price { font-size: 1.1rem; color: #374151; margin-bottom: 1rem; }
.payment-methods { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
.payment-card { border: 2px solid #e0e0e0; border-radius: 8px; padding: 1rem; text-align: center; transition: all 0.3s ease; cursor: pointer; }
.payment-card:hover { border-color: #667eea; }
.payment-card.selected { border-color: #667eea; background: #f0f4ff; }
.payment-icon { font-size: 2rem; margin-bottom: 0.5rem; }
.payment-name { font-weight: 500; color: #374151; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
.modal-content { background-color: white; margin: 15% auto; padding: 0; width: 90%; max-width: 500px; border-radius: 12px; }
.modal-header { padding: 1rem 2rem; border-bottom: 1px solid #e5e7eb; }
.modal-body { padding: 2rem; }
.modal-footer { padding: 1rem 2rem; border-top: 1px solid #e5e7eb; text-align: right; }
.form-row { display: flex; gap: 1rem; }
.form-row .form-group { flex: 1; }
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">VidAI</div>
<nav class="nav">
<a href="/dashboard">Dashboard</a>
<a href="/analyze">Analyze</a>
<a href="/train">Train</a>
<a href="/history">History</a>
<a href="/api_tokens">API Tokens</a>
<a href="/account" class="active">Account</a>
{% if user.get('role') == 'admin' %}
<a href="/admin">Admin</a>
{% endif %}
</nav>
<div class="user-menu">
<span>{{ user.get('username', 'User') }}</span>
<a href="/logout" style="color: #dc2626;">Logout</a>
</div>
</div>
</header>
<div class="container">
<!-- Token Balance -->
<div class="token-balance">
<div class="balance-amount">{{ user.get('tokens', 0) }}</div>
<div class="balance-label">Available Tokens</div>
</div>
<!-- Password Change -->
<div class="account-card">
<div class="card-header">
<h3><i class="fas fa-lock"></i> Change Password</h3>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{% if 'password' in message.lower() %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<form method="post" action="/account/change_password">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" required>
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
</div>
<button type="submit" class="btn"><i class="fas fa-save"></i> Change Password</button>
</form>
</div>
<!-- Token Top-up -->
<div class="account-card">
<div class="card-header">
<h3><i class="fas fa-coins"></i> Top Up Tokens</h3>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{% if 'token' in message.lower() or 'purchase' in message.lower() %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<div class="pricing-grid">
<div class="pricing-card" onclick="selectPackage('starter')">
<div class="pricing-name">Starter Pack</div>
<div class="pricing-tokens">500</div>
<div class="pricing-price">$4.99</div>
</div>
<div class="pricing-card" onclick="selectPackage('professional')">
<div class="pricing-name">Professional Pack</div>
<div class="pricing-tokens">1,200</div>
<div class="pricing-price">$9.99</div>
</div>
<div class="pricing-card" onclick="selectPackage('enterprise')">
<div class="pricing-name">Enterprise Pack</div>
<div class="pricing-tokens">3,000</div>
<div class="pricing-price">$19.99</div>
</div>
<div class="pricing-card" onclick="selectPackage('custom')">
<div class="pricing-name">Custom Amount</div>
<div class="pricing-tokens">
<input type="number" id="custom_tokens" placeholder="100" min="100" style="width: 80px; text-align: center; border: none; background: transparent; font-size: 1.5rem; font-weight: 700; color: #667eea;">
</div>
<div class="pricing-price">Calculated</div>
</div>
</div>
<div class="payment-methods">
<div class="payment-card" onclick="selectPayment('card')">
<div class="payment-icon"><i class="fas fa-credit-card"></i></div>
<div class="payment-name">Credit Card</div>
</div>
<div class="payment-card" onclick="selectPayment('paypal')">
<div class="payment-icon"><i class="fab fa-paypal"></i></div>
<div class="payment-name">PayPal</div>
</div>
<div class="payment-card" onclick="selectPayment('crypto')">
<div class="payment-icon"><i class="fab fa-bitcoin"></i></div>
<div class="payment-name">Cryptocurrency</div>
</div>
</div>
<div style="text-align: center; margin-top: 2rem;">
<button type="button" class="btn btn-success" onclick="purchaseTokens()" id="purchaseBtn" disabled>
<i class="fas fa-shopping-cart"></i> Purchase Tokens
</button>
</div>
</div>
<!-- Payment Methods -->
<div class="account-card">
<div class="card-header">
<h3><i class="fas fa-credit-card"></i> Saved Payment Methods</h3>
</div>
<p style="color: #6b7280; margin-bottom: 1rem;">Save your payment methods for faster checkout.</p>
<div class="payment-methods">
<div class="payment-card" onclick="addPaymentMethod('card')">
<div class="payment-icon"><i class="fas fa-plus"></i></div>
<div class="payment-name">Add Card</div>
</div>
<div class="payment-card" onclick="addPaymentMethod('paypal')">
<div class="payment-icon"><i class="fab fa-paypal"></i></div>
<div class="payment-name">Add PayPal</div>
</div>
<div class="payment-card" onclick="addPaymentMethod('crypto')">
<div class="payment-icon"><i class="fab fa-bitcoin"></i></div>
<div class="payment-name">Add Crypto</div>
</div>
</div>
</div>
</div>
<!-- Purchase Modal -->
<div id="purchaseModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Confirm Purchase</h3>
<span onclick="closePurchaseModal()" style="cursor: pointer; font-size: 1.5rem;">&times;</span>
</div>
<div class="modal-body">
<div id="purchaseSummary"></div>
<p style="color: #dc2626; margin-top: 1rem;"><strong>Note:</strong> This is a demo implementation. In production, this would integrate with actual payment processors.</p>
</div>
<div class="modal-footer">
<button onclick="closePurchaseModal()" class="btn" style="background: #6b7280;">Cancel</button>
<button onclick="confirmPurchase()" class="btn btn-success">Complete Purchase</button>
</div>
</div>
</div>
<script>
let selectedPackage = null;
let selectedPayment = null;
function selectPackage(packageType) {
// Remove selected class from all cards
document.querySelectorAll('.pricing-card').forEach(card => {
card.classList.remove('selected');
});
// Add selected class to clicked card
event.currentTarget.classList.add('selected');
selectedPackage = packageType;
updatePurchaseButton();
}
function selectPayment(paymentType) {
// Remove selected class from all payment cards
document.querySelectorAll('.payment-card').forEach(card => {
card.classList.remove('selected');
});
// Add selected class to clicked card
event.currentTarget.classList.add('selected');
selectedPayment = paymentType;
updatePurchaseButton();
}
function updatePurchaseButton() {
const btn = document.getElementById('purchaseBtn');
if (selectedPackage && selectedPayment) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-shopping-cart"></i> Purchase Tokens';
} else {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-shopping-cart"></i> Select Package & Payment';
}
}
function purchaseTokens() {
if (!selectedPackage || !selectedPayment) {
alert('Please select both a package and payment method.');
return;
}
let tokens, price;
switch (selectedPackage) {
case 'starter':
tokens = 500;
price = 4.99;
break;
case 'professional':
tokens = 1200;
price = 9.99;
break;
case 'enterprise':
tokens = 3000;
price = 19.99;
break;
case 'custom':
tokens = parseInt(document.getElementById('custom_tokens').value) || 100;
price = (tokens / 100) * 0.10; // $0.10 per token
break;
}
const summary = document.getElementById('purchaseSummary');
summary.innerHTML = `
<h4>Purchase Summary</h4>
<p><strong>Package:</strong> ${selectedPackage.charAt(0).toUpperCase() + selectedPackage.slice(1)}</p>
<p><strong>Tokens:</strong> ${tokens.toLocaleString()}</p>
<p><strong>Price:</strong> $${price.toFixed(2)}</p>
<p><strong>Payment Method:</strong> ${selectedPayment.charAt(0).toUpperCase() + selectedPayment.slice(1)}</p>
`;
document.getElementById('purchaseModal').style.display = 'block';
}
function closePurchaseModal() {
document.getElementById('purchaseModal').style.display = 'none';
}
function confirmPurchase() {
// Create form and submit
const form = document.createElement('form');
form.method = 'POST';
form.action = '/account/purchase_tokens';
const packageInput = document.createElement('input');
packageInput.type = 'hidden';
packageInput.name = 'package';
packageInput.value = selectedPackage;
form.appendChild(packageInput);
const paymentInput = document.createElement('input');
paymentInput.type = 'hidden';
paymentInput.name = 'payment_method';
paymentInput.value = selectedPayment;
form.appendChild(paymentInput);
if (selectedPackage === 'custom') {
const tokensInput = document.createElement('input');
tokensInput.type = 'hidden';
tokensInput.name = 'custom_tokens';
tokensInput.value = document.getElementById('custom_tokens').value;
form.appendChild(tokensInput);
}
document.body.appendChild(form);
form.submit();
}
function addPaymentMethod(type) {
alert(`Add ${type} payment method - This would open a payment method form in production.`);
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('purchaseModal');
if (event.target == modal) {
closePurchaseModal();
}
}
</script>
</body>
</html>
\ No newline at end of file
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
<a href="/train">Train</a> <a href="/train">Train</a>
<a href="/history">History</a> <a href="/history">History</a>
<a href="/api_tokens">API Tokens</a> <a href="/api_tokens">API Tokens</a>
<a href="/account">Account</a>
<a href="/admin" class="active">Admin</a> <a href="/admin" class="active">Admin</a>
</nav> </nav>
<div class="user-menu"> <div class="user-menu">
......
...@@ -179,6 +179,7 @@ ...@@ -179,6 +179,7 @@
<a href="/train">Train</a> <a href="/train">Train</a>
<a href="/history">History</a> <a href="/history">History</a>
<a href="/api_tokens">API Tokens</a> <a href="/api_tokens">API Tokens</a>
<a href="/account">Account</a>
{% if user.get('role') == 'admin' %} {% if user.get('role') == 'admin' %}
<a href="/admin">Admin</a> <a href="/admin">Admin</a>
{% endif %} {% endif %}
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
<a href="/train">Train</a> <a href="/train">Train</a>
<a href="/history">History</a> <a href="/history">History</a>
<a href="/api_tokens">API Tokens</a> <a href="/api_tokens">API Tokens</a>
<a href="/account">Account</a>
<a href="/settings">Settings</a> <a href="/settings">Settings</a>
{% if user.get('role') == 'admin' %} {% if user.get('role') == 'admin' %}
<a href="/admin">Admin</a> <a href="/admin">Admin</a>
......
...@@ -42,6 +42,7 @@ ...@@ -42,6 +42,7 @@
<a href="/train">Train</a> <a href="/train">Train</a>
<a href="/history" class="active">History</a> <a href="/history" class="active">History</a>
<a href="/api_tokens">API Tokens</a> <a href="/api_tokens">API Tokens</a>
<a href="/account">Account</a>
<a href="/settings">Settings</a> <a href="/settings">Settings</a>
{% if user.get('role') == 'admin' %} {% if user.get('role') == 'admin' %}
<a href="/admin">Admin</a> <a href="/admin">Admin</a>
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
<a href="/train">Train</a> <a href="/train">Train</a>
<a href="/history">History</a> <a href="/history">History</a>
<a href="/api_tokens">API Tokens</a> <a href="/api_tokens">API Tokens</a>
<a href="/account">Account</a>
<a href="/settings" class="active">Settings</a> <a href="/settings" class="active">Settings</a>
{% if user.get('role') == 'admin' %} {% if user.get('role') == 'admin' %}
<a href="/admin">Admin</a> <a href="/admin">Admin</a>
......
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
<a href="/train" class="active">Train</a> <a href="/train" class="active">Train</a>
<a href="/history">History</a> <a href="/history">History</a>
<a href="/api_tokens">API Tokens</a> <a href="/api_tokens">API Tokens</a>
<a href="/account">Account</a>
<a href="/settings">Settings</a> <a href="/settings">Settings</a>
{% if user.get('role') == 'admin' %} {% if user.get('role') == 'admin' %}
<a href="/admin">Admin</a> <a href="/admin">Admin</a>
......
...@@ -971,6 +971,17 @@ def update_user_tokens_admin(user_id: int, tokens: int) -> bool: ...@@ -971,6 +971,17 @@ def update_user_tokens_admin(user_id: int, tokens: int) -> bool:
return success return success
def update_user_password(user_id: int, password_hash: str) -> bool:
"""Update user password."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('UPDATE users SET password_hash = ? WHERE id = ?', (password_hash, user_id))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def create_user(username: str, password: str, email: str, role: str = 'user', tokens: int = 0) -> tuple[bool, str]: def create_user(username: str, password: str, email: str, role: str = 'user', tokens: int = 0) -> tuple[bool, str]:
"""Create a new user (admin function).""" """Create a new user (admin function)."""
conn = get_db_connection() conn = get_db_connection()
......
...@@ -400,6 +400,96 @@ def delete_api_token(token_id): ...@@ -400,6 +400,96 @@ def delete_api_token(token_id):
return redirect(url_for('api_tokens')) return redirect(url_for('api_tokens'))
@app.route('/account')
@login_required
def account():
"""User account management page."""
user = get_current_user_session()
return render_template('account.html', user=user)
@app.route('/account/change_password', methods=['POST'])
@login_required
def change_password():
"""Change user password."""
user = get_current_user_session()
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
if not current_password or not new_password or not confirm_password:
flash('All password fields are required.', 'error')
return redirect(url_for('account'))
if new_password != confirm_password:
flash('New passwords do not match.', 'error')
return redirect(url_for('account'))
if len(new_password) < 6:
flash('New password must be at least 6 characters long.', 'error')
return redirect(url_for('account'))
# Verify current password
from .database import authenticate_user
auth_user = authenticate_user(user['username'], current_password)
if not auth_user:
flash('Current password is incorrect.', 'error')
return redirect(url_for('account'))
# Update password
import hashlib
password_hash = hashlib.sha256(new_password.encode()).hexdigest()
from .database import update_user_password
if update_user_password(user['id'], password_hash):
flash('Password changed successfully!', 'success')
else:
flash('Failed to change password.', 'error')
return redirect(url_for('account'))
@app.route('/account/purchase_tokens', methods=['POST'])
@login_required
def purchase_tokens():
"""Purchase tokens for user account."""
user = get_current_user_session()
package = request.form.get('package')
payment_method = request.form.get('payment_method')
custom_tokens = request.form.get('custom_tokens')
if not package or not payment_method:
flash('Please select a package and payment method.', 'error')
return redirect(url_for('account'))
# Calculate tokens and cost
if package == 'starter':
tokens = 500
cost = 4.99
elif package == 'professional':
tokens = 1200
cost = 9.99
elif package == 'enterprise':
tokens = 3000
cost = 19.99
elif package == 'custom':
tokens = int(custom_tokens) if custom_tokens else 100
cost = (tokens / 100) * 0.10 # $0.10 per token
else:
flash('Invalid package selected.', 'error')
return redirect(url_for('account'))
# In a real implementation, this would process payment
# For demo purposes, we'll just add the tokens
from .database import update_user_tokens
if update_user_tokens(user['id'], tokens):
flash(f'Successfully purchased {tokens} tokens! (Demo - no actual payment processed)', 'success')
else:
flash('Failed to process token purchase.', 'error')
return redirect(url_for('account'))
@app.route('/update_database_settings', methods=['POST']) @app.route('/update_database_settings', methods=['POST'])
@admin_required @admin_required
def update_database_settings(): def update_database_settings():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment