diff --git a/api/api.go b/api/api.go index 0a885f9..413901a 100644 --- a/api/api.go +++ b/api/api.go @@ -12,6 +12,8 @@ import ( "log/slog" + "github.com/go-chi/chi/v5" + "github.com/go-chi/jwtauth/v5" "github.com/perebaj/contractus" ) @@ -93,3 +95,21 @@ func convert(content, email string) (t []contractus.Transaction, err error) { } return t, nil } + +// Router gather all the routes of the API. +func Router(a Auth, storage transactionStorage) http.Handler { + r := chi.NewRouter() + r.Group(func(r chi.Router) { + r.Use(jwtauth.Verifier(a.JWTAuth())) + r.Use(jwtauth.Authenticator) + + RegisterTransactionsHandler(r, storage) + }) + + r.Group(func(r chi.Router) { + // Public routes + RegisterAuthHandler(r, a) + RegisterSwaggerHandler(r) + }) + return r +} diff --git a/api/docs/api.md b/api/docs/api.md index 6e988f6..d826ffb 100644 --- a/api/docs/api.md +++ b/api/docs/api.md @@ -92,6 +92,7 @@ Response: { "transactions": [ { + "email": "jj@example.com", "type": "string", "date": "2023-09-27T17:34:41.455Z", "product_description": "string", diff --git a/api/docs/api.yml b/api/docs/api.yml index 9669578..05e0db8 100644 --- a/api/docs/api.yml +++ b/api/docs/api.yml @@ -179,6 +179,9 @@ components: Transaction: type: object properties: + email: + type: string + description: User email type: type: string description: Transaction type diff --git a/api/transactionhandler.go b/api/transactionhandler.go index 4001f26..87d534e 100644 --- a/api/transactionhandler.go +++ b/api/transactionhandler.go @@ -65,7 +65,7 @@ func RegisterSwaggerHandler(r chi.Router) { func (s transactionHandler) producerBalance(w http.ResponseWriter, r *http.Request) { email, err := emailFromRequest(r) if err != nil { - sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"}) + sendErr(w, http.StatusUnauthorized, Error{"user unauthorized", "user unauthorized"}) return } @@ -92,7 +92,7 @@ func (s transactionHandler) producerBalance(w http.ResponseWriter, r *http.Reque func (s transactionHandler) affiliateBalance(w http.ResponseWriter, r *http.Request) { email, err := emailFromRequest(r) if err != nil { - sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"}) + sendErr(w, http.StatusUnauthorized, Error{"unauthorized_request", "unauthoriz"}) return } query := r.URL.Query() @@ -118,7 +118,7 @@ func (s transactionHandler) affiliateBalance(w http.ResponseWriter, r *http.Requ func (s transactionHandler) upload(w http.ResponseWriter, r *http.Request) { email, err := emailFromRequest(r) if err != nil { - sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"}) + sendErr(w, http.StatusUnauthorized, Error{"unauthorized_request", "unauthorized request"}) return } @@ -148,7 +148,7 @@ func (s transactionHandler) upload(w http.ResponseWriter, r *http.Request) { func (s transactionHandler) transactions(w http.ResponseWriter, r *http.Request) { email, err := emailFromRequest(r) if err != nil { - sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"}) + sendErr(w, http.StatusUnauthorized, Error{"unauthorized_request", "unauthoriz"}) return } @@ -222,5 +222,10 @@ func emailFromRequest(r *http.Request) (string, error) { if err != nil { return "", fmt.Errorf("failed to get claims from context: %v", err) } + + if claims["email"] == "" { + return "", fmt.Errorf("failed to get email from claims: %v", err) + } + return claims["email"].(string), nil } diff --git a/api/transactionhandler_test.go b/api/transactionhandler_test.go index e1a8fbf..51c2f98 100644 --- a/api/transactionhandler_test.go +++ b/api/transactionhandler_test.go @@ -47,16 +47,9 @@ func TestTransactionHandlerUpload(t *testing.T) { t.Fatal(err) } m := &mockTransactionStorage{} - r := chi.NewRouter() - - tokenAuth := jwtauth.New("HS256", []byte("secret"), nil) - _, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) - r.Group(func(r chi.Router) { - r.Use(jwtauth.Verifier(tokenAuth)) - r.Use(jwtauth.Authenticator) - RegisterTransactionsHandler(r, m) - }) + token, authConfig := authTest(t) + r := Router(authConfig, m) req := httptest.NewRequest(http.MethodPost, "/upload", &b) req.Header.Set("Content-Type", w.FormDataContentType()) @@ -73,17 +66,24 @@ func TestTransactionHandlerUpload(t *testing.T) { } } -func TestTransactionHandlerBalanceProducer(t *testing.T) { - tokenAuth := jwtauth.New("HS256", []byte("secret"), nil) - _, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) +func TestTransactionHandlerUpload_Unauthorized(t *testing.T) { + _, authConfig := authTest(t) + m := &mockTransactionStorage{} + r := Router(authConfig, m) + + req := httptest.NewRequest(http.MethodPost, "/upload", nil) + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + + if resp.Code != http.StatusUnauthorized { + t.Fatalf("expected status code %d, got %d", http.StatusUnauthorized, resp.Code) + } +} +func TestTransactionHandlerBalanceProducer(t *testing.T) { + token, authConfig := authTest(t) m := &mockTransactionStorage{} - r := chi.NewRouter() - r.Group(func(r chi.Router) { - r.Use(jwtauth.Verifier(tokenAuth)) - r.Use(jwtauth.Authenticator) - RegisterTransactionsHandler(r, m) - }) + r := Router(authConfig, m) req := httptest.NewRequest(http.MethodGet, "/balance/producer?name=JOSE%20CARLOS", nil) req.AddCookie(&http.Cookie{ @@ -100,17 +100,24 @@ func TestTransactionHandlerBalanceProducer(t *testing.T) { } } -func TestTransactionHandlerBalanceAffiliate(t *testing.T) { +func TestTransactionHandlerBalanceProducer_Unauthorized(t *testing.T) { + _, authConfig := authTest(t) m := &mockTransactionStorage{} - r := chi.NewRouter() - tokenAuth := jwtauth.New("HS256", []byte("secret"), nil) - _, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) + r := Router(authConfig, m) - r.Group(func(r chi.Router) { - r.Use(jwtauth.Verifier(tokenAuth)) - r.Use(jwtauth.Authenticator) - RegisterTransactionsHandler(r, m) - }) + req := httptest.NewRequest(http.MethodGet, "/balance/producer?name=JOSE%20CARLOS", nil) + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + + if resp.Code != http.StatusUnauthorized { + t.Fatalf("expected status code %d, got %d", http.StatusUnauthorized, resp.Code) + } +} + +func TestTransactionHandlerBalanceAffiliate(t *testing.T) { + token, authConfig := authTest(t) + m := &mockTransactionStorage{} + r := Router(authConfig, m) req := httptest.NewRequest(http.MethodGet, "/balance/affiliate?name=JOSE%20CARLOS", nil) req.AddCookie(&http.Cookie{ @@ -128,16 +135,9 @@ func TestTransactionHandlerBalanceAffiliate(t *testing.T) { } func TestTransactionHandlerTransactions(t *testing.T) { + token, authConfig := authTest(t) m := &mockTransactionStorage{} - r := chi.NewRouter() - tokenAuth := jwtauth.New("HS256", []byte("secret"), nil) - _, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) - - r.Group(func(r chi.Router) { - r.Use(jwtauth.Verifier(tokenAuth)) - r.Use(jwtauth.Authenticator) - RegisterTransactionsHandler(r, m) - }) + r := Router(authConfig, m) req := httptest.NewRequest(http.MethodGet, "/transactions", nil) req.AddCookie(&http.Cookie{ @@ -154,6 +154,20 @@ func TestTransactionHandlerTransactions(t *testing.T) { } } +func TestTransactionHandlerTransactions_Unauthorized(t *testing.T) { + _, authConfig := authTest(t) + m := &mockTransactionStorage{} + r := Router(authConfig, m) + + req := httptest.NewRequest(http.MethodGet, "/transactions", nil) + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + + if resp.Code != http.StatusUnauthorized { + t.Fatalf("expected status code %d, got %d", http.StatusUnauthorized, resp.Code) + } +} + func TestSwaggerHandler(t *testing.T) { r := chi.NewRouter() RegisterSwaggerHandler(r) @@ -175,3 +189,17 @@ func TestSwaggerHandler(t *testing.T) { t.Fatalf("expected status code %d, got %d", http.StatusOK, resp.Code) } } + +// authTest is a helper function to generate a JWT token and Auth struct for testing. +func authTest(t *testing.T) (token string, authConfig Auth) { + t.Helper() + secretKey := "secret" //Fake secret Key + tokenAuth := jwtauth.New("HS256", []byte(secretKey), nil) + _, token, _ = tokenAuth.Encode(map[string]interface{}{"email": "jj@example.com"}) + + authConfig = Auth{ + JWTSecretKey: secretKey, + } + + return token, authConfig +} diff --git a/cmd/contractus/main.go b/cmd/contractus/main.go index 1f7fb42..d8af3a2 100644 --- a/cmd/contractus/main.go +++ b/cmd/contractus/main.go @@ -10,8 +10,6 @@ import ( "log/slog" - "github.com/go-chi/chi/v5" - "github.com/go-chi/jwtauth/v5" "github.com/perebaj/contractus/api" "github.com/perebaj/contractus/postgres" "golang.org/x/oauth2" @@ -77,20 +75,7 @@ func main() { cfg.Auth.GoogleOAuthConfig = &googleOAuthConfig - r := chi.NewRouter() - r.Group(func(r chi.Router) { - // Authenticated routes - r.Use(jwtauth.Verifier(cfg.Auth.JWTAuth())) - r.Use(jwtauth.Authenticator) - - api.RegisterTransactionsHandler(r, storage) - }) - - r.Group(func(r chi.Router) { - // Public routes - api.RegisterAuthHandler(r, cfg.Auth) - api.RegisterSwaggerHandler(r) - }) + r := api.Router(cfg.Auth, storage) svc := &http.Server{ Addr: ":" + cfg.PORT,