masina-dock/frontend/templates/settings.html

452 lines
21 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings - Masina-Dock</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<div class="logo">
<img src="/static/images/logo.svg" alt="Masina-Dock">
<h1>Masina-Dock</h1>
</div>
<nav>
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
<a href="/settings" class="btn" data-translate="settings">Settings</a>
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
</nav>
</header>
<div class="container">
<h2 data-translate="settings">Settings</h2>
<form id="settings-form">
<div class="card" style="margin-bottom: 20px;">
<h3 data-translate="backup_restore_title">Backup & Restore</h3>
<p data-translate="backup_restore_desc">Create a complete backup of all your data or restore from a previous backup.</p>
<div style="display: flex; gap: 10px; margin-top: 15px;">
<button type="button" class="btn btn-success" onclick="createBackup()" data-translate="create_backup">Create Backup</button>
<button type="button" class="btn" onclick="document.getElementById('restore-file').click()" data-translate="restore_backup">Restore Backup</button>
<input type="file" id="restore-file" accept=".zip" style="display: none;" onchange="restoreBackup(this.files[0])">
</div>
</div>
<div class="card" style="margin-bottom: 20px;">
<h3>User Profile</h3>
<div style="display: flex; gap: 20px; align-items: center; margin-bottom: 20px;">
<img id="user-photo-preview" src="" alt="User Photo" style="width: 100px; height: 100px; border-radius: 50%; object-fit: cover; display: none;">
<div style="flex: 1;">
<div class="form-group">
<label for="user-photo">Profile Photo</label>
<input type="file" id="user-photo" accept="image/*" onchange="previewUserPhoto(this)">
</div>
</div>
</div>
<div class="form-group">
<label for="profile-username">Username</label>
<input type="text" id="profile-username" name="username" minlength="3">
</div>
<div class="form-group">
<label for="profile-email">Email</label>
<input type="email" id="profile-email" name="email">
</div>
<button type="button" class="btn" onclick="showModal('change-password-modal')">Change Password</button>
</div>
<div class="card" style="margin-bottom: 20px;">
<h3>Two-Factor Authentication (2FA)</h3>
<p id="2fa-status" style="margin-bottom: 15px;"></p>
<div id="2fa-controls"></div>
</div>
<div class="card" style="margin-bottom: 20px;">
<h3 data-translate="language">Language</h3>
<div class="form-group">
<label for="language" data-translate="select_language">Select Language</label>
<select id="language" name="language">
<option value="en">English</option>
<option value="ro">Romana</option>
</select>
</div>
</div>
<div class="card" style="margin-bottom: 20px;">
<h3 data-translate="units_currency">Units & Currency</h3>
<div class="form-group">
<label for="unit-system" data-translate="measurement_system">Measurement System</label>
<select id="unit-system" name="unit_system">
<option value="metric" data-translate="metric">Metric (km, litres)</option>
<option value="imperial" data-translate="imperial">Imperial (miles, gallons)</option>
</select>
</div>
<div class="form-group">
<label for="currency" data-translate="currency">Currency</label>
<select id="currency" name="currency">
<option value="USD" data-translate="us_dollar">US Dollar (USD)</option>
<option value="GBP" data-translate="british_pound">British Pound (GBP)</option>
<option value="RON" data-translate="romanian_leu">Romanian Leu (RON)</option>
<option value="EUR" data-translate="euro">Euro (EUR)</option>
</select>
</div>
</div>
<div class="card" style="margin-bottom: 20px;">
<h3 data-translate="app_info">Application Info</h3>
<p><strong data-translate="version">Version:</strong> 1.0.0</p>
<p><strong data-translate="database">Database:</strong> SQLite</p>
<p><strong data-translate="current_user">Current User:</strong> <span id="current-user-name"></span></p>
</div>
<div style="display: flex; gap: 10px; justify-content: center; margin-top: 30px;">
<button type="submit" class="btn btn-success" style="font-size: 18px; padding: 12px 40px;" data-translate="save">Save All Changes</button>
</div>
</form>
</div>
<div id="change-password-modal" class="modal">
<div class="modal-content">
<h2 data-translate="change_password">Change Password</h2>
<form id="change-password-form">
<div class="form-group">
<label for="current-password" data-translate="current_password">Current Password</label>
<input type="password" id="current-password" name="current_password" required>
</div>
<div class="form-group">
<label for="new-password" data-translate="new_password">New Password</label>
<input type="password" id="new-password" name="new_password" required minlength="8">
<small style="color: var(--text-secondary);">
Must be at least 8 characters with uppercase, lowercase, and numbers
</small>
</div>
<div class="form-group">
<label for="confirm-password" data-translate="confirm_password">Confirm New Password</label>
<input type="password" id="confirm-password" name="confirm_password" required minlength="8">
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="submit" class="btn btn-success" data-translate="update">Update Password</button>
<button type="button" class="btn" onclick="closeModal('change-password-modal')" data-translate="cancel">Cancel</button>
</div>
</form>
</div>
</div>
<div id="setup-2fa-modal" class="modal">
<div class="modal-content">
<h2>Setup Two-Factor Authentication</h2>
<p>Scan this QR code with your authenticator app:</p>
<div style="text-align: center; margin: 20px 0;">
<img id="qr-code-image" src="" alt="QR Code" style="max-width: 250px;">
</div>
<p><strong>Backup Codes:</strong></p>
<p style="color: var(--text-secondary); font-size: 14px;">Save these codes in a safe place. You can use them if you lose access to your authenticator app.</p>
<div id="backup-codes" style="background: var(--bg-secondary); padding: 15px; border-radius: 4px; font-family: monospace; margin-bottom: 20px;"></div>
<form id="verify-2fa-form">
<div class="form-group">
<label for="verify-code">Enter verification code from your app:</label>
<input type="text" id="verify-code" name="code" required maxlength="6" placeholder="000000">
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="submit" class="btn btn-success">Enable 2FA</button>
<button type="button" class="btn" onclick="closeModal('setup-2fa-modal')">Cancel</button>
</div>
</form>
</div>
</div>
<div id="disable-2fa-modal" class="modal">
<div class="modal-content">
<h2>Disable Two-Factor Authentication</h2>
<p>Enter your password to disable 2FA:</p>
<form id="disable-2fa-form">
<div class="form-group">
<label for="disable-2fa-password">Password</label>
<input type="password" id="disable-2fa-password" name="password" required>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="submit" class="btn btn-danger">Disable 2FA</button>
<button type="button" class="btn" onclick="closeModal('disable-2fa-modal')">Cancel</button>
</div>
</form>
</div>
</div>
<div id="custom-alert-modal" class="modal">
<div class="modal-content" style="max-width: 400px; text-align: center;">
<p id="alert-message" style="margin: 20px 0; font-size: 16px;"></p>
<button class="btn btn-success" onclick="closeCustomAlert()" style="width: 100px;">OK</button>
</div>
</div>
<script src="/static/js/translations.js"></script>
<script src="/static/js/app.js"></script>
<script>
let uploadedPhotoUrl = null;
let user2faEnabled = false;
function showCustomAlert(message) {
document.getElementById('alert-message').textContent = message;
showModal('custom-alert-modal');
}
function closeCustomAlert() {
closeModal('custom-alert-modal');
}
document.addEventListener('DOMContentLoaded', async () => {
await loadUserSettings();
await load2FAStatus();
setTimeout(() => {
translatePage();
}, 200);
});
async function loadUserSettings() {
try {
const settings = await apiRequest('/api/settings');
document.getElementById('profile-username').value = settings.username;
document.getElementById('profile-email').value = settings.email;
document.getElementById('language').value = settings.language || 'en';
document.getElementById('unit-system').value = settings.unit_system || 'metric';
document.getElementById('currency').value = settings.currency || 'GBP';
document.getElementById('current-user-name').textContent = settings.username;
if (settings.photo) {
document.getElementById('user-photo-preview').src = settings.photo;
document.getElementById('user-photo-preview').style.display = 'block';
}
localStorage.setItem('userSettings', JSON.stringify(settings));
} catch (error) {
console.error('Failed to load settings:', error);
}
}
async function load2FAStatus() {
try {
const response = await apiRequest('/api/auth/me');
user2faEnabled = response.two_factor_enabled;
const statusEl = document.getElementById('2fa-status');
const controlsEl = document.getElementById('2fa-controls');
if (user2faEnabled) {
statusEl.innerHTML = '<span style="color: var(--success);">2FA is currently enabled on your account.</span>';
controlsEl.innerHTML = '<button type="button" class="btn btn-danger" onclick="showModal(\'disable-2fa-modal\')">Disable 2FA</button>';
} else {
statusEl.innerHTML = '<span style="color: var(--text-secondary);">2FA is not enabled. Add an extra layer of security to your account.</span>';
controlsEl.innerHTML = '<button type="button" class="btn btn-success" onclick="setup2FA()">Enable 2FA</button>';
}
} catch (error) {
console.error('Failed to load 2FA status:', error);
}
}
async function setup2FA() {
try {
const response = await apiRequest('/api/auth/setup-2fa', { method: 'POST' });
document.getElementById('qr-code-image').src = response.qr_code;
const backupCodesDiv = document.getElementById('backup-codes');
backupCodesDiv.innerHTML = response.backup_codes.map(code => `<div>${code}</div>`).join('');
showModal('setup-2fa-modal');
} catch (error) {
showCustomAlert('Failed to setup 2FA: ' + error.message);
}
}
document.getElementById('verify-2fa-form').addEventListener('submit', async (e) => {
e.preventDefault();
const code = document.getElementById('verify-code').value;
try {
await apiRequest('/api/auth/enable-2fa', {
method: 'POST',
body: JSON.stringify({ code })
});
closeModal('setup-2fa-modal');
showCustomAlert('2FA enabled successfully!');
await load2FAStatus();
} catch (error) {
showCustomAlert('Invalid verification code. Please try again.');
}
});
document.getElementById('disable-2fa-form').addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('disable-2fa-password').value;
try {
await apiRequest('/api/auth/disable-2fa', {
method: 'POST',
body: JSON.stringify({ password })
});
closeModal('disable-2fa-modal');
showCustomAlert('2FA disabled successfully.');
await load2FAStatus();
document.getElementById('disable-2fa-form').reset();
} catch (error) {
showCustomAlert('Failed to disable 2FA: ' + error.message);
}
});
function previewUserPhoto(input) {
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('user-photo-preview').src = e.target.result;
document.getElementById('user-photo-preview').style.display = 'block';
};
reader.readAsDataURL(input.files[0]);
}
}
document.getElementById('settings-form').addEventListener('submit', async (e) => {
e.preventDefault();
const photoFile = document.getElementById('user-photo').files[0];
if (photoFile) {
const formData = new FormData();
formData.append('photo', photoFile);
try {
const response = await fetch('/api/upload/photo', {
method: 'POST',
credentials: 'include',
body: formData
});
const data = await response.json();
uploadedPhotoUrl = data.photo_url;
} catch (error) {
console.error('Photo upload failed:', error);
}
}
const username = document.getElementById('profile-username').value;
const email = document.getElementById('profile-email').value;
const language = document.getElementById('language').value;
const unitSystem = document.getElementById('unit-system').value;
const currency = document.getElementById('currency').value;
try {
if (uploadedPhotoUrl || username || email) {
await apiRequest('/api/user/update-profile', {
method: 'POST',
body: JSON.stringify({
username,
email,
photo: uploadedPhotoUrl
})
});
}
await apiRequest('/api/settings/language', {
method: 'POST',
body: JSON.stringify({ language })
});
await apiRequest('/api/settings/units', {
method: 'POST',
body: JSON.stringify({
unit_system: unitSystem,
currency: currency
})
});
const settings = JSON.parse(localStorage.getItem('userSettings') || '{}');
settings.username = username;
settings.email = email;
settings.language = language;
settings.unit_system = unitSystem;
settings.currency = currency;
if (uploadedPhotoUrl) settings.photo = uploadedPhotoUrl;
localStorage.setItem('userSettings', JSON.stringify(settings));
showCustomAlert('All settings saved successfully. Page will reload.');
setTimeout(() => window.location.reload(), 2000);
} catch (error) {
showCustomAlert('Failed to save settings: ' + error.message);
}
});
async function createBackup() {
try {
window.location.href = '/api/backup/create';
} catch (error) {
showCustomAlert('Failed to create backup: ' + error.message);
}
}
async function restoreBackup(file) {
if (!file) return;
if (!confirm('Restoring a backup will overwrite all current data. Are you sure?')) {
document.getElementById('restore-file').value = '';
return;
}
const formData = new FormData();
formData.append('backup', file);
try {
const response = await fetch('/api/backup/restore', {
method: 'POST',
credentials: 'include',
body: formData
});
const data = await response.json();
if (response.ok) {
showCustomAlert('Backup restored successfully. Page will reload.');
setTimeout(() => window.location.reload(), 2000);
} else {
showCustomAlert('Failed to restore backup: ' + (data.error || 'Unknown error'));
}
} catch (error) {
showCustomAlert('Failed to restore backup: ' + error.message);
} finally {
document.getElementById('restore-file').value = '';
}
}
document.getElementById('change-password-form').addEventListener('submit', async (e) => {
e.preventDefault();
const currentPassword = document.getElementById('current-password').value;
const newPassword = document.getElementById('new-password').value;
const confirmPassword = document.getElementById('confirm-password').value;
if (newPassword !== confirmPassword) {
showCustomAlert('New passwords do not match.');
return;
}
try {
await apiRequest('/api/auth/change-password', {
method: 'POST',
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
showCustomAlert('Password changed successfully.');
closeModal('change-password-modal');
document.getElementById('change-password-form').reset();
} catch (error) {
showCustomAlert('Failed to change password: ' + error.message);
}
});
</script>
</body>
</html>