package authz
import (
"context"
"encoding/json"
"net/http"
"strings"
"sourcecraft.dev/bigbes/sr-ht-core/crypto"
)
// CookieName is the SourceHut unified-login session cookie. Its value is a
// Fernet token encrypted with the instance [sr.ht] network-key.
const CookieName = "sr.ht.unified-login.v1"
// UsernameFromRequest extracts the authenticated username from the unified-login
// cookie, or returns "" for an anonymous viewer. Any failure — missing cookie,
// undecryptable token, malformed JSON — is treated as anonymous rather than an
// error: this service never rejects a request on identity grounds, it only lets
// git.sr.ht decide what an anonymous viewer may see.
func UsernameFromRequest(r *http.Request) string {
c, err := r.Cookie(CookieName)
if err != nil {
return ""
}
// InitCrypto must have run (server.New does it); the network-key here is
// the same Fernet key meta.sr.ht used to seal the cookie.
payload := crypto.DecryptWithoutExpiration([]byte(c.Value))
if payload == nil {
return ""
}
var claims struct {
Name string `json:"name"`
}
if err := json.Unmarshal(payload, &claims); err != nil {
return ""
}
// Cookies carry the bare username; strip a leading "~" defensively in case
// a caller stored the canonical "~user" form.
return strings.TrimPrefix(claims.Name, "~")
}
type ctxKey int
const usernameKey ctxKey = iota
// Middleware stores the cookie-derived username in the request context. It
// never writes a 401: an anonymous viewer flows through with an empty username
// and git.sr.ht enforces visibility downstream.
func Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), usernameKey, UsernameFromRequest(r))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ForContext returns the username stored by Middleware, or "" if absent.
func ForContext(ctx context.Context) string {
username, _ := ctx.Value(usernameKey).(string)
return username
}