diff --git a/account.go b/account.go new file mode 100644 index 0000000..e0ab768 --- /dev/null +++ b/account.go @@ -0,0 +1,415 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "net/http" + "time" +) + +type User struct { + Username string `json:"username"` + Password string `json:"password"` + Role string `json:"role"` +} + +type Session struct { + Token string + Username string + Role string + ExpiresAt time.Time +} + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type LoginResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Token string `json:"token,omitempty"` + Role string `json:"role,omitempty"` +} + +// In-memory storage (replace with database in production) +var ( + users = map[string]User{ + "admin": { + Username: "admin", + Password: "admin123", // In production, use hashed passwords + Role: "admin", + }, + } + sessions = make(map[string]Session) +) + +func generateToken() (string, error) { + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(bytes), nil +} + +func handleLogin(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == "GET" { + // Serve login page + tmpl := ` + + + + + Přihlášení - Správa + + + +
+
+

Přihlášení

+

Administrátorské rozhraní

+
+ +
+ +
+
+ + +
+ +
+ + +
+ + +
+ +
+ Přihlašování... +
+
+ + + +` + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(tmpl)) + return + } + + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(LoginResponse{ + Success: false, + Message: "Method not allowed", + }) + return + } + + var loginReq LoginRequest + if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(LoginResponse{ + Success: false, + Message: "Invalid request format", + }) + return + } + + // Check credentials + user, exists := users[loginReq.Username] + if !exists || user.Password != loginReq.Password { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(LoginResponse{ + Success: false, + Message: "Neplatné přihlašovací údaje", + }) + return + } + + // Generate session token + token, err := generateToken() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(LoginResponse{ + Success: false, + Message: "Chyba při vytváření relace", + }) + return + } + + // Store session + sessions[token] = Session{ + Token: token, + Username: user.Username, + Role: user.Role, + ExpiresAt: time.Now().Add(24 * time.Hour), + } + + json.NewEncoder(w).Encode(LoginResponse{ + Success: true, + Message: "Přihlášení úspěšné", + Token: token, + Role: user.Role, + }) +} + +func handleLogout(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + token := r.Header.Get("Authorization") + if token == "" { + // Try to get from cookie + if cookie, err := r.Cookie("authToken"); err == nil { + token = cookie.Value + } + } + + if token != "" { + delete(sessions, token) + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "Odhlášení úspěšné", + }) +} + +func requireAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token == "" { + // Try to get from cookie + if cookie, err := r.Cookie("authToken"); err == nil { + token = cookie.Value + } + } + + if token == "" { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + session, exists := sessions[token] + if !exists || time.Now().After(session.ExpiresAt) { + if exists { + delete(sessions, token) + } + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + // Extend session + session.ExpiresAt = time.Now().Add(24 * time.Hour) + sessions[token] = session + + next(w, r) + } +} + +func requireAdminAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token == "" { + // Try to get from cookie + if cookie, err := r.Cookie("authToken"); err == nil { + token = cookie.Value + } + } + + if token == "" { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + session, exists := sessions[token] + if !exists || time.Now().After(session.ExpiresAt) || session.Role != "admin" { + if exists { + delete(sessions, token) + } + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + // Extend session + session.ExpiresAt = time.Now().Add(24 * time.Hour) + sessions[token] = session + + next(w, r) + } +} + +func getCurrentUser(r *http.Request) *Session { + token := r.Header.Get("Authorization") + if token == "" { + if cookie, err := r.Cookie("authToken"); err == nil { + token = cookie.Value + } + } + + if token == "" { + return nil + } + + session, exists := sessions[token] + if !exists || time.Now().After(session.ExpiresAt) { + return nil + } + + return &session +} diff --git a/admin.go b/admin.go new file mode 100644 index 0000000..c5ea7cc --- /dev/null +++ b/admin.go @@ -0,0 +1,733 @@ +package main + +import ( + "encoding/json" + "html/template" + "net/http" + "strings" +) + +type GridCard struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Icon string `json:"icon"` + Link string `json:"link"` + Color string `json:"color"` + Order int `json:"order"` + Enabled bool `json:"enabled"` +} + +// In-memory storage for grid cards (replace with database in production) +var gridCards = []GridCard{ + { + ID: "evidence-aut", + Title: "Evidence aut", + Description: "Záznam o jízdách služebním autem", + Icon: "🚗", + Link: "/evidence-aut", + Color: "#004990", + Order: 1, + Enabled: true, + }, + { + ID: "kontakt", + Title: "Kontakt", + Description: "Kontaktní formulář", + Icon: "📧", + Link: "/kontakt", + Color: "#0072b0", + Order: 2, + Enabled: true, + }, +} + +func handleAdmin(w http.ResponseWriter, r *http.Request) { + user := getCurrentUser(r) + if user == nil { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + tmpl := ` + + + + + Administrace + + + +
+

