diff --git a/admin-dashboard.html b/admin-dashboard.html index ce1fd88..7f05ea0 100644 --- a/admin-dashboard.html +++ b/admin-dashboard.html @@ -1004,11 +1004,12 @@

Admin Dashboard

- +
@@ -1062,7 +1063,62 @@ .logout-btn:hover { background-color: #c0392b; } - + + + +

Vítejte v administraci

@@ -3046,6 +3102,163 @@ async function loadBanner() { // Add submission flag at the top of the script 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) { event.preventDefault(); @@ -4295,6 +4508,12 @@ document.addEventListener('DOMContentLoaded', function() { // Initialize banner image upload functionality const dragDropArea = document.getElementById('dragDropArea'); + + // Initialize credentials form + setupCredentialsForm(); + + // Navigation handling + setupNavigation(); const uploadImageBtn = document.getElementById('uploadImageBtn'); const bannerImageInput = document.getElementById('bannerImage'); diff --git a/admin.html b/admin.html index eeaa5e9..b71a2b9 100644 --- a/admin.html +++ b/admin.html @@ -4,85 +4,202 @@ Admin Login - PP Kunovice + +
- +
- +
- +
Chybné přihlašovací údaje
diff --git a/auth.go b/auth.go index 0e719d6..9079049 100644 --- a/auth.go +++ b/auth.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strings" + "sync" "time" "github.com/golang-jwt/jwt/v5" @@ -18,6 +19,17 @@ type Credentials struct { 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 { Username string `json:"username"` jwt.RegisteredClaims @@ -27,9 +39,17 @@ var ( // In production, use environment variable for JWT key jwtKey = getJWTKey() - adminUsername = "admin" - // In a real app, store hashed password and retrieve from a secure storage - adminPasswordHash = mustHashPassword("admin") // Default password, should be changed after first login + // Default credentials + defaultUsername = "admin" + 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 { @@ -48,32 +68,36 @@ func mustHashPassword(password string) string { return string(hash) } -func authenticateUser(creds Credentials) (string, error) { - // In a real app, verify against a database +func authenticateUser(creds Credentials) (string, bool, error) { + credentialsMutex.RLock() + defer credentialsMutex.RUnlock() + 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 { - 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) claims := &Claims{ - Username: creds.Username, + Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), }, } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString(jwtKey) - if err != nil { - return "", err - } - - return tokenString, nil + return token.SignedString(jwtKey) } 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) { if r.Method != http.MethodPost { - http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed) + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var creds Credentials - err := json.NewDecoder(r.Body).Decode(&creds) - if err != nil { - http.Error(w, `{"error":"Invalid request body"}`, http.StatusBadRequest) + if err := json.NewDecoder(r.Body).Decode(&creds); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) return } - token, err := authenticateUser(creds) + tokenString, isDefault, err := authenticateUser(creds) if err != nil { - http.Error(w, `{"error":"Invalid credentials"}`, http.StatusUnauthorized) + http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "token": token, + json.NewEncoder(w).Encode(map[string]interface{}{ + "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, }) } diff --git a/evidence-aut.html b/evidence-aut.html index 6cabfd6..4dbb459 100644 --- a/evidence-aut.html +++ b/evidence-aut.html @@ -76,6 +76,7 @@ OSticket Kanboard Kontakt + Rezervace aut
@@ -87,6 +88,7 @@ OSticket Kanboard Kontakt + Rezervace aut @@ -319,6 +321,7 @@
  • Objednávka obědů
  • Technická podpora
  • Kontakty
  • +
  • Rezervace aut
  • diff --git a/index.html b/index.html index 3ce0390..4437cfa 100644 --- a/index.html +++ b/index.html @@ -549,6 +549,7 @@
  • Objednávka obědů
  • Technická podpora
  • Kontakty
  • +
  • Rezervace aut
  • diff --git a/kontakt/index.html b/kontakt/index.html index 1b7fa93..467389d 100644 --- a/kontakt/index.html +++ b/kontakt/index.html @@ -60,6 +60,7 @@ OSticket Kanboard Kontakt + Rezervace aut @@ -71,6 +72,7 @@ OSticket Kanboard Kontakt + Rezervace aut @@ -247,6 +249,7 @@
  • Objednávka obědů
  • Technická podpora
  • Kontakty
  • +
  • Rezervace aut
  • diff --git a/main.go b/main.go index 27caec6..2df33b6 100644 --- a/main.go +++ b/main.go @@ -141,6 +141,7 @@ func main() { // Authentication routes 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) r.HandleFunc("/api/banner", GetBannerHandler).Methods("GET", "OPTIONS") diff --git a/rezervace-aut.html b/rezervace-aut.html index 479c635..cfa1e92 100644 --- a/rezervace-aut.html +++ b/rezervace-aut.html @@ -922,6 +922,7 @@ OSticket Kanboard Kontakt + Rezervace aut @@ -933,6 +934,7 @@ OSticket Kanboard Kontakt + Rezervace aut @@ -970,20 +972,6 @@ Škoda Fabia - 1Z3 5789 - - - @@ -1044,9 +1032,6 @@ - - - @@ -1173,6 +1158,7 @@
  • Objednávka obědů
  • Technická podpora
  • Kontakty
  • +
  • Rezervace aut