├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yaml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Dockerfile-test ├── LICENSE ├── Makefile ├── README.md ├── api ├── endpoints.go ├── endpoints_test.go ├── middlewares.go ├── middlewares_test.go ├── service.go ├── service_test.go └── transport.go ├── apispec ├── hooks.js ├── mock.json └── user.json ├── db ├── db.go ├── db_test.go └── mongodb │ ├── mongodb.go │ └── mongodb_test.go ├── docker-compose-zipkin.yml ├── docker-compose.yml ├── docker ├── user-db │ ├── Dockerfile │ └── scripts │ │ ├── accounts-create.js │ │ ├── address-insert.js │ │ ├── card-insert.js │ │ ├── customer-insert.js │ │ └── mongo_create_insert.sh └── user │ └── Dockerfile-release ├── glide.lock ├── glide.yaml ├── main.go ├── scripts ├── push.sh └── testcontainer.sh └── users ├── addresses.go ├── addresses_test.go ├── cards.go ├── cards_test.go ├── links.go ├── users.go └── users_test.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | We'd love to accept your contributions; large or small. Simply submit an issue or pull request via Github and involve one of the active members. Simple! But please read the rest of this document to ensure we're all on the same page. 4 | 5 | ## General Rules 6 | 7 | - Be kind and polite. Written language often does not convey the sentiment, so make sure you use lots of jokes and emoticons to get the sentiment across. 8 | - Prefer best practice. Everyone has their preferred style, but try to conform to current best practices. We don't enforce any strict rules. 9 | - Test your code to the best of your abilities. See the testing documentation for the correct scope of your test. 10 | 11 | ## Bug reports or feature requests 12 | 13 | Please open an issue if you have found an issue or have an idea for a new feature. Please follow the bug reporting guidelines if you submit an issue. 14 | 15 | ## New Contributors 16 | 17 | We have a list of issues on Github with "HelpWanted" labels attributed to them. These represent tasks that we don't have time to do, are self-contained and relatively easy to implement. If you'd like to contribute, but don't know where to start, [look here](https://github.com/microservices-demo/microservices-demo/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3AHelpWanted). 18 | 19 | ## Direction 20 | 21 | This project does have a general direction, but we're happy to consider deviating or pivoting from the direction we're currently heading. See the introductory material for details regarding direction. 22 | 23 | With that said, there is absolutely nothing stopping you from submitting a PR. If you've taken the effort to contribute, someone will make the effort to review. 24 | 25 | ## License 26 | 27 | This project is Apache v2.0 licenced. Submitting and merging a PR implies you accept these terms. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - Add labels appropriate to the issue 2 | - Describe the expected behaviour and the actual behaviour 3 | - Describe steps to reproduce the problem 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - Read the contribution guidelines 2 | - Include a reference to a related issue in this repository 3 | - A description of the changes proposed in the pull request -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" # run for branches 7 | tags: 8 | - "*" # run for tags 9 | pull_request: 10 | branches: 11 | - "*" # run for branches 12 | tags: 13 | - "*" # run for tags 14 | 15 | jobs: 16 | test: 17 | defaults: 18 | run: 19 | working-directory: go/src/github.com/microservices-demo/user 20 | runs-on: ubuntu-latest 21 | env: 22 | GROUP: weaveworksdemos 23 | COMMIT: ${{ github.sha }} 24 | REPO: user 25 | GO_VERSION: 1.7.5 26 | GOPATH: /home/runner/work/user/user/go 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | with: 31 | fetch-depth: 1 32 | path: go/src/github.com/microservices-demo/user 33 | 34 | - uses: actions/setup-go@v1 35 | with: 36 | go-version: ${{ env.GO_VERSION }} 37 | 38 | - name: Setup PATH 39 | run: echo "${GOPATH}/bin" >> $GITHUB_PATH 40 | 41 | - name: Install dependencies 42 | run: make deps && go get -v github.com/mattn/goveralls 43 | 44 | - name: Install/Downgrade MongoDB to version 3.6 45 | run: | 46 | wget -qO - https://www.mongodb.org/static/pgp/server-3.6.asc | sudo apt-key add - 47 | echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/3.6 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.6.list 48 | sudo apt-get update 49 | sudo apt-get install -y --allow-downgrades mongodb-org=3.6.20 mongodb-org-server=3.6.20 mongodb-org-shell=3.6.20 mongodb-org-mongos=3.6.20 mongodb-org-tools=3.6.20 || echo "Error is misleading, it was successfully installed" 50 | 51 | - name: Unit Tests 52 | run: glide novendor| xargs go test -v 53 | 54 | - name: Create cover profile 55 | run: make coverprofile 56 | 57 | - name: Run Docker Test 58 | run: make dockertest 59 | 60 | - name: Submit Coveralls 61 | env: 62 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | run: goveralls -coverprofile=cover.profile -service=github 64 | 65 | # Push to dockerhub 66 | - name: Push user to Docker Hub 67 | uses: docker/build-push-action@v1 68 | if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master' 69 | with: 70 | username: ${{ secrets.DOCKER_USER }} 71 | password: ${{ secrets.DOCKER_PASS }} 72 | dockerfile: go/src/github.com/microservices-demo/user/docker/user/Dockerfile-release 73 | path: go/src/github.com/microservices-demo/user 74 | repository: ${{ env.GROUP }}/${{ env.REPO }} 75 | tag_with_ref: true 76 | tag_with_sha: true 77 | 78 | - name: Push user-db to Docker Hub 79 | uses: docker/build-push-action@v1 80 | if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master' 81 | with: 82 | username: ${{ secrets.DOCKER_USER }} 83 | password: ${{ secrets.DOCKER_PASS }} 84 | dockerfile: go/src/github.com/microservices-demo/user/docker/user-db/Dockerfile 85 | path: go/src/github.com/microservices-demo/user/docker/user-db 86 | repository: ${{ env.GROUP }}/${{ env.REPO }} 87 | tag_with_ref: true 88 | tag_with_sha: true 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | docker/user/bin 3 | vendor 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.7.5 3 | sudo: required 4 | env: 5 | - GROUP=weaveworksdemos COMMIT=$TRAVIS_COMMIT TAG=$TRAVIS_TAG 6 | services: 7 | - docker 8 | - mongodb 9 | script: 10 | - make test 11 | - make coverprofile 12 | - make dockertest 13 | install: 14 | - make deps 15 | after_success: 16 | - go get -v github.com/mattn/goveralls 17 | - goveralls -coverprofile=cover.profile -service=travis-ci 18 | - make dockertravisbuild 19 | notifications: 20 | slack: 21 | secure: SRC7z+gqHO4oNFxZgxkPuLKlt5v8QnbgV8mzRIDbaCqyryCLqphD+dKRqF9Ou4gTJthF0++FrJcjso8dbKY1PjRqmNRNJBaeCAnXL34UDq6LQIULchcQjvZZjcNPIXObVrxsT1xd7qg354KtPbt9UsmcuvTiwGw1lsRsDRtB/c6OjCgA85b0QR3LYjL66kZogZew13J7Od/EgLXJqv+Ir9GL69jKpQ1Zu7pI9OXPX8R8PMNbjr6SbhnpkudTd9tipfUL+QsHaf18wuXRH44jOk7DCOWENLhimLYcxUPHSNItxvCH74ci4y5XyPCdfjX6N5x5aiv12SpE/150Cb9mZKbXETBxty8LD4doK8WuL/ov3ViMb15e1tVkrz050yyEHdNm1KzsqoMgxRWwnWstCj+fonsl2QRGsB63j2hEW4w+xm38vZcmaYJSMoZVXYEOl/LQxVHhIHP4jwm9tvwPihm4GjH4e5kwY5kEm7PFKXP2nScVSll2n3LMMd8dVW0J8ijlpt+Dl36CM9HI/2Q0stQeHEA88cQ2bGi8Q2GwUqFd13HnTyBuoS877tb2X/RMbv1wows2ZNeS0IXiLbEOata8yO4CHM17k978pmEKh29XDQtYcP5L5q0kBXj8w4EYGA6kJM9GVMwiqpjHFJnKhbr5OGJMGxg+EYF6E3XLrlo= 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.7-alpine 2 | ENV sourcesdir /go/src/github.com/microservices-demo/user/ 3 | ENV MONGO_HOST mytestdb:27017 4 | ENV HATEAOS user 5 | ENV USER_DATABASE mongodb 6 | 7 | COPY . ${sourcesdir} 8 | RUN apk update 9 | RUN apk add git 10 | RUN go get -v github.com/Masterminds/glide && cd ${sourcesdir} && glide install && go install 11 | 12 | ENTRYPOINT user 13 | EXPOSE 8084 14 | -------------------------------------------------------------------------------- /Dockerfile-test: -------------------------------------------------------------------------------- 1 | FROM mongo:3 2 | 3 | ENV sourcesdir /go/src/github.com/microservices-demo/user/ 4 | ENV GOPATH /go 5 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 6 | 7 | RUN apt-get update && apt-get install -yq git curl 8 | 9 | RUN curl -sSL https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz -o go.tar.gz && \ 10 | tar -C /usr/local -xvf go.tar.gz 11 | RUN go get -v github.com/Masterminds/glide 12 | 13 | COPY . ${sourcesdir} 14 | WORKDIR ${sourcesdir} 15 | RUN glide install 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = weaveworksdemos/user 2 | DBNAME = weaveworksdemos/user-db 3 | INSTANCE = user 4 | TESTDB = weaveworkstestuserdb 5 | OPENAPI = $(INSTANCE)-testopenapi 6 | GROUP = weaveworksdemos 7 | 8 | TAG=$(TRAVIS_COMMIT) 9 | 10 | default: docker 11 | 12 | 13 | pre: 14 | go get -v github.com/Masterminds/glide 15 | 16 | deps: pre 17 | glide install 18 | 19 | rm-deps: 20 | rm -rf vendor 21 | 22 | test: 23 | @docker build -t $(INSTANCE)-test -f ./Dockerfile-test . 24 | @docker run --rm -it $(INSTANCE)-test /bin/sh -c 'glide novendor| xargs go test -v' 25 | 26 | cover: 27 | @glide novendor|xargs go test -v -covermode=count 28 | 29 | coverprofile: 30 | go get github.com/modocache/gover 31 | go test -v -covermode=count -coverprofile=profile.coverprofile 32 | go test -v -covermode=count -coverprofile=db.coverprofile ./db 33 | go test -v -covermode=count -coverprofile=mongo.coverprofile ./db/mongodb 34 | go test -v -covermode=count -coverprofile=api.coverprofile ./api 35 | go test -v -covermode=count -coverprofile=users.coverprofile ./users 36 | gover 37 | mv gover.coverprofile cover.profile 38 | rm *.coverprofile 39 | 40 | 41 | dockerdev: 42 | docker build -t $(INSTANCE)-dev . 43 | 44 | dockertestdb: 45 | docker build -t $(TESTDB) -f docker/user-db/Dockerfile docker/user-db/ 46 | 47 | dockerruntest: dockertestdb dockerdev 48 | docker run -d --name my$(TESTDB) -h my$(TESTDB) $(TESTDB) 49 | docker run -d --name $(INSTANCE)-dev -p 8084:8084 --link my$(TESTDB) -e MONGO_HOST="my$(TESTDB):27017" $(INSTANCE)-dev 50 | 51 | docker: 52 | docker build -t $(NAME) -f docker/user/Dockerfile-release . 53 | 54 | dockerlocal: 55 | docker build -t $(INSTANCE)-local -f docker/user/Dockerfile-release . 56 | 57 | dockertravisbuild: 58 | docker build -t $(NAME):$(TAG) -f docker/user/Dockerfile-release . 59 | docker build -t $(DBNAME):$(TAG) -f docker/user-db/Dockerfile docker/user-db/ 60 | if [ -z "$(DOCKER_PASS)" ]; then \ 61 | echo "This is a build triggered by an external PR. Skipping docker push."; \ 62 | else \ 63 | docker login -u $(DOCKER_USER) -p $(DOCKER_PASS); \ 64 | scripts/push.sh; \ 65 | fi 66 | 67 | mockservice: 68 | docker run -d --name user-mock -h user-mock -v $(PWD)/apispec/mock.json:/data/db.json clue/json-server 69 | 70 | dockertest: dockerruntest 71 | scripts/testcontainer.sh 72 | docker run -h openapi --rm --name $(OPENAPI) --link user-dev -v $(PWD)/apispec/:/tmp/specs/\ 73 | weaveworksdemos/openapi /tmp/specs/$(INSTANCE).json\ 74 | http://$(INSTANCE)-dev:8084/\ 75 | -f /tmp/specs/hooks.js 76 | $(MAKE) cleandocker 77 | 78 | cleandocker: 79 | -docker rm -f my$(TESTDB) 80 | -docker rm -f $(INSTANCE)-dev 81 | -docker rm -f $(OPENAPI) 82 | -docker rm -f user-mock 83 | 84 | clean: cleandocker 85 | rm -rf bin 86 | rm -rf docker/user/bin 87 | rm -rf vendor 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED: User Service 2 | [![Build Status](https://travis-ci.org/microservices-demo/user.svg?branch=master)](https://travis-ci.org/microservices-demo/user) 3 | [![Coverage Status](https://coveralls.io/repos/github/microservices-demo/user/badge.svg?branch=master)](https://coveralls.io/github/microservices-demo/user?branch=master) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/microservices-demo/user)](https://goreportcard.com/report/github.com/microservices-demo/user) 5 | [![](https://images.microbadger.com/badges/image/weaveworksdemos/user.svg)](http://microbadger.com/images/weaveworksdemos/user "Get your own image badge on microbadger.com") 6 | 7 | This service covers user account storage, to include cards and addresses 8 | 9 | ## Bugs, Feature Requests and Contributing 10 | We'd love to see community contributions. We like to keep it simple and use Github issues to track bugs and feature requests and pull requests to manage contributions. 11 | 12 | >## API Spec 13 | 14 | Checkout the API Spec [here](http://microservices-demo.github.io/api/index?url=https://raw.githubusercontent.com/microservices-demo/user/master/apispec/user.json) 15 | 16 | >## Build 17 | 18 | ### Using Go natively 19 | 20 | ```bash 21 | make build 22 | ``` 23 | 24 | ### Using Docker Compose 25 | 26 | ```bash 27 | docker-compose build 28 | ``` 29 | 30 | >## Test 31 | 32 | ```bash 33 | make test 34 | ``` 35 | 36 | >## Run 37 | 38 | ### Natively 39 | ```bash 40 | docker-compose up -d user-db 41 | ./bin/user -port=8080 -database=mongodb -mongo-host=localhost:27017 42 | ``` 43 | 44 | ### Using Docker Compose 45 | ```bash 46 | docker-compose up 47 | ``` 48 | 49 | >## Check 50 | 51 | ```bash 52 | curl http://localhost:8080/health 53 | ``` 54 | 55 | >## Use 56 | 57 | Test user account passwords can be found in the comments in `users-db-test/scripts/customer-insert.js` 58 | 59 | ### Customers 60 | 61 | ```bash 62 | curl http://localhost:8080/customers 63 | ``` 64 | 65 | ### Cards 66 | ```bash 67 | curl http://localhost:8080/cards 68 | ``` 69 | 70 | ### Addresses 71 | 72 | ```bash 73 | curl http://localhost:8080/addresses 74 | ``` 75 | 76 | ### Login 77 | ```bash 78 | curl http://localhost:8080/login 79 | ``` 80 | 81 | ### Register 82 | 83 | ```bash 84 | curl http://localhost:8080/register 85 | ``` 86 | 87 | ## Push 88 | 89 | ```bash 90 | make dockertravisbuild 91 | ``` 92 | 93 | ## Test Zipkin 94 | 95 | To test with Zipkin 96 | 97 | ``` 98 | make 99 | docker-compose -f docker-compose-zipkin.yml build 100 | docker-compose -f docker-compose-zipkin.yml up 101 | ``` 102 | It takes about 10 seconds to seed data 103 | 104 | you should see it at: 105 | [http://localhost:9411/](http://localhost:9411) 106 | 107 | be sure to hit the "Find Traces" button. You may need to reload the page. 108 | 109 | when done you can run: 110 | ``` 111 | docker-compose -f docker-compose-zipkin.yml down 112 | ``` 113 | -------------------------------------------------------------------------------- /api/endpoints.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // endpoints.go contains the endpoint definitions, including per-method request 4 | // and response structs. Endpoints are the binding between the service and 5 | // transport. 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/go-kit/kit/endpoint" 11 | "github.com/go-kit/kit/tracing/opentracing" 12 | "github.com/microservices-demo/user/db" 13 | "github.com/microservices-demo/user/users" 14 | stdopentracing "github.com/opentracing/opentracing-go" 15 | ) 16 | 17 | // Endpoints collects the endpoints that comprise the Service. 18 | type Endpoints struct { 19 | LoginEndpoint endpoint.Endpoint 20 | RegisterEndpoint endpoint.Endpoint 21 | UserGetEndpoint endpoint.Endpoint 22 | UserPostEndpoint endpoint.Endpoint 23 | AddressGetEndpoint endpoint.Endpoint 24 | AddressPostEndpoint endpoint.Endpoint 25 | CardGetEndpoint endpoint.Endpoint 26 | CardPostEndpoint endpoint.Endpoint 27 | DeleteEndpoint endpoint.Endpoint 28 | HealthEndpoint endpoint.Endpoint 29 | } 30 | 31 | // MakeEndpoints returns an Endpoints structure, where each endpoint is 32 | // backed by the given service. 33 | func MakeEndpoints(s Service, tracer stdopentracing.Tracer) Endpoints { 34 | return Endpoints{ 35 | LoginEndpoint: opentracing.TraceServer(tracer, "GET /login")(MakeLoginEndpoint(s)), 36 | RegisterEndpoint: opentracing.TraceServer(tracer, "POST /register")(MakeRegisterEndpoint(s)), 37 | HealthEndpoint: opentracing.TraceServer(tracer, "GET /health")(MakeHealthEndpoint(s)), 38 | UserGetEndpoint: opentracing.TraceServer(tracer, "GET /customers")(MakeUserGetEndpoint(s)), 39 | UserPostEndpoint: opentracing.TraceServer(tracer, "POST /customers")(MakeUserPostEndpoint(s)), 40 | AddressGetEndpoint: opentracing.TraceServer(tracer, "GET /addresses")(MakeAddressGetEndpoint(s)), 41 | AddressPostEndpoint: opentracing.TraceServer(tracer, "POST /addresses")(MakeAddressPostEndpoint(s)), 42 | CardGetEndpoint: opentracing.TraceServer(tracer, "GET /cards")(MakeCardGetEndpoint(s)), 43 | DeleteEndpoint: opentracing.TraceServer(tracer, "DELETE /")(MakeDeleteEndpoint(s)), 44 | CardPostEndpoint: opentracing.TraceServer(tracer, "POST /cards")(MakeCardPostEndpoint(s)), 45 | } 46 | } 47 | 48 | // MakeLoginEndpoint returns an endpoint via the given service. 49 | func MakeLoginEndpoint(s Service) endpoint.Endpoint { 50 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 51 | var span stdopentracing.Span 52 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "login user") 53 | span.SetTag("service", "user") 54 | defer span.Finish() 55 | req := request.(loginRequest) 56 | u, err := s.Login(req.Username, req.Password) 57 | return userResponse{User: u}, err 58 | } 59 | } 60 | 61 | // MakeRegisterEndpoint returns an endpoint via the given service. 62 | func MakeRegisterEndpoint(s Service) endpoint.Endpoint { 63 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 64 | var span stdopentracing.Span 65 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "register user") 66 | span.SetTag("service", "user") 67 | defer span.Finish() 68 | req := request.(registerRequest) 69 | id, err := s.Register(req.Username, req.Password, req.Email, req.FirstName, req.LastName) 70 | return postResponse{ID: id}, err 71 | } 72 | } 73 | 74 | // MakeUserGetEndpoint returns an endpoint via the given service. 75 | func MakeUserGetEndpoint(s Service) endpoint.Endpoint { 76 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 77 | var span stdopentracing.Span 78 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "get users") 79 | span.SetTag("service", "user") 80 | defer span.Finish() 81 | 82 | req := request.(GetRequest) 83 | 84 | userspan := stdopentracing.StartSpan("users from db", stdopentracing.ChildOf(span.Context())) 85 | usrs, err := s.GetUsers(req.ID) 86 | userspan.Finish() 87 | if req.ID == "" { 88 | return EmbedStruct{usersResponse{Users: usrs}}, err 89 | } 90 | if len(usrs) == 0 { 91 | if req.Attr == "addresses" { 92 | return EmbedStruct{addressesResponse{Addresses: make([]users.Address, 0)}}, err 93 | } 94 | if req.Attr == "cards" { 95 | return EmbedStruct{cardsResponse{Cards: make([]users.Card, 0)}}, err 96 | } 97 | return users.User{}, err 98 | } 99 | user := usrs[0] 100 | attrspan := stdopentracing.StartSpan("attributes from db", stdopentracing.ChildOf(span.Context())) 101 | db.GetUserAttributes(&user) 102 | attrspan.Finish() 103 | if req.Attr == "addresses" { 104 | return EmbedStruct{addressesResponse{Addresses: user.Addresses}}, err 105 | } 106 | if req.Attr == "cards" { 107 | return EmbedStruct{cardsResponse{Cards: user.Cards}}, err 108 | } 109 | return user, err 110 | } 111 | } 112 | 113 | // MakeUserPostEndpoint returns an endpoint via the given service. 114 | func MakeUserPostEndpoint(s Service) endpoint.Endpoint { 115 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 116 | var span stdopentracing.Span 117 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "post user") 118 | span.SetTag("service", "user") 119 | defer span.Finish() 120 | req := request.(users.User) 121 | id, err := s.PostUser(req) 122 | return postResponse{ID: id}, err 123 | } 124 | } 125 | 126 | // MakeAddressGetEndpoint returns an endpoint via the given service. 127 | func MakeAddressGetEndpoint(s Service) endpoint.Endpoint { 128 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 129 | var span stdopentracing.Span 130 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "get users") 131 | span.SetTag("service", "user") 132 | defer span.Finish() 133 | req := request.(GetRequest) 134 | addrspan := stdopentracing.StartSpan("addresses from db", stdopentracing.ChildOf(span.Context())) 135 | adds, err := s.GetAddresses(req.ID) 136 | addrspan.Finish() 137 | if req.ID == "" { 138 | return EmbedStruct{addressesResponse{Addresses: adds}}, err 139 | } 140 | if len(adds) == 0 { 141 | return users.Address{}, err 142 | } 143 | return adds[0], err 144 | } 145 | } 146 | 147 | // MakeAddressPostEndpoint returns an endpoint via the given service. 148 | func MakeAddressPostEndpoint(s Service) endpoint.Endpoint { 149 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 150 | var span stdopentracing.Span 151 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "post address") 152 | span.SetTag("service", "user") 153 | defer span.Finish() 154 | req := request.(addressPostRequest) 155 | id, err := s.PostAddress(req.Address, req.UserID) 156 | return postResponse{ID: id}, err 157 | } 158 | } 159 | 160 | // MakeUserGetEndpoint returns an endpoint via the given service. 161 | func MakeCardGetEndpoint(s Service) endpoint.Endpoint { 162 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 163 | var span stdopentracing.Span 164 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "get cards") 165 | span.SetTag("service", "user") 166 | defer span.Finish() 167 | req := request.(GetRequest) 168 | cardspan := stdopentracing.StartSpan("addresses from db", stdopentracing.ChildOf(span.Context())) 169 | cards, err := s.GetCards(req.ID) 170 | cardspan.Finish() 171 | if req.ID == "" { 172 | return EmbedStruct{cardsResponse{Cards: cards}}, err 173 | } 174 | if len(cards) == 0 { 175 | return users.Card{}, err 176 | } 177 | return cards[0], err 178 | } 179 | } 180 | 181 | // MakeCardPostEndpoint returns an endpoint via the given service. 182 | func MakeCardPostEndpoint(s Service) endpoint.Endpoint { 183 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 184 | var span stdopentracing.Span 185 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "post card") 186 | span.SetTag("service", "user") 187 | defer span.Finish() 188 | req := request.(cardPostRequest) 189 | id, err := s.PostCard(req.Card, req.UserID) 190 | return postResponse{ID: id}, err 191 | } 192 | } 193 | 194 | // MakeLoginEndpoint returns an endpoint via the given service. 195 | func MakeDeleteEndpoint(s Service) endpoint.Endpoint { 196 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 197 | var span stdopentracing.Span 198 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "delete entity") 199 | span.SetTag("service", "user") 200 | defer span.Finish() 201 | req := request.(deleteRequest) 202 | err = s.Delete(req.Entity, req.ID) 203 | if err == nil { 204 | return statusResponse{Status: true}, err 205 | } 206 | return statusResponse{Status: false}, err 207 | } 208 | } 209 | 210 | // MakeHealthEndpoint returns current health of the given service. 211 | func MakeHealthEndpoint(s Service) endpoint.Endpoint { 212 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { 213 | var span stdopentracing.Span 214 | span, ctx = stdopentracing.StartSpanFromContext(ctx, "health check") 215 | span.SetTag("service", "user") 216 | defer span.Finish() 217 | health := s.Health() 218 | return healthResponse{Health: health}, nil 219 | } 220 | } 221 | 222 | type GetRequest struct { 223 | ID string 224 | Attr string 225 | } 226 | 227 | type loginRequest struct { 228 | Username string 229 | Password string 230 | } 231 | 232 | type userResponse struct { 233 | User users.User `json:"user"` 234 | } 235 | 236 | type usersResponse struct { 237 | Users []users.User `json:"customer"` 238 | } 239 | 240 | type addressPostRequest struct { 241 | users.Address 242 | UserID string `json:"userID"` 243 | } 244 | 245 | type addressesResponse struct { 246 | Addresses []users.Address `json:"address"` 247 | } 248 | 249 | type cardPostRequest struct { 250 | users.Card 251 | UserID string `json:"userID"` 252 | } 253 | 254 | type cardsResponse struct { 255 | Cards []users.Card `json:"card"` 256 | } 257 | 258 | type registerRequest struct { 259 | Username string `json:"username"` 260 | Password string `json:"password"` 261 | Email string `json:"email"` 262 | FirstName string `json:"firstName"` 263 | LastName string `json:"lastName"` 264 | } 265 | 266 | type statusResponse struct { 267 | Status bool `json:"status"` 268 | } 269 | 270 | type postResponse struct { 271 | ID string `json:"id"` 272 | } 273 | 274 | type deleteRequest struct { 275 | Entity string 276 | ID string 277 | } 278 | 279 | type healthRequest struct { 280 | // 281 | } 282 | 283 | type healthResponse struct { 284 | Health []Health `json:"health"` 285 | } 286 | 287 | type EmbedStruct struct { 288 | Embed interface{} `json:"_embedded"` 289 | } 290 | -------------------------------------------------------------------------------- /api/endpoints_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | /// needs actual tests 4 | 5 | import "testing" 6 | 7 | func TestMakeEndpoints(t *testing.T) { 8 | // eps := MakeEndpoints(TestService) 9 | } 10 | 11 | func TestMakeLoginEndpoint(t *testing.T) { 12 | // l := MakeLoginEndpoint(TestService) 13 | } 14 | 15 | func TestMakeRegisterEndpoint(t *testing.T) { 16 | // r := MakeRegisterEndpoint(TestService) 17 | } 18 | -------------------------------------------------------------------------------- /api/middlewares.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-kit/kit/log" 7 | "github.com/go-kit/kit/metrics" 8 | "github.com/microservices-demo/user/users" 9 | ) 10 | 11 | // Middleware decorates a service. 12 | type Middleware func(Service) Service 13 | 14 | // LoggingMiddleware logs method calls, parameters, results, and elapsed time. 15 | func LoggingMiddleware(logger log.Logger) Middleware { 16 | return func(next Service) Service { 17 | return loggingMiddleware{ 18 | next: next, 19 | logger: logger, 20 | } 21 | } 22 | } 23 | 24 | type loggingMiddleware struct { 25 | next Service 26 | logger log.Logger 27 | } 28 | 29 | func (mw loggingMiddleware) Login(username, password string) (user users.User, err error) { 30 | defer func(begin time.Time) { 31 | mw.logger.Log( 32 | "method", "Login", 33 | "took", time.Since(begin), 34 | ) 35 | }(time.Now()) 36 | return mw.next.Login(username, password) 37 | } 38 | 39 | func (mw loggingMiddleware) Register(username, password, email, first, last string) (string, error) { 40 | defer func(begin time.Time) { 41 | mw.logger.Log( 42 | "method", "Register", 43 | "username", username, 44 | "email", email, 45 | "took", time.Since(begin), 46 | ) 47 | }(time.Now()) 48 | return mw.next.Register(username, password, email, first, last) 49 | } 50 | 51 | func (mw loggingMiddleware) PostUser(user users.User) (id string, err error) { 52 | defer func(begin time.Time) { 53 | mw.logger.Log( 54 | "method", "PostUser", 55 | "username", user.Username, 56 | "email", user.Email, 57 | "result", id, 58 | "took", time.Since(begin), 59 | ) 60 | }(time.Now()) 61 | return mw.next.PostUser(user) 62 | } 63 | 64 | func (mw loggingMiddleware) GetUsers(id string) (u []users.User, err error) { 65 | defer func(begin time.Time) { 66 | who := id 67 | if who == "" { 68 | who = "all" 69 | } 70 | mw.logger.Log( 71 | "method", "GetUsers", 72 | "id", who, 73 | "result", len(u), 74 | "took", time.Since(begin), 75 | ) 76 | }(time.Now()) 77 | return mw.next.GetUsers(id) 78 | } 79 | 80 | func (mw loggingMiddleware) PostAddress(add users.Address, id string) (string, error) { 81 | defer func(begin time.Time) { 82 | mw.logger.Log( 83 | "method", "PostAddress", 84 | "street", add.Street, 85 | "number", add.Number, 86 | "user", id, 87 | "took", time.Since(begin), 88 | ) 89 | }(time.Now()) 90 | return mw.next.PostAddress(add, id) 91 | } 92 | 93 | func (mw loggingMiddleware) GetAddresses(id string) (a []users.Address, err error) { 94 | defer func(begin time.Time) { 95 | who := id 96 | if who == "" { 97 | who = "all" 98 | } 99 | mw.logger.Log( 100 | "method", "GetAddresses", 101 | "id", who, 102 | "result", len(a), 103 | "took", time.Since(begin), 104 | ) 105 | }(time.Now()) 106 | return mw.next.GetAddresses(id) 107 | } 108 | 109 | func (mw loggingMiddleware) PostCard(card users.Card, id string) (string, error) { 110 | defer func(begin time.Time) { 111 | cc := card 112 | cc.MaskCC() 113 | mw.logger.Log( 114 | "method", "PostCard", 115 | "card", cc.LongNum, 116 | "user", id, 117 | "took", time.Since(begin), 118 | ) 119 | }(time.Now()) 120 | return mw.next.PostCard(card, id) 121 | } 122 | 123 | func (mw loggingMiddleware) GetCards(id string) (a []users.Card, err error) { 124 | defer func(begin time.Time) { 125 | who := id 126 | if who == "" { 127 | who = "all" 128 | } 129 | mw.logger.Log( 130 | "method", "GetCards", 131 | "id", who, 132 | "result", len(a), 133 | "took", time.Since(begin), 134 | ) 135 | }(time.Now()) 136 | return mw.next.GetCards(id) 137 | } 138 | 139 | func (mw loggingMiddleware) Delete(entity, id string) (err error) { 140 | defer func(begin time.Time) { 141 | mw.logger.Log( 142 | "method", "Delete", 143 | "entity", entity, 144 | "id", id, 145 | "took", time.Since(begin), 146 | ) 147 | }(time.Now()) 148 | return mw.next.Delete(entity, id) 149 | } 150 | 151 | func (mw loggingMiddleware) Health() (health []Health) { 152 | defer func(begin time.Time) { 153 | mw.logger.Log( 154 | "method", "Health", 155 | "result", len(health), 156 | "took", time.Since(begin), 157 | ) 158 | }(time.Now()) 159 | return mw.next.Health() 160 | } 161 | 162 | type instrumentingService struct { 163 | requestCount metrics.Counter 164 | requestLatency metrics.Histogram 165 | Service 166 | } 167 | 168 | // NewInstrumentingService returns an instance of an instrumenting Service. 169 | func NewInstrumentingService(requestCount metrics.Counter, requestLatency metrics.Histogram, s Service) Service { 170 | return &instrumentingService{ 171 | requestCount: requestCount, 172 | requestLatency: requestLatency, 173 | Service: s, 174 | } 175 | } 176 | 177 | func (s *instrumentingService) Login(username, password string) (users.User, error) { 178 | defer func(begin time.Time) { 179 | s.requestCount.With("method", "login").Add(1) 180 | s.requestLatency.With("method", "login").Observe(time.Since(begin).Seconds()) 181 | }(time.Now()) 182 | 183 | return s.Service.Login(username, password) 184 | } 185 | 186 | func (s *instrumentingService) Register(username, password, email, first, last string) (string, error) { 187 | defer func(begin time.Time) { 188 | s.requestCount.With("method", "register").Add(1) 189 | s.requestLatency.With("method", "register").Observe(time.Since(begin).Seconds()) 190 | }(time.Now()) 191 | 192 | return s.Service.Register(username, password, email, first, last) 193 | } 194 | 195 | func (s *instrumentingService) PostUser(user users.User) (string, error) { 196 | defer func(begin time.Time) { 197 | s.requestCount.With("method", "postUser").Add(1) 198 | s.requestLatency.With("method", "postUser").Observe(time.Since(begin).Seconds()) 199 | }(time.Now()) 200 | 201 | return s.Service.PostUser(user) 202 | } 203 | 204 | func (s *instrumentingService) GetUsers(id string) (u []users.User, err error) { 205 | defer func(begin time.Time) { 206 | s.requestCount.With("method", "getUsers").Add(1) 207 | s.requestLatency.With("method", "getUsers").Observe(time.Since(begin).Seconds()) 208 | }(time.Now()) 209 | 210 | return s.Service.GetUsers(id) 211 | } 212 | 213 | func (s *instrumentingService) PostAddress(add users.Address, id string) (string, error) { 214 | defer func(begin time.Time) { 215 | s.requestCount.With("method", "postAddress").Add(1) 216 | s.requestLatency.With("method", "postAddress").Observe(time.Since(begin).Seconds()) 217 | }(time.Now()) 218 | 219 | return s.Service.PostAddress(add, id) 220 | } 221 | 222 | func (s *instrumentingService) GetAddresses(id string) ([]users.Address, error) { 223 | defer func(begin time.Time) { 224 | s.requestCount.With("method", "getAddresses").Add(1) 225 | s.requestLatency.With("method", "getAddresses").Observe(time.Since(begin).Seconds()) 226 | }(time.Now()) 227 | 228 | return s.Service.GetAddresses(id) 229 | } 230 | 231 | func (s *instrumentingService) PostCard(card users.Card, id string) (string, error) { 232 | defer func(begin time.Time) { 233 | s.requestCount.With("method", "postCard").Add(1) 234 | s.requestLatency.With("method", "postCard").Observe(time.Since(begin).Seconds()) 235 | }(time.Now()) 236 | 237 | return s.Service.PostCard(card, id) 238 | } 239 | 240 | func (s *instrumentingService) GetCards(id string) ([]users.Card, error) { 241 | defer func(begin time.Time) { 242 | s.requestCount.With("method", "getCards").Add(1) 243 | s.requestLatency.With("method", "getCards").Observe(time.Since(begin).Seconds()) 244 | }(time.Now()) 245 | 246 | return s.Service.GetCards(id) 247 | } 248 | 249 | func (s *instrumentingService) Delete(entity, id string) error { 250 | defer func(begin time.Time) { 251 | s.requestCount.With("method", "delete").Add(1) 252 | s.requestLatency.With("method", "delete").Observe(time.Since(begin).Seconds()) 253 | }(time.Now()) 254 | 255 | return s.Service.Delete(entity, id) 256 | } 257 | 258 | func (s *instrumentingService) Health() []Health { 259 | defer func(begin time.Time) { 260 | s.requestCount.With("method", "health").Add(1) 261 | s.requestLatency.With("method", "health").Observe(time.Since(begin).Seconds()) 262 | }(time.Now()) 263 | 264 | return s.Service.Health() 265 | } 266 | -------------------------------------------------------------------------------- /api/middlewares_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | TestLogger bogusLogger = newBogusLogger() 10 | TestMiddleWare Service = LoggingMiddleware(TestLogger)(NewFixedService()) 11 | ) 12 | 13 | type bogusLogger struct { 14 | } 15 | 16 | func newBogusLogger() bogusLogger { 17 | return bogusLogger{} 18 | } 19 | 20 | func (bl bogusLogger) Log(v ...interface{}) error { 21 | _, err := fmt.Println(v) 22 | return err 23 | } 24 | 25 | func TestLoginMiddleWare(t *testing.T) { 26 | } 27 | -------------------------------------------------------------------------------- /api/service.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // service.go contains the definition and implementation (business logic) of the 4 | // user service. Everything here is agnostic to the transport (HTTP). 5 | 6 | import ( 7 | "crypto/sha1" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "time" 12 | 13 | "github.com/microservices-demo/user/db" 14 | "github.com/microservices-demo/user/users" 15 | ) 16 | 17 | var ( 18 | ErrUnauthorized = errors.New("Unauthorized") 19 | ) 20 | 21 | // Service is the user service, providing operations for users to login, register, and retrieve customer information. 22 | type Service interface { 23 | Login(username, password string) (users.User, error) // GET /login 24 | Register(username, password, email, first, last string) (string, error) 25 | GetUsers(id string) ([]users.User, error) 26 | PostUser(u users.User) (string, error) 27 | GetAddresses(id string) ([]users.Address, error) 28 | PostAddress(u users.Address, userid string) (string, error) 29 | GetCards(id string) ([]users.Card, error) 30 | PostCard(u users.Card, userid string) (string, error) 31 | Delete(entity, id string) error 32 | Health() []Health // GET /health 33 | } 34 | 35 | // NewFixedService returns a simple implementation of the Service interface, 36 | func NewFixedService() Service { 37 | return &fixedService{} 38 | } 39 | 40 | type fixedService struct{} 41 | 42 | type Health struct { 43 | Service string `json:"service"` 44 | Status string `json:"status"` 45 | Time string `json:"time"` 46 | } 47 | 48 | func (s *fixedService) Login(username, password string) (users.User, error) { 49 | u, err := db.GetUserByName(username) 50 | if err != nil { 51 | return users.New(), err 52 | } 53 | if u.Password != calculatePassHash(password, u.Salt) { 54 | return users.New(), ErrUnauthorized 55 | } 56 | db.GetUserAttributes(&u) 57 | u.MaskCCs() 58 | return u, nil 59 | 60 | } 61 | 62 | func (s *fixedService) Register(username, password, email, first, last string) (string, error) { 63 | u := users.New() 64 | u.Username = username 65 | u.Password = calculatePassHash(password, u.Salt) 66 | u.Email = email 67 | u.FirstName = first 68 | u.LastName = last 69 | err := db.CreateUser(&u) 70 | return u.UserID, err 71 | } 72 | 73 | func (s *fixedService) GetUsers(id string) ([]users.User, error) { 74 | if id == "" { 75 | us, err := db.GetUsers() 76 | for k, u := range us { 77 | u.AddLinks() 78 | us[k] = u 79 | } 80 | return us, err 81 | } 82 | u, err := db.GetUser(id) 83 | u.AddLinks() 84 | return []users.User{u}, err 85 | } 86 | 87 | func (s *fixedService) PostUser(u users.User) (string, error) { 88 | u.NewSalt() 89 | u.Password = calculatePassHash(u.Password, u.Salt) 90 | err := db.CreateUser(&u) 91 | return u.UserID, err 92 | } 93 | 94 | func (s *fixedService) GetAddresses(id string) ([]users.Address, error) { 95 | if id == "" { 96 | as, err := db.GetAddresses() 97 | for k, a := range as { 98 | a.AddLinks() 99 | as[k] = a 100 | } 101 | return as, err 102 | } 103 | a, err := db.GetAddress(id) 104 | a.AddLinks() 105 | return []users.Address{a}, err 106 | } 107 | 108 | func (s *fixedService) PostAddress(add users.Address, userid string) (string, error) { 109 | err := db.CreateAddress(&add, userid) 110 | return add.ID, err 111 | } 112 | 113 | func (s *fixedService) GetCards(id string) ([]users.Card, error) { 114 | if id == "" { 115 | cs, err := db.GetCards() 116 | for k, c := range cs { 117 | c.AddLinks() 118 | cs[k] = c 119 | } 120 | return cs, err 121 | } 122 | c, err := db.GetCard(id) 123 | c.AddLinks() 124 | return []users.Card{c}, err 125 | } 126 | 127 | func (s *fixedService) PostCard(card users.Card, userid string) (string, error) { 128 | err := db.CreateCard(&card, userid) 129 | return card.ID, err 130 | } 131 | 132 | func (s *fixedService) Delete(entity, id string) error { 133 | return db.Delete(entity, id) 134 | } 135 | 136 | func (s *fixedService) Health() []Health { 137 | var health []Health 138 | dbstatus := "OK" 139 | 140 | err := db.Ping() 141 | if err != nil { 142 | dbstatus = "err" 143 | } 144 | 145 | app := Health{"user", "OK", time.Now().String()} 146 | db := Health{"user-db", dbstatus, time.Now().String()} 147 | 148 | health = append(health, app) 149 | health = append(health, db) 150 | 151 | return health 152 | } 153 | 154 | func calculatePassHash(pass, salt string) string { 155 | h := sha1.New() 156 | io.WriteString(h, salt) 157 | io.WriteString(h, pass) 158 | return fmt.Sprintf("%x", h.Sum(nil)) 159 | } 160 | -------------------------------------------------------------------------------- /api/service_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/microservices-demo/user/users" 7 | ) 8 | 9 | var ( 10 | TestService Service 11 | TestCustomer = users.User{Username: "testuser", Password: ""} 12 | ) 13 | 14 | func init() { 15 | TestService = NewFixedService() 16 | } 17 | 18 | func TestLogin(t *testing.T) { 19 | 20 | } 21 | 22 | func TestRegister(t *testing.T) { 23 | 24 | } 25 | 26 | func TestCalculatePassHash(t *testing.T) { 27 | hash1 := calculatePassHash("eve", "c748112bc027878aa62812ba1ae00e40ad46d497") 28 | if hash1 != "fec51acb3365747fc61247da5e249674cf8463c2" { 29 | t.Error("Eve's password failed hash test") 30 | } 31 | hash2 := calculatePassHash("password", "6c1c6176e8b455ef37da13d953df971c249d0d8e") 32 | if hash2 != "e2de7202bb2201842d041f6de201b10438369fb8" { 33 | t.Error("user's password failed hash test") 34 | } 35 | hash3 := calculatePassHash("password", "bd832b0e10c6882deabc5e8e60a37689e2b708c2") 36 | if hash3 != "8f31df4dcc25694aeb0c212118ae37bbd6e47bcd" { 37 | t.Error("user1's password failed hash test") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /api/transport.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // transport.go contains the binding from endpoints to a concrete transport. 4 | // In our case we just use a REST-y HTTP transport. 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/go-kit/kit/log" 14 | "github.com/go-kit/kit/tracing/opentracing" 15 | httptransport "github.com/go-kit/kit/transport/http" 16 | "github.com/gorilla/mux" 17 | "github.com/microservices-demo/user/users" 18 | stdopentracing "github.com/opentracing/opentracing-go" 19 | "github.com/prometheus/client_golang/prometheus/promhttp" 20 | ) 21 | 22 | var ( 23 | ErrInvalidRequest = errors.New("Invalid request") 24 | ) 25 | 26 | // MakeHTTPHandler mounts the endpoints into a REST-y HTTP handler. 27 | func MakeHTTPHandler(e Endpoints, logger log.Logger, tracer stdopentracing.Tracer) *mux.Router { 28 | r := mux.NewRouter().StrictSlash(false) 29 | options := []httptransport.ServerOption{ 30 | httptransport.ServerErrorLogger(logger), 31 | httptransport.ServerErrorEncoder(encodeError), 32 | } 33 | 34 | // GET /login Login 35 | // GET /register Register 36 | // GET /health Health Check 37 | 38 | r.Methods("GET").Path("/login").Handler(httptransport.NewServer( 39 | e.LoginEndpoint, 40 | decodeLoginRequest, 41 | encodeResponse, 42 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "GET /login", logger)))..., 43 | )) 44 | r.Methods("POST").Path("/register").Handler(httptransport.NewServer( 45 | e.RegisterEndpoint, 46 | decodeRegisterRequest, 47 | encodeResponse, 48 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "POST /register", logger)))..., 49 | )) 50 | r.Methods("GET").PathPrefix("/customers").Handler(httptransport.NewServer( 51 | e.UserGetEndpoint, 52 | decodeGetRequest, 53 | encodeResponse, 54 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "GET /customers", logger)))..., 55 | )) 56 | r.Methods("GET").PathPrefix("/cards").Handler(httptransport.NewServer( 57 | e.CardGetEndpoint, 58 | decodeGetRequest, 59 | encodeResponse, 60 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "GET /cards", logger)))..., 61 | )) 62 | r.Methods("GET").PathPrefix("/addresses").Handler(httptransport.NewServer( 63 | e.AddressGetEndpoint, 64 | decodeGetRequest, 65 | encodeResponse, 66 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "GET /addresses", logger)))..., 67 | )) 68 | r.Methods("POST").Path("/customers").Handler(httptransport.NewServer( 69 | e.UserPostEndpoint, 70 | decodeUserRequest, 71 | encodeResponse, 72 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "POST /customers", logger)))..., 73 | )) 74 | r.Methods("POST").Path("/addresses").Handler(httptransport.NewServer( 75 | e.AddressPostEndpoint, 76 | decodeAddressRequest, 77 | encodeResponse, 78 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "POST /addresses", logger)))..., 79 | )) 80 | r.Methods("POST").Path("/cards").Handler(httptransport.NewServer( 81 | e.CardPostEndpoint, 82 | decodeCardRequest, 83 | encodeResponse, 84 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "POST /cards", logger)))..., 85 | )) 86 | r.Methods("DELETE").PathPrefix("/").Handler(httptransport.NewServer( 87 | e.DeleteEndpoint, 88 | decodeDeleteRequest, 89 | encodeResponse, 90 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "DELETE /", logger)))..., 91 | )) 92 | r.Methods("GET").PathPrefix("/health").Handler(httptransport.NewServer( 93 | e.HealthEndpoint, 94 | decodeHealthRequest, 95 | encodeHealthResponse, 96 | append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "GET /health", logger)))..., 97 | )) 98 | r.Handle("/metrics", promhttp.Handler()) 99 | return r 100 | } 101 | 102 | func encodeError(_ context.Context, err error, w http.ResponseWriter) { 103 | code := http.StatusInternalServerError 104 | switch err { 105 | case ErrUnauthorized: 106 | code = http.StatusUnauthorized 107 | } 108 | w.WriteHeader(code) 109 | w.Header().Set("Content-Type", "application/hal+json") 110 | json.NewEncoder(w).Encode(map[string]interface{}{ 111 | "error": err.Error(), 112 | "status_code": code, 113 | "status_text": http.StatusText(code), 114 | }) 115 | } 116 | 117 | func decodeLoginRequest(_ context.Context, r *http.Request) (interface{}, error) { 118 | u, p, ok := r.BasicAuth() 119 | if !ok { 120 | return loginRequest{}, ErrUnauthorized 121 | } 122 | 123 | return loginRequest{ 124 | Username: u, 125 | Password: p, 126 | }, nil 127 | } 128 | 129 | func decodeRegisterRequest(_ context.Context, r *http.Request) (interface{}, error) { 130 | reg := registerRequest{} 131 | err := json.NewDecoder(r.Body).Decode(®) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return reg, nil 136 | } 137 | 138 | func decodeDeleteRequest(_ context.Context, r *http.Request) (interface{}, error) { 139 | d := deleteRequest{} 140 | u := strings.Split(r.URL.Path, "/") 141 | if len(u) == 3 { 142 | d.Entity = u[1] 143 | d.ID = u[2] 144 | return d, nil 145 | } 146 | return d, ErrInvalidRequest 147 | } 148 | 149 | func decodeGetRequest(_ context.Context, r *http.Request) (interface{}, error) { 150 | g := GetRequest{} 151 | u := strings.Split(r.URL.Path, "/") 152 | if len(u) > 2 { 153 | g.ID = u[2] 154 | if len(u) > 3 { 155 | g.Attr = u[3] 156 | } 157 | } 158 | return g, nil 159 | } 160 | 161 | func decodeUserRequest(_ context.Context, r *http.Request) (interface{}, error) { 162 | defer r.Body.Close() 163 | u := users.User{} 164 | err := json.NewDecoder(r.Body).Decode(&u) 165 | if err != nil { 166 | return nil, err 167 | } 168 | return u, nil 169 | } 170 | 171 | func decodeAddressRequest(_ context.Context, r *http.Request) (interface{}, error) { 172 | defer r.Body.Close() 173 | a := addressPostRequest{} 174 | err := json.NewDecoder(r.Body).Decode(&a) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return a, nil 179 | } 180 | 181 | func decodeCardRequest(_ context.Context, r *http.Request) (interface{}, error) { 182 | defer r.Body.Close() 183 | c := cardPostRequest{} 184 | err := json.NewDecoder(r.Body).Decode(&c) 185 | if err != nil { 186 | return nil, err 187 | } 188 | return c, nil 189 | } 190 | 191 | func decodeHealthRequest(_ context.Context, r *http.Request) (interface{}, error) { 192 | return struct{}{}, nil 193 | } 194 | 195 | func encodeHealthResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { 196 | return encodeResponse(ctx, w, response.(healthResponse)) 197 | } 198 | 199 | func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { 200 | // All of our response objects are JSON serializable, so we just do that. 201 | w.Header().Set("Content-Type", "application/hal+json") 202 | return json.NewEncoder(w).Encode(response) 203 | } 204 | -------------------------------------------------------------------------------- /apispec/hooks.js: -------------------------------------------------------------------------------- 1 | const hooks = require('hooks'); 2 | 3 | hooks.before("/login > GET", function(transaction, done) { 4 | transaction.skip = true; 5 | done(); 6 | }); 7 | 8 | hooks.before("/register > POST", function(transaction, done) { 9 | transaction.request.headers['Content-Type'] = 'application/json'; 10 | transaction.request.body = JSON.stringify( 11 | { 12 | "username": "testuser", 13 | "password": "testpassword" 14 | } 15 | ); 16 | done(); 17 | }); 18 | 19 | hooks.before("/addresses > POST", function(transaction, done) { 20 | transaction.request.headers['Content-Type'] = 'application/json'; 21 | transaction.request.body = JSON.stringify( 22 | { 23 | "street": "teststreet", 24 | "number": "15", 25 | "country": "The Netherlands", 26 | "city": "Den Haag" 27 | } 28 | ); 29 | done(); 30 | }); 31 | 32 | hooks.before("/cards > POST", function(transaction, done) { 33 | transaction.request.headers['Content-Type'] = 'application/json'; 34 | transaction.request.body = JSON.stringify( 35 | { 36 | "longNum": "1111222233334444", 37 | "expires": "11/2020", 38 | "ccv": "123" 39 | } 40 | ); 41 | done(); 42 | }); 43 | -------------------------------------------------------------------------------- /apispec/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "customers": [ 3 | { "id": "57a98d98e4b00679b4a830af", 4 | "firstName": "Test", 5 | "lastname": "Test", 6 | "username": "testymctestface" 7 | } 8 | ], 9 | "cards": [ 10 | { 11 | "id": "57a98d98e4b00679b4a830ae", 12 | "longNum": "23232*****2131", 13 | "expires": "12/18", 14 | "ccv": "940" 15 | } 16 | ], 17 | "addresses": [ 18 | { 19 | "id": "57a98d98e4b00679b4a830ad", 20 | "number": "12", 21 | "street": "Cleverstreet", 22 | "city": "Tinytown", 23 | "postcode": "1923eq", 24 | "country": "Cambodia" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /apispec/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger":"2.0", 3 | "info":{ 4 | "version":"", 5 | "title":"User", 6 | "description":"Provide Customer login, register, retrieval, as well as card and address retrieval", 7 | "license":{ 8 | "name":"MIT", 9 | "url":"http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT" 10 | } 11 | }, 12 | "host":"user", 13 | "basePath":"/", 14 | "securityDefinitions":{ 15 | "basicAuth": { 16 | "type": "basic", 17 | "description" : "HTTP Basic Authentication. Works over `HTTP` and `HTTPS`" 18 | } 19 | }, 20 | "schemes":[ 21 | "http" 22 | ], 23 | "consumes":[ 24 | "application/json;charset=UTF-8" 25 | ], 26 | "produces":[ 27 | "application/hal+json" 28 | ], 29 | "paths":{ 30 | "/login":{ 31 | "get":{ 32 | "description":"Return logged in user", 33 | "operationId":"Get Login", 34 | "produces":[ 35 | "application/json;charset=UTF-8" 36 | ], 37 | "responses":{ 38 | "200":{ 39 | "description":"", 40 | "schema":{ 41 | "$ref":"#/definitions/Getcustomersresponse" 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "/register":{ 48 | "post":{ 49 | "description":"Register new user", 50 | "operationId":"Set User", 51 | "produces":[ 52 | "application/json;charset=UTF-8" 53 | ], 54 | "parameters":[ 55 | { 56 | "name":"register", 57 | "in":"body", 58 | "description":"register object", 59 | "required":true, 60 | "schema":{ 61 | "$ref":"#/definitions/Register" 62 | } 63 | } 64 | ], 65 | "responses":{ 66 | "200":{ 67 | "description":"", 68 | "schema":{ 69 | "$ref":"#/definitions/Statusresponse" 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | "/customers":{ 76 | "get":{ 77 | "description":"Returns all customers", 78 | "operationId":"Get customers", 79 | "produces":[ 80 | "application/json;charset=UTF-8" 81 | ], 82 | "parameters":[ 83 | 84 | ], 85 | "responses":{ 86 | "200":{ 87 | "description":"", 88 | "schema":{ 89 | "$ref":"#/definitions/Getcustomersresponse" 90 | } 91 | } 92 | } 93 | } 94 | }, 95 | "/customers/{id}":{ 96 | "get":{ 97 | "description":"Returns a customer", 98 | "operationId":"Get customer", 99 | "produces":[ 100 | "application/json;charset=UTF-8" 101 | ], 102 | "parameters":[ 103 | { 104 | "name":"id", 105 | "in":"path", 106 | "description":"ID of customer to fetch", 107 | "required":true, 108 | "type":"string", 109 | "default":"57a98d98e4b00679b4a830af" 110 | } 111 | ], 112 | "responses":{ 113 | "200":{ 114 | "description":"", 115 | "schema":{ 116 | "$ref":"#/definitions/Customer" 117 | } 118 | } 119 | } 120 | }, 121 | "delete":{ 122 | "description":"Delete customer", 123 | "operationId":"Delete customer", 124 | "produces":[ 125 | "application/json;charset=UTF-8" 126 | ], 127 | "parameters":[ 128 | { 129 | "name":"id", 130 | "in":"path", 131 | "description":"ID of customer to delete", 132 | "required":true, 133 | "type":"string", 134 | "default":"57a98d98e4b00679b4a830b5" 135 | } 136 | ], 137 | "responses":{ 138 | "200":{ 139 | "description":"", 140 | "schema":{ 141 | "$ref":"#/definitions/Deleteresponse" 142 | } 143 | } 144 | } 145 | } 146 | }, 147 | "/customers/{id}/cards":{ 148 | "get":{ 149 | "description":"Returns a customer", 150 | "operationId":"Get customer cards", 151 | "produces":[ 152 | "application/json;charset=UTF-8" 153 | ], 154 | "parameters":[ 155 | { 156 | "name":"id", 157 | "in":"path", 158 | "description":"ID of customer to fetch", 159 | "required":true, 160 | "type":"string", 161 | "default":"57a98d98e4b00679b4a830af" 162 | } 163 | ], 164 | "responses":{ 165 | "200":{ 166 | "description":"", 167 | "schema":{ 168 | "$ref":"#/definitions/Getcardsresponse" 169 | } 170 | } 171 | } 172 | } 173 | }, 174 | "/customers/{id}/addresses":{ 175 | "get":{ 176 | "description":"Returns an address", 177 | "operationId":"Get customer addresses", 178 | "produces":[ 179 | "application/json;charset=UTF-8" 180 | ], 181 | "parameters":[ 182 | { 183 | "name":"id", 184 | "in":"path", 185 | "description":"ID of customer to fetch", 186 | "required":true, 187 | "type":"string", 188 | "default":"57a98d98e4b00679b4a830af" 189 | } 190 | ], 191 | "responses":{ 192 | "200":{ 193 | "description":"", 194 | "schema":{ 195 | "$ref":"#/definitions/Listaddressresponse" 196 | } 197 | } 198 | } 199 | } 200 | }, 201 | "/cards":{ 202 | "get":{ 203 | "description":"Return all cards", 204 | "operationId":"Get cards", 205 | "produces":[ 206 | "application/json;charset=UTF-8" 207 | ], 208 | "parameters":[ 209 | 210 | ], 211 | "responses":{ 212 | "200":{ 213 | "description":"", 214 | "schema":{ 215 | "$ref":"#/definitions/Getcardsresponse" 216 | } 217 | } 218 | } 219 | }, 220 | "post":{ 221 | "description":"Create new card", 222 | "operationId":"Set Card", 223 | "produces":[ 224 | "application/json;charset=UTF-8" 225 | ], 226 | "parameters":[ 227 | { 228 | "name":"card", 229 | "in":"body", 230 | "description":"Credit card", 231 | "required":true, 232 | "schema":{ 233 | "$ref":"#/definitions/Postcard" 234 | } 235 | } 236 | ], 237 | "responses":{ 238 | "200":{ 239 | "description":"", 240 | "schema":{ 241 | "$ref":"#/definitions/Statusresponse" 242 | } 243 | } 244 | } 245 | } 246 | }, 247 | "/cards/{id}":{ 248 | "get":{ 249 | "description":"Returns a card", 250 | "operationId":"Get card", 251 | "produces":[ 252 | "application/json;charset=UTF-8" 253 | ], 254 | "parameters":[ 255 | { 256 | "name":"id", 257 | "in":"path", 258 | "description":"ID of card to fetch", 259 | "required":true, 260 | "type":"string", 261 | "default":"57a98d98e4b00679b4a830ae" 262 | } 263 | ], 264 | "responses":{ 265 | "200":{ 266 | "description":"", 267 | "schema":{ 268 | "$ref":"#/definitions/Card" 269 | } 270 | } 271 | } 272 | }, 273 | "delete":{ 274 | "description":"Delete card", 275 | "operationId":"Delete card", 276 | "produces":[ 277 | "application/json;charset=UTF-8" 278 | ], 279 | "parameters":[ 280 | { 281 | "name":"id", 282 | "in":"path", 283 | "description":"ID of card to delete", 284 | "required":true, 285 | "type":"string", 286 | "default":"57a98d98e4b00679b4a830ae" 287 | } 288 | ], 289 | "responses":{ 290 | "200":{ 291 | "description":"", 292 | "schema":{ 293 | "$ref":"#/definitions/Deleteresponse" 294 | } 295 | } 296 | } 297 | } 298 | }, 299 | "/addresses":{ 300 | "get":{ 301 | "description":"Returns all addresses", 302 | "operationId":"Get addresses", 303 | "produces":[ 304 | "application/json;charset=UTF-8" 305 | ], 306 | "parameters":[ 307 | 308 | ], 309 | "responses":{ 310 | "200":{ 311 | "description":"", 312 | "schema":{ 313 | "$ref":"#/definitions/Listaddressresponse" 314 | } 315 | } 316 | } 317 | }, 318 | "post":{ 319 | "description":"Create new address", 320 | "operationId":"Set Address", 321 | "produces":[ 322 | "application/json;charset=UTF-8" 323 | ], 324 | "parameters":[ 325 | { 326 | "name":"address", 327 | "in":"body", 328 | "description":"Address", 329 | "required":true, 330 | "schema":{ 331 | "$ref":"#/definitions/Postaddress" 332 | } 333 | } 334 | ], 335 | "responses":{ 336 | "200":{ 337 | "description":"", 338 | "schema":{ 339 | "$ref":"#/definitions/Statusresponse" 340 | } 341 | } 342 | } 343 | } 344 | }, 345 | "/addresses/{id}":{ 346 | "get":{ 347 | "description":"Returns an address", 348 | "operationId":"Get address", 349 | "produces":[ 350 | "application" 351 | ], 352 | "parameters":[ 353 | { 354 | "name":"id", 355 | "in":"path", 356 | "description":"ID of address to fetch", 357 | "required":true, 358 | "type":"string", 359 | "default":"57a98d98e4b00679b4a830ad" 360 | } 361 | ], 362 | "responses":{ 363 | "200":{ 364 | "description":"", 365 | "schema":{ 366 | "$ref":"#/definitions/Address" 367 | } 368 | } 369 | } 370 | }, 371 | "delete":{ 372 | "description":"Delete address", 373 | "operationId":"Delete address", 374 | "produces":[ 375 | "application/json;charset=UTF-8" 376 | ], 377 | "parameters":[ 378 | { 379 | "name":"id", 380 | "in":"path", 381 | "description":"ID of address to delete", 382 | "required":true, 383 | "type":"string", 384 | "default":"57a98d98e4b00679b4a830ad" 385 | } 386 | ], 387 | "responses":{ 388 | "200":{ 389 | "description":"", 390 | "schema":{ 391 | "$ref":"#/definitions/Deleteresponse" 392 | } 393 | } 394 | } 395 | } 396 | } 397 | }, 398 | 399 | "definitions":{ 400 | "Getcustomersresponse":{ 401 | "title":"Get customers response", 402 | "type":"object", 403 | "properties":{ 404 | "_embedded":{ 405 | "type":"object", 406 | "properties":{ 407 | "customer":{ 408 | "type":"array", 409 | "items":{ 410 | "$ref":"#/definitions/Customer" 411 | } 412 | } 413 | } 414 | }, 415 | "_links":{ 416 | "type":"object" 417 | }, 418 | "page":{ 419 | "type":"object" 420 | } 421 | }, 422 | "required":[ 423 | "_embedded" 424 | ] 425 | }, 426 | "Getcardsresponse":{ 427 | "title":"Get cards response", 428 | "type":"object", 429 | "properties":{ 430 | "_embedded":{ 431 | "type":"object", 432 | "properties":{ 433 | "card":{ 434 | "type":"array", 435 | "items":{ 436 | "$ref":"#/definitions/Card" 437 | } 438 | } 439 | } 440 | }, 441 | "_links":{ 442 | "type":"object" 443 | }, 444 | "page":{ 445 | "type":"object" 446 | } 447 | }, 448 | "required":[ 449 | "_embedded" 450 | ] 451 | }, 452 | "Statusresponse":{ 453 | "title":"Post status response", 454 | "type":"object", 455 | "properties":{ 456 | "id":{ 457 | "type":"string" 458 | } 459 | }, 460 | "required":[ 461 | "id" 462 | ] 463 | }, 464 | "Deleteresponse":{ 465 | "title":"Delete status response", 466 | "type":"object", 467 | "properties":{ 468 | "status":{ 469 | "type":"boolean" 470 | } 471 | }, 472 | "required":[ 473 | "status" 474 | ] 475 | }, 476 | "Customer":{ 477 | "type":"object", 478 | "properties":{ 479 | "firstName":{ 480 | "type":"string" 481 | }, 482 | "lastName":{ 483 | "type":"string" 484 | }, 485 | "username":{ 486 | "type":"string" 487 | }, 488 | "_links":{ 489 | "type":"object", 490 | "properties":{ 491 | "self":{ 492 | "type":"object", 493 | "properties":{ 494 | "href":{ 495 | "type":"string" 496 | } 497 | }, 498 | "required":[ 499 | "href" 500 | ] 501 | }, 502 | "customer":{ 503 | "type":"object", 504 | "properties":{ 505 | "href":{ 506 | "type":"string" 507 | } 508 | }, 509 | "required":[ 510 | "href" 511 | ] 512 | }, 513 | "addresses":{ 514 | "type":"object", 515 | "properties":{ 516 | "href":{ 517 | "type":"string" 518 | } 519 | }, 520 | "required":[ 521 | "href" 522 | ] 523 | }, 524 | "cards":{ 525 | "type":"object", 526 | "properties":{ 527 | "href":{ 528 | "type":"string" 529 | } 530 | }, 531 | "required":[ 532 | "href" 533 | ] 534 | } 535 | }, 536 | "required":[ 537 | "self", 538 | "customer" 539 | ] 540 | } 541 | }, 542 | "required":[ 543 | "firstName", 544 | "lastName", 545 | "username", 546 | "_links" 547 | ] 548 | }, 549 | "Card":{ 550 | "type":"object", 551 | "properties":{ 552 | "longNum":{ 553 | "type":"string" 554 | }, 555 | "expires":{ 556 | "type":"string" 557 | }, 558 | "ccv":{ 559 | "type":"string" 560 | }, 561 | "_links":{ 562 | "type":"object", 563 | "properties":{ 564 | "self":{ 565 | "type":"object", 566 | "properties":{ 567 | "href":{ 568 | "type":"string" 569 | } 570 | }, 571 | "required":[ 572 | "href" 573 | ] 574 | }, 575 | "card":{ 576 | "type":"object", 577 | "properties":{ 578 | "href":{ 579 | "type":"string" 580 | } 581 | }, 582 | "required":[ 583 | "href" 584 | ] 585 | } 586 | }, 587 | "required":[ 588 | "self", 589 | "card" 590 | ] 591 | } 592 | }, 593 | "required":[ 594 | "longNum", 595 | "expires", 596 | "ccv", 597 | "_links" 598 | ] 599 | }, 600 | "Address":{ 601 | "type":"object", 602 | "properties":{ 603 | "number":{ 604 | "type":"string" 605 | }, 606 | "street":{ 607 | "type":"string" 608 | }, 609 | "city":{ 610 | "type":"string" 611 | }, 612 | "postcode":{ 613 | "type":"string" 614 | }, 615 | "country":{ 616 | "type":"string" 617 | }, 618 | "_links":{ 619 | "type":"object", 620 | "properties":{ 621 | "self":{ 622 | "type":"object", 623 | "properties":{ 624 | "href":{ 625 | "type":"string" 626 | } 627 | }, 628 | "required":[ 629 | "href" 630 | ] 631 | }, 632 | "address":{ 633 | "type":"object", 634 | "properties":{ 635 | "href":{ 636 | "type":"string" 637 | } 638 | }, 639 | "required":[ 640 | "href" 641 | ] 642 | } 643 | }, 644 | "required":[ 645 | "self", 646 | "address" 647 | ] 648 | } 649 | }, 650 | "required":[ 651 | "number", 652 | "street", 653 | "city", 654 | "postcode", 655 | "country", 656 | "_links" 657 | ] 658 | }, 659 | "Listaddressresponse":{ 660 | "title":"List response", 661 | "type":"object", 662 | "properties":{ 663 | "_embedded":{ 664 | "type":"object", 665 | "properties":{ 666 | "address":{ 667 | "type":"array", 668 | "items":{ 669 | "$ref":"#/definitions/Address" 670 | } 671 | } 672 | } 673 | }, 674 | "_links":{ 675 | "type":"object" 676 | }, 677 | "page":{ 678 | "type":"object" 679 | } 680 | }, 681 | "required":[ 682 | "_embedded" 683 | ] 684 | }, 685 | "Register":{ 686 | "title":"Register", 687 | "type":"object", 688 | "properties":{ 689 | "username":{ 690 | "description":"Username", 691 | "type":"string" 692 | }, 693 | "password":{ 694 | "description":"Password", 695 | "type":"string" 696 | }, 697 | "email":{ 698 | "description":"Email", 699 | "type":"string" 700 | } 701 | }, 702 | "required":[ 703 | "username", 704 | "password" 705 | ] 706 | }, 707 | "Postcard":{ 708 | "title":"Post Card", 709 | "type":"object", 710 | "properties":{ 711 | "longNum":{ 712 | "description":"Credit card number", 713 | "type":"string" 714 | }, 715 | "expires":{ 716 | "description":"Card expiration", 717 | "type":"string" 718 | }, 719 | "ccv":{ 720 | "description":"Card ccv", 721 | "type":"string" 722 | }, 723 | "userID":{ 724 | "description":"User to attach this card to.", 725 | "type":"string" 726 | } 727 | } 728 | }, 729 | "Postaddress":{ 730 | "title":"Post Address", 731 | "type":"object", 732 | "properties":{ 733 | "street":{ 734 | "type":"string" 735 | }, 736 | "number":{ 737 | "type":"string" 738 | }, 739 | "country":{ 740 | "type":"string" 741 | }, 742 | "city":{ 743 | "type":"string" 744 | }, 745 | "postcode":{ 746 | "type":"string" 747 | }, 748 | "userID":{ 749 | "description":"User to attach this card to.", 750 | "type":"string" 751 | } 752 | } 753 | } 754 | } 755 | } 756 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/microservices-demo/user/users" 10 | ) 11 | 12 | // Database represents a simple interface so we can switch to a new system easily 13 | // this is just basic and specific to this microservice 14 | type Database interface { 15 | Init() error 16 | GetUserByName(string) (users.User, error) 17 | GetUser(string) (users.User, error) 18 | GetUsers() ([]users.User, error) 19 | CreateUser(*users.User) error 20 | GetUserAttributes(*users.User) error 21 | GetAddress(string) (users.Address, error) 22 | GetAddresses() ([]users.Address, error) 23 | CreateAddress(*users.Address, string) error 24 | GetCard(string) (users.Card, error) 25 | GetCards() ([]users.Card, error) 26 | Delete(string, string) error 27 | CreateCard(*users.Card, string) error 28 | Ping() error 29 | } 30 | 31 | var ( 32 | database string 33 | //DefaultDb is the database set for the microservice 34 | DefaultDb Database 35 | //DBTypes is a map of DB interfaces that can be used for this service 36 | DBTypes = map[string]Database{} 37 | //ErrNoDatabaseFound error returnes when database interface does not exists in DBTypes 38 | ErrNoDatabaseFound = "No database with name %v registered" 39 | //ErrNoDatabaseSelected is returned when no database was designated in the flag or env 40 | ErrNoDatabaseSelected = errors.New("No DB selected") 41 | ) 42 | 43 | func init() { 44 | flag.StringVar(&database, "database", os.Getenv("USER_DATABASE"), "Database to use, Mongodb or ...") 45 | 46 | } 47 | 48 | //Init inits the selected DB in DefaultDb 49 | func Init() error { 50 | if database == "" { 51 | return ErrNoDatabaseSelected 52 | } 53 | err := Set() 54 | if err != nil { 55 | return err 56 | } 57 | return DefaultDb.Init() 58 | } 59 | 60 | //Set the DefaultDb 61 | func Set() error { 62 | if v, ok := DBTypes[database]; ok { 63 | DefaultDb = v 64 | return nil 65 | } 66 | return fmt.Errorf(ErrNoDatabaseFound, database) 67 | } 68 | 69 | //Register registers the database interface in the DBTypes 70 | func Register(name string, db Database) { 71 | DBTypes[name] = db 72 | } 73 | 74 | //CreateUser invokes DefaultDb method 75 | func CreateUser(u *users.User) error { 76 | return DefaultDb.CreateUser(u) 77 | } 78 | 79 | //GetUserByName invokes DefaultDb method 80 | func GetUserByName(n string) (users.User, error) { 81 | u, err := DefaultDb.GetUserByName(n) 82 | if err == nil { 83 | u.AddLinks() 84 | } 85 | return u, err 86 | } 87 | 88 | //GetUser invokes DefaultDb method 89 | func GetUser(n string) (users.User, error) { 90 | u, err := DefaultDb.GetUser(n) 91 | if err == nil { 92 | u.AddLinks() 93 | } 94 | return u, err 95 | } 96 | 97 | //GetUsers invokes DefaultDb method 98 | func GetUsers() ([]users.User, error) { 99 | us, err := DefaultDb.GetUsers() 100 | for k, _ := range us { 101 | us[k].AddLinks() 102 | } 103 | return us, err 104 | } 105 | 106 | //GetUserAttributes invokes DefaultDb method 107 | func GetUserAttributes(u *users.User) error { 108 | err := DefaultDb.GetUserAttributes(u) 109 | if err != nil { 110 | return err 111 | } 112 | for k, _ := range u.Addresses { 113 | u.Addresses[k].AddLinks() 114 | } 115 | for k, _ := range u.Cards { 116 | u.Cards[k].AddLinks() 117 | } 118 | return nil 119 | } 120 | 121 | //CreateAddress invokes DefaultDb method 122 | func CreateAddress(a *users.Address, userid string) error { 123 | return DefaultDb.CreateAddress(a, userid) 124 | } 125 | 126 | //GetAddress invokes DefaultDb method 127 | func GetAddress(n string) (users.Address, error) { 128 | a, err := DefaultDb.GetAddress(n) 129 | if err == nil { 130 | a.AddLinks() 131 | } 132 | return a, err 133 | } 134 | 135 | //GetAddresses invokes DefaultDb method 136 | func GetAddresses() ([]users.Address, error) { 137 | as, err := DefaultDb.GetAddresses() 138 | for k, _ := range as { 139 | as[k].AddLinks() 140 | } 141 | return as, err 142 | } 143 | 144 | //CreateCard invokes DefaultDb method 145 | func CreateCard(c *users.Card, userid string) error { 146 | return DefaultDb.CreateCard(c, userid) 147 | } 148 | 149 | //GetCard invokes DefaultDb method 150 | func GetCard(n string) (users.Card, error) { 151 | return DefaultDb.GetCard(n) 152 | } 153 | 154 | //GetCards invokes DefaultDb method 155 | func GetCards() ([]users.Card, error) { 156 | cs, err := DefaultDb.GetCards() 157 | for k, _ := range cs { 158 | cs[k].AddLinks() 159 | } 160 | return cs, err 161 | } 162 | 163 | //Delete invokes DefaultDb method 164 | func Delete(entity, id string) error { 165 | return DefaultDb.Delete(entity, id) 166 | } 167 | 168 | //Ping invokes DefaultDB method 169 | func Ping() error { 170 | return DefaultDb.Ping() 171 | } 172 | -------------------------------------------------------------------------------- /db/db_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/microservices-demo/user/users" 9 | ) 10 | 11 | var ( 12 | TestDB = fake{} 13 | ErrFakeError = errors.New("Fake error") 14 | TestAddress = users.Address{ 15 | Street: "street", 16 | Number: "51b", 17 | Country: "Netherlands", 18 | City: "Amsterdam", 19 | ID: "000056", 20 | } 21 | ) 22 | 23 | func TestInit(t *testing.T) { 24 | err := Init() 25 | if err == nil { 26 | t.Error("Expected no registered db error") 27 | } 28 | Register("test", TestDB) 29 | database = "test" 30 | err = Init() 31 | if err != ErrFakeError { 32 | t.Error("expected fake db error from init") 33 | } 34 | TestAddress.AddLinks() 35 | } 36 | 37 | func TestSet(t *testing.T) { 38 | database = "nodb" 39 | err := Set() 40 | if err == nil { 41 | t.Error("Expecting error for no databade found") 42 | } 43 | Register("nodb2", TestDB) 44 | database = "nodb2" 45 | err = Set() 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | } 50 | 51 | func TestRegister(t *testing.T) { 52 | l := len(DBTypes) 53 | Register("test2", TestDB) 54 | if len(DBTypes) != l+1 { 55 | t.Errorf("Expecting %v DB types received %v", l+1, len(DBTypes)) 56 | } 57 | l = len(DBTypes) 58 | Register("test2", TestDB) 59 | if len(DBTypes) != l { 60 | t.Errorf("Expecting %v DB types received %v duplicate names", l, len(DBTypes)) 61 | } 62 | } 63 | 64 | func TestCreateUser(t *testing.T) { 65 | err := CreateUser(&users.User{}) 66 | if err != ErrFakeError { 67 | t.Error("expected fake db error from create") 68 | } 69 | } 70 | 71 | func TestGetUser(t *testing.T) { 72 | _, err := GetUser("test") 73 | if err != ErrFakeError { 74 | t.Error("expected fake db error from get") 75 | } 76 | } 77 | 78 | func TestGetUserByName(t *testing.T) { 79 | _, err := GetUserByName("test") 80 | if err != ErrFakeError { 81 | t.Error("expected fake db error from get") 82 | } 83 | } 84 | 85 | func TestGetUserAttributes(t *testing.T) { 86 | u := users.New() 87 | GetUserAttributes(&u) 88 | if len(u.Addresses) != 1 { 89 | t.Error("expected one address added for GetUserAttributes") 90 | } 91 | if !reflect.DeepEqual(u.Addresses[0], TestAddress) { 92 | t.Error("expected matching addresses") 93 | } 94 | } 95 | 96 | func TestPing(t *testing.T) { 97 | err := Ping() 98 | if err != ErrFakeError { 99 | t.Error("expected fake db error from ping") 100 | } 101 | 102 | } 103 | 104 | type fake struct{} 105 | 106 | func (f fake) Init() error { 107 | return ErrFakeError 108 | } 109 | func (f fake) GetUserByName(name string) (users.User, error) { 110 | return users.User{}, ErrFakeError 111 | } 112 | func (f fake) GetUser(id string) (users.User, error) { 113 | return users.User{}, ErrFakeError 114 | } 115 | 116 | func (f fake) GetUsers() ([]users.User, error) { 117 | return make([]users.User, 0), ErrFakeError 118 | } 119 | 120 | func (f fake) CreateUser(*users.User) error { 121 | return ErrFakeError 122 | } 123 | 124 | func (f fake) GetUserAttributes(u *users.User) error { 125 | u.Addresses = append(u.Addresses, TestAddress) 126 | return nil 127 | } 128 | 129 | func (f fake) GetCard(id string) (users.Card, error) { 130 | return users.Card{}, ErrFakeError 131 | } 132 | 133 | func (f fake) GetCards() ([]users.Card, error) { 134 | return make([]users.Card, 0), ErrFakeError 135 | } 136 | 137 | func (f fake) CreateCard(c *users.Card, id string) error { 138 | return ErrFakeError 139 | } 140 | 141 | func (f fake) GetAddress(id string) (users.Address, error) { 142 | return users.Address{}, ErrFakeError 143 | } 144 | 145 | func (f fake) GetAddresses() ([]users.Address, error) { 146 | return make([]users.Address, 0), ErrFakeError 147 | } 148 | 149 | func (f fake) CreateAddress(u *users.Address, id string) error { 150 | return ErrFakeError 151 | } 152 | 153 | func (f fake) Delete(entity, id string) error { 154 | return ErrFakeError 155 | } 156 | 157 | func (f fake) Ping() error { 158 | return ErrFakeError 159 | } 160 | -------------------------------------------------------------------------------- /db/mongodb/mongodb.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "net/url" 8 | "os" 9 | "time" 10 | 11 | "github.com/microservices-demo/user/users" 12 | 13 | "gopkg.in/mgo.v2" 14 | "gopkg.in/mgo.v2/bson" 15 | ) 16 | 17 | var ( 18 | name string 19 | password string 20 | host string 21 | db = "users" 22 | //ErrInvalidHexID represents a entity id that is not a valid bson ObjectID 23 | ErrInvalidHexID = errors.New("Invalid Id Hex") 24 | ) 25 | 26 | func init() { 27 | flag.StringVar(&name, "mongo-user", os.Getenv("MONGO_USER"), "Mongo user") 28 | flag.StringVar(&password, "mongo-password", os.Getenv("MONGO_PASS"), "Mongo password") 29 | flag.StringVar(&host, "mongo-host", os.Getenv("MONGO_HOST"), "Mongo host") 30 | } 31 | 32 | // Mongo meets the Database interface requirements 33 | type Mongo struct { 34 | //Session is a MongoDB Session 35 | Session *mgo.Session 36 | } 37 | 38 | // Init MongoDB 39 | func (m *Mongo) Init() error { 40 | u := getURL() 41 | var err error 42 | m.Session, err = mgo.DialWithTimeout(u.String(), time.Duration(5)*time.Second) 43 | if err != nil { 44 | return err 45 | } 46 | return m.EnsureIndexes() 47 | } 48 | 49 | // MongoUser is a wrapper for the users 50 | type MongoUser struct { 51 | users.User `bson:",inline"` 52 | ID bson.ObjectId `bson:"_id"` 53 | AddressIDs []bson.ObjectId `bson:"addresses"` 54 | CardIDs []bson.ObjectId `bson:"cards"` 55 | } 56 | 57 | // New Returns a new MongoUser 58 | func New() MongoUser { 59 | u := users.New() 60 | return MongoUser{ 61 | User: u, 62 | AddressIDs: make([]bson.ObjectId, 0), 63 | CardIDs: make([]bson.ObjectId, 0), 64 | } 65 | } 66 | 67 | // AddUserIDs adds userID as string to user 68 | func (mu *MongoUser) AddUserIDs() { 69 | if mu.User.Addresses == nil { 70 | mu.User.Addresses = make([]users.Address, 0) 71 | } 72 | for _, id := range mu.AddressIDs { 73 | mu.User.Addresses = append(mu.User.Addresses, users.Address{ 74 | ID: id.Hex(), 75 | }) 76 | } 77 | if mu.User.Cards == nil { 78 | mu.User.Cards = make([]users.Card, 0) 79 | } 80 | for _, id := range mu.CardIDs { 81 | mu.User.Cards = append(mu.User.Cards, users.Card{ID: id.Hex()}) 82 | } 83 | mu.User.UserID = mu.ID.Hex() 84 | } 85 | 86 | // MongoAddress is a wrapper for Address 87 | type MongoAddress struct { 88 | users.Address `bson:",inline"` 89 | ID bson.ObjectId `bson:"_id"` 90 | } 91 | 92 | // AddID ObjectID as string 93 | func (m *MongoAddress) AddID() { 94 | m.Address.ID = m.ID.Hex() 95 | } 96 | 97 | // MongoCard is a wrapper for Card 98 | type MongoCard struct { 99 | users.Card `bson:",inline"` 100 | ID bson.ObjectId `bson:"_id"` 101 | } 102 | 103 | // AddID ObjectID as string 104 | func (m *MongoCard) AddID() { 105 | m.Card.ID = m.ID.Hex() 106 | } 107 | 108 | // CreateUser Insert user to MongoDB, including connected addresses and cards, update passed in user with Ids 109 | func (m *Mongo) CreateUser(u *users.User) error { 110 | s := m.Session.Copy() 111 | defer s.Close() 112 | id := bson.NewObjectId() 113 | mu := New() 114 | mu.User = *u 115 | mu.ID = id 116 | var carderr error 117 | var addrerr error 118 | mu.CardIDs, carderr = m.createCards(u.Cards) 119 | mu.AddressIDs, addrerr = m.createAddresses(u.Addresses) 120 | c := s.DB("").C("customers") 121 | _, err := c.UpsertId(mu.ID, mu) 122 | if err != nil { 123 | // Gonna clean up if we can, ignore error 124 | // because the user save error takes precedence. 125 | m.cleanAttributes(mu) 126 | return err 127 | } 128 | mu.User.UserID = mu.ID.Hex() 129 | // Cheap err for attributes 130 | if carderr != nil || addrerr != nil { 131 | return fmt.Errorf("%v %v", carderr, addrerr) 132 | } 133 | *u = mu.User 134 | return nil 135 | } 136 | 137 | func (m *Mongo) createCards(cs []users.Card) ([]bson.ObjectId, error) { 138 | s := m.Session.Copy() 139 | defer s.Close() 140 | ids := make([]bson.ObjectId, 0) 141 | defer s.Close() 142 | for k, ca := range cs { 143 | id := bson.NewObjectId() 144 | mc := MongoCard{Card: ca, ID: id} 145 | c := s.DB("").C("cards") 146 | _, err := c.UpsertId(mc.ID, mc) 147 | if err != nil { 148 | return ids, err 149 | } 150 | ids = append(ids, id) 151 | cs[k].ID = id.Hex() 152 | } 153 | return ids, nil 154 | } 155 | 156 | func (m *Mongo) createAddresses(as []users.Address) ([]bson.ObjectId, error) { 157 | ids := make([]bson.ObjectId, 0) 158 | s := m.Session.Copy() 159 | defer s.Close() 160 | for k, a := range as { 161 | id := bson.NewObjectId() 162 | ma := MongoAddress{Address: a, ID: id} 163 | c := s.DB("").C("addresses") 164 | _, err := c.UpsertId(ma.ID, ma) 165 | if err != nil { 166 | return ids, err 167 | } 168 | ids = append(ids, id) 169 | as[k].ID = id.Hex() 170 | } 171 | return ids, nil 172 | } 173 | 174 | func (m *Mongo) cleanAttributes(mu MongoUser) error { 175 | s := m.Session.Copy() 176 | defer s.Close() 177 | c := s.DB("").C("addresses") 178 | _, err := c.RemoveAll(bson.M{"_id": bson.M{"$in": mu.AddressIDs}}) 179 | c = s.DB("").C("cards") 180 | _, err = c.RemoveAll(bson.M{"_id": bson.M{"$in": mu.CardIDs}}) 181 | return err 182 | } 183 | 184 | func (m *Mongo) appendAttributeId(attr string, id bson.ObjectId, userid string) error { 185 | s := m.Session.Copy() 186 | defer s.Close() 187 | c := s.DB("").C("customers") 188 | return c.Update(bson.M{"_id": bson.ObjectIdHex(userid)}, 189 | bson.M{"$addToSet": bson.M{attr: id}}) 190 | } 191 | 192 | func (m *Mongo) removeAttributeId(attr string, id bson.ObjectId, userid string) error { 193 | s := m.Session.Copy() 194 | defer s.Close() 195 | c := s.DB("").C("customers") 196 | return c.Update(bson.M{"_id": bson.ObjectIdHex(userid)}, 197 | bson.M{"$pull": bson.M{attr: id}}) 198 | } 199 | 200 | // GetUserByName Get user by their name 201 | func (m *Mongo) GetUserByName(name string) (users.User, error) { 202 | s := m.Session.Copy() 203 | defer s.Close() 204 | c := s.DB("").C("customers") 205 | mu := New() 206 | err := c.Find(bson.M{"username": name}).One(&mu) 207 | mu.AddUserIDs() 208 | return mu.User, err 209 | } 210 | 211 | // GetUser Get user by their object id 212 | func (m *Mongo) GetUser(id string) (users.User, error) { 213 | s := m.Session.Copy() 214 | defer s.Close() 215 | if !bson.IsObjectIdHex(id) { 216 | return users.New(), errors.New("Invalid Id Hex") 217 | } 218 | c := s.DB("").C("customers") 219 | mu := New() 220 | err := c.FindId(bson.ObjectIdHex(id)).One(&mu) 221 | mu.AddUserIDs() 222 | return mu.User, err 223 | } 224 | 225 | // GetUsers Get all users 226 | func (m *Mongo) GetUsers() ([]users.User, error) { 227 | // TODO: add paginations 228 | s := m.Session.Copy() 229 | defer s.Close() 230 | c := s.DB("").C("customers") 231 | var mus []MongoUser 232 | err := c.Find(nil).All(&mus) 233 | us := make([]users.User, 0) 234 | for _, mu := range mus { 235 | mu.AddUserIDs() 236 | us = append(us, mu.User) 237 | } 238 | return us, err 239 | } 240 | 241 | // GetUserAttributes given a user, load all cards and addresses connected to that user 242 | func (m *Mongo) GetUserAttributes(u *users.User) error { 243 | s := m.Session.Copy() 244 | defer s.Close() 245 | ids := make([]bson.ObjectId, 0) 246 | for _, a := range u.Addresses { 247 | if !bson.IsObjectIdHex(a.ID) { 248 | return ErrInvalidHexID 249 | } 250 | ids = append(ids, bson.ObjectIdHex(a.ID)) 251 | } 252 | var ma []MongoAddress 253 | c := s.DB("").C("addresses") 254 | err := c.Find(bson.M{"_id": bson.M{"$in": ids}}).All(&ma) 255 | if err != nil { 256 | return err 257 | } 258 | na := make([]users.Address, 0) 259 | for _, a := range ma { 260 | a.Address.ID = a.ID.Hex() 261 | na = append(na, a.Address) 262 | } 263 | u.Addresses = na 264 | 265 | ids = make([]bson.ObjectId, 0) 266 | for _, c := range u.Cards { 267 | if !bson.IsObjectIdHex(c.ID) { 268 | return ErrInvalidHexID 269 | } 270 | ids = append(ids, bson.ObjectIdHex(c.ID)) 271 | } 272 | var mc []MongoCard 273 | c = s.DB("").C("cards") 274 | err = c.Find(bson.M{"_id": bson.M{"$in": ids}}).All(&mc) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | nc := make([]users.Card, 0) 280 | for _, ca := range mc { 281 | ca.Card.ID = ca.ID.Hex() 282 | nc = append(nc, ca.Card) 283 | } 284 | u.Cards = nc 285 | return nil 286 | } 287 | 288 | // GetCard Gets card by objects Id 289 | func (m *Mongo) GetCard(id string) (users.Card, error) { 290 | s := m.Session.Copy() 291 | defer s.Close() 292 | if !bson.IsObjectIdHex(id) { 293 | return users.Card{}, errors.New("Invalid Id Hex") 294 | } 295 | c := s.DB("").C("cards") 296 | mc := MongoCard{} 297 | err := c.FindId(bson.ObjectIdHex(id)).One(&mc) 298 | mc.AddID() 299 | return mc.Card, err 300 | } 301 | 302 | // GetCards Gets all cards 303 | func (m *Mongo) GetCards() ([]users.Card, error) { 304 | // TODO: add pagination 305 | s := m.Session.Copy() 306 | defer s.Close() 307 | c := s.DB("").C("cards") 308 | var mcs []MongoCard 309 | err := c.Find(nil).All(&mcs) 310 | cs := make([]users.Card, 0) 311 | for _, mc := range mcs { 312 | mc.AddID() 313 | cs = append(cs, mc.Card) 314 | } 315 | return cs, err 316 | } 317 | 318 | // CreateCard adds card to MongoDB 319 | func (m *Mongo) CreateCard(ca *users.Card, userid string) error { 320 | if userid != "" && !bson.IsObjectIdHex(userid) { 321 | return errors.New("Invalid Id Hex") 322 | } 323 | s := m.Session.Copy() 324 | defer s.Close() 325 | c := s.DB("").C("cards") 326 | id := bson.NewObjectId() 327 | mc := MongoCard{Card: *ca, ID: id} 328 | _, err := c.UpsertId(mc.ID, mc) 329 | if err != nil { 330 | return err 331 | } 332 | // Address for anonymous user 333 | if userid != "" { 334 | err = m.appendAttributeId("cards", mc.ID, userid) 335 | if err != nil { 336 | return err 337 | } 338 | } 339 | mc.AddID() 340 | *ca = mc.Card 341 | return err 342 | } 343 | 344 | // GetAddress Gets an address by object Id 345 | func (m *Mongo) GetAddress(id string) (users.Address, error) { 346 | s := m.Session.Copy() 347 | defer s.Close() 348 | if !bson.IsObjectIdHex(id) { 349 | return users.Address{}, errors.New("Invalid Id Hex") 350 | } 351 | c := s.DB("").C("addresses") 352 | ma := MongoAddress{} 353 | err := c.FindId(bson.ObjectIdHex(id)).One(&ma) 354 | ma.AddID() 355 | return ma.Address, err 356 | } 357 | 358 | // GetAddresses gets all addresses 359 | func (m *Mongo) GetAddresses() ([]users.Address, error) { 360 | // TODO: add pagination 361 | s := m.Session.Copy() 362 | defer s.Close() 363 | c := s.DB("").C("addresses") 364 | var mas []MongoAddress 365 | err := c.Find(nil).All(&mas) 366 | as := make([]users.Address, 0) 367 | for _, ma := range mas { 368 | ma.AddID() 369 | as = append(as, ma.Address) 370 | } 371 | return as, err 372 | } 373 | 374 | // CreateAddress Inserts Address into MongoDB 375 | func (m *Mongo) CreateAddress(a *users.Address, userid string) error { 376 | if userid != "" && !bson.IsObjectIdHex(userid) { 377 | return errors.New("Invalid Id Hex") 378 | } 379 | s := m.Session.Copy() 380 | defer s.Close() 381 | c := s.DB("").C("addresses") 382 | id := bson.NewObjectId() 383 | ma := MongoAddress{Address: *a, ID: id} 384 | _, err := c.UpsertId(ma.ID, ma) 385 | if err != nil { 386 | return err 387 | } 388 | // Address for anonymous user 389 | if userid != "" { 390 | err = m.appendAttributeId("addresses", ma.ID, userid) 391 | if err != nil { 392 | return err 393 | } 394 | } 395 | ma.AddID() 396 | *a = ma.Address 397 | return err 398 | } 399 | 400 | // CreateAddress Inserts Address into MongoDB 401 | func (m *Mongo) Delete(entity, id string) error { 402 | if !bson.IsObjectIdHex(id) { 403 | return errors.New("Invalid Id Hex") 404 | } 405 | s := m.Session.Copy() 406 | defer s.Close() 407 | c := s.DB("").C(entity) 408 | if entity == "customers" { 409 | u, err := m.GetUser(id) 410 | if err != nil { 411 | return err 412 | } 413 | aids := make([]bson.ObjectId, 0) 414 | for _, a := range u.Addresses { 415 | aids = append(aids, bson.ObjectIdHex(a.ID)) 416 | } 417 | cids := make([]bson.ObjectId, 0) 418 | for _, c := range u.Cards { 419 | cids = append(cids, bson.ObjectIdHex(c.ID)) 420 | } 421 | ac := s.DB("").C("addresses") 422 | ac.RemoveAll(bson.M{"_id": bson.M{"$in": aids}}) 423 | cc := s.DB("").C("cards") 424 | cc.RemoveAll(bson.M{"_id": bson.M{"$in": cids}}) 425 | } else { 426 | c := s.DB("").C("customers") 427 | c.UpdateAll(bson.M{}, 428 | bson.M{"$pull": bson.M{entity: bson.ObjectIdHex(id)}}) 429 | } 430 | return c.Remove(bson.M{"_id": bson.ObjectIdHex(id)}) 431 | } 432 | 433 | func getURL() url.URL { 434 | ur := url.URL{ 435 | Scheme: "mongodb", 436 | Host: host, 437 | Path: db, 438 | } 439 | if name != "" { 440 | u := url.UserPassword(name, password) 441 | ur.User = u 442 | } 443 | return ur 444 | } 445 | 446 | // EnsureIndexes ensures username is unique 447 | func (m *Mongo) EnsureIndexes() error { 448 | s := m.Session.Copy() 449 | defer s.Close() 450 | i := mgo.Index{ 451 | Key: []string{"username"}, 452 | Unique: true, 453 | DropDups: true, 454 | Background: true, 455 | Sparse: false, 456 | } 457 | c := s.DB("").C("customers") 458 | return c.EnsureIndex(i) 459 | } 460 | 461 | func (m *Mongo) Ping() error { 462 | s := m.Session.Copy() 463 | defer s.Close() 464 | return s.Ping() 465 | } 466 | -------------------------------------------------------------------------------- /db/mongodb/mongodb_test.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/microservices-demo/user/users" 9 | "gopkg.in/mgo.v2/bson" 10 | "gopkg.in/mgo.v2/dbtest" 11 | ) 12 | 13 | var ( 14 | TestMongo = Mongo{} 15 | TestServer = dbtest.DBServer{} 16 | TestUser = users.User{ 17 | FirstName: "firstname", 18 | LastName: "lastname", 19 | Username: "username", 20 | Password: "blahblah", 21 | Addresses: []users.Address{ 22 | users.Address{ 23 | Street: "street", 24 | }, 25 | }, 26 | } 27 | ) 28 | 29 | func init() { 30 | TestServer.SetPath("/tmp") 31 | } 32 | 33 | func TestMain(m *testing.M) { 34 | TestMongo.Session = TestServer.Session() 35 | TestMongo.EnsureIndexes() 36 | TestMongo.Session.Close() 37 | exitTest(m.Run()) 38 | } 39 | 40 | func exitTest(i int) { 41 | TestServer.Wipe() 42 | TestServer.Stop() 43 | os.Exit(i) 44 | } 45 | 46 | func TestInit(t *testing.T) { 47 | err := TestMongo.Init() 48 | if err.Error() != "no reachable servers" { 49 | t.Error("expecting no reachable servers error") 50 | } 51 | } 52 | 53 | func TestNew(t *testing.T) { 54 | m := New() 55 | if m.AddressIDs == nil || m.CardIDs == nil { 56 | t.Error("Expected non nil arrays") 57 | } 58 | } 59 | 60 | func TestAddUserIDs(t *testing.T) { 61 | m := New() 62 | uid := bson.NewObjectId() 63 | cid := bson.NewObjectId() 64 | aid := bson.NewObjectId() 65 | m.ID = uid 66 | m.AddressIDs = append(m.AddressIDs, aid) 67 | m.CardIDs = append(m.CardIDs, cid) 68 | m.AddUserIDs() 69 | if len(m.Addresses) != 1 && len(m.Cards) != 1 { 70 | t.Error( 71 | fmt.Sprintf( 72 | "Expected one card and one address added.")) 73 | } 74 | if m.Addresses[0].ID != aid.Hex() { 75 | t.Error("Expected matching Address Hex") 76 | } 77 | if m.Cards[0].ID != cid.Hex() { 78 | t.Error("Expected matching Card Hex") 79 | } 80 | if m.UserID != uid.Hex() { 81 | t.Error("Expected matching User Hex") 82 | } 83 | } 84 | 85 | func TestAddressAddId(t *testing.T) { 86 | m := MongoAddress{Address: users.Address{}} 87 | id := bson.NewObjectId() 88 | m.ID = id 89 | m.AddID() 90 | if m.Address.ID != id.Hex() { 91 | t.Error("Expected matching Address Hex") 92 | } 93 | } 94 | 95 | func TestCardAddId(t *testing.T) { 96 | m := MongoCard{Card: users.Card{}} 97 | id := bson.NewObjectId() 98 | m.ID = id 99 | m.AddID() 100 | if m.Card.ID != id.Hex() { 101 | t.Error("Expected matching Card Hex") 102 | } 103 | } 104 | 105 | func TestCreate(t *testing.T) { 106 | TestMongo.Session = TestServer.Session() 107 | defer TestMongo.Session.Close() 108 | err := TestMongo.CreateUser(&TestUser) 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | err = TestMongo.CreateUser(&TestUser) 113 | if err == nil { 114 | t.Error("Expected duplicate key error") 115 | } 116 | } 117 | 118 | func TestGetUserByName(t *testing.T) { 119 | TestMongo.Session = TestServer.Session() 120 | defer TestMongo.Session.Close() 121 | u, err := TestMongo.GetUserByName(TestUser.Username) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | if u.Username != TestUser.Username { 126 | t.Error("expected equal usernames") 127 | } 128 | _, err = TestMongo.GetUserByName("bogususers") 129 | if err == nil { 130 | t.Error("expected not found error") 131 | } 132 | } 133 | 134 | func TestGetUser(t *testing.T) { 135 | TestMongo.Session = TestServer.Session() 136 | defer TestMongo.Session.Close() 137 | _, err := TestMongo.GetUser(TestUser.UserID) 138 | if err != nil { 139 | t.Error(err) 140 | } 141 | } 142 | 143 | func TestGetUserAttributes(t *testing.T) { 144 | TestMongo.Session = TestServer.Session() 145 | defer TestMongo.Session.Close() 146 | 147 | } 148 | func TestGetURL(t *testing.T) { 149 | name = "test" 150 | password = "password" 151 | host = "thishostshouldnotexist:3038" 152 | u := getURL() 153 | if u.String() != "mongodb://test:password@thishostshouldnotexist:3038/users" { 154 | t.Error("expected url mismatch") 155 | } 156 | } 157 | 158 | func TestPing(t *testing.T) { 159 | TestMongo.Session = TestServer.Session() 160 | defer TestMongo.Session.Close() 161 | err := TestMongo.Ping() 162 | if err != nil { 163 | t.Error(err) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /docker-compose-zipkin.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | user: 5 | build: 6 | context: . 7 | image: weaveworksdemos/user 8 | hostname: user 9 | restart: always 10 | read_only: true 11 | environment: 12 | - MONGO_HOST=user-db:27017 13 | - ZIPKIN=http://zipkin:9411/api/v1/spans 14 | - reschedule=on-node-failure 15 | ports: 16 | - "8084:8084" 17 | user-db: 18 | build: 19 | context: ./docker/user-db/ 20 | image: weaveworksdemos/user-db 21 | hostname: user-db 22 | restart: always 23 | cap_drop: 24 | - all 25 | cap_add: 26 | - CHOWN 27 | - SETGID 28 | - SETUID 29 | read_only: true 30 | tmpfs: 31 | - /tmp:rw,noexec,nosuid 32 | environment: 33 | - reschedule=on-node-failure 34 | ports: 35 | - "27017:27017" 36 | zipkin: 37 | image: openzipkin/zipkin 38 | hostname: zipkin 39 | restart: always 40 | cap_drop: 41 | - all 42 | cap_add: 43 | - CHOWN 44 | - SETGID 45 | - SETUID 46 | read_only: true 47 | tmpfs: 48 | - /tmp:rw,noexec,nosuid 49 | environment: 50 | - reschedule=on-node-failure 51 | ports: 52 | - "9411:9411" 53 | zipkinseed: 54 | image: alpine 55 | command: /bin/sh -c 'sleep 10 ; wget http://user:8084/health ; wget http://user:8084/customers ; wget http://user:8084/customers/57a98d98e4b00679b4a830af ; wget http://user:8084/cards' 56 | 57 | 58 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | user: 5 | build: 6 | context: . 7 | image: weaveworksdemos/user 8 | hostname: user 9 | restart: always 10 | cap_drop: 11 | - all 12 | cap_add: 13 | - NET_BIND_SERVICE 14 | read_only: true 15 | environment: 16 | - MONGO_HOST=user-db:27017 17 | - reschedule=on-node-failure 18 | ports: 19 | - "8080:80" 20 | user-db: 21 | build: 22 | context: ./docker/user-db/ 23 | image: weaveworksdemos/user-db 24 | hostname: user-db 25 | restart: always 26 | cap_drop: 27 | - all 28 | cap_add: 29 | - CHOWN 30 | - SETGID 31 | - SETUID 32 | read_only: true 33 | tmpfs: 34 | - /tmp:rw,noexec,nosuid 35 | environment: 36 | - reschedule=on-node-failure 37 | ports: 38 | - "27017:27017" 39 | -------------------------------------------------------------------------------- /docker/user-db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mongo:3 2 | ADD ./scripts /tmp/scripts 3 | 4 | # Modify child mongo to use /data/db-accounts as dbpath (because /data/db wont persist the build because it is already a VOLUME) 5 | RUN mkdir -p /data/db-users \ 6 | && echo "dbpath = /data/db-users" > /etc/mongodb.conf \ 7 | && chown -R mongodb:mongodb /data/db-users 8 | 9 | RUN su - mongodb && mongod --fork --logpath /var/log/mongodb.log --dbpath /data/db-users \ 10 | && /tmp/scripts/mongo_create_insert.sh \ 11 | && mongod --dbpath /data/db-users --shutdown \ 12 | && chown -R mongodb /data/db-users 13 | 14 | # Make the new dir a VOLUME to persist it 15 | VOLUME /data/db-users 16 | 17 | CMD ["mongod", "--config", "/etc/mongodb.conf", "--smallfiles"] 18 | -------------------------------------------------------------------------------- /docker/user-db/scripts/accounts-create.js: -------------------------------------------------------------------------------- 1 | db.createCollection("addresses"); 2 | db.createCollection("cards"); 3 | db.createCollection("customers"); 4 | 5 | 6 | -------------------------------------------------------------------------------- /docker/user-db/scripts/address-insert.js: -------------------------------------------------------------------------------- 1 | function get_results(result) { 2 | print(tojson(result)); 3 | } 4 | 5 | function insert_address(object) { 6 | print(db.addresses.insert(object)); 7 | } 8 | 9 | insert_address({ 10 | "_id": ObjectId("57a98d98e4b00679b4a830ad"), 11 | "number": "246", 12 | "street": "Whitelees Road", 13 | "city": "Glasgow", 14 | "postcode": "G67 3DL", 15 | "country": "United Kingdom" 16 | }); 17 | insert_address({ 18 | "_id": ObjectId("57a98d98e4b00679b4a830b0"), 19 | "number": "246", 20 | "street": "Whitelees Road", 21 | "city": "Glasgow", 22 | "postcode": "G67 3DL", 23 | "country": "United Kingdom" 24 | }); 25 | insert_address({ 26 | "_id": ObjectId("57a98d98e4b00679b4a830b3"), 27 | "number": "4", 28 | "street": "Maes-Y-Deri", 29 | "city": "Aberdare", 30 | "postcode": "CF44 6TF", 31 | "country": "United Kingdom" 32 | }); 33 | insert_address({ 34 | "_id": ObjectId("57a98ddce4b00679b4a830d1"), 35 | "number": "3", 36 | "street": "my road", 37 | "city": "London", 38 | "country": "UK" 39 | }); 40 | 41 | print("________ADDRESS DATA_______"); 42 | db.addresses.find().forEach(get_results); 43 | print("______END ADDRESS DATA_____"); 44 | -------------------------------------------------------------------------------- /docker/user-db/scripts/card-insert.js: -------------------------------------------------------------------------------- 1 | function get_results(result) { 2 | print(tojson(result)); 3 | } 4 | 5 | function insert_card(object) { 6 | print(db.cards.insert(object)); 7 | } 8 | 9 | insert_card({ 10 | "_id": ObjectId("57a98d98e4b00679b4a830ae"), 11 | "longNum": "5953580604169678", 12 | "expires": "08/19", 13 | "ccv": "678" 14 | }); 15 | insert_card({ 16 | "_id": ObjectId("57a98d98e4b00679b4a830b1"), 17 | "longNum": "5544154011345918", 18 | "expires": "08/19", 19 | "ccv": "958" 20 | }); 21 | insert_card({ 22 | "_id": ObjectId("57a98d98e4b00679b4a830b4"), 23 | "longNum": "0908415193175205", 24 | "expires": "08/19", 25 | "ccv": "280" 26 | }); 27 | insert_card({ 28 | "_id": ObjectId("57a98ddce4b00679b4a830d2"), 29 | "longNum": "5429804235432", 30 | "expires": "04/16", 31 | "ccv": "432" 32 | }); 33 | 34 | print("________CARD DATA_______"); 35 | db.cards.find().forEach(get_results); 36 | print("______END CARD DATA_____"); 37 | 38 | 39 | -------------------------------------------------------------------------------- /docker/user-db/scripts/customer-insert.js: -------------------------------------------------------------------------------- 1 | function get_results(result) { 2 | print(tojson(result)); 3 | } 4 | 5 | function insert_customer(object) { 6 | print(db.customers.insert(object)); 7 | } 8 | 9 | insert_customer({ 10 | "_id": ObjectId("57a98d98e4b00679b4a830af"), 11 | "firstName": "Eve", 12 | "lastName": "Berger", 13 | "username": "Eve_Berger", 14 | "password": "fec51acb3365747fc61247da5e249674cf8463c2", 15 | "salt": "c748112bc027878aa62812ba1ae00e40ad46d497", 16 | "addresses": [ObjectId("57a98d98e4b00679b4a830ad")], 17 | "cards": [ObjectId("57a98d98e4b00679b4a830ae")] 18 | }); 19 | //pass eve 20 | insert_customer({ 21 | "_id": ObjectId("57a98d98e4b00679b4a830b2"), 22 | "firstName": "User", 23 | "lastName": "Name", 24 | "username": "user", 25 | "password": "e2de7202bb2201842d041f6de201b10438369fb8", 26 | "salt": "6c1c6176e8b455ef37da13d953df971c249d0d8e", 27 | "addresses": [ObjectId("57a98d98e4b00679b4a830b0")], 28 | "cards": [ObjectId("57a98d98e4b00679b4a830b1")] 29 | }); 30 | //pass password 31 | insert_customer({ 32 | "_id": ObjectId("57a98d98e4b00679b4a830b5"), 33 | "firstName": "User1", 34 | "lastName": "Name1", 35 | "username": "user1", 36 | "password": "8f31df4dcc25694aeb0c212118ae37bbd6e47bcd", 37 | "salt": "bd832b0e10c6882deabc5e8e60a37689e2b708c2", 38 | "addresses": [ObjectId("57a98d98e4b00679b4a830b3")], 39 | "cards": [ObjectId("57a98d98e4b00679b4a830b4")] 40 | }); 41 | //pass passsord 42 | print("_______CUSTOMER DATA_______"); 43 | db.customers.find().forEach(get_results); 44 | print("______END CUSTOMER DATA_____"); 45 | -------------------------------------------------------------------------------- /docker/user-db/scripts/mongo_create_insert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR=$(dirname "$0") 4 | 5 | mongod --fork --logpath /var/log/mongodb.log --dbpath /data/db/ 6 | 7 | FILES=$SCRIPT_DIR/*-create.js 8 | for f in $FILES; do mongo localhost:27017/users $f; done 9 | 10 | FILES=$SCRIPT_DIR/*-insert.js 11 | for f in $FILES; do mongo localhost:27017/users $f; done 12 | -------------------------------------------------------------------------------- /docker/user/Dockerfile-release: -------------------------------------------------------------------------------- 1 | FROM golang:1.7-alpine 2 | 3 | COPY . /go/src/github.com/microservices-demo/user/ 4 | WORKDIR /go/src/github.com/microservices-demo/user/ 5 | 6 | RUN apk update 7 | RUN apk add git 8 | RUN go get -v github.com/Masterminds/glide 9 | RUN glide install && CGO_ENABLED=0 go build -a -installsuffix cgo -o /user main.go 10 | 11 | FROM alpine:3.4 12 | 13 | ENV SERVICE_USER=myuser \ 14 | SERVICE_UID=10001 \ 15 | SERVICE_GROUP=mygroup \ 16 | SERVICE_GID=10001 17 | 18 | RUN addgroup -g ${SERVICE_GID} ${SERVICE_GROUP} && \ 19 | adduser -g "${SERVICE_NAME} user" -D -H -G ${SERVICE_GROUP} -s /sbin/nologin -u ${SERVICE_UID} ${SERVICE_USER} 20 | 21 | ENV HATEAOS user 22 | ENV USER_DATABASE mongodb 23 | ENV MONGO_HOST user-db 24 | 25 | WORKDIR / 26 | EXPOSE 8080 27 | COPY --from=0 /user / 28 | 29 | RUN chmod +x /user && \ 30 | chown -R ${SERVICE_USER}:${SERVICE_GROUP} /user 31 | 32 | USER ${SERVICE_USER} 33 | 34 | CMD ["/user", "-port=8080"] 35 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: ab1322ca6d24e40907af3a701959880ad721afa2e1b67eaf0c945bb74c9a5c30 2 | updated: 2017-04-28T12:53:43.461401871+02:00 3 | imports: 4 | - name: github.com/afex/hystrix-go 5 | version: 39520ddd07a9d9a071d615f7476798659f5a3b89 6 | subpackages: 7 | - hystrix 8 | - name: github.com/apache/thrift 9 | version: e0ccbd6e62e14f32d7c5fe0f9cec6eff3259b863 10 | subpackages: 11 | - lib/go/thrift 12 | - name: github.com/beorn7/perks 13 | version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 14 | subpackages: 15 | - quantile 16 | - name: github.com/davecgh/go-spew 17 | version: 346938d642f2ec3594ed81d874461961cd0faa76 18 | subpackages: 19 | - spew 20 | - name: github.com/eapache/go-resiliency 21 | version: b86b1ec0dd4209a588dc1285cdd471e73525c0b3 22 | subpackages: 23 | - breaker 24 | - name: github.com/eapache/go-xerial-snappy 25 | version: bb955e01b9346ac19dc29eb16586c90ded99a98c 26 | - name: github.com/eapache/queue 27 | version: 44cc805cf13205b55f69e14bcb69867d1ae92f98 28 | - name: github.com/felixge/httpsnoop 29 | version: 287b56e9e314227d3113c7c6b434d31aec68089d 30 | - name: github.com/go-kit/kit 31 | version: fadad6fffe0466b19df9efd9acde5c9a52df5fa4 32 | subpackages: 33 | - circuitbreaker 34 | - endpoint 35 | - log 36 | - metrics 37 | - metrics/internal/lv 38 | - metrics/prometheus 39 | - tracing/opentracing 40 | - transport/http 41 | - name: github.com/go-logfmt/logfmt 42 | version: d4327190ff838312623b09bfeb50d7c93c8d9c1d 43 | - name: github.com/go-stack/stack 44 | version: 100eb0c0a9c5b306ca2fb4f165df21d80ada4b82 45 | - name: github.com/gogo/protobuf 46 | version: f9114dace7bd920b32f943b3c73fafbcbab2bf31 47 | subpackages: 48 | - proto 49 | - name: github.com/golang/protobuf 50 | version: 1f49d83d9aa00e6ce4fc8258c71cc7786aec968a 51 | subpackages: 52 | - proto 53 | - name: github.com/golang/snappy 54 | version: d9eb7a3d35ec988b8585d4a0068e462c27d28380 55 | - name: github.com/gorilla/context 56 | version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 57 | - name: github.com/gorilla/mux 58 | version: cf79e51a62d8219d52060dfc1b4e810414ba2d15 59 | - name: github.com/klauspost/crc32 60 | version: cb6bfca970f6908083f26f39a79009d608efd5cd 61 | - name: github.com/kr/logfmt 62 | version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 63 | - name: github.com/matttproud/golang_protobuf_extensions 64 | version: c12348ce28de40eed0136aa2b644d0ee0650e56c 65 | subpackages: 66 | - pbutil 67 | - name: github.com/opentracing/opentracing-go 68 | version: 5e5abf838007b08f96ae057bc182636a178da0b9 69 | subpackages: 70 | - ext 71 | - log 72 | - name: github.com/openzipkin/zipkin-go-opentracing 73 | version: 594640b9ef7e5c994e8d9499359d693c032d738c 74 | subpackages: 75 | - _thrift/gen-go/scribe 76 | - _thrift/gen-go/zipkincore 77 | - flag 78 | - types 79 | - wire 80 | - name: github.com/pierrec/lz4 81 | version: 5c9560bfa9ace2bf86080bf40d46b34ae44604df 82 | - name: github.com/pierrec/xxHash 83 | version: 5a004441f897722c627870a981d02b29924215fa 84 | subpackages: 85 | - xxHash32 86 | - name: github.com/prometheus/client_golang 87 | version: 5636dc67ae776adf5590da7349e70fbb9559972d 88 | subpackages: 89 | - prometheus 90 | - prometheus/promhttp 91 | - name: github.com/prometheus/client_model 92 | version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6 93 | subpackages: 94 | - go 95 | - name: github.com/prometheus/common 96 | version: 9a94032291f2192936512bab367bc45e77990d6a 97 | subpackages: 98 | - expfmt 99 | - internal/bitbucket.org/ww/goautoneg 100 | - model 101 | - name: github.com/prometheus/procfs 102 | version: abf152e5f3e97f2fafac028d2cc06c1feb87ffa5 103 | - name: github.com/rcrowley/go-metrics 104 | version: 1f30fe9094a513ce4c700b9a54458bbb0c96996c 105 | - name: github.com/Shopify/sarama 106 | version: 0fb560e5f7fbcaee2f75e3c34174320709f69944 107 | - name: github.com/Sirupsen/logrus 108 | version: 10f801ebc38b33738c9d17d50860f484a0988ff5 109 | - name: github.com/weaveworks/common 110 | version: f94043b3da140c7a735b1f2f286d72d19014b200 111 | subpackages: 112 | - errors 113 | - middleware 114 | - user 115 | - name: golang.org/x/net 116 | version: 7394c112eae4dba7e96bfcfe738e6373d61772b4 117 | subpackages: 118 | - context 119 | - context/ctxhttp 120 | - http2 121 | - http2/hpack 122 | - internal/timeseries 123 | - lex/httplex 124 | - trace 125 | - name: golang.org/x/sys 126 | version: 99f16d856c9836c42d24e7ab64ea72916925fa97 127 | subpackages: 128 | - unix 129 | - name: google.golang.org/grpc 130 | version: 50955793b0183f9de69bd78e2ec251cf20aab121 131 | subpackages: 132 | - codes 133 | - credentials 134 | - grpclog 135 | - internal 136 | - metadata 137 | - naming 138 | - peer 139 | - stats 140 | - tap 141 | - transport 142 | - name: gopkg.in/mgo.v2 143 | version: 3f83fa5005286a7fe593b055f0d7771a7dce4655 144 | subpackages: 145 | - bson 146 | - dbtest 147 | - internal/json 148 | - internal/sasl 149 | - internal/scram 150 | - name: gopkg.in/tomb.v2 151 | version: 14b3d72120e8d10ea6e6b7f87f7175734b1faab8 152 | testImports: [] 153 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/microservices-demo/user 2 | import: 3 | - package: github.com/go-kit/kit 4 | version: v0.4.0 5 | subpackages: 6 | - endpoint 7 | - log 8 | - metrics 9 | - metrics/prometheus 10 | - tracing/opentracing 11 | - transport/http 12 | - circuitbreaker 13 | - package: github.com/gorilla/mux 14 | - package: github.com/opentracing/opentracing-go 15 | - package: github.com/openzipkin/zipkin-go-opentracing 16 | - package: github.com/prometheus/client_golang 17 | subpackages: 18 | - prometheus 19 | - prometheus/promhttp 20 | - package: gopkg.in/mgo.v2 21 | subpackages: 22 | - bson 23 | - package: gopkg.in/tomb.v2 24 | - package: github.com/afex/hystrix-go 25 | subpackages: 26 | - hystrix 27 | - package: github.com/felixge/httpsnoop 28 | - package: github.com/weaveworks/common 29 | subpackages: 30 | - middleware 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "syscall" 12 | 13 | corelog "log" 14 | 15 | "github.com/go-kit/kit/log" 16 | kitprometheus "github.com/go-kit/kit/metrics/prometheus" 17 | "github.com/microservices-demo/user/api" 18 | "github.com/microservices-demo/user/db" 19 | "github.com/microservices-demo/user/db/mongodb" 20 | stdopentracing "github.com/opentracing/opentracing-go" 21 | zipkin "github.com/openzipkin/zipkin-go-opentracing" 22 | stdprometheus "github.com/prometheus/client_golang/prometheus" 23 | commonMiddleware "github.com/weaveworks/common/middleware" 24 | ) 25 | 26 | var ( 27 | port string 28 | zip string 29 | ) 30 | 31 | var ( 32 | HTTPLatency = stdprometheus.NewHistogramVec(stdprometheus.HistogramOpts{ 33 | Name: "http_request_duration_seconds", 34 | Help: "Time (in seconds) spent serving HTTP requests.", 35 | Buckets: stdprometheus.DefBuckets, 36 | }, []string{"method", "path", "status_code", "isWS"}) 37 | ) 38 | 39 | const ( 40 | ServiceName = "user" 41 | ) 42 | 43 | func init() { 44 | stdprometheus.MustRegister(HTTPLatency) 45 | flag.StringVar(&zip, "zipkin", os.Getenv("ZIPKIN"), "Zipkin address") 46 | flag.StringVar(&port, "port", "8084", "Port on which to run") 47 | db.Register("mongodb", &mongodb.Mongo{}) 48 | } 49 | 50 | func main() { 51 | 52 | flag.Parse() 53 | // Mechanical stuff. 54 | errc := make(chan error) 55 | 56 | // Log domain. 57 | var logger log.Logger 58 | { 59 | logger = log.NewLogfmtLogger(os.Stderr) 60 | logger = log.With(logger, "ts", log.DefaultTimestampUTC) 61 | logger = log.With(logger, "caller", log.DefaultCaller) 62 | } 63 | 64 | // Find service local IP. 65 | conn, err := net.Dial("udp", "8.8.8.8:80") 66 | if err != nil { 67 | logger.Log("err", err) 68 | os.Exit(1) 69 | } 70 | localAddr := conn.LocalAddr().(*net.UDPAddr) 71 | host := strings.Split(localAddr.String(), ":")[0] 72 | defer conn.Close() 73 | 74 | var tracer stdopentracing.Tracer 75 | { 76 | if zip == "" { 77 | tracer = stdopentracing.NoopTracer{} 78 | } else { 79 | logger := log.With(logger, "tracer", "Zipkin") 80 | logger.Log("addr", zip) 81 | collector, err := zipkin.NewHTTPCollector( 82 | zip, 83 | zipkin.HTTPLogger(logger), 84 | ) 85 | if err != nil { 86 | logger.Log("err", err) 87 | os.Exit(1) 88 | } 89 | tracer, err = zipkin.NewTracer( 90 | zipkin.NewRecorder(collector, false, fmt.Sprintf("%v:%v", host, port), ServiceName), 91 | ) 92 | if err != nil { 93 | logger.Log("err", err) 94 | os.Exit(1) 95 | } 96 | } 97 | stdopentracing.InitGlobalTracer(tracer) 98 | } 99 | dbconn := false 100 | for !dbconn { 101 | err := db.Init() 102 | if err != nil { 103 | if err == db.ErrNoDatabaseSelected { 104 | corelog.Fatal(err) 105 | } 106 | corelog.Print(err) 107 | } else { 108 | dbconn = true 109 | } 110 | } 111 | 112 | fieldKeys := []string{"method"} 113 | // Service domain. 114 | var service api.Service 115 | { 116 | service = api.NewFixedService() 117 | service = api.LoggingMiddleware(logger)(service) 118 | service = api.NewInstrumentingService( 119 | kitprometheus.NewCounterFrom( 120 | stdprometheus.CounterOpts{ 121 | Namespace: "microservices_demo", 122 | Subsystem: "user", 123 | Name: "request_count", 124 | Help: "Number of requests received.", 125 | }, 126 | fieldKeys), 127 | kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{ 128 | Namespace: "microservices_demo", 129 | Subsystem: "user", 130 | Name: "request_latency_microseconds", 131 | Help: "Total duration of requests in microseconds.", 132 | }, fieldKeys), 133 | service, 134 | ) 135 | } 136 | 137 | // Endpoint domain. 138 | endpoints := api.MakeEndpoints(service, tracer) 139 | 140 | // HTTP router 141 | router := api.MakeHTTPHandler(endpoints, logger, tracer) 142 | 143 | httpMiddleware := []commonMiddleware.Interface{ 144 | commonMiddleware.Instrument{ 145 | Duration: HTTPLatency, 146 | RouteMatcher: router, 147 | }, 148 | } 149 | 150 | // Handler 151 | handler := commonMiddleware.Merge(httpMiddleware...).Wrap(router) 152 | 153 | // Create and launch the HTTP server. 154 | go func() { 155 | logger.Log("transport", "HTTP", "port", port) 156 | errc <- http.ListenAndServe(fmt.Sprintf(":%v", port), handler) 157 | }() 158 | 159 | // Capture interrupts. 160 | go func() { 161 | c := make(chan os.Signal) 162 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 163 | errc <- fmt.Errorf("%s", <-c) 164 | }() 165 | 166 | logger.Log("exit", <-errc) 167 | } 168 | -------------------------------------------------------------------------------- /scripts/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ev 4 | 5 | if [[ -z "$GROUP" ]] ; then 6 | echo "Cannot find GROUP env var" 7 | exit 1 8 | fi 9 | 10 | if [[ -z "$COMMIT" ]] ; then 11 | echo "Cannot find COMMIT env var" 12 | exit 1 13 | fi 14 | 15 | push() { 16 | DOCKER_PUSH=1; 17 | while [ $DOCKER_PUSH -gt 0 ] ; do 18 | echo "Pushing $1"; 19 | docker push $1; 20 | DOCKER_PUSH=$(echo $?); 21 | if [[ "$DOCKER_PUSH" -gt 0 ]] ; then 22 | echo "Docker push failed with exit code $DOCKER_PUSH"; 23 | fi; 24 | done; 25 | } 26 | 27 | tag_and_push_all() { 28 | if [[ -z "$1" ]] ; then 29 | echo "Please pass the tag" 30 | exit 1 31 | else 32 | TAG=$1 33 | fi 34 | for m in ./docker/*/; do 35 | REPO=${GROUP}/$(basename $m) 36 | if [[ "$COMMIT" != "$TAG" ]]; then 37 | docker tag ${REPO}:${COMMIT} ${REPO}:${TAG} 38 | fi 39 | push "$REPO:$TAG"; 40 | done; 41 | } 42 | 43 | # Push snapshot when in master 44 | if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 45 | tag_and_push_all master-${COMMIT:0:8} 46 | fi; 47 | 48 | # Push tag and latest when tagged 49 | if [ -n "$TRAVIS_TAG" ]; then 50 | tag_and_push_all ${TRAVIS_TAG} 51 | tag_and_push_all latest 52 | fi; 53 | -------------------------------------------------------------------------------- /scripts/testcontainer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sleep 10 3 | RESPONSECODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8084/customers) 4 | if [ $RESPONSECODE != 200 ] 5 | then 6 | echo Error: bad response code from user service $RESPONSECODE 7 | exit 1 8 | fi 9 | echo Successful response from container $RESPONSECODE 10 | -------------------------------------------------------------------------------- /users/addresses.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | type Address struct { 4 | Street string `json:"street" bson:"street,omitempty"` 5 | Number string `json:"number" bson:"number,omitempty"` 6 | Country string `json:"country" bson:"country,omitempty"` 7 | City string `json:"city" bson:"city,omitempty"` 8 | PostCode string `json:"postcode" bson:"postcode,omitempty"` 9 | ID string `json:"id" bson:"-"` 10 | Links Links `json:"_links"` 11 | } 12 | 13 | func (a *Address) AddLinks() { 14 | a.Links.AddAddress(a.ID) 15 | } 16 | -------------------------------------------------------------------------------- /users/addresses_test.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestAddLinksAdd(t *testing.T) { 9 | domain = "mydomain" 10 | a := Address{ID: "test"} 11 | a.AddLinks() 12 | h := Href{"http://mydomain/addresses/test"} 13 | if !reflect.DeepEqual(a.Links["address"], h) { 14 | t.Error("expected equal address links") 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /users/cards.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Card struct { 9 | LongNum string `json:"longNum" bson:"longNum"` 10 | Expires string `json:"expires" bson:"expires"` 11 | CCV string `json:"ccv" bson:"ccv"` 12 | ID string `json:"id" bson:"-"` 13 | Links Links `json:"_links" bson:"-"` 14 | } 15 | 16 | func (c *Card) MaskCC() { 17 | l := len(c.LongNum) - 4 18 | c.LongNum = fmt.Sprintf("%v%v", strings.Repeat("*", l), c.LongNum[l:]) 19 | } 20 | 21 | func (c *Card) AddLinks() { 22 | c.Links.AddCard(c.ID) 23 | } 24 | -------------------------------------------------------------------------------- /users/cards_test.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestAddLinksCard(t *testing.T) { 9 | domain = "mydomain" 10 | c := Card{ID: "test"} 11 | c.AddLinks() 12 | h := Href{"http://mydomain/cards/test"} 13 | if !reflect.DeepEqual(c.Links["card"], h) { 14 | t.Error("expected equal address links") 15 | } 16 | 17 | } 18 | 19 | func TestMaskCC(t *testing.T) { 20 | test1 := "1234567890" 21 | c := Card{LongNum: test1} 22 | c.MaskCC() 23 | test1comp := "******7890" 24 | if c.LongNum != test1comp { 25 | t.Errorf("Expected matching CC number %v received %v", test1comp, test1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /users/links.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | var ( 10 | domain string 11 | entitymap = map[string]string{ 12 | "customer": "customers", 13 | "address": "addresses", 14 | "card": "cards", 15 | } 16 | ) 17 | 18 | func init() { 19 | flag.StringVar(&domain, "link-domain", os.Getenv("HATEAOS"), "HATEAOS link domain") 20 | } 21 | 22 | type Links map[string]Href 23 | 24 | func (l *Links) AddLink(ent string, id string) { 25 | nl := make(Links) 26 | link := fmt.Sprintf("http://%v/%v/%v", domain, entitymap[ent], id) 27 | nl[ent] = Href{link} 28 | nl["self"] = Href{link} 29 | *l = nl 30 | 31 | } 32 | 33 | func (l *Links) AddAttrLink(attr string, corent string, id string) { 34 | link := fmt.Sprintf("http://%v/%v/%v/%v", domain, entitymap[corent], id, entitymap[attr]) 35 | nl := *l 36 | nl[entitymap[attr]] = Href{link} 37 | *l = nl 38 | } 39 | 40 | func (l *Links) AddCustomer(id string) { 41 | l.AddLink("customer", id) 42 | l.AddAttrLink("address", "customer", id) 43 | l.AddAttrLink("card", "customer", id) 44 | } 45 | 46 | func (l *Links) AddAddress(id string) { 47 | l.AddLink("address", id) 48 | } 49 | 50 | func (l *Links) AddCard(id string) { 51 | l.AddLink("card", id) 52 | } 53 | 54 | type Href struct { 55 | string `json:"href"` 56 | } 57 | -------------------------------------------------------------------------------- /users/users.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "crypto/sha1" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | var ( 13 | ErrNoCustomerInResponse = errors.New("Response has no matching customer") 14 | ErrMissingField = "Error missing %v" 15 | ) 16 | 17 | type User struct { 18 | FirstName string `json:"firstName" bson:"firstName"` 19 | LastName string `json:"lastName" bson:"lastName"` 20 | Email string `json:"-" bson:"email"` 21 | Username string `json:"username" bson:"username"` 22 | Password string `json:"-" bson:"password,omitempty"` 23 | Addresses []Address `json:"-,omitempty" bson:"-"` 24 | Cards []Card `json:"-,omitempty" bson:"-"` 25 | UserID string `json:"id" bson:"-"` 26 | Links Links `json:"_links"` 27 | Salt string `json:"-" bson:"salt"` 28 | } 29 | 30 | func New() User { 31 | u := User{Addresses: make([]Address, 0), Cards: make([]Card, 0)} 32 | u.NewSalt() 33 | return u 34 | } 35 | 36 | func (u *User) Validate() error { 37 | if u.FirstName == "" { 38 | return fmt.Errorf(ErrMissingField, "FirstName") 39 | } 40 | if u.LastName == "" { 41 | return fmt.Errorf(ErrMissingField, "LastName") 42 | } 43 | if u.Username == "" { 44 | return fmt.Errorf(ErrMissingField, "Username") 45 | } 46 | if u.Password == "" { 47 | return fmt.Errorf(ErrMissingField, "Password") 48 | } 49 | return nil 50 | } 51 | 52 | func (u *User) MaskCCs() { 53 | for k, c := range u.Cards { 54 | c.MaskCC() 55 | u.Cards[k] = c 56 | } 57 | } 58 | 59 | func (u *User) AddLinks() { 60 | u.Links.AddCustomer(u.UserID) 61 | } 62 | 63 | func (u *User) NewSalt() { 64 | h := sha1.New() 65 | io.WriteString(h, strconv.Itoa(int(time.Now().UnixNano()))) 66 | u.Salt = fmt.Sprintf("%x", h.Sum(nil)) 67 | } 68 | -------------------------------------------------------------------------------- /users/users_test.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestNew(t *testing.T) { 9 | u := New() 10 | if len(u.Addresses) != 0 && len(u.Cards) != 0 { 11 | t.Error("Expected zero length addresses and cards") 12 | } 13 | } 14 | 15 | func TestValidate(t *testing.T) { 16 | u := New() 17 | err := u.Validate() 18 | if err.Error() != fmt.Sprintf(ErrMissingField, "FirstName") { 19 | t.Error("Expected missing first name error") 20 | } 21 | u.FirstName = "test" 22 | err = u.Validate() 23 | if err.Error() != fmt.Sprintf(ErrMissingField, "LastName") { 24 | t.Error("Expected missing last name error") 25 | } 26 | u.LastName = "test" 27 | err = u.Validate() 28 | if err.Error() != fmt.Sprintf(ErrMissingField, "Username") { 29 | t.Error("Expected missing username error") 30 | } 31 | u.Username = "test" 32 | err = u.Validate() 33 | if err.Error() != fmt.Sprintf(ErrMissingField, "Password") { 34 | t.Error("Expected missing password error") 35 | } 36 | u.Password = "test" 37 | err = u.Validate() 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | } 42 | 43 | func TestMaskCCs(t *testing.T) { 44 | u := New() 45 | u.Cards = append(u.Cards, Card{LongNum: "abcdefg"}) 46 | u.Cards = append(u.Cards, Card{LongNum: "hijklmnopqrs"}) 47 | u.MaskCCs() 48 | if u.Cards[0].LongNum != "***defg" { 49 | t.Error("Card one CC not masked") 50 | } 51 | if u.Cards[1].LongNum != "********pqrs" { 52 | t.Error("Card two CC not masked") 53 | } 54 | } 55 | --------------------------------------------------------------------------------