// 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 } // getCookieToken extracts the authentication token from the request cookie. func (h *Handler) getCookieToken(r *http.Request) string { // Check cookie named "token" if cookie, err := r.Cookie("token"); err == nil { return cookie.Value } return "" } // 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) { // Get token from cookie only (HTML endpoint) token := h.getCookieToken(r) if token == "" { // No token provided, redirect to login http.Redirect(w, r, "/login", http.StatusSeeOther) return } // Validate token (just check if valid, don't need role check for profile) var userID int err := h.db.Pool().QueryRow(r.Context(), `SELECT user_id FROM yggdrasil_sessions WHERE access_token = $1 AND expires_at > NOW()`, token, ).Scan(&userID) if err != nil { // Invalid or expired token, redirect to login http.Redirect(w, r, "/login", http.StatusSeeOther) return } // User is authenticated, render profile page h.render(w, "profile.html", pageData{Title: "Профиль"}) } func (h *Handler) adminPage(w http.ResponseWriter, r *http.Request) { // Get token from cookie only (HTML endpoint) token := h.getCookieToken(r) 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) } }