From 75ea7c70c216bdae7c16a00d7b9cef83e1c4ae0d Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Sun, 7 Jun 2026 23:11:51 +0300 Subject: [PATCH] auth: implement cookie-based auth for HTML endpoints and Bearer token auth for API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ' 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 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 --- internal/api/api.go | 12 ++++++++++ internal/templates/html/admin.html | 2 +- internal/templates/templates.go | 37 ++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 3721020..f9a2b37 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "strings" + "time" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/auth" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" @@ -246,6 +247,17 @@ func (h *Handler) webLogin(w http.ResponseWriter, r *http.Request) { return } + // Set authentication token in cookie for web frontend + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: token, + Path: "/", + Expires: time.Now().Add(7 * 24 * time.Hour), + HttpOnly: true, + Secure: r.TLS != nil, // Set secure flag if HTTPS + SameSite: http.SameSiteLaxMode, + }) + utils.WriteJSON(w, http.StatusOK, webLoginResponse{ Token: token, UUID: user.UUID, diff --git a/internal/templates/html/admin.html b/internal/templates/html/admin.html index 81ebb35..d4f1b76 100644 --- a/internal/templates/html/admin.html +++ b/internal/templates/html/admin.html @@ -177,7 +177,7 @@ document.getElementById('modpack-form').addEventListener('submit', async (e) => try { const response = await fetch('/api/admin/modpacks', { method: 'POST', - headers { + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, diff --git a/internal/templates/templates.go b/internal/templates/templates.go index b29b33b..60df733 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -29,6 +29,15 @@ type Handler struct { 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)} @@ -64,12 +73,36 @@ func (h *Handler) registerPage(w http.ResponseWriter, r *http.Request) { } 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) { - // Extract bearer token from Authorization header - token := auth.ExtractBearer(r.Header.Get("Authorization")) + // 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)