├── .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 | [](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 |
2 |
3 |

4 |
5 |
6 |
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 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
8 |
9 |
Installed CLI Plugins
10 |
14 |
Essential Links
15 |
22 |
Ecosystem
23 |
30 |
31 |
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 | }
--------------------------------------------------------------------------------