package api import ( "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" errImageNotFound = "Image not found" errValidationFailed = "Validation failed" msgPostDeleted = "Post deleted" msgImageRemoved = "Image removed" permAccessPosts = "golem15.journal.access_posts" permAccessOtherPosts = "golem15.journal.access_other_posts" permAccessPublish = "golem15.journal.access_publish" journalCover = "golem15.journal.*" ) func writeAPIError(w http.ResponseWriter, status int, message string) { wire.WriteJSON(w, status, map[string]any{"error": message}) } func writeValidation(w http.ResponseWriter, errs map[string][]string) { wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{ "error": errValidationFailed, "errors": errs, }) } 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) != "" } // resolveError maps cabana.ResolveBackend errors to the PHP Journal API bodies. func resolveError(w http.ResponseWriter, err error) { if errors.Is(err, cabana.ErrTokenScope) { writeAPIError(w, http.StatusForbidden, errInsufficientPerms) return } writeAuthRequired(w, err) } // optionalBackendPrincipal verifies a backend JWT or personal API token only // when Authorization Bearer is present. Failure is anonymous: public GET must // not 401. Uses cabana.ResolveBackend with cover golem15.journal.*. func optionalBackendPrincipal(app *backpack.App, r *http.Request) *http.Request { if r == nil || !hasBearer(r) { return r } out, _, err := cabana.ResolveBackend(app, r, journalCover) if err != nil || out == nil { return r } return out } // requireBackendPrincipal authenticates a backend JWT or personal API token // and writes the PHP {error:"Authentication required"} or // {error:"Insufficient permissions"} body (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) { out, pr, err := cabana.ResolveBackend(app, r, journalCover) if err != nil || pr == nil { resolveError(w, err) return r, nil, false } return out, 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 }