├── .circleci
└── config.yml
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── client
├── .dockerignore
├── .env
├── .gitignore
├── .storybook
│ ├── addons.js
│ ├── config.js
│ └── preview-head.html
├── Dockerfile
├── LICENSE
├── README.md
├── TODO.md
├── cypress.json
├── cypress
│ ├── .gitignore
│ ├── fixtures
│ │ └── example.json
│ ├── integration
│ │ └── 1-add-new-note_spec.js
│ ├── plugins
│ │ └── index.js
│ └── support
│ │ ├── commands.js
│ │ └── index.js
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── scripts
│ └── setup.js
├── src
│ ├── actions
│ │ ├── auth.js
│ │ ├── index.js
│ │ ├── notes.js
│ │ ├── ui.js
│ │ └── users.js
│ ├── components
│ │ ├── App.js
│ │ ├── ErrorDisplay.js
│ │ ├── LoginPage.js
│ │ ├── MainPage.js
│ │ ├── Navbar.js
│ │ ├── NewGroup.js
│ │ ├── NewNote.js
│ │ ├── Note.js
│ │ ├── NoteLayout.css
│ │ ├── NoteLayout.js
│ │ ├── NoteList.css
│ │ ├── NoteList.js
│ │ ├── PrivateRoute.js
│ │ ├── ShareGroup.js
│ │ ├── ShareNote.js
│ │ ├── ShareSelect.js
│ │ └── UserProfile.js
│ ├── constants
│ │ ├── auth.js
│ │ ├── index.js
│ │ ├── notes.js
│ │ ├── ui.js
│ │ └── users.js
│ ├── history.js
│ ├── index.js
│ ├── reducers
│ │ ├── alert.js
│ │ ├── auth.js
│ │ ├── index.js
│ │ ├── modals.js
│ │ ├── notes.js
│ │ └── users.js
│ ├── services
│ │ ├── auth.js
│ │ ├── index.js
│ │ ├── notes.js
│ │ ├── users.js
│ │ └── utils.js
│ ├── store.js
│ └── utils.js
└── stories
│ └── index.stories.js
├── k8s
├── client.yaml
├── ingress-controller-preprod.yaml
├── ingress.yaml
├── production-issuer.yaml
├── server.yaml
└── staging-issuer.yaml
├── scripts
├── build-images.sh
└── push-images.sh
├── server
├── .dockerignore
├── .gitignore
├── .realize.yaml
├── Dockerfile
├── LICENSE
├── README.md
├── TODO.md
├── go.mod
├── go.sum
├── jwt.go
├── key.pem
├── main.go
├── main_test.go
├── notes-db
│ └── .gitkeep
├── notes.go
├── public.pem
└── users.go
└── skaffold.yaml
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build-server:
4 | docker:
5 | - image: circleci/golang:1.12
6 | working_directory: /home/circleci/notes
7 | steps:
8 | - checkout
9 | - attach_workspace:
10 | at: ./
11 | - run: ( cd server && go test && go build . )
12 | - persist_to_workspace:
13 | root: ./
14 | paths:
15 | - ./server
16 | client-install:
17 | docker:
18 | - image: circleci/node:11
19 | working_directory: /home/circleci/notes
20 | steps:
21 | - checkout
22 | - attach_workspace:
23 | at: ./
24 | - run: npm ci --prefix client
25 | - persist_to_workspace:
26 | root: ./
27 | paths:
28 | - ./client
29 | create-app:
30 | docker:
31 | - image: circleci/node:11
32 | environment:
33 | DATAPEPS_API_HOST: preprod-api.datapeps.com
34 | steps:
35 | - attach_workspace:
36 | at: ./
37 | - run:
38 | command: |
39 | cd server
40 | openssl genrsa -out key.pem 2048
41 | openssl rsa -in key.pem -outform PEM -pubout -out public.pem
42 | - run: node client/scripts/setup.js
43 | - run: npm run build --prefix client
44 | - persist_to_workspace:
45 | root: ./
46 | paths:
47 | - ./client
48 | - ./server
49 | test-e2e:
50 | docker:
51 | - image: cypress/base:10
52 | environment:
53 | ## this enables colors in the output
54 | TERM: xterm
55 | steps:
56 | - attach_workspace:
57 | at: ./
58 | - run:
59 | command: |
60 | ( cd server ; ./server & )
61 | ( cd client ; npm i serve ; node_modules/.bin/serve -s build -l 3000 & )
62 | cd client && node_modules/.bin/cypress install && node_modules/.bin/cypress run
63 | - store_artifacts:
64 | path: client/cypress/videos
65 | - store_artifacts:
66 | path: client/cypress/screenshots
67 | workflows:
68 | version: 2
69 | build:
70 | jobs:
71 | - build-server
72 | - client-install
73 | - create-app:
74 | requires:
75 | - build-server
76 | - client-install
77 | - test-e2e:
78 | requires:
79 | - create-app
80 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "authorizator",
4 | "cors",
5 | "crossorigin",
6 | "datapeps",
7 | "genrsa",
8 | "getall",
9 | "gonic",
10 | "gorm",
11 | "jinzhu",
12 | "js",
13 | "keypair",
14 | "minified",
15 | "nbsp",
16 | "openssl",
17 | "outform",
18 | "priv",
19 | "pubout",
20 | "qor",
21 | "sqlite",
22 | "unclip",
23 | "uncomment",
24 | "unscoped",
25 | "xmlns"
26 | ],
27 | "spellright.language": ["en"],
28 | "spellright.documentTypes": ["markdown", "latex", "plaintext"]
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 WALLIX
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Notes
2 |
3 | Notes is simple note-taking application, which serves as a demo for [DataPeps](https://datapeps.com). This application may also be a good starter for anyone interested in the Go/React stack.
4 |
5 | 
6 |
7 | # Directory structure
8 |
9 | - `/server` contains a REST service built with Go and SQLite
10 | - `/client` contains a web client built with React
11 |
12 | Please refer to each directory README to build and run.
13 |
14 | # Adding DataPeps
15 |
16 | Notes was built as a tutorial for the implementention of [End-to-End Encryption (E2EE)](https://en.wikipedia.org/wiki/End-to-end_encryption) with [DataPeps](https://github.com/wallix/datapeps-sdk-js). Thanks to E2EE, Notes will be strongly protected with encryption performed directly on client devices. Anyone that can access servers legally (admins, ...) or not (attackers, ...) will not be able to read user information.
17 |
18 | _This branch already has DataPeps support built-in. What was done from the parent commit is detailed below._
19 |
20 | Adding DataPeps support requires **no modification of the server code**. Only the `client/` needs to be updated.
21 |
22 | To add DataPeps support, awaiting the forthcoming blog post:
23 |
24 | 1. Checkout the datapeps branch `git checkout datapeps`
25 | 2. To review the datapeps integration `git diff master datapeps`
26 |
27 | There are [wonderful slides](https://github.com/wallix/notes/files/2686280/DataPeps.Notes.Demo.pdf) available as well. And if they're not, please tell us in the issues!
28 |
29 | # Warning
30 |
31 | Passwords are stored unencrypted in the database. It is not a problem when using Notes with DataPeps, but it should not be run as such without.
32 |
33 | # License
34 |
35 | Released under the Apache License
36 |
--------------------------------------------------------------------------------
/client/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | Dockerfile
3 |
4 | .env.local
5 |
--------------------------------------------------------------------------------
/client/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=https://demo-notes-api.datapeps.com
2 | REACT_APP_DATAPEPS_API=https://api.datapeps.com
3 | REACT_APP_DATAPEPS_APP_ID=demo-note
4 | REACT_APP_GROUP_SEED=E
5 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # production
7 | build
8 |
9 | # misc
10 | .DS_Store
11 | npm-debug.log
12 |
13 | .env.*
14 |
--------------------------------------------------------------------------------
/client/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register';
2 | import '@storybook/addon-links/register';
3 |
--------------------------------------------------------------------------------
/client/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from "@storybook/react";
2 |
3 | // automatically import all files ending in *.stories.js
4 | const req = require.context("../stories", true, /\.stories\.js$/);
5 | function loadStories() {
6 | req.keys().forEach(filename => req(filename));
7 | }
8 |
9 | configure(loadStories, module);
10 |
--------------------------------------------------------------------------------
/client/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
7 |
33 |
--------------------------------------------------------------------------------
/client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:11-alpine
2 |
3 | WORKDIR /home/node
4 |
5 | ADD package.json .
6 | ADD package-lock.json .
7 | RUN npm ci --only=prod
8 |
9 | ADD . /home/node
10 | RUN npm run build
11 |
12 | FROM node:11-alpine
13 | RUN npm install -g serve
14 | COPY --from=0 /home/node/build /build
15 |
16 | ENTRYPOINT [ "serve", "-s", "build" ]
17 |
--------------------------------------------------------------------------------
/client/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 WALLIX
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Notes
2 |
3 | A React/redux client for [demo-note-server](https://github.com/wallix/demo-note-server).
4 | This project will be part of the standard WALLIX DataPeps demo.
5 |
6 | ## Available Scripts
7 |
8 | In the project directory, you can run:
9 |
10 | ### `npm start`
11 |
12 | Runs the app in the development mode.
13 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
14 |
15 | The page will reload if you make edits.
16 | You will also see any lint errors in the console.
17 |
18 | ### `npm run build`
19 |
20 | Builds the app for production to the `build` folder.
21 | It correctly bundles React in production mode and optimizes the build for the best performance.
22 |
23 | The build is minified and the filenames include the hashes.
24 | Your app is ready to be deployed!
25 |
26 | ### `npm run eject`
27 |
28 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
29 |
30 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
31 |
32 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
33 |
34 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
35 |
36 | # About
37 |
38 | Licensed under the Apache license.
39 |
40 | (c) WALLIX, written by Henri Binsztok.
41 |
--------------------------------------------------------------------------------
/client/TODO.md:
--------------------------------------------------------------------------------
1 | # Roadmap
2 |
3 | ## Before DataPeps
4 |
5 | - Use `react-router` to implement `/login` DONE
6 | - Add sign in DONE
7 | - Get notes from server DONE
8 | - Post note to server DONE
9 | - Actual IDs from server DONE
10 | - Implement soft-delete DONE
11 | - Implement password update DONE
12 | - Implement user subscribe DONE
13 | - Note Sharing WIP
14 | - Cleanup code, rewrite w/ best practices WIP
15 |
16 | ## DataPeps
17 |
18 | - Add DataPeps SDK DONE
19 | - Integrate DataPeps application changes
20 | - User account "migration"
21 | - Share Notes with DataPeps: Left as an exercise to the reader
22 |
23 | # Bugs
24 |
25 | # UI-UX
26 |
27 | - Create note in Modal from 'New Note +' button in NavBar DONE
28 | - Align button in NavBar DONE
29 | - Delete button (x) in top right only DONE
30 | - 3 columns (depending on width) DONE
31 | - Better and more error messages
32 |
33 | # Source code
34 |
35 | - Rename Todo to Note DONE
36 | - Rename completed to IsDeleted DONE
37 | - Remove Formik DONE
38 | - Remove distinction between containers and components DONE
39 | - Use async/await WIP
40 |
41 | - Use 'import PropTypes from "prop-types";' MAYBE
42 | - Write tests in .spec.js files MAYBE
43 |
--------------------------------------------------------------------------------
/client/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3000",
3 | "viewportWidth": 1000,
4 | "viewportHeight": 800
5 | }
6 |
--------------------------------------------------------------------------------
/client/cypress/.gitignore:
--------------------------------------------------------------------------------
1 | screenshots/
2 | videos/
3 |
--------------------------------------------------------------------------------
/client/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/client/cypress/integration/1-add-new-note_spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const crypto = require("crypto"),
4 | shasum = crypto.createHash("sha1");
5 |
6 | shasum.update(new Date().getTime() + "");
7 |
8 | let seed = shasum.digest("hex").substring(0, 7);
9 | // Change seed by using command : CYPRESS_SEED="blabla" cypress open
10 | seed = Cypress.env("SEED") || seed;
11 |
12 | const username = `alice.${seed}`;
13 | const password = `password1234=?`;
14 |
15 | const formatedDate = new Intl.DateTimeFormat("en-US", {
16 | year: "numeric",
17 | month: "long",
18 | day: "numeric",
19 | hour: "numeric",
20 | minute: "numeric",
21 | second: "numeric",
22 | hour12: false
23 | }).format(new Date());
24 | const encryptedContent = `Here is a new encrypted note -- ${formatedDate}`;
25 | const encryptedSharedContent = `Here is a new encrypted shared note -- ${formatedDate}`;
26 |
27 | describe(`Notes creation ${seed}`, function() {
28 | for (let name of ["alice", "bob", "charlie"]) {
29 | it(`${name} create an account`, function() {
30 | cy.visit("/");
31 |
32 | let login = `${name}.${seed}`;
33 |
34 | cy.contains("Create an account").click();
35 | cy.get('.modal-body [name="username"]').type(login);
36 | cy.get('[name="password1"]').type(password);
37 | cy.get('[name="password2"]').type(password);
38 | cy.get('[data-test="create"]').click();
39 |
40 | // login
41 | cy.get('.panel-body [name="username"]').type(login);
42 | cy.get('[name="password"]').type(password);
43 | cy.get('[data-test="login-btn"]').click();
44 | cy.get('[data-test="create"]').should("not.exist");
45 |
46 | cy.visit("/");
47 | cy.login(login, password);
48 | });
49 | }
50 |
51 | it("Login error should appear and disapear", () => {
52 | const errorMsg = "incorrect Username or Password";
53 | cy.visit("/");
54 | cy.login("toto", "atat", false);
55 | cy.contains(errorMsg).should("exist");
56 | cy.get(".alert").click();
57 | cy.login(`alice.${seed}`, password);
58 | cy.contains("div.alert", errorMsg).should("not.exist");
59 | });
60 |
61 | it("alice sign in an create new note", function() {
62 | cy.visit("/");
63 |
64 | cy.get('[name="username"]').type(username);
65 | cy.get('[name="password"]').type(password);
66 | cy.get('[data-test="login-btn"]').click();
67 |
68 | // cy.get("div.modal-content", { timeout: 30000 }).then(modalNewPassword => {
69 | // if (modalNewPassword.find('[name="password1"]').length > 0) {
70 | // cy.get('[name="password1"]').type(password2);
71 | // cy.get('[name="password2"]').type(password2);
72 | // cy.get('[data-test="create"]').click();
73 | // }
74 | // });
75 |
76 | // Create new note
77 | cy.contains("New Note").click();
78 | cy.get('[name="title"]').type("New note encrypted");
79 | cy.get('[name="content"]').type(encryptedContent);
80 | cy.get('[data-test="save"]').click();
81 |
82 | for (let content of [encryptedContent]) {
83 | cy.contains("div.panel-body", content, { timeout: 20000 }).should(
84 | "exist"
85 | );
86 | }
87 |
88 | cy.get("#basic-nav-dropdown").click();
89 | cy.contains("Logout").click();
90 | });
91 |
92 | // it("Alice sign with her old password and get an error", function() {
93 | // cy.visit("/");
94 |
95 | // cy.get('[name="username"]').type(username);
96 | // cy.get('[name="password"]').type(password);
97 | // cy.get('[data-test="login-btn"]').click();
98 |
99 | // cy.contains("div.alert-danger", "Incorrect Password", {
100 | // timeout: 30000
101 | // }).should("exist");
102 | // });
103 |
104 | it("alice sign in and find her note", function() {
105 | cy.visit("/");
106 |
107 | cy.login(username, password);
108 |
109 | cy.contains("div.panel-body", encryptedContent, {
110 | timeout: 20000
111 | }).should("exist");
112 |
113 | // cy.get("#basic-nav-dropdown", { timeout: 10000 }).click();
114 | // cy.contains("Logout").click();
115 | });
116 | });
117 |
118 | describe(`Notes sharing ${seed}`, function() {
119 | it(`alice.${seed} share a new note`, function() {
120 | cy.visit("/");
121 |
122 | cy.login(`alice.${seed}`, password);
123 |
124 | // Test if precedent title exists
125 | cy.contains("div", "New note encrypted", {
126 | timeout: 10000
127 | }).should("exist");
128 |
129 | // In case of lots of notes, I need to wait them to be all decrypted
130 | // cy.get("li.list-group-item", { timeout: 30000 }).each(() => {
131 | // cy.wait(800);
132 | // });
133 |
134 | cy.contains("button", "New Note", {
135 | timeout: 20000
136 | }).click();
137 |
138 | cy.get('[name="title"]').type("New note encrypted and shared");
139 | cy.get('[name="content"]').type(encryptedSharedContent);
140 |
141 | const shareWith = `charlie.${seed}`;
142 |
143 | cy.shareWith(shareWith);
144 | cy.contains("Save").click();
145 |
146 | cy.contains(".modal-footer", "Save", { timeout: 60000 }).should(
147 | "not.exist"
148 | );
149 |
150 | // New note should appear and use .shared css class
151 | cy.contains("div.panel-body", encryptedSharedContent, { timeout: 20000 })
152 | .parentsUntil("li")
153 | .find(".shared");
154 | });
155 |
156 | it(`charlie.${seed} find his notes`, function() {
157 | cy.visit("/");
158 |
159 | cy.login(`charlie.${seed}`, password);
160 |
161 | // Test if precedent title exists
162 | cy.contains("div", "New note encrypted and shared", {
163 | timeout: 10000
164 | }).should("exist");
165 | cy.contains("div.panel-body", encryptedSharedContent, {
166 | timeout: 20000
167 | }).should("exist");
168 | });
169 |
170 | it("alice sign in and extends share with bob", function() {
171 | cy.visit("/");
172 |
173 | cy.login(username, password);
174 |
175 | cy.contains("div.panel-body", encryptedSharedContent, { timeout: 20000 })
176 | .parentsUntil("li")
177 | .find(".shared")
178 | .parent()
179 | .click();
180 |
181 | // Search bob
182 | const shareWith = `bob.${seed}`;
183 |
184 | cy.shareWith(shareWith);
185 |
186 | cy.contains("Save").click();
187 | cy.contains("Save").should("not.exist");
188 |
189 | // Click on shared button
190 | cy.contains("div.panel-body", encryptedSharedContent, { timeout: 20000 })
191 | .parentsUntil("li")
192 | .find(".shared")
193 | .parent()
194 | .click();
195 |
196 | cy.contains("span", "bob", { timeout: 5000 }).should("exist");
197 | cy.contains("Cancel").click();
198 | });
199 |
200 | it(`bob.${seed} find his notes`, function() {
201 | cy.visit("/");
202 |
203 | cy.login(`bob.${seed}`, password);
204 |
205 | cy.contains("div.panel-body", encryptedSharedContent, {
206 | timeout: 10000
207 | }).should("exist");
208 | });
209 | });
210 |
211 | describe(`Sharing with groups ${seed}`, () => {
212 | const group1name = `Group test -- 🎲 ${Math.floor(Math.random() * 100)}`;
213 |
214 | const notes = new Array(2).fill(1).map((x, idx) => ({
215 | title: `Group note #${idx + 1}`,
216 | content: `Note #${idx +
217 | 1} group ${group1name} containing text -- ${formatedDate}`
218 | }));
219 |
220 | it(`alice ${seed} create a group`, () => {
221 | cy.visit("/");
222 | cy.login(username, password);
223 | cy.get('[data-test="new-group"]').click();
224 | cy.get(".modal-body").within(() => {
225 | cy.get("input:first").should("have.attr", "placeholder", "Name");
226 | cy.get("input:first").type(group1name);
227 | cy.shareWith(`bob.${seed}`);
228 | cy.shareWith(`charlie.${seed}`);
229 | });
230 |
231 | cy.get('[data-test="save"]').click();
232 | cy.contains(group1name).click();
233 |
234 | for (let note of notes) {
235 | cy.contains("button", "New Note", {
236 | timeout: 20000
237 | }).click();
238 | cy.get(".modal-body").within(() => {
239 | cy.get("input").should("have.attr", "placeholder", "Title");
240 | cy.get("input").type(note.title);
241 | cy.get("textarea").should("have.attr", "placeholder", "Content");
242 | cy.get("textarea").type(note.content);
243 | });
244 | cy.contains("Save").click();
245 | cy.contains(".panel-body", note.content, { timeout: 30000 }).should(
246 | "exist"
247 | );
248 | }
249 | });
250 |
251 | it(`alice ${seed} access to the group`, () => {
252 | cy.visit("/");
253 | cy.login(username, password);
254 | // Select the group
255 | cy.contains(group1name).click();
256 | // Create notes
257 |
258 | for (let note of notes) {
259 | cy.contains(".panel-body", note.content, { timeout: 30000 }).should(
260 | "exist"
261 | );
262 | }
263 | });
264 |
265 | it(`bob ${seed} access to the group`, () => {
266 | cy.visit("/");
267 | cy.login(`bob.${seed}`, password);
268 | // Select the group
269 | cy.contains(group1name).click();
270 | // See the note
271 | cy.contains(".panel-body", notes[0].content, { timeout: 30000 }).should(
272 | "exist"
273 | );
274 | });
275 |
276 | it(`charlie ${seed} access to the group`, () => {
277 | cy.visit("/");
278 | cy.login(`charlie.${seed}`, password);
279 | // Select the group
280 | cy.contains(group1name).click();
281 | // See the note
282 | cy.contains(".panel-body", notes[0].content, { timeout: 30000 }).should(
283 | "exist"
284 | );
285 | });
286 |
287 | it(`alice ${seed} remove charlie from the group`, () => {
288 | cy.visit("/");
289 | cy.login(username, password);
290 | // Select the group
291 | cy.contains(group1name)
292 | .parent()
293 | .within(() => {
294 | cy.get('[data-test="edit-group"]').click();
295 | });
296 |
297 | cy.contains(`charlie.${seed}`)
298 | .parent()
299 | .within(() => {
300 | cy.get("svg").click();
301 | });
302 |
303 | // cy.shareWith(`alice.${seed}`);
304 | // cy.shareWith(`bob.${seed}`);
305 | cy.contains("Save").click();
306 | cy.contains(".modal-footer", "Save", { timeout: 5000 }).should("not.exist");
307 | // Wait for the request to be executed
308 | cy.wait(2000);
309 |
310 | // Select the group
311 | // Note buggy on purpose for this point. Charlie is removed from the group only on Datapeps
312 | // cy.contains(group1name)
313 | // .parent()
314 | // .within(() => {
315 | // cy.get('[data-test="edit-group"]').click();
316 | // });
317 | // cy.contains(`charlie.${seed}`).should("not.exist");
318 | });
319 |
320 | it(`charlie ${seed} stop seeing the note`, () => {
321 | cy.visit("/");
322 | cy.login(`charlie.${seed}`, password);
323 | // Select the group
324 | cy.contains(group1name).click();
325 | cy.wait(5000);
326 | // Don't see the note
327 | cy.get(".panel-body").should("not.contain", notes[0].content);
328 | });
329 |
330 | it(`Alice ${seed} delete the second group's note`, () => {
331 | cy.visit("/");
332 | cy.login(username, password);
333 | // Select the group
334 | cy.contains(group1name).click();
335 |
336 | cy.contains(".panel-body", notes[1].content, { timeout: 30000 }).should(
337 | "exist"
338 | );
339 | cy.contains(notes[1].content)
340 | .parent(".panel")
341 | .within(() => {
342 | cy.get("button").click();
343 | });
344 | // Note should display in red and hide delete button
345 | cy.contains(notes[1].content)
346 | .parent(".panel")
347 | .within(() => {
348 | cy.get("button").should("not.exist");
349 | });
350 | // Force refresh, note should disapear
351 | cy.get('[data-test="refresh"]').click();
352 | cy.contains(notes[1].content).should("not.exist");
353 | });
354 | it(`charlie ${seed} stop seeing the second note`, () => {
355 | cy.visit("/");
356 | cy.login(`bob.${seed}`, password);
357 | // Select the group
358 | cy.contains(group1name).click();
359 |
360 | cy.wait(5000);
361 |
362 | cy.contains(notes[0].content).should("exist");
363 |
364 | // Don't see the second note
365 | cy.get(".panel-body").should("not.contain", notes[1].content);
366 | });
367 | });
368 |
--------------------------------------------------------------------------------
/client/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | }
18 |
--------------------------------------------------------------------------------
/client/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
27 | Cypress.Commands.add("login", (login, pw, should_success = true) => {
28 | cy.get('[name="username"]')
29 | .clear()
30 | .type(login);
31 | cy.get('[name="password"]')
32 | .clear()
33 | .type(pw);
34 | cy.get('[data-test="login-btn"]').click();
35 |
36 | if (should_success)
37 | cy.contains("button", "New Note", { timeout: 20000 }).should("exist");
38 | else
39 | cy.contains("div.alert", "incorrect Username or Password", {
40 | timeout: 20000
41 | }).should("exist");
42 | });
43 |
44 | Cypress.Commands.add("shareWith", shareWith => {
45 | cy.get("#ShareSelect > div > div:first-child").click();
46 | cy.wait(500);
47 | cy.get("#ShareSelect input").type(shareWith, { force: true });
48 | cy.wait(500);
49 | cy.get("#ShareSelect > div:nth-of-type(2) > div:nth-of-type(1)").should(
50 | "contain",
51 | shareWith
52 | );
53 | cy.get("#ShareSelect > div:nth-of-type(2) > div:nth-of-type(1)").click();
54 | });
55 |
--------------------------------------------------------------------------------
/client/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notes",
3 | "version": "0.0.1",
4 | "private": true,
5 | "devDependencies": {
6 | "@types/node": "^11.13.2",
7 | "cypress": "^3.2.0",
8 | "node-fetch": "^2.3.0",
9 | "@storybook/react": "^5.0.6",
10 | "@storybook/addon-actions": "^5.0.6",
11 | "@storybook/addon-links": "^5.0.6",
12 | "@storybook/addons": "^5.0.6",
13 | "@babel/core": "^7.4.3",
14 | "babel-loader": "^8.0.5"
15 | },
16 | "dependencies": {
17 | "datapeps-sdk": "^1.0.4",
18 | "history": "^4.9.0",
19 | "long": "^4.0.0",
20 | "react": "^16.8.6",
21 | "react-bootstrap": "^0.32.4",
22 | "react-dom": "^16.8.6",
23 | "react-redux": "^7.0.1",
24 | "react-router-bootstrap": "^0.25.0",
25 | "react-router-dom": "^5.0.0",
26 | "react-scripts": "^2.1.8",
27 | "react-select": "^2.4.2",
28 | "redux": "^4.0.1",
29 | "redux-logger": "^3.0.6",
30 | "redux-thunk": "^2.3.0"
31 | },
32 | "scripts": {
33 | "start": "react-scripts start",
34 | "build": "react-scripts build",
35 | "eject": "react-scripts eject",
36 | "test": "cypress run",
37 | "storybook": "start-storybook -p 6006",
38 | "build-storybook": "build-storybook"
39 | },
40 | "browserslist": [
41 | ">0.2%",
42 | "not dead",
43 | "not ie <= 11",
44 | "not op_mini all"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Notes
7 |
8 |
14 |
15 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/client/scripts/setup.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 | ///
3 | if (global["fetch"] === undefined) {
4 | global["fetch"] = require("node-fetch");
5 | }
6 | if (global["Headers"] === undefined) {
7 | global["Headers"] = require("node-fetch").Headers;
8 | }
9 |
10 | const DataPeps = require("datapeps-sdk");
11 | const fs = require("fs");
12 | const crypto = require("crypto"),
13 | shasum = crypto.createHash("sha1");
14 |
15 | let APIHost = "https://api.datapeps.com";
16 |
17 | if (process.env.DATAPEPS_API_HOST) {
18 | process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
19 | APIHost = "https://" + process.env.DATAPEPS_API_HOST;
20 | DataPeps.configure(APIHost);
21 | }
22 |
23 | const login = process.env.DATAPEPS_LOGIN || "cypress.tester";
24 | const password = process.env.DATAPEPS_PASSWORD || "Azertyuiop33";
25 |
26 | const createApp = async () => {
27 | const key = fs.readFileSync(__dirname + "/../../server/public.pem", {
28 | encoding: "utf-8"
29 | });
30 | shasum.update(key);
31 | const seed = shasum.digest("hex").substring(0, 8);
32 |
33 | const loginApp = "Notes." + seed;
34 |
35 | let session = await DataPeps.Session.login(login, password);
36 | let created = false;
37 |
38 | try {
39 | await new DataPeps.IdentityAPI(session).create(
40 | {
41 | login: loginApp,
42 | kind: "pepsswarm/3",
43 | name: `My Private Note (${seed})`,
44 | payload: new TextEncoder().encode(
45 | JSON.stringify({
46 | description: `My Private Note #${seed}`
47 | })
48 | )
49 | },
50 | { sharingGroup: [login] }
51 | );
52 | created = true;
53 | } catch (e) {
54 | if ("kind" in e && e.kind === DataPeps.ServerError.IdentityAlreadyExists) {
55 | // Ok that's fine
56 | } else {
57 | throw e;
58 | }
59 | }
60 |
61 | const dotEnv = `REACT_APP_DATAPEPS_API=${APIHost}\nREACT_APP_API_URL=http://localhost:8080\nREACT_APP_DATAPEPS_APP_ID=${loginApp}`;
62 |
63 | fs.writeFileSync(__dirname + "/../.env.local", dotEnv);
64 | fs.writeFileSync(__dirname + "/../.env.test", dotEnv);
65 |
66 | await new DataPeps.ApplicationAPI(session).putConfig(loginApp, {
67 | jwt: {
68 | key: new TextEncoder().encode(key),
69 | signAlgorithm: DataPeps.ApplicationJWT.Algorithm.RS256,
70 | claimForLogin: "id"
71 | }
72 | });
73 |
74 | return { login: loginApp, success: true, created };
75 | };
76 |
77 | createApp()
78 | .then(app => {
79 | console.log(JSON.stringify(app));
80 | process.exit();
81 | })
82 | .catch(e => {
83 | console.error(e);
84 | process.exit(1);
85 | });
86 |
--------------------------------------------------------------------------------
/client/src/actions/auth.js:
--------------------------------------------------------------------------------
1 | import { authConstants, uiConstants } from "../constants";
2 | import { authService } from "../services";
3 | import { uiActions } from "./index";
4 |
5 | function login(username, password) {
6 | return async dispatch => {
7 | dispatch(request());
8 | try {
9 | dispatch(success(await authService.login(username, password)));
10 | dispatch(uiActions.clear());
11 | } catch (error) {
12 | dispatch(failure(error));
13 | dispatch(uiActions.error(error.message));
14 | }
15 | };
16 |
17 | function request() {
18 | return { type: authConstants.LOGIN_REQUEST };
19 | }
20 | function success({ user, datapeps }) {
21 | return { type: authConstants.LOGIN_SUCCESS, user, datapeps };
22 | }
23 | function failure(error) {
24 | return { type: authConstants.LOGIN_FAILURE, error };
25 | }
26 | }
27 |
28 | function logout() {
29 | return async dispatch => {
30 | await authService.logout();
31 | return dispatch({ type: authConstants.LOGOUT });
32 | };
33 | }
34 |
35 | function changePassword(p1, p2, modalName) {
36 | return async dispatch => {
37 | if (p1 === p2) {
38 | dispatch(request());
39 | try {
40 | await authService.updatePassword(p1);
41 | dispatch(success());
42 | } catch (e) {
43 | dispatch(failure(e));
44 | }
45 | dispatch(uiActions.closeModal(modalName));
46 | }
47 | };
48 |
49 | function request() {
50 | return { type: authConstants.CHANGE_REQUEST };
51 | }
52 | function success() {
53 | return { type: authConstants.CHANGE_SUCCESS };
54 | }
55 | function failure(error) {
56 | return { type: authConstants.CHANGE_FAILURE, error };
57 | }
58 | }
59 |
60 | function subscribe(username, p1, p2) {
61 | return dispatch => {
62 | if (p1 === p2) {
63 | dispatch(request());
64 | dispatch(uiActions.closeModal(uiConstants.UserSubscribeModal));
65 | authService
66 | .subscribe(username, p1)
67 | .then(ok => dispatch(success()), error => dispatch(failure(error)));
68 | }
69 | };
70 | function request() {
71 | return { type: authConstants.SUBSCRIBE_REQUEST };
72 | }
73 | function success() {
74 | return { type: authConstants.SUBSCRIBE_SUCCESS };
75 | }
76 | function failure(error) {
77 | return { type: authConstants.SUBSCRIBE_FAILURE, error };
78 | }
79 | }
80 |
81 | export const authActions = {
82 | login,
83 | logout,
84 | changePassword,
85 | subscribe
86 | };
87 |
--------------------------------------------------------------------------------
/client/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from "./auth";
2 | export * from "./notes";
3 | export * from "./users";
4 | export * from "./ui";
5 |
--------------------------------------------------------------------------------
/client/src/actions/notes.js:
--------------------------------------------------------------------------------
1 | import { notesConstants, uiConstants } from "../constants";
2 | import { notesService } from "../services";
3 | import { uiActions } from "./ui";
4 |
5 | function addNote(Title, Content, sharedIds) {
6 | return (dispatch, getState) => {
7 | let note = {
8 | type: notesConstants.ADD_NOTE,
9 | Title,
10 | Content
11 | };
12 | const group = getState().selectedGroup;
13 | if (group == null) {
14 | dispatch(postNote(note, sharedIds));
15 | } else {
16 | dispatch(postNote(note, sharedIds, group.ID));
17 | }
18 | dispatch(uiActions.closeModal(uiConstants.NewNoteModal));
19 | };
20 | }
21 |
22 | function shareNote(note, sharers) {
23 | return async dispatch => {
24 | try {
25 | await notesService.shareNote(note, sharers);
26 | const newNote = await notesService.getNote(note.ID);
27 | dispatch(success(newNote));
28 | } catch (error) {
29 | dispatch(failure(error));
30 | }
31 | dispatch(uiActions.closeModal(uiConstants.ShareNoteModal));
32 | };
33 | function success(note) {
34 | return { type: notesConstants.SHARE_SUCCESS, note };
35 | }
36 | function failure(error) {
37 | return { type: notesConstants.SHARE_FAILURE, error };
38 | }
39 | }
40 |
41 | function deleteNote(id) {
42 | return async (dispatch, getState) => {
43 | let del = {
44 | type: notesConstants.DELETE_NOTE,
45 | id
46 | };
47 | const group = getState().selectedGroup;
48 | try {
49 | if (group) {
50 | await notesService.deleteGroupNote(id, group.ID);
51 | } else {
52 | await notesService.deleteNote(id);
53 | }
54 | dispatch(del);
55 | } catch (error) {
56 | dispatch(failure(error));
57 | }
58 | };
59 | function failure(error) {
60 | return { type: notesConstants.DEL_FAILURE, error };
61 | }
62 | }
63 |
64 | function postNote(note, users, groupID) {
65 | return async dispatch => {
66 | dispatch(request());
67 |
68 | try {
69 | const response = await notesService.postNote(note, groupID, users);
70 | note.ID = response.noteID;
71 | await notesService.shareNote(note, users);
72 | const newNote = await notesService.getNote(note.ID, groupID);
73 | dispatch(success(newNote));
74 | } catch (error) {
75 | dispatch(failure(error));
76 | }
77 | };
78 | function request() {
79 | return { type: notesConstants.POST_REQUEST };
80 | }
81 | function success(note) {
82 | return { type: notesConstants.POST_SUCCESS, note };
83 | }
84 | function failure(error) {
85 | return { type: notesConstants.POST_FAILURE, error };
86 | }
87 | }
88 |
89 | function getNotes(group) {
90 | return dispatch => {
91 | dispatch(request());
92 | if (group == null) {
93 | getUserNotes(dispatch);
94 | } else {
95 | getGroupNotes(dispatch, group.ID);
96 | }
97 | };
98 |
99 | function request() {
100 | return { type: notesConstants.GETALL_REQUEST };
101 | }
102 | function success(notes) {
103 | return { type: notesConstants.GETALL_SUCCESS, notes };
104 | }
105 | function failure(error) {
106 | return { type: notesConstants.GETALL_FAILURE, error };
107 | }
108 |
109 | async function getUserNotes(dispatch) {
110 | try {
111 | let { notes } = await notesService.getNotes();
112 | dispatch(success({ notes }));
113 | } catch (error) {
114 | dispatch(failure(error));
115 | }
116 | }
117 |
118 | async function getGroupNotes(dispatch, groupID) {
119 | try {
120 | let { notes } = await notesService.getGroupNotes(groupID);
121 | dispatch(success({ notes }));
122 | } catch (error) {
123 | dispatch(failure(error));
124 | }
125 | }
126 | }
127 |
128 | export const noteActions = {
129 | addNote,
130 | shareNote,
131 | deleteNote,
132 | getNotes
133 | };
134 |
--------------------------------------------------------------------------------
/client/src/actions/ui.js:
--------------------------------------------------------------------------------
1 | import { alertConstants, uiConstants } from "../constants";
2 |
3 | export const uiActions = {
4 | success,
5 | error,
6 | clear,
7 | openModal,
8 | closeModal
9 | };
10 |
11 | function success(message) {
12 | return { type: alertConstants.SUCCESS, message };
13 | }
14 |
15 | function error(error) {
16 | let message = error;
17 | if (error && typeof message !== "string" && error.message != null) {
18 | message = error.message;
19 | }
20 | return { type: alertConstants.ERROR, message };
21 | }
22 |
23 | function clear() {
24 | return { type: alertConstants.CLEAR };
25 | }
26 |
27 | function openModal(id, payload) {
28 | return {
29 | type: uiConstants.OPEN_MODAL,
30 | id,
31 | payload
32 | };
33 | }
34 |
35 | function closeModal(id) {
36 | return {
37 | type: uiConstants.CLOSE_MODAL,
38 | id
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/client/src/actions/users.js:
--------------------------------------------------------------------------------
1 | import { usersConstants, uiConstants, alertConstants } from "../constants";
2 | import { usersService } from "../services";
3 | import { uiActions } from "./ui";
4 | import { noteActions } from "./notes";
5 |
6 | function getGroups() {
7 | return async dispatch => {
8 | dispatch(request());
9 |
10 | try {
11 | const response = await usersService.getGroups();
12 | dispatch(success(response.groups));
13 | } catch (error) {
14 | dispatch(failure(error));
15 | }
16 | };
17 | function request() {
18 | return { type: usersConstants.GETGROUPLIST_REQUEST };
19 | }
20 | function success(groups) {
21 | return { type: usersConstants.GETGROUPLIST_SUCCESS, groups };
22 | }
23 | function failure(error) {
24 | return { type: usersConstants.GETGROUPLIST_FAILURE, error };
25 | }
26 | }
27 |
28 | function addGroup(group) {
29 | return dispatch => {
30 | dispatch(postGroup(group));
31 | dispatch(uiActions.closeModal(uiConstants.NewGroupModal));
32 | };
33 | }
34 |
35 | function postGroup(group) {
36 | return async dispatch => {
37 | dispatch(request());
38 |
39 | try {
40 | const response = await usersService.postGroup(group);
41 | dispatch(success({ ...group, ID: response.id }));
42 | } catch (error) {
43 | dispatch(failure(error));
44 | }
45 | };
46 | function request() {
47 | return { type: usersConstants.POSTGROUPLIST_REQUEST };
48 | }
49 | function success(group) {
50 | return { type: usersConstants.POSTGROUPLIST_SUCCESS, group };
51 | }
52 | function failure(error) {
53 | return { type: usersConstants.POSTGROUPLIST_FAILURE, error };
54 | }
55 | }
56 |
57 | function selectGroup(group) {
58 | return async dispatch => {
59 | try {
60 | dispatch(success(group));
61 | dispatch(noteActions.getNotes(group));
62 | } catch (error) {
63 | dispatch(failure(error));
64 | }
65 | };
66 | function success(group) {
67 | return {
68 | type:
69 | group == null
70 | ? usersConstants.SELECT_MY_NOTES
71 | : usersConstants.SELECT_GROUP_NOTE,
72 | group
73 | };
74 | }
75 | function failure(error) {
76 | return { error };
77 | }
78 | }
79 |
80 | function refresh() {
81 | return async (dispatch, getState) => {
82 | try {
83 | const group = getState().selectedGroup;
84 | dispatch(success(group));
85 | dispatch(noteActions.getNotes(group));
86 | dispatch(getGroups());
87 | } catch (error) {
88 | dispatch(failure(error));
89 | }
90 | };
91 | function success(group) {
92 | return {
93 | type:
94 | group == null
95 | ? usersConstants.SELECT_MY_NOTES
96 | : usersConstants.SELECT_GROUP_NOTE,
97 | group
98 | };
99 | }
100 | function failure(error) {
101 | return { type: alertConstants.ERROR, error };
102 | }
103 | }
104 |
105 | export const usersActions = {
106 | getGroups,
107 | addGroup,
108 | selectGroup,
109 | refresh
110 | };
111 |
--------------------------------------------------------------------------------
/client/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Router } from "react-router-dom";
3 | import { connect } from "react-redux";
4 |
5 | import { uiActions } from "../actions";
6 | import { history } from "../history";
7 |
8 | // Components
9 | import PrivateRoute from "./PrivateRoute";
10 | import MainPage from "./MainPage";
11 | import ErrorDisplay from "./ErrorDisplay";
12 |
13 | class App extends React.Component {
14 | constructor(props) {
15 | super(props);
16 |
17 | const { dispatch } = this.props;
18 | history.listen((location, action) => {
19 | // clear alert on location change
20 | dispatch(uiActions.clear());
21 | });
22 | }
23 |
24 | render() {
25 | return (
26 |
27 |
28 |
29 |
30 |
33 |
34 |
38 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | wallix/notes
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 | }
60 |
61 | export default connect()(App);
62 |
--------------------------------------------------------------------------------
/client/src/components/ErrorDisplay.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Modal } from "react-bootstrap";
4 | import { uiActions } from "../actions";
5 |
6 | const ErrorDisplay = ({ alert, dispatch }) => {
7 | return (
8 | dispatch(uiActions.clear())}
11 | className={`alert ${alert.type}`}
12 | >
13 | {alert.message}
14 |
15 | );
16 | };
17 |
18 | function mapStateToProps(state) {
19 | const { alert } = state;
20 | return {
21 | alert
22 | };
23 | }
24 |
25 | export default connect(mapStateToProps)(ErrorDisplay);
26 |
--------------------------------------------------------------------------------
/client/src/components/LoginPage.js:
--------------------------------------------------------------------------------
1 | // Adapted from http://jasonwatmore.com/post/2017/12/07/react-redux-jwt-authentication-tutorial-example
2 |
3 | import React from "react";
4 | import { connect } from "react-redux";
5 | import { Button, Jumbotron, Panel } from "react-bootstrap";
6 |
7 | import { uiConstants } from "../constants";
8 | import { authActions, uiActions } from "../actions";
9 | import UserProfile from "./UserProfile";
10 |
11 | class LoginPage extends React.Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | // reset login status
16 | this.props.dispatch(authActions.logout());
17 |
18 | this.state = {
19 | username: "",
20 | password: "",
21 | submitted: false
22 | };
23 |
24 | this.handleChange = this.handleChange.bind(this);
25 | this.handleSubmit = this.handleSubmit.bind(this);
26 | }
27 |
28 | handleChange(e) {
29 | const { name, value } = e.target;
30 | this.setState({ [name]: value });
31 | }
32 |
33 | handleSubmit(e) {
34 | e.preventDefault();
35 |
36 | this.setState({ submitted: true });
37 | const { username, password } = this.state;
38 | const { dispatch } = this.props;
39 | if (username && password) {
40 | dispatch(authActions.login(username, password));
41 | }
42 | }
43 |
44 | render() {
45 | // const { loggingIn } = this.props;
46 | const { username, password, submitted } = this.state;
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
Notes
54 |
55 | Notes is simple note-taking application, which serves as a demo
56 | for DataPeps . This client is
57 | implemented with React, while the accompanying REST service is
58 | built with Go.
59 |
60 |
61 |
63 | this.props.dispatch(
64 | uiActions.openModal(uiConstants.UserSubscribeModal)
65 | )
66 | }
67 | >
68 | Create an account
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Login
77 |
78 |
79 |
122 |
123 |
124 |
125 |
126 | );
127 | }
128 | }
129 |
130 | function mapStateToProps(state) {
131 | const { loggingIn } = state.auth;
132 | return {
133 | loggingIn
134 | };
135 | }
136 |
137 | export default connect(mapStateToProps)(LoginPage);
138 |
--------------------------------------------------------------------------------
/client/src/components/MainPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 | import { noteActions, usersActions } from "../actions";
5 | import NoteList from "./NoteList";
6 | import Navigation from "./Navbar";
7 | import "./NoteList.css";
8 |
9 | class MainPage extends React.Component {
10 | componentDidMount() {
11 | this.props.dispatch(noteActions.getNotes());
12 | this.props.dispatch(usersActions.getGroups());
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | function mapStateToProps(state) {
26 | return {
27 | user: state.auth.user,
28 | notes: state.notes,
29 | group: state.selectedGroup
30 | };
31 | }
32 |
33 | export default connect(mapStateToProps)(MainPage);
34 |
--------------------------------------------------------------------------------
/client/src/components/Navbar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import {
4 | Button,
5 | Navbar,
6 | Nav,
7 | NavDropdown,
8 | MenuItem,
9 | Glyphicon
10 | } from "react-bootstrap";
11 |
12 | import { parseJWT } from "../utils";
13 | import { uiConstants, authConstants } from "../constants";
14 | import { uiActions, usersActions } from "../actions";
15 | import NewNote from "./NewNote";
16 | import ShareNote from "./ShareNote";
17 | import UserProfile from "./UserProfile";
18 |
19 | class Navigation extends React.Component {
20 | render() {
21 | const { user } = this.props;
22 | // this should not fail...
23 | const username = user ? parseJWT(user.token).id : "User";
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Notes
33 |
34 |
35 |
36 |
37 |
38 | {
40 | this.props.dispatch(
41 | uiActions.openModal(uiConstants.NewNoteModal)
42 | );
43 | }}
44 | >
45 | New Note
46 |
47 | {
51 | this.props.dispatch(usersActions.refresh());
52 | }}
53 | >
54 |
55 |
56 |
57 |
58 |
63 |
66 | this.props.dispatch(
67 | uiActions.openModal(uiConstants.ChangePasswordModal)
68 | )
69 | }
70 | >
71 | User profile
72 |
73 |
74 |
77 | this.props.dispatch({
78 | type: authConstants.LOGOUT
79 | })
80 | }
81 | >
82 | Logout
83 |
84 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 | }
92 |
93 | function mapStateToProps(state) {
94 | const { notes, auth } = state;
95 | const { user } = auth;
96 | return {
97 | user,
98 | notes
99 | };
100 | }
101 |
102 | export default connect(mapStateToProps)(Navigation);
103 |
--------------------------------------------------------------------------------
/client/src/components/NewGroup.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Modal, Button, Form, FormControl } from "react-bootstrap";
4 | import ShareSelect from "./ShareSelect";
5 |
6 | import { uiConstants } from "../constants";
7 | import { uiActions, usersActions } from "../actions";
8 | import { parseJWT } from "../utils";
9 |
10 | class NewGroup extends React.Component {
11 | constructor(props, context) {
12 | super(props, context);
13 |
14 | this.changeName = this.changeName.bind(this);
15 | this.changeSharingGroup = this.changeSharingGroup.bind(this);
16 | this.validate = this.validate.bind(this);
17 | this.onAddGroup = this.onAddGroup.bind(this);
18 |
19 | this.state = {
20 | name: "",
21 | users: []
22 | };
23 | }
24 |
25 | changeName(e) {
26 | this.setState({ name: e.target.value });
27 | }
28 | changeSharingGroup(list) {
29 | this.setState({ users: list });
30 | }
31 | validate() {
32 | return this.state.name !== "";
33 | }
34 |
35 | render() {
36 | const { modals, closeModal } = this.props;
37 | return (
38 |
39 | closeModal(uiConstants.NewGroupModal)}
42 | >
43 |
44 | New Group
45 |
46 |
47 |
56 |
57 |
58 | closeModal(uiConstants.NewGroupModal)}>
59 | Cancel
60 |
61 |
68 | Save
69 |
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | async onAddGroup() {
77 | const user = this.props.user;
78 | const username = user ? parseJWT(user.token).id : "User";
79 | await this.props.addGroup({
80 | name: this.state.name,
81 | users: [username].concat(this.state.users)
82 | });
83 | }
84 | }
85 |
86 | function mapStateToProps(state) {
87 | return {
88 | modals: state.modals.modals,
89 | user: state.auth.user
90 | };
91 | }
92 | const mapDispatchToProps = {
93 | ...uiActions,
94 | ...usersActions
95 | };
96 |
97 | export default connect(
98 | mapStateToProps,
99 | mapDispatchToProps
100 | )(NewGroup);
101 |
--------------------------------------------------------------------------------
/client/src/components/NewNote.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Modal, Button, Form, FormControl } from "react-bootstrap";
4 |
5 | import ShareSelect from "./ShareSelect";
6 |
7 | import { uiConstants } from "../constants";
8 | import { noteActions, uiActions } from "../actions";
9 |
10 | class NewNote extends React.Component {
11 | constructor(props, context) {
12 | super(props, context);
13 |
14 | this.changeTitle = this.changeTitle.bind(this);
15 | this.changeContent = this.changeContent.bind(this);
16 | this.changeProtection = this.changeProtection.bind(this);
17 | this.changeSharingGroup = this.changeSharingGroup.bind(this);
18 | this.validate = this.validate.bind(this);
19 | this.onAddNote = this.onAddNote.bind(this);
20 |
21 | this.state = {
22 | title: "",
23 | content: "",
24 | protected: true,
25 | sharingList: []
26 | };
27 | }
28 |
29 | changeTitle(e) {
30 | this.setState({ title: e.target.value });
31 | }
32 | changeContent(e) {
33 | this.setState({ content: e.target.value });
34 | }
35 | changeProtection(e) {
36 | this.setState({ protected: e.target.checked });
37 | }
38 | changeSharingGroup(list) {
39 | this.setState({ sharingList: list });
40 | }
41 | validate() {
42 | return this.state.title !== "";
43 | }
44 |
45 | render() {
46 | const { modals, closeModal, group } = this.props;
47 | return (
48 |
49 | closeModal(uiConstants.NewNoteModal)}
52 | >
53 |
54 | New Note
55 |
56 |
57 |
75 |
76 |
77 | closeModal(uiConstants.NewNoteModal)}>
78 | Cancel
79 |
80 |
87 | Save
88 |
89 |
90 |
91 |
92 | );
93 | }
94 |
95 | async onAddNote() {
96 | let title = this.state.title;
97 | let content = this.state.content;
98 | this.props.addNote(title, content, this.state.sharingList);
99 | }
100 | }
101 |
102 | const mapStateToProps = state => ({
103 | modals: state.modals.modals,
104 | group: state.selectedGroup
105 | });
106 | const mapDispatchToProps = {
107 | ...uiActions,
108 | ...noteActions
109 | };
110 |
111 | export default connect(
112 | mapStateToProps,
113 | mapDispatchToProps
114 | )(NewNote);
115 |
--------------------------------------------------------------------------------
/client/src/components/Note.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { noteActions, uiActions } from "../actions";
4 | import { NoteLayout } from "./NoteLayout";
5 | import { uiConstants } from "../constants";
6 |
7 | class Note extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | Title: props.Title,
12 | Content: props.Content,
13 | style: "info"
14 | };
15 | }
16 |
17 | render() {
18 | const { DeletedAt, ID, deleteNote, Users, group, Error } = this.props;
19 | const { Title, Content, style } = this.state;
20 |
21 | return (
22 | {
33 | this.props.openModal(uiConstants.ShareNoteModal, {
34 | note: this.props
35 | });
36 | },
37 | group
38 | }}
39 | />
40 | );
41 | }
42 | }
43 |
44 | const mapStateToProps = state => ({
45 | group: state.selectedGroup
46 | });
47 | const mapDispatchToProps = {
48 | ...noteActions,
49 | ...uiActions
50 | };
51 |
52 | export default connect(
53 | mapStateToProps,
54 | mapDispatchToProps
55 | )(Note);
56 |
--------------------------------------------------------------------------------
/client/src/components/NoteLayout.css:
--------------------------------------------------------------------------------
1 | div.note-item .notshared {
2 | color: lightgray;
3 | }
4 | div.note-item .shared {
5 | color: #f0ad4e;
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/components/NoteLayout.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Alert, Panel, Button, ButtonGroup, Glyphicon } from "react-bootstrap";
3 | import "./NoteLayout.css";
4 |
5 | export const NoteLayout = ({
6 | DeletedAt,
7 | ID,
8 | deleteNote,
9 | Title,
10 | Content,
11 | Error,
12 | style,
13 | Users,
14 | openShareModal,
15 | group
16 | }) => (
17 |
18 |
19 | {Title}
20 |
21 |
22 | {Content}
23 | {Error && {Error} }
24 |
25 | {DeletedAt || (
26 |
27 |
28 | {group != null ? null : (
29 |
30 | 1 ? "shared" : "notshared"}
32 | glyph="share"
33 | />
34 |
35 | )}
36 | deleteNote(ID)}>
37 |
38 |
39 |
40 |
41 | )}
42 |
43 | );
44 |
--------------------------------------------------------------------------------
/client/src/components/NoteList.css:
--------------------------------------------------------------------------------
1 | .notes-sidebar .nav-item {
2 | border-radius: 10px;
3 | background-color: #f8f8f8;
4 | border-color: #e7e7e7;
5 | border-width: 1px;
6 | border-style: solid;
7 | margin: 15px 10px;
8 | }
9 |
10 | .notes-sidebar .nav-item.active {
11 | background-color: #d9edf7;
12 | border-color: #bce8f1;
13 | }
14 |
15 | .notes-sidebar .nav-link {
16 | text-align: center;
17 | position: relative;
18 | display: block;
19 | padding: 10px 15px;
20 | width: 100%;
21 | }
22 |
23 | .notes-sidebar .nav {
24 | background-color: #f8f8f8;
25 | border-color: #e7e7e7;
26 | border-width: 1px;
27 | border-style: solid;
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/components/NoteList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Row, Col, Glyphicon } from "react-bootstrap";
4 |
5 | import "./NoteList.css";
6 |
7 | import Note from "./Note";
8 | import { uiActions, usersActions } from "../actions";
9 | import { uiConstants } from "../constants";
10 | import NewGroup from "./NewGroup";
11 | import ShareGroup from "./ShareGroup";
12 |
13 | const NoteList = ({ notes, selectedGroup, groups, dispatch }) => {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
26 |
27 | {
31 | dispatch(usersActions.selectGroup());
32 | }}
33 | >
34 | My Notes
35 |
36 |
37 |
38 |
44 |
45 |
46 | {groups.length} groups
47 |
48 |
49 | {groups.map(group => (
50 |
58 | {
61 | dispatch(usersActions.selectGroup(group));
62 | }}
63 | >
64 | {group.name}
65 | {
68 | dispatch(usersActions.refresh());
69 | }}
70 | >
71 | {
74 | e.stopPropagation();
75 | dispatch(
76 | uiActions.openModal(uiConstants.ShareGroupModal, {
77 | group
78 | })
79 | );
80 | }}
81 | glyph="edit"
82 | />
83 |
84 |
85 |
86 | ))}
87 |
88 | {
91 | dispatch(uiActions.openModal(uiConstants.NewGroupModal));
92 | }}
93 | data-test="new-group"
94 | >
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | {notes.map(note => (
103 |
107 |
108 |
109 | ))}
110 |
111 |
112 |
113 |
114 | );
115 | };
116 |
117 | const mapStateToProps = state => ({
118 | notes: state.notes,
119 | groups: state.groups,
120 | selectedGroup: state.selectedGroup
121 | });
122 |
123 | export default connect(mapStateToProps)(NoteList);
124 |
--------------------------------------------------------------------------------
/client/src/components/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Route } from "react-router-dom";
4 |
5 | import LoginPage from "./LoginPage";
6 |
7 | const PrivateRoute = ({ component: Component, auth, ...rest }) => (
8 |
11 | auth.user != null ? :
12 | }
13 | />
14 | );
15 |
16 | const mapStateToProps = state => ({
17 | auth: state.auth
18 | });
19 |
20 | export default connect(mapStateToProps)(PrivateRoute);
21 |
--------------------------------------------------------------------------------
/client/src/components/ShareGroup.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Modal, Button, Form } from "react-bootstrap";
4 | import ShareSelect from "./ShareSelect";
5 |
6 | import { uiConstants } from "../constants";
7 | import { uiActions } from "../actions";
8 | import { usersService } from "../services";
9 |
10 | class ShareGroup extends React.Component {
11 | constructor(props, context) {
12 | super(props, context);
13 | this.changeSharingGroup = this.changeSharingGroup.bind(this);
14 | this.onShareGroup = this.onShareGroup.bind(this);
15 |
16 | this.state = {
17 | sharingList: []
18 | };
19 | }
20 |
21 | changeSharingGroup(list) {
22 | this.setState({ sharingList: list });
23 | }
24 |
25 | render() {
26 | const { modals, payload, closeModal } = this.props;
27 | if (payload == null) {
28 | return null;
29 | }
30 | const { group } = payload;
31 | if (group == null) {
32 | return null;
33 | }
34 | let currentSharer = [];
35 | if (group.users) {
36 | currentSharer = group.users.map(u => u.username);
37 | }
38 | return (
39 |
40 | closeModal(uiConstants.ShareGroupModal)}
43 | >
44 |
45 | Edit group "{group.name}"
46 |
47 |
48 |
57 |
58 |
59 | closeModal(uiConstants.ShareGroupModal)}>
60 | Cancel
61 |
62 |
69 | Save
70 |
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | async onShareGroup() {
78 | usersService.shareGroup(
79 | this.props.payload.group.ID,
80 | this.state.sharingList
81 | );
82 | this.props.closeModal(uiConstants.ShareGroupModal);
83 | }
84 | }
85 |
86 | const mapStateToProps = state => ({
87 | modals: state.modals.modals,
88 | payload: state.modals.payload
89 | });
90 | const mapDispatchToProps = {
91 | ...uiActions
92 | };
93 |
94 | export default connect(
95 | mapStateToProps,
96 | mapDispatchToProps
97 | )(ShareGroup);
98 |
--------------------------------------------------------------------------------
/client/src/components/ShareNote.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Modal, Button, Form } from "react-bootstrap";
4 | import ShareSelect from "./ShareSelect";
5 |
6 | import { uiConstants } from "../constants";
7 | import { noteActions, uiActions } from "../actions";
8 |
9 | class ShareNote extends React.Component {
10 | constructor(props, context) {
11 | super(props, context);
12 | this.changeSharingGroup = this.changeSharingGroup.bind(this);
13 | this.onShareNote = this.onShareNote.bind(this);
14 |
15 | this.state = {
16 | sharingList: [],
17 | users: []
18 | };
19 | }
20 |
21 | changeSharingGroup(list) {
22 | this.setState({ sharingList: list });
23 | }
24 |
25 | render() {
26 | const { modals, closeModal } = this.props;
27 |
28 | let currentSharer = [];
29 | if (
30 | this.props.payload &&
31 | this.props.payload.note &&
32 | this.props.payload.note.Users
33 | ) {
34 | currentSharer = this.props.payload.note.Users.map(u => u.username);
35 | }
36 |
37 | return (
38 | closeModal(uiConstants.ShareNoteModal)}
41 | >
42 |
43 | Extends share of note with...
44 |
45 |
46 | Shared with:
47 |
48 | {currentSharer.map(login => (
49 |
50 | {login}
51 |
52 | ))}
53 |
54 |
64 |
65 |
66 | closeModal(uiConstants.ShareNoteModal)}>
67 | Cancel
68 |
69 |
76 | Save
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | async onShareNote() {
84 | try {
85 | const {
86 | payload: { note }
87 | } = this.props;
88 | this.props.shareNote(note, this.state.sharingList);
89 | } catch (e) {
90 | console.log(e);
91 | }
92 | }
93 | }
94 |
95 | const mapStateToProps = state => ({
96 | modals: state.modals.modals,
97 | payload: state.modals.payload,
98 | notes: state.notes
99 | });
100 | const mapDispatchToProps = {
101 | ...uiActions,
102 | ...noteActions
103 | };
104 |
105 | export default connect(
106 | mapStateToProps,
107 | mapDispatchToProps
108 | )(ShareNote);
109 |
--------------------------------------------------------------------------------
/client/src/components/ShareSelect.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Select from "react-select";
3 | import { usersService } from "../services";
4 |
5 | class ShareSelect extends React.Component {
6 | constructor(props, context) {
7 | super(props, context);
8 | this.state = {
9 | users: []
10 | };
11 | this.handleInputChange = this.handleInputChange.bind(this);
12 | }
13 |
14 | render() {
15 | return (
16 | this.props.onChange(options.map(o => o.value))}
22 | options={this.state.users.map(user => ({ label: user, value: user }))}
23 | onInputChange={this.handleInputChange}
24 | />
25 | );
26 | }
27 |
28 | handleInputChange(search) {
29 | (async () => {
30 | const response = await usersService.getUsers(search);
31 | this.setState({ users: response.users == null ? [] : response.users });
32 | })();
33 | return search;
34 | }
35 | }
36 |
37 | export default ShareSelect;
38 |
--------------------------------------------------------------------------------
/client/src/components/UserProfile.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Modal, Button, Form, FormControl } from "react-bootstrap";
4 |
5 | import { parseJWT } from "../utils";
6 | import { uiConstants } from "../constants";
7 | import { authActions, uiActions } from "../actions";
8 |
9 | class UserProfile extends React.Component {
10 | constructor(props, context) {
11 | super(props, context);
12 |
13 | this.changeUsername = this.changeUsername.bind(this);
14 | this.changePassword1 = this.changePassword1.bind(this);
15 | this.changePassword2 = this.changePassword2.bind(this);
16 | this.check = this.check.bind(this);
17 |
18 | this.state = {
19 | username:
20 | props.modalName === uiConstants.UserSubscribeModal ? "" : "user",
21 | password1: "",
22 | password2: ""
23 | };
24 | }
25 |
26 | changeUsername(e) {
27 | this.setState({ username: e.target.value });
28 | }
29 | changePassword1(e) {
30 | this.setState({ password1: e.target.value });
31 | }
32 | changePassword2(e) {
33 | this.setState({ password2: e.target.value });
34 | }
35 | check() {
36 | if (this.state.username === "") {
37 | return 1;
38 | }
39 | if (
40 | this.state.password1 !== this.state.password2 ||
41 | this.state.password1.length <= 6
42 | ) {
43 | return 2;
44 | }
45 | return 0;
46 | }
47 |
48 | render() {
49 | const props = this.props;
50 | const userValue = props.user
51 | ? { value: parseJWT(props.user.token).id }
52 | : {};
53 | return (
54 |
55 |
props.closeModal(props.modalName)}
58 | >
59 |
60 |
61 | {props.modalName === uiConstants.ChangePasswordModal
62 | ? "User Profile"
63 | : props.modalName === uiConstants.UserSubscribeModal
64 | ? "Create Account"
65 | : props.modalName === uiConstants.DataPepsUpdate
66 | ? "Update Your Password"
67 | : "?"}
68 |
69 |
70 |
71 |
72 | {props.modalName === uiConstants.DataPepsUpdate ? (
73 |
74 |
75 | Notes now uses{" "}
76 |
77 | end-to-end encryption
78 | {" "}
79 | with DataPeps to protect
80 | your data!
81 |
82 |
83 | Please update your password to automatically use encryption
84 | for your notes.
85 |
86 |
87 | ) : (
88 |
89 |
Username
90 |
105 |
106 | )}
107 |
108 | {props.modalName === uiConstants.UserSubscribeModal
109 | ? "Password"
110 | : "Change Password"}
111 |
112 |
131 |
132 |
133 | props.closeModal(props.modalName)}>
134 | Cancel
135 |
136 | {
140 | props.modalName === uiConstants.UserSubscribeModal
141 | ? props.subscribe(
142 | this.state.username,
143 | this.state.password1,
144 | this.state.password2
145 | )
146 | : props.changePassword(
147 | this.state.password1,
148 | this.state.password2,
149 | props.modalName
150 | );
151 | }}
152 | disabled={this.check() !== 0}
153 | type="submit"
154 | >
155 | {props.modalName === uiConstants.UserSubscribeModal
156 | ? "Create"
157 | : "Update"}
158 |
159 |
160 |
161 |
162 | );
163 | }
164 | }
165 |
166 | const mapStateToProps = state => ({
167 | modals: state.modals.modals
168 | });
169 | const mapDispatchToProps = {
170 | ...uiActions,
171 | ...authActions
172 | };
173 |
174 | export default connect(
175 | mapStateToProps,
176 | mapDispatchToProps
177 | )(UserProfile);
178 |
--------------------------------------------------------------------------------
/client/src/constants/auth.js:
--------------------------------------------------------------------------------
1 | export const authConstants = {
2 | SUBSCRIBE_REQUEST: "SUBSCRIBE_REQUEST",
3 | SUBSCRIBE_SUCCESS: "SUBSCRIBE_SUCCESS",
4 | SUBSCRIBE_FAILURE: "SUBSCRIBE_FAILURE",
5 | LOGIN_REQUEST: "USER_LOGIN_REQUEST",
6 | LOGIN_SUCCESS: "USER_LOGIN_SUCCESS",
7 | LOGIN_FAILURE: "USER_LOGIN_FAILURE",
8 | LOGOUT: "USER_LOGOUT",
9 | CHANGE_REQUEST: "USER_CHANGE_REQUEST",
10 | CHANGE_SUCCESS: "USER_CHANGE_SUCCESS",
11 | CHANGE_FAILURE: "USER_CHANGE_FAILURE"
12 | };
13 |
--------------------------------------------------------------------------------
/client/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export * from "./ui";
2 | export * from "./auth";
3 | export * from "./notes";
4 | export * from "./users";
5 |
--------------------------------------------------------------------------------
/client/src/constants/notes.js:
--------------------------------------------------------------------------------
1 | export const notesConstants = {
2 | ADD_NOTE: "NOTES_ADD_NOTE",
3 | DELETE_NOTE: "NOTES_DELETE_NOTE",
4 | GETALL_REQUEST: "NOTES_GETALL_REQUEST",
5 | GETALL_SUCCESS: "NOTES_GETALL_SUCCESS",
6 | GETALL_FAILURE: "NOTES_GETALL_FAILURE",
7 | POST_REQUEST: "NOTES_POST_REQUEST",
8 | POST_SUCCESS: "NOTES_POST_SUCCESS",
9 | POST_FAILURE: "NOTES_POST_FAILURE",
10 | SHARE_SUCCESS: "NOTES_SHARE_SUCCESS",
11 | SHARE_FAILURE: "NOTES_SHARE_FAILURE",
12 | DEL_FAILURE: "NOTES_DEL_FAILURE",
13 | GET_USERS_LIST: "NOTES_GET_USERS"
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/constants/ui.js:
--------------------------------------------------------------------------------
1 | export const alertConstants = {
2 | SUCCESS: "ALERT_SUCCESS",
3 | ERROR: "ALERT_ERROR",
4 | CLEAR: "ALERT_CLEAR"
5 | };
6 |
7 | export const uiConstants = {
8 | OPEN_MODAL: "OPEN_MODAL",
9 | CLOSE_MODAL: "CLOSE_MODAL",
10 | NewNoteModal: "NEW_NOTE",
11 | NewGroupModal: "NEW_GROUP",
12 | ShareNoteModal: "SHARE_NOTE",
13 | ChangePasswordModal: "CHANGE_PASSWORD",
14 | UserSubscribeModal: "USER_SUBSCRIBE",
15 | DataPepsUpdate: "DATAPEPS_UPDATE"
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/constants/users.js:
--------------------------------------------------------------------------------
1 | export const usersConstants = {
2 | GETGROUPLIST_REQUEST: "NOTES_GETGROUPLIST_REQUEST",
3 | GETGROUPLIST_SUCCESS: "NOTES_GETGROUPLIST_SUCCESS",
4 | GETGROUPLIST_FAILURE: "NOTES_GETGROUPLIST_FAILURE",
5 | POSTGROUPLIST_REQUEST: "NOTES_POSTGROUPLIST_REQUEST",
6 | POSTGROUPLIST_SUCCESS: "NOTES_POSTGROUPLIST_SUCCESS",
7 | POSTGROUPLIST_FAILURE: "NOTES_POSTGROUPLIST_FAILURE",
8 | SELECT_GROUP_NOTE: "SELECT_GROUP_NOTE",
9 | SELECT_MY_NOTES: "SELECT_MY_NOTES"
10 | };
11 |
--------------------------------------------------------------------------------
/client/src/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from "history";
2 |
3 | export const history = createBrowserHistory();
4 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "react-dom";
3 |
4 | // redux + thunk
5 | import { Provider } from "react-redux";
6 | import store from "./store";
7 |
8 | // main application
9 | import App from "./components/App";
10 |
11 | import * as DataPeps from "datapeps-sdk";
12 |
13 | if (process.env.REACT_APP_DATAPEPS_API != null) {
14 | DataPeps.configure(process.env.REACT_APP_DATAPEPS_API);
15 | }
16 |
17 | render(
18 |
19 |
20 | ,
21 | document.getElementById("root")
22 | );
23 |
--------------------------------------------------------------------------------
/client/src/reducers/alert.js:
--------------------------------------------------------------------------------
1 | import { alertConstants } from "../constants";
2 |
3 | export function alert(state = {}, action) {
4 | switch (action.type) {
5 | case alertConstants.SUCCESS:
6 | return {
7 | type: "alert-success",
8 | message: action.message
9 | };
10 | case alertConstants.ERROR:
11 | return {
12 | type: "alert-danger",
13 | message: action.message
14 | };
15 | case alertConstants.CLEAR:
16 | return {};
17 | default:
18 | return state;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { authConstants } from "../constants";
2 |
3 | export function auth(state = {}, action) {
4 | switch (action.type) {
5 | case authConstants.LOGIN_REQUEST:
6 | return {
7 | loggingIn: true
8 | };
9 | case authConstants.LOGIN_SUCCESS:
10 | return {
11 | loggedIn: true,
12 | user: action.user,
13 | datapeps: action.datapeps
14 | };
15 | case authConstants.LOGIN_FAILURE:
16 | return {};
17 | case authConstants.LOGOUT:
18 | return {};
19 | default:
20 | return state;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 |
3 | import { auth } from "./auth";
4 | import { alert } from "./alert";
5 | import { modals } from "./modals";
6 | import { notes } from "./notes";
7 | import { groups, selectedGroup } from "./users";
8 |
9 | export default combineReducers({
10 | auth,
11 | alert,
12 | modals,
13 | notes,
14 | groups,
15 | selectedGroup
16 | });
17 |
--------------------------------------------------------------------------------
/client/src/reducers/modals.js:
--------------------------------------------------------------------------------
1 | import { uiConstants } from "../constants";
2 | const initialState = {
3 | modals: [],
4 | payload: {}
5 | };
6 |
7 | export function modals(state = initialState, action) {
8 | switch (action.type) {
9 | case uiConstants.OPEN_MODAL:
10 | return {
11 | ...state,
12 | modals: state.modals.concat(action.id),
13 | payload: action.payload
14 | };
15 | case uiConstants.CLOSE_MODAL:
16 | return {
17 | ...state,
18 | modals: state.modals.filter(item => item !== action.id),
19 | payload: null
20 | };
21 | default:
22 | return state;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/reducers/notes.js:
--------------------------------------------------------------------------------
1 | import { notesConstants, authConstants } from "../constants";
2 |
3 | export const notes = (state = [], action) => {
4 | switch (action.type) {
5 | case notesConstants.POST_SUCCESS:
6 | return [...state, action.note];
7 | case notesConstants.SHARE_SUCCESS:
8 | return state.map(n => (n.ID === action.note.ID ? action.note : n));
9 | case notesConstants.DELETE_NOTE:
10 | return state.map(note =>
11 | note.ID === action.id
12 | ? { ...note, DeletedAt: note.DeletedAt ? null : true }
13 | : note
14 | );
15 | case notesConstants.GETALL_SUCCESS:
16 | return [...action.notes.notes];
17 | case authConstants.LOGOUT:
18 | return [];
19 | default:
20 | return state;
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/reducers/users.js:
--------------------------------------------------------------------------------
1 | import { usersConstants } from "../constants";
2 |
3 | export const groups = (state = [], action) => {
4 | switch (action.type) {
5 | case usersConstants.GETGROUPLIST_SUCCESS:
6 | return [...action.groups];
7 | case usersConstants.POSTGROUPLIST_SUCCESS:
8 | return [action.group].concat(state);
9 | default:
10 | return state;
11 | }
12 | };
13 |
14 | export const selectedGroup = (state = null, action) => {
15 | switch (action.type) {
16 | case usersConstants.SELECT_GROUP_NOTE:
17 | return action.group;
18 | case usersConstants.SELECT_MY_NOTES:
19 | return null;
20 | default:
21 | return state;
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/client/src/services/auth.js:
--------------------------------------------------------------------------------
1 | import { ApplicationJWT, IdentityAPI, SDKError } from "datapeps-sdk";
2 | import { handleResponse } from "./utils";
3 | import store from "../store";
4 |
5 | export async function login(username, password) {
6 | try {
7 | const connector = {
8 | createSession: async (login, password) =>
9 | await loginNotes(login, password),
10 | getToken: async user => user.token
11 | };
12 | const {
13 | session: datapeps,
14 | appSession: user
15 | } = await ApplicationJWT.createSession(
16 | process.env.REACT_APP_DATAPEPS_APP_ID,
17 | username,
18 | password,
19 | connector
20 | );
21 | return { user, datapeps };
22 | } catch (error) {
23 | if (error.kind) {
24 | switch (error.kind) {
25 | case SDKError.IdentityInvalidKeySet:
26 | throw new Error("Bad Password");
27 | default:
28 | }
29 | }
30 | throw error;
31 | }
32 | }
33 |
34 | async function loginNotes(username, password) {
35 | const requestOptions = {
36 | method: "POST",
37 | headers: { "Content-Type": "application/json" },
38 | body: JSON.stringify({ username, password })
39 | };
40 |
41 | const response = await fetch(
42 | `${process.env.REACT_APP_API_URL}/login`,
43 | requestOptions
44 | );
45 | const user = await handleResponse(response);
46 | return user;
47 | }
48 |
49 | export async function logout() {
50 | store.getState().auth.datapeps &&
51 | (await store.getState().auth.datapeps.logout());
52 | }
53 |
54 | export async function subscribe(username, password) {
55 | const requestOptions = {
56 | method: "POST",
57 | headers: { "Content-Type": "application/json" },
58 | body: JSON.stringify({ username, password })
59 | };
60 | const response = await fetch(
61 | `${process.env.REACT_APP_API_URL}/subscribe`,
62 | requestOptions
63 | );
64 | return handleResponse(response);
65 | }
66 |
67 | export async function updatePassword(password) {
68 | const datapeps = store.getState().auth.datapeps;
69 | await new IdentityAPI(datapeps).renewKeys(datapeps.login, password);
70 | }
71 |
72 | // Since datapeps is integrated we change the DataPeps password instead of the application password
73 | // export async function updatePassword(password) {
74 | // const token = localStorage.getItem("user");
75 | // const username = parseJWT(token).id;
76 | // const requestOptions = {
77 | // method: "POST",
78 | // headers: authHeader(true),
79 | // body: JSON.stringify({ username: username, password: password })
80 | // };
81 | // const response = await fetch(
82 | // `${process.env.REACT_APP_API_URL}/auth/update/password`,
83 | // requestOptions
84 | // );
85 | // return handleResponse(response);
86 | // }
87 |
--------------------------------------------------------------------------------
/client/src/services/index.js:
--------------------------------------------------------------------------------
1 | import * as authService from "./auth";
2 | import * as notesService from "./notes";
3 | import * as usersService from "./users";
4 | export { authService, notesService, usersService };
5 |
--------------------------------------------------------------------------------
/client/src/services/notes.js:
--------------------------------------------------------------------------------
1 | import { ResourceAPI, ID, getLogin } from "datapeps-sdk";
2 |
3 | import { handleResponse, authHeader, groupLogin } from "./utils";
4 | import store from "../store";
5 |
6 | export async function postNote(note, groupID, users) {
7 | note = await encryptNote(note, groupID, users);
8 | const requestOptions = {
9 | method: "POST",
10 | headers: authHeader(true),
11 | body: JSON.stringify(note)
12 | };
13 |
14 | const response = await fetch(
15 | `${process.env.REACT_APP_API_URL}/auth/${
16 | groupID == null ? "notes" : `group/${groupID}/notes`
17 | }`,
18 | requestOptions
19 | );
20 | return handleResponse(response);
21 | }
22 |
23 | export async function getNote(id, groupID) {
24 | const requestOptions = {
25 | method: "GET",
26 | headers: authHeader(false)
27 | };
28 |
29 | const response = await fetch(
30 | `${process.env.REACT_APP_API_URL}/auth${
31 | groupID !== undefined ? `/group/${groupID}` : ""
32 | }/notes/${id}`,
33 | requestOptions
34 | );
35 | const { note } = await handleResponse(response);
36 | return await decryptNote(note);
37 | }
38 |
39 | export async function getNotes() {
40 | const requestOptions = {
41 | method: "GET",
42 | headers: authHeader(false)
43 | };
44 |
45 | const response = await fetch(
46 | `${process.env.REACT_APP_API_URL}/auth/notes`,
47 | requestOptions
48 | );
49 | return handleNotesResponse(response);
50 | }
51 |
52 | export async function getUsers(note) {
53 | const {
54 | auth: { datapeps },
55 | selectedGroup: group
56 | } = store.getState();
57 | const rApi = new ResourceAPI(datapeps);
58 | const options = group == null ? null : { assume: groupLogin(group.ID) };
59 | const sharing = await rApi.getSharingGroup(note.resourceID, options);
60 | return sharing.map(s => s.identityID.login).filter(l => l !== datapeps.login);
61 | }
62 |
63 | export async function getGroupNotes(groupID) {
64 | const requestOptions = {
65 | method: "GET",
66 | headers: authHeader(false)
67 | };
68 |
69 | const response = await fetch(
70 | `${process.env.REACT_APP_API_URL}/auth/group/${groupID}/notes`,
71 | requestOptions
72 | );
73 | return handleNotesResponse(response);
74 | }
75 |
76 | export async function deleteNote(id) {
77 | const requestOptions = {
78 | method: "DELETE",
79 | headers: authHeader(false)
80 | };
81 |
82 | const response = await fetch(
83 | `${process.env.REACT_APP_API_URL}/auth/notes/${id}`,
84 | requestOptions
85 | );
86 | return handleResponse(response);
87 | }
88 |
89 | export async function deleteGroupNote(id, groupId) {
90 | const requestOptions = {
91 | method: "DELETE",
92 | headers: authHeader(false)
93 | };
94 |
95 | const response = await fetch(
96 | `${process.env.REACT_APP_API_URL}/auth/group/${groupId}/notes/${id}`,
97 | requestOptions
98 | );
99 | return handleResponse(response);
100 | }
101 |
102 | export async function shareNote(note, sharingList) {
103 | if (sharingList == null || sharingList.length === 0) {
104 | return;
105 | }
106 | let datapeps = store.getState().auth.datapeps;
107 | await new ResourceAPI(datapeps).extendSharingGroup(
108 | note.resourceID,
109 | sharingList.map(u => getLogin(u, process.env.REACT_APP_DATAPEPS_APP_ID))
110 | );
111 | await Promise.all(
112 | sharingList.map(async u => {
113 | const requestOptions = {
114 | method: "POST",
115 | headers: authHeader(false)
116 | };
117 |
118 | return await fetch(
119 | `${process.env.REACT_APP_API_URL}/auth/share/${note.ID}/${u}`,
120 | requestOptions
121 | );
122 | })
123 | );
124 | }
125 |
126 | async function handleNotesResponse(response) {
127 | const { notes } = await handleResponse(response);
128 | return { notes: await Promise.all(notes.map(decryptNote)) };
129 | }
130 |
131 | async function encryptNote(note, groupID, users) {
132 | let datapeps = store.getState().auth.datapeps;
133 | let sharingGroup = groupID == null ? [datapeps.login] : [groupLogin(groupID)];
134 | if (users != null) {
135 | sharingGroup = sharingGroup.concat(
136 | users.map(u => getLogin(u, process.env.REACT_APP_DATAPEPS_APP_ID))
137 | );
138 | }
139 | const resource = await new ResourceAPI(datapeps).create(
140 | "note",
141 | {
142 | description: note.title,
143 | URI: `${process.env.REACT_APP_API_URL}/auth/notes`,
144 | MIMEType: "text/plain"
145 | },
146 | sharingGroup
147 | );
148 | note.resourceID = resource.id;
149 | return {
150 | ...note,
151 | Title: ID.clip(resource.id, resource.encrypt(note.Title)),
152 | Content: resource.encrypt(note.Content)
153 | };
154 | }
155 |
156 | async function decryptNote(note) {
157 | try {
158 | const {
159 | auth: { datapeps },
160 | selectedGroup: group
161 | } = store.getState();
162 | const { id, data: encryptedTitle } = ID.unclip(note.Title);
163 | const api = new ResourceAPI(datapeps);
164 | const options = group == null ? null : { assume: groupLogin(group.ID) };
165 | const resource = await api.get(id, options);
166 | const Title = resource.decrypt(encryptedTitle);
167 | const Content = resource.decrypt(note.Content);
168 | return { ...note, Title, Content, resourceID: id };
169 | } catch (e) {
170 | return { ...note, Error: e.message };
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/client/src/services/users.js:
--------------------------------------------------------------------------------
1 | import { IdentityAPI, getLogin } from "datapeps-sdk";
2 | import { authHeader, handleResponse, groupLogin } from "./utils";
3 | import store from "../store";
4 |
5 | export async function getGroups() {
6 | const requestOptions = {
7 | method: "GET",
8 | headers: authHeader(false)
9 | };
10 |
11 | const response = await fetch(
12 | `${process.env.REACT_APP_API_URL}/auth/groups`,
13 | requestOptions
14 | );
15 | return handleResponse(response);
16 | }
17 |
18 | export async function shareGroup(groupID, sharingList) {
19 | const datapeps = store.getState().auth.datapeps;
20 | await new IdentityAPI(datapeps).replaceSharingGroup(
21 | groupLogin(groupID),
22 | sharingList.map(u => getLogin(u, process.env.REACT_APP_DATAPEPS_APP_ID))
23 | );
24 | }
25 |
26 | export async function postGroup(group) {
27 | const requestOptions = {
28 | method: "POST",
29 | headers: authHeader(true),
30 | body: JSON.stringify(group)
31 | };
32 |
33 | const response = await handleResponse(
34 | await fetch(`${process.env.REACT_APP_API_URL}/auth/group`, requestOptions)
35 | );
36 | let api = new IdentityAPI(store.getState().auth.datapeps);
37 | await api.create(
38 | {
39 | kind: "group",
40 | login: groupLogin(response.id),
41 | name: `Demo notes group: ${group.name}`
42 | },
43 | {
44 | sharingGroup: group.users.map(u =>
45 | getLogin(u, process.env.REACT_APP_DATAPEPS_APP_ID)
46 | )
47 | }
48 | );
49 | return response;
50 | }
51 |
52 | export async function getUsers(search = "") {
53 | const requestOptions = {
54 | method: "GET",
55 | headers: authHeader(false)
56 | };
57 |
58 | let query = "";
59 | if (search !== "") {
60 | query = `?search=${search}`;
61 | }
62 |
63 | const response = await fetch(
64 | `${process.env.REACT_APP_API_URL}/auth/users${query}`,
65 | requestOptions
66 | );
67 | return handleResponse(response);
68 | }
69 |
--------------------------------------------------------------------------------
/client/src/services/utils.js:
--------------------------------------------------------------------------------
1 | import store from "../store";
2 | import { getLogin } from "datapeps-sdk";
3 |
4 | export async function handleResponse(response) {
5 | const text = await response.text();
6 | const data = text && JSON.parse(text);
7 | if (!response.ok) {
8 | const error = (data && data.message) || response.statusText;
9 | throw new Error(error);
10 | }
11 |
12 | return data;
13 | }
14 |
15 | // return authorization header with JWT token
16 | export function authHeader(isJSON) {
17 | let res = isJSON ? { "Content-Type": "application/json" } : {};
18 | let user = store.getState().auth.user;
19 | if (user && user.token) {
20 | return { ...res, Authorization: "Bearer " + user.token };
21 | } else {
22 | return res;
23 | }
24 | }
25 |
26 | export function groupLogin(groupID) {
27 | return getLogin(
28 | `group-${groupID}-${process.env.REACT_APP_GROUP_SEED}`,
29 | process.env.REACT_APP_DATAPEPS_APP_ID
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from "redux";
2 | import thunkMiddleware from "redux-thunk";
3 | import { createLogger } from "redux-logger";
4 | import rootReducer from "./reducers";
5 |
6 | const loggerMiddleware = createLogger();
7 |
8 | export default createStore(
9 | rootReducer,
10 | applyMiddleware(thunkMiddleware, loggerMiddleware)
11 | );
12 |
--------------------------------------------------------------------------------
/client/src/utils.js:
--------------------------------------------------------------------------------
1 | // extract data from JWT token
2 | export function parseJWT(token) {
3 | var base64Url = token.split(".")[1];
4 | var base64 = base64Url.replace("-", "+").replace("_", "/");
5 | return JSON.parse(window.atob(base64));
6 | }
7 |
--------------------------------------------------------------------------------
/client/stories/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { storiesOf } from "@storybook/react";
3 | import { NoteLayout } from "../src/components/NoteLayout";
4 | import { Provider } from "react-redux";
5 | import { createStore, applyMiddleware } from "redux";
6 | import thunkMiddleware from "redux-thunk";
7 | import rootReducer from "../src/reducers";
8 |
9 | const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));
10 |
11 | storiesOf("Note", module)
12 | .addDecorator(story => {story()} )
13 | .add("with text", () => (
14 |
15 |
16 |
20 | {}
27 | }}
28 | />
29 |
30 |
34 | {}
42 | }}
43 | />
44 |
45 |
46 |
47 | ));
48 |
--------------------------------------------------------------------------------
/k8s/client.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: notes-client
5 | spec:
6 | selector:
7 | matchLabels:
8 | app: notes-client
9 | template:
10 | metadata:
11 | labels:
12 | app: notes-client
13 | spec:
14 | containers:
15 | - name: notes-client
16 | image: gcr.io/peps-146814/notes-client:latest
17 | imagePullPolicy: Always
18 | ports:
19 | - containerPort: 5000
20 | ---
21 | apiVersion: v1
22 | kind: Service
23 | metadata:
24 | name: notes-client
25 | spec:
26 | type: NodePort
27 | selector:
28 | app: notes-client
29 | ports:
30 | - port: 80
31 | targetPort: 5000
32 |
--------------------------------------------------------------------------------
/k8s/ingress-controller-preprod.yaml:
--------------------------------------------------------------------------------
1 | kind: Service
2 | apiVersion: v1
3 | metadata:
4 | name: ingress-nginx
5 | namespace: ingress-nginx
6 | labels:
7 | app.kubernetes.io/name: ingress-nginx
8 | app.kubernetes.io/part-of: ingress-nginx
9 | spec:
10 | externalTrafficPolicy: Local
11 | type: LoadBalancer
12 | loadBalancerIP: 35.190.199.77
13 | selector:
14 | app.kubernetes.io/name: ingress-nginx
15 | app.kubernetes.io/part-of: ingress-nginx
16 | ports:
17 | - name: http
18 | port: 80
19 | targetPort: http
20 | - name: https
21 | port: 443
22 | targetPort: https
23 |
24 | ---
25 |
26 |
--------------------------------------------------------------------------------
/k8s/ingress.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: extensions/v1beta1
2 | kind: Ingress
3 | metadata:
4 | name: note
5 | annotations:
6 | kubernetes.io/ingress.class: "nginx"
7 | certmanager.k8s.io/issuer: "letsencrypt-prod"
8 | certmanager.k8s.io/acme-challenge-type: http01
9 | spec:
10 | tls:
11 | - hosts:
12 | - demo-notes.datapeps.com
13 | secretName: demo-notes.datapeps.com-tls
14 | - hosts:
15 | - demo-notes-api.datapeps.com
16 | secretName: demo-notes-api.datapeps.com-tls
17 | rules:
18 | - host: demo-notes.datapeps.com
19 | http:
20 | paths:
21 | - path: /
22 | backend:
23 | serviceName: notes-client
24 | servicePort: 80
25 | - host: demo-notes-api.datapeps.com
26 | http:
27 | paths:
28 | - path: /
29 | backend:
30 | serviceName: notes-server
31 | servicePort: 80
32 |
--------------------------------------------------------------------------------
/k8s/production-issuer.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: certmanager.k8s.io/v1alpha1
2 | kind: Issuer
3 | metadata:
4 | name: letsencrypt-prod
5 | spec:
6 | acme:
7 | # The ACME server URL
8 | server: https://acme-v02.api.letsencrypt.org/directory
9 | # Email address used for ACME registration
10 | email: qbo@wallix.com
11 | # Name of a secret used to store the ACME account private key
12 | privateKeySecretRef:
13 | name: letsencrypt-prod
14 | # Enable the HTTP-01 challenge provider
15 | http01: {}
16 |
--------------------------------------------------------------------------------
/k8s/server.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: notes-server
5 | spec:
6 | selector:
7 | matchLabels:
8 | app: notes-server
9 | template:
10 | metadata:
11 | labels:
12 | app: notes-server
13 | spec:
14 | containers:
15 | - name: notes-server
16 | image: gcr.io/peps-146814/notes-server:latest
17 | imagePullPolicy: Always
18 | ports:
19 | - containerPort: 8080
20 | volumeMounts:
21 | - name: notes-server-db
22 | mountPath: /notes-db
23 | volumes:
24 | - name: notes-server-db
25 | persistentVolumeClaim:
26 | claimName: notes-server-db
27 | ---
28 | apiVersion: v1
29 | kind: Service
30 | metadata:
31 | name: notes-server
32 | spec:
33 | type: NodePort
34 | selector:
35 | app: notes-server
36 | ports:
37 | - port: 80
38 | targetPort: 8080
39 | ---
40 | apiVersion: v1
41 | kind: PersistentVolume
42 | metadata:
43 | name: notes-server-db
44 | spec:
45 | storageClassName: ""
46 | capacity:
47 | storage: 100G
48 | accessModes:
49 | - ReadWriteOnce
50 | gcePersistentDisk:
51 | pdName: notes-server-db
52 | ---
53 | kind: PersistentVolumeClaim
54 | apiVersion: v1
55 | metadata:
56 | name: notes-server-db
57 | spec:
58 | storageClassName: ""
59 | accessModes:
60 | - ReadWriteOnce
61 | resources:
62 | requests:
63 | storage: 100G
64 |
--------------------------------------------------------------------------------
/k8s/staging-issuer.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: certmanager.k8s.io/v1alpha1
2 | kind: Issuer
3 | metadata:
4 | name: letsencrypt-staging
5 | spec:
6 | acme:
7 | # The ACME server URL
8 | server: https://acme-staging-v02.api.letsencrypt.org/directory
9 | # Email address used for ACME registration
10 | email: datapeps@wallix.com
11 | # Name of a secret used to store the ACME account private key
12 | privateKeySecretRef:
13 | name: letsencrypt-staging
14 | # Enable the HTTP-01 challenge provider
15 | http01: {}
16 |
--------------------------------------------------------------------------------
/scripts/build-images.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash -e
2 |
3 | export PREFIXTAG="notes-"
4 | export BUILDTAG=${BUILDTAG:=dev}
5 |
6 | if [ -z "$SERVICES" ]; then
7 | SERVICES=`find . -maxdepth 2 -name Dockerfile | sed 's#./\(.*\)/Dockerfile#\1#'`
8 | fi;
9 |
10 | for service in $SERVICES; do
11 | DOCKERTAG=${PREFIXTAG}$service:$BUILDTAG
12 | cd $service
13 | echo "### Building service $service with tag $DOCKERTAG"
14 | docker build -t $DOCKERTAG .
15 | cd -
16 | done
--------------------------------------------------------------------------------
/scripts/push-images.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash -e
2 |
3 | export PREFIXTAG="notes-"
4 |
5 | if [ -z "${BUILDTAG}" ]; then
6 | echo "Cannot push images: Missing BUILDTAG in environment"
7 | exit 1
8 | fi
9 |
10 | if [ -z "$SERVICES" ]; then
11 | SERVICES=`find . -maxdepth 2 -name Dockerfile | sed 's#./\(.*\)/Dockerfile#\1#'`
12 | fi;
13 |
14 | for service in $SERVICES; do
15 | DOCKERTAG=${PREFIXTAG}$service:$BUILDTAG
16 | REMOTETAG=gcr.io/peps-146814/$DOCKERTAG
17 | echo "### Pushing $DOCKERTAG to $REMOTETAG"
18 | docker tag $DOCKERTAG $REMOTETAG
19 | docker push $REMOTETAG
20 | done
21 |
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 | server
2 | *.db
3 | Dockerfile
4 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | server
2 | *.db
3 | *.pem
4 |
--------------------------------------------------------------------------------
/server/.realize.yaml:
--------------------------------------------------------------------------------
1 | settings:
2 | legacy:
3 | force: false
4 | interval: 0s
5 | schema:
6 | - name: server
7 | path: .
8 | commands:
9 | test:
10 | status: true
11 | method: go test ./...
12 | install:
13 | status: true
14 | method: go build .
15 | run:
16 | status: true
17 | method: ./server
18 | watcher:
19 | extensions:
20 | - go
21 | paths:
22 | - /
23 | ignored_paths:
24 | - .git
25 | - .realize
26 | - vendor
27 |
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.12-alpine
2 |
3 | ENV GO111MODULE on
4 |
5 | RUN apk add --no-cache git build-base
6 |
7 | WORKDIR /go/src/github.com/wallix/notes/server
8 | COPY go.mod .
9 | COPY go.sum .
10 |
11 | RUN go mod download
12 |
13 | # Copy the sources
14 | COPY . /go/src/github.com/wallix/notes/server
15 |
16 | RUN go test
17 | RUN go build .
18 |
19 | FROM alpine
20 | COPY --from=0 /go/src/github.com/wallix/notes/server/server .
21 | COPY --from=0 /go/src/github.com/wallix/notes/server/*.pem /
22 |
23 | CMD [ "./server" ]
24 |
--------------------------------------------------------------------------------
/server/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 WALLIX
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | This is the server for the Notes application, written in Go.
2 |
3 | # Quick start
4 |
5 | Download go modules dependencies:
6 |
7 | ```sh
8 | go mod download
9 | ```
10 |
11 | Building the server:
12 |
13 | ```sh
14 | go build
15 | ```
16 |
17 | Create RSA keypair:
18 |
19 | ```sh
20 | openssl genrsa -out key.pem 2048
21 | openssl rsa -in key.pem -outform PEM -pubout -out public.pem
22 | ```
23 |
24 | Running the server:
25 |
26 | ```sh
27 | ./server
28 | ```
29 |
30 | # Usage
31 |
32 | Create a new user:
33 |
34 | ```sh
35 | http -v --json POST localhost:8080/subscribe username=admin password=admin
36 | ```
37 |
38 | Login:
39 |
40 | ```sh
41 | http -v --json POST localhost:8080/login username=admin password=admin
42 | ```
43 |
44 | Create a note:
45 |
46 | ```sh
47 | http -f POST localhost:8000/auth/notes "Authorization:Bearer xxxxxxxxx" "Content-Type: application/json" title="Cool Title" content="And an awesome content."
48 | ```
49 |
50 | Get all notes:
51 |
52 | ```sh
53 | http -f GET localhost:8000/auth/notes "Authorization:Bearer xxxxxxxxx" "Content-Type: application/json"
54 | ```
55 |
56 | where xxxxxxxxx is the full token.
57 |
58 | # About
59 |
60 | Licensed under the Apache license.
61 |
62 | (c) WALLIX, written by Henri Binsztok.
63 |
--------------------------------------------------------------------------------
/server/TODO.md:
--------------------------------------------------------------------------------
1 | % Roadmap
2 |
3 | # MVP
4 |
5 | - Users in database DONE
6 | - Create account (subscribe) DONE
7 | - Notes in database WIP
8 |
9 | # Bugs
10 |
11 | - multiple requests to /login
12 |
13 | # Source code
14 |
15 | - More tests: Login WIP, password update, create note and get note, get notes, ... Unlawful login, forbidden queries, etc.
16 | - Use primary key login
17 | - Split in multiple files
18 | - Better HTTP error codes https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
19 |
20 | # Security
21 |
22 | - CSRF protection with https://github.com/justinas/nosurf
23 |
24 | # Later, maybe
25 |
26 | - Long note update by diff (in crypto world, will be FHE demo ;)
27 | - Test if we can add something like qor (initial test fails)
28 |
--------------------------------------------------------------------------------
/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/wallix/notes/server
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/appleboy/gin-jwt/v2 v2.6.2
7 | github.com/gin-contrib/cors v1.3.0
8 | github.com/gin-gonic/gin v1.4.0
9 | github.com/jinzhu/gorm v1.9.8
10 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/server/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
5 | github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
6 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
7 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
9 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
10 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
11 | github.com/appleboy/gin-jwt v2.5.0+incompatible h1:oLQTP1fiGDoDKoC2UDqXD9iqCP44ABIZMMenfH/xCqw=
12 | github.com/appleboy/gin-jwt v2.5.0+incompatible/go.mod h1:pG7tv32IEe5wEh1NSQzcyD02ZZAqZWp07RdGiIhgaRQ=
13 | github.com/appleboy/gin-jwt/v2 v2.6.2 h1:aW8jd9Zt5lU5W18GvLMO3/T9O8DETfW3O7GzGxcL6So=
14 | github.com/appleboy/gin-jwt/v2 v2.6.2/go.mod h1:fPyTIp4l5gtQnThEGuMBzCcfvMVSs9dsfrZlXsaTJMY=
15 | github.com/appleboy/gofight/v2 v2.1.1/go.mod h1:6E7pthKhmwss84j/zEixBNim8Q6ahhHcYOtmW5ts5vA=
16 | github.com/astaxie/beego v1.11.1/go.mod h1:i69hVzgauOPSw5qeyF4GVZhn7Od0yG5bbCGzmhbWxgQ=
17 | github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
18 | github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
19 | github.com/belogik/goes v0.0.0-20151229125003-e54d722c3aff/go.mod h1:PhH1ZhyCzHKt4uAasyx+ljRCgoezetRNf59CUtwUkqY=
20 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
21 | github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
22 | github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
23 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
24 | github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
25 | github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
26 | github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
27 | github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
28 | github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
30 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
31 | github.com/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
32 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
33 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
34 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
35 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
36 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
37 | github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
38 | github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
39 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
40 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
41 | github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxlg=
42 | github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc=
43 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
44 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
45 | github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
46 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
47 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
48 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
49 | github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
50 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
51 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
52 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
53 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
54 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
55 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
56 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
57 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
58 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
59 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
60 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
61 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
62 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
63 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
64 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
65 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
66 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
67 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
68 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
69 | github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
70 | github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
71 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
72 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
73 | github.com/jinzhu/gorm v1.9.8 h1:n5uvxqLepIP2R1XF7pudpt9Rv8I3m7G9trGxJVjLZ5k=
74 | github.com/jinzhu/gorm v1.9.8/go.mod h1:bdqTT3q6dhSph2K3pWxrHP6nqxuAp2yQ3KFtc3U3F84=
75 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
76 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
77 | github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
78 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
79 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
80 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
81 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
82 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
83 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
84 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
85 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
86 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
87 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
88 | github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
89 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
90 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
91 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
92 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
93 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
94 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
95 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
96 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
97 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
98 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
99 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
100 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
101 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
102 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
103 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
104 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
105 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
106 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
107 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
108 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
109 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
110 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
111 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
112 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
113 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
114 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
115 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
116 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
117 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
118 | github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
119 | github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg=
120 | github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
121 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
122 | github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
123 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
124 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
125 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
126 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
127 | github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
128 | github.com/tidwall/gjson v1.2.1/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=
129 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
130 | github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
131 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
132 | github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
133 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
134 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
135 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
136 | github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
137 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
138 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
139 | golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
140 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
141 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
142 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
143 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
144 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
145 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
146 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
147 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
148 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
149 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
150 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
151 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
152 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
153 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
154 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
155 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
156 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
157 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
158 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
159 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
160 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
161 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
162 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
163 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
164 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
165 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
166 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
167 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
168 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA=
169 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
170 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
171 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
172 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
173 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
174 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
175 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
176 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
177 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
178 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
179 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
180 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
181 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
182 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
183 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
184 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
185 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
186 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
187 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
188 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 h1:N46iQqOtHry7Hxzb9PGrP68oovQmj7EhudNoKHvbOvI=
189 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0/go.mod h1:hdNXC2Z9yC029rvsQ/on2ZNQ44Z2XToVhpXXbR+J05A=
190 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
191 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
192 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
193 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
194 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
195 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
196 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
197 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
198 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
199 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
200 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
201 |
--------------------------------------------------------------------------------
/server/jwt.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/appleboy/gin-jwt/v2"
5 | "github.com/gin-gonic/gin"
6 | )
7 |
8 | var identityKey = "id"
9 |
10 | func makePayLoad(data interface{}) jwt.MapClaims {
11 | if v, ok := data.(*User); ok {
12 | return jwt.MapClaims{
13 | identityKey: v.Username,
14 | }
15 | }
16 | return jwt.MapClaims{}
17 | }
18 |
19 | func makeIdentityHandler(c *gin.Context) interface{} {
20 | claims := jwt.ExtractClaims(c)
21 | return &User{
22 | Username: claims["id"].(string),
23 | }
24 | }
25 |
26 | func (e *Env) makeAuthenticator(c *gin.Context) (interface{}, error) {
27 | var request Credentials
28 | if err := c.ShouldBind(&request); err != nil {
29 | return "", jwt.ErrMissingLoginValues
30 | }
31 | var user User
32 | err := e.db.Table("users").
33 | Joins("JOIN auths ON users.id = user_id").
34 | Where("username = ? and password = ?", request.Username, request.Password).First(&user).Error
35 | if err != nil {
36 | return nil, jwt.ErrFailedAuthentication
37 | }
38 | return &User{
39 | Username: request.Username,
40 | }, nil
41 | }
42 |
43 | func makeAuthorizator(data interface{}, c *gin.Context) bool {
44 | _, ok := data.(*User)
45 | return ok
46 | }
47 |
48 | func makeUnauthorized(c *gin.Context, code int, message string) {
49 | c.JSON(code, gin.H{
50 | "code": code,
51 | "message": message,
52 | })
53 | }
54 |
55 | // retrieves owner (username) from JWT token
56 | func getOwner(c *gin.Context) string {
57 | user, _ := c.Get(identityKey)
58 | owner := user.(*User).Username
59 | return owner
60 | }
61 |
--------------------------------------------------------------------------------
/server/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEogIBAAKCAQEAsbNOF38cqpFnHc3KTSyew5bYZl2bnqFiUbcdR/IlhbPl/Ne/
3 | HJjlGYESKHPXwQl2PV0ZsqyYyNM2pQvHtmTtZE8oRPLzOiASnpQYeaT1EqnxranH
4 | RQ0a080LklzcvV2DDUkEzrQdhvmsCUnFHl3jbdQ96oGWYNqdQFqrcfhVl+kPM/Er
5 | Se/wtHwJgo4UhC/GFWLN/rB2tzH3OIvMnq+FRd4AxSNOrHGGFtsds6IcbznOyoxw
6 | KMiAjqOjb52ph+0ia1YTSJy4GjTPXXaa4ucgkIaJoGXNZ25da04N3dI3WD6rE/EU
7 | ByhYveCz41bUG1WHAf5D99GaXY1a1l96EW0ByQIDAQABAoIBACiEaLbkzcV6d9eM
8 | 16a3GNAh8d+NUnFd/MwDA5Mm/DU7KqJ3EuVTW1FwY5KDr1sdbC9brgNWZVPNDPWe
9 | 9P96jcJEZjsqZhkHvRcZT2dVHGgQBcICJyRwc4B9jTjnIZGv47TAxG7ZFK50+Sv4
10 | /RAKelPNg/yXZZFZ52cIBXVsGSAUq2wwJXvUddq8wbVpO9Cy6DDRCpfb3mQRERUt
11 | ke88c39jPNzgqL2V8GLI5J/rx8HzokFHJ6+aFVN8POzLWmDb/2rnv1YMVJdtBIv1
12 | b63RAig9xxOPaOh3BKaRZEi1ESv/0O371kV+WWEVxUkMyMeNv6dvIMJ712dr6jmg
13 | zljBB70CgYEA4gkj5ttaiK0xoUbLZ+IFiCeN8i2WMEvcht+gkhIK9LUx9pGWQhfw
14 | 1VQGZqgDgmHJCc+dan8iPUEFMmnXhdh7yULEfxrufknVTqvZ6UPhrDgcnPdlp8DE
15 | dR4Zr82Yj3Fkyv9VLy3C+jL3kZvbgxwTFqCBBl3SuWArn07ylnwFgisCgYEAyUHV
16 | lPOhP/QyBWFZOXUuhti4nlUleZfMyxcR/vfqVG91Q+qT7O77JhnsjjB2iTvTnb/+
17 | TeT2rj9AuUREEnJ24IkNTOEonxEe5jeQz4MEJyU1QYZv/gtmVndHiljox2OJSegn
18 | 8Re+trH8VQ+UXa+a2jwg6goYecQwwkUFp0UrddsCgYBeIgpWkRFyDBa4IICDQcil
19 | /DsMGVoLwPwVGzqGuobfhDpLgjP+UHQWk4ia5euYN9r+f/0BpfJ/af2dEiEUd6SN
20 | m4WznWanJ15zBfSZRZDNJQ0dfZZTN19ZmvB9m3SmgEXGmHFEVZ12jxU1CaBwSJfW
21 | e39gRGCGnPttu/YhH4M3wwKBgCu/edQFGZozVORCgwiwZkq9tXQFgj3qN4Q2IZ1Q
22 | +skb6Vu4FCu+zy07GWbXNg0iyh9Sas835D+AVGtxYXK5Gwo4AIIjt5bMO/FDRuE0
23 | 06RGvErgFFwe0kIdb5mtNfyRsHg2VOhdhwFjszwyRMgQshKaW3VEeImPkiHIqy/v
24 | I0FdAoGATw1Vy/UEzO0DKz0Flbm6cR+kz29eD06nJ2kk/C4swulHc8L/iwtEb5Va
25 | w8wi/vqXepjPTN8VkACN94PUa8OoSknFcH6UCBfKMLnN6FW0zdfcZRGNcMj8GUli
26 | lJ5mD1iU1e+Sw46YcvaffVTFe8M+K56ec72BZQ6GEE35HlnM5iQ=
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/appleboy/gin-jwt/v2"
9 | "github.com/gin-contrib/cors"
10 | "github.com/gin-gonic/gin"
11 | "github.com/jinzhu/gorm"
12 | _ "github.com/jinzhu/gorm/dialects/sqlite"
13 | )
14 |
15 | // Env represents the server environment (db, etc.)
16 | type Env struct {
17 | db *gorm.DB
18 | }
19 |
20 | func groupMembershipRequired(e *Env) gin.HandlerFunc {
21 | return func(c *gin.Context) {
22 | groupID := c.Param("id")
23 | owner := getOwner(c)
24 | var login User
25 |
26 | // e.db.First(&group, "id = ?", groupID)
27 | // e.db.First(&login, "username = ?", owner)
28 | // err := e.db.Model(&group).Related(&login, "Users").Error
29 | err := e.db.
30 | Joins("JOIN group_users ON user_id = users.id AND group_id = ?", groupID).
31 | Where("username = ?", owner).
32 | Find(&login).Error
33 |
34 | if err != nil {
35 | c.JSON(http.StatusForbidden, gin.H{"err": "You're not allowed to access or manage this group"})
36 | c.Abort()
37 | return
38 | }
39 |
40 | c.Next()
41 | }
42 | }
43 |
44 | func (e *Env) httpEngine() *gin.Engine {
45 | r := gin.Default()
46 |
47 | // CORS configuration
48 | config := cors.DefaultConfig()
49 | config.AllowOrigins = []string{"*"}
50 | config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "User-Agent", "Referrer", "Host", "Token", "Authorization"}
51 | r.Use(cors.New(config))
52 |
53 | // JWT middleware
54 | authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
55 | Realm: "Notes Server 0.0.1",
56 | SigningAlgorithm: "RS256",
57 | PubKeyFile: "./public.pem",
58 | PrivKeyFile: "./key.pem",
59 | Timeout: time.Hour,
60 | MaxRefresh: time.Hour,
61 | IdentityKey: identityKey,
62 | PayloadFunc: makePayLoad,
63 | IdentityHandler: makeIdentityHandler,
64 | Authenticator: e.makeAuthenticator,
65 | Authorizator: makeAuthorizator,
66 | Unauthorized: makeUnauthorized,
67 | TokenLookup: "header: Authorization, query: token, cookie: jwt",
68 | TimeFunc: time.Now,
69 | })
70 | if err != nil {
71 | log.Fatal("JWT Error: " + err.Error())
72 | }
73 |
74 | r.POST("/subscribe", e.subscribeHandler)
75 | r.POST("/login", authMiddleware.LoginHandler)
76 |
77 | auth := r.Group("/auth")
78 | // Refresh time can be longer than token timeout
79 | auth.GET("/refresh_token", authMiddleware.RefreshHandler)
80 | auth.Use(authMiddleware.MiddlewareFunc())
81 | {
82 | auth.POST("/update/password", e.subscribeHandler)
83 |
84 | auth.GET("/notes", e.noteListHandler)
85 | auth.GET("/notes/:id", e.noteGetHandler)
86 | auth.POST("/notes", e.notePostHandler)
87 | auth.PATCH("/notes/:id", e.notePostHandler)
88 | auth.DELETE("/notes/:id", e.noteDelete)
89 |
90 | auth.GET("/users", e.userListHandler)
91 |
92 | auth.POST("/group", e.groupCreateHandler)
93 | auth.GET("/groups", e.groupListHandler)
94 |
95 | group := auth.Group("/group/:id")
96 | group.Use(groupMembershipRequired(e))
97 | {
98 | group.GET("", e.groupGetHandler)
99 | group.PATCH("", e.groupEditHandler)
100 | group.GET("/notes", e.noteGroupListHandler)
101 | group.POST("/notes", e.noteGroupPostHandler)
102 | group.GET("/notes/:noteId", e.noteGroupGetHandler)
103 | group.DELETE("/notes/:noteId", e.noteGroupDeleteHandler)
104 | }
105 |
106 | auth.POST("/share/:id/:with", e.noteShareHandler)
107 | }
108 |
109 | r.GET("/ping", func(c *gin.Context) {
110 | c.JSON(200, gin.H{
111 | "message": "pong",
112 | })
113 | })
114 |
115 | return r
116 | }
117 |
118 | func openEnv(name string) *Env {
119 | db, err := gorm.Open("sqlite3", name)
120 | if err != nil {
121 | panic("failed to connect database")
122 | }
123 | // Migrate the schema
124 | db.AutoMigrate(&Note{}, &Auth{}, &User{}, &Group{})
125 | return &Env{db: db}
126 | }
127 |
128 | func main() {
129 | env := openEnv("./notes-db/notesE.db")
130 | defer env.db.Close()
131 | env.httpEngine().Run()
132 | }
133 |
--------------------------------------------------------------------------------
/server/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "net/http/httptest"
10 | "os"
11 | "sort"
12 | "strings"
13 | "testing"
14 |
15 | "github.com/gin-gonic/gin"
16 | )
17 |
18 | var env *Env
19 | var server *gin.Engine
20 | var debug bool
21 |
22 | // Simple test
23 | func TestHandlePingReturnsWithStatusOK(t *testing.T) {
24 | request, _ := http.NewRequest("GET", "/ping", nil)
25 | response := httptest.NewRecorder()
26 | server.ServeHTTP(response, request)
27 |
28 | if response.Code != http.StatusOK {
29 | t.Errorf("Status code expected %v, got: %v", http.StatusOK, response.Code)
30 | }
31 | }
32 |
33 | func getJSON(t *testing.T, url string, token string, expectedStatus int) (map[string]interface{}, error) {
34 | var m map[string]interface{}
35 | request, err := http.NewRequest("GET", url, strings.NewReader(""))
36 | if err != nil {
37 | return m, err
38 | }
39 | request.Header.Add("Content-Type", "application/json")
40 | request.Header.Add("Authorization", "Bearer "+token)
41 |
42 | response := httptest.NewRecorder()
43 | server.ServeHTTP(response, request)
44 |
45 | if response.Code != expectedStatus {
46 | t.Errorf("Status code expected %v, got: %v", expectedStatus, response.Code)
47 | }
48 |
49 | err = json.NewDecoder(response.Body).Decode(&m)
50 | return m, err
51 | }
52 |
53 | func deleteJSON(t *testing.T, url string, token string, expectedStatus int) (map[string]interface{}, error) {
54 | var m map[string]interface{}
55 | request, err := http.NewRequest("DELETE", url, strings.NewReader(""))
56 | if err != nil {
57 | return m, err
58 | }
59 | request.Header.Add("Content-Type", "application/json")
60 | request.Header.Add("Authorization", "Bearer "+token)
61 |
62 | response := httptest.NewRecorder()
63 | server.ServeHTTP(response, request)
64 |
65 | if response.Code != expectedStatus {
66 | t.Errorf("Status code expected %v, got: %v", expectedStatus, response.Code)
67 | }
68 |
69 | err = json.NewDecoder(response.Body).Decode(&m)
70 | return m, nil
71 | }
72 |
73 | // Utility function to POST JSON and get JSON result
74 | // TODO: extract Bearer
75 | func methodJSON(t *testing.T, method string, url string, data map[string]interface{}, token *string, expectedStatus int) (map[string]interface{}, error) {
76 | var m map[string]interface{}
77 | body, _ := json.Marshal(data)
78 |
79 | if debug {
80 | t.Log("postJSON:query body= ", string(body))
81 | }
82 |
83 | request, err := http.NewRequest(method, url, bytes.NewReader(body))
84 | if err != nil {
85 | return m, err
86 | }
87 | request.Header.Add("Content-Type", "application/json")
88 | if token != nil {
89 | request.Header.Add("Authorization", "Bearer "+*token)
90 | }
91 |
92 | response := httptest.NewRecorder()
93 | server.ServeHTTP(response, request)
94 |
95 | if response.Code != expectedStatus {
96 | return m, fmt.Errorf("Status code expected %v, got: %v", expectedStatus, response.Code)
97 | }
98 |
99 | err = json.NewDecoder(response.Body).Decode(&m)
100 |
101 | // print headers and body
102 | if debug {
103 | b, _ := json.Marshal(response.HeaderMap)
104 | t.Log("postJSON:response headers= ", string(b))
105 | b, _ = json.Marshal(m)
106 | t.Log("postJSON:response body= ", string(b))
107 | }
108 |
109 | return m, err
110 | }
111 |
112 | func postJSON(t *testing.T, url string, data map[string]interface{}, token *string, expectedStatus int) (map[string]interface{}, error) {
113 | return methodJSON(t, "POST", url, data, token, expectedStatus)
114 | }
115 |
116 | func patchJSON(t *testing.T, url string, data map[string]interface{}, token *string, expectedStatus int) (map[string]interface{}, error) {
117 | return methodJSON(t, "PATCH", url, data, token, expectedStatus)
118 | }
119 |
120 | func TestCreateUserAndLogInPostAndGetNotes(t *testing.T) {
121 | user := map[string]interface{}{
122 | "username": "admin",
123 | "password": "admintopsecretpass",
124 | }
125 | note := map[string]interface{}{
126 | "title": "this is title",
127 | "content": "this is content",
128 | }
129 | // login before subscribe
130 | result, err := postJSON(t, "/login", user, nil, http.StatusUnauthorized)
131 | if err != nil {
132 | t.Fatalf("Non-expected error: %v", err)
133 | }
134 | // subscribe
135 | result, err = postJSON(t, "/subscribe", user, nil, 200)
136 | if err != nil {
137 | t.Fatalf("Non-expected error: %v", err)
138 | }
139 | if result["status"] != "user created" {
140 | t.Fatalf("Non-expected response: %v", result)
141 | }
142 | // login
143 | result, err = postJSON(t, "/login", user, nil, 200)
144 | if err != nil {
145 | t.Fatalf("Non-expected error: %v", err)
146 | }
147 | token := result["token"].(string)
148 | // post note (twice)
149 | result, err = postJSON(t, "/auth/notes", note, &token, 200)
150 | if err != nil {
151 | t.Fatalf("Non-expected error: %v", err)
152 | }
153 | result, err = postJSON(t, "/auth/notes", note, &token, 200)
154 | if err != nil {
155 | t.Fatalf("Non-expected error: %v", err)
156 | }
157 | // get notes and check length
158 | result, err = getJSON(t, "/auth/notes", token, 200)
159 | if err != nil {
160 | t.Fatalf("Non-expected error: %v", err)
161 | }
162 | if errMsg, exists := result["err"]; exists {
163 | t.Fatalf("Unexpected error %+v %+v %+v", err, errMsg, result)
164 | }
165 | notes := result["notes"].([]interface{})
166 | if len(notes) != 2 {
167 | t.Fatalf("Wrong number of notes: %v", len(notes))
168 | }
169 | note0 := notes[0].(map[string]interface{})
170 | if note0["Title"].(string) != "this is title" {
171 | t.Fatalf("First note has wrong title: %v", note0["Title"].(string))
172 | }
173 | }
174 |
175 | func TestNoteDelete(t *testing.T) {
176 | // users := []Credentials{Credentials{"alice-delete", "haha"}, Credentials{"bob-delete", "hoho"}}
177 | users := []map[string]interface{}{
178 | map[string]interface{}{
179 | "username": "alice-delete",
180 | "password": "haha",
181 | },
182 | map[string]interface{}{
183 | "username": "bob-delete",
184 | "password": "hoho",
185 | },
186 | }
187 |
188 | tokens := make([]string, 2)
189 | noteIds := make([]float64, 2)
190 |
191 | for idx, user := range users {
192 | // subscribe
193 | result, err := postJSON(t, "/subscribe", user, nil, 200)
194 | if err != nil {
195 | t.Fatalf("Non-expected error: %v", err)
196 | }
197 | if result["status"] != "user created" {
198 | t.Fatalf("Non-expected response: %v", result)
199 | }
200 | // login
201 | result, err = postJSON(t, "/login", user, nil, 200)
202 | if err != nil {
203 | t.Fatalf("Non-expected error: %v", err)
204 | }
205 | tokens[idx] = result["token"].(string)
206 | // post note
207 | note := map[string]interface{}{
208 | "title": fmt.Sprintf("Note of %s", user["username"]),
209 | "content": "this is content",
210 | }
211 | result, err = postJSON(t, "/auth/notes", note, &tokens[idx], 200)
212 | if err != nil {
213 | t.Fatalf("Non-expected error: %v", err)
214 | }
215 | log.Printf("Result : %+v", result)
216 | noteIds[idx] = result["noteID"].(float64)
217 | }
218 |
219 | // User 1 delete note of user 2
220 | _, err := deleteJSON(t, fmt.Sprintf("/auth/notes/%v", noteIds[1]), tokens[0], http.StatusForbidden)
221 | if err != nil {
222 | t.Fatalf("Non-expected error: %v", err)
223 | }
224 | // User 1 delete note of user 1
225 | _, err = deleteJSON(t, fmt.Sprintf("/auth/notes/%v", noteIds[0]), tokens[0], http.StatusOK)
226 | if err != nil {
227 | t.Fatalf("Non-expected error: %v", err)
228 | }
229 | }
230 |
231 | func TestUserListing(t *testing.T) {
232 | var err error
233 | user1 := map[string]interface{}{
234 | "username": "toto_for_listing",
235 | "password": "totopass",
236 | }
237 | user2 := map[string]interface{}{
238 | "username": "titi_for_listing",
239 | "password": "titipass",
240 | }
241 | // create 2 users
242 | _, err = postJSON(t, "/subscribe", user1, nil, 200)
243 | if err != nil {
244 | t.Fatalf("Non-expected error: %v", err)
245 | }
246 | _, err = postJSON(t, "/subscribe", user2, nil, 200)
247 | if err != nil {
248 | t.Fatalf("Non-expected error: %v", err)
249 | }
250 |
251 | // login
252 | result, err := postJSON(t, "/login", user1, nil, 200)
253 | if err != nil {
254 | t.Fatalf("Non-expected error: %v", err)
255 | }
256 | token := result["token"].(string)
257 |
258 | result, err = getJSON(t, "/auth/users", token, 200)
259 | if err != nil {
260 | t.Fatalf("Non-expected error: %v", err)
261 | }
262 |
263 | if _, ok := result["err"].([]interface{}); ok {
264 | t.Fatalf("I should not receive error %+v", result)
265 | }
266 |
267 | if users, ok := result["users"].([]interface{}); ok {
268 | if len(users) < 2 {
269 | t.Fatalf("User list should contains at least 2 users")
270 | }
271 |
272 | user1Received := false
273 | user2Received := false
274 |
275 | for _, u := range users {
276 | if u.(string) == user1["username"].(string) {
277 | user1Received = true
278 | }
279 | if u.(string) == user2["username"].(string) {
280 | user2Received = true
281 | }
282 | }
283 |
284 | if !user1Received {
285 | t.Fatalf("User list should contains user1")
286 | }
287 | if !user2Received {
288 | t.Fatalf("User list should contains user2")
289 | }
290 | } else {
291 | t.Fatalf("I should not receive %+v", result)
292 | }
293 |
294 | result, err = getJSON(t, "/auth/users?search=titi", token, 200)
295 | if err != nil {
296 | t.Fatalf("Non-expected error: %v", err)
297 | }
298 |
299 | users := result["users"].([]interface{})
300 | if len(users) < 1 {
301 | t.Fatalf("Expect to receive at least one titi")
302 | }
303 |
304 | }
305 |
306 | func TestNoteSharing(t *testing.T) {
307 | var err error
308 | user1 := map[string]interface{}{
309 | "username": "toto",
310 | "password": "totopass",
311 | }
312 | user2 := map[string]interface{}{
313 | "username": "titi",
314 | "password": "titipass",
315 | }
316 | note := map[string]interface{}{
317 | "title": "title will be shared",
318 | "content": "content will be shared",
319 | }
320 | note2 := map[string]interface{}{
321 | "title": "title will not be shared",
322 | "content": "content will not be shared",
323 | }
324 | // create 2 users
325 | _, err = postJSON(t, "/subscribe", user1, nil, 200)
326 | if err != nil {
327 | t.Fatalf("Non-expected error: %v", err)
328 | }
329 | result, err := postJSON(t, "/subscribe", user2, nil, 200)
330 | if err != nil {
331 | t.Fatalf("Non-expected error: %v", err)
332 | }
333 | // user2ID := result["userID"]
334 | // login first user and get token
335 | result, err = postJSON(t, "/login", user1, nil, 200)
336 | if err != nil {
337 | t.Fatalf("Non-expected error: %v", err)
338 | }
339 | token := result["token"].(string)
340 | // user1 creates note and get ID
341 | result, err = postJSON(t, "/auth/notes", note, &token, 200)
342 | if err != nil {
343 | t.Fatalf("Non-expected error: %v", err)
344 | }
345 | noteID := int(result["noteID"].(float64))
346 |
347 | // user2 logs in
348 | result, err = postJSON(t, "/login", user2, nil, 200)
349 | if err != nil {
350 | t.Fatalf("Non-expected error: %v", err)
351 | }
352 | token2 := result["token"].(string)
353 | // get notes and check length
354 | result, err = getJSON(t, "/auth/notes", token2, 200)
355 | if err != nil {
356 | t.Fatalf("Non-expected error: %v", err)
357 | }
358 | notes := result["notes"].([]interface{})
359 | if len(notes) != 0 {
360 | t.Fatalf("Wrong number of notes: %v in %+v", len(notes), notes)
361 | }
362 |
363 | // user2 creates note and get ID
364 | result, err = postJSON(t, "/auth/notes", note2, &token2, 200)
365 | if err != nil {
366 | t.Fatalf("Non-expected error: %v", err)
367 | }
368 | noteIDuser2 := int(result["noteID"].(float64))
369 |
370 | // user1 get user2's note and fail
371 | result, err = getJSON(t, fmt.Sprintf("/auth/notes/%v", noteIDuser2), token, http.StatusForbidden)
372 | if err != nil {
373 | t.Fatalf("Non-expected error: %v", err)
374 | }
375 | // notes = result["notes"].([]interface{})
376 | // if len(notes) != 2 {
377 | // t.Fatalf("Wrong number of notes: %v in %+v", len(notes), notes)
378 | // }
379 |
380 | // user1 shares note with user2
381 | empty := map[string]interface{}{}
382 | result, err = postJSON(t, fmt.Sprintf("/auth/share/%v/%v", noteID, user2["username"]), empty, &token, 200)
383 | if err != nil {
384 | t.Fatalf("Non-expected error: %v", err)
385 | }
386 | // user1 shares note2 with user2 and fail
387 | result, err = postJSON(t, fmt.Sprintf("/auth/share/%v/%v", noteIDuser2, user2["username"]), empty, &token, http.StatusForbidden)
388 | if err != nil {
389 | t.Fatalf("Non-expected error: %v", err)
390 | }
391 |
392 | // user2 get notes and check length
393 | result, err = getJSON(t, "/auth/notes", token2, 200)
394 | if err != nil {
395 | t.Fatalf("Non-expected error: %v", err)
396 | }
397 | notes = result["notes"].([]interface{})
398 | if len(notes) != 2 {
399 | t.Fatalf("Wrong number of notes: %v in %+v", len(notes), notes)
400 | }
401 | }
402 |
403 | func TestGroup(t *testing.T) {
404 | var err error
405 | user1 := map[string]interface{}{
406 | "username": "toto_group",
407 | "password": "totopass",
408 | }
409 | user2 := map[string]interface{}{
410 | "username": "titi_group",
411 | "password": "titipass",
412 | }
413 | user3 := map[string]interface{}{
414 | "username": "tata_group",
415 | "password": "tatapass",
416 | }
417 | group := map[string]interface{}{
418 | "name": "my group",
419 | "users": []string{user1["username"].(string), user2["username"].(string)},
420 | }
421 | // create 3 users
422 | _, err = postJSON(t, "/subscribe", user1, nil, 200)
423 | if err != nil {
424 | t.Fatalf("Non-expected error: %v", err)
425 | }
426 | _, err = postJSON(t, "/subscribe", user2, nil, 200)
427 | if err != nil {
428 | t.Fatalf("Non-expected error: %v", err)
429 | }
430 | _, err = postJSON(t, "/subscribe", user3, nil, 200)
431 | if err != nil {
432 | t.Fatalf("Non-expected error: %v", err)
433 | }
434 | // login first user and get token
435 | result, err := postJSON(t, "/login", user1, nil, 200)
436 | if err != nil {
437 | t.Fatalf("Non-expected error: %v", err)
438 | }
439 | token1 := result["token"].(string)
440 | result, err = postJSON(t, "/login", user2, nil, 200)
441 | if err != nil {
442 | t.Fatalf("Non-expected error: %v", err)
443 | }
444 | token2 := result["token"].(string)
445 | result, err = postJSON(t, "/login", user3, nil, 200)
446 | if err != nil {
447 | t.Fatalf("Non-expected error: %v", err)
448 | }
449 | token3 := result["token"].(string)
450 | // user1 creates the group
451 | result, err = postJSON(t, "/auth/group", group, &token1, 200)
452 | if err != nil {
453 | t.Fatalf("Non-expected error: %v", err)
454 | }
455 | groupID := result["id"]
456 | // user1 get the group description
457 | result, err = getJSON(t, fmt.Sprintf("/auth/group/%v", groupID), token1, 200)
458 | if err != nil {
459 | t.Fatalf("Non-expected error: %v", err)
460 | }
461 | compareGroups(t, group, result["group"].(map[string]interface{}))
462 | // user1 get his groups
463 | result, err = getJSON(t, "/auth/groups", token1, 200)
464 | if err != nil {
465 | t.Fatalf("Non-expected error: %v", err)
466 | }
467 | hasGroups(t, []string{"my group"}, result["groups"].([]interface{}))
468 | // user1 edit the group description
469 | group = map[string]interface{}{
470 | "name": "my renamed group",
471 | "users": []string{user1["username"].(string)},
472 | }
473 | _, err = patchJSON(t, fmt.Sprintf("/auth/group/%v", groupID), group, &token1, 200)
474 | if err != nil {
475 | t.Fatalf("Non-expected error: %v", err)
476 | }
477 | // user1 get the group description after edit
478 | result, err = getJSON(t, fmt.Sprintf("/auth/group/%v", groupID), token1, 200)
479 | if err != nil {
480 | t.Fatalf("Non-expected error: %v", err)
481 | }
482 | compareGroups(t, group, result["group"].(map[string]interface{}))
483 | // user1 create another group
484 | anotherGroup := map[string]interface{}{
485 | "name": "another group",
486 | "users": []string{user1["username"].(string), user2["username"].(string)},
487 | }
488 | result, err = postJSON(t, "/auth/group", anotherGroup, &token1, 200)
489 | if err != nil {
490 | t.Fatalf("Non-expected error: %v", err)
491 | }
492 | sharedGroupID := result["id"]
493 | // user1 get his groups
494 | result, err = getJSON(t, "/auth/groups", token1, 200)
495 | if err != nil {
496 | t.Fatalf("Non-expected error: %v", err)
497 | }
498 | hasGroups(t, []string{"another group", "my renamed group"}, result["groups"].([]interface{}))
499 | // post note (twice)
500 | note := map[string]interface{}{
501 | "title": "this is title group",
502 | "content": "this is content group",
503 | }
504 | result, err = postJSON(t, fmt.Sprintf("/auth/group/%v/notes", groupID), note, &token1, 200)
505 | if err != nil {
506 | t.Fatalf("Non-expected error: %v", err)
507 | }
508 | result, err = postJSON(t, fmt.Sprintf("/auth/group/%v/notes", groupID), note, &token1, 200)
509 | if err != nil {
510 | t.Fatalf("Non-expected error: %v", err)
511 | }
512 | // get notes and check length
513 | result, err = getJSON(t, fmt.Sprintf("/auth/group/%v/notes", groupID), token1, 200)
514 | if err != nil {
515 | t.Fatalf("Non-expected error: %v", err)
516 | }
517 | notes := result["notes"].([]interface{})
518 | if len(notes) != 2 {
519 | t.Fatalf("Wrong number of notes: %v", len(notes))
520 | }
521 | note0 := notes[0].(map[string]interface{})
522 | if note0["Title"].(string) != "this is title group" {
523 | t.Fatalf("First note has wrong title: %v", note0["Title"].(string))
524 | }
525 | // Test get of 1 note
526 | // get notes and check length
527 | result, err = getJSON(t, fmt.Sprintf("/auth/group/%v/notes/%v", groupID, note0["ID"]), token1, 200)
528 | if err != nil {
529 | t.Fatalf("Non-expected error: %v", err)
530 | }
531 | newNote0 := result["note"].(map[string]interface{})
532 | if note0["CreatedAt"] != newNote0["CreatedAt"] {
533 | t.Fatalf("Group note is different than before")
534 | }
535 | // user2 can access group 2, not user 3
536 | _, err = getJSON(t, fmt.Sprintf("/auth/group/%v/notes", sharedGroupID), token2, 200)
537 | if err != nil {
538 | t.Fatalf("Non-expected error: %v", err)
539 | }
540 | _, err = getJSON(t, fmt.Sprintf("/auth/group/%v/notes", sharedGroupID), token3, http.StatusForbidden)
541 | if err != nil {
542 | t.Fatalf("Non-expected error: %v", err)
543 | }
544 | }
545 |
546 | func TestGroupNoteDelete(t *testing.T) {
547 | var err error
548 | user1 := map[string]interface{}{
549 | "username": "toto_group_2",
550 | "password": "totopass2",
551 | }
552 | group := map[string]interface{}{
553 | "name": "my group",
554 | "users": []string{user1["username"].(string)},
555 | }
556 | // create 1 users
557 | _, err = postJSON(t, "/subscribe", user1, nil, 200)
558 | if err != nil {
559 | t.Fatalf("Non-expected error: %v", err)
560 | }
561 | // login first user and get token
562 | result, err := postJSON(t, "/login", user1, nil, 200)
563 | if err != nil {
564 | t.Fatalf("Non-expected error: %v", err)
565 | }
566 | token1 := result["token"].(string)
567 |
568 | note := map[string]interface{}{
569 | "title": "title",
570 | "content": "content",
571 | }
572 | // user1 creates note and get ID
573 | result, err = postJSON(t, "/auth/notes", note, &token1, 200)
574 | if err != nil {
575 | t.Fatalf("Non-expected error: %v", err)
576 | }
577 | noteID := int(result["noteID"].(float64))
578 |
579 | // user1 creates the group
580 | result, err = postJSON(t, "/auth/group", group, &token1, 200)
581 | if err != nil {
582 | t.Fatalf("Non-expected error: %v", err)
583 | }
584 | groupID := result["id"]
585 |
586 | // post note in the group
587 | note = map[string]interface{}{
588 | "title": "this is title group note",
589 | "content": "this is content group note",
590 | }
591 | result, err = postJSON(t, fmt.Sprintf("/auth/group/%v/notes", groupID), note, &token1, 200)
592 | if err != nil {
593 | t.Fatalf("Non-expected error: %v", err)
594 | }
595 |
596 | // get notes and check length
597 | result, err = getJSON(t, fmt.Sprintf("/auth/group/%v/notes", groupID), token1, 200)
598 | if err != nil {
599 | t.Fatalf("Non-expected error: %v", err)
600 | }
601 | notes := result["notes"].([]interface{})
602 | if len(notes) != 1 {
603 | t.Fatalf("Wrong number of notes: %v", len(notes))
604 | }
605 | note0 := notes[0].(map[string]interface{})
606 |
607 | // DELETE the note
608 | result, err = deleteJSON(t, fmt.Sprintf("/auth/group/%v/notes/%v", groupID, note0["ID"]), token1, 200)
609 | if err != nil {
610 | t.Fatalf("Non-expected error: %v", err)
611 | }
612 |
613 | // get notes and check length
614 | result, err = getJSON(t, fmt.Sprintf("/auth/group/%v/notes", groupID), token1, 200)
615 | if err != nil {
616 | t.Fatalf("Non-expected error: %v", err)
617 | }
618 | notes = result["notes"].([]interface{})
619 | if len(notes) != 0 {
620 | t.Fatalf("Wrong number of notes: %v", len(notes))
621 | }
622 | // DELETE the note using group authorization and fail
623 | result, err = deleteJSON(t, fmt.Sprintf("/auth/group/%v/notes/%v", groupID, noteID), token1, 404)
624 | if err != nil {
625 | t.Fatalf("Non-expected error: %v", err)
626 | }
627 |
628 | }
629 |
630 | func hasGroups(t *testing.T, expected []string, groups []interface{}) {
631 | if len(expected) != len(groups) {
632 | t.Fatalf("Expected %v groups, have %v groups", len(expected), len(groups))
633 | }
634 | var groupsName []string
635 | for _, g := range groups {
636 | name := g.(map[string]interface{})["name"].(string)
637 | groupsName = append(groupsName, name)
638 | }
639 | sort.StringSlice(expected).Sort()
640 | sort.StringSlice(groupsName).Sort()
641 | for i := range expected {
642 | if expected[i] != groupsName[i] {
643 | t.Fatalf("expect groups(%v), has group(%v)", expected, groupsName)
644 | }
645 | }
646 | }
647 |
648 | func compareGroups(t *testing.T, post, result map[string]interface{}) {
649 | if result["name"].(string) != post["name"].(string) {
650 | t.Fatalf("The group is not well named, %v != %v", result["name"], post["name"])
651 | }
652 | var resultUsers []string
653 | users := result["users"].([]interface{})
654 | for _, u := range users {
655 | resultUsers = append(resultUsers, u.(map[string]interface{})["username"].(string))
656 | }
657 | postUsers := post["users"].([]string)
658 | if len(resultUsers) != len(postUsers) {
659 | t.Fatalf("The group has not the same size: %v vs %v", len(postUsers), len(resultUsers))
660 | }
661 | sort.StringSlice(resultUsers).Sort()
662 | sort.StringSlice(postUsers).Sort()
663 | for i := range postUsers {
664 | if postUsers[i] != resultUsers[i] {
665 | t.Fatalf("The groups are note equals")
666 | }
667 | }
668 | }
669 |
670 | func TestMain(m *testing.M) {
671 | // setup database
672 | _ = os.Remove("test.db")
673 | env = openEnv("test.db")
674 | debug = false
675 | env.db.LogMode(debug)
676 | defer env.db.Close()
677 | // setup server
678 | server = env.httpEngine()
679 |
680 | result := m.Run()
681 | os.Exit(result)
682 | }
683 |
--------------------------------------------------------------------------------
/server/notes-db/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wallix/notes/a7f2048d8ed037de1d0a0cc1666dc9c3240d0ae7/server/notes-db/.gitkeep
--------------------------------------------------------------------------------
/server/notes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/jinzhu/gorm"
10 | )
11 |
12 | // Note represents a note
13 | type Note struct {
14 | gorm.Model
15 | Title string
16 | Content string
17 | Users []*User `gorm:"many2many:note_shared;"`
18 | Groups []*Group `gorm:"many2many:note_groups;"`
19 | }
20 |
21 | func (e *Env) noteListHandler(c *gin.Context) {
22 | var notes []Note
23 | user, err := e.getUser(getOwner(c))
24 | if err != nil {
25 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
26 | return
27 | }
28 |
29 | err = e.db.Preload("Users").Model(&user).Related(¬es, "Notes").Error
30 | if err != nil {
31 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
32 | return
33 | }
34 |
35 | c.JSON(http.StatusOK, gin.H{"notes": notes})
36 | }
37 |
38 | func (e *Env) noteGetHandler(c *gin.Context) {
39 | var note Note
40 | noteID := c.Param("id")
41 | user, err := e.getUser(getOwner(c))
42 | if err != nil {
43 | c.JSON(http.StatusForbidden, gin.H{"err": err})
44 | return
45 | }
46 | err = e.db.Preload("Users").Model(&user).Where("note_id = ?", noteID).Related(¬e, "Notes").Error
47 | if err != nil {
48 | c.JSON(http.StatusForbidden, gin.H{"err": err})
49 | return
50 | }
51 | c.JSON(http.StatusOK, gin.H{"note": note})
52 | }
53 |
54 | func (e *Env) noteShareHandler(c *gin.Context) {
55 | var note Note
56 | noteID := c.Param("id")
57 | recipientID := c.Param("with")
58 | user, err := e.getUser(getOwner(c))
59 | if err != nil {
60 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
61 | return
62 | }
63 | // must be in the group user to share
64 | err = e.db.Model(&user).Where("notes.id = ?", noteID).Related(¬e, "Notes").Error
65 | if err != nil {
66 | c.JSON(http.StatusForbidden, gin.H{"err": err})
67 | return
68 | }
69 | // get user to share with
70 | login, err := e.getUser(recipientID)
71 | if err != nil {
72 | c.JSON(http.StatusBadRequest, gin.H{"err": err})
73 | return
74 | }
75 | // append user
76 | err = e.db.Model(¬e).Association("Users").Append(login).Error
77 | if err != nil {
78 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
79 | return
80 | }
81 | c.JSON(http.StatusOK, gin.H{})
82 | }
83 |
84 | func validateNote(note Note) error {
85 | if note.Title == "" {
86 | return errors.New("empty title")
87 | }
88 | return nil
89 | }
90 |
91 | func (e *Env) createOrUpdateNote(noteID string, note *Note) error {
92 | // create new note
93 | if noteID == "" {
94 | return e.db.Create(¬e).Error
95 | }
96 | // or update previous note
97 | var previous Note
98 | e.db.Where("ID = ?", noteID).First(&previous)
99 | previous.Title = note.Title
100 | previous.Content = note.Content
101 | return e.db.Save(&previous).Error
102 | }
103 |
104 | // FIXME: returns noteID==0 when PATCH
105 | func (e *Env) notePostHandler(c *gin.Context) {
106 | var err error
107 | var note Note
108 | c.ShouldBindJSON(¬e)
109 | err = validateNote(note)
110 | if err != nil {
111 | c.JSON(http.StatusBadRequest, gin.H{"err": err})
112 | return
113 | }
114 | // get owner
115 | usr, err := e.getUser(getOwner(c))
116 | if err != nil {
117 | c.JSON(http.StatusNotFound, gin.H{"err": fmt.Errorf("Author not found")})
118 | }
119 | // set note owner
120 | note.Users = []*User{usr}
121 | // get the (optional) id from path
122 | noteID := c.Param("id")
123 | // create or update the note
124 | err = e.createOrUpdateNote(noteID, ¬e)
125 | if err != nil {
126 | c.JSON(http.StatusInternalServerError, gin.H{"err": err}) // SECURITY
127 | return
128 | }
129 |
130 | c.JSON(http.StatusOK, gin.H{
131 | "noteID": note.ID,
132 | })
133 | }
134 |
135 | func (e *Env) noteGroupPostHandler(c *gin.Context) {
136 | var err error
137 | var note Note
138 | c.ShouldBindJSON(¬e)
139 | err = validateNote(note)
140 | if err != nil {
141 | c.JSON(http.StatusBadRequest, gin.H{"err": err})
142 | return
143 | }
144 |
145 | // set note owner
146 | // note.Owner = getOwner(c)
147 | // get the (optional) id from path
148 | groupID := c.Param("id")
149 | // create or update the note
150 | err = e.createGroupNote(¬e, groupID)
151 | if err != nil {
152 | c.JSON(http.StatusInternalServerError, gin.H{"err": err}) // SECURITY
153 | return
154 | }
155 |
156 | c.JSON(http.StatusOK, gin.H{
157 | "noteID": note.ID,
158 | })
159 | }
160 |
161 | func (e *Env) noteGroupGetHandler(c *gin.Context) {
162 | var group Group
163 | var note Note
164 | groupID := c.Param("id")
165 | noteID := c.Param("noteId")
166 | err := e.db.First(&group, groupID).Error
167 | if err != nil {
168 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
169 | return
170 | }
171 | err = e.db.Model(&group).Where("notes.id = ?", noteID).Related(¬e, "Notes").Error
172 | if err != nil {
173 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
174 | return
175 | }
176 | c.JSON(http.StatusOK, gin.H{"note": note})
177 | }
178 |
179 | func (e *Env) noteGroupListHandler(c *gin.Context) {
180 | var group Group
181 | var notes []Note
182 | groupID := c.Param("id")
183 | err := e.db.First(&group, groupID).Error
184 | if err != nil {
185 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
186 | return
187 | }
188 | err = e.db.Model(&group).Related(¬es, "Notes").Error
189 | if err != nil {
190 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
191 | return
192 | }
193 | c.JSON(http.StatusOK, gin.H{"notes": notes})
194 | }
195 |
196 | func (e *Env) createGroupNote(note *Note, groupID string) error {
197 | // associate with the group
198 | var group Group
199 | err := e.db.Where("id = ?", groupID).First(&group).Error
200 | if err != nil {
201 | return err
202 | }
203 | note.Groups = []*Group{&group}
204 | // create new note
205 | err = e.db.Save(¬e).Error
206 | if err != nil {
207 | return err
208 | }
209 | return nil
210 | }
211 |
212 | func (e *Env) noteDelete(c *gin.Context) {
213 | var note Note
214 | // get owner and note id
215 | noteID := c.Param("id")
216 | user, err := e.getUser(getOwner(c))
217 | if err != nil {
218 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
219 | return
220 | }
221 | // must be in the group user to delete
222 | err = e.db.Model(&user).Where("notes.id = ?", noteID).Related(¬e, "Notes").Error
223 | if err != nil {
224 | c.JSON(http.StatusForbidden, gin.H{"err": err})
225 | return
226 | }
227 | // delete the user association with the note
228 | err = e.db.Model(&user).Association("Notes").Delete(¬e).Error
229 | if err != nil {
230 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
231 | return
232 | }
233 | c.JSON(http.StatusOK, gin.H{})
234 | }
235 |
236 | func (e *Env) noteGroupDeleteHandler(c *gin.Context) {
237 | var note Note
238 | var group Group
239 | groupID := c.Param("id")
240 | noteID := c.Param("noteId")
241 | // Check the relation between the group and the note
242 | if e.db.
243 | Joins("JOIN note_groups ON note_id = notes.id AND group_id = ?", groupID).
244 | First(¬e, noteID).RecordNotFound() {
245 | c.JSON(http.StatusNotFound, gin.H{"err": "Not Found"})
246 | return
247 | }
248 | // Get the group
249 | err := e.db.First(&group, groupID).Error
250 | if err != nil {
251 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
252 | return
253 | }
254 | // delete the group association with the note
255 | err = e.db.Model(¬e).Association("Groups").Delete(&group).Error
256 | if err != nil {
257 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
258 | return
259 | }
260 | c.JSON(http.StatusOK, gin.H{})
261 | }
262 |
--------------------------------------------------------------------------------
/server/public.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsbNOF38cqpFnHc3KTSye
3 | w5bYZl2bnqFiUbcdR/IlhbPl/Ne/HJjlGYESKHPXwQl2PV0ZsqyYyNM2pQvHtmTt
4 | ZE8oRPLzOiASnpQYeaT1EqnxranHRQ0a080LklzcvV2DDUkEzrQdhvmsCUnFHl3j
5 | bdQ96oGWYNqdQFqrcfhVl+kPM/ErSe/wtHwJgo4UhC/GFWLN/rB2tzH3OIvMnq+F
6 | Rd4AxSNOrHGGFtsds6IcbznOyoxwKMiAjqOjb52ph+0ia1YTSJy4GjTPXXaa4ucg
7 | kIaJoGXNZ25da04N3dI3WD6rE/EUByhYveCz41bUG1WHAf5D99GaXY1a1l96EW0B
8 | yQIDAQAB
9 | -----END PUBLIC KEY-----
10 |
--------------------------------------------------------------------------------
/server/users.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/appleboy/gin-jwt/v2"
9 | "github.com/gin-gonic/gin"
10 | "github.com/jinzhu/gorm"
11 | )
12 |
13 | // Credentials is the json object for credentials
14 | type Credentials struct {
15 | Username string `json:"username"`
16 | Password string `json:"password"`
17 | }
18 |
19 | // User represents a user
20 | type User struct {
21 | gorm.Model
22 | Username string `form:"username" json:"username" binding:"required"`
23 | Notes []*Note `json:"-" gorm:"many2many:note_shared;"`
24 | Groups []*Group `json:"-" gorm:"many2many:group_users;"`
25 | }
26 |
27 | // Auth contains password
28 | type Auth struct {
29 | gorm.Model
30 | UserID int
31 | User User `gorm:"foreignkey:UserID"`
32 | Password string
33 | }
34 |
35 | // Group represents a group
36 | type Group struct {
37 | gorm.Model
38 | Name string `form:"name" json:"name" binding:"required"`
39 | Users []*User `json:"users" gorm:"many2many:group_users;"`
40 | Notes []*Note `gorm:"many2many:note_groups;"`
41 | }
42 |
43 | func (e *Env) getUser(username string) (*User, error) {
44 | var login User
45 | err := e.db.Where("username = ?", username).First(&login).Error
46 | return &login, err
47 | }
48 |
49 | func (e *Env) getUsers(username []string) ([]*User, error) {
50 | var logins []*User
51 | err := e.db.Where("username IN (?)", username).Find(&logins).Error
52 | return logins, err
53 | }
54 |
55 | func (e *Env) changePassword(username string, json Credentials) error {
56 | var login Auth
57 | if username != json.Username {
58 | return (errors.New("username does not match"))
59 | }
60 | e.db.Where("username = ?", username).First(&login)
61 | login.Password = json.Password
62 | return e.db.Save(&login).Error
63 | }
64 |
65 | // create a new account, or update the password of an existing account
66 | func (e *Env) subscribeHandler(c *gin.Context) {
67 | var login Credentials
68 | if err := c.ShouldBindJSON(&login); err != nil {
69 | c.JSON(http.StatusBadRequest, gin.H{"err": err}) // SECURITY
70 | return
71 | }
72 | // case: user is already logged in, password update
73 | // TODO: move to jwt.go helper function
74 | claims := jwt.ExtractClaims(c)
75 | _, exists := c.Get(identityKey)
76 | if exists {
77 | err := e.changePassword(claims["id"].(string), login)
78 | if err != nil {
79 | c.JSON(http.StatusInternalServerError, gin.H{"err": err}) // SECURITY
80 | return
81 | }
82 | c.JSON(http.StatusOK, gin.H{"status": "password changed"})
83 | return
84 | }
85 | // case: user is not logged in, create a new account
86 | var user User
87 | err := e.db.Table("users").
88 | Where("username = ? and password = ?", login.Username, login.Password).
89 | Joins("JOIN auths ON users.id = user_id").First(&user).Error
90 | if err == nil {
91 | c.JSON(http.StatusUnauthorized, gin.H{"err": "user exists"})
92 | return
93 | }
94 | var query = Auth{Password: login.Password, User: User{Username: login.Username}}
95 | err = e.db.Save(&query).Error
96 | if err != nil {
97 | c.JSON(http.StatusInternalServerError, gin.H{"err": err}) // SECURITY
98 | return
99 | }
100 | c.JSON(http.StatusOK, gin.H{"status": "user created", "userID": query.ID})
101 | }
102 |
103 | func (e *Env) userListHandler(c *gin.Context) {
104 | var usernames []string
105 | search := c.Query("search")
106 |
107 | req := e.db.Model(&User{}).
108 | Where("deleted_at IS NULL")
109 |
110 | if len(search) > 0 {
111 | req = req.Where("username LIKE ?",
112 | fmt.Sprintf("%%%s%%", search))
113 | }
114 |
115 | err := req.
116 | Order("created_at").
117 | Pluck("username", &usernames).Error
118 |
119 | if err != nil {
120 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
121 | return
122 | }
123 | c.JSON(http.StatusOK, gin.H{"users": usernames})
124 | }
125 |
126 | // CreateGroupRequest format of create request
127 | type CreateGroupRequest struct {
128 | Name string
129 | Users []string
130 | }
131 |
132 | func (e *Env) groupCreateHandler(c *gin.Context) {
133 | var request CreateGroupRequest
134 | err := c.ShouldBindJSON(&request)
135 | if err != nil {
136 | c.JSON(http.StatusBadRequest, gin.H{"err": err}) // SECURITY
137 | return
138 | }
139 | users, err := e.getUsers(request.Users)
140 | if err != nil {
141 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
142 | return
143 | }
144 | group := &Group{
145 | Name: request.Name,
146 | Users: users,
147 | }
148 | err = e.db.Save(group).Error
149 | if err != nil {
150 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
151 | return
152 | }
153 | c.JSON(http.StatusOK, gin.H{"id": group.ID})
154 | }
155 |
156 | func (e *Env) groupGetHandler(c *gin.Context) {
157 | var group Group
158 | ID := c.Param("id")
159 | // owner := getOwner(c)
160 | err := e.db.Where("id = ?", ID).First(&group).Error
161 | if err != nil {
162 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
163 | return
164 | }
165 | err = e.db.Model(&group).Related(&group.Users, "Users").Error
166 | if err != nil {
167 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
168 | return
169 | }
170 | c.JSON(http.StatusOK, gin.H{"group": group})
171 | }
172 |
173 | // GroupEditRequest format of edit request
174 | type GroupEditRequest struct {
175 | Name string
176 | Users []string
177 | }
178 |
179 | func (e *Env) groupEditHandler(c *gin.Context) {
180 | var request GroupEditRequest
181 | err := c.ShouldBindJSON(&request)
182 | ID := c.Param("id")
183 | // owner := getOwner(c)
184 | var group Group
185 | err = e.db.Where("id = ?", ID).First(&group).Error
186 | if err != nil {
187 | c.JSON(http.StatusUnauthorized, gin.H{"err": err})
188 | return
189 | }
190 | group.Name = request.Name
191 | err = e.db.Save(&group).Error
192 | if err != nil {
193 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
194 | return
195 | }
196 | users, err := e.getUsers(request.Users)
197 | if err != nil {
198 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
199 | return
200 | }
201 | err = e.db.Model(&group).Association("Users").Replace(users).Error
202 | if err != nil {
203 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
204 | return
205 | }
206 | c.JSON(http.StatusOK, gin.H{})
207 | }
208 |
209 | func (e *Env) groupListHandler(c *gin.Context) {
210 | owner, err := e.getUser(getOwner(c))
211 | err = e.db.Preload("Users").Model(&owner).Related(&owner.Groups, "Groups").Error
212 | if err != nil {
213 | c.JSON(http.StatusInternalServerError, gin.H{"err": err})
214 | return
215 | }
216 | c.JSON(http.StatusOK, gin.H{"groups": owner.Groups})
217 | }
218 |
--------------------------------------------------------------------------------
/skaffold.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: skaffold/v1beta8
2 | kind: Config
3 | build:
4 | artifacts:
5 | - image: note-client
6 | context: client
7 | - image: note-server
8 | context: server
9 | tagPolicy:
10 | sha256: {}
11 | deploy:
12 | kubectl:
13 | manifests:
14 | - k8s/*.yaml
15 |
--------------------------------------------------------------------------------