Details:
• HTML endpoints (/, /profile, /admin, /login, /register):
- Authenticate via HTTP-only cookie named 'token'
- Handlers in internal/templates/templates.go check cookie validity
- /admin endpoint additionally checks for role='admin'
- Unauthenticated users redirected to /login
- Non-admin users accessing /admin get HTTP 403 Forbidden
• API endpoints (/api/*):
- Authenticate via Bearer token in Authorization header only
- Handlers in internal/api/api.go use authenticateRequest() function
- Function extracts token from 'Authorization: Bearer <token>' header
- Validates token against yggdrasil_sessions table
- No cookie checking for API endpoints (launcher compatibility)
• Web login (/api/web/login):
- Sets HTTP-only cookie 'token' for browser storage
- Returns JSON with token, UUID, username for JS localStorage
- Maintains backward compatibility with existing JavaScript
• JavaScript in HTML pages:
- Gets token from localStorage (set by login response)
- Sets Authorization: Bearer <token> header for API fetch calls
- Updated admin.html and profile.js to include token in headers
This separation ensures:
• HTML endpoints work automatically with browser cookies
• API endpoints work with browsers (via JS) and launchers (Bearer tokens)
• Security sensitive actions require proper role validation
• Clean separation of concerns between document and API interfaces
173 lines
5.2 KiB
Go
173 lines
5.2 KiB
Go
// 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)
|
|
}
|
|
}
|