remove publisher module
7 files changed, 0 insertions(+), 489 deletions(-)
changed files
D domain/content/publisher/app.go
@@ -1,101 +0,0 @@ -package publisher - -import ( - "context" - "net/http" - - "alin.ovh/x/log" - "github.com/Southclaws/fault" - "github.com/Southclaws/fault/fmsg" - "github.com/coreos/go-oidc/v3/oidc" - "go.hacdias.com/indielib/indieauth" - "golang.org/x/oauth2" - - "alin.ovh/homestead/domain/web/templates" - "alin.ovh/homestead/shared/config" - ihttp "alin.ovh/homestead/shared/http" -) - -type OIDCClientConfig struct { - URL config.URL - ClientID string - ClientSecret string -} - -type Options struct { - Development bool `conf:"-"` - OIDC OIDCClientConfig - BaseURL *config.URL - VCSRemoteURL *config.URL `conf:"default:https://git.alin.ovh/website"` -} - -type Service struct { - log *log.Logger - indieauthServer *indieauth.Server - siteSettings templates.SiteSettings - baseURL *config.URL - oauth2Config oauth2.Config - oidcVerifier *oidc.IDTokenVerifier -} - -func New(opts *Options, log *log.Logger) (*Service, error) { - ctx := context.Background() - provider, err := oidc.NewProvider(ctx, opts.OIDC.URL.String()) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("failed to create OIDC provider")) - } - - if opts.OIDC.ClientID == "" || opts.OIDC.ClientSecret == "" { - return nil, fault.New("OIDC client ID and secret are required") - } - - oauth2Config := oauth2.Config{ - ClientID: opts.OIDC.ClientID, - ClientSecret: opts.OIDC.ClientSecret, - RedirectURL: opts.BaseURL.JoinPath("/auth/callback").String(), - - // Discovery returns the OAuth2 endpoints. - Endpoint: provider.Endpoint(), - - // "openid" is a required scope for OpenID Connect flows. - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - } - - oidcConfig := &oidc.Config{ - ClientID: opts.OIDC.ClientID, - } - - verifier := provider.Verifier(oidcConfig) - - service := &Service{ - log: log, - baseURL: opts.BaseURL, - indieauthServer: indieauth.NewServer(true, &http.Client{}), - siteSettings: templates.SiteSettings{ - Title: "Barkeep", - Language: "en-GB", - Menu: []config.MenuItem{}, - InjectLiveReload: opts.Development, - }, - oauth2Config: oauth2Config, - oidcVerifier: verifier, - } - - if opts.BaseURL.Path == "" { - opts.BaseURL.Path = "/" - } - err = indieauth.IsValidProfileURL(opts.BaseURL.String()) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("invalid base URL")) - } - - return service, nil -} - -func (s *Service) RegisterHandlers(mux *ihttp.ServeMux) { - mux.HandleFunc("/admin/style.css", s.Style) - mux.HandleFunc("/admin/auth/login", s.Login) - mux.HandleFunc("/admin/auth/callback", s.Callback) - mux.HandleFunc("/admin/auth/logout", s.Logout) - mux.HandleFunc("/admin/{$}", s.Index) -}
D domain/content/publisher/mux.go
@@ -1,271 +0,0 @@ -package publisher - -import ( - "crypto/rand" - "encoding/base64" - "encoding/json" - "net/http" - - "golang.org/x/oauth2" - - "alin.ovh/homestead/domain/content/publisher/templates" - basetpl "alin.ovh/homestead/domain/web/templates" - ihttp "alin.ovh/homestead/shared/http" -) - -type user struct { - ID string `json:"sub"` - Email string `json:"email"` - EmailVerified bool `json:"email_verified"` - Name string `json:"name"` -} - -const ( - sessionCookieName = "oidc_session" - stateCookieName = "oidc_state" - verifierCookieName = "oidc_verifier" -) - -var ( - ErrCannotDetermineUser = ihttp.NewError("cannot determine user", http.StatusInternalServerError) - ErrRenderFailure = ihttp.NewError("failed to render page", http.StatusInternalServerError) -) - -func (s *Service) Index(w http.ResponseWriter, r *http.Request) error { - userName := "Guest" - isLoggedIn := false - if user, err := getUserFromRequest(r); err == nil { - isLoggedIn = true - userName = user.Name - if userName == "" { - userName = user.Email - } - } - - w.Header().Set("Vary", "Cookie") - - err := templates.IndexPage(s.siteSettings, templates.PageSettings{ - User: userName, - IsLoggedIn: isLoggedIn, - PageSettings: basetpl.PageSettings{ - Title: "Home", - }, - }).Render(w) - if err != nil { - return ErrRenderFailure - } - - return nil -} - -// Login initiates the OIDC authentication flow with PKCE S256. -// PKCE (Proof Key for Code Exchange) protects against authorization code interception attacks -// by using a cryptographically random verifier and its SHA256 hash challenge. -// See: https://www.rfc-editor.org/rfc/rfc7636 -func (s *Service) Login(w http.ResponseWriter, r *http.Request) error { - // Generate random state for CSRF protection - state, err := generateRandomState() - if err != nil { - s.log.Error("failed to generate state", "error", err) - - return ihttp.NewError("failed to generate state", http.StatusInternalServerError) - } - - // Generate PKCE verifier for S256 challenge - verifier := oauth2.GenerateVerifier() - - // Store state in cookie for verification in callback - http.SetCookie(w, &http.Cookie{ - Name: stateCookieName, - Value: state, - Path: s.baseURL.Path, - MaxAge: 600, // 10 minutes - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteLaxMode, - }) - - // Store PKCE verifier in cookie for token exchange - http.SetCookie(w, &http.Cookie{ - Name: verifierCookieName, - Value: verifier, - Path: s.baseURL.Path, - MaxAge: 600, // 10 minutes - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteLaxMode, - }) - - // Redirect to OIDC provider with PKCE S256 challenge - url := s.oauth2Config.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier)) - http.Redirect(w, r, url, http.StatusFound) - - return nil -} - -// Callback handles the OIDC provider's redirect after authentication. -// It verifies the state parameter (CSRF protection), exchanges the authorization code -// for tokens using the PKCE verifier, validates the ID token, and creates a session. -func (s *Service) Callback(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - - // Verify state parameter (CSRF protection) - stateCookie, err := r.Cookie(stateCookieName) - if err != nil { - s.log.Error("state cookie not found", "error", err) - - return ihttp.NewError("invalid state", http.StatusBadRequest) - } - - state := r.URL.Query().Get("state") - if state != stateCookie.Value { - s.log.Error("state mismatch") - - return ihttp.NewError("invalid state", http.StatusBadRequest) - } - - // Retrieve PKCE verifier for token exchange - verifierCookie, err := r.Cookie(verifierCookieName) - if err != nil { - s.log.Error("verifier cookie not found", "error", err) - - return ihttp.NewError("invalid verifier", http.StatusBadRequest) - } - - // Clear state cookie - http.SetCookie(w, &http.Cookie{ - Name: stateCookieName, - Value: "", - Path: s.baseURL.Path, - MaxAge: -1, - HttpOnly: true, - }) - - // Clear verifier cookie - http.SetCookie(w, &http.Cookie{ - Name: verifierCookieName, - Value: "", - Path: s.baseURL.Path, - MaxAge: -1, - HttpOnly: true, - }) - - // Exchange code for token with PKCE verifier - code := r.URL.Query().Get("code") - oauth2Token, err := s.oauth2Config.Exchange( - ctx, - code, - oauth2.VerifierOption(verifierCookie.Value), - ) - if err != nil { - s.log.Error("failed to exchange token", "error", err) - - return ihttp.NewError("failed to exchange token", http.StatusInternalServerError) - } - - // Extract ID Token from OAuth2 token - rawIDToken, ok := oauth2Token.Extra("id_token").(string) - if !ok { - s.log.Error("no id_token in token response") - - return ihttp.NewError("no id_token in token response", http.StatusInternalServerError) - } - - // Verify ID Token - idToken, err := s.oidcVerifier.Verify(ctx, rawIDToken) - if err != nil { - s.log.Error("failed to verify ID token", "error", err) - - return ihttp.NewError("failed to verify ID token", http.StatusInternalServerError) - } - - // Extract user claims - var userInfo user - if err := idToken.Claims(&userInfo); err != nil { - s.log.Error("failed to parse claims", "error", err) - - return ihttp.NewError("failed to parse claims", http.StatusInternalServerError) - } - - // Store user info in session cookie - sessionData, err := json.Marshal(userInfo) - if err != nil { - s.log.Error("failed to marshal session data", "error", err) - - return ihttp.NewError("failed to create session", http.StatusInternalServerError) - } - - http.SetCookie(w, &http.Cookie{ - Name: sessionCookieName, - Value: base64.StdEncoding.EncodeToString(sessionData), - Path: s.baseURL.Path, - MaxAge: 86400 * 7, // 7 days - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteLaxMode, - }) - - s.log.Info("user logged in", "email", userInfo.Email, "name", userInfo.Name) - - http.Redirect(w, r, s.baseURL.Path, http.StatusFound) - - return nil -} - -func (s *Service) Logout(w http.ResponseWriter, r *http.Request) error { - // Clear session cookie - http.SetCookie(w, &http.Cookie{ - Name: sessionCookieName, - Value: "", - Path: s.baseURL.Path, - MaxAge: -1, - HttpOnly: true, - }) - - s.log.Info("user logged out") - - // Redirect to home page - http.Redirect(w, r, s.baseURL.Path, http.StatusFound) - - return nil -} - -// getUserFromRequest extracts and decodes user information from the session cookie. -// Returns an error if the cookie is missing or invalid. -func getUserFromRequest(r *http.Request) (*user, error) { - cookie, err := r.Cookie(sessionCookieName) - if err != nil { - return nil, err - } - - sessionData, err := base64.StdEncoding.DecodeString(cookie.Value) - if err != nil { - return nil, err - } - - var userInfo user - if err := json.Unmarshal(sessionData, &userInfo); err != nil { - return nil, err - } - - return &userInfo, nil -} - -// generateRandomState generates a cryptographically secure random state string -// for CSRF protection in the OAuth2/OIDC flow. -func generateRandomState() (string, error) { - b := make([]byte, 32) - _, err := rand.Read(b) - if err != nil { - return "", err - } - - return base64.URLEncoding.EncodeToString(b), nil -} - -func (s *Service) Style(w http.ResponseWriter, r *http.Request) error { - w.Header().Set("Content-Type", "text/css") - http.ServeFileFS(w, r, basetpl.Files, "style.css") - - return nil -}
D domain/content/publisher/templates/index.go
@@ -1,44 +0,0 @@ -package templates - -import ( - g "alin.ovh/gomponents" - . "alin.ovh/gomponents/html" - - base "alin.ovh/homestead/domain/web/templates" -) - -type PageSettings struct { - User string - IsLoggedIn bool - base.PageSettings -} - -func IndexPage(site base.SiteSettings, page PageSettings) g.Node { - return Layout(site, page.PageSettings, - Div( - H1(g.Textf("Welcome, %s!", page.User)), - g.If(page.IsLoggedIn, - Div( - P(g.Text("You are currently logged in.")), - P( - A( - g.Attr("href", "/admin/auth/logout"), - g.Text("Logout"), - ), - ), - ), - ), - g.If(!page.IsLoggedIn, - Div( - P(g.Text("Please log in to access protected features.")), - P( - A( - g.Attr("href", "/admin/auth/login"), - g.Text("Login"), - ), - ), - ), - ), - ), - ) -}
D domain/content/publisher/templates/layout.go
@@ -1,40 +0,0 @@ -package templates - -import ( - g "alin.ovh/gomponents" - c "alin.ovh/gomponents/components" - . "alin.ovh/gomponents/html" - - base "alin.ovh/homestead/domain/web/templates" -) - -func Layout(site base.SiteSettings, page base.PageSettings, children ...g.Node) g.Node { - return c.HTML5(c.HTML5Props{ - Title: site.Title, - Description: "", - Language: site.Language, - Head: []g.Node{ - Link( - Rel("stylesheet"), - Href("/style.css"), - ), - }, - Body: []g.Node{ - Header( - H1(A( - base.ExtendAttrs(base.Attrs{ - "class": "title", - "href": "/", - }, page.TitleAttrs), - g.Text(site.Title), - )), - ), - Main(ID("main"), g.Group(children)), - Footer( - A(Href("https://git.alin.ovh/homestead"), g.Text("Source Code")), - ), - g.If(site.InjectLiveReload, base.LiveReload), - }, - HTMLAttrs: []g.Node{}, - }) -}
M domain/web/website.go → domain/web/website.go
@@ -14,7 +14,6 @@ "alin.ovh/homestead/domain/analytics" "alin.ovh/homestead/domain/analytics/goatcounter" "alin.ovh/homestead/domain/analytics/nullcounter" "alin.ovh/homestead/domain/content/fetcher" - "alin.ovh/homestead/domain/content/publisher" "alin.ovh/homestead/domain/identity" "alin.ovh/homestead/domain/identity/oidc" "alin.ovh/homestead/domain/web/server"@@ -37,7 +36,6 @@ Timezone config.Timezone `conf:"default:Europe/Berlin"` Goatcounter goatcounter.Options OIDC oidc.Config - OIDCClient publisher.OIDCClientConfig Redis *events.RedisOptions LiveReload *livereload.LiveReload `conf:"-"`@@ -50,7 +48,6 @@ counter analytics.Counter log *log.Logger reader storage.Reader identity *identity.Service - publisher *publisher.Service *server.App }@@ -90,15 +87,6 @@ return nil, fault.Wrap(err, fmsg.With("could not create update listener")) } website.identity = identity.New(opts.OIDC, log.Named("identity")) - website.publisher, err = publisher.New(&publisher.Options{ - Development: opts.Development, - BaseURL: opts.BaseURL.JoinPath("/admin/"), - OIDC: opts.OIDCClient, - }, log.Named("publisher")) - if err != nil { - log.Warn("could not initialise publisher", "error", err) - } - if opts.Goatcounter.URL == nil || opts.Goatcounter.Token == "" { if !opts.Development { log.Warn("in production without a goatcounter url/token")@@ -167,9 +155,6 @@ mux := ihttp.NewServeMux(log.Named("http")) mux.HandleError(website.ErrorHandler) mux.Handle("/", website) website.identity.RegisterHandlers(mux) - if website.publisher != nil { - website.publisher.RegisterHandlers(mux) - } if opts.Development { staticHandler := func(w http.ResponseWriter, r *http.Request) error {
M go.mod → go.mod
@@ -12,7 +12,6 @@ github.com/adrg/frontmatter v0.2.0 github.com/andybalholm/brotli v1.2.0 github.com/ardanlabs/conf/v3 v3.10.0 github.com/benpate/digit v0.13.12 - github.com/coreos/go-oidc/v3 v3.17.0 github.com/deckarep/golang-set/v2 v2.8.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-git/go-git/v5 v5.18.0@@ -24,9 +23,7 @@ github.com/redis/go-redis/v9 v9.17.2 github.com/snabb/sitemap v1.0.4 github.com/stefanfritsch/goldmark-fences v1.0.0 github.com/yuin/goldmark v1.7.13 - go.hacdias.com/indielib v0.4.3 go.uber.org/zap v1.27.1 - golang.org/x/oauth2 v0.34.0 ) require (@@ -49,7 +46,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.8.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect@@ -76,6 +72,4 @@ golang.org/x/sys v0.43.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect moul.io/zapfilter v1.7.0 // indirect - willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e // indirect - willnorris.com/go/webmention v0.0.0-20250531043116-33a44c5fb605 // indirect )
M go.sum → go.sum
@@ -53,8 +53,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= -github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=@@ -83,8 +81,6 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=@@ -177,8 +173,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.hacdias.com/indielib v0.4.3 h1:1QT0ZzMk+vMkoe4uZ31DLnWlLklGPgBYry0I+lCl0qM= -go.hacdias.com/indielib v0.4.3/go.mod h1:W7tSM6pCiM2JLdZ8xzSMpPf3GBB2hz+ONvGfvdp6S9o= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=@@ -215,8 +209,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=@@ -277,7 +269,3 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= moul.io/zapfilter v1.7.0 h1:7aFrG4N72bDH9a2BtYUuUaDS981Dxu3qybWfeqaeBDU= moul.io/zapfilter v1.7.0/go.mod h1:M+N2s+qZiA+bzRoyKMVRxyuERijS2ovi2pnMyiOGMvc= -willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e h1:TRIOwo0NxN4KVSgYlYmiQktd9I96YgZ3942/JVzhwTM= -willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e/go.mod h1:zzo0hFA/E/nl1ZAjXiXA7KCKwCTdgBU+7HXltGgHeGA= -willnorris.com/go/webmention v0.0.0-20250531043116-33a44c5fb605 h1:emBiWPMoSWXbAwOBbDd/2RSKdIyAw38S4UlFB91OzoY= -willnorris.com/go/webmention v0.0.0-20250531043116-33a44c5fb605/go.mod h1:HizkYGdDsLSSolZorYKuV/l7Y2BXEfiOSjShTrCMNsM=