- Create welcome surface: topbar + getting-started card shown when no extension surfaces are installed. Admins see a link to Packages, non-admins see a message to contact their administrator. - Update resolveDefaultSurface priority chain: user preference → global config → first extension → /welcome - Register welcome as a core surface with authenticated auth - Final fallback is now /welcome instead of /admin, breaking the infinite back-button loop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
178 lines
5.5 KiB
Go
178 lines
5.5 KiB
Go
package pages
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SeedSurfaces writes core surface manifests to the registry table.
|
|
// Called once at startup. Does NOT overwrite the enabled flag — admin
|
|
// toggles survive restarts.
|
|
func (e *Engine) SeedSurfaces() {
|
|
if e.stores.Packages == nil {
|
|
log.Printf("[pages] Surface registry not available — skipping seed")
|
|
return
|
|
}
|
|
ctx := context.Background()
|
|
for _, s := range e.surfaces {
|
|
manifest := map[string]any{
|
|
"route": s.Route,
|
|
"alt_routes": s.AltRoutes,
|
|
"template": s.Template,
|
|
"components": s.Components,
|
|
"data_requires": s.DataRequires,
|
|
"auth": s.Auth,
|
|
"layout": s.Layout,
|
|
}
|
|
if err := e.stores.Packages.Seed(ctx, s.ID, s.Title, s.Source, manifest); err != nil {
|
|
log.Printf("[pages] Failed to seed surface %q: %v", s.ID, err)
|
|
}
|
|
}
|
|
log.Printf("[pages] Seeded %d core surfaces into registry", len(e.surfaces))
|
|
|
|
// v0.31.0: Clean up old "editor" core surface row.
|
|
// Editor is now an installable package — the old seed row would show a
|
|
// broken nav link until the package .pkg is uploaded via admin UI.
|
|
if sr, err := e.stores.Packages.Get(ctx, "editor"); err == nil && sr != nil && sr.Source == "extension" {
|
|
if sr.Type == "" || sr.Type == "surface" {
|
|
if delErr := e.stores.Packages.Delete(ctx, "editor"); delErr != nil {
|
|
log.Printf("[pages] Failed to clean up old editor surface: %v", delErr)
|
|
} else {
|
|
log.Printf("[pages] Cleaned up old editor core surface — install editor .pkg via admin UI")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// IsSurfaceEnabled checks if a surface is enabled in the registry.
|
|
// Admin is always enabled (system-critical).
|
|
// Returns true if the surface is not found (fail-open for backward compat).
|
|
func (e *Engine) IsSurfaceEnabled(surfaceID string) bool {
|
|
if surfaceID == "admin" {
|
|
return true
|
|
}
|
|
if e.stores.Packages == nil {
|
|
return true // No registry = all surfaces enabled (backward compat)
|
|
}
|
|
ctx := context.Background()
|
|
sr, err := e.stores.Packages.Get(ctx, surfaceID)
|
|
if err != nil || sr == nil {
|
|
return true // Not in registry = assume enabled (backward compat)
|
|
}
|
|
return sr.Enabled
|
|
}
|
|
|
|
// EnabledSurfaceIDs returns a list of enabled surface IDs for nav rendering.
|
|
func (e *Engine) EnabledSurfaceIDs() []string {
|
|
if e.stores.Packages == nil {
|
|
// No registry — return all core surface IDs
|
|
var all []string
|
|
for _, s := range e.surfaces {
|
|
all = append(all, s.ID)
|
|
}
|
|
return all
|
|
}
|
|
ctx := context.Background()
|
|
ids, err := e.stores.Packages.ListEnabled(ctx)
|
|
if err != nil {
|
|
log.Printf("[pages] Failed to load enabled surfaces: %v", err)
|
|
// Fallback: return all core surface IDs
|
|
var all []string
|
|
for _, s := range e.surfaces {
|
|
all = append(all, s.ID)
|
|
}
|
|
return all
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// DefaultSurfaceRedirect returns a handler for GET / that redirects to the
|
|
// configured default surface, falling back to the first enabled extension
|
|
// surface, then /welcome.
|
|
func (e *Engine) DefaultSurfaceRedirect() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
target := e.resolveDefaultSurface(c.Request.Context(), userID)
|
|
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+target)
|
|
}
|
|
}
|
|
|
|
// resolveDefaultSurface returns the path to redirect to (without BasePath).
|
|
// Priority: user preference → global default_surface → first enabled extension → /welcome.
|
|
func (e *Engine) resolveDefaultSurface(ctx context.Context, userID string) string {
|
|
if e.stores.GlobalConfig == nil || e.stores.Packages == nil {
|
|
return "/welcome"
|
|
}
|
|
|
|
// 1. Check user preference (default_surface in user settings)
|
|
if userID != "" && e.stores.Users != nil {
|
|
if user, err := e.stores.Users.GetByID(ctx, userID); err == nil && user != nil {
|
|
if id, _ := user.Settings["default_surface"].(string); id != "" {
|
|
if path := e.surfacePath(ctx, id); path != "" {
|
|
return path
|
|
}
|
|
// User's chosen surface is missing or disabled — fall through
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Check admin-configured global default_surface
|
|
if raw, err := e.stores.GlobalConfig.Get(ctx, "default_surface"); err == nil && raw != nil {
|
|
if id, ok := raw["id"].(string); ok && id != "" {
|
|
if path := e.surfacePath(ctx, id); path != "" {
|
|
return path
|
|
}
|
|
// Configured surface is missing or disabled — fall through
|
|
}
|
|
}
|
|
|
|
// 3. First enabled extension surface
|
|
surfaces, err := e.stores.Packages.ListEnabledByType(ctx, "surface")
|
|
if err == nil {
|
|
for _, s := range surfaces {
|
|
if s.Source == "core" {
|
|
continue
|
|
}
|
|
if s.Enabled {
|
|
return "/s/" + s.ID
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Fallback — welcome surface (no extensions installed)
|
|
return "/welcome"
|
|
}
|
|
|
|
// surfacePath returns the URL path for a surface ID, or "" if the surface
|
|
// doesn't exist or is disabled.
|
|
func (e *Engine) surfacePath(ctx context.Context, id string) string {
|
|
pkg, err := e.stores.Packages.Get(ctx, id)
|
|
if err != nil || pkg == nil || !pkg.Enabled {
|
|
return ""
|
|
}
|
|
|
|
// Core surfaces live at /{id}, extension surfaces at /s/{slug}
|
|
if pkg.Source == "core" {
|
|
// Look up the core surface's base route
|
|
if sm := e.GetSurface(id); sm != nil {
|
|
// Use the first alt route (without params) or strip params from Route
|
|
if len(sm.AltRoutes) > 0 {
|
|
return sm.AltRoutes[0]
|
|
}
|
|
}
|
|
return "/" + id
|
|
}
|
|
return "/s/" + id
|
|
}
|
|
|
|
// disabledRedirect returns a handler that redirects to /.
|
|
// The DefaultSurfaceRedirect handler will resolve to the appropriate surface.
|
|
func (e *Engine) disabledRedirect() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/admin")
|
|
}
|
|
}
|