implement auth using Auth0

This commit is contained in:
2025-12-29 23:49:23 -07:00
parent c9100383e4
commit cc74842e63
18 changed files with 797 additions and 212 deletions
+102 -194
View File
@@ -5,234 +5,142 @@ import (
"fmt"
"html/template"
"net/http"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/angelbeltran/templater"
"ruben/inventory2/internal/domains/accounts"
"ruben/inventory2/internal/domains/authentication"
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
"ruben/inventory2/internal/domains/raw_events"
)
func NewSiteHandler(
dir string,
type Server struct {
mux *http.ServeMux
contentDir string
templater *templater.Templater
rawEvents *raw_events.Store
accts *accounts.Store
etsy *etsy_platform.Platform
auth *authentication.Authenticator
}
func NewServer(
contentDir string,
rawEvents *raw_events.Store,
accts *accounts.Store,
etsy *etsy_platform.Platform,
) http.Handler {
mux := http.NewServeMux()
auth *authentication.Authenticator,
) *Server {
s := &Server{
mux: http.NewServeMux(),
contentDir: contentDir,
templater: templater.NewTemplater(
contentDir+"/templates",
func() template.FuncMap {
return template.FuncMap{
"buildSitePath": func(parts ...any) string {
strParts := make([]string, len(parts))
for i, p := range parts {
strParts[i] = fmt.Sprint(p)
}
// TODO: make "/site" dynamic somehow
//return path.Join(append([]string{"/site"}, strParts...)...)
return path.Join(strParts...)
},
"splitPath": func(p string) []string {
if p == "" {
return nil
}
return strings.Split(strings.TrimSuffix(strings.TrimPrefix(p, "/"), "/"), "/")
},
"prettyPrintJSON": func(j json.RawMessage) string {
b, err := json.MarshalIndent(j, " ", "")
if err != nil {
return string(j)
}
return string(b)
},
"parseInt64": func(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
},
"addInt": func(a, b int) int {
return a + b
},
"subInt": func(a, b int) int {
return a - b
},
"multInt": func(a, b int) int {
return a * b
},
}
},
),
rawEvents: rawEvents,
accts: accts,
etsy: etsy,
auth: auth,
}
// api routes
mux.HandleFunc("POST /accounts", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
email := r.FormValue("email")
if email == "" {
http.Error(w, "no email provided", http.StatusBadRequest)
return
}
s.mux.HandleFunc("GET /login", s.loginPage)
s.mux.HandleFunc("GET /login/callback", s.loginCallback)
s.mux.HandleFunc("GET /logout", s.logoutPage)
acct, err := accts.CreateAccount(ctx, email)
if err != nil {
http.Error(w, fmt.Sprintf("failed to create account: %w", err), http.StatusInternalServerError)
return
}
// TODO: eliminate once no longer used.
s.mux.HandleFunc("POST /login", s.login)
http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther)
})
// TODO: when a user is created, we should make an account for them that is associated with their openid subject.
// - then this can go away
s.mux.HandleFunc("POST /accounts", s.createAccount)
mux.HandleFunc("POST /log-in", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
email := r.FormValue("email")
if email == "" {
http.Error(w, "no email provided", http.StatusBadRequest)
return
}
// TODO: test the new auth middleware
s.mux.Handle("GET /test-auth", s.authenticate(http.HandlerFunc(s.testAuthEndpoint)))
acct, err := accts.GetAccountByEmail(ctx, email)
if err != nil {
http.Error(w, fmt.Sprintf("failed to create account: %w", err), http.StatusInternalServerError)
return
}
// webpage content
http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther)
})
// non-html routes
scfs := http.FileServer(http.Dir(dir + "/scripts"))
mux.Handle("/scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
scfs := http.FileServer(http.Dir(contentDir + "/scripts"))
s.mux.Handle("GET /scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
if path.Ext(r.URL.Path) == ".gz" {
w.Header().Set("Content-Encoding", "gzip")
}
scfs.ServeHTTP(w, r)
})))
mux.Handle("/styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(dir+"/styles"))))
s.mux.Handle("GET /styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(contentDir+"/styles"))))
// html page routes
s.mux.HandleFunc("GET /", s.serveTemplates)
tmplr := templater.NewTemplater(
dir+"/templates",
func() template.FuncMap {
return template.FuncMap{
"buildSitePath": func(parts ...any) string {
strParts := make([]string, len(parts))
for i, p := range parts {
strParts[i] = fmt.Sprint(p)
}
// TODO: make "/site" dynamic somehow
return path.Join(append([]string{"/site"}, strParts...)...)
},
"splitPath": func(p string) []string {
if p == "" {
return nil
}
return strings.Split(strings.TrimSuffix(strings.TrimPrefix(p, "/"), "/"), "/")
},
"prettyPrintJSON": func(j json.RawMessage) string {
b, err := json.MarshalIndent(j, " ", "")
if err != nil {
return string(j)
}
return string(b)
},
"parseInt64": func(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
},
"addInt": func(a, b int) int {
return a + b
},
"subInt": func(a, b int) int {
return a - b
},
"multInt": func(a, b int) int {
return a * b
},
}
},
)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
name, pathParams := getPageTemplateNameForURL(r.URL)
b, err := tmplr.ExecutePage(
name,
"Request",
r,
// add services here
"RawEvents",
rawEvents.WithContext(ctx),
"URLCalc",
newURLCalculator(r.URL),
"PathParams",
pathParams,
"Accounts",
accts.WithContext(ctx),
"Etsy",
etsy.WithContext(ctx),
)
if err != nil {
// TODO: handle 'not found' as a 404?
fmt.Println("[ERROR]: failed to load or parse layout template:", err)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
w.Write(b)
})
return mux
return s
}
// TODO: clean this up...
// TODO: somehow tell what the path params are and pass them up.
// - then consider pushing this functionality into the template library.
//
// getPageTemplateNameForURL eliminate any trailing .html or /, and checks for any
// file with path parameters in the name, eg '{abc}.html.tmpl', prefering exact filename matches.
func getPageTemplateNameForURL(u *url.URL) (name string, params map[string]string) {
fp := strings.TrimPrefix(strings.TrimSuffix(strings.TrimSuffix(u.Path, ".html"), "/"), "/")
if fp == "" {
// "/" maps to "/index"
fp = "index"
// http.Handler implementation
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
}
// POST /accounts
func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
email := r.FormValue("email")
if email == "" {
http.Error(w, "no email provided", http.StatusBadRequest)
return
}
fpParts := strings.Split(fp, "/")
res := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(fpParts)
for _, combs := range res {
const pageBodiesPrefix = "internal/site/templates/page_bodies"
pattern := path.Join(pageBodiesPrefix, path.Join(combs...)) + ".html.tmpl"
matches, _ := filepath.Glob(pattern)
if len(matches) == 0 {
pattern := path.Join(pageBodiesPrefix, path.Join(combs...), "index") + ".html.tmpl"
matches, _ = filepath.Glob(pattern)
}
if len(matches) > 0 {
match := matches[0]
name = strings.TrimPrefix(strings.TrimSuffix(match, ".html.tmpl"), pageBodiesPrefix+"/")
patternParts := strings.Split(name, "/")
params = make(map[string]string)
for i, pp := range patternParts {
if strings.HasPrefix(pp, "{") && strings.HasSuffix(pp, "}") {
params[pp[1:len(pp)-1]] = fpParts[i]
}
}
return name, params
}
acct, err := s.accts.CreateAccount(ctx, email)
if err != nil {
http.Error(w, fmt.Sprintf("failed to create account: %v", err), http.StatusInternalServerError)
return
}
return fp, nil
}
func getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(filepathParts []string) [][]string {
switch len(filepathParts) {
case 0:
return nil
case 1:
return [][]string{
[]string{filepathParts[0]},
[]string{"{*}"},
}
default:
tailCombs := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(filepathParts[1:])
combs := make([][]string, 2*len(tailCombs))
for i, c := range tailCombs {
combs[i*2] = append([]string{filepathParts[0]}, c...)
combs[i*2+1] = append([]string{"{*}"}, c...)
}
return combs
}
}
type URLCalculator struct {
url *url.URL
}
func newURLCalculator(u *url.URL) URLCalculator {
cpy := *u
return URLCalculator{
url: &cpy,
}
}
func (c URLCalculator) SetQueryParam(k string, v any) string {
u := *c.url
q := u.Query()
q.Set(k, fmt.Sprint(v))
u.RawQuery = q.Encode()
return u.String()
//http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther)
http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acct.ID), http.StatusSeeOther)
}