Files
sm-journal-plugin/controllers/api/auth.go
Jakub Zych f2e5b3d072 feat(15-03): serve anonymous GET /_journal/api/v1/posts as a published list
- Public and write groups under /_journal/api/v1 with journal-public-api and journal-api buckets (Max 120)
- Index omits drafts and future published_at; per_page default 9 max 30
- Optional backend JWT on GET; missing Bearer on writes returns PHP {error:Authentication required}

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:54:41 +02:00

121 lines
3.7 KiB
Go

package api
import (
"database/sql"
"errors"
"net/http"
"strings"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/wire"
"gorm.io/gorm"
)
const (
errAuthenticationRequired = "Authentication required"
errInsufficientPerms = "Insufficient permissions"
errPublishPermission = "You do not have permission to publish posts"
errEditPermission = "You do not have permission to edit this post"
errPostNotFound = "Post not found"
errValidationFailed = "Validation failed"
permAccessPosts = "golem15.journal.access_posts"
permAccessOtherPosts = "golem15.journal.access_other_posts"
permAccessPublish = "golem15.journal.access_publish"
backendJWTBlacklistTable = "backend_jwt_blacklist"
)
func writeAPIError(w http.ResponseWriter, status int, message string) {
wire.WriteJSON(w, status, map[string]any{"error": message})
}
func writeAuthRequired(w http.ResponseWriter, _ error) {
writeAPIError(w, http.StatusUnauthorized, errAuthenticationRequired)
}
func hasBearer(r *http.Request) bool {
if r == nil {
return false
}
h := strings.TrimSpace(r.Header.Get("Authorization"))
token, ok := strings.CutPrefix(h, "Bearer ")
return ok && strings.TrimSpace(token) != ""
}
func newBackendGuard(app *backpack.App, write func(http.ResponseWriter, error)) bouncer.Guard {
secret := ""
if app != nil && app.Config != nil {
secret = strings.TrimSpace(app.Config.String("admin.jwt.secret"))
}
var gdb *gorm.DB
var sqlDB *sql.DB
var reg *cabana.Registry
if app != nil {
gdb, _ = app.Lookup[*gorm.DB]()
sqlDB, _ = app.Lookup[*sql.DB]()
reg, _ = app.Lookup[*cabana.Registry]()
if sqlDB == nil && gdb != nil {
if db, err := gdb.DB(); err == nil {
sqlDB = db
}
}
}
var bl bouncer.BlacklistStore
if sqlDB != nil {
bl = bouncer.NewPostgresBlacklist(sqlDB, backendJWTBlacklistTable)
}
return bouncer.NewBackendJWTGuard(secret, cabana.BackendUsers{DB: gdb, Registry: reg}, bl, write)
}
// optionalBackendPrincipal verifies a backend JWT only when Authorization
// Bearer is present. Failure is anonymous: public GET must not 401.
func optionalBackendPrincipal(app *backpack.App, r *http.Request) *http.Request {
if r == nil || !hasBearer(r) {
return r
}
pr, err := newBackendGuard(app, nil).Authenticate(r)
if err != nil || pr == nil {
return r
}
return r.WithContext(bouncer.WithUser(r.Context(), pr))
}
// requireBackendPrincipal authenticates a backend-audience JWT and writes the
// PHP {error:"Authentication required"} body when the token is missing or
// invalid (D-15). It does not use cabana's admin unauthenticated envelope.
func requireBackendPrincipal(app *backpack.App, w http.ResponseWriter, r *http.Request) (*http.Request, *bouncer.Principal, bool) {
pr, err := newBackendGuard(app, writeAuthRequired).Authenticate(r)
if err != nil || pr == nil {
writeAuthRequired(w, err)
return r, nil, false
}
r = r.WithContext(bouncer.WithUser(r.Context(), pr))
return r, pr, true
}
func isEditor(pr *bouncer.Principal) bool {
return cabana.Allows(pr, []string{permAccessPosts})
}
func requireAccessPosts(w http.ResponseWriter, pr *bouncer.Principal) bool {
if !cabana.Allows(pr, []string{permAccessPosts}) {
writeAPIError(w, http.StatusForbidden, errInsufficientPerms)
return false
}
return true
}
func gdb(app *backpack.App) (*gorm.DB, error) {
if app == nil {
return nil, errors.New("journal api: application is not configured")
}
db, ok := app.Lookup[*gorm.DB]()
if !ok || db == nil {
return nil, errors.New("journal api: database is not configured")
}
return db, nil
}