Administrace

+
+ Přihlášen jako: {{.Username}} + +
+
+ +
+
+

Správa karet hlavní stránky

+
+
+ + + +
+ +
+
+
+ + + + + + +` + + t, err := template.New("admin").Parse(tmpl) + if err != nil { + http.Error(w, "Template error", http.StatusInternalServerError) + return + } + + data := struct { + Username string + Role string + }{ + Username: user.Username, + Role: user.Role, + } + + w.Header().Set("Content-Type", "text/html") + t.Execute(w, data) +} + +func handleAdminCards(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case "GET": + json.NewEncoder(w).Encode(gridCards) + + case "POST": + var card GridCard + if err := json.NewDecoder(r.Body).Decode(&card); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"}) + return + } + + // Check if updating existing card + found := false + for i, existingCard := range gridCards { + if existingCard.ID == card.ID { + gridCards[i] = card + found = true + break + } + } + + if !found { + gridCards = append(gridCards, card) + } + + json.NewEncoder(w).Encode(map[string]string{"message": "Card saved successfully"}) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"}) + } +} + +func handleAdminCardToggle(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"}) + return + } + + // Extract card ID from URL path + path := r.URL.Path + parts := strings.Split(path, "/") + if len(parts) < 4 { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Invalid card ID"}) + return + } + + cardID := parts[3] // /admin/cards/{id}/toggle + + for i, card := range gridCards { + if card.ID == cardID { + gridCards[i].Enabled = !gridCards[i].Enabled + json.NewEncoder(w).Encode(map[string]string{"message": "Card toggled successfully"}) + return + } + } + + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "Card not found"}) +} + +func handleAdminCardDelete(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method != "DELETE" { + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"}) + return + } + + // Extract card ID from URL path + path := r.URL.Path + parts := strings.Split(path, "/") + if len(parts) < 4 { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Invalid card ID"}) + return + } + + cardID := parts[3] // /admin/cards/{id} + + for i, card := range gridCards { + if card.ID == cardID { + // Remove card from slice + gridCards = append(gridCards[:i], gridCards[i+1:]...) + json.NewEncoder(w).Encode(map[string]string{"message": "Card deleted successfully"}) + return + } + } + + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "Card not found"}) +} + +func handleGetCards(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Filter only enabled cards and sort by order + var enabledCards []GridCard + for _, card := range gridCards { + if card.Enabled { + enabledCards = append(enabledCards, card) + } + } + + // Sort by order + for i := 0; i < len(enabledCards)-1; i++ { + for j := i + 1; j < len(enabledCards); j++ { + if enabledCards[i].Order > enabledCards[j].Order { + enabledCards[i], enabledCards[j] = enabledCards[j], enabledCards[i] + } + } + } + + json.NewEncoder(w).Encode(enabledCards) +} diff --git a/index.html b/index.html index a397a7e..fc10b4a 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,29 @@ transform: translateY(-5px); box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); } + .auth-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + .auth-modal { + background: white; + padding: 2rem; + border-radius: 10px; + width: 90%; + max-width: 400px; + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); + } + .admin-panel { + display: none; + }