This commit is contained in:
Tomas Dvorak
2025-05-26 11:39:32 +02:00
parent d3581993a7
commit 4ec4444a51
4 changed files with 1157 additions and 1160 deletions
+415 -415
View File
@@ -1,415 +1,415 @@
package main package admin
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"net/http" "net/http"
"time" "time"
) )
type User struct { type User struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Role string `json:"role"` Role string `json:"role"`
} }
type Session struct { type Session struct {
Token string Token string
Username string Username string
Role string Role string
ExpiresAt time.Time ExpiresAt time.Time
} }
type LoginRequest struct { type LoginRequest struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
type LoginResponse struct { type LoginResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
Role string `json:"role,omitempty"` Role string `json:"role,omitempty"`
} }
// In-memory storage (replace with database in production) // In-memory storage (replace with database in production)
var ( var (
users = map[string]User{ users = map[string]User{
"admin": { "admin": {
Username: "admin", Username: "admin",
Password: "admin123", // In production, use hashed passwords Password: "admin123", // In production, use hashed passwords
Role: "admin", Role: "admin",
}, },
} }
sessions = make(map[string]Session) sessions = make(map[string]Session)
) )
func generateToken() (string, error) { func generateToken() (string, error) {
bytes := make([]byte, 32) bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
return "", err return "", err
} }
return base64.URLEncoding.EncodeToString(bytes), nil return base64.URLEncoding.EncodeToString(bytes), nil
} }
func handleLogin(w http.ResponseWriter, r *http.Request) { func HandleLogin(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" { if r.Method == "GET" {
// Serve login page // Serve login page
tmpl := `<!DOCTYPE html> tmpl := `<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Přihlášení - Správa</title> <title>Přihlášení - Správa</title>
<style> <style>
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.login-container { .login-container {
background: white; background: white;
padding: 2rem; padding: 2rem;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
} }
.login-header { .login-header {
text-align: center; text-align: center;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.login-header h1 { .login-header h1 {
color: #333; color: #333;
font-size: 2rem; font-size: 2rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.login-header p { .login-header p {
color: #666; color: #666;
font-size: 0.9rem; font-size: 0.9rem;
} }
.form-group { .form-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.form-group label { .form-group label {
display: block; display: block;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: #333; color: #333;
font-weight: 500; font-weight: 500;
} }
.form-group input { .form-group input {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
border: 2px solid #e1e5e9; border: 2px solid #e1e5e9;
border-radius: 5px; border-radius: 5px;
font-size: 1rem; font-size: 1rem;
transition: border-color 0.3s; transition: border-color 0.3s;
} }
.form-group input:focus { .form-group input:focus {
outline: none; outline: none;
border-color: #667eea; border-color: #667eea;
} }
.login-button { .login-button {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border: none; border: none;
border-radius: 5px; border-radius: 5px;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: transform 0.2s; transition: transform 0.2s;
} }
.login-button:hover { .login-button:hover {
transform: translateY(-2px); transform: translateY(-2px);
} }
.login-button:disabled { .login-button:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
} }
.error-message { .error-message {
background: #fee; background: #fee;
color: #c33; color: #c33;
padding: 0.75rem; padding: 0.75rem;
border-radius: 5px; border-radius: 5px;
margin-bottom: 1rem; margin-bottom: 1rem;
display: none; display: none;
} }
.loading { .loading {
display: none; display: none;
text-align: center; text-align: center;
margin-top: 1rem; margin-top: 1rem;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="login-container"> <div class="login-container">
<div class="login-header"> <div class="login-header">
<h1>Přihlášení</h1> <h1>Přihlášení</h1>
<p>Administrátorské rozhraní</p> <p>Administrátorské rozhraní</p>
</div> </div>
<div class="error-message" id="errorMessage"></div> <div class="error-message" id="errorMessage"></div>
<form id="loginForm"> <form id="loginForm">
<div class="form-group"> <div class="form-group">
<label for="username">Uživatelské jméno</label> <label for="username">Uživatelské jméno</label>
<input type="text" id="username" name="username" required> <input type="text" id="username" name="username" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Heslo</label> <label for="password">Heslo</label>
<input type="password" id="password" name="password" required> <input type="password" id="password" name="password" required>
</div> </div>
<button type="submit" class="login-button" id="loginButton"> <button type="submit" class="login-button" id="loginButton">
Přihlásit se Přihlásit se
</button> </button>
</form> </form>
<div class="loading" id="loading"> <div class="loading" id="loading">
Přihlašování... Přihlašování...
</div> </div>
</div> </div>
<script> <script>
document.getElementById('loginForm').addEventListener('submit', async function(e) { document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const username = document.getElementById('username').value; const username = document.getElementById('username').value;
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
const errorDiv = document.getElementById('errorMessage'); const errorDiv = document.getElementById('errorMessage');
const loginButton = document.getElementById('loginButton'); const loginButton = document.getElementById('loginButton');
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
// Reset error // Reset error
errorDiv.style.display = 'none'; errorDiv.style.display = 'none';
loginButton.disabled = true; loginButton.disabled = true;
loading.style.display = 'block'; loading.style.display = 'block';
try { try {
const response = await fetch('/login', { const response = await fetch('/login', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
username: username, username: username,
password: password password: password
}) })
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
localStorage.setItem('authToken', data.token); localStorage.setItem('authToken', data.token);
localStorage.setItem('userRole', data.role); localStorage.setItem('userRole', data.role);
window.location.href = '/admin'; window.location.href = '/admin';
} else { } else {
errorDiv.textContent = data.message; errorDiv.textContent = data.message;
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
} }
} catch (error) { } catch (error) {
errorDiv.textContent = 'Chyba při přihlašování. Zkuste to znovu.'; errorDiv.textContent = 'Chyba při přihlašování. Zkuste to znovu.';
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
} finally { } finally {
loginButton.disabled = false; loginButton.disabled = false;
loading.style.display = 'none'; loading.style.display = 'none';
} }
}); });
</script> </script>
</body> </body>
</html>` </html>`
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(tmpl)) w.Write([]byte(tmpl))
return return
} }
if r.Method != "POST" { if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(LoginResponse{ json.NewEncoder(w).Encode(LoginResponse{
Success: false, Success: false,
Message: "Method not allowed", Message: "Method not allowed",
}) })
return return
} }
var loginReq LoginRequest var loginReq LoginRequest
if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil { if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(LoginResponse{ json.NewEncoder(w).Encode(LoginResponse{
Success: false, Success: false,
Message: "Invalid request format", Message: "Invalid request format",
}) })
return return
} }
// Check credentials // Check credentials
user, exists := users[loginReq.Username] user, exists := users[loginReq.Username]
if !exists || user.Password != loginReq.Password { if !exists || user.Password != loginReq.Password {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(LoginResponse{ json.NewEncoder(w).Encode(LoginResponse{
Success: false, Success: false,
Message: "Neplatné přihlašovací údaje", Message: "Neplatné přihlašovací údaje",
}) })
return return
} }
// Generate session token // Generate session token
token, err := generateToken() token, err := generateToken()
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(LoginResponse{ json.NewEncoder(w).Encode(LoginResponse{
Success: false, Success: false,
Message: "Chyba při vytváření relace", Message: "Chyba při vytváření relace",
}) })
return return
} }
// Store session // Store session
sessions[token] = Session{ sessions[token] = Session{
Token: token, Token: token,
Username: user.Username, Username: user.Username,
Role: user.Role, Role: user.Role,
ExpiresAt: time.Now().Add(24 * time.Hour), ExpiresAt: time.Now().Add(24 * time.Hour),
} }
json.NewEncoder(w).Encode(LoginResponse{ json.NewEncoder(w).Encode(LoginResponse{
Success: true, Success: true,
Message: "Přihlášení úspěšné", Message: "Přihlášení úspěšné",
Token: token, Token: token,
Role: user.Role, Role: user.Role,
}) })
} }
func handleLogout(w http.ResponseWriter, r *http.Request) { func HandleLogout(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
token := r.Header.Get("Authorization") token := r.Header.Get("Authorization")
if token == "" { if token == "" {
// Try to get from cookie // Try to get from cookie
if cookie, err := r.Cookie("authToken"); err == nil { if cookie, err := r.Cookie("authToken"); err == nil {
token = cookie.Value token = cookie.Value
} }
} }
if token != "" { if token != "" {
delete(sessions, token) delete(sessions, token)
} }
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"success": true, "success": true,
"message": "Odhlášení úspěšné", "message": "Odhlášení úspěšné",
}) })
} }
func requireAuth(next http.HandlerFunc) http.HandlerFunc { func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization") token := r.Header.Get("Authorization")
if token == "" { if token == "" {
// Try to get from cookie // Try to get from cookie
if cookie, err := r.Cookie("authToken"); err == nil { if cookie, err := r.Cookie("authToken"); err == nil {
token = cookie.Value token = cookie.Value
} }
} }
if token == "" { if token == "" {
http.Redirect(w, r, "/login", http.StatusFound) http.Redirect(w, r, "/login", http.StatusFound)
return return
} }
session, exists := sessions[token] session, exists := sessions[token]
if !exists || time.Now().After(session.ExpiresAt) { if !exists || time.Now().After(session.ExpiresAt) {
if exists { if exists {
delete(sessions, token) delete(sessions, token)
} }
http.Redirect(w, r, "/login", http.StatusFound) http.Redirect(w, r, "/login", http.StatusFound)
return return
} }
// Extend session // Extend session
session.ExpiresAt = time.Now().Add(24 * time.Hour) session.ExpiresAt = time.Now().Add(24 * time.Hour)
sessions[token] = session sessions[token] = session
next(w, r) next(w, r)
} }
} }
func requireAdminAuth(next http.HandlerFunc) http.HandlerFunc { func RequireAdminAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization") token := r.Header.Get("Authorization")
if token == "" { if token == "" {
// Try to get from cookie // Try to get from cookie
if cookie, err := r.Cookie("authToken"); err == nil { if cookie, err := r.Cookie("authToken"); err == nil {
token = cookie.Value token = cookie.Value
} }
} }
if token == "" { if token == "" {
http.Redirect(w, r, "/login", http.StatusFound) http.Redirect(w, r, "/login", http.StatusFound)
return return
} }
session, exists := sessions[token] session, exists := sessions[token]
if !exists || time.Now().After(session.ExpiresAt) || session.Role != "admin" { if !exists || time.Now().After(session.ExpiresAt) || session.Role != "admin" {
if exists { if exists {
delete(sessions, token) delete(sessions, token)
} }
http.Redirect(w, r, "/login", http.StatusFound) http.Redirect(w, r, "/login", http.StatusFound)
return return
} }
// Extend session // Extend session
session.ExpiresAt = time.Now().Add(24 * time.Hour) session.ExpiresAt = time.Now().Add(24 * time.Hour)
sessions[token] = session sessions[token] = session
next(w, r) next(w, r)
} }
} }
func getCurrentUser(r *http.Request) *Session { func GetCurrentUser(r *http.Request) *Session {
token := r.Header.Get("Authorization") token := r.Header.Get("Authorization")
if token == "" { if token == "" {
if cookie, err := r.Cookie("authToken"); err == nil { if cookie, err := r.Cookie("authToken"); err == nil {
token = cookie.Value token = cookie.Value
} }
} }
if token == "" { if token == "" {
return nil return nil
} }
session, exists := sessions[token] session, exists := sessions[token]
if !exists || time.Now().After(session.ExpiresAt) { if !exists || time.Now().After(session.ExpiresAt) {
return nil return nil
} }
return &session return &session
} }
File diff suppressed because it is too large Load Diff
-4
View File
@@ -1,7 +1,5 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
@@ -25,8 +23,6 @@ golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+9 -8
View File
@@ -15,6 +15,7 @@ import (
"time" "time"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
"ppve/admin" // Import the local admin package
) )
type TripEntry struct { type TripEntry struct {
@@ -81,26 +82,26 @@ func main() {
http.Redirect(w, r, "http://webportal:8080/", http.StatusFound) http.Redirect(w, r, "http://webportal:8080/", http.StatusFound)
})) }))
// Authentication routes // Authentication routes
http.HandleFunc("/login", enableCORS(handleLogin)) http.HandleFunc("/login", enableCORS(admin.HandleLogin))
http.HandleFunc("/logout", enableCORS(handleLogout)) http.HandleFunc("/logout", enableCORS(admin.HandleLogout))
// Admin routes (protected) // Admin routes (protected)
http.HandleFunc("/admin", enableCORS(requireAdminAuth(handleAdmin))) http.HandleFunc("/admin", enableCORS(admin.RequireAdminAuth(admin.HandleAdmin)))
http.HandleFunc("/admin/cards", enableCORS(requireAdminAuth(handleAdminCards))) http.HandleFunc("/admin/cards", enableCORS(admin.RequireAdminAuth(admin.HandleAdminCards)))
http.HandleFunc("/admin/cards/", enableCORS(requireAdminAuth(func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/admin/cards/", enableCORS(admin.RequireAdminAuth(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path path := r.URL.Path
if strings.HasSuffix(path, "/toggle") { if strings.HasSuffix(path, "/toggle") {
handleAdminCardToggle(w, r) admin.HandleAdminCardToggle(w, r)
} else if r.Method == "DELETE" { } else if r.Method == "DELETE" {
handleAdminCardDelete(w, r) admin.HandleAdminCardDelete(w, r)
} else { } else {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
} }
}))) })))
// Public API to get cards for homepage // Public API to get cards for homepage
http.HandleFunc("/api/cards", enableCORS(handleGetCards)) http.HandleFunc("/api/cards", enableCORS(admin.HandleGetCards))
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {