├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── client ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.vue │ ├── assets │ └── logo.png │ ├── components │ └── HelloWorld.vue │ └── main.js ├── docker-compose.yml ├── go.mod ├── go.sum ├── heroku.yml └── server ├── .env ├── app ├── app.go └── router.go ├── auth ├── auth.go └── auth_test.go ├── controller ├── controller_mock_setup.go ├── login_controller.go ├── login_controller_test.go ├── ping_controller.go ├── todo_controller.go ├── todo_controller_test.go ├── user_controller.go └── user_controller_test.go ├── main.go ├── middlewares └── middlewares.go ├── model ├── auth_uuid.go ├── auth_uuid_test.go ├── base_model.go ├── base_model_test.go ├── todo.go ├── todo_test.go ├── user.go └── user_test.go └── service ├── signin_service.go └── signin_service_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: # basic units of work in a run 3 | build: # runs not using Workflows must have a `build` job as entry point 4 | docker: # run the steps with Docker 5 | - image: circleci/golang:1.12 6 | - image: circleci/postgres:9.6-alpine 7 | environment: # environment variables for primary container 8 | POSTGRES_USER: steven 9 | POSTGRES_DB: manage_jwt_test 10 | 11 | environment: # environment variables for the build itself 12 | GO111MODULE: "on" #we don't rely on GOPATH 13 | 14 | working_directory: ~/usr/src/app # Go module is used, so we dont need to worry about GOPATH 15 | 16 | steps: # steps that comprise the `build` job 17 | - checkout # check out source code to working directory 18 | - run: 19 | name: "Fetch dependencies" 20 | command: go mod download 21 | 22 | # Wait for Postgres to be ready before proceeding 23 | - run: 24 | name: Waiting for Postgres to be ready 25 | command: dockerize -wait tcp://localhost:5432 -timeout 1m 26 | 27 | - run: 28 | name: Run unit tests 29 | environment: # environment variables for the database url and path to migration files 30 | FORUM_DB_URL: "postgres://steven@localhost:5432/manage_jwt_test?sslmode=disable" 31 | command: go test -v ./... # our test is inside the "tests" folder, so target only that 32 | 33 | workflows: 34 | version: 2 35 | build-workflow: 36 | jobs: 37 | - build 38 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | **/node_modules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the Go API 2 | FROM golang:latest AS builder 3 | ADD . /app 4 | WORKDIR /app/server 5 | RUN go mod download 6 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w" -a -o /main . 7 | 8 | # Build the Vue application 9 | FROM node:alpine AS node_builder 10 | COPY --from=builder /app/client ./ 11 | RUN npm install 12 | RUN npm run build 13 | 14 | #FInal build for production 15 | FROM alpine:latest 16 | RUN apk --no-cache add ca-certificates 17 | COPY --from=builder /main ./ 18 | COPY --from=builder /app/server/.env . 19 | 20 | #When build is run on a vue file, the dist folder is created, copy it to web 21 | COPY --from=node_builder /dist ./web 22 | RUN chmod +x ./main 23 | EXPOSE 8080 24 | CMD ./main 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/victorsteven/manage-jwt.svg?style=svg)](https://circleci.com/gh/victorsteven/manage-jwt) 2 | 3 | 4 | #### Clone 5 | 6 | - Clone this project to your local machine `https://github.com/victorsteven/manage-jwt.git` 7 | 8 | #### Heroku App 9 | 10 | Application was deployed to Heroku. Use public URL [https://manage-jwt.herokuapp.com](https://manage-jwt.herokuapp.com) with API endpoints. 11 | 12 | #### Setup 13 | 14 | - Add your database details in the .env file. 15 | 16 | - Running Application 17 | > Run the command below 18 | ```shell 19 | $ go run main.go 20 | ``` 21 | - Use `http://localhost:8888` as base url for endpoints 22 | 23 | ## API Endpoints 24 | 25 | | METHOD | DESCRIPTION | ENDPOINTS | 26 | | ------ | --------------------------------------- | ------------------------- | 27 | | POST | Register/Signup | `/user` | 28 | | POST | Login | `/login` | 29 | | POST | Create a todo | `/todo` | 30 | | POST | Logout | `/logout` | 31 | 32 | ## Tests 33 | 34 | - Run test for all endpoints 35 | > run the command below(ensure that your test details is setup in the .env file) 36 | ```shell 37 | $ go test ./... 38 | ``` 39 | 40 | ## Author 41 | 42 | - [Steven Victor](https://twitter.com/stevensunflash) 43 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.4.4", 12 | "vue": "^2.6.10" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "^4.1.0", 16 | "@vue/cli-plugin-eslint": "^4.1.0", 17 | "@vue/cli-service": "^4.1.0", 18 | "babel-eslint": "^10.0.3", 19 | "eslint": "^5.16.0", 20 | "eslint-plugin-vue": "^5.0.0", 21 | "vue-template-compiler": "^2.6.10" 22 | }, 23 | "eslintConfig": { 24 | "root": true, 25 | "env": { 26 | "node": true 27 | }, 28 | "extends": [ 29 | "plugin:vue/essential", 30 | "eslint:recommended" 31 | ], 32 | "rules": {}, 33 | "parserOptions": { 34 | "parser": "babel-eslint" 35 | } 36 | }, 37 | "browserslist": [ 38 | "> 1%", 39 | "last 2 versions" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorsteven/manage-jwt/22018ceed963630e4211d66d605fa962318c6dea/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | client 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorsteven/manage-jwt/22018ceed963630e4211d66d605fa962318c6dea/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | container_name: todo_app 5 | build: 6 | context: . 7 | dockerfile: ./Dockerfile 8 | ports: 9 | - 8888:8888 10 | restart: on-failure 11 | volumes: # without this volume mapping to the directory of our project, live reloading wont happen 12 | - .:/usr/src/app 13 | depends_on: 14 | - todo-postgres # This service depends on postgres. Start that first. 15 | networks: 16 | - todo 17 | 18 | todo-postgres: 19 | image: postgres:latest 20 | container_name: todo_db_postgres 21 | environment: 22 | - POSTGRES_USER=${DB_USER} 23 | - POSTGRES_PASSWORD=${DB_PASSWORD} 24 | - POSTGRES_DB=${DB_NAME} 25 | - DATABASE_HOST=${DB_HOST} 26 | ports: 27 | - '5432:5432' 28 | volumes: 29 | - database_postgres:/var/lib/postgresql/data 30 | networks: 31 | - todo 32 | 33 | volumes: 34 | database_postgres: 35 | 36 | # Networks to be created to facilitate communication between containers 37 | networks: 38 | todo: 39 | driver: bridge 40 | 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module jwt-across-platforms 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/badoux/checkmail v0.0.0-20181210160741-9661bd69e9ad 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 8 | github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607 9 | github.com/gin-gonic/gin v1.5.0 10 | github.com/jinzhu/gorm v1.9.12 11 | github.com/joho/godotenv v1.3.0 12 | github.com/myesui/uuid v1.0.0 // indirect 13 | github.com/stretchr/testify v1.4.0 14 | github.com/twinj/uuid v1.0.0 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/badoux/checkmail v0.0.0-20181210160741-9661bd69e9ad h1:kXfVkP8xPSJXzicomzjECcw6tv1Wl9h1lNenWBfNKdg= 2 | github.com/badoux/checkmail v0.0.0-20181210160741-9661bd69e9ad/go.mod h1:r5ZalvRl3tXevRNJkwIB6DC4DD3DMjIlY9NEU1XGoaQ= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 7 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 10 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 11 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 12 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 13 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 14 | github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607 h1:MrIm8EEPue08JS4eh+b08IOG+wd0WRWEHWnewNfWFX0= 15 | github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= 16 | github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= 17 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 18 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 19 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 20 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 21 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 22 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 23 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 24 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 25 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 28 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 30 | github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= 31 | github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= 32 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 33 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 34 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 35 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 36 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 37 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 38 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 39 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 40 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 41 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 42 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 43 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 44 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 45 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 46 | github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= 47 | github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 48 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 49 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 50 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 51 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 52 | github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= 53 | github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 57 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 58 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 59 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 60 | github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk= 61 | github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= 62 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 63 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 64 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 65 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 66 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 67 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 68 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= 69 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 70 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 71 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 72 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 75 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 77 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 78 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 82 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 83 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= 84 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 85 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 86 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 87 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | worker: 5 | dockerfile: Dockerfile -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | DB_HOST=127.0.0.1 #running the app locally 2 | #DB_HOST=todo-postgres #running the app with docker locally 3 | DB_DRIVER=postgres 4 | DB_USER=steven 5 | DB_PASSWORD=here 6 | DB_NAME=manage_jwt 7 | DB_PORT=5432 8 | API_SECRET=nksdmlkfmfsd 9 | 10 | #Test Database 11 | DB_USER_TEST=steven 12 | DB_PASSWORD_TEST=here 13 | DB_NAME_TEST=manage_jwt_test 14 | -------------------------------------------------------------------------------- /server/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/joho/godotenv" 6 | "log" 7 | "jwt-across-platforms/server/model" 8 | "os" 9 | ) 10 | 11 | func init() { 12 | var err error 13 | err = godotenv.Load() 14 | if err != nil { 15 | log.Fatalf("Error getting env, %v", err) 16 | } 17 | } 18 | var router = gin.Default() 19 | 20 | func StartApp() { 21 | //var conn Connect 22 | dbdriver := os.Getenv("DB_DRIVER") 23 | username := os.Getenv("DB_USER") 24 | password := os.Getenv("DB_PASSWORD") 25 | host := os.Getenv("DB_HOST") 26 | database := os.Getenv("DB_NAME") 27 | db_port := os.Getenv("DB_PORT") 28 | 29 | _, err := model.Model.Initialize(dbdriver, username, password, db_port, host, database) 30 | if err != nil { 31 | log.Fatal("Error connecting to the database: ", err) 32 | } 33 | route() 34 | 35 | port := os.Getenv("PORT") //using heroku host 36 | if port == "" { 37 | port = "8888" //localhost 38 | } 39 | log.Fatal(router.Run(":"+port)) 40 | } 41 | -------------------------------------------------------------------------------- /server/app/router.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/gin-gonic/contrib/static" 5 | "jwt-across-platforms/server/controller" 6 | "jwt-across-platforms/server/middlewares" 7 | ) 8 | 9 | func route() { 10 | router.Use(static.Serve("/", static.LocalFile("./web", true))) //for the vue app 11 | 12 | router.GET("/", controller.Index) 13 | router.POST("/user", controller.CreateUser) 14 | router.POST("/todo", middlewares.TokenAuthMiddleware(), controller.CreateTodo) 15 | router.POST("/login", controller.Login) 16 | router.POST("/logout", middlewares.TokenAuthMiddleware(), controller.LogOut) 17 | } 18 | -------------------------------------------------------------------------------- /server/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dgrijalva/jwt-go" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type AuthDetails struct { 14 | AuthUuid string 15 | UserId uint64 16 | } 17 | 18 | func CreateToken(authD AuthDetails) (string, error) { 19 | claims := jwt.MapClaims{} 20 | claims["authorized"] = true 21 | claims["auth_uuid"] = authD.AuthUuid 22 | claims["user_id"] = authD.UserId 23 | claims["exp"] = time.Now().Add(time.Minute * 15).Unix() 24 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 25 | return token.SignedString([]byte(os.Getenv("API_SECRET"))) 26 | } 27 | 28 | 29 | //func GenerateTokenPair() (map[string]string, error) { 30 | // // Create token 31 | // token := jwt.New(jwt.SigningMethodHS256) 32 | // 33 | // // Set claims 34 | // // This is the information which frontend can use 35 | // // The backend can also decode the token and get admin etc. 36 | // claims := token.Claims.(jwt.MapClaims) 37 | // claims["sub"] = 1 38 | // claims["name"] = "Jon Doe" 39 | // claims["admin"] = true 40 | // claims["exp"] = time.Now().Add(time.Minute * 15).Unix() 41 | // 42 | // // Generate encoded token and send it as response. 43 | // // The signing string should be secret (a generated UUID works too) 44 | // t, err := token.SignedString([]byte("secret")) 45 | // if err != nil { 46 | // return nil, err 47 | // } 48 | // 49 | // refreshToken := jwt.New(jwt.SigningMethodHS256) 50 | // rtClaims := refreshToken.Claims.(jwt.MapClaims) 51 | // rtClaims["sub"] = 1 52 | // rtClaims["exp"] = time.Now().Add(time.Hour * 24).Unix() 53 | // 54 | // rt, err := refreshToken.SignedString([]byte("secret")) 55 | // if err != nil { 56 | // return nil, err 57 | // } 58 | // 59 | // return map[string]string{ 60 | // "access_token": t, 61 | // "refresh_token": rt, 62 | // }, nil 63 | //} 64 | 65 | 66 | //func CreateToken(authD AuthDetails) (map[string]string, error) { 67 | // //generate 15 min token 68 | // claimsShort := jwt.MapClaims{} 69 | // claimsShort["authorized"] = true 70 | // claimsShort["auth_uuid"] = authD.AuthUuid 71 | // claimsShort["user_id"] = authD.UserId 72 | // claimsShort["exp"] = time.Now().Add(time.Minute * 15).Unix() 73 | // tokenShort := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsShort) 74 | // short, err := tokenShort.SignedString([]byte(os.Getenv("API_SECRET"))) 75 | // if err != nil { 76 | // fmt.Println("Error generating short token: ", err) 77 | // return nil, err 78 | // } 79 | // 80 | // //generate refresh token 24hours long 81 | // claimsLong := jwt.MapClaims{} 82 | // claimsLong["exp"] = time.Now().Add(time.Hour * 24).Unix() 83 | // tokenLong := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsLong) 84 | // long, err := tokenLong.SignedString([]byte(os.Getenv("API_SECRET"))) 85 | // if err != nil { 86 | // fmt.Println("Error generating long token: ", err) 87 | // return nil, err 88 | // } 89 | // 90 | // //refreshToken := jwt.New(jwt.SigningMethodHS256) 91 | // //rtClaims := refreshToken.Claims.(jwt.MapClaims) 92 | // //rtClaims["sub"] = 1 93 | // //rtClaims["exp"] = time.Now().Add(time.Hour * 24).Unix() 94 | // //rt, err := refreshToken.SignedString([]byte("secret")) 95 | // //if err != nil { 96 | // // return nil, err 97 | // //} 98 | // 99 | // return map[string]string{ 100 | // "access_token": short, 101 | // "refresh_token": long, 102 | // }, nil 103 | //} 104 | 105 | func TokenValid(r *http.Request) error { 106 | token, err := VerifyToken(r) 107 | if err != nil { 108 | return err 109 | } 110 | if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid { 111 | return err 112 | } 113 | return nil 114 | } 115 | 116 | func VerifyToken(r *http.Request) (*jwt.Token, error) { 117 | tokenString := ExtractToken(r) 118 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 119 | //Make sure that the token method conform to "SigningMethodHMAC" 120 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 121 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 122 | } 123 | return []byte(os.Getenv("API_SECRET")), nil 124 | }) 125 | if err != nil { 126 | return nil, err 127 | } 128 | return token, nil 129 | } 130 | 131 | //get the token from the request body 132 | func ExtractToken(r *http.Request) string { 133 | keys := r.URL.Query() 134 | token := keys.Get("token") 135 | if token != "" { 136 | return token 137 | } 138 | bearToken := r.Header.Get("Authorization") 139 | //normally Authorization the_token_xxx 140 | strArr := strings.Split(bearToken, " ") 141 | if len(strArr) == 2 { 142 | return strArr[1] 143 | } 144 | return "" 145 | } 146 | 147 | func ExtractTokenAuth(r *http.Request) (*AuthDetails, error) { 148 | token, err := VerifyToken(r) 149 | if err != nil { 150 | return nil, err 151 | } 152 | claims, ok := token.Claims.(jwt.MapClaims) //the token claims should conform to MapClaims 153 | if ok && token.Valid { 154 | authUuid, ok := claims["auth_uuid"].(string) //convert the interface to string 155 | if !ok { 156 | return nil, err 157 | } 158 | userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64) 159 | if err != nil { 160 | return nil, err 161 | } 162 | return &AuthDetails{ 163 | AuthUuid: authUuid, 164 | UserId: userId, 165 | }, nil 166 | } 167 | return nil, err 168 | } 169 | -------------------------------------------------------------------------------- /server/auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "net/http" 7 | "testing" 8 | ) 9 | //READ THIS TO UNDERSTAND THE TEST CASES BELOW: 10 | //Remember what the program emphasizes, we didnt specify how long a token will last while creating it. Which means, a token can last forever. So, the token used below is a valid one, except you alter it. 11 | //The way we invalidate a token is to create a new jwt with a different uuid, thereby rendering the formerly created valid token invalid. 12 | //We ran all test with a valid token. If have time, you can add test cases and use a random token, then assert for the errors. As an example, i altered the token in this test "TestToken_Invalid", as error was the result 13 | 14 | func TestCreateToken(t *testing.T) { 15 | au := AuthDetails{ 16 | AuthUuid: "43b78a87-6bcf-439a-ab2e-940d50c4dc33", //this can be anything 17 | UserId: 1, 18 | } 19 | token, err := CreateToken(au) 20 | assert.Nil(t, err) 21 | assert.NotNil(t, token) 22 | } 23 | 24 | func TestVerifyToken(t *testing.T) { 25 | //In order to generate a request, let use the logout endpoint 26 | req, err := http.NewRequest("POST", "/logout", nil) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkduc" 31 | 32 | tokenString := fmt.Sprintf("Bearer %v", token) 33 | req.Header.Set("Authorization", tokenString) 34 | 35 | jwtAns, err := VerifyToken(req) 36 | 37 | assert.Nil(t, err) 38 | assert.NotNil(t, jwtAns) //this is of type *jwt.Token 39 | } 40 | 41 | func TestExtractToken(t *testing.T) { 42 | //In order to generate a request, let use the logout endpoint 43 | req, err := http.NewRequest("POST", "/logout", nil) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkduc" 48 | 49 | tokenString := fmt.Sprintf("Bearer %v", token) 50 | req.Header.Set("Authorization", tokenString) 51 | 52 | result := ExtractToken(req) 53 | assert.NotNil(t, result) 54 | assert.EqualValues(t, result, token) 55 | } 56 | 57 | //Check the auth details from the token: 58 | func TestExtractTokenAuth(t *testing.T) { 59 | //In order to generate a request, let use the logout endpoint 60 | req, err := http.NewRequest("POST", "/logout", nil) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkduc" 65 | 66 | tokenString := fmt.Sprintf("Bearer %v", token) 67 | req.Header.Set("Authorization", tokenString) 68 | 69 | result, err := ExtractTokenAuth(req) 70 | assert.Nil(t, err) 71 | assert.NotNil(t, result) 72 | assert.NotNil(t, result.UserId) 73 | assert.NotNil(t, result.AuthUuid) 74 | } 75 | 76 | 77 | func TestTokenValid(t *testing.T) { 78 | //In order to generate a request, let use the logout endpoint 79 | req, err := http.NewRequest("POST", "/logout", nil) 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkduc" 84 | 85 | tokenString := fmt.Sprintf("Bearer %v", token) 86 | req.Header.Set("Authorization", tokenString) 87 | 88 | errToken := TokenValid(req) 89 | assert.Nil(t, errToken) 90 | } 91 | 92 | //i added garbage to the token, so is not valid 93 | func TestToken_Invalid(t *testing.T) { 94 | //In order to generate a request, let use the logout endpoint 95 | req, err := http.NewRequest("POST", "/logout", nil) 96 | if err != nil { 97 | t.Error(err) 98 | } 99 | token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkducxx" 100 | 101 | tokenString := fmt.Sprintf("Bearer %v", token) 102 | req.Header.Set("Authorization", tokenString) 103 | 104 | errToken := TokenValid(req) 105 | assert.NotNil(t, errToken) 106 | assert.EqualValues(t, "illegal base64 data at input byte 45", errToken.Error()) 107 | } -------------------------------------------------------------------------------- /server/controller/controller_mock_setup.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "jwt-across-platforms/server/auth" 6 | "jwt-across-platforms/server/model" 7 | ) 8 | 9 | var ( 10 | createUserModel func(*model.User) (*model.User, error) 11 | createAuthModel func(uint64) (*model.Auth, error) 12 | signIn func(auth.AuthDetails) (string, error) 13 | getUserByEmail func(string) (*model.User, error) 14 | fetchAuth func(*auth.AuthDetails) (*model.Auth, error) 15 | createTodoModel func(*model.Todo) (*model.Todo, error) 16 | deleteAuth func(*auth.AuthDetails) error 17 | ) 18 | 19 | type fakeServer struct {} 20 | type fakeSignin struct {} 21 | 22 | //Since this methods are under the modelInterface, we must define all method there, but observe that the ones we dont need just have "panic("implement me")", while the ones we need to mock have contents. 23 | func (fs *fakeServer) Initialize(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) (*gorm.DB, error) { 24 | panic("implement me") 25 | } 26 | func (fs *fakeServer) ValidateEmail(string) error { 27 | panic("implement me") 28 | } 29 | 30 | func (fs *fakeServer) GetUserByEmail(email string) (*model.User, error) { 31 | return getUserByEmail(email) 32 | } 33 | func (fs *fakeServer) CreateTodo(todo *model.Todo) (*model.Todo, error) { 34 | return createTodoModel(todo) 35 | } 36 | func (fs *fakeServer) FetchAuth(au *auth.AuthDetails) (*model.Auth, error) { 37 | return fetchAuth(au) 38 | } 39 | func (fs *fakeServer) DeleteAuth(au *auth.AuthDetails) error { 40 | return deleteAuth(au) 41 | } 42 | 43 | func (fs *fakeServer) CreateUser(user *model.User) (*model.User, error) { 44 | return createUserModel(user) 45 | } 46 | func (fs *fakeServer) CreateAuth(userId uint64) (*model.Auth, error) { 47 | return createAuthModel(userId) 48 | } 49 | 50 | func (fs *fakeSignin) SignIn(authD auth.AuthDetails) (string, error) { 51 | return signIn(authD) 52 | } 53 | -------------------------------------------------------------------------------- /server/controller/login_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "log" 6 | "jwt-across-platforms/server/auth" 7 | "jwt-across-platforms/server/model" 8 | "jwt-across-platforms/server/service" 9 | "net/http" 10 | ) 11 | 12 | func Login(c *gin.Context) { 13 | var u model.User 14 | if err := c.ShouldBindJSON(&u); err != nil { 15 | c.JSON(http.StatusUnprocessableEntity, err.Error()) 16 | return 17 | } 18 | //check if the user exist: 19 | user, err := model.Model.GetUserByEmail(u.Email) 20 | if err != nil { 21 | c.JSON(http.StatusNotFound, err.Error()) 22 | return 23 | } 24 | //since after the user logged out, we destroyed that record in the database so that same jwt token can't be used twice. We need to create the token again 25 | authData, err := model.Model.CreateAuth(user.ID) 26 | if err != nil { 27 | c.JSON(http.StatusInternalServerError, err.Error()) 28 | return 29 | } 30 | var authD auth.AuthDetails 31 | authD.UserId = authData.UserID 32 | authD.AuthUuid = authData.AuthUUID 33 | 34 | token, loginErr := service.Authorize.SignIn(authD) 35 | if loginErr != nil { 36 | c.JSON(http.StatusForbidden, "Please try to login later") 37 | return 38 | } 39 | c.JSON(http.StatusOK, token) 40 | } 41 | 42 | func LogOut(c *gin.Context) { 43 | au, err := auth.ExtractTokenAuth(c.Request) 44 | if err != nil { 45 | c.JSON(http.StatusUnauthorized, "unauthorized") 46 | return 47 | } 48 | delErr := model.Model.DeleteAuth(au) 49 | if delErr != nil { 50 | log.Println(delErr) 51 | c.JSON(http.StatusUnauthorized, "unauthorized") 52 | return 53 | } 54 | c.JSON(http.StatusOK, "Successfully logged out") 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /server/controller/login_controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/gin-gonic/gin" 9 | "github.com/stretchr/testify/assert" 10 | "jwt-across-platforms/server/auth" 11 | "jwt-across-platforms/server/model" 12 | "jwt-across-platforms/server/service" 13 | "net/http" 14 | "net/http/httptest" 15 | "testing" 16 | ) 17 | 18 | //NOTE WE ARE PERFORMING UNIT TESTS ON THE LOGIN FUNCTION, SO WE MOCKED ALL FUNCTIONS/METHODS THAT THE LOGIN DEPEND. CHECK OUT THE FILE "controller_mock_setup.go" FILE TO SEE HOW THE MOCK IS CREATED AND USED HERE 19 | 20 | func TestLogin_Success(t *testing.T) { 21 | model.Model = &fakeServer{} 22 | service.Authorize = &fakeSignin{} 23 | 24 | getUserByEmail = func(email string) (*model.User, error) { 25 | return &model.User{ 26 | ID: 1, 27 | Email: "sunflash@gmail.com", 28 | }, nil 29 | } 30 | createAuthModel = func(uint64) (*model.Auth, error) { 31 | return &model.Auth{ 32 | ID: 1, 33 | UserID: 1, 34 | AuthUUID: "83b09612-9dfc-4c1d-8f7d-a589acec7081", 35 | }, nil 36 | } 37 | signIn = func(auth.AuthDetails) (string, error) { 38 | return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiI4M2IwOTYxMi05ZGZjLTRjMWQtOGY3ZC1hNTg5YWNlYzcwODEiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo1fQ.otegNS-W9OE8RsqGtiyJRCB-H0YXBygNXP91qeCPdF8", nil 39 | } 40 | 41 | //Now let test only the controller implementation, void of external methods. Remember, the end result when the function runs to to return a JWT. And that JWT that will be returned is the one we have defined above. 42 | u := model.User{ 43 | Email: "vicsdfddt@gmail.com", 44 | } 45 | byteSlice, err := json.Marshal(&u) 46 | if err != nil { 47 | t.Error("Cannot marshal to json") 48 | } 49 | r := gin.Default() 50 | req, err := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(byteSlice)) 51 | if err != nil { 52 | t.Errorf("this is the error: %v\n", err) 53 | } 54 | rr := httptest.NewRecorder() 55 | r.POST("/user/login", Login) 56 | r.ServeHTTP(rr, req) 57 | 58 | var token string 59 | err = json.Unmarshal(rr.Body.Bytes(), &token) 60 | assert.Nil(t, err) 61 | assert.EqualValues(t, http.StatusOK, rr.Code) 62 | assert.EqualValues(t, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiI4M2IwOTYxMi05ZGZjLTRjMWQtOGY3ZC1hNTg5YWNlYzcwODEiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo1fQ.otegNS-W9OE8RsqGtiyJRCB-H0YXBygNXP91qeCPdF8", token) 63 | } 64 | 65 | //An example is when the email is not found in the database. 66 | //We only mock according to demand. In the test below, we mocked only the GetUserEmail method, since execution will stop there, because we told it to return not found 67 | func TestLogin_Not_Found_User(t *testing.T) { 68 | model.Model = &fakeServer{} 69 | service.Authorize = &fakeSignin{} 70 | 71 | getUserByEmail = func(email string) (*model.User, error) { 72 | return nil, errors.New("email not found") 73 | } 74 | u := model.User{ 75 | Email: "vicsdfddt@gmail.com", 76 | } 77 | byteSlice, err := json.Marshal(&u) 78 | if err != nil { 79 | t.Error("Cannot marshal to json") 80 | } 81 | r := gin.Default() 82 | req, err := http.NewRequest(http.MethodPost, "/user/login", bytes.NewReader(byteSlice)) 83 | if err != nil { 84 | t.Errorf("this is the error: %v\n", err) 85 | } 86 | rr := httptest.NewRecorder() 87 | r.POST("/user/login", Login) 88 | r.ServeHTTP(rr, req) 89 | 90 | var errString string 91 | err = json.Unmarshal(rr.Body.Bytes(), &errString) 92 | assert.Nil(t, err) 93 | assert.NotNil(t, errString) 94 | assert.EqualValues(t, http.StatusNotFound, rr.Code) 95 | assert.EqualValues(t, "email not found", errString) 96 | } 97 | 98 | 99 | func TestLogOut_Success(t *testing.T) { 100 | //Now exchange the real implementation with our mock 101 | model.Model = &fakeServer{} 102 | 103 | fetchAuth = func(*auth.AuthDetails) (*model.Auth, error) { 104 | return &model.Auth{ 105 | ID: 1, 106 | UserID: 1, 107 | AuthUUID: "83b09612-9dfc-4c1d-8f7d-a589acec7081", 108 | }, nil 109 | } 110 | deleteAuth = func(au *auth.AuthDetails) error { 111 | return nil //no errors deleting 112 | } 113 | 114 | r := gin.Default() 115 | req, err := http.NewRequest(http.MethodPost, "/logout", nil) 116 | if err != nil { 117 | t.Errorf("this is the error: %v\n", err) 118 | } 119 | //It is an authenticated user can create a todo, so, lets pass a token to our request headers 120 | tk := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkduc" 121 | tokenString := fmt.Sprintf("Bearer %v", tk) 122 | req.Header.Set("Authorization", tokenString) 123 | 124 | rr := httptest.NewRecorder() 125 | r.POST("/logout", LogOut) 126 | r.ServeHTTP(rr, req) 127 | 128 | var loggedOut string 129 | err = json.Unmarshal(rr.Body.Bytes(), &loggedOut) 130 | assert.Nil(t, err) 131 | assert.EqualValues(t, http.StatusOK, rr.Code) 132 | assert.EqualValues(t, "Successfully logged out", loggedOut) 133 | } 134 | 135 | //Anything from empty or wrong token returns unauthorized. From the example, we used a wrong token. we added wrong letters at the end. 136 | //Since for sure a todo will not be created, we avoided mocking the fetchAuth and the deleteAuth methods. 137 | func TestLogout_Unauthorized_User(t *testing.T) { 138 | 139 | r := gin.Default() 140 | req, err := http.NewRequest(http.MethodPost, "/logout", nil) 141 | if err != nil { 142 | t.Errorf("this is the error: %v\n", err) 143 | } 144 | //It is an authenticated user can create a todo, so, lets pass a token to our request headers 145 | tk := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkducxx" 146 | tokenString := fmt.Sprintf("Bearer %v", tk) 147 | req.Header.Set("Authorization", tokenString) 148 | 149 | rr := httptest.NewRecorder() 150 | r.POST("/logout", LogOut) 151 | r.ServeHTTP(rr, req) 152 | 153 | var errMsg string 154 | err = json.Unmarshal(rr.Body.Bytes(), &errMsg) 155 | assert.Nil(t, err) 156 | assert.EqualValues(t, http.StatusUnauthorized, rr.Code) 157 | assert.EqualValues(t, "unauthorized", errMsg) 158 | } -------------------------------------------------------------------------------- /server/controller/ping_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func Index(c *gin.Context) { 9 | c.JSON(http.StatusOK, gin.H{ 10 | "status": http.StatusOK, 11 | "success": "Welcome! We are glad to have you here. Use Postman or your favorite tool to: Signup using: /user. Login using: /login. Create a todo using: /todo. Logout using: /logout", 12 | }) 13 | } -------------------------------------------------------------------------------- /server/controller/todo_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "jwt-across-platforms/server/auth" 6 | "jwt-across-platforms/server/model" 7 | "net/http" 8 | ) 9 | 10 | func CreateTodo(c *gin.Context) { 11 | 12 | var td model.Todo 13 | if err := c.ShouldBindJSON(&td); err != nil { 14 | c.JSON(http.StatusUnprocessableEntity, "invalid json") 15 | return 16 | } 17 | tokenAuth, err := auth.ExtractTokenAuth(c.Request) 18 | if err != nil { 19 | c.JSON(http.StatusUnauthorized, "unauthorized") 20 | return 21 | } 22 | foundAuth, err := model.Model.FetchAuth(tokenAuth) 23 | if err != nil { 24 | c.JSON(http.StatusUnauthorized, "unauthorized") 25 | return 26 | } 27 | td.UserID = foundAuth.UserID 28 | todo, err := model.Model.CreateTodo(&td) 29 | if err != nil { 30 | c.JSON(http.StatusInternalServerError, err.Error()) 31 | return 32 | } 33 | c.JSON(http.StatusCreated, todo) 34 | } 35 | -------------------------------------------------------------------------------- /server/controller/todo_controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "github.com/stretchr/testify/assert" 9 | "jwt-across-platforms/server/auth" 10 | "jwt-across-platforms/server/model" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | ) 15 | 16 | 17 | func TestCreateTodo_Success(t *testing.T) { 18 | //Now exchange the real implementation with our mock 19 | model.Model = &fakeServer{} 20 | 21 | fetchAuth = func(*auth.AuthDetails) (*model.Auth, error) { 22 | return &model.Auth{ 23 | ID: 1, 24 | UserID: 1, 25 | AuthUUID: "83b09612-9dfc-4c1d-8f7d-a589acec7081", 26 | }, nil 27 | } 28 | createTodoModel = func(*model.Todo) (*model.Todo, error) { 29 | return &model.Todo{ 30 | ID: 1, 31 | UserID: 1, 32 | Title: "the title", 33 | }, nil 34 | } 35 | todo := model.Todo{ 36 | Title: "the title", 37 | } 38 | byteSlice, err := json.Marshal(&todo) 39 | if err != nil { 40 | t.Error("Cannot marshal to json") 41 | } 42 | r := gin.Default() 43 | req, err := http.NewRequest(http.MethodPost, "/todo", bytes.NewReader(byteSlice)) 44 | if err != nil { 45 | t.Errorf("this is the error: %v\n", err) 46 | } 47 | //It is an authenticated user can create a todo, so, lets pass a token to our request headers 48 | tk := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkduc" 49 | tokenString := fmt.Sprintf("Bearer %v", tk) 50 | req.Header.Set("Authorization", tokenString) 51 | 52 | rr := httptest.NewRecorder() 53 | r.POST("/todo", CreateTodo) 54 | r.ServeHTTP(rr, req) 55 | 56 | var newTodo model.Todo 57 | err = json.Unmarshal(rr.Body.Bytes(), &newTodo) 58 | assert.Nil(t, err) 59 | assert.EqualValues(t, http.StatusCreated, rr.Code) 60 | assert.EqualValues(t, 1, newTodo.UserID) 61 | assert.EqualValues(t, "the title", newTodo.Title) 62 | } 63 | 64 | //Anything from empty or wrong token returns unauthorized. From the example, we used a wrong token. we added wrong letters at the end. 65 | //Since for sure a todo will not be created, we avoided mocking the fetchAuth and the createTodoModel methods. 66 | func TestCreateTodo_Unauthorized_User(t *testing.T) { 67 | todo := model.Todo{ 68 | Title: "the title", 69 | } 70 | byteSlice, err := json.Marshal(&todo) 71 | if err != nil { 72 | t.Error("Cannot marshal to json") 73 | } 74 | r := gin.Default() 75 | req, err := http.NewRequest(http.MethodPost, "/todo", bytes.NewReader(byteSlice)) 76 | if err != nil { 77 | t.Errorf("this is the error: %v\n", err) 78 | } 79 | //It is an authenticated user can create a todo, so, lets pass a token to our request headers 80 | tk := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiJjMmUxYjBjMy00ZGRjLTQ0NjUtYWVkNC1iNGE2NDM5NzI4M2MiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjoxfQ.FWbfdhEJeK7mjZ-lWvs9scuyUrSKPrC4xafUoEqkducxx" 81 | tokenString := fmt.Sprintf("Bearer %v", tk) 82 | req.Header.Set("Authorization", tokenString) 83 | 84 | rr := httptest.NewRecorder() 85 | r.POST("/todo", CreateTodo) 86 | r.ServeHTTP(rr, req) 87 | 88 | var errMsg string 89 | err = json.Unmarshal(rr.Body.Bytes(), &errMsg) 90 | assert.Nil(t, err) 91 | assert.EqualValues(t, http.StatusUnauthorized, rr.Code) 92 | assert.EqualValues(t, "unauthorized", errMsg) 93 | } 94 | 95 | //When wrong input is supplied. Here also, we wont mock any external methods 96 | func TestCreateTodo_Invalid_Input(t *testing.T) { 97 | invalidTitle := 12345 //using an integer instead of a string 98 | byteSlice, err := json.Marshal(&invalidTitle) 99 | if err != nil { 100 | t.Error("Cannot marshal to json") 101 | } 102 | r := gin.Default() 103 | req, err := http.NewRequest(http.MethodPost, "/todo", bytes.NewReader(byteSlice)) 104 | if err != nil { 105 | t.Errorf("this is the error: %v\n", err) 106 | } 107 | rr := httptest.NewRecorder() 108 | r.POST("/todo", CreateTodo) 109 | r.ServeHTTP(rr, req) 110 | 111 | var msg string 112 | err = json.Unmarshal(rr.Body.Bytes(), &msg) //since we outputted the error as string in the controller 113 | assert.Nil(t, err) //we can unmarshall without issues 114 | assert.EqualValues(t, "invalid json", msg) 115 | assert.EqualValues(t, http.StatusUnprocessableEntity, rr.Code) 116 | } -------------------------------------------------------------------------------- /server/controller/user_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "jwt-across-platforms/server/model" 6 | "net/http" 7 | ) 8 | 9 | func CreateUser(c *gin.Context) { 10 | var u model.User 11 | if err := c.ShouldBindJSON(&u); err != nil { 12 | c.JSON(http.StatusUnprocessableEntity, "invalid json") 13 | return 14 | } 15 | user, err := model.Model.CreateUser(&u) 16 | if err != nil { 17 | c.JSON(http.StatusInternalServerError, err.Error()) 18 | return 19 | } 20 | c.JSON(http.StatusCreated, user) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /server/controller/user_controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/gin-gonic/gin" 7 | "github.com/stretchr/testify/assert" 8 | "jwt-across-platforms/server/model" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | 15 | func TestCreateUser_Success(t *testing.T) { 16 | 17 | model.Model = &fakeServer{} //this is where the swapping of the real method with the fake one 18 | 19 | createUserModel = func(*model.User) (*model.User, error) { 20 | return &model.User{ 21 | ID: 1, 22 | Email: "sunflash@gmail.com", 23 | }, nil 24 | } 25 | //Now let test only the controller implementation, void of external methods. Remember, the end result when the function runs to to return a JWT. And that JWT that will be returned is the one we have defined above. 26 | u := model.User{ 27 | Email: "vicsdfddt@gmail.com", 28 | } 29 | byteSlice, err := json.Marshal(&u) 30 | if err != nil { 31 | t.Error("Cannot marshal to json") 32 | } 33 | r := gin.Default() 34 | req, err := http.NewRequest(http.MethodPost, "/user", bytes.NewReader(byteSlice)) 35 | if err != nil { 36 | t.Errorf("this is the error: %v\n", err) 37 | } 38 | rr := httptest.NewRecorder() 39 | r.POST("/user", CreateUser) 40 | r.ServeHTTP(rr, req) 41 | 42 | var user model.User 43 | err = json.Unmarshal(rr.Body.Bytes(), &user) 44 | assert.Nil(t, err) 45 | assert.EqualValues(t, http.StatusCreated, rr.Code) 46 | assert.EqualValues(t, 1, user.ID) 47 | assert.EqualValues(t, "sunflash@gmail.com", user.Email) 48 | } 49 | 50 | //We dont need to mock anything here, since our execution will never call the external methods 51 | //Now use an integer instead of a string for the input email 52 | func TestCreateUser_Invalid_Input(t *testing.T) { 53 | invalidEmail := 12345 //using an integer instead of a string 54 | byteSlice, err := json.Marshal(&invalidEmail) 55 | if err != nil { 56 | t.Error("Cannot marshal to json") 57 | } 58 | r := gin.Default() 59 | req, err := http.NewRequest(http.MethodPost, "/user", bytes.NewReader(byteSlice)) 60 | if err != nil { 61 | t.Errorf("this is the error: %v\n", err) 62 | } 63 | rr := httptest.NewRecorder() 64 | r.POST("/user", CreateUser) 65 | r.ServeHTTP(rr, req) 66 | 67 | var msg string 68 | err = json.Unmarshal(rr.Body.Bytes(), &msg) //since we outputted the error as string in the controller 69 | assert.Nil(t, err) //we can unmarshall without issues 70 | assert.EqualValues(t, "invalid json", msg) 71 | assert.EqualValues(t, http.StatusUnprocessableEntity, rr.Code) 72 | } -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "jwt-across-platforms/server/app" 4 | 5 | func main() { 6 | app.StartApp() 7 | } 8 | -------------------------------------------------------------------------------- /server/middlewares/middlewares.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "jwt-across-platforms/server/auth" 6 | "net/http" 7 | ) 8 | 9 | func TokenAuthMiddleware() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | err := auth.TokenValid(c.Request) 12 | if err != nil { 13 | c.JSON(http.StatusUnauthorized, "You need to be authorized to access this route") 14 | c.Abort() 15 | return 16 | } 17 | c.Next() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/model/auth_uuid.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/twinj/uuid" 5 | "jwt-across-platforms/server/auth" 6 | ) 7 | 8 | type Auth struct { 9 | ID uint64 `gorm:"primary_key;auto_increment" json:"id"` 10 | UserID uint64 `gorm:";not null;" json:"user_id"` 11 | AuthUUID string `gorm:"size:255;not null;" json:"auth_uuid"` 12 | } 13 | 14 | func (s *Server) FetchAuth(authD *auth.AuthDetails) (*Auth, error) { 15 | au := &Auth{} 16 | err := s.DB.Debug().Where("user_id = ? AND auth_uuid = ?", authD.UserId, authD.AuthUuid).Take(&au).Error 17 | if err != nil { 18 | return nil, err 19 | } 20 | return au, nil 21 | } 22 | 23 | //Once a user row in the auth table 24 | func (s *Server) DeleteAuth(authD *auth.AuthDetails) error { 25 | au := &Auth{} 26 | db := s.DB.Debug().Where("user_id = ? AND auth_uuid = ?", authD.UserId, authD.AuthUuid).Take(&au).Delete(&au) 27 | if db.Error != nil { 28 | return db.Error 29 | } 30 | return nil 31 | } 32 | 33 | //Once the user signup/login, create a row in the auth table, with a new uuid 34 | func (s *Server) CreateAuth(userId uint64) (*Auth, error) { 35 | au := &Auth{} 36 | au.AuthUUID = uuid.NewV4().String() //generate a new UUID each time 37 | au.UserID = userId 38 | err := s.DB.Debug().Create(&au).Error 39 | if err != nil { 40 | return nil, err 41 | } 42 | return au, nil 43 | } 44 | -------------------------------------------------------------------------------- /server/model/auth_uuid_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "log" 6 | "jwt-across-platforms/server/auth" 7 | "testing" 8 | ) 9 | 10 | //Auth is created when a user signin. 11 | //The actual signing in of the user is not handled in the model. the model business is simply: 12 | //Give me a data that conform to the types i have established, then i will help you save it in the database. 13 | //Testing of signing in will be done in the controller. 14 | func TestCreateAuth_Success(t *testing.T) { 15 | //Initialize DB: 16 | var err error 17 | server.DB, err = server.database() 18 | if err != nil { 19 | log.Fatalf("cannot connect to the db: %v", err) 20 | } 21 | defer server.DB.Close() 22 | err = refreshUserTable() 23 | if err != nil { 24 | log.Fatalf("cannot refresh db tables: %v", err) 25 | } 26 | //lets see the database and get that user and use his id: 27 | user, err := seedOneUser() 28 | if err != nil { 29 | log.Fatalf("cannot refresh db tables: %v", err) 30 | } 31 | newAuth, err := server.CreateAuth(user.ID) 32 | assert.Nil(t, err) 33 | assert.EqualValues(t, newAuth.ID, 1) 34 | assert.EqualValues(t, newAuth.UserID, user.ID) 35 | //since, a random uuid will be created, lets asset that the uuid is not nil: 36 | assert.NotNil(t, newAuth.AuthUUID) 37 | } 38 | 39 | func TestFetchAuth_Success(t *testing.T) { 40 | //Initialize DB: 41 | var err error 42 | server.DB, err = server.database() 43 | if err != nil { 44 | log.Fatalf("cannot connect to the db: %v", err) 45 | } 46 | defer server.DB.Close() 47 | 48 | err = refreshUserTable() 49 | if err != nil { 50 | log.Fatalf("cannot refresh db tables: %v", err) 51 | } 52 | //lets see the database and get that auth: 53 | au, err := seedOneAuth() 54 | if err != nil { 55 | log.Fatalf("cannot refresh db tables: %v", err) 56 | } 57 | fetchAuth := &auth.AuthDetails{ 58 | AuthUuid: "43b78a87-6bcf-439a-ab2e-940d50c4dc33", 59 | UserId: 1, 60 | } 61 | gotAuth, err := server.FetchAuth(fetchAuth) 62 | assert.Nil(t, err) 63 | assert.EqualValues(t, gotAuth.ID, 1) 64 | assert.EqualValues(t, gotAuth.UserID, au.UserID) 65 | assert.EqualValues(t, gotAuth.AuthUUID, au.AuthUUID) 66 | } 67 | 68 | func TestDeleteAuth_Success(t *testing.T) { 69 | //Initialize DB: 70 | var err error 71 | server.DB, err = server.database() 72 | if err != nil { 73 | log.Fatalf("cannot connect to the db: %v", err) 74 | } 75 | defer server.DB.Close() 76 | 77 | err = refreshUserTable() 78 | if err != nil { 79 | log.Fatalf("cannot refresh db tables: %v", err) 80 | } 81 | //lets see the database and get that auth: 82 | au, err := seedOneAuth() 83 | if err != nil { 84 | log.Fatalf("cannot refresh db tables: %v", err) 85 | } 86 | fetchAuth := &auth.AuthDetails{ 87 | AuthUuid: au.AuthUUID, 88 | UserId: au.UserID, 89 | } 90 | err = server.DeleteAuth(fetchAuth) 91 | assert.Nil(t, err) 92 | } 93 | -------------------------------------------------------------------------------- /server/model/base_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | _ "github.com/jinzhu/gorm/dialects/postgres" //postgres database driver 7 | "jwt-across-platforms/server/auth" 8 | ) 9 | 10 | type Server struct { 11 | DB *gorm.DB 12 | } 13 | 14 | var ( 15 | //Server now implements the modelInterface, so he can define its methods 16 | Model modelInterface = &Server{} 17 | ) 18 | 19 | type modelInterface interface { 20 | //db initialization 21 | Initialize(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) (*gorm.DB, error) 22 | 23 | //user methods 24 | ValidateEmail(string) error 25 | CreateUser(*User) (*User, error) 26 | GetUserByEmail(string) (*User, error) 27 | 28 | //todo methods: 29 | CreateTodo(*Todo) (*Todo, error) 30 | 31 | 32 | //auth methods: 33 | FetchAuth(*auth.AuthDetails) (*Auth, error) 34 | DeleteAuth(*auth.AuthDetails) error 35 | CreateAuth(uint64) (*Auth, error) 36 | } 37 | 38 | func (s *Server) Initialize(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) (*gorm.DB, error) { 39 | var err error 40 | DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword) 41 | s.DB, err = gorm.Open(Dbdriver, DBURL) 42 | if err != nil { 43 | return nil, err 44 | } 45 | s.DB.Debug().AutoMigrate( 46 | &User{}, 47 | &Auth{}, 48 | &Todo{}, 49 | ) 50 | return s.DB, nil 51 | } 52 | -------------------------------------------------------------------------------- /server/model/base_model_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/joho/godotenv" 6 | "log" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestMain(m *testing.M) { 12 | var err error 13 | err = godotenv.Load(os.ExpandEnv("./../.env")) 14 | if err != nil { 15 | log.Fatalf("Error getting env %v\n", err) 16 | } 17 | os.Exit(m.Run()) 18 | } 19 | 20 | 21 | 22 | func (s *Server) database() (*gorm.DB, error) { 23 | dbDriver := os.Getenv("DB_DRIVER") 24 | username := os.Getenv("DB_USER_TEST") 25 | password := os.Getenv("DB_PASSWORD_TEST") 26 | host := os.Getenv("DB_HOST") 27 | database := os.Getenv("DB_NAME_TEST") 28 | port := os.Getenv("DB_PORT") 29 | 30 | //var err error 31 | return s.Initialize(dbDriver, username, password, port, host, database) 32 | //return s.DB, err 33 | } 34 | 35 | var ( 36 | server = Server{} 37 | ) 38 | //Drop test db data if exist: 39 | func refreshUserTable() error { 40 | err := server.DB.DropTableIfExists(&User{}, &Todo{}, &Auth{}).Error 41 | if err != nil { 42 | return err 43 | } 44 | err = server.DB.AutoMigrate(&User{}, &Todo{}, &Auth{}).Error 45 | if err != nil { 46 | return err 47 | } 48 | log.Printf("Successfully refreshed tables") 49 | return nil 50 | } 51 | 52 | func seedOneUser() (*User, error) { 53 | user := &User{ 54 | Email: "frank@gmail.com", 55 | } 56 | err := server.DB.Create(&user).Error 57 | if err != nil { 58 | return nil, err 59 | } 60 | return user, nil 61 | } 62 | 63 | func seedOneAuth() (*Auth, error) { 64 | au := &Auth{ 65 | AuthUUID: "43b78a87-6bcf-439a-ab2e-940d50c4dc33", 66 | UserID: 1, 67 | } 68 | err := server.DB.Create(&au).Error 69 | if err != nil { 70 | return nil, err 71 | } 72 | return au, nil 73 | } 74 | -------------------------------------------------------------------------------- /server/model/todo.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "errors" 4 | 5 | type Todo struct { 6 | ID uint64 `gorm:"primary_key;auto_increment" json:"id"` 7 | UserID uint64 `gorm:"not null" json:"user_id"` 8 | Title string `gorm:"size:255;not null" json:"title"` 9 | } 10 | 11 | func (s *Server) CreateTodo(todo *Todo) (*Todo, error) { 12 | if todo.Title == "" { 13 | return nil, errors.New("please provide a valid title") 14 | } 15 | if todo.UserID == 0 { 16 | return nil, errors.New("a valid user id is required") 17 | } 18 | err := s.DB.Debug().Create(&todo).Error 19 | if err != nil { 20 | return nil, err 21 | } 22 | return todo, nil 23 | } 24 | -------------------------------------------------------------------------------- /server/model/todo_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func TestCreateTodo_Success(t *testing.T) { 10 | //Initialize DB: 11 | var err error 12 | server.DB, err = server.database() 13 | if err != nil { 14 | log.Fatalf("cannot connect to the db: %v", err) 15 | } 16 | defer server.DB.Close() 17 | err = refreshUserTable() 18 | if err != nil { 19 | log.Fatalf("cannot refresh db tables: %v", err) 20 | } 21 | user, err := seedOneUser() 22 | if err != nil { 23 | log.Fatalf("cannot seed user: %v", err) 24 | } 25 | //Todo created 26 | todo := &Todo{Title: "Todo title", UserID: user.ID} 27 | newTodo, err := server.CreateTodo(todo) 28 | assert.Nil(t, err) 29 | assert.EqualValues(t, newTodo.ID, 1) 30 | assert.EqualValues(t, newTodo.Title, todo.Title) 31 | } 32 | 33 | //For the following tests, it is not the job of the model to check if the userId is valid or not. That must have checked in the controllers. What the tests do is, just to ensure that data is inserted in the database, to that affect, once we pass in a valid uint64 for the userId, irrespective of what the number is, our test will pass. 34 | func TestCreateTodo_Empty_Todo(t *testing.T) { 35 | //We will not be hitting the database since the execution will stop when the title is empty 36 | todo := &Todo{Title: "", UserID: 1} 37 | newTodo, err := server.CreateTodo(todo) 38 | assert.NotNil(t, err) 39 | assert.Nil(t, newTodo) 40 | assert.EqualValues(t, err.Error(), "please provide a valid title") 41 | } 42 | 43 | func TestCreateTodo_No_UserID(t *testing.T) { 44 | //We will not be hitting the database since the execution will stop when the userId is less than or equal to zero 45 | todo := &Todo{Title: "the title", UserID: 0} 46 | newTodo, err := server.CreateTodo(todo) 47 | assert.NotNil(t, err) 48 | assert.Nil(t, newTodo) 49 | assert.EqualValues(t, err.Error(), "a valid user id is required") 50 | } 51 | -------------------------------------------------------------------------------- /server/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "github.com/badoux/checkmail" 6 | ) 7 | 8 | type User struct { 9 | ID uint64 `gorm:"primary_key;auto_increment" json:"id"` 10 | Email string `gorm:"size:255;not null;unique" json:"email"` 11 | } 12 | 13 | func (s *Server) ValidateEmail(email string) error { 14 | if email == "" { 15 | return errors.New("required email") 16 | } 17 | if email != "" { 18 | if err := checkmail.ValidateFormat(email); err != nil { 19 | return errors.New("invalid email") 20 | } 21 | } 22 | return nil 23 | } 24 | 25 | func (s *Server) CreateUser(user *User) (*User, error) { 26 | emailErr := s.ValidateEmail(user.Email) 27 | if emailErr != nil { 28 | return nil, emailErr 29 | } 30 | err := s.DB.Debug().Create(&user).Error 31 | if err != nil { 32 | return nil, err 33 | } 34 | return user, nil 35 | } 36 | 37 | func (s *Server) GetUserByEmail(email string) (*User, error) { 38 | user := &User{} 39 | err := s.DB.Debug().Where("email = ?", email).Take(&user).Error 40 | if err != nil { 41 | return nil, err 42 | } 43 | return user, nil 44 | } 45 | 46 | -------------------------------------------------------------------------------- /server/model/user_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func TestValidateEmail_Success(t *testing.T) { 10 | //Correct email 11 | email := "stevensunflash@gmail.com" 12 | err := server.ValidateEmail(email) 13 | assert.Nil(t, err) 14 | } 15 | 16 | //Using table test to check the two failures at once 17 | func TestValidateEmail_Failure(t *testing.T) { 18 | samples := []struct { 19 | email string 20 | errMsgInvalid string 21 | errMsgEmptyEmail string 22 | }{ 23 | { 24 | //Invalid email 25 | email: "stevensunflash.com", 26 | errMsgInvalid: "invalid email", 27 | }, 28 | { 29 | //Empty email 30 | email: "", 31 | errMsgEmptyEmail: "required email", 32 | }, 33 | } 34 | for _, v := range samples { 35 | err := server.ValidateEmail(v.email) 36 | 37 | assert.NotNil(t, err) //there must be an error in either case 38 | 39 | if err != nil && v.errMsgInvalid != "" { 40 | assert.EqualValues(t, v.errMsgInvalid, "invalid email") 41 | } 42 | if err != nil && v.errMsgEmptyEmail != "" { 43 | assert.EqualValues(t, v.errMsgEmptyEmail, "required email") 44 | } 45 | } 46 | } 47 | 48 | func TestCreateUser_Success(t *testing.T) { 49 | //Initialize DB: 50 | var err error 51 | server.DB, err = server.database() 52 | if err != nil { 53 | log.Fatalf("cannot connect to the db: %v", err) 54 | } 55 | defer server.DB.Close() 56 | err = refreshUserTable() 57 | if err != nil { 58 | log.Fatalf("cannot refresh db tables: %v", err) 59 | } 60 | //User created 61 | user := &User{Email: "stevensunflash@gmail.com"} 62 | u, err := server.CreateUser(user) 63 | assert.Nil(t, err) 64 | assert.EqualValues(t, u.ID, 1) 65 | assert.EqualValues(t, u.Email, "stevensunflash@gmail.com") 66 | } 67 | 68 | func TestCreateUser_Duplicate_Email(t *testing.T) { 69 | //Initialize DB: 70 | var err error 71 | server.DB, err = server.database() 72 | if err != nil { 73 | log.Fatalf("cannot connect to the db: %v", err) 74 | } 75 | defer server.DB.Close() 76 | err = refreshUserTable() 77 | if err != nil { 78 | log.Fatalf("cannot refresh db tables: %v", err) 79 | } 80 | _, err = seedOneUser() 81 | if err != nil { 82 | log.Fatalf("cannot seed user: %v", err) 83 | } 84 | //remember we have seeded this user, so we want to insert him again, it should fail 85 | userRequest := &User{Email: "frank@gmail.com"} 86 | u, err := server.CreateUser(userRequest) 87 | assert.NotNil(t, err) 88 | assert.Nil(t, u) 89 | assert.Contains(t, err.Error(), "duplicate") 90 | } 91 | 92 | //We will test only for success here, you can write failure cases if you have time, and also to improve ur code coverage 93 | func TestGetUserByEmail_Success(t *testing.T) { 94 | //Initialize DB: 95 | var err error 96 | server.DB, err = server.database() 97 | if err != nil { 98 | log.Fatalf("cannot connect to the db: %v", err) 99 | } 100 | defer server.DB.Close() 101 | 102 | err = refreshUserTable() 103 | if err != nil { 104 | log.Fatalf("cannot refresh db tables: %v", err) 105 | } 106 | user, err := seedOneUser() 107 | if err != nil { 108 | log.Fatalf("cannot seed user: %v", err) 109 | } 110 | email := "frank@gmail.com" 111 | getUser, err := server.GetUserByEmail(email) 112 | assert.Nil(t, err) 113 | assert.EqualValues(t, getUser.Email, user.Email) 114 | } 115 | -------------------------------------------------------------------------------- /server/service/signin_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "jwt-across-platforms/server/auth" 4 | 5 | //because i will mock signin in the future while writing test cases, it will be defined in an interface: 6 | type sigInInterface interface { 7 | SignIn(auth.AuthDetails) (string, error) 8 | } 9 | 10 | type signInStruct struct {} 11 | 12 | //let expose this interface: 13 | var ( 14 | Authorize sigInInterface = &signInStruct{} 15 | ) 16 | 17 | func (si *signInStruct) SignIn(authD auth.AuthDetails) (string, error) { 18 | token, err := auth.CreateToken(authD) 19 | if err != nil { 20 | return "", err 21 | } 22 | return token, nil 23 | } -------------------------------------------------------------------------------- /server/service/signin_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "jwt-across-platforms/server/auth" 6 | "testing" 7 | ) 8 | 9 | var sign = signInStruct{} 10 | 11 | func TestSignInStruct_SignIn(t *testing.T) { 12 | var authD auth.AuthDetails 13 | authD.UserId = 1 14 | authD.AuthUuid = "83b09612-9dfc-4c1d-8f7d-a589acec7081" 15 | 16 | token, err := sign.SignIn(authD) 17 | assert.Nil(t, err) 18 | assert.NotNil(t, token) 19 | } --------------------------------------------------------------------------------