mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-04 20:42:59 +00:00
test
This commit is contained in:
+225
-6
@@ -1004,11 +1004,12 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1>Admin Dashboard</h1>
|
<h1>Admin Dashboard</h1>
|
||||||
<nav class="admin-nav">
|
<div class="admin-nav">
|
||||||
<a href="#aplikace" class="nav-link active">Aplikace</a>
|
<a href="#aplikace" class="nav-link active" data-section="aplikace">Aplikace</a>
|
||||||
<a href="#banner" class="nav-link">Správa banneru</a>
|
<a href="#banner" class="nav-link" data-section="banner">Správa banneru</a>
|
||||||
<a href="#rezervace" class="nav-link">Správa rezervací</a>
|
<a href="#rezervace" class="nav-link" data-section="rezervace">Správa rezervací</a>
|
||||||
</nav>
|
<a href="#nastaveni" class="nav-link" data-section="nastaveni">Nastavení</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="logout-btn" id="logoutBtn">Odhlásit se</button>
|
<button class="logout-btn" id="logoutBtn">Odhlásit se</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1062,7 +1063,62 @@
|
|||||||
.logout-btn:hover {
|
.logout-btn:hover {
|
||||||
background-color: #c0392b;
|
background-color: #c0392b;
|
||||||
}
|
}
|
||||||
</style>
|
</div>
|
||||||
|
|
||||||
|
<!-- Credentials Management Section -->
|
||||||
|
<div class="card" style="margin: 2rem auto; max-width: 800px; display: none;" id="credentials">
|
||||||
|
<h3>Nastavení přihlašování</h3>
|
||||||
|
|
||||||
|
<div id="credentialsAlert" class="hidden p-4 mb-4 rounded"></div>
|
||||||
|
|
||||||
|
<form id="credentialsForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="currentUsername" class="block text-sm font-medium text-gray-700">Aktuální uživatelské jméno</label>
|
||||||
|
<input type="text" id="currentUsername" name="currentUsername" required
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="currentPassword" class="block text-sm font-medium text-gray-700">Aktuální heslo</label>
|
||||||
|
<input type="password" id="currentPassword" name="currentPassword" required
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 pt-4">
|
||||||
|
<h4 class="text-md font-medium text-gray-900 mb-3">Nové přihlašovací údaje</h4>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="newUsername" class="block text-sm font-medium text-gray-700">Nové uživatelské jméno</label>
|
||||||
|
<input type="text" id="newUsername" name="newUsername" required
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="newPassword" class="block text-sm font-medium text-gray-700">Nové heslo</label>
|
||||||
|
<input type="password" id="newPassword" name="newPassword" required minlength="8"
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Heslo musí mít alespoň 8 znaků</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="confirmPassword" class="block text-sm font-medium text-gray-700">Potvrďte nové heslo</label>
|
||||||
|
<input type="password" id="confirmPassword" name="confirmPassword" required minlength="8"
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-3 pt-4">
|
||||||
|
<button type="button" onclick="document.getElementById('credentials').style.display = 'none'"
|
||||||
|
class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
|
Zrušit
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
|
Uložit změny
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Vítejte v administraci</h2>
|
<h2>Vítejte v administraci</h2>
|
||||||
@@ -3046,6 +3102,163 @@ async function loadBanner() {
|
|||||||
// Add submission flag at the top of the script
|
// Add submission flag at the top of the script
|
||||||
let isSubmitting = false;
|
let isSubmitting = false;
|
||||||
|
|
||||||
|
// Setup credentials form
|
||||||
|
function setupCredentialsForm() {
|
||||||
|
const credentialsForm = document.getElementById('credentialsForm');
|
||||||
|
if (!credentialsForm) return;
|
||||||
|
|
||||||
|
credentialsForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Prevent multiple submissions
|
||||||
|
if (isSubmitting) return;
|
||||||
|
isSubmitting = true;
|
||||||
|
|
||||||
|
const formData = new FormData(credentialsForm);
|
||||||
|
const currentUsername = formData.get('currentUsername');
|
||||||
|
const currentPassword = formData.get('currentPassword');
|
||||||
|
const newUsername = formData.get('newUsername');
|
||||||
|
const newPassword = formData.get('newPassword');
|
||||||
|
const confirmPassword = formData.get('confirmPassword');
|
||||||
|
|
||||||
|
// Reset previous errors
|
||||||
|
document.querySelectorAll('.form-control').forEach(el => el.classList.remove('border-red-500'));
|
||||||
|
const alertEl = document.getElementById('credentialsAlert');
|
||||||
|
alertEl.classList.add('hidden');
|
||||||
|
|
||||||
|
// Client-side validation
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
if (!currentUsername || !currentPassword) {
|
||||||
|
showError('Vyplňte prosím aktuální přihlašovací údaje.');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword && newPassword.length < 8) {
|
||||||
|
document.getElementById('newPassword').classList.add('border-red-500');
|
||||||
|
showError('Nové heslo musí mít alespoň 8 znaků.');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
document.getElementById('confirmPassword').classList.add('border-red-500');
|
||||||
|
showError('Nová hesla se neshodují.');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
isSubmitting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare request data
|
||||||
|
const requestData = {
|
||||||
|
currentUsername,
|
||||||
|
currentPassword,
|
||||||
|
newUsername: newUsername || undefined,
|
||||||
|
newPassword: newPassword || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/update-credentials', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Nepodařilo se aktualizovat přihlašovací údaje');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
showSuccess('Přihlašovací údaje byly úspěšně aktualizovány. Budete odhlášeni za 3 sekundy...');
|
||||||
|
|
||||||
|
// Logout after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chyba při aktualizaci přihlašovacích údajů:', error);
|
||||||
|
showError(error.message || 'Nastala chyba při aktualizaci přihlašovacích údajů');
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
const alertEl = document.getElementById('credentialsAlert');
|
||||||
|
alertEl.textContent = message;
|
||||||
|
alertEl.className = 'bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4';
|
||||||
|
alertEl.classList.remove('hidden');
|
||||||
|
alertEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess(message) {
|
||||||
|
const alertEl = document.getElementById('credentialsAlert');
|
||||||
|
alertEl.textContent = message;
|
||||||
|
alertEl.className = 'bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4';
|
||||||
|
alertEl.classList.remove('hidden');
|
||||||
|
alertEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup navigation
|
||||||
|
function setupNavigation() {
|
||||||
|
const navLinks = document.querySelectorAll('.nav-link');
|
||||||
|
const sections = document.querySelectorAll('.card[id]');
|
||||||
|
|
||||||
|
// Show section based on hash or default to first section
|
||||||
|
function showSection(sectionId) {
|
||||||
|
// Hide all sections
|
||||||
|
sections.forEach(section => {
|
||||||
|
section.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show selected section
|
||||||
|
const targetSection = document.getElementById(sectionId);
|
||||||
|
if (targetSection) {
|
||||||
|
targetSection.style.display = 'block';
|
||||||
|
} else if (sections.length > 0) {
|
||||||
|
// Default to first section if target not found
|
||||||
|
sections[0].style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update active nav link
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.classList.toggle('active', link.getAttribute('data-section') === sectionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update URL hash
|
||||||
|
window.location.hash = `#${sectionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nav link clicks
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const sectionId = link.getAttribute('data-section');
|
||||||
|
showSection(sectionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle initial load
|
||||||
|
const initialSection = window.location.hash ? window.location.hash.substring(1) : 'aplikace';
|
||||||
|
showSection(initialSection);
|
||||||
|
|
||||||
|
// Handle browser back/forward
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
const sectionId = window.location.hash ? window.location.hash.substring(1) : 'aplikace';
|
||||||
|
showSection(sectionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function saveBanner(event) {
|
async function saveBanner(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -4295,6 +4508,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Initialize banner image upload functionality
|
// Initialize banner image upload functionality
|
||||||
const dragDropArea = document.getElementById('dragDropArea');
|
const dragDropArea = document.getElementById('dragDropArea');
|
||||||
|
|
||||||
|
// Initialize credentials form
|
||||||
|
setupCredentialsForm();
|
||||||
|
|
||||||
|
// Navigation handling
|
||||||
|
setupNavigation();
|
||||||
const uploadImageBtn = document.getElementById('uploadImageBtn');
|
const uploadImageBtn = document.getElementById('uploadImageBtn');
|
||||||
const bannerImageInput = document.getElementById('bannerImage');
|
const bannerImageInput = document.getElementById('bannerImage');
|
||||||
|
|
||||||
|
|||||||
+149
-32
@@ -4,85 +4,202 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Admin Login - PP Kunovice</title>
|
<title>Admin Login - PP Kunovice</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
<style>
|
<style>
|
||||||
body {
|
:root {
|
||||||
font-family: Arial, sans-serif;
|
--primary: #2563eb; /* Blue-600 from Tailwind */
|
||||||
background-color: #f5f5f5;
|
--primary-hover: #1d4ed8; /* Blue-700 */
|
||||||
|
--error: #ef4444;
|
||||||
|
--text: #1f2937; /* Gray-800 */
|
||||||
|
--text-light: #6b7280; /* Gray-500 */
|
||||||
|
--bg: #f3f4f6; /* Gray-100 */
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--border: #e5e7eb; /* Gray-200 */
|
||||||
|
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100vh;
|
padding: 1.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
background-color: white;
|
background: var(--card-bg);
|
||||||
padding: 2rem;
|
padding: 2.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 1rem;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 420px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-top: 4px solid var(--primary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header {
|
.login-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header h1 {
|
.login-header h1 {
|
||||||
color: #333;
|
color: var(--primary);
|
||||||
margin: 0;
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
color: var(--text-light);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.25rem;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
color: #555;
|
color: var(--text);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem 1rem;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 0.375rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background-color: white;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||||
|
border-left-width: 2px;
|
||||||
|
border-left-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
.login-button {
|
.login-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem 1.5rem;
|
||||||
background-color: #4CAF50;
|
background-color: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 0.5rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.3s;
|
transition: all 0.2s ease;
|
||||||
}
|
|
||||||
.login-button:hover {
|
|
||||||
background-color: #45a049;
|
|
||||||
}
|
|
||||||
.error-message {
|
|
||||||
color: #f44336;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: var(--error);
|
||||||
|
text-align: center;
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 0.5rem;
|
||||||
display: none;
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-container {
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<h1>Admin Login</h1>
|
<h1>Přihlášení do administrace</h1>
|
||||||
|
<p>Pro pokračování se prosím přihlaste</p>
|
||||||
</div>
|
</div>
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Uživatelské jméno</label>
|
<label for="username">Uživatelské jméno</label>
|
||||||
<input type="text" id="username" name="username" required>
|
<input type="text" id="username" name="username" required
|
||||||
|
placeholder="Zadejte své uživatelské jméno">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Heslo</label>
|
<label for="password">Heslo</label>
|
||||||
<input type="password" id="password" name="password" required>
|
<input type="password" id="password" name="password" required
|
||||||
|
placeholder="Zadejte své heslo">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="login-button">Přihlásit se</button>
|
<button type="submit" class="login-button">
|
||||||
|
<i class="fas fa-sign-in-alt"></i>
|
||||||
|
<span>Přihlásit se</span>
|
||||||
|
</button>
|
||||||
<div id="errorMessage" class="error-message">
|
<div id="errorMessage" class="error-message">
|
||||||
Chybné přihlašovací údaje
|
Chybné přihlašovací údaje
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -18,6 +19,17 @@ type Credentials struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateCredentialsRequest struct {
|
||||||
|
CurrentUsername string `json:"currentUsername"`
|
||||||
|
CurrentPassword string `json:"currentPassword"`
|
||||||
|
NewUsername string `json:"newUsername"`
|
||||||
|
NewPassword string `json:"newPassword"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CredentialsResponse struct {
|
||||||
|
IsDefaultCredentials bool `json:"isDefaultCredentials"`
|
||||||
|
}
|
||||||
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
@@ -27,9 +39,17 @@ var (
|
|||||||
// In production, use environment variable for JWT key
|
// In production, use environment variable for JWT key
|
||||||
jwtKey = getJWTKey()
|
jwtKey = getJWTKey()
|
||||||
|
|
||||||
adminUsername = "admin"
|
// Default credentials
|
||||||
// In a real app, store hashed password and retrieve from a secure storage
|
defaultUsername = "admin"
|
||||||
adminPasswordHash = mustHashPassword("admin") // Default password, should be changed after first login
|
defaultPassword = "admin"
|
||||||
|
defaultPasswordHash = mustHashPassword(defaultPassword)
|
||||||
|
|
||||||
|
// Current credentials (in-memory, would be from DB in production)
|
||||||
|
adminUsername = defaultUsername
|
||||||
|
adminPasswordHash = defaultPasswordHash
|
||||||
|
|
||||||
|
// Mutex for thread-safe credential updates
|
||||||
|
credentialsMutex sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func getJWTKey() []byte {
|
func getJWTKey() []byte {
|
||||||
@@ -48,32 +68,36 @@ func mustHashPassword(password string) string {
|
|||||||
return string(hash)
|
return string(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticateUser(creds Credentials) (string, error) {
|
func authenticateUser(creds Credentials) (string, bool, error) {
|
||||||
// In a real app, verify against a database
|
credentialsMutex.RLock()
|
||||||
|
defer credentialsMutex.RUnlock()
|
||||||
|
|
||||||
if creds.Username != adminUsername {
|
if creds.Username != adminUsername {
|
||||||
return "", errors.New("invalid credentials")
|
return "", false, errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(adminPasswordHash), []byte(creds.Password)); err != nil {
|
if err := bcrypt.CompareHashAndPassword([]byte(adminPasswordHash), []byte(creds.Password)); err != nil {
|
||||||
return "", errors.New("invalid credentials")
|
return "", false, errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create JWT token
|
// Check if using default credentials
|
||||||
|
isDefault := creds.Username == defaultUsername && bcrypt.CompareHashAndPassword(
|
||||||
|
[]byte(defaultPasswordHash), []byte(creds.Password)) == nil
|
||||||
|
|
||||||
|
tokenString, err := createToken(creds.Username)
|
||||||
|
return tokenString, isDefault, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func createToken(username string) (string, error) {
|
||||||
expirationTime := time.Now().Add(24 * time.Hour)
|
expirationTime := time.Now().Add(24 * time.Hour)
|
||||||
claims := &Claims{
|
claims := &Claims{
|
||||||
Username: creds.Username,
|
Username: username,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
tokenString, err := token.SignedString(jwtKey)
|
return token.SignedString(jwtKey)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenString, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyToken(tokenString string) (*Claims, error) {
|
func verifyToken(tokenString string) (*Claims, error) {
|
||||||
@@ -126,27 +150,63 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginHandler handles user login
|
||||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var creds Credentials
|
var creds Credentials
|
||||||
err := json.NewDecoder(r.Body).Decode(&creds)
|
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
|
||||||
if err != nil {
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
http.Error(w, `{"error":"Invalid request body"}`, http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := authenticateUser(creds)
|
tokenString, isDefault, err := authenticateUser(creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"Invalid credentials"}`, http.StatusUnauthorized)
|
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"token": token,
|
"token": tokenString,
|
||||||
|
"isDefaultCredentials": isDefault,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCredentialsHandler handles credential updates
|
||||||
|
func UpdateCredentialsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateCreds UpdateCredentialsRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&updateCreds); err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate current credentials
|
||||||
|
if _, _, err := authenticateUser(Credentials{
|
||||||
|
Username: updateCreds.CurrentUsername,
|
||||||
|
Password: updateCreds.CurrentPassword,
|
||||||
|
}); err != nil {
|
||||||
|
http.Error(w, "Invalid current credentials", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update credentials
|
||||||
|
credentialsMutex.Lock()
|
||||||
|
defer credentialsMutex.Unlock()
|
||||||
|
|
||||||
|
adminUsername = updateCreds.NewUsername
|
||||||
|
adminPasswordHash = mustHashPassword(updateCreds.NewPassword)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]bool{
|
||||||
|
"success": true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="http://webportal:8080" class="hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="hover:text-brand-light-blue">Kontakt</a>
|
||||||
|
<a href="http://webportal/rezervace-aut" class="hover:text-brand-light-blue">Rezervace aut</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@
|
|||||||
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
||||||
|
<a href="http://webportal/rezervace-aut" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Rezervace aut</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -319,6 +321,7 @@
|
|||||||
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
||||||
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
||||||
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
||||||
|
<li><a href="http://webportal/rezervace-aut" class="hover:text-white">Rezervace aut</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -549,6 +549,7 @@
|
|||||||
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
||||||
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
||||||
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
||||||
|
<li><a href="http://webportal/rezervace-aut" class="hover:text-white">Rezervace aut</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="http://webportal:8080" class="hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="hover:text-brand-light-blue">Kontakt</a>
|
||||||
|
<a href="http://webportal/rezervace-aut" class="hover:text-brand-light-blue">Rezervace aut</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
||||||
|
<a href="http://webportal/rezervace-aut" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Rezervace aut</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -247,6 +249,7 @@
|
|||||||
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
||||||
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
||||||
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
||||||
|
<li><a href="http://webportal/rezervace-aut" class="hover:text-white">Rezervace aut</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ func main() {
|
|||||||
|
|
||||||
// Authentication routes
|
// Authentication routes
|
||||||
r.HandleFunc("/api/login", LoginHandler).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/login", LoginHandler).Methods("POST", "OPTIONS")
|
||||||
|
r.HandleFunc("/api/update-credentials", UpdateCredentialsHandler).Methods("POST", "OPTIONS")
|
||||||
|
|
||||||
// Public endpoints (must be defined before protected ones)
|
// Public endpoints (must be defined before protected ones)
|
||||||
r.HandleFunc("/api/banner", GetBannerHandler).Methods("GET", "OPTIONS")
|
r.HandleFunc("/api/banner", GetBannerHandler).Methods("GET", "OPTIONS")
|
||||||
|
|||||||
+3
-17
@@ -922,6 +922,7 @@
|
|||||||
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="http://webportal:8080" class="hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="hover:text-brand-light-blue">Kontakt</a>
|
||||||
|
<a href="http://webportal/rezervace-aut" class="hover:text-brand-light-blue">Rezervace aut</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -933,6 +934,7 @@
|
|||||||
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
||||||
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
||||||
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
||||||
|
<a href="http://webportal/rezervace-aut" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Rezervace aut</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -970,20 +972,6 @@
|
|||||||
<span class="vehicle-badge vehicle-škoda-fabia">
|
<span class="vehicle-badge vehicle-škoda-fabia">
|
||||||
<i class="fas fa-car"></i>Škoda Fabia - 1Z3 5789
|
<i class="fas fa-car"></i>Škoda Fabia - 1Z3 5789
|
||||||
</span>
|
</span>
|
||||||
</button> <button class="vehicle-filter-btn" data-vehicle="BMW 218d - 6Z5 4739">
|
|
||||||
<span class="vehicle-badge vehicle-bmw-218d">
|
|
||||||
<i class="fas fa-car-side"></i>BMW 218d - 6Z5 4739
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button class="vehicle-filter-btn" data-vehicle="BMW 218d - 6Z5 4740">
|
|
||||||
<span class="vehicle-badge vehicle-bmw-218d">
|
|
||||||
<i class="fas fa-car-side"></i>BMW 218d - 6Z5 4740
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button class="vehicle-filter-btn" data-vehicle="Škoda Superb - 2BY 2398">
|
|
||||||
<span class="vehicle-badge vehicle-škoda-superb">
|
|
||||||
<i class="fas fa-car-side"></i>Škoda Superb - 2BY 2398
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1044,9 +1032,6 @@
|
|||||||
<option value="VW Caddy - 4Z1 8241">VW Caddy - 4Z1 8241</option>
|
<option value="VW Caddy - 4Z1 8241">VW Caddy - 4Z1 8241</option>
|
||||||
<option value="VW Golf - 5Z5 8694">VW Golf - 5Z5 8694</option>
|
<option value="VW Golf - 5Z5 8694">VW Golf - 5Z5 8694</option>
|
||||||
<option value="Škoda Fabia - 1Z3 5789">Škoda Fabia - 1Z3 5789</option>
|
<option value="Škoda Fabia - 1Z3 5789">Škoda Fabia - 1Z3 5789</option>
|
||||||
<option value="BMW 218d - 6Z5 4739">BMW 218d - 6Z5 4739</option>
|
|
||||||
<option value="BMW 218d - 6Z5 4740">BMW 218d - 6Z5 4740</option>
|
|
||||||
<option value="Škoda Superb - 2BY 2398">Škoda Superb - 2BY 2398</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<!-- High Traffic Warning -->
|
<!-- High Traffic Warning -->
|
||||||
@@ -1173,6 +1158,7 @@
|
|||||||
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
||||||
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
||||||
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
||||||
|
<li><a href="http://webportal/rezervace-aut" class="hover:text-white">Rezervace aut</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user