Skip to content

Commit

Permalink
Merge pull request #20 from perebaj/feat_authorization_email
Browse files Browse the repository at this point in the history
✨ feat: authorization email
  • Loading branch information
perebaj authored Sep 29, 2023
2 parents 3cf41e6 + 0da6008 commit 80a7b9c
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 45 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ Jamming around orders with API endpoints 🎸

## Environment Variables

**Required** environment variables
If you want to run this project locally you must set up these environment variables:

CONTRACTUS_POSTGRES_URL
CONTRACTUS_GOOGLE_CLIENT_ID
CONTRACTUS_GOOGLE_CLIENT_SECRET
CONTRACTUS_DOMAIN
CONTRACTUS_JWT_SECRET_KEY

This could be made in the [docker-compose.yml](./docker-compose.yaml)

## Get Started

To start the service locally, you can type `make dev/start` and after that you can use the docker container IP to play around the routes, `make ip`
Expand Down Expand Up @@ -64,7 +66,13 @@ Local:
## API documentation
[API Docs](api/docs/)

## Attention points
- For a while, the integration-tests just ran locally not in CI, this increased the time to ship code 🚀
- We don't have a way to paginate transactions. 😔
- To publish images and new releases, for now, the only way is using the command line, isn't automate by CI yet 😔
## It's also good to know
- You can log in to the service using your Google account. No bureaucracy to 🎸
- Structured logs all the way 🥸
- Deployed to the open sea through Heroku 🌊 (Check the repository details to access the link)
---
- For a while, the integration-tests just ran locally not in CI 😔, this increased the time to 🚀 code;
- We don't have a way to paginate transactions yet; 😔
- To publish images and new releases, for now, the only way is using the command line, isn't automate by CI yet; 😔
- The infra isn't automated by the power of the IAC yet.😔 Button engineer only. 🔘✅

4 changes: 2 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func parseFile(r *http.Request) (content string, err error) {
}

