// package templates handles Go html/template rendering for the website.
package templates
import (
"embed"
"html/template"
"log"
"net/http"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/auth"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database"
)
//go:embed html/*.html
var templateFS embed.FS
// pageData is passed to all templates.
type pageData struct {
Title string
Username string
UUID string
}
// Handler serves template-rendered pages.
type Handler struct {
db *database.DB
cfg *config.Config
templates map[string]*template.Template
}
// NewHandler creates a new templates handler and parses embedded templates.
func NewHandler(db *database.DB, cfg *config.Config) *Handler {
h := &Handler{db: db, cfg: cfg, templates: make(map[string]*template.Template)}
h.parseTemplates()
return h
}
// RegisterRoutes mounts template-rendered pages.
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /", h.index)
mux.HandleFunc("GET /login", h.loginPage)
mux.HandleFunc("GET /register", h.registerPage)
mux.HandleFunc("GET /profile", h.profilePage)
mux.HandleFunc("GET /admin", h.adminPage)
}
// ── Page handlers ──────────────────────────────────────────────
func (h *Handler) index(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
h.render(w, "index.html", pageData{Title: "Главная"})
}
func (h *Handler) loginPage(w http.ResponseWriter, r *http.Request) {
h.render(w, "login.html", pageData{Title: "Вход"})
}
func (h *Handler) registerPage(w http.ResponseWriter, r *http.Request) {
h.render(w, "register.html", pageData{Title: "Регистрация"})
}
func (h *Handler) profilePage(w http.ResponseWriter, r *http.Request) {
h.render(w, "profile.html", pageData{Title: "Профиль"})
}
func (h *Handler) adminPage(w http.ResponseWriter, r *http.Request) {
// Extract bearer token from Authorization header
token := auth.ExtractBearer(r.Header.Get("Authorization"))
if token == "" {
// No token provided, redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Validate token and check admin role
var userID int
var role string
err := h.db.Pool().QueryRow(r.Context(),
`SELECT u.id, u.role
FROM yggdrasil_sessions s
JOIN users u ON u.id = s.user_id
WHERE s.access_token = $1 AND s.expires_at > NOW()`,
token,
).Scan(&userID, &role)
if err != nil {
// Invalid or expired token, redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
if role != "admin" {
// Not admin, show forbidden
http.Error(w, "Forbidden: admin access required", http.StatusForbidden)
return
}
// User is authenticated and has admin role, render admin page
h.render(w, "admin.html", pageData{Title: "Админ-панель"})
}
// render executes the "base.html" template which {{template "content" .}}
// pulls in the per-page content block.
func (h *Handler) render(w http.ResponseWriter, page string, data pageData) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl, ok := h.templates[page]
if !ok {
log.Printf("Template not found: %s", page)
http.Error(w, "Template not found", http.StatusInternalServerError)
return
}
if err := tmpl.ExecuteTemplate(w, "base.html", data); err != nil {
log.Printf("Template error (%s): %v", page, err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
// ── Template parsing ───────────────────────────────────────────
// parseTemplates parses each page template individually by combining the base
// layout with the specific page content. This avoids the issue where
// multiple {{define "content"}} blocks in wildcard-parsed files overwrite
// each other (last alphabetically wins).
func (h *Handler) parseTemplates() {
pages := []string{"index.html", "login.html", "register.html", "profile.html", "admin.html"}
for _, page := range pages {
tmpl, err := template.New("base.html").Funcs(template.FuncMap{}).ParseFS(templateFS, "html/base.html", "html/"+page)
if err != nil {
log.Printf("Template parse error (%s): %v", page, err)
continue
}
h.templates[page] = tmpl
log.Printf("Loaded template: %s", page)
}
}