Feat default surface routing (#4)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #4.
This commit is contained in:
@@ -553,6 +553,18 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
||||
log.Printf("[packages] %s: %d dependencies recorded", pkgID, len(deps))
|
||||
}
|
||||
|
||||
// v0.2.1: Auto-set default_surface when the first extension surface is installed.
|
||||
if (pkgType == "surface" || pkgType == "full") && pkgSource != "core" {
|
||||
if raw, err := h.stores.GlobalConfig.Get(c.Request.Context(), "default_surface"); err != nil || raw == nil {
|
||||
dflt := models.JSONMap{"id": pkgID}
|
||||
if setErr := h.stores.GlobalConfig.Set(c.Request.Context(), "default_surface", dflt, ""); setErr != nil {
|
||||
log.Printf("[packages] failed to auto-set default_surface to %s: %v", pkgID, setErr)
|
||||
} else {
|
||||
log.Printf("[packages] Auto-set default_surface to %s (first extension surface)", pkgID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": pkgID,
|
||||
"title": title,
|
||||
|
||||
@@ -26,10 +26,10 @@ func NewTeamPackageSettingsHandler(s store.Stores) *TeamPackageSettingsHandler {
|
||||
|
||||
// GetTeamPackageSettings returns the team-scoped settings for a package,
|
||||
// along with the overridable schema keys for the UI.
|
||||
// GET /api/v1/teams/:teamId/packages/:pkgId/settings
|
||||
// GET /api/v1/teams/:teamId/packages/:id/settings
|
||||
func (h *TeamPackageSettingsHandler) GetTeamPackageSettings(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
pkgID := c.Param("pkgId")
|
||||
pkgID := c.Param("id")
|
||||
|
||||
pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID)
|
||||
if err != nil || pkg == nil {
|
||||
@@ -60,10 +60,10 @@ func (h *TeamPackageSettingsHandler) GetTeamPackageSettings(c *gin.Context) {
|
||||
|
||||
// UpdateTeamPackageSettings saves team-scoped overrides for a package.
|
||||
// Non-overridable keys are stripped before saving.
|
||||
// PUT /api/v1/teams/:teamId/packages/:pkgId/settings
|
||||
// PUT /api/v1/teams/:teamId/packages/:id/settings
|
||||
func (h *TeamPackageSettingsHandler) UpdateTeamPackageSettings(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
pkgID := c.Param("pkgId")
|
||||
pkgID := c.Param("id")
|
||||
|
||||
pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID)
|
||||
if err != nil || pkg == nil {
|
||||
@@ -108,10 +108,10 @@ func (h *TeamPackageSettingsHandler) UpdateTeamPackageSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// DeleteTeamPackageSettings removes team-scoped overrides for a package.
|
||||
// DELETE /api/v1/teams/:teamId/packages/:pkgId/settings
|
||||
// DELETE /api/v1/teams/:teamId/packages/:id/settings
|
||||
func (h *TeamPackageSettingsHandler) DeleteTeamPackageSettings(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
pkgID := c.Param("pkgId")
|
||||
pkgID := c.Param("id")
|
||||
|
||||
if err := h.stores.Packages.DeleteTeamSettings(c.Request.Context(), pkgID, teamID); err != nil {
|
||||
c.JSON(500, gin.H{"error": "failed to delete team settings"})
|
||||
|
||||
@@ -478,9 +478,9 @@ func main() {
|
||||
|
||||
// Team package settings — cascade overrides (v0.2.0)
|
||||
teamPkgSettingsH := handlers.NewTeamPackageSettingsHandler(stores)
|
||||
teamScoped.GET("/packages/:pkgId/settings", teamPkgSettingsH.GetTeamPackageSettings)
|
||||
teamScoped.PUT("/packages/:pkgId/settings", teamPkgSettingsH.UpdateTeamPackageSettings)
|
||||
teamScoped.DELETE("/packages/:pkgId/settings", teamPkgSettingsH.DeleteTeamPackageSettings)
|
||||
teamScoped.GET("/packages/:id/settings", teamPkgSettingsH.GetTeamPackageSettings)
|
||||
teamScoped.PUT("/packages/:id/settings", teamPkgSettingsH.UpdateTeamPackageSettings)
|
||||
teamScoped.DELETE("/packages/:id/settings", teamPkgSettingsH.DeleteTeamPackageSettings)
|
||||
|
||||
// Team audit log (team admins only — RequireTeamAdmin on group)
|
||||
teamScoped.GET("/audit", teams.ListTeamAuditLog)
|
||||
@@ -683,10 +683,8 @@ func main() {
|
||||
pages.SetVersion(Version)
|
||||
pageEngine := pages.New(cfg, stores)
|
||||
|
||||
// Root redirect → default surface (admin for now, configurable in v0.2.0)
|
||||
base.GET("/", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, cfg.BasePath+"/admin")
|
||||
})
|
||||
// Root redirect → configurable default surface (v0.2.1)
|
||||
base.GET("/", pageEngine.DefaultSurfaceRedirect())
|
||||
|
||||
// Login page — no auth required
|
||||
base.GET("/login", pageEngine.RenderLogin())
|
||||
|
||||
@@ -391,7 +391,7 @@ func (e *Engine) RenderExtensionSurface() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
if !sr.Enabled {
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/")
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/admin")
|
||||
return
|
||||
}
|
||||
if sr.Source == "core" {
|
||||
|
||||
@@ -90,9 +90,77 @@ func (e *Engine) EnabledSurfaceIDs() []string {
|
||||
return ids
|
||||
}
|
||||
|
||||
// disabledRedirect returns a handler that redirects to the root.
|
||||
func (e *Engine) disabledRedirect() gin.HandlerFunc {
|
||||
// DefaultSurfaceRedirect returns a handler for GET / that redirects to the
|
||||
// configured default surface, falling back to the first enabled extension
|
||||
// surface, then /admin.
|
||||
func (e *Engine) DefaultSurfaceRedirect() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/")
|
||||
target := e.resolveDefaultSurface(c.Request.Context())
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+target)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveDefaultSurface returns the path to redirect to (without BasePath).
|
||||
// Priority: configured default_surface → first enabled extension surface → /admin.
|
||||
func (e *Engine) resolveDefaultSurface(ctx context.Context) string {
|
||||
if e.stores.GlobalConfig == nil || e.stores.Packages == nil {
|
||||
return "/admin"
|
||||
}
|
||||
|
||||
// 1. Check configured 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
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback
|
||||
return "/admin"
|
||||
}
|
||||
|
||||
// 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 /admin.
|
||||
// Uses /admin (not /) to avoid a redirect loop when the default surface
|
||||
// is the one being disabled.
|
||||
func (e *Engine) disabledRedirect() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/admin")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user