mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-05 04:52:58 +00:00
Delete admin directory
This commit is contained in:
@@ -1,415 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
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 := `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Přihlášení - Správa</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
background: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header h1 {
|
|
||||||
color: #333;
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header p {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: 2px solid #e1e5e9;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: #fee;
|
|
||||||
color: #c33;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: none;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-container">
|
|
||||||
<div class="login-header">
|
|
||||||
<h1>Přihlášení</h1>
|
|
||||||
<p>Administrátorské rozhraní</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="error-message" id="errorMessage"></div>
|
|
||||||
|
|
||||||
<form id="loginForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Uživatelské jméno</label>
|
|
||||||
<input type="text" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Heslo</label>
|
|
||||||
<input type="password" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="login-button" id="loginButton">
|
|
||||||
Přihlásit se
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="loading" id="loading">
|
|
||||||
Přihlašování...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const username = document.getElementById('username').value;
|
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
const errorDiv = document.getElementById('errorMessage');
|
|
||||||
const loginButton = document.getElementById('loginButton');
|
|
||||||
const loading = document.getElementById('loading');
|
|
||||||
|
|
||||||
// Reset error
|
|
||||||
errorDiv.style.display = 'none';
|
|
||||||
loginButton.disabled = true;
|
|
||||||
loading.style.display = 'block';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
|
||||||
password: password
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
localStorage.setItem('authToken', data.token);
|
|
||||||
localStorage.setItem('userRole', data.role);
|
|
||||||
window.location.href = '/admin';
|
|
||||||
} else {
|
|
||||||
errorDiv.textContent = data.message;
|
|
||||||
errorDiv.style.display = 'block';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorDiv.textContent = 'Chyba při přihlašování. Zkuste to znovu.';
|
|
||||||
errorDiv.style.display = 'block';
|
|
||||||
} finally {
|
|
||||||
loginButton.disabled = false;
|
|
||||||
loading.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
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
|
|
||||||
}
|
|
||||||
-733
@@ -1,733 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
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 := `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Administrace</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-btn {
|
|
||||||
background: rgba(255,255,255,0.2);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-btn:hover {
|
|
||||||
background: rgba(255,255,255,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 2rem auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section h2 {
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom: 2px solid #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-item {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-toggle {
|
|
||||||
width: 50px;
|
|
||||||
height: 24px;
|
|
||||||
background: #ccc;
|
|
||||||
border-radius: 12px;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-toggle.active {
|
|
||||||
background: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-toggle::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: white;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
transition: transform 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-toggle.active::before {
|
|
||||||
transform: translateX(26px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus,
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
background: #5a67d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: #e53e3e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #c53030;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-card-btn {
|
|
||||||
background: #38a169;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-card-btn:hover {
|
|
||||||
background: #2f855a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
margin: 5% auto;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
color: #aaa;
|
|
||||||
float: right;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close:hover {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-message {
|
|
||||||
background: #c6f6d5;
|
|
||||||
color: #22543d;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: #fed7d7;
|
|
||||||
color: #822727;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.header {
|
|
||||||
padding: 1rem;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1>Administrace</h1>
|
|
||||||
<div class="user-info">
|
|
||||||
<span>Přihlášen jako: <strong>{{.Username}}</strong></span>
|
|
||||||
<button class="logout-btn" onclick="logout()">Odhlásit se</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="section">
|
|
||||||
<h2>Správa karet hlavní stránky</h2>
|
|
||||||
<div class="success-message" id="successMessage"></div>
|
|
||||||
<div class="error-message" id="errorMessage"></div>
|
|
||||||
|
|
||||||
<button class="add-card-btn" onclick="openAddCardModal()">Přidat novou kartu</button>
|
|
||||||
|
|
||||||
<div class="cards-grid" id="cardsGrid">
|
|
||||||
<!-- Cards will be loaded here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add/Edit Card Modal -->
|
|
||||||
<div id="cardModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close" onclick="closeCardModal()">×</span>
|
|
||||||
<h3 id="modalTitle">Přidat kartu</h3>
|
|
||||||
<form id="cardForm">
|
|
||||||
<input type="hidden" id="cardId" name="id">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cardTitle">Název</label>
|
|
||||||
<input type="text" id="cardTitle" name="title" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cardDescription">Popis</label>
|
|
||||||
<textarea id="cardDescription" name="description" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cardIcon">Ikona (emoji nebo text)</label>
|
|
||||||
<input type="text" id="cardIcon" name="icon">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cardLink">Odkaz</label>
|
|
||||||
<input type="text" id="cardLink" name="link" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cardColor">Barva</label>
|
|
||||||
<input type="color" id="cardColor" name="color" value="#004990">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cardOrder">Pořadí</label>
|
|
||||||
<input type="number" id="cardOrder" name="order" min="1" value="1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn">Uložit</button>
|
|
||||||
<button type="button" class="btn" onclick="closeCardModal()">Zrušit</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let cards = [];
|
|
||||||
|
|
||||||
// Load cards on page load
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
loadCards();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadCards() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/admin/cards', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': localStorage.getItem('authToken') || ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
cards = await response.json();
|
|
||||||
renderCards();
|
|
||||||
} else {
|
|
||||||
showError('Chyba při načítání karet');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showError('Chyba při načítání karet');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCards() {
|
|
||||||
const grid = document.getElementById('cardsGrid');
|
|
||||||
grid.innerHTML = '';
|
|
||||||
|
|
||||||
cards.sort((a, b) => a.order - b.order).forEach(card => {
|
|
||||||
const cardElement = document.createElement('div');
|
|
||||||
cardElement.className = 'card-item';
|
|
||||||
cardElement.innerHTML = ` + "`" + `
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-title">${card.icon} ${card.title}</div>
|
|
||||||
<div class="card-toggle ${card.enabled ? 'active' : ''}"
|
|
||||||
onclick="toggleCard('${card.id}')"></div>
|
|
||||||
</div>
|
|
||||||
<p><strong>Popis:</strong> ${card.description}</p>
|
|
||||||
<p><strong>Odkaz:</strong> ${card.link}</p>
|
|
||||||
<p><strong>Barva:</strong> <span style="background: ${card.color}; padding: 2px 8px; color: white; border-radius: 3px;">${card.color}</span></p>
|
|
||||||
<p><strong>Pořadí:</strong> ${card.order}</p>
|
|
||||||
<div style="margin-top: 1rem;">
|
|
||||||
<button class="btn" onclick="editCard('${card.id}')">Upravit</button>
|
|
||||||
<button class="btn btn-danger" onclick="deleteCard('${card.id}')">Smazat</button>
|
|
||||||
</div>
|
|
||||||
` + "`" + `;
|
|
||||||
grid.appendChild(cardElement);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleCard(cardId) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(` + "`" + `/admin/cards/${cardId}/toggle` + "`" + `, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': localStorage.getItem('authToken') || ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await loadCards();
|
|
||||||
showSuccess('Karta byla aktualizována');
|
|
||||||
} else {
|
|
||||||
showError('Chyba při aktualizaci karty');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showError('Chyba při aktualizaci karty');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAddCardModal() {
|
|
||||||
document.getElementById('modalTitle').textContent = 'Přidat kartu';
|
|
||||||
document.getElementById('cardForm').reset();
|
|
||||||
document.getElementById('cardId').value = '';
|
|
||||||
document.getElementById('cardModal').style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function editCard(cardId) {
|
|
||||||
const card = cards.find(c => c.id === cardId);
|
|
||||||
if (!card) return;
|
|
||||||
|
|
||||||
document.getElementById('modalTitle').textContent = 'Upravit kartu';
|
|
||||||
document.getElementById('cardId').value = card.id;
|
|
||||||
document.getElementById('cardTitle').value = card.title;
|
|
||||||
document.getElementById('cardDescription').value = card.description;
|
|
||||||
document.getElementById('cardIcon').value = card.icon;
|
|
||||||
document.getElementById('cardLink').value = card.link;
|
|
||||||
document.getElementById('cardColor').value = card.color;
|
|
||||||
document.getElementById('cardOrder').value = card.order;
|
|
||||||
document.getElementById('cardModal').style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeCardModal() {
|
|
||||||
document.getElementById('cardModal').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('cardForm').addEventListener('submit', async function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const formData = new FormData(e.target);
|
|
||||||
const cardData = {
|
|
||||||
id: formData.get('id') || generateId(),
|
|
||||||
title: formData.get('title'),
|
|
||||||
description: formData.get('description'),
|
|
||||||
icon: formData.get('icon'),
|
|
||||||
link: formData.get('link'),
|
|
||||||
color: formData.get('color'),
|
|
||||||
order: parseInt(formData.get('order')),
|
|
||||||
enabled: true
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/admin/cards', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': localStorage.getItem('authToken') || ''
|
|
||||||
},
|
|
||||||
body: JSON.stringify(cardData)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
closeCardModal();
|
|
||||||
await loadCards();
|
|
||||||
showSuccess('Karta byla uložena');
|
|
||||||
} else {
|
|
||||||
showError('Chyba při ukládání karty');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showError('Chyba při ukládání karty');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function deleteCard(cardId) {
|
|
||||||
if (!confirm('Opravdu chcete smazat tuto kartu?')) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(` + "`" + `/admin/cards/${cardId}` + "`" + `, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Authorization': localStorage.getItem('authToken') || ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await loadCards();
|
|
||||||
showSuccess('Karta byla smazána');
|
|
||||||
} else {
|
|
||||||
showError('Chyba při mazání karty');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showError('Chyba při mazání karty');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
try {
|
|
||||||
await fetch('/logout', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': localStorage.getItem('authToken') || ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.removeItem('authToken');
|
|
||||||
localStorage.removeItem('userRole');
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateId() {
|
|
||||||
return 'card-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSuccess(message) {
|
|
||||||
const successDiv = document.getElementById('successMessage');
|
|
||||||
successDiv.textContent = message;
|
|
||||||
successDiv.style.display = 'block';
|
|
||||||
setTimeout(() => {
|
|
||||||
successDiv.style.display = 'none';
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
const errorDiv = document.getElementById('errorMessage');
|
|
||||||
errorDiv.textContent = message;
|
|
||||||
errorDiv.style.display = 'block';
|
|
||||||
setTimeout(() => {
|
|
||||||
errorDiv.style.display = 'none';
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close modal when clicking outside
|
|
||||||
window.onclick = function(event) {
|
|
||||||
const modal = document.getElementById('cardModal');
|
|
||||||
if (event.target === modal) {
|
|
||||||
closeCardModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user