package web import ( "net/http" "net/url" "sort" "strings" "github.com/vaughan0/go-ini" "sourcecraft.dev/bigbes/sr-ht-core/config" "sourcecraft.dev/bigbes/sr-ht-spec/authn" ) // navCanonical is the SourceHut service-switcher order. Services not listed // here (including our own spec) sort alphabetically after these. var navCanonical = []string{"hub", "git", "hg", "lists", "todo", "builds", "man", "meta"} // navExcluded are service sections that never appear in the switcher: paste and // pages have no top-level UI worth linking, and hub is rendered as the brand. var navExcluded = map[string]bool{"paste": true, "pages": true, "hub": true} // navItem is one entry in the service switcher. type navItem struct { Name string // short service name, e.g. "git" Origin string // external origin URL Active bool // true for spec.sr.ht (this service) } // buildNav derives the service switcher from the shared config: every section // whose name ends in ".sr.ht" (with a configured origin) except paste/pages/hub, // ordered by navCanonical then alphabetically, with spec.sr.ht marked active. // // The ".sr.ht" suffix is the whole membership rule — it is what // core.sr.ht's own _network does, and it is why our section must be named // literally "spec.sr.ht" no matter what host it is served from. func buildNav(conf ini.File) []navItem { var items []navItem for section := range conf { if !strings.HasSuffix(section, ".sr.ht") { continue } short := strings.TrimSuffix(section, ".sr.ht") if navExcluded[short] { continue } origin := config.GetOrigin(conf, section, true) if origin == "" { continue } items = append(items, navItem{ Name: short, Origin: origin, Active: section == authn.ConfigSection, }) } sort.SliceStable(items, func(i, j int) bool { ci, cj := canonIndex(items[i].Name), canonIndex(items[j].Name) if ci != cj { return ci < cj } return items[i].Name < items[j].Name }) return items } // canonIndex returns a service's position in navCanonical, or a sentinel past // the end for services that are not canonically ordered. func canonIndex(name string) int { for i, n := range navCanonical { if n == name { return i } } return len(navCanonical) } // viewData is the root value every template is executed against: the chrome // fields are common to all pages; Data carries the page-specific payload. type viewData struct { Title string SiteName string HubOrigin string // non-empty ⇒ brand links to hub instead of "/" Nav []navItem Username string // "" for a viewer with no authority LoginURL string LogoutURL string RegisterURL string ProfileURL string CSSHref string // "" when the binary was built without a stylesheet Environment string ShowBanner bool // ContainerClass selects the width of the page's content wrapper. ContainerClass string Data any } // chrome builds the common chrome fields for a request. Login return_to is the // current full URL (so the viewer lands back where they were); logout return_to // is this service's origin. // // Username is the *authoritative* identity, not whatever the cookie said: a // logged-in human who is not the instance owner resolves to anonymous, so the // nav shows "log in" to them rather than greeting them by a name that grants // nothing. func (s *Server) chrome(r *http.Request) viewData { p := authn.PrincipalFromContext(r.Context()) username := "" if p.IsOwner() { username = p.Owner } current := s.origin + r.URL.RequestURI() loginURL := s.metaOrigin + "/login?return_to=" + url.QueryEscape(current) logoutURL := s.metaOrigin + "/logout?return_to=" + url.QueryEscape(s.origin) profileURL := s.metaOrigin + "/profile" if s.hubOrigin != "" && username != "" { profileURL = s.hubOrigin + "/~" + username } return viewData{ ContainerClass: "container", SiteName: s.siteName, HubOrigin: s.hubOrigin, Nav: s.nav, Username: username, LoginURL: loginURL, LogoutURL: logoutURL, RegisterURL: s.metaOrigin, ProfileURL: profileURL, CSSHref: s.cssHref, Environment: strings.ToUpper(s.environment), ShowBanner: s.environment != "" && s.environment != "production", } } // loginRedirect sends a viewer with no read authority to meta.sr.ht's login, // with return_to pointing back at what they asked for. There is no login flow // of our own — identity is the shared unified-login cookie and nothing else. func (s *Server) loginRedirect(w http.ResponseWriter, r *http.Request) { current := s.origin + r.URL.RequestURI() http.Redirect(w, r, s.metaOrigin+"/login?return_to="+url.QueryEscape(current), http.StatusFound) }