mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-04 04:22:58 +00:00
Add files via upload
This commit is contained in:
+415
@@ -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 := `<!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
|
||||
}
|
||||
@@ -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 := `<!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)
|
||||
}
|
||||
+220
-2
@@ -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;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
@@ -54,6 +77,19 @@
|
||||
<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://webportal/kontakt" class="hover:text-brand-light-blue">Kontakt</a>
|
||||
<div id="admin-link" class="hidden">
|
||||
<a href="/admin" class="hover:text-brand-light-blue">Administrace</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<div id="user-info" class="hidden">
|
||||
<span id="username" class="text-sm"></span>
|
||||
<button id="logout-btn" class="text-sm hover:text-brand-light-blue">Odhlásit</button>
|
||||
</div>
|
||||
<div id="login-btn" class="text-sm hover:text-brand-light-blue">
|
||||
<button onclick="showLoginModal()">Přihlásit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,6 +101,9 @@
|
||||
<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="webportal/kontakt" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
||||
<div id="mobile-admin-link" class="hidden">
|
||||
<a href="/admin" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Administrace</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -87,6 +126,24 @@
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<!-- Login Modal -->
|
||||
<div id="login-modal" class="auth-container">
|
||||
<div class="auth-modal">
|
||||
<h2 class="text-2xl font-bold mb-4 text-center">Přihlášení</h2>
|
||||
<form id="login-form" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Uživatelské jméno</label>
|
||||
<input type="text" id="username" name="username" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-blue">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Heslo</label>
|
||||
<input type="password" id="password" name="password" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-blue">
|
||||
</div>
|
||||
<button type="submit" class="w-full bg-brand-blue text-white py-2 px-4 rounded-md hover:bg-brand-light-blue transition-colors">Přihlásit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mb-8 max-w-xl mx-auto">
|
||||
<div class="relative">
|
||||
@@ -99,7 +156,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Apps Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" id="apps-grid">
|
||||
<!-- 1. Car trips app -->
|
||||
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-blue-600" data-name="zápis cest aut project">
|
||||
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-blue-100 text-blue-600 mb-4">
|
||||
@@ -180,7 +237,168 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
|
||||
<!-- Admin Panel -->
|
||||
<div id="admin-panel" class="admin-panel">
|
||||
<div class="p-6 bg-white rounded-lg shadow">
|
||||
<h2 class="text-2xl font-bold mb-4">Správa aplikací</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Admin cards will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Authentication state
|
||||
let authToken = localStorage.getItem('authToken');
|
||||
let currentUser = null;
|
||||
|
||||
// Show/hide login modal
|
||||
function showLoginModal() {
|
||||
document.getElementById('login-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideLoginModal() {
|
||||
document.getElementById('login-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Handle login form submission
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: formData.get('username'),
|
||||
password: formData.get('password'),
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
authToken = data.token;
|
||||
currentUser = { username: formData.get('username'), role: data.role };
|
||||
localStorage.setItem('authToken', authToken);
|
||||
updateAuthUI();
|
||||
hideLoginModal();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Update UI based on auth state
|
||||
function updateAuthUI() {
|
||||
const userDiv = document.getElementById('user-info');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const adminLink = document.getElementById('admin-link');
|
||||
const mobileAdminLink = document.getElementById('mobile-admin-link');
|
||||
|
||||
if (currentUser) {
|
||||
userDiv.classList.remove('hidden');
|
||||
loginBtn.classList.add('hidden');
|
||||
document.getElementById('username').textContent = currentUser.username;
|
||||
|
||||
if (currentUser.role === 'admin') {
|
||||
adminLink.classList.remove('hidden');
|
||||
mobileAdminLink.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
userDiv.classList.add('hidden');
|
||||
loginBtn.classList.remove('hidden');
|
||||
adminLink.classList.add('hidden');
|
||||
mobileAdminLink.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle logout
|
||||
document.getElementById('logout-btn').addEventListener('click', async () => {
|
||||
const response = await fetch('/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
authToken = null;
|
||||
currentUser = null;
|
||||
localStorage.removeItem('authToken');
|
||||
updateAuthUI();
|
||||
}
|
||||
});
|
||||
|
||||
// Load cards on page load
|
||||
async function loadCards() {
|
||||
try {
|
||||
const response = await fetch('/api/cards');
|
||||
const cards = await response.json();
|
||||
|
||||
const grid = document.getElementById('apps-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
cards.forEach(card => {
|
||||
if (card.enabled) {
|
||||
const cardElement = document.createElement('div');
|
||||
cardElement.className = 'card bg-white rounded-xl shadow p-6 border-t-4';
|
||||
cardElement.style.borderColor = card.color;
|
||||
cardElement.innerHTML = `
|
||||
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-${card.color}-100 text-${card.color}-600 mb-4">
|
||||
<i class="fas ${card.icon} text-2xl"></i>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-2">${card.title}</h2>
|
||||
<p class="text-gray-600 mb-4">${card.description}</p>
|
||||
<a href="${card.link}" class="block text-center bg-${card.color}-600 hover:bg-${card.color}-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||
Otevřít aplikaci
|
||||
</a>
|
||||
`;
|
||||
grid.appendChild(cardElement);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading cards:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (authToken) {
|
||||
// Verify token
|
||||
fetch('/api/verify', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
updateAuthUI();
|
||||
} else {
|
||||
authToken = null;
|
||||
localStorage.removeItem('authToken');
|
||||
updateAuthUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
loadCards();
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('search').addEventListener('input', (e) => {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const cards = document.querySelectorAll('#apps-grid .card');
|
||||
|
||||
cards.forEach(card => {
|
||||
const title = card.querySelector('h2').textContent.toLowerCase();
|
||||
const description = card.querySelector('p').textContent.toLowerCase();
|
||||
const isVisible = title.includes(searchTerm) || description.includes(searchTerm);
|
||||
card.style.display = isVisible ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<footer class="bg-gray-800 text-gray-400 py-8 mt-12">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
|
||||
@@ -80,6 +80,27 @@ func main() {
|
||||
time.Sleep(2 * time.Second)
|
||||
http.Redirect(w, r, "http://webportal:8080/", http.StatusFound)
|
||||
}))
|
||||
// Authentication routes
|
||||
http.HandleFunc("/login", enableCORS(handleLogin))
|
||||
http.HandleFunc("/logout", enableCORS(handleLogout))
|
||||
|
||||
// Admin routes (protected)
|
||||
http.HandleFunc("/admin", enableCORS(requireAdminAuth(handleAdmin)))
|
||||
http.HandleFunc("/admin/cards", enableCORS(requireAdminAuth(handleAdminCards)))
|
||||
|
||||
http.HandleFunc("/admin/cards/", enableCORS(requireAdminAuth(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if strings.HasSuffix(path, "/toggle") {
|
||||
handleAdminCardToggle(w, r)
|
||||
} else if r.Method == "DELETE" {
|
||||
handleAdminCardDelete(w, r)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
})))
|
||||
|
||||
// Public API to get cards for homepage
|
||||
http.HandleFunc("/api/cards", enableCORS(handleGetCards))
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
|
||||
Reference in New Issue
Block a user