Add authenticated content editing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Daniel Noulet 2026-09-19 14:56:49 +02:00
parent fac7588d23
commit fd2877da9e
15 changed files with 581 additions and 37 deletions

View file

@ -4,6 +4,8 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/example/cms/internal/domain"
"github.com/example/cms/internal/identity"
"github.com/example/cms/internal/service"
"github.com/example/cms/internal/web/mapping"
@ -13,13 +15,17 @@ import (
)
type Handler struct {
auth *service.AuthService
google identity.GoogleProvider
auth *service.AuthService
google identity.GoogleProvider
content service.IContentService
}
func NewHandler(a *service.AuthService, google identity.GoogleProvider) *Handler {
return &Handler{auth: a, google: google}
}
func NewHandlerWithContent(a *service.AuthService, google identity.GoogleProvider, content service.IContentService) *Handler {
return &Handler{auth: a, google: google, content: content}
}
func (h *Handler) Routes(origins string) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { jsonWrite(w, 200, map[string]string{"status": "ok"}) })
@ -29,6 +35,9 @@ func (h *Handler) Routes(origins string) http.Handler {
mux.HandleFunc("/api/auth/session", h.session)
mux.HandleFunc("/admin/login", h.adminLogin)
mux.HandleFunc("/api/test/protected", h.protected)
mux.HandleFunc("/api/protected", h.protected)
mux.HandleFunc("/api/content", h.contentList)
mux.HandleFunc("/api/content/", h.contentDetail)
return cors(mux, origins)
}
func (h *Handler) googleLogin(w http.ResponseWriter, r *http.Request) {
@ -77,7 +86,12 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request) {
jsonWrite(w, http.StatusUnauthorized, map[string]string{"authenticated": "false"})
return
}
jsonWrite(w, http.StatusOK, map[string]bool{"authenticated": true})
u, err := h.auth.UserByID(r.Context(), *s.UserID)
if err != nil {
jsonWrite(w, http.StatusUnauthorized, map[string]string{"authenticated": "false"})
return
}
jsonWrite(w, http.StatusOK, map[string]any{"authenticated": true, "user": u, "canEdit": h.auth.CanEdit(r.Context(), s)})
}
func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
@ -128,6 +142,87 @@ func (h *Handler) protected(w http.ResponseWriter, r *http.Request) {
}
jsonWrite(w, 200, map[string]string{"status": "authenticated"})
}
func (h *Handler) sessionForRequest(r *http.Request) (domain.Session, bool) {
t := bearerOrCookie(r)
if t == "" {
return domain.Session{}, false
}
if s, e := h.auth.Authenticate(r.Context(), t); e == nil {
return s, true
}
if s, e := h.auth.AdminAuthenticate(r.Context(), t); e == nil {
return s, true
}
return domain.Session{}, false
}
func (h *Handler) contentList(w http.ResponseWriter, r *http.Request) {
if h.content == nil {
jsonWrite(w, 503, map[string]string{"error": "content unavailable"})
return
}
if _, ok := h.sessionForRequest(r); !ok {
jsonWrite(w, 401, map[string]string{"error": "authentication required"})
return
}
if r.Method != "GET" {
jsonWrite(w, http.StatusMethodNotAllowed, nil)
return
}
limit, offset := 20, 0
_, _ = fmt.Sscanf(r.URL.Query().Get("limit"), "%d", &limit)
_, _ = fmt.Sscanf(r.URL.Query().Get("offset"), "%d", &offset)
items, e := h.content.List(r.Context(), limit, offset)
if e != nil {
jsonWrite(w, 500, map[string]string{"error": e.Error()})
return
}
jsonWrite(w, 200, map[string]any{"items": items, "limit": limit, "offset": offset})
}
func (h *Handler) contentDetail(w http.ResponseWriter, r *http.Request) {
if h.content == nil {
jsonWrite(w, 503, map[string]string{"error": "content unavailable"})
return
}
slug := strings.TrimPrefix(r.URL.Path, "/api/content/")
if r.Method == "GET" {
if _, ok := h.sessionForRequest(r); !ok {
jsonWrite(w, 401, map[string]string{"error": "authentication required"})
return
}
c, e := h.content.Get(r.Context(), slug)
if e != nil {
jsonWrite(w, 404, map[string]string{"error": "content not found"})
return
}
jsonWrite(w, 200, c)
return
}
if r.Method != "PUT" {
jsonWrite(w, 405, nil)
return
}
s, ok := h.sessionForRequest(r)
if !ok {
jsonWrite(w, 401, map[string]string{"error": "authentication required"})
return
}
if !h.auth.CanEdit(r.Context(), s) {
jsonWrite(w, 403, map[string]string{"error": "forbidden"})
return
}
var c domain.Content
if !decode(r, &c) {
jsonWrite(w, 400, map[string]string{"error": "invalid JSON"})
return
}
c.Slug = slug
out, e := h.content.Update(r.Context(), c, s)
if e != nil {
jsonWrite(w, 400, map[string]string{"error": e.Error()})
return
}
jsonWrite(w, 200, out)
}
func bearerOrCookie(r *http.Request) string {
header := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
if header != "" {
@ -155,7 +250,7 @@ func cors(next http.Handler, origin string) http.Handler {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS")
if r.Method == "OPTIONS" {
w.WriteHeader(204)
return