452 lines
21 KiB
HTML
452 lines
21 KiB
HTML
|
|
<!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>
|