diff --git a/apperrors/apperrors.go b/apperrors/apperrors.go new file mode 100644 index 0000000..767fafe --- /dev/null +++ b/apperrors/apperrors.go @@ -0,0 +1,69 @@ +package apperrors + +import ( + "encoding/json" + "errors" + "net/http" + + l "github.com/sirupsen/logrus" +) + +// ErrorStruct - struct used to convert error messages into required JSON format +type ErrorStruct struct { + Message string `json:"message,omitempty"` //Error Message + Status int `json:"status,omitempty"` //HTTP Response status code +} + +// Error - prints out an error +func Error(appError error, msg string, triggeringError error) { + l.WithFields(l.Fields{"appError": appError, "message": msg}).Error(triggeringError) +} + +// Warn - for warnings +func Warn(appError error, msg string, triggeringError error) { + l.WithFields(l.Fields{"appError": appError, "message": msg}).Warn(triggeringError) +} + +// JSONError - This function writes out an error response with the status +// header passed in +func JSONError(rw http.ResponseWriter, status int, err error) { + + errObj := ErrorStruct{ + Message: err.Error(), + Status: status, + } + + errJSON, err := json.Marshal(&errObj) + if err != nil { + Warn(err, "Error in AppErrors marshalling JSON", err) + } + rw.WriteHeader(status) + rw.Header().Add("Content-Type", "application/json") + rw.Write(errJSON) + return +} + +// ErrRecordNotFound - for when a database record isn't found +var ErrRecordNotFound = errors.New("Database record not found") + +// ErrInvalidToken - used when a JSON Web Token ("JWT") cannot be validated +// by the JWT library +var ErrInvalidToken = errors.New("Invalid Token") + +// ErrSignedString - failed to sign the token string +var ErrSignedString = errors.New("Failed to sign token string") + +// ErrMissingAuthHeader - When the HTTP request doesn't contain an 'Authorization' header +var ErrMissingAuthHeader = errors.New("Missing Auth header") + +// ErrJSONParseFail - If json.Unmarshal or json.Marshal returns an error +var ErrJSONParseFail = errors.New("Failed to parse JSON response (likely not valid JSON)") + +// ErrNoSigningKey - there isn't a signing key defined in the app configuration +var ErrNoSigningKey = errors.New("no JWT signing key specified; cannot authenticate users. Define JWT_SECRET in application.yml and restart") + +// ErrFailedToCreate - Record Creation Failed +var ErrFailedToCreate = errors.New("Failed to create database record") + +// ErrUnknown - Generic Error For Unknown Errors +var ErrUnknown = errors.New("unknown/unexpected error has occurred") diff --git a/config/config.go b/config/config.go index af428d1..15bdce4 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,6 @@ package config import ( - "errors" "fmt" "strconv" @@ -9,23 +8,31 @@ import ( ) var ( - appName string - appPort int + appName string + appPort int + jwtKey string + jwtExpiryDurationHours int ) func Load() { - viper.SetDefault("APP_NAME", "app") + + viper.SetDefault("APP_NAME", "e-commerce") viper.SetDefault("APP_PORT", "8002") - viper.SetConfigName("application") + viper.SetConfigName("application") // name of config file (without extension) viper.SetConfigType("yaml") viper.AddConfigPath("./") viper.AddConfigPath("./..") viper.AddConfigPath("./../..") viper.ReadInConfig() viper.AutomaticEnv() + + // Check for the presence of JWT_KEY and JWT_EXPIRY_DURATION_HOURS + JWTKey() + JWTExpiryDurationHours() } +// AppName - returns the app name func AppName() string { if appName == "" { appName = ReadEnvString("APP_NAME") @@ -33,6 +40,7 @@ func AppName() string { return appName } +// AppPort - returns application http port func AppPort() int { if appPort == 0 { appPort = ReadEnvInt("APP_PORT") @@ -40,6 +48,17 @@ func AppPort() int { return appPort } +// JWTKey - returns the JSON Web Token key +func JWTKey() []byte { + return []byte(ReadEnvString("JWT_SECRET")) +} + +// JWTExpiryDurationHours - returns duration for jwt expiry in int +func JWTExpiryDurationHours() int { + return ReadEnvInt("JWT_EXPIRY_DURATION_HOURS") +} + +// ReadEnvInt - reads an environment variable as an integer func ReadEnvInt(key string) int { checkIfSet(key) v, err := strconv.Atoi(viper.GetString(key)) @@ -49,11 +68,13 @@ func ReadEnvInt(key string) int { return v } +// ReadEnvString - reads an environment variable as a string func ReadEnvString(key string) string { checkIfSet(key) return viper.GetString(key) } +// ReadEnvBool - reads environment variable as a boolean func ReadEnvBool(key string) bool { checkIfSet(key) return viper.GetBool(key) @@ -61,7 +82,7 @@ func ReadEnvBool(key string) bool { func checkIfSet(key string) { if !viper.IsSet(key) { - err := errors.New(fmt.Sprintf("Key %s is not set", key)) + err := fmt.Errorf("Key %s is not set", key) panic(err) } } diff --git a/db/cart.go b/db/cart.go new file mode 100644 index 0000000..ad16459 --- /dev/null +++ b/db/cart.go @@ -0,0 +1,35 @@ +package db + +import ( + "context" + + "github.com/lib/pq" + logger "github.com/sirupsen/logrus" +) + +// CartProduct is rquirement of frontend as json response +type CartProduct struct { + Id int `db:"product_id" json:"id"` + Name string `db:"product_name" json:"product_title"` + Quantity int `db:"quantity" json:"quantity"` + Category string `db:"category_name" json:"category,omitempty"` + Price float64 `db:"price" json:"product_price"` + Description string `db:"description" json:"description"` + ImageUrls pq.StringArray `db:"image_urls" json:"image_url,*"` + Discount float32 `db:"discount" json:"discount"` + Tax float32 `db:"tax" json:"tax"` +} + +const ( + joinCartProductQuery = `SELECT cart.product_id, products.name as product_name, cart.quantity, category.name as category_name, products.price , products.description, products.image_urls, products.discount, products.tax from cart inner join products on cart.product_id=products.id inner join category on category.id=products.category_id where cart.id=$1 ORDER BY cart.product_id ASC;` +) + +// *pgStore because deps.Store.GetCart - deps is of struct Dependencies - vch is assigned to db conn obj +func (s *pgStore) GetCart(ctx context.Context, user_id int) (cart_products []CartProduct, err error) { + err = s.db.Select(&cart_products, joinCartProductQuery, user_id) + if err != nil { + logger.WithField("err", err.Error()).Error("Error fetching data from cart") + return + } + return +} diff --git a/db/cart_test.go b/db/cart_test.go new file mode 100644 index 0000000..d3928a5 --- /dev/null +++ b/db/cart_test.go @@ -0,0 +1,77 @@ +package db + +import ( + "context" + "errors" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "github.com/lib/pq" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type CartTestSuite struct { + suite.Suite + dbStore Storer + db *sqlx.DB + sqlmock sqlmock.Sqlmock +} + +var image_urls = pq.StringArray([]string{"url1", "url2"}) + +var expectedCart = CartProduct{ + Id: 1, + Name: "laptop", + Quantity: 1, + Category: "electronics", + Price: 202, + Description: "description", + ImageUrls: image_urls, + Discount: 20, + Tax: 5, +} + +func (suite *CartTestSuite) SetupTest() { + dbStore, dbConn, sqlmock := InitMockDB() + suite.dbStore = dbStore + suite.db = dbConn + suite.sqlmock = sqlmock + mockedRows = suite.getMockedRows() +} + +func (suite *CartTestSuite) TearDownTest() { + suite.db.Close() +} + +func (suite *CartTestSuite) getMockedRows() (mockedRows *sqlmock.Rows) { + mockedRows = suite.sqlmock.NewRows([]string{"product_id", "product_name", "quantity", "category_name", "price", "description", "image_urls", "discount", "tax"}). + AddRow(1, "laptop", 1, "electronics", 202, "description", image_urls, 20, 5) + return +} + +func (suite *CartTestSuite) TestGetCartSuccess() { + + suite.sqlmock.ExpectQuery(joinCartProductQuery). + WithArgs(1). + WillReturnRows(mockedRows) + + cart, err := suite.dbStore.GetCart(context.Background(), expectedCart.Id) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []CartProduct{expectedCart}, cart) +} + +func (suite *CartTestSuite) TestCartFailure() { + + suite.db.Close() + //Close connection to test failure case + + suite.sqlmock.ExpectQuery(joinCartProductQuery). + WillReturnRows(mockedRows) + + _, err := suite.dbStore.GetCart(context.Background(), expectedCart.Id) + + assert.NotNil(suite.T(), err) + assert.Equal(suite.T(), errors.New("sql: database is closed"), err) +} diff --git a/db/common_test.go b/db/common_test.go new file mode 100644 index 0000000..9135bcc --- /dev/null +++ b/db/common_test.go @@ -0,0 +1,38 @@ +package db + +import ( + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + logger "github.com/sirupsen/logrus" + "github.com/stretchr/testify/suite" +) + +var ( + now time.Time + mockedRows *sqlmock.Rows +) + +func InitMockDB() (s Storer, sqlConn *sqlx.DB, sqlmockInstance sqlmock.Sqlmock) { + + // sqlmock.New() gives {error : not able to match sql queries} ,so adding these parameters (sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) to sqlmock.New allows complex queries like "Join" to be matched + mockDB, sqlmock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + logger.WithField("err:", err).Error("error initializing mock db") + return + } + + sqlmockInstance = sqlmock + sqlxDB := sqlx.NewDb(mockDB, "sqlmock") + + var pgStoreConn pgStore + pgStoreConn.db = sqlxDB + + return &pgStoreConn, sqlxDB, sqlmockInstance +} + +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(CartTestSuite)) +} diff --git a/db/db.go b/db/db.go index 2d1556a..af5ba5e 100644 --- a/db/db.go +++ b/db/db.go @@ -6,7 +6,9 @@ import ( type Storer interface { ListUsers(context.Context) ([]User, error) - //Create(context.Context, User) error - //GetUser(context.Context) (User, error) - //Delete(context.Context, string) error + AuthenticateUser(context.Context, User) (User, error) + GetUser(context.Context, int) (User, error) + CreateBlacklistedToken(context.Context, BlacklistedToken) error + CheckBlacklistedToken(context.Context, string) (bool, int) + GetCart(context.Context, int) ([]CartProduct, error) } diff --git a/db/mock.go b/db/mock.go index a8047e7..3097ad5 100644 --- a/db/mock.go +++ b/db/mock.go @@ -14,3 +14,29 @@ func (m *DBMockStore) ListUsers(ctx context.Context) (users []User, err error) { args := m.Called(ctx) return args.Get(0).([]User), args.Error(1) } + +//deps.store.GetCart(ctx,id) +func (m *DBMockStore) GetCart(ctx context.Context, user_id int) (cart_products []CartProduct, err error) { + args := m.Called(ctx, user_id) + return args.Get(0).([]CartProduct), args.Error(1) +} + +func (m *DBMockStore) AuthenticateUser(ctx context.Context, u User) (user User, err error) { + args := m.Called(ctx, u) + return args.Get(0).(User), args.Error(1) +} + +func (m *DBMockStore) CheckBlacklistedToken(ctx context.Context, token string) (bool, int) { + args := m.Called(ctx, token) + return args.Get(0).(bool), args.Get(1).(int) +} + +func (m *DBMockStore) CreateBlacklistedToken(ctx context.Context, token BlacklistedToken) (err error) { + args := m.Called(ctx, token) + return args.Get(0).(error) +} + +func (m *DBMockStore) GetUser(ctx context.Context, id int) (user User, err error) { + args := m.Called(ctx, id) + return args.Get(0).(User), args.Error(1) +} diff --git a/db/user.go b/db/user.go index 17715e9..4986e70 100644 --- a/db/user.go +++ b/db/user.go @@ -2,17 +2,30 @@ package db import ( "context" + "database/sql" + + ae "joshsoftware/go-e-commerce/apperrors" logger "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" ) type User struct { - Name string `db:"name" json:"full_name"` - Age int `db:"age" json:"age"` + ID int `db:"id" json:"id"` + FirstName string `db:"first_name" json:"first_name"` + LastName string `db:"last_name" json:"last_name"` + Email string `db:"email" json:"email"` + Mobile string `db:"mobile" json:"mobile"` + Address string `db:"address" json:"address"` + Password string `db:"password" json:"password"` + Country string `db:"country" json:"country"` + State string `db:"state" json:"state"` + City string `db:"city" json:"city"` + CreatedAt string `db:"created_at" json:"created_at"` } func (s *pgStore) ListUsers(ctx context.Context) (users []User, err error) { - err = s.db.Select(&users, "SELECT * FROM users ORDER BY name ASC") + err = s.db.Select(&users, "SELECT * FROM users ORDER BY first_name ASC") if err != nil { logger.WithField("err", err.Error()).Error("Error listing users") return @@ -20,3 +33,35 @@ func (s *pgStore) ListUsers(ctx context.Context) (users []User, err error) { return } + +//GetUser function is used to Get a Particular User +func (s *pgStore) GetUser(ctx context.Context, id int) (user User, err error) { + + err = s.db.Get(&user, "SELECT * FROM users WHERE id=$1", id) + if err != nil { + if err == sql.ErrNoRows { + err = ae.ErrRecordNotFound + } + logger.WithField("err", err.Error()).Error("Query Failed") + return + } + + return +} + +//AuthenticateUser Function checks if User has Registered before Login +// and Has Entered Correct Credentials +func (s *pgStore) AuthenticateUser(ctx context.Context, u User) (user User, err error) { + + err = s.db.Get(&user, "SELECT * FROM users where email = $1", u.Email) + if err != nil { + logger.WithField("err", err.Error()).Error("No such User Available") + return + } + + if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password)); err != nil { + // If the two passwords don't match, return a 401 status + logger.WithField("Error", err.Error()) + } + return +} diff --git a/db/user_blacklisted_tokens.go b/db/user_blacklisted_tokens.go new file mode 100644 index 0000000..4cc9dc2 --- /dev/null +++ b/db/user_blacklisted_tokens.go @@ -0,0 +1,49 @@ +package db + +import ( + "context" + "fmt" + "time" + + logger "github.com/sirupsen/logrus" +) + +//BlacklistedToken - struct representing a token to be blacklisted (logout) +type BlacklistedToken struct { + ID int `db:"id" json:"id"` + UserID float64 `db:"user_id" json:"user_id"` + Token string `db:"token" json:"token"` + ExpirationDate time.Time `db:"expiration_date" json:"expiration_date"` +} + +const ( + insertBlacklistedToken = `INSERT INTO user_blacklisted_tokens +(user_id, token, expiration_date) +VALUES ($1, $2, $3)` +) + +//CreateBlacklistedToken function to insert the blacklisted token in database +func (s *pgStore) CreateBlacklistedToken(ctx context.Context, token BlacklistedToken) (err error) { + _, err = s.db.Exec(insertBlacklistedToken, token.UserID, token.Token, token.ExpirationDate) + + if err != nil { + errMsg := fmt.Sprintf("Error inserting the blacklisted token for user with id %v", token.UserID) + logger.WithField("err", err.Error()).Error(errMsg) + return + } + return +} + +//CheckBlacklistedToken function to check if token is blacklisted earlier +func (s *pgStore) CheckBlacklistedToken(ctx context.Context, token string) (bool, int) { + + var userID int + query1 := fmt.Sprintf("SELECT user_id FROM user_blacklisted_tokens WHERE token='%s'", token) + err := s.db.QueryRow(query1).Scan(&userID) + + if err != nil { + logger.WithField("err", err.Error()).Error("Either Query Failed or No Rows Found") + return false, -1 + } + return true, userID +} diff --git a/go.mod b/go.mod index bd1ae4f..8d1fd40 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module joshsoftware/go-e-commerce go 1.14 require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/auth0/go-jwt-middleware v0.0.0-20200810150920-a32d7af194d1 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gorilla/mux v1.8.0 github.com/jmoiron/sqlx v1.2.0 github.com/lib/pq v1.8.0 @@ -12,4 +15,5 @@ require ( github.com/stretchr/testify v1.6.1 github.com/urfave/cli v1.22.4 github.com/urfave/negroni v1.0.0 -) + golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 +) \ No newline at end of file diff --git a/go.sum b/go.sum index 0b155a0..8d76f3f 100644 --- a/go.sum +++ b/go.sum @@ -13,18 +13,23 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/auth0/go-jwt-middleware v0.0.0-20200810150920-a32d7af194d1 h1:lnVadil6o8krZE47ms2PCxhXcki/UwoqiB0axOIV3mk= +github.com/auth0/go-jwt-middleware v0.0.0-20200810150920-a32d7af194d1/go.mod h1:mF0ip7kTEFtnhBJbd/gJe62US3jykNN+dcZoZakJCCA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -35,6 +40,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -45,6 +51,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -68,6 +75,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -105,6 +114,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -164,6 +174,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -203,6 +214,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/main.go b/main.go index e0ad81d..a263257 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,7 @@ func main() { } func startApp() (err error) { - store, err := db.Init() + store, err := db.Init() //pg.go - datatype will be &pgStore - *sql.DB if err != nil { logger.WithField("err", err.Error()).Error("Database init failed") return @@ -75,7 +75,8 @@ func startApp() (err error) { } // mux router - router := service.InitRouter(deps) + router := service.InitRouter(deps) // init router - return mux.NewRouter with all hanldefuncs + //in router.go why have in some places we have used hanldefunc and some handle // init web server server := negroni.Classic() diff --git a/migrations/1587381324_create_users.down.sql b/migrations/1587381324_create_users.down.sql index cc1f647..c99ddcd 100644 --- a/migrations/1587381324_create_users.down.sql +++ b/migrations/1587381324_create_users.down.sql @@ -1 +1 @@ -DROP TABLE users; +DROP TABLE IF EXISTS users; diff --git a/migrations/1587381324_create_users.up.sql b/migrations/1587381324_create_users.up.sql index f893282..e87d896 100644 --- a/migrations/1587381324_create_users.up.sql +++ b/migrations/1587381324_create_users.up.sql @@ -1,4 +1,13 @@ -CREATE TABLE users ( - name text, - age integer -); +CREATE TABLE IF NOT EXISTS users ( + id SERIAL NOT NULL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255), + email VARCHAR(255) NOT NULL UNIQUE, + mobile VARCHAR(20), + country VARCHAR(100), + state VARCHAR(100), + city VARCHAR(100), + address TEXT, + password TEXT, + created_at TIMESTAMP DEFAULT (NOW() AT TIME ZONE 'UTC') +); \ No newline at end of file diff --git a/migrations/1599802515_blacklisted_tokens.down.sql b/migrations/1599802515_blacklisted_tokens.down.sql new file mode 100644 index 0000000..cbc5281 --- /dev/null +++ b/migrations/1599802515_blacklisted_tokens.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS user_blacklisted_tokens; diff --git a/migrations/1599802515_blacklisted_tokens.up.sql b/migrations/1599802515_blacklisted_tokens.up.sql new file mode 100644 index 0000000..ea33576 --- /dev/null +++ b/migrations/1599802515_blacklisted_tokens.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS user_blacklisted_tokens( + id SERIAL NOT NULL PRIMARY KEY, + user_id BIGINT REFERENCES users(id), + token TEXT, + expiration_date TIMESTAMP +); \ No newline at end of file diff --git a/migrations/1601589672_create_products.down.sql b/migrations/1601589672_create_products.down.sql new file mode 100644 index 0000000..4ecd689 --- /dev/null +++ b/migrations/1601589672_create_products.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS products; \ No newline at end of file diff --git a/migrations/1601589672_create_products.up.sql b/migrations/1601589672_create_products.up.sql new file mode 100644 index 0000000..13d0d37 --- /dev/null +++ b/migrations/1601589672_create_products.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS products ( + id SERIAL PRIMARY KEY, + name varchar(50) UNIQUE NOT NULL , + description varchar(200) NOT NULL, + price float NOT NULL, + discount float DEFAULT 0, + quantity int NOT NULL , + tax float DEFAULT 0, + category_id BIGINT NOT NULL , + brand varchar(50) NOT NULL , + color varchar(50) NOT NULL DEFAULT '', + size varchar(50) NOT NULL DEFAULT '', + image_urls text[], + FOREIGN KEY(category_id) + REFERENCES Category(id) ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/migrations/1601589769_create_category.down.sql b/migrations/1601589769_create_category.down.sql new file mode 100644 index 0000000..f3f51fd --- /dev/null +++ b/migrations/1601589769_create_category.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS category; \ No newline at end of file diff --git a/migrations/1601589769_create_category.up.sql b/migrations/1601589769_create_category.up.sql new file mode 100644 index 0000000..790f1cc --- /dev/null +++ b/migrations/1601589769_create_category.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS category ( + id SERIAL NOT NULL PRIMARY KEY, + name varchar(50) NOT NULL, + description TEXT +); \ No newline at end of file diff --git a/migrations/1601589773_create_cart.down.sql b/migrations/1601589773_create_cart.down.sql new file mode 100644 index 0000000..baacbaf --- /dev/null +++ b/migrations/1601589773_create_cart.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS cart; \ No newline at end of file diff --git a/migrations/1601589773_create_cart.up.sql b/migrations/1601589773_create_cart.up.sql new file mode 100644 index 0000000..d650886 --- /dev/null +++ b/migrations/1601589773_create_cart.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS cart ( + id integer NOT NULL , + product_id BIGINT NOT NULL, + quantity integer, + FOREIGN KEY(product_id) + REFERENCES products(id) ON DELETE CASCADE, + PRIMARY KEY(id, product_id) +); \ No newline at end of file diff --git a/service/cart_http.go b/service/cart_http.go new file mode 100644 index 0000000..adcea86 --- /dev/null +++ b/service/cart_http.go @@ -0,0 +1,58 @@ +package service + +import ( + "encoding/json" + "net/http" + "strings" + + logger "github.com/sirupsen/logrus" +) + +// @Title listCart +// @Description list all Product inside cart +// @Router /user/id/cart [get] +// @Accept json +// @Success 200 {object} +// @Failure 400 {object} +func getCartHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + authToken := req.Header.Get("Authorization") + if strings.HasPrefix(strings.ToUpper(authToken), "BEARER") { + authToken = authToken[len("BEARER "):] + } + + userID, _, err := getDataFromToken(authToken) + if err != nil { + logger.WithField("err", err.Error()).Error("Error fetching data from token") + error := errorResponse{ + Error: "Error fetching data from token", + } + responses(rw, http.StatusUnauthorized, error) + return + } + + cart_products, err := deps.Store.GetCart(req.Context(), int(userID)) + if err != nil { + logger.WithField("err", err.Error()).Error("Error fetching data from database") + error := errorResponse{ + Error: "Error fetching data from database", + } + responses(rw, http.StatusInternalServerError, error) + return + } + + respBytes, err := json.Marshal(cart_products) + if err != nil { + logger.WithField("err", err.Error()).Error("Error marshaling cart data") + error := errorResponse{ + Error: "Error marshaling cart data", + } + responses(rw, http.StatusInternalServerError, error) + return + } + + rw.Header().Add("Content-Type", "application/json") + rw.Write(respBytes) + }) +} diff --git a/service/cart_http_test.go b/service/cart_http_test.go new file mode 100644 index 0000000..1089c18 --- /dev/null +++ b/service/cart_http_test.go @@ -0,0 +1,120 @@ +package service + +import ( + "encoding/json" + "errors" + "joshsoftware/go-e-commerce/db" + "net/http" + + "github.com/lib/pq" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +var image_urls = pq.StringArray([]string{"url1", "url2"}) + +var testCartProduct = []db.CartProduct{ + { + Id: 1, + Name: "abc", + Quantity: 10, + Category: "clothing", + Price: 2000, + Description: "abc", + ImageUrls: image_urls, + Discount: 20, + Tax: 5, + }, +} + +type CartHandlerTestSuite struct { + suite.Suite + dbMock *db.DBMockStore +} + +func (suite *CartHandlerTestSuite) SetupTest() { + suite.dbMock = &db.DBMockStore{} +} + +func (suite *CartHandlerTestSuite) TestGetCartSuccess() { + suite.dbMock.On("GetCart", mock.Anything, mock.Anything).Return( + []db.CartProduct{ + db.CartProduct{ + Id: 1, + Name: "abc", + Quantity: 10, + Category: "clothing", + Price: 2000, + Description: "abc", + ImageUrls: image_urls, + Discount: 20, + Tax: 5, + }, + }, + nil, + ) + + recorder := makeHTTPCallWithJWTMiddleware(http.MethodGet, + "/cart", + "/cart", + "", + getCartHandler(Dependencies{Store: suite.dbMock}), + ) + + actual := []db.CartProduct{} + _ = json.Unmarshal(recorder.Body.Bytes(), &actual) + + assert.Equal(suite.T(), http.StatusOK, recorder.Code) + assert.Equal(suite.T(), testCartProduct, actual) + + suite.dbMock.AssertExpectations(suite.T()) + +} + +func (suite *CartHandlerTestSuite) TestGetCartDbFailureFetchError() { + suite.dbMock.On("GetCart", mock.Anything, mock.Anything).Return( + []db.CartProduct{}, + errors.New("Error fetching data from database"), + ) + + recorder := makeHTTPCallWithJWTMiddleware(http.MethodGet, + "/cart", + "/cart", + "", + getCartHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusInternalServerError, recorder.Code) + assert.Equal(suite.T(), `{"error":"Error fetching data from database"}`, recorder.Body.String()) + + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *CartHandlerTestSuite) TestGetCartDbFailureJSONMarshallError() { + suite.dbMock.On("GetCart", mock.Anything, mock.Anything).Return( + []db.CartProduct{ + db.CartProduct{ + Id: 1, + Name: "abc", + Quantity: 10, + Category: "clothing", + Price: 2000, + Description: "abc", + ImageUrls: image_urls, + Discount: 20, + Tax: 5, + }, + }, + errors.New("Error marshaling cart data"), + ) + recorder := makeHTTPCallWithJWTMiddleware(http.MethodGet, + "/cart", + "/cart", + "", + getCartHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusInternalServerError, recorder.Code) + suite.dbMock.AssertExpectations(suite.T()) +} diff --git a/service/common_http_test.go b/service/common_http_test.go new file mode 100644 index 0000000..c4115fe --- /dev/null +++ b/service/common_http_test.go @@ -0,0 +1,60 @@ +package service + +import ( + "joshsoftware/go-e-commerce/config" + "net/http" + "net/http/httptest" + "strings" + "testing" + + jwtmiddleware "github.com/auth0/go-jwt-middleware" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" + "github.com/urfave/negroni" +) + +func TestExampleTestSuite(t *testing.T) { + config.Load() + suite.Run(t, new(CartHandlerTestSuite)) +} + +func makeHTTPCall(method, path, requestURL, body string, handlerFunc http.HandlerFunc) (recorder *httptest.ResponseRecorder) { + // create a http request using the given parameters + req, _ := http.NewRequest(method, requestURL, strings.NewReader(body)) + // test recorder created for capturing api responses + recorder = httptest.NewRecorder() + // create a router to serve the handler in test with the prepared request + router := mux.NewRouter() + router.HandleFunc(path, handlerFunc).Methods(method) + + // serve the request and write the response to recorder + router.ServeHTTP(recorder, req) + return +} + +func makeHTTPCallWithJWTMiddleware(method, path, requestURL, body string, handlerFunc http.HandlerFunc) (recorder *httptest.ResponseRecorder) { + // create jwt token with userID + JWTToken, _ := generateJwt(1) + + req, _ := http.NewRequest(method, requestURL, strings.NewReader(body)) + req.Header.Set("Authorization", "Bearer "+JWTToken) + recorder = httptest.NewRecorder() + // create a router to serve the handler in test with the prepared request + router := mux.NewRouter() + + jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{ + ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { + return config.JWTKey(), nil + }, + SigningMethod: jwt.SigningMethodHS256, + }) + router.Handle(path, negroni.New( + negroni.HandlerFunc(jwtMiddleware.HandlerWithNext), + negroni.Wrap(http.HandlerFunc(handlerFunc)), + )).Methods(method) + + router.ServeHTTP(recorder, req) + return +} diff --git a/service/response.go b/service/response.go new file mode 100644 index 0000000..ea62096 --- /dev/null +++ b/service/response.go @@ -0,0 +1,39 @@ +package service + +import ( + "encoding/json" + "net/http" + + logger "github.com/sirupsen/logrus" +) + +type successResponse struct { + Data interface{} `json:"data"` +} + +type errorResponse struct { + Error interface{} `json:"error"` +} + +type messageObject struct { + Message string `json:"message"` +} + +type errorObject struct { + Code string `json:"code"` + messageObject + Fields map[string]string `json:"fields"` +} + +func responses(rw http.ResponseWriter, status int, responseBody interface{}) { + respBytes, err := json.Marshal(responseBody) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while marshaling core values data") + rw.WriteHeader(http.StatusInternalServerError) + return + } + + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(status) + rw.Write(respBytes) +} diff --git a/service/router.go b/service/router.go index 121ad64..f3ae5a4 100644 --- a/service/router.go +++ b/service/router.go @@ -23,6 +23,42 @@ func InitRouter(deps Dependencies) (router *mux.Router) { // Version 1 API management v1 := fmt.Sprintf("application/vnd.%s.v1", config.AppName()) + //Route for User Login + router.HandleFunc("/login", userLoginHandler(deps)).Methods(http.MethodPost).Headers(versionHeader, v1) + + //Router for User Logout + router.Handle("/logout", jwtMiddleWare(userLogoutHandler(deps), deps)).Methods(http.MethodDelete).Headers(versionHeader, v1) + + //Router for Get All Users router.HandleFunc("/users", listUsersHandler(deps)).Methods(http.MethodGet).Headers(versionHeader, v1) + + router.Handle("/user", jwtMiddleWare(getUserHandler(deps), deps)).Methods(http.MethodGet).Headers(versionHeader, v1) + + router.HandleFunc("/cart", getCartHandler(deps)).Methods(http.MethodGet).Headers(versionHeader, v1) + return } + +//jwtMiddleWare function is used to authenticate and authorize the incoming request +func jwtMiddleWare(endpoint http.Handler, deps Dependencies) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + authToken := req.Header["Token"] + + _, _, err := getDataFromToken(authToken[0]) + if err != nil { + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte("Unauthorized")) + return + } + + //Fetching Status of Token Being Blacklisted or Not + // Unauthorized User if Token BlackListed + if isBlacklisted, _ := deps.Store.CheckBlacklistedToken(req.Context(), authToken[0]); isBlacklisted { + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte("Unauthorized")) + return + } + endpoint.ServeHTTP(rw, req) + }) +} diff --git a/service/session_http.go b/service/session_http.go new file mode 100644 index 0000000..9915005 --- /dev/null +++ b/service/session_http.go @@ -0,0 +1,160 @@ +package service + +import ( + "encoding/json" + "fmt" + ae "joshsoftware/go-e-commerce/apperrors" + "joshsoftware/go-e-commerce/config" + "joshsoftware/go-e-commerce/db" + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" + logger "github.com/sirupsen/logrus" +) + +//AuthBody stores responce body for login +type AuthBody struct { + Message string `json:"meassage"` + Token string `json:"token"` +} + +//generateJWT function generates and return a new JWT token +func generateJwt(userID int) (tokenString string, err error) { + mySigningKey := config.JWTKey() + if mySigningKey == nil { + ae.Error(ae.ErrNoSigningKey, "Application error: No signing key configured", err) + return + } + + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaims) + claims["id"] = userID + claims["exp"] = time.Now().Add(time.Duration(config.JWTExpiryDurationHours()) * time.Hour).Unix() + + tokenString, err = token.SignedString(mySigningKey) + if err != nil { + ae.Error(ae.ErrSignedString, "Failed To Get Signed String", err) + return + } + return +} + +//userLoginHandler function take credentials in json +// and check if the credentials are correct +// also generate and returns a JWT token in the case of correct crendential +func userLoginHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + user := db.User{} + + //fetching the json object to get crendentials of users + err := json.NewDecoder(req.Body).Decode(&user) + if err != nil { + logger.WithField("err", err.Error()).Error("JSON Decoding Failed") + rw.WriteHeader(http.StatusBadRequest) + return + } + + //TODO change no need to return user object from Authentication + //checking if the user is authenticated or not + // by passing the credentials to the AuthenticateUser function + user, err1 := deps.Store.AuthenticateUser(req.Context(), user) + if err1 != nil { + logger.WithField("err", err1.Error()).Error("Invalid Credentials") + rw.WriteHeader(http.StatusUnauthorized) + return + } + + //Generate new JWT token if the user is authenticated + // and return the token in request header + token, err := generateJwt(user.ID) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte("Token Generation Failure")) + return + } + authbody := AuthBody{ + Message: "Login Successfull", + Token: token, + } + + respBytes, err := json.Marshal(authbody) + if err != nil { + ae.Error(ae.ErrJSONParseFail, "JSON Parsing Failed", err) + } + + rw.Header().Add("Content-Type", "application/json") + rw.Write(respBytes) + }) +} + +//userLogoutHandler function logs the user off +// and add the valid JWT token in BlacklistedToken +func userLogoutHandler(deps Dependencies) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + //fetching the token from header + authToken := req.Header["Token"] + + //fetching details from the token + userID, expirationTimeStamp, err := getDataFromToken(authToken[0]) + if err != nil { + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte("Unauthorized")) + return + } + expirationDate := time.Unix(expirationTimeStamp, 0) + + //create a BlacklistedToken to add in database + // To blacklist a user valid token + userBlackListedToken := db.BlacklistedToken{ + UserID: userID, + ExpirationDate: expirationDate, + Token: authToken[0], + } + + err = deps.Store.CreateBlacklistedToken(req.Context(), userBlackListedToken) + if err != nil { + ae.Error(ae.ErrFailedToCreate, "Error creating blaclisted token record", err) + rw.Header().Add("Content-Type", "application/json") + ae.JSONError(rw, http.StatusInternalServerError, err) + return + } + + rw.Write([]byte("Logged Out Successfully")) + rw.WriteHeader(http.StatusOK) + return + }) +} + +func getDataFromToken(Token string) (userID float64, expirationTime int64, err error) { + mySigningKey := config.JWTKey() + + //Checking if token not present in header + if len(Token) < 1 { + ae.Error(ae.ErrMissingAuthHeader, "Missing Authentication Token From Header", err) + return + } + + token, err := jwt.Parse(Token, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("There was an error") + } + return mySigningKey, nil + }) + if err != nil { + ae.Error(ae.ErrInvalidToken, "Invalid Token", err) + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + + if !ok && !token.Valid { + ae.Error(ae.ErrInvalidToken, "Invalid Token", err) + return + } + + userID = claims["id"].(float64) + expirationTime = int64(claims["exp"].(float64)) + return +} diff --git a/service/user_http.go b/service/user_http.go index c544bcd..bfc8d17 100644 --- a/service/user_http.go +++ b/service/user_http.go @@ -33,3 +33,33 @@ func listUsersHandler(deps Dependencies) http.HandlerFunc { rw.Write(respBytes) }) } + +func getUserHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + //fetch usedId from request + authToken := req.Header["Token"] + userID, _, err := getDataFromToken(authToken[0]) + if err != nil { + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte("Unauthorized")) + return + } + + user, err1 := deps.Store.GetUser(req.Context(), int(userID)) + if err1 != nil { + logger.WithField("err", err.Error()).Error("Error fetching data") + rw.WriteHeader(http.StatusInternalServerError) + return + } + + respBytes, err := json.Marshal(user) + if err != nil { + logger.WithField("err", err.Error()).Error("Error marshaling users data") + rw.WriteHeader(http.StatusInternalServerError) + return + } + + rw.Header().Add("Content-Type", "application/json") + rw.Write(respBytes) + }) +} diff --git a/service/user_http_test.go b/service/user_http_test.go index 4e2b367..6d43c33 100644 --- a/service/user_http_test.go +++ b/service/user_http_test.go @@ -1,84 +1 @@ package service - -import ( - "errors" - "joshsoftware/go-e-commerce/db" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" -) - -// Define the suite, and absorb the built-in basic suite -// functionality from testify - including assertion methods. -type UsersHandlerTestSuite struct { - suite.Suite - - dbMock *db.DBMockStore -} - -func (suite *UsersHandlerTestSuite) SetupTest() { - suite.dbMock = &db.DBMockStore{} -} - -func TestExampleTestSuite(t *testing.T) { - suite.Run(t, new(UsersHandlerTestSuite)) -} - -func (suite *UsersHandlerTestSuite) TestListUsersSuccess() { - suite.dbMock.On("ListUsers", mock.Anything).Return( - []db.User{ - db.User{Name: "test-user", Age: 18}, - }, - nil, - ) - - recorder := makeHTTPCall( - http.MethodGet, - "/users", - "", - listUsersHandler(Dependencies{Store: suite.dbMock}), - ) - - assert.Equal(suite.T(), http.StatusOK, recorder.Code) - assert.Equal(suite.T(), `[{"full_name":"test-user","age":18}]`, recorder.Body.String()) - suite.dbMock.AssertExpectations(suite.T()) -} - -func (suite *UsersHandlerTestSuite) TestListUsersWhenDBFailure() { - suite.dbMock.On("ListUsers", mock.Anything).Return( - []db.User{}, - errors.New("error fetching user records"), - ) - - recorder := makeHTTPCall( - http.MethodGet, - "/users", - "", - listUsersHandler(Dependencies{Store: suite.dbMock}), - ) - - assert.Equal(suite.T(), http.StatusInternalServerError, recorder.Code) - suite.dbMock.AssertExpectations(suite.T()) -} - -func makeHTTPCall(method, path, body string, handlerFunc http.HandlerFunc) (recorder *httptest.ResponseRecorder) { - // create a http request using the given parameters - req, _ := http.NewRequest(method, path, strings.NewReader(body)) - - // test recorder created for capturing api responses - recorder = httptest.NewRecorder() - - // create a router to serve the handler in test with the prepared request - router := mux.NewRouter() - router.HandleFunc(path, handlerFunc).Methods(method) - - // serve the request and write the response to recorder - router.ServeHTTP(recorder, req) - return -}