diff --git a/apperrors/apperrors.go b/apperrors/apperrors.go new file mode 100644 index 0000000..2704665 --- /dev/null +++ b/apperrors/apperrors.go @@ -0,0 +1,68 @@ +package apperrors + +import ( + "encoding/json" + "errors" + l "github.com/sirupsen/logrus" + "net/http" +) + +// 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().Set("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/application.yml.default b/application.yml.default index 2dec452..c3919a2 100644 --- a/application.yml.default +++ b/application.yml.default @@ -1,4 +1,4 @@ -APP_NAME: "samplemgr" +APP_NAME: "app" APP_PORT: "33001" # MongoDB URI diff --git a/config/config.go b/config/config.go index af428d1..b2229c2 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,6 @@ package config import ( - "errors" "fmt" "strconv" @@ -9,12 +8,15 @@ import ( ) var ( - appName string - appPort int + appName string + appPort int + jwtKey string + jwtExpiryDurationHours int ) +// Load - loads all the environment variables and/or params in application.yml func Load() { - viper.SetDefault("APP_NAME", "app") + viper.SetDefault("APP_NAME", "e-commerce") viper.SetDefault("APP_PORT", "8002") viper.SetConfigName("application") @@ -24,8 +26,13 @@ func Load() { 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 int(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,19 +68,22 @@ 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) } +//CheckIfSet checks if all the necessary keys are set 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..45da687 --- /dev/null +++ b/db/cart.go @@ -0,0 +1,51 @@ +package db + +import( + "context" + logger "github.com/sirupsen/logrus" +) + +func (s *pgStore) AddToCart(ctx context.Context, cartID, productID int) (rowsAffected int64, err error) { + insert := `INSERT INTO cart (id, product_id, quantity) VALUES ($1, $2, 1)` + result, err := s.db.Exec(insert, cartID, productID) + if err != nil { + logger.WithField("err", err.Error()).Error("Error adding to cart") + return + } + + rowsAffected, err = result.RowsAffected() + if err != nil { + logger.WithField("err", err.Error()).Error("Error while fetching affected rows") + } + return +} + +func (s *pgStore) DeleteFromCart(ctx context.Context, cartID, productID int) (rowsAffected int64, err error) { + delete := `DELETE FROM cart WHERE id = $1 AND product_id = $2` + result, err := s.db.Exec(delete, cartID, productID) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while removing from cart") + return + } + + rowsAffected, err = result.RowsAffected() + if err != nil { + logger.WithField("err", err.Error()).Error("Error while fetching affected rows") + } + return +} + +func (s *pgStore) UpdateIntoCart(ctx context.Context, quantity, cartID, productID int) (rowsAffected int64, err error) { + update := `UPDATE cart SET quantity = $1 WHERE id = $2 AND product_id = $3` + result, err := s.db.Exec(update, quantity, cartID, productID) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while updating into cart") + return + } + + rowsAffected, err = result.RowsAffected() + if err != nil { + logger.WithField("err", err.Error()).Error("Error while fetching affected rows") + } + return +} \ No newline at end of file diff --git a/db/db.go b/db/db.go index 2d1556a..5277bb4 100644 --- a/db/db.go +++ b/db/db.go @@ -4,8 +4,16 @@ import ( "context" ) +// Storer - an interface we use to expose methods that do stuff to the underlying database type Storer interface { ListUsers(context.Context) ([]User, error) + AuthenticateUser(context.Context, User) (User, error) + GetUser(context.Context, int) (User, error) + CreateBlacklistedToken(context.Context, BlacklistedToken) error + CheckBlacklistedToken(context.Context, string) (bool, int) + AddToCart(context.Context, int, int) (int64, error) + DeleteFromCart(context.Context, int, int) (int64, error) + UpdateIntoCart(context.Context, int, int, int) (int64, error) //Create(context.Context, User) error //GetUser(context.Context) (User, error) //Delete(context.Context, string) error diff --git a/db/pg.go b/db/pg.go index bd23fc1..eb56cf5 100644 --- a/db/pg.go +++ b/db/pg.go @@ -10,6 +10,7 @@ import ( "time" "github.com/jmoiron/sqlx" + //lib/pq internally configures with database/sql library" _ "github.com/lib/pq" "github.com/mattes/migrate" "github.com/mattes/migrate/database/postgres" diff --git a/db/user.go b/db/user.go index 17715e9..5a5fe07 100644 --- a/db/user.go +++ b/db/user.go @@ -2,17 +2,31 @@ package db import ( "context" - + "database/sql" logger "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" + ae "joshsoftware/go-e-commerce/apperrors" + "time" ) +//User Struct for declaring attributes of User 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 time.Time `db:"created_at" json:"created_at"` } +//ListUsers function to fetch all Users From Database 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 +34,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..99d80d6 --- /dev/null +++ b/db/user_blacklisted_tokens.go @@ -0,0 +1,48 @@ +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 { + return false, -1 + } + return true, userID +} diff --git a/go.mod b/go.mod index bd1ae4f..4a5c631 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,16 @@ module joshsoftware/go-e-commerce go 1.14 require ( + 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 github.com/mattes/migrate v3.0.1+incompatible + github.com/rs/cors v1.7.0 github.com/sirupsen/logrus v1.6.0 github.com/spf13/viper v1.7.1 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 ) diff --git a/go.sum b/go.sum index 0b155a0..5949fe2 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,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= @@ -154,6 +155,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -203,6 +206,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..9f9a23a 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "github.com/rs/cors" "joshsoftware/go-e-commerce/config" "joshsoftware/go-e-commerce/db" "joshsoftware/go-e-commerce/service" @@ -70,6 +71,13 @@ func startApp() (err error) { return } + c := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"POST", "GET", "DELETE", "PUT", "PATCH", "OPTIONS"}, + AllowCredentials: true, + Debug: true, + }) + deps := service.Dependencies{ Store: store, } @@ -79,6 +87,7 @@ func startApp() (err error) { // init web server server := negroni.Classic() + server.Use(c) server.UseHandler(router) port := config.AppPort() // This can be changed to the service port number via environment variable. diff --git a/migrations/1587381324_create_users.up.sql b/migrations/1587381324_create_users.up.sql index f893282..75c0b9a 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/1599504021_create_cart.down.sql b/migrations/1599504021_create_cart.down.sql new file mode 100644 index 0000000..baacbaf --- /dev/null +++ b/migrations/1599504021_create_cart.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS cart; \ No newline at end of file diff --git a/migrations/1599504021_create_cart.up.sql b/migrations/1599504021_create_cart.up.sql new file mode 100644 index 0000000..277754b --- /dev/null +++ b/migrations/1599504021_create_cart.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS cart ( + id INTEGER NOT NULL, + product_id INTEGER NOT NULL REFERENCES products(id), + quantity INTEGER, + PRIMARY KEY(id, product_id) +); \ No newline at end of file diff --git a/migrations/1599589830_blacklisted_tokens.down.sql b/migrations/1599589830_blacklisted_tokens.down.sql new file mode 100644 index 0000000..fa6445e --- /dev/null +++ b/migrations/1599589830_blacklisted_tokens.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS user_blacklisted_tokens; \ No newline at end of file diff --git a/migrations/1599589830_blacklisted_tokens.up.sql b/migrations/1599589830_blacklisted_tokens.up.sql new file mode 100644 index 0000000..4e6dad1 --- /dev/null +++ b/migrations/1599589830_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/service/cart_http.go b/service/cart_http.go new file mode 100644 index 0000000..2be90f5 --- /dev/null +++ b/service/cart_http.go @@ -0,0 +1,182 @@ +package service + +import ( + "strconv" + "encoding/json" + "net/http" + logger "github.com/sirupsen/logrus" +) + +type successResponse struct { + Message string `json: "message"` +} + +type errorResponse struct { + Error string `json: "error"` +} + +func response(rw http.ResponseWriter, status int, responseData interface{}){ + respBody, err := json.Marshal(responseData) + if err != nil { + logger.WithField("err", err.Error()).Error("error while marshling") + rw.WriteHeader(http.StatusInternalServerError) + return + } + rw.Header().Add("Content-Type","application/json") + rw.WriteHeader(status) + rw.Write(respBody) +} + +func addToCartHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + authToken := req.Header["Token"] + cartID, _, err := getDataFromToken(authToken[0]) + if err != nil { + logger.WithField("err", err.Error()).Error("Unauthorized user") + error := errorResponse { + Error : "Unauthorized user", + } + response(rw, http.StatusUnauthorized, error) + return + } + + productID, err := strconv.Atoi(req.URL.Query()["productID"][0]) + if err != nil { + logger.WithField("err", err.Error()).Error("product_id is missing") + error := errorResponse { + Error : "product_id missing", + } + response(rw, http.StatusBadRequest, error) + return + } + + rowsAffected, err := deps.Store.AddToCart(req.Context(), int(cartID), productID) + if err != nil { + logger.WithField("err", err.Error()).Error("error while adding to cart") + error := errorResponse { + Error : "could not add item", + } + response(rw, http.StatusInternalServerError, error) + return + } + + if rowsAffected != 1 { + success := successResponse { + Message : "zero rows affected", + } + response(rw, http.StatusOK, success) + return + } + + success := successResponse{ + Message: "Item added successfully", + } + response(rw, http.StatusOK, success) + }) +} + +func deleteFromCartHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + authToken := req.Header["Token"] + cartID, _, err := getDataFromToken(authToken[0]) + if err != nil { + logger.WithField("err", err.Error()).Error("Unauthorized user") + error := errorResponse { + Error : "Unauthorized user", + } + response(rw, http.StatusUnauthorized, error) + return + } + + productID, err := strconv.Atoi(req.URL.Query()["productID"][0]) + if err != nil { + logger.WithField("err", err.Error()).Error("product_id is missing") + error := errorResponse { + Error : "product_id missing", + } + response(rw, http.StatusBadRequest, error) + return + } + + rowsAffected, err := deps.Store.DeleteFromCart(req.Context(), int(cartID), productID) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while removing from cart") + error := errorResponse { + Error : "could not remove item", + } + response(rw, http.StatusInternalServerError, error) + return + } + + if rowsAffected != 1 { + success := successResponse { + Message : "zero rows affected", + } + response(rw, http.StatusOK, success) + return + } + + success := successResponse{ + Message: "Item removed successfully", + } + response(rw, http.StatusOK, success) + }) +} + +func updateIntoCartHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + authToken := req.Header["Token"] + cartID, _, err := getDataFromToken(authToken[0]) + if err != nil { + logger.WithField("err", err.Error()).Error("Unauthorized user") + error := errorResponse { + Error : "Unauthorized user", + } + response(rw, http.StatusUnauthorized, error) + return + } + + productID, err := strconv.Atoi(req.URL.Query()["productID"][0]) + if err != nil { + logger.WithField("err", err.Error()).Error("product_id is missing") + error := errorResponse { + Error : "product_id missing", + } + response(rw, http.StatusBadRequest, error) + return + } + + quantity, err := strconv.Atoi(req.URL.Query()["quantity"][0]) + if err != nil { + logger.WithField("err", err.Error()).Error("quantity is missing") + error := errorResponse { + Error : "quantity missing", + } + response(rw, http.StatusBadRequest, error) + return + } + + rowsAffected, err := deps.Store.UpdateIntoCart(req.Context(), quantity, int(cartID), productID) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while updating to cart") + error := errorResponse { + Error : "could not update quantity", + } + response(rw, http.StatusInternalServerError, error) + return + } + + if rowsAffected != 1 { + success := successResponse { + Message : "zero rows affected", + } + response(rw, http.StatusOK, success) + return + } + + success := successResponse{ + Message : "Quantity updated successfully", + } + response(rw, http.StatusOK, success) + }) +} \ No newline at end of file diff --git a/service/dependencies.go b/service/dependencies.go index 431979b..071be44 100644 --- a/service/dependencies.go +++ b/service/dependencies.go @@ -2,6 +2,7 @@ package service import "joshsoftware/go-e-commerce/db" +//Dependencies Structure type Dependencies struct { Store db.Storer // define other service dependencies diff --git a/service/ping_http.go b/service/ping_http.go index d30d164..3689ac6 100644 --- a/service/ping_http.go +++ b/service/ping_http.go @@ -7,6 +7,7 @@ import ( logger "github.com/sirupsen/logrus" ) +//PingResponse Struct type PingResponse struct { Message string `json:"message"` } @@ -20,6 +21,6 @@ func pingHandler(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusInternalServerError) } - rw.Header().Add("Content-Type", "application/json") + rw.Header().Set("Content-Type", "application/json") rw.Write(respBytes) } diff --git a/service/responce.go b/service/responce.go new file mode 100644 index 0000000..ea62096 --- /dev/null +++ b/service/responce.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..90bc1fe 100644 --- a/service/router.go +++ b/service/router.go @@ -2,18 +2,16 @@ package service import ( "fmt" - "net/http" - - "joshsoftware/go-e-commerce/config" - "github.com/gorilla/mux" + "joshsoftware/go-e-commerce/config" + "net/http" ) const ( versionHeader = "Accept" ) -/* The routing mechanism. Mux helps us define handler functions and the access methods */ +/*InitRouter is The routing mechanism. Mux helps us define handler functions and the access methods */ func InitRouter(deps Dependencies) (router *mux.Router) { router = mux.NewRouter() @@ -23,6 +21,60 @@ 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 Get User from ID + router.Handle("/user", jwtMiddleWare(getUserHandler(deps), deps)).Methods(http.MethodGet).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) + //routes for cart operations + router.HandleFunc("/cart", addToCartHandler(deps)).Methods(http.MethodPost).Headers(versionHeader, v1) + router.HandleFunc("/cart", deleteFromCartHandler(deps)).Methods(http.MethodDelete).Headers(versionHeader, v1) + router.HandleFunc("/cart", updateIntoCartHandler(deps)).Methods(http.MethodPut).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.Get("Token") + + //Checking if token not present in header + if len(authToken) < 1 { + responses(rw, http.StatusUnauthorized, errorResponse{ + Error: messageObject{ + Message: "Missing Authorization Token", + }, + }) + return + } + + _, _, err := getDataFromToken(authToken) + if err != nil { + responses(rw, http.StatusUnauthorized, errorResponse{ + Error: messageObject{ + Message: "Unauthorized User", + }, + }) + return + } + + //Fetching Status of Token Being Blacklisted or Not + // Unauthorized User if Token BlackListed + if isBlacklisted, _ := deps.Store.CheckBlacklistedToken(req.Context(), authToken); isBlacklisted { + responses(rw, http.StatusUnauthorized, errorResponse{ + Error: messageObject{ + Message: "Unauthorized User", + }, + }) + return + } + endpoint.ServeHTTP(rw, req) + }) +} diff --git a/service/session_http.go b/service/session_http.go new file mode 100644 index 0000000..24c4e24 --- /dev/null +++ b/service/session_http.go @@ -0,0 +1,168 @@ +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") + responses(rw, http.StatusBadRequest, errorResponse{ + Error: messageObject{ + Message: "JSON Decoding Failed", + }, + }) + 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") + responses(rw, http.StatusUnauthorized, errorResponse{ + Error: messageObject{ + Message: "Invalid Credentials", + }, + }) + 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 { + responses(rw, http.StatusInternalServerError, errorResponse{ + Error: messageObject{ + Message: "Token Generation Failure", + }, + }) + return + } + + responses(rw, http.StatusOK, successResponse{ + Data: authBody{ + Message: "Login Successfull", + Token: token, + }, + }) + }) +} + +//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.Get("Token") + + //fetching details from the token + userID, expirationTimeStamp, err := getDataFromToken(authToken) + if err != nil { + responses(rw, http.StatusUnauthorized, errorResponse{ + Error: messageObject{ + Message: "Unauthorized User", + }, + }) + 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, + } + + err = deps.Store.CreateBlacklistedToken(req.Context(), userBlackListedToken) + if err != nil { + ae.Error(ae.ErrFailedToCreate, "Error creating blaclisted token record", err) + responses(rw, http.StatusInternalServerError, errorResponse{ + Error: messageObject{ + Message: "Internal Server Error", + }, + }) + return + } + responses(rw, http.StatusOK, successResponse{ + Data: messageObject{ + Message: "Logged Out Successfully", + }, + }) + return + }) +} + +func getDataFromToken(Token string) (userID float64, expirationTime int64, err error) { + mySigningKey := config.JWTKey() + + 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 while parsing the token") + } + 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..7638e58 100644 --- a/service/user_http.go +++ b/service/user_http.go @@ -1,35 +1,62 @@ package service import ( - "encoding/json" - "net/http" - logger "github.com/sirupsen/logrus" + "joshsoftware/go-e-commerce/db" + "net/http" ) -// @Title listUsers -// @Description list all User -// @Router /users [get] -// @Accept json -// @Success 200 {object} -// @Failure 400 {object} +//listUsersHandler function fetch all users from database +// and return as json object func listUsersHandler(deps Dependencies) http.HandlerFunc { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { users, err := deps.Store.ListUsers(req.Context()) if err != nil { logger.WithField("err", err.Error()).Error("Error fetching data") - rw.WriteHeader(http.StatusInternalServerError) + responses(rw, http.StatusInternalServerError, errorResponse{ + Error: messageObject{ + Message: "Internal Server Error", + }, + }) + return + } + + responses(rw, http.StatusOK, successResponse{ + Data: users, + }) + }) +} + +//listUsersHandler function fetch specific user from database +// and return as json object +func getUserHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + //fetch usedId from request + authToken := req.Header.Get("Token") + userID, _, err := getDataFromToken(authToken) + if err != nil { + responses(rw, http.StatusUnauthorized, errorResponse{ + Error: messageObject{ + Message: "Unauthorized User", + }, + }) return } - respBytes, err := json.Marshal(users) + user := db.User{} + user, err = deps.Store.GetUser(req.Context(), int(userID)) if err != nil { - logger.WithField("err", err.Error()).Error("Error marshaling users data") - rw.WriteHeader(http.StatusInternalServerError) + logger.WithField("err", err.Error()).Error("Error fetching data") + responses(rw, http.StatusInternalServerError, errorResponse{ + Error: messageObject{ + Message: "Internal Server Error", + }, + }) return } - rw.Header().Add("Content-Type", "application/json") - rw.Write(respBytes) + responses(rw, http.StatusOK, successResponse{ + Data: user, + }) }) }