Add files via upload

This commit is contained in:
Tomáš Dvořák
2025-05-26 12:34:08 +02:00
committed by GitHub
parent cb9560e109
commit da051a507c
7 changed files with 541 additions and 19 deletions
+77
View File
@@ -0,0 +1,77 @@
# Admin Login System
This document provides information about the admin login system for the PP Kunovice web application.
## Default Admin Credentials
- **Username**: `admin`
- **Password**: `admin123`
**Important**: Change the default password after the first login in a production environment.
## Accessing the Admin Panel
1. Navigate to `/admin` in your web browser
2. Enter the admin credentials
3. After successful login, you'll be redirected to the admin dashboard
## API Endpoints
### Login
- **URL**: `/api/login`
- **Method**: `POST`
- **Content-Type**: `application/json`
- **Request Body**:
```json
{
"username": "admin",
"password": "admin123"
}
```
- **Success Response**:
- **Code**: 200 OK
- **Content**:
```json
{
"token": "jwt.token.here"
}
```
- **Error Response**:
- **Code**: 401 Unauthorized
- **Content**:
```json
{
"error": "Invalid credentials"
}
```
### Protected Endpoints
All protected endpoints require a valid JWT token in the `Authorization` header:
```
Authorization: Bearer <token>
```
## Environment Variables
- `JWT_SECRET`: Secret key used to sign JWT tokens (default: auto-generated)
- `PORT`: Port the server listens on (default: 80)
## Security Notes
1. Always use HTTPS in production
2. Change the default admin password
3. Set a strong `JWT_SECRET` environment variable in production
4. Consider implementing rate limiting for login attempts
5. Keep the server and dependencies up to date
## Development
To run the server in development mode:
```bash
go run .
```
The admin interface will be available at `http://localhost/admin`
+128
View File
@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - PP Kunovice</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.header {
background-color: #333;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
margin: 0;
font-size: 1.5rem;
}
.logout-btn {
background: none;
border: 1px solid white;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.logout-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.dashboard-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.card {
background-color: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card h3 {
margin-top: 0;
color: #333;
}
.card p {
color: #666;
margin-bottom: 0;
}
</style>
</head>
<body>
<div class="header">
<h1>Admin Dashboard</h1>
<button class="logout-btn" id="logoutBtn">Odhlásit se</button>
</div>
<div class="container">
<h2>Vítejte v administraci</h2>
<div class="dashboard-cards">
<div class="card">
<h3>Uživatelé</h3>
<p>Správa uživatelských účtů</p>
</div>
<div class="card">
<h3>Nastavení</h3>
<p>Konfigurace systému</p>
</div>
<div class="card">
<h3>Statistiky</h3>
<p>Přehled aktivit</p>
</div>
</div>
</div>
<script>
// Check if user is authenticated
const token = localStorage.getItem('token');
if (!token) {
window.location.href = '/admin.html';
}
// Logout functionality
document.getElementById('logoutBtn').addEventListener('click', function() {
localStorage.removeItem('token');
window.location.href = '/';
});
// Add token to all fetch requests
const originalFetch = window.fetch;
window.fetch = async function(resource, init = {}) {
// Set up the headers
const headers = new Headers(init.headers || {});
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
// Make the request
const response = await originalFetch(resource, {
...init,
headers
});
// If unauthorized, redirect to login
if (response.status === 401) {
localStorage.removeItem('token');
window.location.href = '/admin.html';
return response;
}
return response;
};
</script>
</body>
</html>
+131
View File
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - PP Kunovice</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.login-container {
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
color: #333;
margin: 0;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #555;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.login-button {
width: 100%;
padding: 0.75rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
}
.login-button:hover {
background-color: #45a049;
}
.error-message {
color: #f44336;
text-align: center;
margin-top: 1rem;
display: none;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>Admin Login</h1>
</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">Přihlásit se</button>
<div id="errorMessage" class="error-message">
Chybné přihlašovací údaje
</div>
</form>
</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 errorMessage = document.getElementById('errorMessage');
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password
})
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
// Save the token to localStorage
localStorage.setItem('token', data.token);
// Redirect to admin dashboard or home page
window.location.href = '/';
} catch (error) {
console.error('Login error:', error);
errorMessage.style.display = 'block';
}
});
</script>
</body>
</html>
+152
View File
@@ -0,0 +1,152 @@
package main
import (
"encoding/json"
"errors"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
var (
// In production, use environment variable for JWT key
jwtKey = getJWTKey()
adminUsername = "admin"
// In a real app, store hashed password and retrieve from a secure storage
adminPasswordHash = mustHashPassword("admin123") // Default password, should be changed after first login
)
func getJWTKey() []byte {
key := os.Getenv("JWT_SECRET")
if key == "" {
return []byte("default-secret-key-change-in-production")
}
return []byte(key)
}
func mustHashPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Fatal("Failed to hash password:", err)
}
return string(hash)
}
func authenticateUser(creds Credentials) (string, error) {
// In a real app, verify against a database
if creds.Username != adminUsername {
return "", errors.New("invalid credentials")
}
if err := bcrypt.CompareHashAndPassword([]byte(adminPasswordHash), []byte(creds.Password)); err != nil {
return "", errors.New("invalid credentials")
}
// Create JWT token
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
Username: creds.Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
func verifyToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return jwtKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
// AuthMiddleware verifies the JWT token in the Authorization header
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get token from Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, `{"error":"Authorization header required"}`, http.StatusUnauthorized)
return
}
// Format: Bearer <token>
tokenString := ""
if len(authHeader) > 7 && strings.ToUpper(authHeader[0:7]) == "BEARER " {
tokenString = authHeader[7:]
} else {
http.Error(w, `{"error":"Authorization header format must be Bearer <token>"}`, http.StatusUnauthorized)
return
}
_, err := verifyToken(tokenString)
if err != nil {
http.Error(w, `{"error":"Invalid or expired token`+err.Error()+`"}`, http.StatusUnauthorized)
return
}
// Token is valid, proceed with the request
next.ServeHTTP(w, r)
})
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var creds Credentials
err := json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
http.Error(w, `{"error":"Invalid request body"}`, http.StatusBadRequest)
return
}
token, err := authenticateUser(creds)
if err != nil {
http.Error(w, `{"error":"Invalid credentials"}`, http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
})
}
+2
View File
@@ -8,6 +8,8 @@ require (
) )
require ( require (
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/tiendc/go-deepcopy v1.6.0 // indirect github.com/tiendc/go-deepcopy v1.6.0 // indirect
+4
View File
@@ -2,6 +2,10 @@ 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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
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=
+46 -18
View File
@@ -14,6 +14,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/gorilla/mux"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
) )
@@ -39,27 +40,53 @@ type GeoCoords struct {
func main() { func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetFlags(log.LstdFlags | log.Lshortfile)
r := mux.NewRouter()
// Set up reverse proxy to kontakt service // Set up reverse proxy to kontakt service
kontaktURL, _ := url.Parse("http://webportal:8080") kontaktURL, _ := url.Parse("http://webportal:8080")
kontaktProxy := httputil.NewSingleHostReverseProxy(kontaktURL) kontaktProxy := httputil.NewSingleHostReverseProxy(kontaktURL)
http.Handle("/kontakt/", http.StripPrefix("/kontakt", kontaktProxy)) // Public routes
r.PathPrefix("/kontakt/").Handler(http.StripPrefix("/kontakt", kontaktProxy))
http.HandleFunc("/submit", enableCORS(handleSubmit)) r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc("/health", enableCORS(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`)) w.Write([]byte(`{"status":"ok"}`))
})) }).Methods("GET", "OPTIONS")
http.HandleFunc("/", enableCORS(func(w http.ResponseWriter, r *http.Request) { // Authentication routes
r.HandleFunc("/api/login", loginHandler).Methods("POST", "OPTIONS")
// Protected API routes
api := r.PathPrefix("/api").Subrouter()
api.Use(authMiddleware)
api.HandleFunc("/submit", handleSubmit).Methods("POST")
// Admin routes
r.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "admin.html")
}).Methods("GET")
r.HandleFunc("/admin/dashboard", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "admin-dashboard.html")
}).Methods("GET")
// Static file server for public files
fs := http.FileServer(http.Dir("."))
r.PathPrefix("/").Handler(fs)
// Redirect root to index.html
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.ServeFile(w, r, "index.html") http.ServeFile(w, r, "index.html")
})) }
}).Methods("GET")
http.HandleFunc("/evidence-aut", enableCORS(func(w http.ResponseWriter, r *http.Request) { // Public route for evidence-aut.html
r.HandleFunc("/evidence-aut", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "evidence-aut.html") http.ServeFile(w, r, "evidence-aut.html")
})) }).Methods("GET")
http.HandleFunc("/kontakt", enableCORS(func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/kontakt", func(w http.ResponseWriter, r *http.Request) {
// Check if kontakt service is already running // Check if kontakt service is already running
resp, err := http.Get("http://webportal:8080/health") resp, err := http.Get("http://webportal:8080/health")
if err == nil && resp.StatusCode == 200 { if err == nil && resp.StatusCode == 200 {
@@ -79,7 +106,10 @@ func main() {
// Wait briefly for service to start // Wait briefly for service to start
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
http.Redirect(w, r, "http://webportal:8080/", http.StatusFound) http.Redirect(w, r, "http://webportal:8080/", http.StatusFound)
})) }).Methods("GET")
// Apply CORS middleware to all routes
handler := enableCORS(r)
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {
@@ -87,14 +117,14 @@ func main() {
} }
log.Printf("Server běží na portu %s", port) log.Printf("Server běží na portu %s", port)
err := http.ListenAndServe(":"+port, nil) err := http.ListenAndServe(":"+port, handler)
if err != nil { if err != nil {
log.Fatalf("Chyba při spuštění serveru: %v", err) log.Fatalf("Chyba při spuštění serveru: %v", err)
} }
} }
func enableCORS(next http.HandlerFunc) http.HandlerFunc { func enableCORS(next http.Handler) http.Handler {
return func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
@@ -104,10 +134,8 @@ func enableCORS(next http.HandlerFunc) http.HandlerFunc {
return return
} }
if next != nil { next.ServeHTTP(w, r)
next(w, r) })
}
}
} }
func handleSubmit(w http.ResponseWriter, r *http.Request) { func handleSubmit(w http.ResponseWriter, r *http.Request) {