Compare commits
24 Commits
3ca41ec9da
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d3a71e1d0f | |||
| 09e65803c9 | |||
| c38a33a7aa | |||
| 53b673f86d | |||
| b6c2506bd4 | |||
| 9ee7281b6b | |||
| 3c18473588 | |||
| cbe7ddc51b | |||
| 94440e78de | |||
| 9491495395 | |||
| c4b404e7a9 | |||
| 68cb9ac73e | |||
| dc4c306817 | |||
| 930319d143 | |||
| dc31d4edb4 | |||
| 335aff579c | |||
| 00f2953e7f | |||
| 2c9da94143 | |||
| 5dfffcbcb7 | |||
| 57583e1436 | |||
| 6623d747ab | |||
| 8fe76406d8 | |||
| 9f207a0f5c | |||
| a897d5943b |
204
admin.go
204
admin.go
@@ -15,7 +15,10 @@ type difficultyLevel struct {
|
||||
func adminLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
http.ServeFile(w, r, "templates/adminLogin.html")
|
||||
err := adminLoginTemplate.Execute(w, false)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not render template", http.StatusInternalServerError)
|
||||
}
|
||||
case http.MethodPost:
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Error parsing form", http.StatusBadRequest)
|
||||
@@ -29,7 +32,10 @@ func adminLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
).Scan(new(int))
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
err := adminLoginTemplate.Execute(w, true)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not render template", http.StatusInternalServerError)
|
||||
}
|
||||
case err != nil:
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
default:
|
||||
@@ -38,7 +44,8 @@ func adminLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Value: base64.StdEncoding.EncodeToString([]byte(username + ":" + hashPassword(password))),
|
||||
Path: "/admin/",
|
||||
HttpOnly: true,
|
||||
MaxAge: 3600,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: true,
|
||||
})
|
||||
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
|
||||
}
|
||||
@@ -62,8 +69,8 @@ func isAdmin(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
var username, passwordHash string
|
||||
regexp := regexp.MustCompile(`^([^:]+):([a-f0-9]+)$`)
|
||||
matches := regexp.FindStringSubmatch(string(decoded))
|
||||
regex := regexp.MustCompile(`^([^:]+):([a-f0-9]+)$`)
|
||||
matches := regex.FindStringSubmatch(string(decoded))
|
||||
if len(matches) != 3 {
|
||||
return false
|
||||
}
|
||||
@@ -81,7 +88,7 @@ func adminHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, "templates/adminPanel.html")
|
||||
http.ServeFileFS(w, r, templatesFS, "templates/adminPanel.html")
|
||||
}
|
||||
|
||||
func adminTeamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -123,7 +130,7 @@ func adminTeamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// Fetch all teams with their difficulty levels
|
||||
// Teams
|
||||
rows, err := db.Query("SELECT name, difficulty_levels.level_name, last_cipher, penalty FROM teams JOIN difficulty_levels ON teams.difficulty_level = difficulty_levels.id ORDER BY teams.difficulty_level, teams.name")
|
||||
rows, err := db.Query("SELECT teams.id, name, difficulty_levels.level_name, last_cipher, last_loaded_cipher, penalty FROM teams JOIN difficulty_levels ON teams.difficulty_level = difficulty_levels.id ORDER BY teams.difficulty_level, teams.name")
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -132,7 +139,7 @@ func adminTeamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var teams []TeamTemplateS
|
||||
for rows.Next() {
|
||||
var team TeamTemplateS
|
||||
if err := rows.Scan(&team.TeamName, &team.Difficulty, &team.LastCipher, &team.Penalties); err != nil {
|
||||
if err := rows.Scan(&team.ID, &team.TeamName, &team.Difficulty, &team.LastCipher, &team.LastLoadedCipher, &team.Penalties); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -177,7 +184,7 @@ func AdminStartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
_, err := db.Exec("UPDATE teams SET last_cipher = 1, penalty = 0")
|
||||
_, err := db.Exec("UPDATE teams SET last_cipher = 1, last_loaded_cipher = 0, penalty = 0")
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -195,8 +202,102 @@ func AdminRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Error parsing form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Deleting an existing route point
|
||||
if r.PostForm.Has("delete") {
|
||||
cipherID := r.FormValue("delete")
|
||||
_, err := db.Exec("DELETE FROM tasks WHERE id = ?", cipherID)
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/routes", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// Adding a new route point
|
||||
order := r.FormValue("order")
|
||||
level := r.FormValue("level")
|
||||
position := r.FormValue("position")
|
||||
cipher := r.FormValue("cipher")
|
||||
endClue := r.FormValue("endclue")
|
||||
if order == "" || level == "" || position == "" || cipher == "" || endClue == "" {
|
||||
http.Error(w, "All fields are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err := db.Exec("INSERT INTO tasks (order_num, difficulty_level, position_id, cipher_id, end_clue) VALUES (?, ?, ?, ?, ?)", order, level, position, cipher, endClue)
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/routes", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// Fetch all ciphers for the dropdown
|
||||
rows, err := db.Query("SELECT id FROM ciphers ORDER BY id")
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var ciphers []int
|
||||
for rows.Next() {
|
||||
var cipher int
|
||||
if err := rows.Scan(&cipher); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ciphers = append(ciphers, cipher)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Fetch all positions for the dropdown
|
||||
rows, err = db.Query("SELECT id FROM positions ORDER BY id")
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var positions []int
|
||||
for rows.Next() {
|
||||
var position int
|
||||
if err := rows.Scan(&position); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
positions = append(positions, position)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Levels for the dropdown
|
||||
rows, err = db.Query("SELECT id FROM difficulty_levels ORDER BY id")
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var levels []int
|
||||
for rows.Next() {
|
||||
var level int
|
||||
if err := rows.Scan(&level); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
levels = append(levels, level)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Fetch all difficulty levels
|
||||
rows, err := db.Query("SELECT id, level_name FROM difficulty_levels ORDER BY id")
|
||||
rows, err = db.Query("SELECT id, level_name FROM difficulty_levels ORDER BY id")
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -212,10 +313,10 @@ func AdminRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
difficultyLevels = append(difficultyLevels, level)
|
||||
}
|
||||
// For each difficulty level, fetch the corresponding tasks and their details
|
||||
var routes []AdminRoutesTemplateS
|
||||
var routes []AdminRouteTemplateS
|
||||
for _, level := range difficultyLevels {
|
||||
var route AdminRoutesTemplateS
|
||||
rows, err := db.Query("SELECT tasks.order_num, CIPHERS.assignment, CIPHERS.clue, tasks.end_clue, POSITIONS.gps, POSITIONS.clue, CIPHERS.solution FROM TASKS JOIN CIPHERS ON TASKS.cipher_id = ciphers.id JOIN POSITIONS on TASKS.position_id = POSITIONS.id WHERE TASKS.difficulty_level=? ORDER BY TASKS.order_num;", level.ID)
|
||||
var route AdminRouteTemplateS
|
||||
rows, err := db.Query("SELECT tasks.id, tasks.order_num, CIPHERS.assignment, CIPHERS.clue, tasks.end_clue, POSITIONS.gps, POSITIONS.clue, CIPHERS.solution, COALESCE(QR_CODES.uid, '') FROM TASKS JOIN CIPHERS ON TASKS.cipher_id = ciphers.id JOIN POSITIONS on TASKS.position_id = POSITIONS.id LEFT JOIN QR_CODES ON QR_CODES.position_id = POSITIONS.id WHERE TASKS.difficulty_level=? ORDER BY TASKS.order_num;", level.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -224,10 +325,11 @@ func AdminRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
route.Name = level.LevelName
|
||||
for rows.Next() {
|
||||
var cipher CipherTemplateS
|
||||
if err := rows.Scan(&cipher.Order, &cipher.Assignment, &cipher.HelpText, &cipher.FinalClue, &cipher.Coordinates, &cipher.PositionHint, &cipher.Solution); err != nil {
|
||||
if err := rows.Scan(&cipher.ID, &cipher.Order, &cipher.Assignment, &cipher.HelpText, &cipher.FinalClue, &cipher.Coordinates, &cipher.PositionHint, &cipher.Solution, &cipher.URL); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cipher.URL = domain + "/qr/" + cipher.URL
|
||||
route.Ciphers = append(route.Ciphers, cipher)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -236,7 +338,15 @@ func AdminRouteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
routes = append(routes, route)
|
||||
}
|
||||
if err := AdminRoutesTemplate.Execute(w, routes); err != nil {
|
||||
// Prepare data for the template
|
||||
data := AdminRoutesTemplateS{
|
||||
Routes: routes,
|
||||
Levels: levels,
|
||||
Positions: positions,
|
||||
Ciphers: ciphers,
|
||||
}
|
||||
// Render the template
|
||||
if err := AdminRoutesTemplate.Execute(w, data); err != nil {
|
||||
http.Error(w, "Template error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -254,8 +364,8 @@ func AdminLevelHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// Deleting an existing difficulty level
|
||||
if r.PostForm.Has("delete") {
|
||||
levelName := r.FormValue("delete")
|
||||
_, err := db.Exec("DELETE FROM difficulty_levels WHERE level_name = ?", levelName)
|
||||
id := r.FormValue("delete")
|
||||
_, err := db.Exec("DELETE FROM difficulty_levels WHERE id = ?", id)
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -277,16 +387,16 @@ func AdminLevelHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/levels", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
rows, err := db.Query("SELECT level_name FROM difficulty_levels ORDER BY id")
|
||||
rows, err := db.Query("SELECT id, level_name FROM difficulty_levels ORDER BY id")
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var difficultyLevels []string
|
||||
var difficultyLevels []AdminLevelTemplateS
|
||||
for rows.Next() {
|
||||
var level string
|
||||
if err := rows.Scan(&level); err != nil {
|
||||
var level AdminLevelTemplateS
|
||||
if err := rows.Scan(&level.ID, &level.Name); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -385,6 +495,27 @@ func AdminPositionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/positions", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if r.PostForm.Has("update") {
|
||||
// Updating an existing position
|
||||
positionID := r.FormValue("update")
|
||||
gps := r.FormValue("gps")
|
||||
if gps == "" {
|
||||
http.Error(w, "GPS field cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
clue := r.FormValue("clue")
|
||||
if clue == "" {
|
||||
http.Error(w, "Clue field cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err := db.Exec("UPDATE positions SET gps = ?, clue = ? WHERE id = ?", gps, clue, positionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/positions", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// Adding a new position
|
||||
gps := r.FormValue("gps")
|
||||
clue := r.FormValue("clue")
|
||||
@@ -508,3 +639,34 @@ func AdminQRHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func AdminPenaltiesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAdmin(r) {
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// Fetch all penalties with team names and task orders
|
||||
rows, err := db.Query("SELECT teams.name, tasks.order_num, penalties.minutes FROM penalties JOIN teams ON penalties.team_id = teams.id JOIN tasks ON penalties.task_id = tasks.id ORDER BY teams.name, tasks.order_num")
|
||||
if err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var penalties []AdminPenaltiesTemplateS
|
||||
for rows.Next() {
|
||||
var penalty AdminPenaltiesTemplateS
|
||||
if err := rows.Scan(&penalty.TeamName, &penalty.TaskOrder, &penalty.Minutes); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
penalties = append(penalties, penalty)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := AdminPenaltiesTemplate.Execute(w, penalties); err != nil {
|
||||
http.Error(w, "Template error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ CREATE TABLE TEAMS (
|
||||
difficulty_level INTEGER NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
last_cipher INTEGER DEFAULT 0, -- index of cipher which team is solving or searching now
|
||||
last_loaded_cipher INTEGER DEFAULT 0, -- index of cipher which team has loaded last time
|
||||
penalty INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (difficulty_level) REFERENCES DIFFICULTY_LEVELS(id)
|
||||
);
|
||||
|
||||
@@ -5,10 +5,10 @@ INSERT INTO DIFFICULTY_LEVELS (id, level_name) VALUES
|
||||
(3, 'Těžká');
|
||||
|
||||
-- Vložení týmů: heslo1, heslo2, heslo3
|
||||
INSERT INTO TEAMS (id, name, difficulty_level, password, last_cipher, penalty) VALUES
|
||||
(1, 'Rychlé šípy', 1, '4bc2ef0648cdf275032c83bb1e87dd554d47f4be293670042212c8a01cc2ccbe', 1, 0),
|
||||
(2, 'Vlčí smečka', 2, '274efeaa827a33d7e35be9a82cd6150b7caf98f379a4252aa1afce45664dcbe1', 1, 10),
|
||||
(3, 'Orli', 3, '05af533c6614544a704c4cf51a45be5c10ff19bd10b7aa1dfe47efc0fd059ede', 1, 5);
|
||||
INSERT INTO TEAMS (id, name, difficulty_level, password, last_cipher, last_loaded_cipher, penalty) VALUES
|
||||
(1, 'Rychlé šípy', 1, '4bc2ef0648cdf275032c83bb1e87dd554d47f4be293670042212c8a01cc2ccbe', 1, 0, 0),
|
||||
(2, 'Vlčí smečka', 2, '274efeaa827a33d7e35be9a82cd6150b7caf98f379a4252aa1afce45664dcbe1', 1, 0, 10),
|
||||
(3, 'Orli', 3, '05af533c6614544a704c4cf51a45be5c10ff19bd10b7aa1dfe47efc0fd059ede', 1, 0, 5);
|
||||
|
||||
-- Vložení pozic
|
||||
INSERT INTO POSITIONS (id, gps, clue) VALUES
|
||||
|
||||
115
klice.go
115
klice.go
@@ -8,11 +8,18 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
const domain = "http://localhost:8080"
|
||||
const domain = "https://klice.h21.cz"
|
||||
const dbFile = "./klice.db"
|
||||
|
||||
const (
|
||||
smallHelpPenalty = 5
|
||||
giveUpPenalty = 30
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
@@ -34,7 +41,10 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := db.QueryRow("SELECT 1 FROM teams WHERE password = ?", hashedPassword).Scan(new(int))
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
http.Error(w, "No team found", http.StatusUnauthorized)
|
||||
err = LoginTemplate.Execute(w, true)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not render template", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
case err != nil:
|
||||
http.Error(w, "Could not retrieve team", http.StatusInternalServerError)
|
||||
@@ -42,9 +52,12 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
sessionID := hashedPassword
|
||||
cookie := &http.Cookie{
|
||||
Name: "session_id",
|
||||
Value: sessionID,
|
||||
Path: "/",
|
||||
Name: "session_id",
|
||||
Value: sessionID,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: true,
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
@@ -52,13 +65,16 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err == nil {
|
||||
redir.MaxAge = -1
|
||||
http.SetCookie(w, redir)
|
||||
http.Redirect(w, r, redir.Value, http.StatusSeeOther)
|
||||
http.Redirect(w, r, safeRedirectURL(redir.Value), http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, "/team", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
case http.MethodGet:
|
||||
http.ServeFile(w, r, "templates/login.html")
|
||||
err := LoginTemplate.Execute(w, false)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not render template", http.StatusInternalServerError)
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
@@ -101,6 +117,7 @@ func isLoggedIn(w http.ResponseWriter, r *http.Request) (bool, int) {
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: true,
|
||||
}
|
||||
http.SetCookie(w, redir)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
@@ -138,8 +155,8 @@ func teamInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func qrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uid, found := strings.CutPrefix(r.URL.Path, "/qr/")
|
||||
if !found || uid == "" {
|
||||
uid := r.PathValue("qr")
|
||||
if uid == "" {
|
||||
http.Error(w, "Invalid QR code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -190,12 +207,12 @@ func qrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "This task is not yet available", http.StatusForbidden)
|
||||
return
|
||||
} else if order == last_cipher {
|
||||
last_cipher = order
|
||||
_, err = db.Exec("UPDATE teams SET last_cipher = ? WHERE id = ?", order, teamID)
|
||||
_, err := db.Exec("UPDATE teams SET last_loaded_cipher = ? WHERE id = ?", order, teamID)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not update last cipher", http.StatusInternalServerError)
|
||||
http.Error(w, "Could not update last loaded cipher", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
} else if order < last_cipher {
|
||||
help = 2
|
||||
}
|
||||
@@ -210,7 +227,7 @@ func qrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
CipherTemplateData := CipherTemplateS{
|
||||
Order: uint(cipherID),
|
||||
Order: uint(order),
|
||||
Assignment: template.HTML(assignment),
|
||||
HelpText: "",
|
||||
FinalClue: "",
|
||||
@@ -220,18 +237,20 @@ func qrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// get penalties for this task and team
|
||||
err = db.QueryRow("SELECT minutes FROM penalties WHERE team_id = ? AND task_id = ?", teamID, taskID).Scan(&penalty)
|
||||
if err == sql.ErrNoRows {
|
||||
penalty = 0
|
||||
} else if err != nil {
|
||||
http.Error(w, "Could not retrieve penalties", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// determine help level based on penalties
|
||||
if penalty > 0 && penalty < 15 {
|
||||
help = 1
|
||||
} else if penalty >= 15 {
|
||||
help = 2
|
||||
if help == 0 {
|
||||
err = db.QueryRow("SELECT minutes FROM penalties WHERE team_id = ? AND task_id = ?", teamID, taskID).Scan(&penalty)
|
||||
if err == sql.ErrNoRows {
|
||||
penalty = 0
|
||||
} else if err != nil {
|
||||
http.Error(w, "Could not retrieve penalties", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// determine help level based on penalties
|
||||
if penalty > 0 && penalty <= smallHelpPenalty {
|
||||
help = 1
|
||||
} else if penalty > smallHelpPenalty {
|
||||
help = 2
|
||||
}
|
||||
}
|
||||
|
||||
// handle answer and help form submission
|
||||
@@ -242,13 +261,13 @@ func qrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if r.FormValue("help") == "1" && help == 0 { // small help
|
||||
help = 1
|
||||
db.Exec("INSERT INTO penalties (team_id, task_id, minutes) VALUES (?, ?, 5)", teamID, taskID)
|
||||
db.Exec("UPDATE teams SET penalty = penalty + 5 WHERE id = ?", teamID)
|
||||
db.Exec("INSERT INTO penalties (team_id, task_id, minutes) VALUES (?, ?, ?)", teamID, taskID, smallHelpPenalty)
|
||||
db.Exec("UPDATE teams SET penalty = penalty + ? WHERE id = ?", smallHelpPenalty, teamID)
|
||||
} else if r.FormValue("help") == "2" && help == 1 { // give up
|
||||
help = 2
|
||||
db.Exec("UPDATE penalties SET minutes = 30 WHERE team_id = ? AND task_id = ?", teamID, taskID)
|
||||
db.Exec("UPDATE teams SET penalty = penalty + 30, last_cipher = ? WHERE id = ?", order+1, teamID)
|
||||
} else if answer := r.FormValue("solution"); answer != "" { // answer submission
|
||||
db.Exec("UPDATE penalties SET minutes = minutes + ? WHERE team_id = ? AND task_id = ?", giveUpPenalty, teamID, taskID)
|
||||
db.Exec("UPDATE teams SET penalty = penalty + ?, last_cipher = ? WHERE id = ?", giveUpPenalty, order+1, teamID)
|
||||
} else if answer := r.FormValue("solution"); answer != "" && help < 2 { // answer submission
|
||||
var correctAnswer string
|
||||
err = db.QueryRow("SELECT solution FROM CIPHERS WHERE id = ?", cipherID).Scan(&correctAnswer)
|
||||
if err != nil {
|
||||
@@ -276,7 +295,7 @@ func qrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Could not retrieve help text", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
CipherTemplateData.HelpText = helpText
|
||||
CipherTemplateData.HelpText = template.HTML(helpText)
|
||||
case 2: // next cipher
|
||||
// get end clue
|
||||
var endClue string
|
||||
@@ -289,15 +308,17 @@ func qrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
CipherTemplateData.FinalClue = endClue
|
||||
// get coordinates
|
||||
var coordinates string
|
||||
err = db.QueryRow("SELECT gps FROM POSITIONS WHERE id = (SELECT position_id FROM TASKS WHERE id = (SELECT id FROM TASKS WHERE order_num = ? AND difficulty_level = (SELECT difficulty_level FROM teams WHERE id = ?)))", order+1, teamID).Scan(&coordinates)
|
||||
var coordinates, positionHint string
|
||||
err = db.QueryRow("SELECT gps, clue FROM POSITIONS WHERE id = (SELECT position_id FROM TASKS WHERE id = (SELECT id FROM TASKS WHERE order_num = ? AND difficulty_level = (SELECT difficulty_level FROM teams WHERE id = ?)))", order+1, teamID).Scan(&coordinates, &positionHint)
|
||||
if err == sql.ErrNoRows {
|
||||
coordinates = ""
|
||||
coordinates = "Konec, vraťte se."
|
||||
positionHint = "KONEC"
|
||||
} else if err != nil {
|
||||
http.Error(w, "Could not retrieve coordinates", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
CipherTemplateData.Coordinates = coordinates
|
||||
CipherTemplateData.PositionHint = positionHint
|
||||
// get solution
|
||||
var solution string
|
||||
err = db.QueryRow("SELECT solution FROM CIPHERS WHERE id = ?", cipherID).Scan(&solution)
|
||||
@@ -319,9 +340,16 @@ func qrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func safeRedirectURL(u string) string {
|
||||
if strings.HasPrefix(u, "/") && !strings.HasPrefix(u, "//") {
|
||||
return u
|
||||
}
|
||||
return "/team"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
db, err = sql.Open("sqlite3", "./klice.db?_fk=on")
|
||||
db, err = sql.Open("sqlite3", dbFile+"?_fk=on")
|
||||
if err != nil {
|
||||
fmt.Println("Error opening database:", err)
|
||||
return
|
||||
@@ -333,7 +361,7 @@ func main() {
|
||||
http.HandleFunc("/login", loginHandler)
|
||||
http.HandleFunc("/logout", logoutHandler)
|
||||
http.HandleFunc("/team", teamInfoHandler)
|
||||
http.HandleFunc("/qr/", qrHandler)
|
||||
http.HandleFunc("/qr/{qr...}", qrHandler)
|
||||
// admin app
|
||||
http.HandleFunc("/admin/login", adminLoginHandler)
|
||||
http.HandleFunc("/admin/logout", adminLogoutHandler)
|
||||
@@ -345,7 +373,20 @@ func main() {
|
||||
http.HandleFunc("/admin/cipher", AdminCipherHandler)
|
||||
http.HandleFunc("/admin/positions", AdminPositionsHandler)
|
||||
http.HandleFunc("/admin/qr", AdminQRHandler)
|
||||
http.HandleFunc("/admin/penalties", AdminPenaltiesHandler)
|
||||
|
||||
// static files
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: nil,
|
||||
ReadTimeout: 10 * time.Second, // zabrání Slowloris útokům
|
||||
WriteTimeout: 15 * time.Second, // omezení dlouhých odpovědí
|
||||
IdleTimeout: 60 * time.Second, // ukončení nečinných spojení
|
||||
MaxHeaderBytes: 1 << 20, // max. 1 MB hlavičky
|
||||
}
|
||||
|
||||
fmt.Println("Server started at :8080")
|
||||
http.ListenAndServe(":8080", nil)
|
||||
srv.ListenAndServe()
|
||||
}
|
||||
|
||||
61
templates.go
61
templates.go
@@ -1,26 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
//go:embed templates/*.html
|
||||
var templatesFS embed.FS
|
||||
|
||||
type CipherTemplateS struct {
|
||||
ID int
|
||||
Order uint
|
||||
Assignment template.HTML
|
||||
HelpText string
|
||||
HelpText template.HTML
|
||||
FinalClue string
|
||||
Coordinates string
|
||||
PositionHint string
|
||||
Solution string
|
||||
Help int
|
||||
Wrong bool
|
||||
URL string
|
||||
}
|
||||
|
||||
type TeamTemplateS struct {
|
||||
TeamName string
|
||||
Difficulty string
|
||||
LastCipher int
|
||||
Penalties int
|
||||
ID int
|
||||
TeamName string
|
||||
Difficulty string
|
||||
LastCipher int
|
||||
LastLoadedCipher int
|
||||
Penalties int
|
||||
}
|
||||
|
||||
type DifficultyLevelS struct {
|
||||
@@ -33,16 +41,23 @@ type TeamsTemplateS struct {
|
||||
Difficulties []DifficultyLevelS
|
||||
}
|
||||
|
||||
type AdminRoutesTemplateS struct {
|
||||
type AdminRouteTemplateS struct {
|
||||
Name string
|
||||
Ciphers []CipherTemplateS
|
||||
}
|
||||
|
||||
type AdminRoutesTemplateS struct {
|
||||
Routes []AdminRouteTemplateS
|
||||
Levels []int
|
||||
Positions []int
|
||||
Ciphers []int
|
||||
}
|
||||
|
||||
type AdminCipherTemplateS struct {
|
||||
ID int
|
||||
Assignment string
|
||||
Assignment template.HTML
|
||||
Solution string
|
||||
Clue string
|
||||
Clue template.HTML
|
||||
}
|
||||
|
||||
type AdminPositionsTemplateS struct {
|
||||
@@ -64,11 +79,25 @@ type AdminQRTemplateS struct {
|
||||
Positions []int
|
||||
}
|
||||
|
||||
var CipherTemplate = template.Must(template.ParseFiles("templates/assignment.html"))
|
||||
var TeamTemplate = template.Must(template.ParseFiles("templates/team.html"))
|
||||
var AdminTeamsTemplate = template.Must(template.ParseFiles("templates/adminTeams.html"))
|
||||
var AdminRoutesTemplate = template.Must(template.ParseFiles("templates/adminRoutes.html"))
|
||||
var AdminLevelTemplate = template.Must(template.ParseFiles("templates/adminLevels.html"))
|
||||
var AdminCipherTemplate = template.Must(template.ParseFiles("templates/adminCiphers.html"))
|
||||
var AdminPositionsTemplate = template.Must(template.ParseFiles("templates/adminPositions.html"))
|
||||
var AdminQRsTemplate = template.Must(template.ParseFiles("templates/adminQR.html"))
|
||||
type AdminLevelTemplateS struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
type AdminPenaltiesTemplateS struct {
|
||||
TeamName string
|
||||
TaskOrder uint
|
||||
Minutes int
|
||||
}
|
||||
|
||||
var CipherTemplate = template.Must(template.ParseFS(templatesFS, "templates/assignment.html"))
|
||||
var TeamTemplate = template.Must(template.ParseFS(templatesFS, "templates/team.html"))
|
||||
var AdminTeamsTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminTeams.html"))
|
||||
var AdminRoutesTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminRoutes.html"))
|
||||
var AdminLevelTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminLevels.html"))
|
||||
var AdminCipherTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminCiphers.html"))
|
||||
var AdminPositionsTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminPositions.html"))
|
||||
var AdminQRsTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminQR.html"))
|
||||
var AdminPenaltiesTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminPenalties.html"))
|
||||
var LoginTemplate = template.Must(template.ParseFS(templatesFS, "templates/login.html"))
|
||||
var adminLoginTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminLogin.html"))
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
<h2>Nová šifra</h2>
|
||||
<form action="/admin/cipher" method="post">
|
||||
<label for="assignment">Zadání:</label>
|
||||
<input type="text" id="assignment" name="assignment" required>
|
||||
<textarea id="assignment" name="assignment" cols="40" rows="5" required></textarea>
|
||||
<label for="solution">Řešení:</label>
|
||||
<input type="text" id="solution" name="solution" required>
|
||||
<label for="clue">Nápověda:</label>
|
||||
<input type="text" id="clue" name="clue" required>
|
||||
<textarea id="clue" name="clue" cols="40" rows="5" required></textarea>
|
||||
<input type="submit" value="Přidat šifru">
|
||||
</form>
|
||||
<hr>
|
||||
|
||||
@@ -10,15 +10,17 @@
|
||||
<h1>Úrovně obtížnosti</h1>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Jméno</th>
|
||||
<th>Smazat</th>
|
||||
</tr>
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.}}</td>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.Name}}</td>
|
||||
<td>
|
||||
<form action="/admin/levels" method="post">
|
||||
<input type="hidden" name="delete" value="{{.}}">
|
||||
<input type="hidden" name="delete" value="{{.ID}}">
|
||||
<button type="submit">Smazat</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
@@ -2,17 +2,33 @@
|
||||
<html lang="cs">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Admin Login</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Admin — přihlášení</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Admin Login</h1>
|
||||
<form method="post">
|
||||
Uživatelské jméno: <input type="text" name="username"><br>
|
||||
Heslo: <input type="password" name="password"><br>
|
||||
<input type="submit" value="Přihlásit se">
|
||||
|
||||
{{if .}}
|
||||
<p style="color: red;"><strong>Chybné uživatelské jméno nebo heslo.</strong></p>
|
||||
{{end}}
|
||||
|
||||
<form method="post" autocomplete="off">
|
||||
<div>
|
||||
<label for="username">Uživatelské jméno</label><br>
|
||||
<input id="username" name="username" type="text" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password">Heslo</label><br>
|
||||
<input id="password" name="password" type="password" required>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Přihlásit se">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p><a href="/admin">Zpět na admin panel</a></p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -14,6 +14,7 @@
|
||||
<a href="/admin/cipher">Šifry</a> <br>
|
||||
<a href="/admin/positions">Pozice</a> <br>
|
||||
<a href="/admin/qr">QR Kódy</a> <br>
|
||||
<a href="/admin/penalties">Penalizace týmů</a> <br>
|
||||
<hr>
|
||||
<form method="post" action="/admin/logout">
|
||||
<input type="submit" value="Logout">
|
||||
|
||||
29
templates/adminPenalties.html
Normal file
29
templates/adminPenalties.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Penalizace týmů</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Penalizace týmů</h1>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>Název týmu</th>
|
||||
<th>Pořadí šifry</th>
|
||||
<th>Minuty</th>
|
||||
</tr>
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.TeamName}}</td>
|
||||
<td>{{.TaskOrder}}</td>
|
||||
<td>{{.Minutes}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
<hr>
|
||||
<a href="/admin">Zpět na admin panel</a>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -19,8 +19,16 @@
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.GPS}}</td>
|
||||
<td>{{.Clue}}</td>
|
||||
<form action="/admin/positions" method="post">
|
||||
<input type="hidden" name="update" value="{{.ID}}">
|
||||
<td>
|
||||
<input type="text" name="gps" value="{{.GPS}}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="clue" value="{{.Clue}}">
|
||||
<input type="submit" value="Upravit">
|
||||
</td>
|
||||
</form>
|
||||
<td><a href="{{.URL}}">{{.URL}}</a></td>
|
||||
<td>
|
||||
<form action="/admin/positions" method="post">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</tr>
|
||||
{{range .QRs}}
|
||||
<tr>
|
||||
<td>{{.URL}}</td>
|
||||
<td><a href="{{.URL}}">{{.URL}}</a></td>
|
||||
<td>{{.Position}}</td>
|
||||
<td>{{.GPS}}</td>
|
||||
<td>
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
<body>
|
||||
<h1>Trasy</h1>
|
||||
{{range .}}
|
||||
{{range .Routes}}
|
||||
<h2>{{.Name}}</h2>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Pořadí</th>
|
||||
<th>Souřadnice</th>
|
||||
<th>Nápověda pozice</th>
|
||||
@@ -19,9 +20,12 @@
|
||||
<th>Nápověda</th>
|
||||
<th>Řešení</th>
|
||||
<th>Cílová indicie</th>
|
||||
<th>URL</th>
|
||||
<th>Smazat</th>
|
||||
</tr>
|
||||
{{range .Ciphers}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.Order}}</td>
|
||||
<td>{{.Coordinates}}</td>
|
||||
<td>{{.PositionHint}}</td>
|
||||
@@ -29,11 +33,39 @@
|
||||
<td>{{.HelpText}}</td>
|
||||
<td>{{.Solution}}</td>
|
||||
<td>{{.FinalClue}}</td>
|
||||
<td><a href="{{.URL}}">{{.URL}}</a></td>
|
||||
<td>
|
||||
<form action="/admin/routes" method="post">
|
||||
<input type="hidden" name="delete" value="{{.ID}}">
|
||||
<button type="submit">Smazat</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
<hr>
|
||||
{{end}}
|
||||
<h2>Přidat bod trasy</h2>
|
||||
<form action="/admin/routes" method="post">
|
||||
Pořadí: <input type="number" name="order" required> <br>
|
||||
Obtížnost: <select name="level" required>
|
||||
{{range .Levels}}
|
||||
<option value="{{.}}">{{.}}</option>
|
||||
{{end}}
|
||||
</select> <br>
|
||||
Šifra: <select name="cipher" required>
|
||||
{{range .Ciphers}}
|
||||
<option value="{{.}}">{{.}}</option>
|
||||
{{end}}
|
||||
</select> <br>
|
||||
Pozice : <select name="position" required>
|
||||
{{range .Positions}}
|
||||
<option value="{{.}}">{{.}}</option>
|
||||
{{end}}
|
||||
</select> <br>
|
||||
Koncová indicie: <input type="text" name="endclue">
|
||||
<button type="submit">Přidat bod trasy</button>
|
||||
</form>
|
||||
<hr>
|
||||
<a href="/admin">Zpět na admin panel</a>
|
||||
</body>
|
||||
|
||||
@@ -10,18 +10,21 @@
|
||||
<h1>Týmy</h1>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Název týmu</th>
|
||||
<th>Obtížnost</th>
|
||||
<th>Poslední šifra</th>
|
||||
<th>Právě řešená šifra</th>
|
||||
<th>Poslední načtená šifra</th>
|
||||
<th>Penalizace (minuty)</th>
|
||||
<th>Smazat tým</th>
|
||||
</th>
|
||||
</tr>
|
||||
{{range .Teams}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.TeamName}}</td>
|
||||
<td>{{.Difficulty}}</td>
|
||||
<td>{{.LastCipher}}</td>
|
||||
<td>{{.LastLoadedCipher}}</td>
|
||||
<td>{{.Penalties}}</td>
|
||||
<td>
|
||||
<form action="/admin/teams" method="post">
|
||||
|
||||
@@ -2,54 +2,269 @@
|
||||
<html lang="cs">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Zadání šifry {{.Order}}</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f7fb;
|
||||
--card: #ffffff;
|
||||
--accent: #0b6efd;
|
||||
--muted: #6b7280;
|
||||
--danger: #ef4444;
|
||||
--border: #e6e9ee;
|
||||
--radius: 12px;
|
||||
--gap: 14px;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: linear-gradient(180deg, #f8fbff, var(--bg));
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: 900px;
|
||||
margin: 28px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 24px rgba(20, 30, 60, 0.06);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--muted);
|
||||
margin-bottom: 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.assignment {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 18px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(11, 110, 253, 0.12);
|
||||
background: linear-gradient(180deg, var(--accent), #0654d6);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn.warn {
|
||||
background: var(--danger);
|
||||
border-color: rgba(239, 68, 68, 0.9);
|
||||
}
|
||||
|
||||
.helpbox {
|
||||
background: #f8faff;
|
||||
border: 1px solid rgba(11, 110, 253, 0.08);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
color: #0b2540;
|
||||
}
|
||||
|
||||
.solution {
|
||||
background: #fbffef;
|
||||
border: 1px solid rgba(34, 197, 94, 0.12);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
color: #0b3a12;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="submit"],
|
||||
button[type="button"] {
|
||||
padding: 9px 12px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.gps {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input[readonly] {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width:640px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Zadání šifry {{.Order}}</h1>
|
||||
<p>{{.Assignment}}</p>
|
||||
<hr>
|
||||
{{if eq .Help 0}}
|
||||
<p>Požádat o malou nápovědu.</p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="help" value="1">
|
||||
<input type="submit" value="Zobrazit nápovědu">
|
||||
</form>
|
||||
{{else if eq .Help 1}}
|
||||
<p>Nápověda: {{.HelpText}}</p>
|
||||
<p>Vzdát se a ukázat pozici další šifry.</p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="help" value="2">
|
||||
<input type="submit" value="Vzdát se">
|
||||
</form>
|
||||
{{else}}
|
||||
<p>Řešení: {{.Solution}}</p>
|
||||
{{end}}
|
||||
<hr>
|
||||
{{if ne .Help 2}}
|
||||
{{if .Wrong}}<p style="color:red;">Špatné řešení, zkus to znovu.</p>{{end}}
|
||||
<form method="post">
|
||||
Řešení: <input type="text" name="solution"><br>
|
||||
<input type="submit" value="Odeslat">
|
||||
</form>
|
||||
{{else}}
|
||||
<p>
|
||||
Souřadnice další šifry:
|
||||
<input id="gps" value="{{.Coordinates}}" readonly />
|
||||
<br>
|
||||
<button onclick="copyToClipboard()">Zkopírovat do schránky</button>
|
||||
</p>
|
||||
<p>Nápověda k nalezení cíle: {{.FinalClue}}</p>
|
||||
{{end}}
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h1>Zadání šifry {{.Order}}</h1>
|
||||
<div class="meta">Aktuální úkol</div>
|
||||
|
||||
<div class="assignment">
|
||||
{{.Assignment}}
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
{{if eq .Help 0}}
|
||||
<div>
|
||||
<div class="muted">Požádat o malou nápovědu</div>
|
||||
<form method="post" style="margin-top:8px;">
|
||||
<input type="hidden" name="help" value="1">
|
||||
<input class="btn" type="submit" value="Zobrazit nápovědu">
|
||||
</form>
|
||||
</div>
|
||||
{{else if eq .Help 1}}
|
||||
<div style="flex:1">
|
||||
<div class="helpbox">Nápověda: {{.HelpText}}</div>
|
||||
<div style="margin-top:12px">
|
||||
<div class="muted">Pokud se chcete vzdát a získat pozici další šifry:</div>
|
||||
<form method="post" style="margin-top:8px;">
|
||||
<input type="hidden" name="help" value="2">
|
||||
<input class="btn warn" type="submit" value="Vzdát se">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="flex:1">
|
||||
<div class="solution">Řešení: {{.Solution}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{{if ne .Help 2}}
|
||||
{{if .Wrong}}<p class="error">Špatné řešení, zkus to znovu.</p>{{end}}
|
||||
<form method="post" style="margin-top:12px;">
|
||||
<label for="solution">Zadejte řešení</label>
|
||||
<div class="row">
|
||||
<input id="solution" name="solution" type="text" autocomplete="off"
|
||||
placeholder="Zadejte odpověď...">
|
||||
<input class="btn ghost" type="submit" value="Odeslat">
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div>
|
||||
<p class="muted">Souřadnice další šifry</p>
|
||||
<div class="row">
|
||||
<div class="gps" style="flex:1">
|
||||
<input id="gps" value="{{.Coordinates}}" readonly>
|
||||
<button class="btn ghost" type="button" onclick="copyToClipboard()">Zkopírovat</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted" style="margin-top:8px">Nápověda k nalezení pozice: <strong>{{.PositionHint}}</strong>
|
||||
</p>
|
||||
<p class="muted">Nápověda k nalezení cíle: <strong>{{.FinalClue}}</strong></p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard() {
|
||||
const el = document.getElementById('gps');
|
||||
if (!el) return;
|
||||
const text = el.value || el.innerText;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).catch(() => { fallbackCopy(el); });
|
||||
} else {
|
||||
fallbackCopy(el);
|
||||
}
|
||||
}
|
||||
function fallbackCopy(el) {
|
||||
el.select ? el.select() : null;
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (e) { /* ignore */ }
|
||||
window.getSelection ? window.getSelection().removeAllRanges() : null;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
function copyToClipboard() {
|
||||
let copyText = document.querySelector("#gps");
|
||||
copyText.select();
|
||||
document.execCommand("copy");
|
||||
}
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -3,17 +3,123 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Přihlášení</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f7fb;
|
||||
--card: #ffffff;
|
||||
--accent: #0b6efd;
|
||||
--muted: #6b7280;
|
||||
--radius: 12px;
|
||||
--border: #e6e9ee;
|
||||
--error-bg: #fff1f2;
|
||||
--error-border: #fecaca;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: linear-gradient(180deg, #f8fbff, var(--bg));
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: 420px;
|
||||
margin: 72px auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
padding: 20px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 10px 30px rgba(20, 30, 60, 0.06);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 20px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: var(--muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--error-bg);
|
||||
border: 1px solid var(--error-border);
|
||||
color: #9f1239;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width:420px) {
|
||||
.wrap {
|
||||
margin: 32px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Login</h1>
|
||||
<form action="/login" method="post">
|
||||
<label for="password">Heslo:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<br>
|
||||
<button type="submit">Přihlásit se</button>
|
||||
</form>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h1>Přihlášení do týmu</h1>
|
||||
<div class="sub">Zadejte své týmové heslo</div>
|
||||
|
||||
{{if .}}
|
||||
<div class="error">Chybné heslo — zkuste to prosím znovu.</div>
|
||||
{{end}}
|
||||
|
||||
<form action="/login" method="post">
|
||||
<div>
|
||||
<label for="password">Heslo</label>
|
||||
<input id="password" name="password" type="password" required autocomplete="current-password">
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn" type="submit">Přihlásit se</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -3,18 +3,112 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Tým: {{.TeamName}}</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f7fb;
|
||||
--card: #ffffff;
|
||||
--accent: #0b6efd;
|
||||
--muted: #6b7280;
|
||||
--radius: 12px;
|
||||
--border: #e6e9ee;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: linear-gradient(180deg, #f8fbff, var(--bg));
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: 720px;
|
||||
margin: 36px auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
padding: 18px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 20px rgba(20, 30, 60, 0.06);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 20px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--muted);
|
||||
margin-bottom: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 15px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.logout {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logout:hover {
|
||||
box-shadow: 0 4px 12px rgba(11, 110, 253, 0.06);
|
||||
}
|
||||
|
||||
@media (max-width:560px) {
|
||||
.info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Tým: {{.TeamName}}</h1>
|
||||
Obtížnost: {{.Difficulty}}<br>
|
||||
Poslední šifra : {{.LastCipher}}<br>
|
||||
Penalizace: {{.Penalties}}<br>
|
||||
<hr>
|
||||
<form action="/logout" method="get">
|
||||
<input type="submit" value="Odhlásit">
|
||||
</form>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h1>Tým: {{.TeamName}}</h1>
|
||||
<div class="meta">Přehled týmu</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="details">
|
||||
<div><span class="label">Obtížnost:</span> {{.Difficulty}}</div>
|
||||
<div><span class="label">Právě řeší šifru:</span> {{.LastCipher}}</div>
|
||||
<div><span class="label">Penalizace (min):</span> {{.Penalties}}</div>
|
||||
</div>
|
||||
|
||||
<form action="/logout" method="get">
|
||||
<button class="logout" type="submit">Odhlásit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user