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)
}