diff --git a/.env_rename_me b/.env_rename_me new file mode 100644 index 0000000..b453a5d --- /dev/null +++ b/.env_rename_me @@ -0,0 +1,11 @@ +ENV=LOCAL +PORT=9000 +SSL=TRUE +DB_USER="postgres" +DB_PASS="postgres" +DB_NAME="golang_gin_db" +ACCESS_SECRET="ashasdjhjhjadhasdaa123" +REFERSH_SECRET="hjsajdhkjhf41jhagggdga" +REDIS_SECRET="hjfhjhasdfkyuy2" +REDIS_HOST=127.0.0.1:6379 +REDIS_PASSWORD= \ No newline at end of file diff --git a/.gitignore b/.gitignore index e69de29..36fcc86 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,16 @@ + +# OS generated files # +###################### +.DS_Store +.DS_Store? + +# Logs # +###################### +*.log + +# SENSITVE DATA # +.env +cert/ + +# BUILD # +./gin-boilerplate \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index f6ab5da..5c0669a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ language: go go: - - 1.10.x - 1.11.x + - 1.12.x + - 1.13.x + - 1.14.x - master services: @@ -11,15 +13,17 @@ services: dist: trusty addons: - postgresql: "9.6" + postgresql: "12" apt: sources: - - ubuntu-toolchain-r-test + - ubuntu-toolchain-r-test packages: - - g++-4.8 - - gcc-4.8 - - clang - - postgresql-9.6-postgis-2.3 + - g++-4.8 + - gcc-4.8 + - clang + - postgresql-12 + - postgresql-client-12 + - postgresql-server-dev-12 before_install: - "psql -c 'create database golang_gin_db;' -U postgres" @@ -28,8 +32,10 @@ before_install: - sleep 3 install: - - go get -t -v ./... - - go get github.com/bmizerany/assert + - go mod init + - go list -m all + - mv .env_rename_me .env + - mkdir cert/ && sh generate-certificate.sh script: - - go test -v ./tests/* + - go test -v ./tests/* diff --git a/README.md b/README.md index c7288a0..9671e5e 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,23 @@ [![Build Status](https://travis-ci.org/Massad/gin-boilerplate.svg?branch=master)](https://travis-ci.org/Massad/gin-boilerplate) [![Join the chat at https://gitter.im/Massad/gin-boilerplate](https://badges.gitter.im/Massad/gin-boilerplate.svg)](https://gitter.im/Massad/gin-boilerplate?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Welcome to **Golang Gin boilerplate**! +Welcome to **Golang Gin boilerplate** v2 -The fastest way to deploy a restful api's with [Gin Framework](https://gin-gonic.github.io/gin/) with a structured project that defaults to **PostgreSQL** database and **Redis** as the session storage. +The fastest way to deploy a restful api's with [Gin Framework](https://gin-gonic.github.io/gin/) with a structured project that defaults to **PostgreSQL** database and **JWT** authentication middleware stored in **Redis** ## Configured with -* [go-gorp](https://github.com/go-gorp/gorp): Go Relational Persistence -* [RedisStore](https://github.com/gin-gonic/contrib/tree/master/sessions): Gin middleware for session management with multi-backend support (currently cookie, Redis). -* Built-in **CORS Middleware** -* Feature **PostgreSQL 9.6** JSON queries -* Unit test +- [go-gorp](https://github.com/go-gorp/gorp): Go Relational Persistence +- [jwt-go](github.com/dgrijalva/jwt-go): JSON Web Tokens (JWT) as middleware +- [go-redis](https://github.com/go-redis/redis): Redis support for Go +- Go Modules +- Built-in **CORS Middleware** +- Built-in **RequestID Middleware** +- Feature **PostgreSQL 12** with JSON/JSONB queries & trigger functions +- SSL Support +- Enviroment support +- Unit test +- And few other important utilties to kickstart any project ### Installation @@ -26,23 +32,52 @@ $ cd $GOPATH/src/github.com/Massad/gin-boilerplate ``` ``` -$ go get -t -v ./... +$ go mod init ``` -> Sometimes you need to get this package manually ``` -$ go get github.com/bmizerany/assert +$ go list -m all ``` You will find the **database.sql** in `db/database.sql` And you can import the postgres database using this command: + ``` $ psql -U postgres -h localhost < ./db/database.sql ``` +Tip: + +You will find that we added 2 trigger functions to the dabatase: + +- public.created_at_column() +- public.update_at_column() + +Those are added to the `updated_at` and `created_at` columns to update the latest timestamp automatically in both **user** and **article** tables. You can explore the tables and public schema for more info. + ## Running Your Application +Rename .env_rename_me to .env and place your credentials + +``` +$ mv .env_rename_me .env +``` + +Generate SSL certificates (Optional) + +> If you don't SSL now, change `SSL=TRUE` to `SSL=FALSE` in the `.env` file + +``` +$ mkdir cert/ +``` + +``` +$ sh generate-certificate.sh +``` + +> Make sure to change the values in .env for your databases + ``` $ go run *.go ``` @@ -63,19 +98,66 @@ $ ./gin-boilerplate $ go test -v ./tests/* ``` - ## Import Postman Collection (API's) + You can import from this [link](https://www.getpostman.com/collections/ac0680f90961bafd5de7). If you don't have **Postman**, check this link [https://www.getpostman.com](https://www.getpostman.com/) +Includes the following: + +- User + - Login + - Register + - Logout +- Article + - Create + - Update + - Get Article + - Get Articles + - Delete +- Auth + - Refresh Token + +Tip: Add the generated `access_token` from the success login in the **global variable** for later use in other requests in the "**Tests**" tab in **Login** reqeust: + +``` +pm.test("Status code is 200", function () { + pm.response.to.have.status(200); + + var jsonData = JSON.parse(responseBody); + pm.globals.set("token", jsonData.token.access_token); + pm.globals.set("refresh_token", jsonData.token.refresh_token); + +}); +``` + +And in each request that needs to be authenticated add the following **Authorization**: + + Authorization -> Bearer Token with value of {{token}} + +In this way, whenever you hit login from Postman it will automatically take the `access_token` and fills it in the golbal variable for later use. And of course, it will update it whenever you hit login again. + +## Version 1 + + No longer supported + +You will find the last update on v1 in [v1-session-cookies-auth](https://github.com/Massad/gin-boilerplate/tree/v1-session-cookies-auth) branch or [v1.0.5 release](https://github.com/Massad/gin-boilerplate/releases/tag/1.05) that supported the authentication using the **session** and **cookies** stored in **Redis** if needed. + +- [RedisStore](https://github.com/gin-gonic/contrib/tree/master/sessions): Gin middleware for session management with multi-backend support (currently cookie, Redis). + ## Contribution You are welcome to contribute to keep it up to date and always improving! If you have any question or need help, drop a message at [https://gitter.im/Massad/gin-boilerplate](https://gitter.im/Massad/gin-boilerplate) +## Credit + +The implemented JWT inspired from this article: [Using JWT for Authentication in a Golang Application](https://www.nexmo.com/blog/2020/03/13/using-jwt-for-authentication-in-a-golang-application-dr) worth reading it, thanks [Victor Steven](https://www.nexmo.com/blog/author/victor-steven) + --- ## License + (The MIT License) Permission is hereby granted, free of charge, to any person obtaining diff --git a/controllers/article.go b/controllers/article.go index 3892a4c..d262683 100644 --- a/controllers/article.go +++ b/controllers/article.go @@ -18,134 +18,114 @@ var articleModel = new(models.ArticleModel) //Create ... func (ctrl ArticleController) Create(c *gin.Context) { - userID := getUserID(c) - - if userID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"message": "Please login first"}) - c.Abort() - return - } + if userID := getUserID(c); userID != 0 { + var articleForm forms.ArticleForm - var articleForm forms.ArticleForm + if c.ShouldBindJSON(&articleForm) != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid form"}) + c.Abort() + return + } - if c.ShouldBindJSON(&articleForm) != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid form", "form": articleForm}) - c.Abort() - return - } + articleID, err := articleModel.Create(userID, articleForm) - articleID, err := articleModel.Create(userID, articleForm) + if articleID == 0 && err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"message": "Article could not be created", "error": err.Error()}) + c.Abort() + return + } - if articleID > 0 && err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"message": "Article could not be created", "error": err.Error()}) - c.Abort() - return + c.JSON(http.StatusOK, gin.H{"message": "Article created", "id": articleID}) } - - c.JSON(http.StatusOK, gin.H{"message": "Article created", "id": articleID}) } //All ... func (ctrl ArticleController) All(c *gin.Context) { - userID := getUserID(c) + if userID := getUserID(c); userID != 0 { - if userID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"message": "Please login first"}) - c.Abort() - return - } + data, err := articleModel.All(userID) - data, err := articleModel.All(userID) + if err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"Message": "Could not get articles", "error": err.Error()}) + c.Abort() + return + } - if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"Message": "Could not get the articles", "error": err.Error()}) - c.Abort() - return + c.JSON(http.StatusOK, gin.H{"data": data}) } - - c.JSON(http.StatusOK, gin.H{"data": data}) } //One ... func (ctrl ArticleController) One(c *gin.Context) { - userID := getUserID(c) + if userID := getUserID(c); userID != 0 { - if userID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"message": "Please login first"}) - c.Abort() - return - } + id := c.Param("id") + if id, err := strconv.ParseInt(id, 10, 64); err == nil { - id := c.Param("id") + data, err := articleModel.One(userID, id) - if id, err := strconv.ParseInt(id, 10, 64); err == nil { + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"Message": "Article not found", "error": err.Error()}) + c.Abort() + return + } - data, err := articleModel.One(userID, id) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"Message": "Article not found", "error": err.Error()}) - c.Abort() - return + c.JSON(http.StatusOK, gin.H{"data": data}) + + } else { + c.JSON(http.StatusNotFound, gin.H{"Message": "Invalid parameter"}) } - c.JSON(http.StatusOK, gin.H{"data": data}) - } else { - c.JSON(http.StatusNotFound, gin.H{"Message": "Invalid parameter"}) } } //Update ... func (ctrl ArticleController) Update(c *gin.Context) { - userID := getUserID(c) + if userID := getUserID(c); userID != 0 { - if userID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"message": "Please login first"}) - c.Abort() - return - } + id := c.Param("id") + if id, err := strconv.ParseInt(id, 10, 64); err == nil { - id := c.Param("id") - if id, err := strconv.ParseInt(id, 10, 64); err == nil { + var articleForm forms.ArticleForm - var articleForm forms.ArticleForm + if c.ShouldBindJSON(&articleForm) != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid form"}) + c.Abort() + return + } - if c.ShouldBindJSON(&articleForm) != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid parameters", "form": articleForm}) - c.Abort() - return - } + err := articleModel.Update(userID, id, articleForm) + if err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"Message": "Article could not be updated", "error": err.Error()}) + c.Abort() + return + } - err := articleModel.Update(userID, id, articleForm) - if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"Message": "Article could not be updated", "error": err.Error()}) - c.Abort() - return + c.JSON(http.StatusOK, gin.H{"message": "Article updated"}) + + } else { + c.JSON(http.StatusNotFound, gin.H{"Message": "Invalid parameter", "error": err.Error()}) } - c.JSON(http.StatusOK, gin.H{"message": "Article updated"}) - } else { - c.JSON(http.StatusNotFound, gin.H{"Message": "Invalid parameter", "error": err.Error()}) } } //Delete ... func (ctrl ArticleController) Delete(c *gin.Context) { - userID := getUserID(c) + if userID := getUserID(c); userID != 0 { - if userID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"message": "Please login first"}) - c.Abort() - return - } + id := c.Param("id") + if id, err := strconv.ParseInt(id, 10, 64); err == nil { - id := c.Param("id") - if id, err := strconv.ParseInt(id, 10, 64); err == nil { + err := articleModel.Delete(userID, id) + if err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"Message": "Article could not be deleted", "error": err.Error()}) + c.Abort() + return + } - err := articleModel.Delete(userID, id) - if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"Message": "Article could not be deleted", "error": err.Error()}) - c.Abort() - return + c.JSON(http.StatusOK, gin.H{"message": "Article deleted"}) + + } else { + c.JSON(http.StatusNotFound, gin.H{"Message": "Invalid parameter"}) } - c.JSON(http.StatusOK, gin.H{"message": "Article deleted"}) - } else { - c.JSON(http.StatusNotFound, gin.H{"Message": "Invalid parameter"}) } } diff --git a/controllers/auth.go b/controllers/auth.go new file mode 100644 index 0000000..3276854 --- /dev/null +++ b/controllers/auth.go @@ -0,0 +1,98 @@ +package controllers + +import ( + "fmt" + "net/http" + "os" + "strconv" + + "github.com/Massad/gin-boilerplate/forms" + "github.com/Massad/gin-boilerplate/models" + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" +) + +//AuthController ... +type AuthController struct{} + +var authModel = new(models.AuthModel) + +//TokenValid ... +func (ctl AuthController) TokenValid(c *gin.Context) { + err := authModel.TokenValid(c.Request) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization, please login again"}) + c.Abort() + return + } +} + +//Refresh ... +func (ctl AuthController) Refresh(c *gin.Context) { + var tokenForm forms.Token + + if c.ShouldBindJSON(&tokenForm) != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid form", "form": tokenForm}) + c.Abort() + return + } + + //verify the token + token, err := jwt.Parse(tokenForm.RefreshToken, func(token *jwt.Token) (interface{}, error) { + //Make sure that the token method conform to "SigningMethodHMAC" + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(os.Getenv("REFRESH_SECRET")), nil + }) + //if there is an error, the token must have expired + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization, please login again"}) + return + } + //is token valid? + if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization, please login again"}) + return + } + //Since token is valid, get the uuid: + claims, ok := token.Claims.(jwt.MapClaims) //the token claims should conform to MapClaims + if ok && token.Valid { + refreshUUID, ok := claims["refresh_uuid"].(string) //convert the interface to string + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization, please login again"}) + return + } + userID, err := strconv.ParseInt(fmt.Sprintf("%.f", claims["user_id"]), 10, 64) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization, please login again"}) + return + } + //Delete the previous Refresh Token + deleted, delErr := authModel.DeleteAuth(refreshUUID) + if delErr != nil || deleted == 0 { //if any goes wrong + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization, please login again"}) + return + } + + //Create new pairs of refresh and access tokens + ts, createErr := authModel.CreateToken(userID) + if createErr != nil { + c.JSON(http.StatusForbidden, gin.H{"message": "Invalid authorization, please login again"}) + return + } + //save the tokens metadata to redis + saveErr := authModel.CreateAuth(userID, ts) + if saveErr != nil { + c.JSON(http.StatusForbidden, gin.H{"message": "Invalid authorization, please login again"}) + return + } + tokens := map[string]string{ + "access_token": ts.AccessToken, + "refresh_token": ts.RefreshToken, + } + c.JSON(http.StatusOK, tokens) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid authorization, please login again"}) + } +} diff --git a/controllers/user.go b/controllers/user.go index 4d55c6c..a999230 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -6,7 +6,6 @@ import ( "net/http" - "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" ) @@ -16,63 +15,52 @@ type UserController struct{} var userModel = new(models.UserModel) //getUserID ... -func getUserID(c *gin.Context) int64 { - session := sessions.Default(c) - userID := session.Get("user_id") - if userID != nil { - return models.ConvertToInt64(userID) - } - return 0 -} +func getUserID(c *gin.Context) (userID int64) { -//getSessionUserInfo ... -func getSessionUserInfo(c *gin.Context) (userSessionInfo models.UserSessionInfo) { - session := sessions.Default(c) - userID := session.Get("user_id") - if userID != nil { - userSessionInfo.ID = models.ConvertToInt64(userID) - userSessionInfo.Name = session.Get("user_name").(string) - userSessionInfo.Email = session.Get("user_email").(string) + tokenAuth, err := authModel.ExtractTokenMetadata(c.Request) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Please login first."}) + return 0 } - return userSessionInfo + userID, err = authModel.FetchAuth(tokenAuth) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Please login first."}) + return 0 + } + + return userID } -//Signin ... -func (ctrl UserController) Signin(c *gin.Context) { - var signinForm forms.SigninForm +//Login ... +func (ctrl UserController) Login(c *gin.Context) { + var loginForm forms.LoginForm - if c.ShouldBindJSON(&signinForm) != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid form", "form": signinForm}) + if c.ShouldBindJSON(&loginForm) != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid form"}) c.Abort() return } - user, err := userModel.Signin(signinForm) + user, token, err := userModel.Login(loginForm) if err == nil { - session := sessions.Default(c) - session.Set("user_id", user.ID) - session.Set("user_email", user.Email) - session.Set("user_name", user.Name) - session.Save() - - c.JSON(http.StatusOK, gin.H{"message": "User signed in", "user": user}) + c.JSON(http.StatusOK, gin.H{"message": "User signed in", "user": user, "token": token}) } else { - c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid signin details", "error": err.Error()}) + c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid login details", "error": err.Error()}) } } -//Signup ... -func (ctrl UserController) Signup(c *gin.Context) { - var signupForm forms.SignupForm +//Register ... +func (ctrl UserController) Register(c *gin.Context) { + var registerForm forms.RegisterForm - if c.ShouldBindJSON(&signupForm) != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid form", "form": signupForm}) + if c.ShouldBindJSON(®isterForm) != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"message": "Invalid form"}) c.Abort() return } - user, err := userModel.Signup(signupForm) + user, err := userModel.Register(registerForm) if err != nil { c.JSON(http.StatusNotAcceptable, gin.H{"message": err.Error()}) @@ -81,22 +69,26 @@ func (ctrl UserController) Signup(c *gin.Context) { } if user.ID > 0 { - session := sessions.Default(c) - session.Set("user_id", user.ID) - session.Set("user_email", user.Email) - session.Set("user_name", user.Name) - session.Save() - c.JSON(http.StatusOK, gin.H{"message": "Success signup", "user": user}) + c.JSON(http.StatusOK, gin.H{"message": "Successfully registered", "user": user}) } else { - c.JSON(http.StatusNotAcceptable, gin.H{"message": "Could not signup this user", "error": err.Error()}) + c.JSON(http.StatusNotAcceptable, gin.H{"message": "Could not register this user", "error": err.Error()}) } } -//Signout ... -func (ctrl UserController) Signout(c *gin.Context) { - session := sessions.Default(c) - session.Clear() - session.Save() - c.JSON(http.StatusOK, gin.H{"message": "Signed out..."}) +//Logout ... +func (ctrl UserController) Logout(c *gin.Context) { + + au, err := authModel.ExtractTokenMetadata(c.Request) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "User not logged in"}) + return + } + deleted, delErr := authModel.DeleteAuth(au.AccessUUID) + if delErr != nil || deleted == 0 { //if any goes wrong + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid request"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Successfully logged out"}) } diff --git a/db/db.go b/db/db.go index ed27801..5b2eb5f 100644 --- a/db/db.go +++ b/db/db.go @@ -4,8 +4,11 @@ import ( "database/sql" "fmt" "log" + "os" + "strconv" "github.com/go-gorp/gorp" + _redis "github.com/go-redis/redis/v7" _ "github.com/lib/pq" //import postgres ) @@ -14,22 +17,12 @@ type DB struct { *sql.DB } -const ( - //DbUser ... - DbUser = "postgres" - //DbPassword ... - DbPassword = "postgres" - //DbName ... - DbName = "golang_gin_db" -) - var db *gorp.DbMap //Init ... func Init() { - dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", - DbUser, DbPassword, DbName) + dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", os.Getenv("DB_USER"), os.Getenv("DB_PASS"), os.Getenv("DB_NAME")) var err error db, err = ConnectDB(dbinfo) @@ -57,3 +50,26 @@ func ConnectDB(dataSourceName string) (*gorp.DbMap, error) { func GetDB() *gorp.DbMap { return db } + +//RedisClient ... +var RedisClient *_redis.Client + +//InitRedis ... +func InitRedis(params ...string) { + + var redisHost = os.Getenv("REDIS_HOST") + var redisPassword = os.Getenv("REDIS_PASSWORD") + + db, _ := strconv.Atoi(params[0]) + + RedisClient = _redis.NewClient(&_redis.Options{ + Addr: redisHost, + Password: redisPassword, + DB: db, + }) +} + +//GetRedis ... +func GetRedis() *_redis.Client { + return RedisClient +} diff --git a/forms/auth.go b/forms/auth.go new file mode 100644 index 0000000..40ecfe6 --- /dev/null +++ b/forms/auth.go @@ -0,0 +1,6 @@ +package forms + +//Token ... +type Token struct { + RefreshToken string `form:"refresh_token" json:"refresh_token" binding:"required"` +} diff --git a/forms/user.go b/forms/user.go index fa11f68..286fa84 100644 --- a/forms/user.go +++ b/forms/user.go @@ -1,13 +1,13 @@ package forms -//SigninForm ... -type SigninForm struct { +//LoginForm ... +type LoginForm struct { Email string `form:"email" json:"email" binding:"required,email"` Password string `form:"password" json:"password" binding:"required"` } -//SignupForm ... -type SignupForm struct { +//RegisterForm ... +type RegisterForm struct { Name string `form:"name" json:"name" binding:"required,max=100"` Email string `form:"email" json:"email" binding:"required,email"` Password string `form:"password" json:"password" binding:"required"` diff --git a/generate-certificate.sh b/generate-certificate.sh new file mode 100755 index 0000000..8bc4b02 --- /dev/null +++ b/generate-certificate.sh @@ -0,0 +1,18 @@ +#!/usr/bin/bash + +cd cert/ + +ip=$(ifconfig | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p') +echo $ip + +openssl genrsa -out myCA.key 2048 + +openssl req -x509 -new -key myCA.key -out myCA.cer -days 730 -subj /CN=$ip + +openssl genrsa -out mycert1.key 2048 + +openssl req -new -out mycert1.req -key mycert1.key -subj /CN=$ip + +openssl x509 -req -in mycert1.req -out mycert1.cer -CAkey myCA.key -CA myCA.cer -days 365 -CAcreateserial -CAserial serial + +cd ../ \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c1b367c --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module github.com/Massad/gin-boilerplate + +go 1.14 + +require ( + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/fatih/color v1.9.0 // indirect + github.com/gin-contrib/gzip v0.0.1 + github.com/gin-gonic/gin v1.6.3 + github.com/go-gorp/gorp v2.2.0+incompatible + github.com/go-redis/redis/v7 v7.2.0 + github.com/joho/godotenv v1.3.0 + github.com/lib/pq v1.5.2 + github.com/myesui/uuid v1.0.0 // indirect + github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8 // indirect + github.com/twinj/uuid v1.0.0 + golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 + gopkg.in/stretchr/testify.v1 v1.2.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f05d502 --- /dev/null +++ b/go.sum @@ -0,0 +1,128 @@ +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +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 v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM= +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/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc= +github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-gorp/gorp v1.7.2 h1:C5uGH8zK2qjMJZGC308ZegdGXMrMjYmA++IIMeKSKnc= +github.com/go-gorp/gorp v2.2.0+incompatible h1:xAUh4QgEeqPPhK3vxZN+bzrim1z5Av6q837gtjUlshc= +github.com/go-gorp/gorp v2.2.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= +github.com/go-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs= +github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw= +github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= +github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= +github.com/nelsam/hel v1.0.1 h1:0DjDg+MCRH5z0nh4rVN4sG4y7Tvfo7n+lbKtCN9MN4k= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8 h1:9l4ZflKvAPvgJy4S4U993X/WL8uqVMf7CkESXG/1ujA= +github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk= +github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88= +golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= +gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 464510d..ca9aebf 100644 --- a/main.go +++ b/main.go @@ -2,17 +2,22 @@ package main import ( "fmt" + "log" "net/http" + "os" "runtime" "github.com/Massad/gin-boilerplate/controllers" "github.com/Massad/gin-boilerplate/db" + "github.com/gin-contrib/gzip" + "github.com/joho/godotenv" + uuid "github.com/twinj/uuid" - "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" ) //CORSMiddleware ... +//CORS (Cross-Origin Resource Sharing) func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost") @@ -31,33 +36,73 @@ func CORSMiddleware() gin.HandlerFunc { } } +//RequestIDMiddleware ... +//Generate a unique ID and attach it to each request for future reference or use +func RequestIDMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + uuid := uuid.NewV4() + c.Writer.Header().Set("X-Request-Id", uuid.String()) + c.Next() + } +} + +var auth = new(controllers.AuthController) + +//TokenAuthMiddleware ... +//JWT Authentication middleware attached to each request that needs to be authenitcated to validate the access_token in the header +func TokenAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + auth.TokenValid(c) + c.Next() + } +} + func main() { + + //Start the default gin server r := gin.Default() - store, _ := sessions.NewRedisStore(10, "tcp", "localhost:6379", "", []byte("secret")) - r.Use(sessions.Sessions("gin-boilerplate-session", store)) + //Load the .env file + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file, please create one in the root directory") + } r.Use(CORSMiddleware()) + r.Use(RequestIDMiddleware()) + r.Use(gzip.Gzip(gzip.DefaultCompression)) + //Start PostgreSQL database + //Example: db.GetDB() - More info in the models folder db.Init() + //Start Redis on database 1 - it's used to store the JWT but you can use it for anythig else + //Example: db.GetRedis().Set(KEY, VALUE, at.Sub(now)).Err() + db.InitRedis("1") + v1 := r.Group("/v1") { /*** START USER ***/ user := new(controllers.UserController) - v1.POST("/user/signin", user.Signin) - v1.POST("/user/signup", user.Signup) - v1.GET("/user/signout", user.Signout) + v1.POST("/user/login", user.Login) + v1.POST("/user/register", user.Register) + v1.GET("/user/logout", user.Logout) + + /*** START AUTH ***/ + auth := new(controllers.AuthController) + + //Rerfresh the token when needed to generate new access_token and refresh_token for the user + v1.POST("/token/refresh", auth.Refresh) /*** START Article ***/ article := new(controllers.ArticleController) - v1.POST("/article", article.Create) - v1.GET("/articles", article.All) - v1.GET("/article/:id", article.One) - v1.PUT("/article/:id", article.Update) - v1.DELETE("/article/:id", article.Delete) + v1.POST("/article", TokenAuthMiddleware(), article.Create) + v1.GET("/articles", TokenAuthMiddleware(), article.All) + v1.GET("/article/:id", TokenAuthMiddleware(), article.One) + v1.PUT("/article/:id", TokenAuthMiddleware(), article.Update) + v1.DELETE("/article/:id", TokenAuthMiddleware(), article.Delete) } r.LoadHTMLGlob("./public/html/*") @@ -75,5 +120,27 @@ func main() { c.HTML(404, "404.html", gin.H{}) }) - r.Run(":9000") + fmt.Println("SSL", os.Getenv("SSL")) + port := os.Getenv("PORT") + + if os.Getenv("ENV") == "PRODUCTION" { + gin.SetMode(gin.ReleaseMode) + } + + if os.Getenv("SSL") == "TRUE" { + + SSLKeys := &struct { + CERT string + KEY string + }{} + + //Generated using sh generate-certificate.sh + SSLKeys.CERT = "./cert/myCA.cer" + SSLKeys.KEY = "./cert/myCA.key" + + r.RunTLS(":"+port, SSLKeys.CERT, SSLKeys.KEY) + } else { + r.Run(":" + port) + } + } diff --git a/models/article.go b/models/article.go index 69d4150..aff772f 100644 --- a/models/article.go +++ b/models/article.go @@ -2,7 +2,6 @@ package models import ( "errors" - "time" "github.com/Massad/gin-boilerplate/db" "github.com/Massad/gin-boilerplate/forms" @@ -24,24 +23,7 @@ type ArticleModel struct{} //Create ... func (m ArticleModel) Create(userID int64, form forms.ArticleForm) (articleID int64, err error) { - getDb := db.GetDB() - - userModel := new(UserModel) - - checkUser, err := userModel.One(userID) - - if err != nil && checkUser.ID > 0 { - return 0, errors.New("User doesn't exist") - } - - _, err = getDb.Exec("INSERT INTO public.article(user_id, title, content, updated_at, created_at) VALUES($1, $2, $3, $4, $5) RETURNING id", userID, form.Title, form.Content, time.Now().Unix(), time.Now().Unix()) - - if err != nil { - return 0, err - } - - articleID, err = getDb.SelectInt("SELECT id FROM public.article WHERE user_id=$1 ORDER BY id DESC LIMIT 1", userID) - + err = db.GetDB().QueryRow("INSERT INTO public.article(user_id, title, content) VALUES($1, $2, $3) RETURNING id", userID, form.Title, form.Content).Scan(&articleID) return articleID, err } @@ -65,7 +47,7 @@ func (m ArticleModel) Update(userID int64, id int64, form forms.ArticleForm) (er return errors.New("Article not found") } - _, err = db.GetDB().Exec("UPDATE public.article SET title=$1, content=$2, updated_at=$3 WHERE id=$4", form.Title, form.Content, time.Now().Unix(), id) + _, err = db.GetDB().Exec("UPDATE public.article SET title=$2, content=$3 WHERE id=$1", id, form.Title, form.Content) return err } diff --git a/models/auth.go b/models/auth.go new file mode 100644 index 0000000..3421201 --- /dev/null +++ b/models/auth.go @@ -0,0 +1,174 @@ +package models + +import ( + "fmt" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/Massad/gin-boilerplate/db" + "github.com/dgrijalva/jwt-go" + uuid "github.com/twinj/uuid" +) + +//TokenDetails ... +type TokenDetails struct { + AccessToken string + RefreshToken string + AccessUUID string + RefreshUUID string + AtExpires int64 + RtExpires int64 +} + +//AccessDetails ... +type AccessDetails struct { + AccessUUID string + UserID int64 +} + +//Token ... +type Token struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +//AuthModel ... +type AuthModel struct{} + +//CreateToken ... +func (m AuthModel) CreateToken(userID int64) (*TokenDetails, error) { + + td := &TokenDetails{} + td.AtExpires = time.Now().Add(time.Minute * 15).Unix() + td.AccessUUID = uuid.NewV4().String() + + td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix() + td.RefreshUUID = uuid.NewV4().String() + + var err error + //Creating Access Token + atClaims := jwt.MapClaims{} + atClaims["authorized"] = true + atClaims["access_uuid"] = td.AccessUUID + atClaims["user_id"] = userID + atClaims["exp"] = td.AtExpires + + at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) + td.AccessToken, err = at.SignedString([]byte(os.Getenv("ACCESS_SECRET"))) + if err != nil { + return nil, err + } + //Creating Refresh Token + rtClaims := jwt.MapClaims{} + rtClaims["refresh_uuid"] = td.RefreshUUID + rtClaims["user_id"] = userID + rtClaims["exp"] = td.RtExpires + rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims) + td.RefreshToken, err = rt.SignedString([]byte(os.Getenv("REFRESH_SECRET"))) + if err != nil { + return nil, err + } + return td, nil +} + +//CreateAuth ... +func (m AuthModel) CreateAuth(userid int64, td *TokenDetails) error { + at := time.Unix(td.AtExpires, 0) //converting Unix to UTC(to Time object) + rt := time.Unix(td.RtExpires, 0) + now := time.Now() + + errAccess := db.GetRedis().Set(td.AccessUUID, strconv.Itoa(int(userid)), at.Sub(now)).Err() + if errAccess != nil { + return errAccess + } + errRefresh := db.GetRedis().Set(td.RefreshUUID, strconv.Itoa(int(userid)), rt.Sub(now)).Err() + if errRefresh != nil { + return errRefresh + } + return nil +} + +//ExtractToken ... +func (m AuthModel) ExtractToken(r *http.Request) string { + bearToken := r.Header.Get("Authorization") + //normally Authorization the_token_xxx + strArr := strings.Split(bearToken, " ") + if len(strArr) == 2 { + return strArr[1] + } + return "" +} + +//VerifyToken ... +func (m AuthModel) VerifyToken(r *http.Request) (*jwt.Token, error) { + tokenString := m.ExtractToken(r) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + //Make sure that the token method conform to "SigningMethodHMAC" + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(os.Getenv("ACCESS_SECRET")), nil + }) + if err != nil { + return nil, err + } + return token, nil +} + +//TokenValid ... +func (m AuthModel) TokenValid(r *http.Request) error { + token, err := m.VerifyToken(r) + if err != nil { + return err + } + if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid { + return err + } + return nil +} + +//ExtractTokenMetadata ... +func (m AuthModel) ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) { + token, err := m.VerifyToken(r) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + accessUUID, ok := claims["access_uuid"].(string) + if !ok { + return nil, err + } + userID, err := strconv.ParseInt(fmt.Sprintf("%.f", claims["user_id"]), 10, 64) + if err != nil { + return nil, err + } + return &AccessDetails{ + AccessUUID: accessUUID, + UserID: userID, + }, nil + } + return nil, err +} + +//FetchAuth ... +func (m AuthModel) FetchAuth(authD *AccessDetails) (int64, error) { + userid, err := db.GetRedis().Get(authD.AccessUUID).Result() + if err != nil { + return 0, err + } + userID, _ := strconv.ParseInt(userid, 10, 64) + return userID, nil +} + +//DeleteAuth ... +func (m AuthModel) DeleteAuth(givenUUID string) (int64, error) { + deleted, err := db.GetRedis().Del(givenUUID).Result() + if err != nil { + return 0, err + } + return deleted, nil +} diff --git a/models/user.go b/models/user.go index a35b518..d795210 100644 --- a/models/user.go +++ b/models/user.go @@ -2,7 +2,6 @@ package models import ( "errors" - "time" "github.com/Massad/gin-boilerplate/db" "github.com/Massad/gin-boilerplate/forms" @@ -12,42 +11,55 @@ import ( //User ... type User struct { - ID int `db:"id, primarykey, autoincrement" json:"id"` + ID int64 `db:"id, primarykey, autoincrement" json:"id"` Email string `db:"email" json:"email"` Password string `db:"password" json:"-"` Name string `db:"name" json:"name"` - UpdatedAt int64 `db:"updated_at" json:"updated_at"` - CreatedAt int64 `db:"created_at" json:"created_at"` + UpdatedAt int64 `db:"updated_at" json:"-"` + CreatedAt int64 `db:"created_at" json:"-"` } //UserModel ... type UserModel struct{} -//Signin ... -func (m UserModel) Signin(form forms.SigninForm) (user User, err error) { +var authModel = new(AuthModel) + +//Login ... +func (m UserModel) Login(form forms.LoginForm) (user User, token Token, err error) { err = db.GetDB().SelectOne(&user, "SELECT id, email, password, name, updated_at, created_at FROM public.user WHERE email=LOWER($1) LIMIT 1", form.Email) if err != nil { - return user, err + return user, token, err } + //Compare the password form and database if match bytePassword := []byte(form.Password) byteHashedPassword := []byte(user.Password) err = bcrypt.CompareHashAndPassword(byteHashedPassword, bytePassword) if err != nil { - return user, errors.New("Invalid password") + return user, token, errors.New("Invalid password") } - return user, nil + //Generate the JWT auth token + tokenDetails, err := authModel.CreateToken(user.ID) + + saveErr := authModel.CreateAuth(user.ID, tokenDetails) + if saveErr == nil { + token.AccessToken = tokenDetails.AccessToken + token.RefreshToken = tokenDetails.RefreshToken + } + + return user, token, nil } -//Signup ... -func (m UserModel) Signup(form forms.SignupForm) (user User, err error) { +//Register ... +func (m UserModel) Register(form forms.RegisterForm) (user User, err error) { getDb := db.GetDB() + //Check if the user exists in database checkUser, err := getDb.SelectInt("SELECT count(id) FROM public.user WHERE email=LOWER($1) LIMIT 1", form.Email) if err != nil { @@ -55,25 +67,22 @@ func (m UserModel) Signup(form forms.SignupForm) (user User, err error) { } if checkUser > 0 { - return user, errors.New("User exists") + return user, errors.New("User already exists") } bytePassword := []byte(form.Password) hashedPassword, err := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost) if err != nil { - panic(err) + panic(err) //Something really went wrong here... } - res, err := getDb.Exec("INSERT INTO public.user(email, password, name, updated_at, created_at) VALUES($1, $2, $3, $4, $5) RETURNING id", form.Email, string(hashedPassword), form.Name, time.Now().Unix(), time.Now().Unix()) + //Create the user and return back the user ID + err = getDb.QueryRow("INSERT INTO public.user(email, password, name) VALUES($1, $2, $3) RETURNING id", form.Email, string(hashedPassword), form.Name).Scan(&user.ID) - if res != nil && err == nil { - err = getDb.SelectOne(&user, "SELECT id, email, name, updated_at, created_at FROM public.user WHERE email=LOWER($1) LIMIT 1", form.Email) - if err == nil { - return user, nil - } - } + user.Name = form.Name + user.Email = form.Email - return user, errors.New("Not registered") + return user, err } //One ... diff --git a/tests/article_test.go b/tests/article_test.go index a076b1d..bfc620f 100644 --- a/tests/article_test.go +++ b/tests/article_test.go @@ -6,6 +6,7 @@ import ( "bytes" "encoding/json" "fmt" + "os" "io/ioutil" "log" @@ -16,9 +17,9 @@ import ( "github.com/Massad/gin-boilerplate/controllers" "github.com/Massad/gin-boilerplate/db" "github.com/Massad/gin-boilerplate/forms" + "github.com/joho/godotenv" "github.com/bmizerany/assert" - "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" ) @@ -26,17 +27,19 @@ func SetupRouter() *gin.Engine { r := gin.Default() gin.SetMode(gin.TestMode) - store, _ := sessions.NewRedisStore(10, "tcp", "localhost:6379", "", []byte("secret")) - r.Use(sessions.Sessions("gin-boilerplate-session", store)) - v1 := r.Group("/v1") { /*** START USER ***/ user := new(controllers.UserController) - v1.POST("/user/signin", user.Signin) - v1.POST("/user/signup", user.Signup) - v1.GET("/user/signout", user.Signout) + v1.POST("/user/login", user.Login) + v1.POST("/user/register", user.Register) + v1.GET("/user/logout", user.Logout) + + /*** START AUTH ***/ + auth := new(controllers.AuthController) + + v1.POST("/token/refresh", auth.Refresh) /*** START Article ***/ article := new(controllers.ArticleController) @@ -56,11 +59,14 @@ func main() { r.Run() } -var signinCookie string +var loginCookie string var testEmail = "test-gin-boilerplate@test.com" var testPassword = "123456" +var accessToken string +var refreshToken string + var articleID int /** @@ -70,27 +76,37 @@ var articleID int * Must pass */ func TestIntDB(t *testing.T) { + + //Load the .env file + err := godotenv.Load("../.env") + if err != nil { + log.Fatal("Error loading .env file, please create one in the root directory") + } + + fmt.Println("DB_PASS", os.Getenv("DB_PASS")) + db.Init() + db.InitRedis("1") } /** -* TestSignup +* TestRegister * Test user registration * * Must return response code 200 */ -func TestSignup(t *testing.T) { +func TestRegister(t *testing.T) { testRouter := SetupRouter() - var signupForm forms.SignupForm + var registerForm forms.RegisterForm - signupForm.Name = "testing" - signupForm.Email = testEmail - signupForm.Password = testPassword + registerForm.Name = "testing" + registerForm.Email = testEmail + registerForm.Password = testPassword - data, _ := json.Marshal(signupForm) + data, _ := json.Marshal(registerForm) - req, err := http.NewRequest("POST", "/v1/user/signup", bytes.NewBufferString(string(data))) + req, err := http.NewRequest("POST", "/v1/user/register", bytes.NewBufferString(string(data))) req.Header.Set("Content-Type", "application/json") if err != nil { @@ -104,23 +120,23 @@ func TestSignup(t *testing.T) { } /** -* TestSignupInvalidEmail +* TestRegisterInvalidEmail * Test user registration with invalid email * * Must return response code 406 */ -func TestSignupInvalidEmail(t *testing.T) { +func TestRegisterInvalidEmail(t *testing.T) { testRouter := SetupRouter() - var signupForm forms.SignupForm + var registerForm forms.RegisterForm - signupForm.Name = "testing" - signupForm.Email = "invalid@email" - signupForm.Password = testPassword + registerForm.Name = "testing" + registerForm.Email = "invalid@email" + registerForm.Password = testPassword - data, _ := json.Marshal(signupForm) + data, _ := json.Marshal(registerForm) - req, err := http.NewRequest("POST", "/v1/user/signup", bytes.NewBufferString(string(data))) + req, err := http.NewRequest("POST", "/v1/user/register", bytes.NewBufferString(string(data))) req.Header.Set("Content-Type", "application/json") if err != nil { @@ -134,23 +150,23 @@ func TestSignupInvalidEmail(t *testing.T) { } /** -* TestSignin -* Test user signin -* and store the cookie on local variable [signinCookie] +* TestLogin +* Test user login +* and get the access_token and refresh_token stored * * Must return response code 200 */ -func TestSignin(t *testing.T) { +func TestLogin(t *testing.T) { testRouter := SetupRouter() - var signinForm forms.SigninForm + var loginForm forms.LoginForm - signinForm.Email = testEmail - signinForm.Password = testPassword + loginForm.Email = testEmail + loginForm.Password = testPassword - data, _ := json.Marshal(signinForm) + data, _ := json.Marshal(loginForm) - req, err := http.NewRequest("POST", "/v1/user/signin", bytes.NewBufferString(string(data))) + req, err := http.NewRequest("POST", "/v1/user/login", bytes.NewBufferString(string(data))) req.Header.Set("Content-Type", "application/json") if err != nil { @@ -161,11 +177,64 @@ func TestSignin(t *testing.T) { testRouter.ServeHTTP(resp, req) - signinCookie = resp.Header().Get("Set-Cookie") + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + var res = &struct { + Message string `json:"message"` + User struct { + CreatedAt int64 `json:"created_at"` + Email string `json:"email"` + ID int64 `json:"id"` + Name string `json:"name"` + UpdatedAt int64 `json:"updated_at"` + } `json:"user"` + Token struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + } `json:"token"` + }{} + + json.Unmarshal(body, &res) + + accessToken = res.Token.AccessToken + refreshToken = res.Token.RefreshToken assert.Equal(t, resp.Code, http.StatusOK) } +/** +* TestInvalidLogin +* Test invalid login +* +* Must return response code 406 + */ +func TestInvalidLogin(t *testing.T) { + testRouter := SetupRouter() + + var loginForm forms.LoginForm + + loginForm.Email = "wrong@email.com" + loginForm.Password = testPassword + + data, _ := json.Marshal(loginForm) + + req, err := http.NewRequest("POST", "/v1/user/login", bytes.NewBufferString(string(data))) + req.Header.Set("Content-Type", "application/json") + + if err != nil { + fmt.Println(err) + } + + resp := httptest.NewRecorder() + + testRouter.ServeHTTP(resp, req) + + assert.Equal(t, resp.Code, http.StatusNotAcceptable) +} + /** * TestCreateArticle * Test article creation @@ -184,14 +253,13 @@ func TestCreateArticle(t *testing.T) { req, err := http.NewRequest("POST", "/v1/article", bytes.NewBufferString(string(data))) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Cookie", signinCookie) + req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", accessToken)) if err != nil { fmt.Println(err) } resp := httptest.NewRecorder() - testRouter.ServeHTTP(resp, req) body, err := ioutil.ReadAll(resp.Body) @@ -228,91 +296,105 @@ func TestCreateInvalidArticle(t *testing.T) { req, err := http.NewRequest("POST", "/v1/article", bytes.NewBufferString(string(data))) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Cookie", signinCookie) + req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", accessToken)) if err != nil { fmt.Println(err) } resp := httptest.NewRecorder() - testRouter.ServeHTTP(resp, req) + assert.Equal(t, resp.Code, http.StatusNotAcceptable) } /** -* TestCreateArticleNotSignedIn -* Test article creation with a not signed in user +* TestGetArticle +* Test getting one article * -* Must return response code 401 +* Must return response code 200 */ -func TestCreateArticleNotSignedIn(t *testing.T) { +func TestGetArticle(t *testing.T) { testRouter := SetupRouter() - var articleForm forms.ArticleForm + req, err := http.NewRequest("GET", fmt.Sprintf("/v1/article/%d", articleID), nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", accessToken)) - articleForm.Title = "Testing article title" - articleForm.Content = "Testing article content" + if err != nil { + fmt.Println(err) + } - data, _ := json.Marshal(articleForm) + resp := httptest.NewRecorder() + testRouter.ServeHTTP(resp, req) - req, err := http.NewRequest("POST", "/v1/article", bytes.NewBufferString(string(data))) - req.Header.Set("Content-Type", "application/json") + assert.Equal(t, resp.Code, http.StatusOK) +} + +/** +* TestGetInvalidArticle +* Test getting invalid article +* +* Must return response code 404 + */ +func TestGetInvalidArticle(t *testing.T) { + testRouter := SetupRouter() + + req, err := http.NewRequest("GET", "/v1/article/invalid", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", accessToken)) if err != nil { fmt.Println(err) } resp := httptest.NewRecorder() - testRouter.ServeHTTP(resp, req) - assert.Equal(t, resp.Code, http.StatusUnauthorized) + + assert.Equal(t, resp.Code, http.StatusNotFound) } /** -* TestGetArticle -* Test getting one article +* TestGetArticleNotLoggedin +* Test getting the article with logged out user * -* Must return response code 200 +* Must return response code 401 */ -func TestGetArticle(t *testing.T) { +func TestGetArticleNotLoggedin(t *testing.T) { testRouter := SetupRouter() - url := fmt.Sprintf("/v1/article/%d", articleID) - - req, err := http.NewRequest("GET", url, nil) - req.Header.Set("Cookie", signinCookie) + req, err := http.NewRequest("GET", fmt.Sprintf("/v1/article/%d", articleID), nil) + req.Header.Set("Content-Type", "application/json") if err != nil { fmt.Println(err) } resp := httptest.NewRecorder() - testRouter.ServeHTTP(resp, req) - assert.Equal(t, resp.Code, http.StatusOK) + + assert.Equal(t, resp.Code, http.StatusUnauthorized) } /** -* TestGetInvalidArticle -* Test getting invalid article +* TestCreateArticleUnauthorized +* Test getting the article with unauthorized user (wrong or expired access_token) * -* Must return response code 404 +* Must return response code 401 */ -func TestGetInvalidArticle(t *testing.T) { +func TestCreateArticleUnauthorized(t *testing.T) { testRouter := SetupRouter() - req, err := http.NewRequest("GET", "/v1/article/invalid", nil) - req.Header.Set("Cookie", signinCookie) + req, err := http.NewRequest("GET", fmt.Sprintf("/v1/article/%d", articleID), nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", "abc123")) if err != nil { fmt.Println(err) } resp := httptest.NewRecorder() - testRouter.ServeHTTP(resp, req) - assert.Equal(t, resp.Code, http.StatusNotFound) + + assert.Equal(t, resp.Code, http.StatusUnauthorized) } /** @@ -335,15 +417,15 @@ func TestUpdateArticle(t *testing.T) { req, err := http.NewRequest("PUT", url, bytes.NewBufferString(string(data))) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Cookie", signinCookie) + req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", accessToken)) if err != nil { fmt.Println(err) } resp := httptest.NewRecorder() - testRouter.ServeHTTP(resp, req) + assert.Equal(t, resp.Code, http.StatusOK) } @@ -359,36 +441,94 @@ func TestDeleteArticle(t *testing.T) { url := fmt.Sprintf("/v1/article/%d", articleID) req, err := http.NewRequest("DELETE", url, nil) - req.Header.Set("Cookie", signinCookie) + req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", accessToken)) if err != nil { fmt.Println(err) } resp := httptest.NewRecorder() + testRouter.ServeHTTP(resp, req) + + assert.Equal(t, resp.Code, http.StatusOK) +} + +/** +* TestRefreshToken +* Test refreshing the token with valid refresh_token +* +* Must return response code 200 + */ +func TestRefreshToken(t *testing.T) { + testRouter := SetupRouter() + + var tokenForm forms.Token + + tokenForm.RefreshToken = refreshToken + + data, _ := json.Marshal(tokenForm) + + req, err := http.NewRequest("POST", "/v1/token/refresh", bytes.NewBufferString(string(data))) + req.Header.Set("Content-Type", "application/json") + + if err != nil { + fmt.Println(err) + } + resp := httptest.NewRecorder() testRouter.ServeHTTP(resp, req) + assert.Equal(t, resp.Code, http.StatusOK) } +/** +* TestInvalidRefreshToken +* Test refreshing the token with invalid refresh_token +* +* Must return response code 401 + */ +func TestInvalidRefreshToken(t *testing.T) { + testRouter := SetupRouter() + + var tokenForm forms.Token + + //Since we didn't update it in the test before - this will not be valid anymore + tokenForm.RefreshToken = refreshToken + + data, _ := json.Marshal(tokenForm) + + req, err := http.NewRequest("POST", "/v1/token/refresh", bytes.NewBufferString(string(data))) + req.Header.Set("Content-Type", "application/json") + + if err != nil { + fmt.Println(err) + } + + resp := httptest.NewRecorder() + testRouter.ServeHTTP(resp, req) + + assert.Equal(t, resp.Code, http.StatusUnauthorized) +} + /** * TestUserSignout -* Test signout a user +* Test logout a user * * Must return response code 200 */ -func TestUserSignout(t *testing.T) { +func TestUserLogout(t *testing.T) { testRouter := SetupRouter() - req, err := http.NewRequest("GET", "/v1/user/signout", nil) + req, err := http.NewRequest("GET", "/v1/user/logout", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", accessToken)) if err != nil { fmt.Println(err) } resp := httptest.NewRecorder() - testRouter.ServeHTTP(resp, req) + assert.Equal(t, resp.Code, http.StatusOK) }