diff --git a/README.md b/README.md index 8919094..7a5e5cf 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ ## Nice to haves - [ ] Drop in a good logger +- [ ] log all errors caught by the http server ## Constraints diff --git a/internal/consts/errors.go b/internal/consts/errors.go index 5e2dea1..7b70776 100644 --- a/internal/consts/errors.go +++ b/internal/consts/errors.go @@ -3,6 +3,6 @@ package consts import "errors" var ( - // TODO: use throught the database methods and the site template parsing to treat it as a 404 (not a redirect) ErrNotFound = errors.New("not found") + ErrConflict = errors.New("conflict") ) diff --git a/internal/domains/accounts/accounts.go b/internal/domains/accounts/accounts.go index fe743ea..9438771 100644 --- a/internal/domains/accounts/accounts.go +++ b/internal/domains/accounts/accounts.go @@ -25,6 +25,10 @@ type ( ID int64 Email string } + + OAuthUser struct { + UserID string + } ) func NewStore(db *pgxpool.Pool) *Store { @@ -52,6 +56,7 @@ func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Accou @user_id, @email ) + ON CONFLICT DO NOTHING RETURNING account_id `, @@ -67,7 +72,7 @@ func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Accou acctID, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - return Account{}, consts.ErrNotFound + return Account{}, fmt.Errorf("account already exists: %w", consts.ErrConflict) } return Account{}, fmt.Errorf("failed to scan row: %w", err) } @@ -171,6 +176,55 @@ func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, }, nil } +func (db *Store) GetUserAndAccountByAccessToken(ctx context.Context, accessToken string) (OAuthUser, *Account, error) { + rows, err := db.db.Query( + ctx, + ` + SELECT + u.user_id, + a.account_id, + a.email + FROM oauth_users u + LEFT JOIN oauth_tokens t + ON u.user_id = id_token_subject + LEFT JOIN accounts a + USING (user_id) + WHERE access_token = @access_token + `, + pgx.NamedArgs{ + "access_token": accessToken, + }, + ) + if err != nil { + return OAuthUser{}, nil, fmt.Errorf("failed to perform query: %w", err) + } + + type Row struct { + User_ID string + Account_ID *int64 + Email *string + } + + r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return OAuthUser{}, nil, consts.ErrNotFound + } + return OAuthUser{}, nil, fmt.Errorf("failed to scan row: %w", err) + } + + var acct *Account + if r.Account_ID != nil { + acct = &Account{ + UserID: r.User_ID, + ID: *r.Account_ID, + Email: *r.Email, + } + } + + return OAuthUser{UserID: r.User_ID}, acct, nil +} + func (db *StoreWithContext) CreateAccount(userID, email string) (Account, error) { return db.db.CreateAccount(db.ctx, userID, email) } diff --git a/internal/domains/authentication/auth.go b/internal/domains/authentication/auth.go index 146f411..ffa3157 100644 --- a/internal/domains/authentication/auth.go +++ b/internal/domains/authentication/auth.go @@ -100,22 +100,16 @@ func (a *Authenticator) Exchange(ctx context.Context, state, code string) (acces return "", time.Time{}, fmt.Errorf("failed to exchange an authorization code for a token: %w", err) } - fmt.Println("TOKEN:", token) - idToken, err := a.VerifyIDToken(ctx, token) if err != nil { return "", time.Time{}, fmt.Errorf("failed to verify ID Token: %w", err) } - fmt.Println("ID TOKEN:", idToken) - var claims map[string]any if err := idToken.Claims(&claims); err != nil { return "", time.Time{}, fmt.Errorf("Failed to obtain id token claims: %w", err) } - fmt.Println("CUSTOM CLAIMS / PROFILE:", claims) - claimsJSON, _ := json.Marshal(claims) // store the token, and the potentially new user diff --git a/internal/site/accounts.go b/internal/site/accounts.go index 0edd443..c3bbddb 100644 --- a/internal/site/accounts.go +++ b/internal/site/accounts.go @@ -1,9 +1,11 @@ package site import ( + "errors" "fmt" "net/http" + "ruben/inventory2/internal/consts" "ruben/inventory2/internal/site/response" ) @@ -15,10 +17,14 @@ func (s *Server) createAccount(r *http.Request) (response.Response, error) { return nil, response.BadRequest().Msg("no email provided") } - userID := getAccessTokenClaims(ctx).Subject + userID := getIdentity(ctx).User.UserID acct, err := s.accts.CreateAccount(ctx, userID, email) if err != nil { + if errors.Is(err, consts.ErrConflict) { + return nil, response.Conflict(). + Msg("user already has an account") + } return nil, response.Errorf("failed to create account: %w", err) } diff --git a/internal/site/auth.go b/internal/site/auth.go index 7d2c27c..5744a24 100644 --- a/internal/site/auth.go +++ b/internal/site/auth.go @@ -5,24 +5,82 @@ import ( "errors" "fmt" "net/http" + "strconv" + "strings" "time" "ruben/inventory2/internal/consts" + "ruben/inventory2/internal/domains/accounts" "ruben/inventory2/internal/domains/authentication" "ruben/inventory2/internal/site/response" ) // just keep this around long enough for testing auth middleware.. func (s *Server) testAuthEndpoint(r *http.Request) (response.Response, error) { - fmt.Println("AUTH TEST SUCCESS:", getAccessTokenClaims(r.Context())) + fmt.Println("AUTH TEST SUCCESS:", getIdentity(r.Context())) return response.TemporaryRedirect("/"), nil } -type customClaimsKey struct{} +type identity struct { + AccessToken string + Claims authentication.AccessTokenClaims + User accounts.OAuthUser + Account *accounts.Account +} + +func (s *Server) addIdentity(fn response.HandlerFunc) response.HandlerFunc { + return func(r *http.Request) (response.Response, error) { + r, err := s.addIdentityToRequest(r) + if err != nil { + return nil, err + } + + return fn(r) + } +} + +func (s *Server) addIdentityToRequest(r *http.Request) (*http.Request, error) { + ck, err := r.Cookie("access_token") + if err != nil { + return r, nil + } + + ctx := r.Context() + + accessToken := ck.Value + + claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return r, nil + } + + return r, response.Errorf("failed to load authentication details: %w", err) + } + + if expiration.Before(time.Now()) { + return r, nil + } + + user, acct, err := s.accts.GetUserAndAccountByAccessToken(ctx, accessToken) + if err != nil { + return r, response.Errorf("failed to load user and account defails: %w", err) + } + + return r.WithContext(setIdentity(ctx, identity{ + AccessToken: accessToken, + Claims: claims, + User: user, + Account: acct, + })), nil +} + +// TODO: after getting auth, consider making http handler functions take 'claims', etc, as function arguments +// - then consider doing the same with authorizationAssertions // auth middleware to verify access_token cookie and set custom claims in the request context -func (s *Server) authenticate(f response.HandlerFunc) response.HandlerFunc { +func (s *Server) authenticateAndAddIdentity(f response.HandlerFunc, assertions ...authorizationAssertions) response.HandlerFunc { return func(r *http.Request) (response.Response, error) { ck, err := r.Cookie("access_token") if err != nil { @@ -31,7 +89,9 @@ func (s *Server) authenticate(f response.HandlerFunc) response.HandlerFunc { ctx := r.Context() - claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, ck.Value) + accessToken := ck.Value + + claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken) if err != nil { if errors.Is(err, consts.ErrNotFound) { return response.TemporaryRedirect("/"), nil @@ -44,7 +104,32 @@ func (s *Server) authenticate(f response.HandlerFunc) response.HandlerFunc { return response.TemporaryRedirect("/").Cookie(getExpiredCookie("access_token")), nil } - return f(r.WithContext(setAccessTokenClaims(ctx, claims))) + user, acct, err := s.accts.GetUserAndAccountByAccessToken(ctx, accessToken) + if err != nil { + return nil, response.Errorf("failed to authorize: %w", err) + } + + for _, as := range assertions { + if res, err := as(r); res != nil || err != nil { + return res, err + } + } + + return f(r.WithContext(setIdentity(ctx, identity{ + AccessToken: accessToken, + Claims: claims, + User: user, + Account: acct, + }))) + } +} + +type authorizationAssertions = response.HandlerFunc + +// TODO: test this! +func authorizeByMatchingAccountID_tmp(acctIDPathPosition int) authorizationAssertions { + return func(r *http.Request) (response.Response, error) { + return nil, authorizeByMatchingAccountID(r, acctIDPathPosition) } } @@ -67,13 +152,37 @@ func (s *Server) getAccessTokenClaims(r *http.Request) (authentication.AccessTok return claims, true } -// stores custom claims in request context -func setAccessTokenClaims(ctx context.Context, claims authentication.AccessTokenClaims) context.Context { - return context.WithValue(ctx, customClaimsKey{}, claims) +type identityKey struct{} + +// stores identity in request context +func setIdentity(ctx context.Context, id identity) context.Context { + return context.WithValue(ctx, identityKey{}, id) } -// get custom claims from request context -func getAccessTokenClaims(ctx context.Context) authentication.AccessTokenClaims { - c, _ := ctx.Value(customClaimsKey{}).(authentication.AccessTokenClaims) - return c +// get identity from request context +func getIdentity(ctx context.Context) identity { + id, _ := ctx.Value(identityKey{}).(identity) + return id +} + +func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error { + pathParts := strings.Split(strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), "/"), "/") + if len(pathParts) < acctIDPathPosition { + return fmt.Errorf("authorization failed due to unexpected path: %s", r.URL.Path) + } + + part := pathParts[acctIDPathPosition-1] + acctID, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return response.NotFound(). + Msgf("account does not exist: %s", part) + } + + id := getIdentity(r.Context()) + if id.Account == nil || id.Account.ID != acctID { + return response.Unauthorized(). + Msgf("user does not have access to account %d", acctID) + } + + return nil } diff --git a/internal/site/response/body.go b/internal/site/response/body.go new file mode 100644 index 0000000..2909578 --- /dev/null +++ b/internal/site/response/body.go @@ -0,0 +1,84 @@ +package response + +import ( + "fmt" + "io" + "net/http" + "ruben/inventory2/internal/site/redirect" +) + +type ( + bodyRes struct { + body io.ReadCloser + res Response + } +) + +var _ Response = bodyRes{} + +func Body(body io.ReadCloser) Response { + return bodyRes{ + body: body, + } +} + +func (b bodyRes) String() string { + if b.res != nil { + return fmt.Sprintf(`{"body": %q, "nested": %s}`, b.body, b.res) + } + return fmt.Sprintf(`{"body": %q}`, b.body) +} + +func (b bodyRes) wrap(res Response) Response { + b.res = res + return b +} + +func (b bodyRes) Status(code int) Response { + return Status(code).wrap(b) +} + +func (b bodyRes) Redirect(code redirect.Code, to string) Response { + return Redirect(code, to).wrap(b) +} + +func (b bodyRes) JSON(body any) Response { + return JSON(body).wrap(b) +} + +func (b bodyRes) Body(body io.ReadCloser) Response { + b.body = body + return b +} + +func (b bodyRes) Cookie(ck http.Cookie) Response { + return Cookie(ck).wrap(b) +} + +func (b bodyRes) getStatus() (int, bool) { + if b.res == nil { + return 0, false + } + + return b.res.getStatus() +} + +func (b bodyRes) getRedirect() (code redirect.Code, to string, ok bool) { + if b.res == nil { + return 0, "", false + } + + return b.res.getRedirect() +} + +func (b bodyRes) getBody() (body io.ReadCloser, ok bool, err error) { + return b.body, true, nil +} + +func (b bodyRes) getCookies() []http.Cookie { + if b.res != nil { + return b.res.getCookies() + } + + return nil +} diff --git a/internal/site/response/cookie.go b/internal/site/response/cookie.go index 7b6316e..2c9acab 100644 --- a/internal/site/response/cookie.go +++ b/internal/site/response/cookie.go @@ -24,9 +24,9 @@ func Cookie(c http.Cookie) Response { func (c cookieRes) String() string { if c.res != nil { - return fmt.Sprintf(`{"cookie": %q, "nested": %s}`, c.cookie, c.res) + return fmt.Sprintf(`{"cookie": %q, "nested": %s}`, &c.cookie, c.res) } - return fmt.Sprintf(`{"cookie": %q}`, c.cookie) + return fmt.Sprintf(`{"cookie": %q}`, &c.cookie) } func (c cookieRes) wrap(res Response) Response { @@ -46,6 +46,10 @@ func (c cookieRes) JSON(body any) Response { return JSON(body).wrap(c) } +func (c cookieRes) Body(body io.ReadCloser) Response { + return Body(body).wrap(c) +} + func (c cookieRes) Cookie(ck http.Cookie) Response { return Cookie(ck).wrap(c) } diff --git a/internal/site/response/error.go b/internal/site/response/error.go index d412522..d03d950 100644 --- a/internal/site/response/error.go +++ b/internal/site/response/error.go @@ -48,6 +48,12 @@ func Forbidden() ErrorResponse { } } +func Conflict() ErrorResponse { + return ErrorResponse{ + status: http.StatusConflict, + } +} + // builder pattern implementation func (e ErrorResponse) Msg(msg string) ErrorResponse { @@ -55,6 +61,11 @@ func (e ErrorResponse) Msg(msg string) ErrorResponse { return e } +func (e ErrorResponse) Msgf(format string, args ...any) ErrorResponse { + e.msg = fmt.Sprintf(format, args...) + return e +} + func (e ErrorResponse) Status(status int) ErrorResponse { e.status = status return e @@ -92,33 +103,33 @@ func (e ErrorResponse) Unwrap() error { // nested response value resolution -func (e ErrorResponse) getStatus() (int, bool) { +func (e ErrorResponse) GetStatus() (int, bool) { if e.status != 0 { return e.status, true } - ce, ok := getError(e.err) + ce, ok := GetError(e.err) if ok { - return ce.getStatus() + return ce.GetStatus() } return 0, false } -func (e ErrorResponse) getMsg() (string, bool) { +func (e ErrorResponse) GetMsg() (string, bool) { if e.msg != "" { return e.msg, true } - ce, ok := getError(e.err) + ce, ok := GetError(e.err) if ok { - return ce.getMsg() + return ce.GetMsg() } return "", false } -func getError(err error) (e ErrorResponse, ok bool) { +func GetError(err error) (e ErrorResponse, ok bool) { ok = errors.As(err, &e) return e, ok } diff --git a/internal/site/response/json.go b/internal/site/response/json.go index 8116d95..c3f1856 100644 --- a/internal/site/response/json.go +++ b/internal/site/response/json.go @@ -49,6 +49,10 @@ func (j jsonRes) JSON(body any) Response { return j } +func (j jsonRes) Body(body io.ReadCloser) Response { + return Body(body).wrap(j) +} + func (j jsonRes) Cookie(ck http.Cookie) Response { return Cookie(ck).wrap(j) } diff --git a/internal/site/response/redirect.go b/internal/site/response/redirect.go index 0dab1a1..17cd5b1 100644 --- a/internal/site/response/redirect.go +++ b/internal/site/response/redirect.go @@ -87,6 +87,10 @@ func (r redirectRes) JSON(body any) Response { return JSON(body).wrap(r) } +func (r redirectRes) Body(body io.ReadCloser) Response { + return Body(body).wrap(r) +} + func (r redirectRes) Cookie(ck http.Cookie) Response { return Cookie(ck).wrap(r) } diff --git a/internal/site/response/response.go b/internal/site/response/response.go index ee03842..94394e6 100644 --- a/internal/site/response/response.go +++ b/internal/site/response/response.go @@ -11,6 +11,7 @@ type ( Response interface { Status(int) Response Redirect(code redirect.Code, to string) Response + Body(io.ReadCloser) Response JSON(any) Response Cookie(http.Cookie) Response diff --git a/internal/site/response/status.go b/internal/site/response/status.go index 6b12e74..61a0929 100644 --- a/internal/site/response/status.go +++ b/internal/site/response/status.go @@ -48,6 +48,10 @@ func (s statusRes) JSON(body any) Response { return JSON(body).wrap(s) } +func (s statusRes) Body(body io.ReadCloser) Response { + return Body(body).wrap(s) +} + func (s statusRes) Cookie(ck http.Cookie) Response { return Cookie(ck).wrap(s) } diff --git a/internal/site/response/write.go b/internal/site/response/write.go index fc51fa2..43cb57c 100644 --- a/internal/site/response/write.go +++ b/internal/site/response/write.go @@ -46,8 +46,8 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) { func WriteError(w http.ResponseWriter, err error) { var status int - if e, ok := getError(err); ok { - if status, ok = e.getStatus(); !ok { + if e, ok := GetError(err); ok { + if status, ok = e.GetStatus(); !ok { status = http.StatusInternalServerError } } diff --git a/internal/site/site.go b/internal/site/site.go index 01a7554..ef37fe6 100644 --- a/internal/site/site.go +++ b/internal/site/site.go @@ -19,7 +19,7 @@ import ( ) type Server struct { - mux *http.ServeMux + http.Handler contentDir string templater *templater.Templater rawEvents *raw_events.Store @@ -35,8 +35,9 @@ func NewServer( etsy *etsy_platform.Platform, auth *authentication.Authenticator, ) *Server { + mux := http.NewServeMux() + s := &Server{ - mux: http.NewServeMux(), contentDir: contentDir, templater: templater.NewTemplater( contentDir+"/templates", @@ -91,36 +92,32 @@ func NewServer( // api routes - s.mux.HandleFunc("GET /login", response.Handler(s.loginPage)) - s.mux.HandleFunc("GET /login/callback", response.Handler(s.loginCallback)) - s.mux.HandleFunc("GET /logout", response.Handler(s.logoutPage)) - s.mux.Handle("POST /accounts", response.Handler(s.authenticate(s.createAccount))) + mux.Handle("GET /login", response.Handler(s.loginPage)) + mux.Handle("GET /login/callback", response.Handler(s.loginCallback)) + mux.Handle("GET /logout", response.Handler(s.logoutPage)) + mux.Handle("POST /accounts", response.Handler(s.authenticateAndAddIdentity(s.createAccount))) // TODO: eliminate once no longer used. - s.mux.HandleFunc("POST /login", response.Handler(s.login)) + mux.HandleFunc("POST /login", response.Handler(s.login)) // TODO: get rid of this, once we're confident this isn't needed... - s.mux.Handle("GET /test-auth", response.Handler(s.authenticate(s.testAuthEndpoint))) + mux.Handle("GET /test-auth", response.Handler(s.authenticateAndAddIdentity(s.testAuthEndpoint))) // webpage content scfs := http.FileServer(http.Dir(contentDir + "/scripts")) - s.mux.Handle("GET /scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + 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) }))) - s.mux.Handle("GET /styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(contentDir+"/styles")))) + mux.Handle("GET /styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(contentDir+"/styles")))) - // TODO: put auth on individual templates, somehow... - s.mux.HandleFunc("GET /", s.serveTemplates) + mux.HandleFunc("GET /", response.Handler(s.addIdentity(s.serveTemplates))) + + s.Handler = mux return s } - -// http.Handler implementation -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - s.mux.ServeHTTP(w, r) -} diff --git a/internal/site/templates.go b/internal/site/templates.go index e5b96ad..284748b 100644 --- a/internal/site/templates.go +++ b/internal/site/templates.go @@ -1,38 +1,28 @@ package site import ( + "bytes" "errors" "fmt" + "io" "net/http" "net/url" "os" "path" "path/filepath" + "ruben/inventory2/internal/site/response" "strings" - - "ruben/inventory2/internal/domains/accounts" ) // GET / -func (s *Server) serveTemplates(w http.ResponseWriter, r *http.Request) { +func (s *Server) serveTemplates(r *http.Request) (response.Response, error) { ctx := r.Context() name, pathParams := getPageTemplateNameForURL(r.URL) - var ( - acct accounts.Account - userID string - ) - claims, ok := s.getAccessTokenClaims(r) - if ok { - userID = claims.Subject - acct, _ = s.accts.GetAccountByUserID(ctx, userID) - } - - b, err := s.templater.ExecutePage( - name, + templateArgs := []any{ "Request", r, - // add services here + // add services and data here "RawEvents", s.rawEvents.WithContext(ctx), "URLCalc", @@ -44,37 +34,26 @@ func (s *Server) serveTemplates(w http.ResponseWriter, r *http.Request) { "Etsy", s.etsy.WithContext(ctx), - // claims - "Claims", - claims, - "UserID", - userID, - "Account", - acct, - ) - if err != nil { - // TODO: handle 401 - - // TODO: handle 403 - - // TODO: handle 404 - if isFileNotFoundError(err) { - } - - // 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 + // TODO: apply auth to all templates needed! + // auth tooling + /* + AccessToken string + Claims authentication.AccessTokenClaims + User accounts.OAuthUser + Account *accounts.Account + */ + "Identity", + getIdentity(r.Context()), + "Auth", + newTemplateAuthenticator(r), } - w.Write(b) -} + b, err := s.templater.ExecutePage(name, templateArgs...) + if err != nil { + return s.handleTemplateError(err, templateArgs...) + } -func isFileNotFoundError(err error) bool { - var pe *os.PathError - isPathErr := errors.As(err, &pe) - - return isPathErr && pe.Err != nil && pe.Err.Error() == "no such file or directory" + return response.Body(io.NopCloser(bytes.NewBuffer(b))), nil } // TODO: clean this up... @@ -142,6 +121,63 @@ func getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(file } } +func (s *Server) handleTemplateError(err error, templateArgs ...any) (response.Response, error) { + code := getHTTPStatusCode(err) + + if code == http.StatusNotFound || + code == http.StatusForbidden || + code == http.StatusUnauthorized || + isFileNotFoundError(err) { + + b, err := s.templater.ExecutePage("not-found", templateArgs...) + if err != nil { + fmt.Println("failed to render not found page:", err) + return nil, response.NotFound(). + Wrap(err). + Msg("resource not found") + } + + return response.Body(io.NopCloser(bytes.NewBuffer(b))), nil + } + + if code == http.StatusConflict { + b, err := s.templater.ExecutePage("conflict", templateArgs...) + if err != nil { + fmt.Println("failed to render conflict page:", err) + return nil, response.Conflict(). + Wrap(err). + Msg("conflict") + } + + return response.Body(io.NopCloser(bytes.NewBuffer(b))), nil + } + + return nil, fmt.Errorf("failed to render page: %w", err) +} + +func isFileNotFoundError(err error) bool { + var pe *os.PathError + isPathErr := errors.As(err, &pe) + + return isPathErr && pe.Err != nil && pe.Err.Error() == "no such file or directory" +} + +func getHTTPStatusCode(err error) int { + rerr, ok := response.GetError(err) + if !ok { + return http.StatusInternalServerError + } + + code, ok := rerr.GetStatus() + if !ok { + return http.StatusInternalServerError + } + + return code +} + +// template tooling + type URLCalculator struct { url *url.URL } @@ -161,3 +197,22 @@ func (c URLCalculator) SetQueryParam(k string, v any) string { return u.String() } + +// template authenticator + +type templateAuthenticator struct { + req *http.Request +} + +func newTemplateAuthenticator(req *http.Request) *templateAuthenticator { + return &templateAuthenticator{ + req: req, + } +} + +// templateAuthorizationFunc these shouild always return an empty string +type templateAuthorizationFunc = func() (string, error) + +func (a *templateAuthenticator) ByMatchingAccountID(acctIDPathPosition int) (string, error) { + return "", authorizeByMatchingAccountID(a.req, acctIDPathPosition) +} diff --git a/internal/site/templates/component_bodies/nav_bar.html.tmpl b/internal/site/templates/component_bodies/nav_bar.html.tmpl index 4afcfaf..ed1f481 100644 --- a/internal/site/templates/component_bodies/nav_bar.html.tmpl +++ b/internal/site/templates/component_bodies/nav_bar.html.tmpl @@ -2,7 +2,19 @@ {{/* "dot" . */}} - diff --git a/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl b/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl index 9197dd6..2314644 100644 --- a/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl +++ b/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl @@ -1,16 +1,20 @@ +{{- .Auth.ByMatchingAccountID 2 }} + {{- define "title" }} Inventory++ Account {{ end }} -