// convert is an internal function responsible for converting the file content into transactions.
func convert(content string) (t []contractus.Transaction, err error) {
func convert(content, email string) (t []contractus.Transaction, err error) {
content = strings.Replace(content, "\t", "", -1)
var re = regexp.MustCompile(`(?m).*$\n`)
for _, match := range re.FindAllString(content, -1) {
Expand All @@ -84,7 +84,7 @@ func convert(content string) (t []contractus.Transaction, err error) {
ProductPriceCents: match[56:66],
SellerName: match[66:],
}
transac, err := rawTransaction.Convert()
transac, err := rawTransaction.Convert(email)
if err != nil {
return nil, fmt.Errorf("failed to convert transaction: %v", err)
}
Expand Down
6 changes: 5 additions & 1 deletion api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ func TestConvert(t *testing.T) {
content := `12022-01-15T19:20:30-03:00CURSO DE BEM-ESTAR 0000012750JOSE CARLOS
12021-12-03T11:46:02-03:00DOMINANDO INVESTIMENTOS 0000050000MARIA CANDIDA
`
transac, err := convert(content)
transac, err := convert(content, "[email protected]")
if err != nil {
t.Fatal(err)
}

want := []contractus.Transaction{
{
Email: "[email protected]",
Type: 1,
Date: time.Date(2022, 01, 15, 22, 20, 30, 0, time.UTC),
ProductDescription: "CURSO DE BEM-ESTAR",
Expand All @@ -50,6 +51,7 @@ func TestConvert(t *testing.T) {
Action: "venda produtor",
},
{
Email: "[email protected]",
Type: 1,
Date: time.Date(2021, 12, 03, 14, 46, 02, 0, time.UTC),
ProductDescription: "DOMINANDO INVESTIMENTOS",
Expand All @@ -61,6 +63,7 @@ func TestConvert(t *testing.T) {
}

if len(transac) == len(want) {
assert(t, transac[0].Email, want[0].Email)
assert(t, transac[0].Type, want[0].Type)
assert(t, transac[0].Date.Format(time.RFC3339), want[0].Date.Format(time.RFC3339))
assert(t, transac[0].ProductDescription, want[0].ProductDescription)
Expand All @@ -69,6 +72,7 @@ func TestConvert(t *testing.T) {
assert(t, transac[0].SellerType, want[0].SellerType)
assert(t, transac[0].Action, want[0].Action)

assert(t, transac[1].Email, want[1].Email)
assert(t, transac[1].Type, want[1].Type)
assert(t, transac[1].Date.Format(time.RFC3339), want[1].Date.Format(time.RFC3339))
assert(t, transac[1].ProductDescription, want[1].ProductDescription)
Expand Down
51 changes: 41 additions & 10 deletions api/transactionhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ import (
"log/slog"

"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
"github.com/go-openapi/runtime/middleware"
"github.com/perebaj/contractus"
"github.com/perebaj/contractus/postgres"
)

type transactionStorage interface {
SaveTransaction(ctx context.Context, t []contractus.Transaction) error
Balance(ctx context.Context, sellerType, sellerName string) (*contractus.BalanceResponse, error)
Transactions(ctx context.Context) (contractus.TransactionResponse, error)
Balance(ctx context.Context, sellerType, sellerName, email string) (*contractus.BalanceResponse, error)
Transactions(ctx context.Context, email string) (contractus.TransactionResponse, error)
}

type transactionHandler struct {
Expand Down Expand Up @@ -62,16 +63,20 @@ func RegisterSwaggerHandler(r chi.Router) {
}

func (s transactionHandler) producerBalance(w http.ResponseWriter, r *http.Request) {
// _, claims, _ := jwtauth.FromContext(r.Context())
// slog.Info("Claims", "claims", claims)
email, err := emailFromRequest(r)
if err != nil {
sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"})
return
}

query := r.URL.Query()
name := query.Get("name")
if name == "" {
sendErr(w, http.StatusBadRequest, Error{"name_required", "name is required"})
return
}

b, err := s.storage.Balance(r.Context(), "producer", name)
b, err := s.storage.Balance(r.Context(), "producer", name, email)
if err != nil {
if err == postgres.ErrSellerNotFound {
sendErr(w, http.StatusNotFound, Error{"seller_not_found", "seller not found"})
Expand All @@ -85,14 +90,19 @@ 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"})
return
}
query := r.URL.Query()
name := query.Get("name")
if name == "" {
sendErr(w, http.StatusBadRequest, Error{"name_required", "name is required"})
return
}

b, err := s.storage.Balance(r.Context(), "affiliate", name)
b, err := s.storage.Balance(r.Context(), "affiliate", name, email)
if err != nil {
if err == postgres.ErrSellerNotFound {
sendErr(w, http.StatusNotFound, Error{"seller_not_found", "seller not found"})
Expand All @@ -106,14 +116,20 @@ 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"})
return
}

content, err := parseFile(r)
if err != nil {
slog.Error("Failed to parse file", "error", err)
sendErr(w, http.StatusBadRequest, Error{"invalid_file", "invalid file"})
return
}

transactions, err := convert(content)
transactions, err := convert(content, email)
if err != nil {
slog.Error("Failed to convert transactions", "error", err, "content", content)
sendErr(w, http.StatusBadRequest, Error{"invalid_file", "invalid file"})
Expand All @@ -130,13 +146,19 @@ func (s transactionHandler) upload(w http.ResponseWriter, r *http.Request) {
}

func (s transactionHandler) transactions(w http.ResponseWriter, r *http.Request) {
tResponse, err := s.storage.Transactions(r.Context())
email, err := emailFromRequest(r)
if err != nil {
sendErr(w, http.StatusBadRequest, Error{"email_required", "email is required"})
return
}

t, err := s.storage.Transactions(r.Context(), email)
if err != nil {
sendErr(w, http.StatusInternalServerError, err)
return
}

send(w, http.StatusOK, tResponse)
send(w, http.StatusOK, t)
}

// Transaction represents the raw transaction from the file.
Expand All @@ -150,7 +172,7 @@ type Transaction struct {

// Convert transform the raw transaction to the business Transaction structure.
// TODO(JOJO) Join errors in one, and return all the errors.
func (t *Transaction) Convert() (*contractus.Transaction, error) {
func (t *Transaction) Convert(email string) (*contractus.Transaction, error) {
typeInt, err := strconv.Atoi(t.Type)
if err != nil {
return nil, fmt.Errorf("failed to convert type: %v", err)
Expand All @@ -171,6 +193,7 @@ func (t *Transaction) Convert() (*contractus.Transaction, error) {
sellerName := strings.Replace(t.SellerName, "\n", "", -1)

transac := &contractus.Transaction{
Email: email,
Type: typeInt,
Date: dateTime.UTC(),
ProductDescription: prodDesc,
Expand All @@ -193,3 +216,11 @@ func (t *Transaction) Convert() (*contractus.Transaction, error) {

return transac, nil
}

func emailFromRequest(r *http.Request) (string, error) {
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
return "", fmt.Errorf("failed to get claims from context: %v", err)
}
return claims["email"].(string), nil
}
50 changes: 38 additions & 12 deletions api/transactionhandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"

"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
"github.com/perebaj/contractus"
)

Expand All @@ -19,11 +20,11 @@ func (m *mockTransactionStorage) SaveTransaction(_ context.Context, _ []contract
return nil
}

func (m *mockTransactionStorage) Balance(_ context.Context, _ string, _ string) (*contractus.BalanceResponse, error) {
func (m *mockTransactionStorage) Balance(_ context.Context, _ string, _ string, _ string) (*contractus.BalanceResponse, error) {
return nil, nil
}

func (m *mockTransactionStorage) Transactions(_ context.Context) (contractus.TransactionResponse, error) {
func (m *mockTransactionStorage) Transactions(_ context.Context, _ string) (contractus.TransactionResponse, error) {
return contractus.TransactionResponse{}, nil
}

Expand All @@ -48,13 +49,20 @@ func TestTransactionHandlerUpload(t *testing.T) {
m := &mockTransactionStorage{}
r := chi.NewRouter()

RegisterTransactionsHandler(r, m)
tokenAuth := jwtauth.New("HS256", []byte("secret"), nil)
_, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "[email protected]"})

r.Group(func(r chi.Router) {
r.Use(jwtauth.Verifier(tokenAuth))
r.Use(jwtauth.Authenticator)
RegisterTransactionsHandler(r, m)
})

req := httptest.NewRequest(http.MethodPost, "/upload", &b)
req.Header.Set("Content-Type", w.FormDataContentType())
req.AddCookie(&http.Cookie{
Name: "jwt",
Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InBlcmViYWpAZ21haWwuY29tIn0.7uJvLACFC_2470iO_G8_xLaa1ChFgxQxHBvS9nzOMDM",
Value: token,
})
resp := httptest.NewRecorder()

Expand All @@ -66,36 +74,48 @@ func TestTransactionHandlerUpload(t *testing.T) {
}

func TestTransactionHandlerBalanceProducer(t *testing.T) {
tokenAuth := jwtauth.New("HS256", []byte("secret"), nil)
_, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "[email protected]"})

m := &mockTransactionStorage{}
r := chi.NewRouter()

RegisterTransactionsHandler(r, 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)
req.AddCookie(&http.Cookie{
Name: "jwt",
Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InBlcmViYWpAZ21haWwuY29tIn0.7uJvLACFC_2470iO_G8_xLaa1ChFgxQxHBvS9nzOMDM",
Value: token,
})

resp := httptest.NewRecorder()

r.ServeHTTP(resp, req)

if resp.Code != http.StatusOK {
t.Fatalf("expected status code %d, got %d", http.StatusOK, resp.Code)
t.Fatalf("expected status code %d, got %d, Response Body %s", http.StatusOK, resp.Code, resp.Body.String())
}
}

func TestTransactionHandlerBalanceAffiliate(t *testing.T) {
m := &mockTransactionStorage{}
r := chi.NewRouter()
tokenAuth := jwtauth.New("HS256", []byte("secret"), nil)
_, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "[email protected]"})

RegisterTransactionsHandler(r, 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/affiliate?name=JOSE%20CARLOS", nil)
req.AddCookie(&http.Cookie{
Name: "jwt",
Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InBlcmViYWpAZ21haWwuY29tIn0.7uJvLACFC_2470iO_G8_xLaa1ChFgxQxHBvS9nzOMDM",
Value: token,
})

resp := httptest.NewRecorder()
Expand All @@ -110,13 +130,19 @@ func TestTransactionHandlerBalanceAffiliate(t *testing.T) {
func TestTransactionHandlerTransactions(t *testing.T) {
m := &mockTransactionStorage{}
r := chi.NewRouter()
tokenAuth := jwtauth.New("HS256", []byte("secret"), nil)
_, token, _ := tokenAuth.Encode(map[string]interface{}{"email": "[email protected]"})

RegisterTransactionsHandler(r, m)
r.Group(func(r chi.Router) {
r.Use(jwtauth.Verifier(tokenAuth))
r.Use(jwtauth.Authenticator)
RegisterTransactionsHandler(r, m)
})

req := httptest.NewRequest(http.MethodGet, "/transactions", nil)
req.AddCookie(&http.Cookie{
Name: "jwt",
Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InBlcmViYWpAZ21haWwuY29tIn0.7uJvLACFC_2470iO_G8_xLaa1ChFgxQxHBvS9nzOMDM",
Value: token,
})

resp := httptest.NewRecorder()
Expand Down
Empty file.
12 changes: 12 additions & 0 deletions postgres/migrations/000002_add_transactions_email.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
BEGIN;

ALTER TABLE transactions ADD COLUMN email TEXT;

UPDATE transactions SET email = '[email protected]' WHERE email IS NULL;

ALTER TABLE transactions ALTER COLUMN email SET NOT NULL ;

ALTER TABLE transactions ADD CONSTRAINT empty_email CHECK (email <> '');

COMMIT;

Loading

0 comments on commit 80a7b9c

Please sign in to comment.