From 891b63f2595b91c28942eae6bd7f718f4bf24996 Mon Sep 17 00:00:00 2001 From: lbenedar Date: Fri, 20 Mar 2026 18:07:41 +0300 Subject: [PATCH] ch16.3 --- cmd/api/context.go | 26 +++++++++++++++++++++++ cmd/api/errors.go | 7 ++++++ cmd/api/middleware.go | 48 ++++++++++++++++++++++++++++++++++++++++++ cmd/api/routes.go | 2 +- internal/data/users.go | 6 ++++++ 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 cmd/api/context.go diff --git a/cmd/api/context.go b/cmd/api/context.go new file mode 100644 index 0000000..8a4669a --- /dev/null +++ b/cmd/api/context.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "net/http" + + "gitea.local.lab/Lbenedar/greenlight/internal/data" +) + +type contextKey string + +const userContextKey = contextKey("user") + +func (app *application) contextSetUser(r *http.Request, user *data.User) *http.Request { + ctx := context.WithValue(r.Context(), userContextKey, user) + return r.WithContext(ctx) +} + +func (app *application) contextGetUser(r *http.Request) *data.User { + user, ok := r.Context().Value(userContextKey).(*data.User) + if !ok { + panic("missing user value in request context") + } + + return user +} diff --git a/cmd/api/errors.go b/cmd/api/errors.go index 93f6dd0..c403eeb 100644 --- a/cmd/api/errors.go +++ b/cmd/api/errors.go @@ -61,3 +61,10 @@ func (app *application) invalidCredentialsResponse(w http.ResponseWriter, r *htt message := "invalid authentication credentials" app.errorResponse(w, r, http.StatusUnauthorized, message) } + +func (app *application) invalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", "Bearer") + + message := "invalid or missing authentication token" + app.errorResponse(w, r, http.StatusUnauthorized, message) +} diff --git a/cmd/api/middleware.go b/cmd/api/middleware.go index a959ebb..9c4da55 100644 --- a/cmd/api/middleware.go +++ b/cmd/api/middleware.go @@ -1,12 +1,16 @@ package main import ( + "errors" "fmt" "net" "net/http" + "strings" "sync" "time" + "gitea.local.lab/Lbenedar/greenlight/internal/data" + "gitea.local.lab/Lbenedar/greenlight/internal/validator" "golang.org/x/time/rate" ) @@ -75,3 +79,47 @@ func (app *application) rateLimit(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +func (app *application) authenticate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Vary", "Authorization") + + authorizationHeader := r.Header.Get("Authorization") + + if authorizationHeader == "" { + r = app.contextSetUser(r, data.AnonymousUser) + next.ServeHTTP(w, r) + return + } + + headerParts := strings.Split(authorizationHeader, " ") + if len(headerParts) != 2 || headerParts[0] != "Bearer" { + app.invalidCredentialsResponse(w, r) + return + } + + token := headerParts[1] + + v := validator.New() + + if data.ValidateTokenPlaintext(v, token); !v.Valid() { + app.invalidCredentialsResponse(w, r) + return + } + + user, err := app.models.Users.GetForToken(data.ScopeAuthentication, token) + if err != nil { + switch { + case errors.Is(err, data.ErrRecordNotFound): + app.invalidAuthenticationTokenResponse(w, r) + default: + app.serverErrorResponse(w, r, err) + } + return + } + + r = app.contextSetUser(r, user) + + next.ServeHTTP(w, r) + }) +} diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 4c54f5b..4305de0 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -25,5 +25,5 @@ func (app *application) routes() http.Handler { router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler) - return app.recoverPanic(app.rateLimit(router)) + return app.recoverPanic(app.rateLimit(app.authenticate(router))) } diff --git a/internal/data/users.go b/internal/data/users.go index eea41be..b442415 100644 --- a/internal/data/users.go +++ b/internal/data/users.go @@ -15,6 +15,8 @@ var ( ErrDuplicateEmail = errors.New("duplicate email") ) +var AnonymousUser = &User{} + type User struct { ID int64 `json:"id"` CreatedAt time.Time `json:"created_at"` @@ -25,6 +27,10 @@ type User struct { Version int `json:"-"` } +func (u *User) IsAnonymous() bool { + return u == AnonymousUser +} + type password struct { plaintext *string hash []byte