Browse Source

🔥 Remove old frontend (#649)

pull/13907/head
Sebastián Ramírez 1 year ago
committed by GitHub
parent
commit
458d712d44
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .env
  2. 9
      docker-compose.override.yml
  3. 26
      docker-compose.yml
  4. 1
      frontend/.dockerignore
  5. 9
      frontend/.env
  6. 21
      frontend/.gitignore
  7. 1
      frontend/.nvmrc
  8. 30
      frontend/Dockerfile
  9. 46
      frontend/README.md
  10. 10
      frontend/babel.config.js
  11. 9
      frontend/nginx-backend-not-found.conf
  12. 11
      frontend/nginx.conf
  13. 74
      frontend/package.json
  14. BIN
      frontend/public/favicon.ico
  15. BIN
      frontend/public/img/icons/android-chrome-192x192.png
  16. BIN
      frontend/public/img/icons/android-chrome-512x512.png
  17. BIN
      frontend/public/img/icons/apple-touch-icon-120x120.png
  18. BIN
      frontend/public/img/icons/apple-touch-icon-152x152.png
  19. BIN
      frontend/public/img/icons/apple-touch-icon-180x180.png
  20. BIN
      frontend/public/img/icons/apple-touch-icon-60x60.png
  21. BIN
      frontend/public/img/icons/apple-touch-icon-76x76.png
  22. BIN
      frontend/public/img/icons/apple-touch-icon.png
  23. BIN
      frontend/public/img/icons/favicon-16x16.png
  24. BIN
      frontend/public/img/icons/favicon-32x32.png
  25. BIN
      frontend/public/img/icons/msapplication-icon-144x144.png
  26. BIN
      frontend/public/img/icons/mstile-150x150.png
  27. 149
      frontend/public/img/icons/safari-pinned-tab.svg
  28. 21
      frontend/public/index.html
  29. 20
      frontend/public/manifest.json
  30. 2
      frontend/public/robots.txt
  31. 43
      frontend/src/App.vue
  32. 45
      frontend/src/api.ts
  33. BIN
      frontend/src/assets/logo.png
  34. 8
      frontend/src/component-hooks.ts
  35. 77
      frontend/src/components/NotificationsManager.vue
  36. 11
      frontend/src/components/RouterComponent.vue
  37. 34
      frontend/src/components/UploadButton.vue
  38. 14
      frontend/src/env.ts
  39. 23
      frontend/src/interfaces/index.ts
  40. 19
      frontend/src/main.ts
  41. 4
      frontend/src/plugins/vee-validate.ts
  42. 6
      frontend/src/plugins/vuetify.ts
  43. 26
      frontend/src/registerServiceWorker.ts
  44. 97
      frontend/src/router.ts
  45. 13
      frontend/src/shims-tsx.d.ts
  46. 4
      frontend/src/shims-vue.d.ts
  47. 60
      frontend/src/store/admin/actions.ts
  48. 18
      frontend/src/store/admin/getters.ts
  49. 15
      frontend/src/store/admin/index.ts
  50. 20
      frontend/src/store/admin/mutations.ts
  51. 5
      frontend/src/store/admin/state.ts
  52. 19
      frontend/src/store/index.ts
  53. 173
      frontend/src/store/main/actions.ts
  54. 29
      frontend/src/store/main/getters.ts
  55. 21
      frontend/src/store/main/index.ts
  56. 43
      frontend/src/store/main/mutations.ts
  57. 17
      frontend/src/store/main/state.ts
  58. 5
      frontend/src/store/state.ts
  59. 5
      frontend/src/utils.ts
  60. 58
      frontend/src/views/Login.vue
  61. 52
      frontend/src/views/PasswordRecovery.vue
  62. 84
      frontend/src/views/ResetPassword.vue
  63. 37
      frontend/src/views/main/Dashboard.vue
  64. 182
      frontend/src/views/main/Main.vue
  65. 38
      frontend/src/views/main/Start.vue
  66. 28
      frontend/src/views/main/admin/Admin.vue
  67. 83
      frontend/src/views/main/admin/AdminUsers.vue
  68. 97
      frontend/src/views/main/admin/CreateUser.vue
  69. 163
      frontend/src/views/main/admin/EditUser.vue
  70. 46
      frontend/src/views/main/profile/UserProfile.vue
  71. 97
      frontend/src/views/main/profile/UserProfileEdit.vue
  72. 86
      frontend/src/views/main/profile/UserProfileEditPassword.vue
  73. 15
      frontend/tests/unit/upload-button.spec.ts
  74. 41
      frontend/tsconfig.json
  75. 19
      frontend/tslint.json
  76. 35
      frontend/vue.config.js

1
.env

@ -47,4 +47,3 @@ TRAEFIK_PUBLIC_TAG=traefik-public
DOCKER_IMAGE_BACKEND=backend
DOCKER_IMAGE_CELERYWORKER=celery
DOCKER_IMAGE_FRONTEND=frontend
DOCKER_IMAGE_NEW_FRONTEND=new-frontend

9
docker-compose.override.yml

@ -72,15 +72,6 @@ services:
args:
INSTALL_DEV: ${INSTALL_DEV-true}
frontend:
build:
context: ./frontend
args:
FRONTEND_ENV: dev
labels:
# - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`)
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`old-frontend.localhost.tiangolo.com`)
networks:
traefik-public:
# For local dev, don't expect an external Traefik network

26
docker-compose.yml

@ -2,7 +2,7 @@ version: "3.3"
services:
proxy:
image: traefik:v2.2
image: traefik:v2.3
networks:
- ${TRAEFIK_PUBLIC_NETWORK?Variable not set}
- default
@ -167,24 +167,12 @@ services:
frontend:
image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${TAG-latest}'
build:
context: ./frontend
args:
FRONTEND_ENV: ${FRONTEND_ENV-production}
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`)
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
# new-frontend:
# image: '${DOCKER_IMAGE_NEW_FRONTEND?Variable not set}:${TAG-latest}'
# build:
# context: ./new-frontend
# labels:
# - traefik.enable=true
# - traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
# - traefik.http.routers.${STACK_NAME?Variable not set}-new-frontend-http.rule=PathPrefix(`/`)
# - traefik.http.services.${STACK_NAME?Variable not set}-new-frontend.loadbalancer.server.port=80
context: ./new-frontend
labels:
- traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`)
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
volumes:
app-db-data:

1
frontend/.dockerignore

@ -1 +0,0 @@
node_modules

9
frontend/.env

@ -1,9 +0,0 @@
VUE_APP_DOMAIN_DEV=localhost
# VUE_APP_DOMAIN_DEV=local.dockertoolbox.tiangolo.com
# VUE_APP_DOMAIN_DEV=localhost.tiangolo.com
VUE_APP_DOMAIN_STAG=
VUE_APP_DOMAIN_PROD=
VUE_APP_NAME=
VUE_APP_ENV=development
# VUE_APP_ENV=staging
# VUE_APP_ENV=production

21
frontend/.gitignore

@ -1,21 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

1
frontend/.nvmrc

@ -1 +0,0 @@
18.12.1

30
frontend/Dockerfile

@ -1,30 +0,0 @@
# Stage 0, "build-stage", based on Node.js, to build and compile the frontend
FROM node:18.12.1 as build-stage
WORKDIR /app
COPY package*.json /app/
RUN npm install
COPY ./ /app/
ARG FRONTEND_ENV=production
ENV VUE_APP_ENV=${FRONTEND_ENV}
ENV NODE_OPTIONS="--openssl-legacy-provider"
# Comment out the next line to disable tests
RUN npm run test:unit
RUN npm run build
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
FROM nginx:1.15
COPY --from=build-stage /app/dist/ /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY ./nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf

46
frontend/README.md

@ -1,46 +0,0 @@
# frontend
## Node Requirements
You can use either [fnm](https://github.com/Schniz/fnm) or [nvm](https://github.com/nvm-sh/nvm) to manage your Node.js versions.
### Using nvm
If you prefer nvm, run the following command to install the recommended Node.js version:
```
nvm install
```
### Using fnm
If you prefer fnm, run the following command to install the recommended Node.js version:
```
fnm install
```
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your tests
```
npm run test
```
### Lints and fixes files
```
npm run lint
```
### Run your unit tests
```
npm run test:unit
```

10
frontend/babel.config.js

@ -1,10 +0,0 @@
module.exports = {
"presets": [
[
"@vue/cli-plugin-babel/preset",
{
"useBuiltIns": "entry"
}
]
]
}

9
frontend/nginx-backend-not-found.conf

@ -1,9 +0,0 @@
location /api {
return 404;
}
location /docs {
return 404;
}
location /redoc {
return 404;
}

11
frontend/nginx.conf

@ -1,11 +0,0 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
include /etc/nginx/extra-conf.d/*.conf;
}

74
frontend/package.json

@ -1,74 +0,0 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@babel/polyfill": "^7.2.5",
"axios": "^0.18.0",
"core-js": "^3.4.3",
"register-service-worker": "^1.0.0",
"typesafe-vuex": "^3.1.1",
"vee-validate": "^2.1.7",
"vue": "^2.5.22",
"vue-class-component": "^6.0.0",
"vue-property-decorator": "^7.3.0",
"vue-router": "^3.0.2",
"vuetify": "^1.4.4",
"vuex": "^3.1.0"
},
"devDependencies": {
"@types/jest": "^23.3.13",
"@vue/cli-plugin-babel": "^4.1.1",
"@vue/cli-plugin-pwa": "^4.1.1",
"@vue/cli-plugin-typescript": "^4.1.1",
"@vue/cli-plugin-unit-jest": "^4.1.1",
"@vue/cli-service": "^4.1.1",
"@vue/test-utils": "^1.0.0-beta.28",
"babel-core": "7.0.0-bridge.0",
"ts-jest": "^23.10.5",
"typescript": "^3.2.4",
"vue-cli-plugin-vuetify": "^2.0.2",
"vue-template-compiler": "^2.5.22"
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"jest": {
"moduleFileExtensions": [
"js",
"jsx",
"json",
"vue",
"ts",
"tsx"
],
"transform": {
"^.+\\.vue$": "vue-jest",
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
"^.+\\.tsx?$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"snapshotSerializers": [
"jest-serializer-vue"
],
"testMatch": [
"**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
],
"testURL": "http://localhost/"
}
}

BIN
frontend/public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
frontend/public/img/icons/android-chrome-192x192.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

BIN
frontend/public/img/icons/android-chrome-512x512.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

BIN
frontend/public/img/icons/apple-touch-icon-120x120.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

BIN
frontend/public/img/icons/apple-touch-icon-152x152.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

BIN
frontend/public/img/icons/apple-touch-icon-180x180.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

BIN
frontend/public/img/icons/apple-touch-icon-60x60.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

BIN
frontend/public/img/icons/apple-touch-icon-76x76.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

BIN
frontend/public/img/icons/apple-touch-icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

BIN
frontend/public/img/icons/favicon-16x16.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

BIN
frontend/public/img/icons/favicon-32x32.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
frontend/public/img/icons/msapplication-icon-144x144.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
frontend/public/img/icons/mstile-150x150.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

149
frontend/public/img/icons/safari-pinned-tab.svg

@ -1,149 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
fill="#000000" stroke="none">
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
-9615 0 20 -32z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

21
frontend/public/index.html

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= VUE_APP_NAME %></title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
</head>
<body>
<noscript>
<strong>We're sorry but <%= VUE_APP_NAME %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

20
frontend/public/manifest.json

@ -1,20 +0,0 @@
{
"name": "frontend",
"short_name": "frontend",
"icons": [
{
"src": "/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}

2
frontend/public/robots.txt

@ -1,2 +0,0 @@
User-agent: *
Disallow:

43
frontend/src/App.vue

@ -1,43 +0,0 @@
<template>
<div id="app">
<v-app>
<v-content v-if="loggedIn===null">
<v-container fill-height>
<v-layout align-center justify-center>
<v-flex>
<div class="text-xs-center">
<div class="headline my-5">Loading...</div>
<v-progress-circular size="100" indeterminate color="primary"></v-progress-circular>
</div>
</v-flex>
</v-layout>
</v-container>
</v-content>
<router-view v-else />
<NotificationsManager></NotificationsManager>
</v-app>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import NotificationsManager from '@/components/NotificationsManager.vue';
import { readIsLoggedIn } from '@/store/main/getters';
import { dispatchCheckLoggedIn } from '@/store/main/actions';
@Component({
components: {
NotificationsManager,
},
})
export default class App extends Vue {
get loggedIn() {
return readIsLoggedIn(this.$store);
}
public async created() {
await dispatchCheckLoggedIn(this.$store);
}
}
</script>

45
frontend/src/api.ts

@ -1,45 +0,0 @@
import axios from 'axios';
import { apiUrl } from '@/env';
import { IUserProfile, IUserProfileUpdate, IUserProfileCreate } from './interfaces';
function authHeaders(token: string) {
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
export const api = {
async logInGetToken(username: string, password: string) {
const params = new URLSearchParams();
params.append('username', username);
params.append('password', password);
return axios.post(`${apiUrl}/api/v1/login/access-token`, params);
},
async getMe(token: string) {
return axios.get<IUserProfile>(`${apiUrl}/api/v1/users/me`, authHeaders(token));
},
async updateMe(token: string, data: IUserProfileUpdate) {
return axios.put<IUserProfile>(`${apiUrl}/api/v1/users/me`, data, authHeaders(token));
},
async getUsers(token: string) {
return axios.get<IUserProfile[]>(`${apiUrl}/api/v1/users/`, authHeaders(token));
},
async updateUser(token: string, userId: number, data: IUserProfileUpdate) {
return axios.put(`${apiUrl}/api/v1/users/${userId}`, data, authHeaders(token));
},
async createUser(token: string, data: IUserProfileCreate) {
return axios.post(`${apiUrl}/api/v1/users/`, data, authHeaders(token));
},
async passwordRecovery(email: string) {
return axios.post(`${apiUrl}/api/v1/password-recovery/${email}`);
},
async resetPassword(password: string, token: string) {
return axios.post(`${apiUrl}/api/v1/reset-password/`, {
new_password: password,
token,
});
},
};

BIN
frontend/src/assets/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

8
frontend/src/component-hooks.ts

@ -1,8 +0,0 @@
import Component from 'vue-class-component';
// Register the router hooks with their names
Component.registerHooks([
'beforeRouteEnter',
'beforeRouteLeave',
'beforeRouteUpdate', // for vue-router 2.2+
]);

77
frontend/src/components/NotificationsManager.vue

@ -1,77 +0,0 @@
<template>
<div>
<v-snackbar auto-height :color="currentNotificationColor" v-model="show">
<v-progress-circular class="ma-2" indeterminate v-show="showProgress"></v-progress-circular>{{ currentNotificationContent }}
<v-btn flat @click.native="close">Close</v-btn>
</v-snackbar>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
import { AppNotification } from '@/store/main/state';
import { commitRemoveNotification } from '@/store/main/mutations';
import { readFirstNotification } from '@/store/main/getters';
import { dispatchRemoveNotification } from '@/store/main/actions';
@Component
export default class NotificationsManager extends Vue {
public show: boolean = false;
public text: string = '';
public showProgress: boolean = false;
public currentNotification: AppNotification | false = false;
public async hide() {
this.show = false;
await new Promise((resolve, reject) => setTimeout(() => resolve(), 500));
}
public async close() {
await this.hide();
await this.removeCurrentNotification();
}
public async removeCurrentNotification() {
if (this.currentNotification) {
commitRemoveNotification(this.$store, this.currentNotification);
}
}
public get firstNotification() {
return readFirstNotification(this.$store);
}
public async setNotification(notification: AppNotification | false) {
if (this.show) {
await this.hide();
}
if (notification) {
this.currentNotification = notification;
this.showProgress = notification.showProgress || false;
this.show = true;
} else {
this.currentNotification = false;
}
}
@Watch('firstNotification')
public async onNotificationChange(
newNotification: AppNotification | false,
oldNotification: AppNotification | false,
) {
if (newNotification !== this.currentNotification) {
await this.setNotification(newNotification);
if (newNotification) {
dispatchRemoveNotification(this.$store, { notification: newNotification, timeout: 6500 });
}
}
}
public get currentNotificationContent() {
return this.currentNotification && this.currentNotification.content || '';
}
public get currentNotificationColor() {
return this.currentNotification && this.currentNotification.color || 'info';
}
}
</script>

11
frontend/src/components/RouterComponent.vue

@ -1,11 +0,0 @@
<template>
<router-view></router-view>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class RouterComponent extends Vue {
}
</script>

34
frontend/src/components/UploadButton.vue

@ -1,34 +0,0 @@
<template>
<div>
<v-btn :color="color" @click="trigger"><slot>Choose File</slot></v-btn>
<input :multiple="multiple" class="visually-hidden" type="file" v-on:change="files" ref="fileInput">
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from 'vue-property-decorator';
@Component
export default class UploadButton extends Vue {
@Prop(String) public color: string | undefined;
@Prop({default: false}) public multiple!: boolean;
@Emit()
public files(e): FileList {
return e.target.files;
}
public trigger() {
(this.$refs.fileInput as HTMLElement).click();
}
}
</script>
<style scoped>
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
</style>

14
frontend/src/env.ts

@ -1,14 +0,0 @@
const env = process.env.VUE_APP_ENV;
let envApiUrl = '';
if (env === 'production') {
envApiUrl = `https://${process.env.VUE_APP_DOMAIN_PROD}`;
} else if (env === 'staging') {
envApiUrl = `https://${process.env.VUE_APP_DOMAIN_STAG}`;
} else {
envApiUrl = `http://${process.env.VUE_APP_DOMAIN_DEV}`;
}
export const apiUrl = envApiUrl;
export const appName = process.env.VUE_APP_NAME;

23
frontend/src/interfaces/index.ts

@ -1,23 +0,0 @@
export interface IUserProfile {
email: string;
is_active: boolean;
is_superuser: boolean;
full_name: string;
id: number;
}
export interface IUserProfileUpdate {
email?: string;
full_name?: string;
password?: string;
is_active?: boolean;
is_superuser?: boolean;
}
export interface IUserProfileCreate {
email: string;
full_name?: string;
password?: string;
is_active?: boolean;
is_superuser?: boolean;
}

19
frontend/src/main.ts

@ -1,19 +0,0 @@
import '@babel/polyfill';
// Import Component hooks before component definitions
import './component-hooks';
import Vue from 'vue';
import './plugins/vuetify';
import './plugins/vee-validate';
import App from './App.vue';
import router from './router';
import store from '@/store';
import './registerServiceWorker';
import 'vuetify/dist/vuetify.min.css';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');

4
frontend/src/plugins/vee-validate.ts

@ -1,4 +0,0 @@
import Vue from 'vue';
import VeeValidate from 'vee-validate';
Vue.use(VeeValidate);

6
frontend/src/plugins/vuetify.ts

@ -1,6 +0,0 @@
import Vue from 'vue';
import Vuetify from 'vuetify';
Vue.use(Vuetify, {
iconfont: 'md',
});

26
frontend/src/registerServiceWorker.ts

@ -1,26 +0,0 @@
/* tslint:disable:no-console */
import { register } from 'register-service-worker';
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB',
);
},
cached() {
console.log('Content has been cached for offline use.');
},
updated() {
console.log('New content is available; please refresh.');
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
console.error('Error during service worker registration:', error);
},
});
}

97
frontend/src/router.ts

@ -1,97 +0,0 @@
import Vue from 'vue';
import Router from 'vue-router';
import RouterComponent from './components/RouterComponent.vue';
Vue.use(Router);
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
component: () => import(/* webpackChunkName: "start" */ './views/main/Start.vue'),
children: [
{
path: 'login',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "login" */ './views/Login.vue'),
},
{
path: 'recover-password',
component: () => import(/* webpackChunkName: "recover-password" */ './views/PasswordRecovery.vue'),
},
{
path: 'reset-password',
component: () => import(/* webpackChunkName: "reset-password" */ './views/ResetPassword.vue'),
},
{
path: 'main',
component: () => import(/* webpackChunkName: "main" */ './views/main/Main.vue'),
children: [
{
path: 'dashboard',
component: () => import(/* webpackChunkName: "main-dashboard" */ './views/main/Dashboard.vue'),
},
{
path: 'profile',
component: RouterComponent,
redirect: 'profile/view',
children: [
{
path: 'view',
component: () => import(
/* webpackChunkName: "main-profile" */ './views/main/profile/UserProfile.vue'),
},
{
path: 'edit',
component: () => import(
/* webpackChunkName: "main-profile-edit" */ './views/main/profile/UserProfileEdit.vue'),
},
{
path: 'password',
component: () => import(
/* webpackChunkName: "main-profile-password" */ './views/main/profile/UserProfileEditPassword.vue'),
},
],
},
{
path: 'admin',
component: () => import(/* webpackChunkName: "main-admin" */ './views/main/admin/Admin.vue'),
redirect: 'admin/users/all',
children: [
{
path: 'users',
redirect: 'users/all',
},
{
path: 'users/all',
component: () => import(
/* webpackChunkName: "main-admin-users" */ './views/main/admin/AdminUsers.vue'),
},
{
path: 'users/edit/:id',
name: 'main-admin-users-edit',
component: () => import(
/* webpackChunkName: "main-admin-users-edit" */ './views/main/admin/EditUser.vue'),
},
{
path: 'users/create',
name: 'main-admin-users-create',
component: () => import(
/* webpackChunkName: "main-admin-users-create" */ './views/main/admin/CreateUser.vue'),
},
],
},
],
},
],
},
{
path: '/*', redirect: '/',
},
],
});

13
frontend/src/shims-tsx.d.ts

@ -1,13 +0,0 @@
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

4
frontend/src/shims-vue.d.ts

@ -1,4 +0,0 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

60
frontend/src/store/admin/actions.ts

@ -1,60 +0,0 @@
import { api } from '@/api';
import { ActionContext } from 'vuex';
import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces';
import { State } from '../state';
import { AdminState } from './state';
import { getStoreAccessors } from 'typesafe-vuex';
import { commitSetUsers, commitSetUser } from './mutations';
import { dispatchCheckApiError } from '../main/actions';
import { commitAddNotification, commitRemoveNotification } from '../main/mutations';
type MainContext = ActionContext<AdminState, State>;
export const actions = {
async actionGetUsers(context: MainContext) {
try {
const response = await api.getUsers(context.rootState.main.token);
if (response) {
commitSetUsers(context, response.data);
}
} catch (error) {
await dispatchCheckApiError(context, error);
}
},
async actionUpdateUser(context: MainContext, payload: { id: number, user: IUserProfileUpdate }) {
try {
const loadingNotification = { content: 'saving', showProgress: true };
commitAddNotification(context, loadingNotification);
const response = (await Promise.all([
api.updateUser(context.rootState.main.token, payload.id, payload.user),
await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
]))[0];
commitSetUser(context, response.data);
commitRemoveNotification(context, loadingNotification);
commitAddNotification(context, { content: 'User successfully updated', color: 'success' });
} catch (error) {
await dispatchCheckApiError(context, error);
}
},
async actionCreateUser(context: MainContext, payload: IUserProfileCreate) {
try {
const loadingNotification = { content: 'saving', showProgress: true };
commitAddNotification(context, loadingNotification);
const response = (await Promise.all([
api.createUser(context.rootState.main.token, payload),
await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
]))[0];
commitSetUser(context, response.data);
commitRemoveNotification(context, loadingNotification);
commitAddNotification(context, { content: 'User successfully created', color: 'success' });
} catch (error) {
await dispatchCheckApiError(context, error);
}
},
};
const { dispatch } = getStoreAccessors<AdminState, State>('');
export const dispatchCreateUser = dispatch(actions.actionCreateUser);
export const dispatchGetUsers = dispatch(actions.actionGetUsers);
export const dispatchUpdateUser = dispatch(actions.actionUpdateUser);

18
frontend/src/store/admin/getters.ts

@ -1,18 +0,0 @@
import { AdminState } from './state';
import { getStoreAccessors } from 'typesafe-vuex';
import { State } from '../state';
export const getters = {
adminUsers: (state: AdminState) => state.users,
adminOneUser: (state: AdminState) => (userId: number) => {
const filteredUsers = state.users.filter((user) => user.id === userId);
if (filteredUsers.length > 0) {
return { ...filteredUsers[0] };
}
},
};
const { read } = getStoreAccessors<AdminState, State>('');
export const readAdminOneUser = read(getters.adminOneUser);
export const readAdminUsers = read(getters.adminUsers);

15
frontend/src/store/admin/index.ts

@ -1,15 +0,0 @@
import { mutations } from './mutations';
import { getters } from './getters';
import { actions } from './actions';
import { AdminState } from './state';
const defaultState: AdminState = {
users: [],
};
export const adminModule = {
state: defaultState,
mutations,
actions,
getters,
};

20
frontend/src/store/admin/mutations.ts

@ -1,20 +0,0 @@
import { IUserProfile } from '@/interfaces';
import { AdminState } from './state';
import { getStoreAccessors } from 'typesafe-vuex';
import { State } from '../state';
export const mutations = {
setUsers(state: AdminState, payload: IUserProfile[]) {
state.users = payload;
},
setUser(state: AdminState, payload: IUserProfile) {
const users = state.users.filter((user: IUserProfile) => user.id !== payload.id);
users.push(payload);
state.users = users;
},
};
const { commit } = getStoreAccessors<AdminState, State>('');
export const commitSetUser = commit(mutations.setUser);
export const commitSetUsers = commit(mutations.setUsers);

5
frontend/src/store/admin/state.ts

@ -1,5 +0,0 @@
import { IUserProfile } from '@/interfaces';
export interface AdminState {
users: IUserProfile[];
}

19
frontend/src/store/index.ts

@ -1,19 +0,0 @@
import Vue from 'vue';
import Vuex, { StoreOptions } from 'vuex';
import { mainModule } from './main';
import { State } from './state';
import { adminModule } from './admin';
Vue.use(Vuex);
const storeOptions: StoreOptions<State> = {
modules: {
main: mainModule,
admin: adminModule,
},
};
export const store = new Vuex.Store<State>(storeOptions);
export default store;

173
frontend/src/store/main/actions.ts

@ -1,173 +0,0 @@
import { api } from '@/api';
import router from '@/router';
import { getLocalToken, removeLocalToken, saveLocalToken } from '@/utils';
import { AxiosError } from 'axios';
import { getStoreAccessors } from 'typesafe-vuex';
import { ActionContext } from 'vuex';
import { State } from '../state';
import {
commitAddNotification,
commitRemoveNotification,
commitSetLoggedIn,
commitSetLogInError,
commitSetToken,
commitSetUserProfile,
} from './mutations';
import { AppNotification, MainState } from './state';
type MainContext = ActionContext<MainState, State>;
export const actions = {
async actionLogIn(context: MainContext, payload: { username: string; password: string }) {
try {
const response = await api.logInGetToken(payload.username, payload.password);
const token = response.data.access_token;
if (token) {
saveLocalToken(token);
commitSetToken(context, token);
commitSetLoggedIn(context, true);
commitSetLogInError(context, false);
await dispatchGetUserProfile(context);
await dispatchRouteLoggedIn(context);
commitAddNotification(context, { content: 'Logged in', color: 'success' });
} else {
await dispatchLogOut(context);
}
} catch (err) {
commitSetLogInError(context, true);
await dispatchLogOut(context);
}
},
async actionGetUserProfile(context: MainContext) {
try {
const response = await api.getMe(context.state.token);
if (response.data) {
commitSetUserProfile(context, response.data);
}
} catch (error) {
await dispatchCheckApiError(context, error);
}
},
async actionUpdateUserProfile(context: MainContext, payload) {
try {
const loadingNotification = { content: 'saving', showProgress: true };
commitAddNotification(context, loadingNotification);
const response = (await Promise.all([
api.updateMe(context.state.token, payload),
await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
]))[0];
commitSetUserProfile(context, response.data);
commitRemoveNotification(context, loadingNotification);
commitAddNotification(context, { content: 'Profile successfully updated', color: 'success' });
} catch (error) {
await dispatchCheckApiError(context, error);
}
},
async actionCheckLoggedIn(context: MainContext) {
if (!context.state.isLoggedIn) {
let token = context.state.token;
if (!token) {
const localToken = getLocalToken();
if (localToken) {
commitSetToken(context, localToken);
token = localToken;
}
}
if (token) {
try {
const response = await api.getMe(token);
commitSetLoggedIn(context, true);
commitSetUserProfile(context, response.data);
} catch (error) {
await dispatchRemoveLogIn(context);
}
} else {
await dispatchRemoveLogIn(context);
}
}
},
async actionRemoveLogIn(context: MainContext) {
removeLocalToken();
commitSetToken(context, '');
commitSetLoggedIn(context, false);
},
async actionLogOut(context: MainContext) {
await dispatchRemoveLogIn(context);
await dispatchRouteLogOut(context);
},
async actionUserLogOut(context: MainContext) {
await dispatchLogOut(context);
commitAddNotification(context, { content: 'Logged out', color: 'success' });
},
actionRouteLogOut(context: MainContext) {
if (router.currentRoute.path !== '/login') {
router.push('/login');
}
},
async actionCheckApiError(context: MainContext, payload: AxiosError) {
if (payload.response!.status === 401) {
await dispatchLogOut(context);
}
},
actionRouteLoggedIn(context: MainContext) {
if (router.currentRoute.path === '/login' || router.currentRoute.path === '/') {
router.push('/main');
}
},
async removeNotification(context: MainContext, payload: { notification: AppNotification, timeout: number }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commitRemoveNotification(context, payload.notification);
resolve(true);
}, payload.timeout);
});
},
async passwordRecovery(context: MainContext, payload: { username: string }) {
const loadingNotification = { content: 'Sending password recovery email', showProgress: true };
try {
commitAddNotification(context, loadingNotification);
const response = (await Promise.all([
api.passwordRecovery(payload.username),
await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
]))[0];
commitRemoveNotification(context, loadingNotification);
commitAddNotification(context, { content: 'Password recovery email sent', color: 'success' });
await dispatchLogOut(context);
} catch (error) {
commitRemoveNotification(context, loadingNotification);
commitAddNotification(context, { color: 'error', content: 'Incorrect username' });
}
},
async resetPassword(context: MainContext, payload: { password: string, token: string }) {
const loadingNotification = { content: 'Resetting password', showProgress: true };
try {
commitAddNotification(context, loadingNotification);
const response = (await Promise.all([
api.resetPassword(payload.password, payload.token),
await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
]))[0];
commitRemoveNotification(context, loadingNotification);
commitAddNotification(context, { content: 'Password successfully reset', color: 'success' });
await dispatchLogOut(context);
} catch (error) {
commitRemoveNotification(context, loadingNotification);
commitAddNotification(context, { color: 'error', content: 'Error resetting password' });
}
},
};
const { dispatch } = getStoreAccessors<MainState | any, State>('');
export const dispatchCheckApiError = dispatch(actions.actionCheckApiError);
export const dispatchCheckLoggedIn = dispatch(actions.actionCheckLoggedIn);
export const dispatchGetUserProfile = dispatch(actions.actionGetUserProfile);
export const dispatchLogIn = dispatch(actions.actionLogIn);
export const dispatchLogOut = dispatch(actions.actionLogOut);
export const dispatchUserLogOut = dispatch(actions.actionUserLogOut);
export const dispatchRemoveLogIn = dispatch(actions.actionRemoveLogIn);
export const dispatchRouteLoggedIn = dispatch(actions.actionRouteLoggedIn);
export const dispatchRouteLogOut = dispatch(actions.actionRouteLogOut);
export const dispatchUpdateUserProfile = dispatch(actions.actionUpdateUserProfile);
export const dispatchRemoveNotification = dispatch(actions.removeNotification);
export const dispatchPasswordRecovery = dispatch(actions.passwordRecovery);
export const dispatchResetPassword = dispatch(actions.resetPassword);

29
frontend/src/store/main/getters.ts

@ -1,29 +0,0 @@
import { MainState } from './state';
import { getStoreAccessors } from 'typesafe-vuex';
import { State } from '../state';
export const getters = {
hasAdminAccess: (state: MainState) => {
return (
state.userProfile &&
state.userProfile.is_superuser && state.userProfile.is_active);
},
loginError: (state: MainState) => state.logInError,
dashboardShowDrawer: (state: MainState) => state.dashboardShowDrawer,
dashboardMiniDrawer: (state: MainState) => state.dashboardMiniDrawer,
userProfile: (state: MainState) => state.userProfile,
token: (state: MainState) => state.token,
isLoggedIn: (state: MainState) => state.isLoggedIn,
firstNotification: (state: MainState) => state.notifications.length > 0 && state.notifications[0],
};
const {read} = getStoreAccessors<MainState, State>('');
export const readDashboardMiniDrawer = read(getters.dashboardMiniDrawer);
export const readDashboardShowDrawer = read(getters.dashboardShowDrawer);
export const readHasAdminAccess = read(getters.hasAdminAccess);
export const readIsLoggedIn = read(getters.isLoggedIn);
export const readLoginError = read(getters.loginError);
export const readToken = read(getters.token);
export const readUserProfile = read(getters.userProfile);
export const readFirstNotification = read(getters.firstNotification);

21
frontend/src/store/main/index.ts

@ -1,21 +0,0 @@
import { mutations } from './mutations';
import { getters } from './getters';
import { actions } from './actions';
import { MainState } from './state';
const defaultState: MainState = {
isLoggedIn: null,
token: '',
logInError: false,
userProfile: null,
dashboardMiniDrawer: false,
dashboardShowDrawer: true,
notifications: [],
};
export const mainModule = {
state: defaultState,
mutations,
actions,
getters,
};

43
frontend/src/store/main/mutations.ts

@ -1,43 +0,0 @@
import { IUserProfile } from '@/interfaces';
import { MainState, AppNotification } from './state';
import { getStoreAccessors } from 'typesafe-vuex';
import { State } from '../state';
export const mutations = {
setToken(state: MainState, payload: string) {
state.token = payload;
},
setLoggedIn(state: MainState, payload: boolean) {
state.isLoggedIn = payload;
},
setLogInError(state: MainState, payload: boolean) {
state.logInError = payload;
},
setUserProfile(state: MainState, payload: IUserProfile) {
state.userProfile = payload;
},
setDashboardMiniDrawer(state: MainState, payload: boolean) {
state.dashboardMiniDrawer = payload;
},
setDashboardShowDrawer(state: MainState, payload: boolean) {
state.dashboardShowDrawer = payload;
},
addNotification(state: MainState, payload: AppNotification) {
state.notifications.push(payload);
},
removeNotification(state: MainState, payload: AppNotification) {
state.notifications = state.notifications.filter((notification) => notification !== payload);
},
};
const {commit} = getStoreAccessors<MainState | any, State>('');
export const commitSetDashboardMiniDrawer = commit(mutations.setDashboardMiniDrawer);
export const commitSetDashboardShowDrawer = commit(mutations.setDashboardShowDrawer);
export const commitSetLoggedIn = commit(mutations.setLoggedIn);
export const commitSetLogInError = commit(mutations.setLogInError);
export const commitSetToken = commit(mutations.setToken);
export const commitSetUserProfile = commit(mutations.setUserProfile);
export const commitAddNotification = commit(mutations.addNotification);
export const commitRemoveNotification = commit(mutations.removeNotification);

17
frontend/src/store/main/state.ts

@ -1,17 +0,0 @@
import { IUserProfile } from '@/interfaces';
export interface AppNotification {
content: string;
color?: string;
showProgress?: boolean;
}
export interface MainState {
token: string;
isLoggedIn: boolean | null;
logInError: boolean;
userProfile: IUserProfile | null;
dashboardMiniDrawer: boolean;
dashboardShowDrawer: boolean;
notifications: AppNotification[];
}

5
frontend/src/store/state.ts

@ -1,5 +0,0 @@
import { MainState } from './main/state';
export interface State {
main: MainState;
}

5
frontend/src/utils.ts

@ -1,5 +0,0 @@
export const getLocalToken = () => localStorage.getItem('token');
export const saveLocalToken = (token: string) => localStorage.setItem('token', token);
export const removeLocalToken = () => localStorage.removeItem('token');

58
frontend/src/views/Login.vue

@ -1,58 +0,0 @@
<template>
<v-content>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>{{appName}}</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-card-text>
<v-form @keyup.enter="submit">
<v-text-field @keyup.enter="submit" v-model="email" prepend-icon="person" name="login" label="Login" type="text"></v-text-field>
<v-text-field @keyup.enter="submit" v-model="password" prepend-icon="lock" name="password" label="Password" id="password" type="password"></v-text-field>
</v-form>
<div v-if="loginError">
<v-alert :value="loginError" transition="fade-transition" type="error">
Incorrect email or password
</v-alert>
</div>
<v-flex class="caption text-xs-right"><router-link to="/recover-password">Forgot your password?</router-link></v-flex>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click.prevent="submit">Login</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { api } from '@/api';
import { appName } from '@/env';
import { readLoginError } from '@/store/main/getters';
import { dispatchLogIn } from '@/store/main/actions';
@Component
export default class Login extends Vue {
public email: string = '';
public password: string = '';
public appName = appName;
public get loginError() {
return readLoginError(this.$store);
}
public submit() {
dispatchLogIn(this.$store, {username: this.email, password: this.password});
}
}
</script>
<style>
</style>

52
frontend/src/views/PasswordRecovery.vue

@ -1,52 +0,0 @@
<template>
<v-content>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>{{appName}} - Password Recovery</v-toolbar-title>
</v-toolbar>
<v-card-text>
<p class="subheading">A password recovery email will be sent to the registered account</p>
<v-form @keyup.enter="submit" v-model="valid" ref="form" @submit.prevent="" lazy-validation>
<v-text-field @keyup.enter="submit" label="Username" type="text" prepend-icon="person" v-model="username" v-validate="'required'" data-vv-name="username" :error-messages="errors.collect('username')" required></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="cancel">Cancel</v-btn>
<v-btn @click.prevent="submit" :disabled="!valid">
Recover Password
</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { appName } from '@/env';
import { dispatchPasswordRecovery } from '@/store/main/actions';
@Component
export default class Login extends Vue {
public valid = true;
public username: string = '';
public appName = appName;
public cancel() {
this.$router.back();
}
public submit() {
dispatchPasswordRecovery(this.$store, { username: this.username });
}
}
</script>
<style>
</style>

84
frontend/src/views/ResetPassword.vue

@ -1,84 +0,0 @@
<template>
<v-content>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>{{appName}} - Reset Password</v-toolbar-title>
</v-toolbar>
<v-card-text>
<p class="subheading">Enter your new password below</p>
<v-form @keyup.enter="submit" v-model="valid" ref="form" @submit.prevent="" lazy-validation>
<v-text-field type="password" ref="password" label="Password" data-vv-name="password" data-vv-delay="100" data-vv-rules="required" v-validate="'required'" v-model="password1" :error-messages="errors.first('password')">
</v-text-field>
<v-text-field type="password" label="Confirm Password" data-vv-name="password_confirmation" data-vv-delay="100" data-vv-rules="required|confirmed:$password" data-vv-as="password" v-validate="'required|confirmed:password'" v-model="password2" :error-messages="errors.first('password_confirmation')">
</v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="cancel">Cancel</v-btn>
<v-btn @click="reset">Clear</v-btn>
<v-btn @click="submit" :disabled="!valid">Save</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Store } from 'vuex';
import { IUserProfileUpdate } from '@/interfaces';
import { appName } from '@/env';
import { commitAddNotification } from '@/store/main/mutations';
import { dispatchResetPassword } from '@/store/main/actions';
@Component
export default class UserProfileEdit extends Vue {
public appName = appName;
public valid = true;
public password1 = '';
public password2 = '';
public mounted() {
this.checkToken();
}
public reset() {
this.password1 = '';
this.password2 = '';
this.$validator.reset();
}
public cancel() {
this.$router.push('/');
}
public checkToken() {
const token = (this.$router.currentRoute.query.token as string);
if (!token) {
commitAddNotification(this.$store, {
content: 'No token provided in the URL, start a new password recovery',
color: 'error',
});
this.$router.push('/recover-password');
} else {
return token;
}
}
public async submit() {
if (await this.$validator.validateAll()) {
const token = this.checkToken();
if (token) {
await dispatchResetPassword(this.$store, { token, password: this.password1 });
this.$router.push('/');
}
}
}
}
</script>

37
frontend/src/views/main/Dashboard.vue

@ -1,37 +0,0 @@
<template>
<v-container fluid>
<v-card class="ma-3 pa-3">
<v-card-title primary-title>
<div class="headline primary--text">Dashboard</div>
</v-card-title>
<v-card-text>
<div class="headline font-weight-light ma-5">Welcome {{greetedUser}}</div>
</v-card-text>
<v-card-actions>
<v-btn to="/main/profile/view">View Profile</v-btn>
<v-btn to="/main/profile/edit">Edit Profile</v-btn>
<v-btn to="/main/profile/password">Change Password</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Store } from 'vuex';
import { readUserProfile } from '@/store/main/getters';
@Component
export default class Dashboard extends Vue {
get greetedUser() {
const userProfile = readUserProfile(this.$store);
if (userProfile) {
if (userProfile.full_name) {
return userProfile.full_name;
} else {
return userProfile.email;
}
}
}
}
</script>

182
frontend/src/views/main/Main.vue

@ -1,182 +0,0 @@
<template>
<div>
<v-navigation-drawer persistent :mini-variant="miniDrawer" v-model="showDrawer" fixed app>
<v-layout column fill-height>
<v-list>
<v-subheader>Main menu</v-subheader>
<v-list-tile to="/main/dashboard">
<v-list-tile-action>
<v-icon>web</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Dashboard</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/main/profile/view">
<v-list-tile-action>
<v-icon>person</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Profile</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/main/profile/edit">
<v-list-tile-action>
<v-icon>edit</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Edit Profile</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/main/profile/password">
<v-list-tile-action>
<v-icon>vpn_key</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Change Password</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-divider></v-divider>
<v-list subheader v-show="hasAdminAccess">
<v-subheader>Admin</v-subheader>
<v-list-tile to="/main/admin/users/all">
<v-list-tile-action>
<v-icon>group</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Manage Users</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/main/admin/users/create">
<v-list-tile-action>
<v-icon>person_add</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Create User</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-spacer></v-spacer>
<v-list>
<v-list-tile @click="logout">
<v-list-tile-action>
<v-icon>close</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Logout</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-divider></v-divider>
<v-list-tile @click="switchMiniDrawer">
<v-list-tile-action>
<v-icon v-html="miniDrawer ? 'chevron_right' : 'chevron_left'"></v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Collapse</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-layout>
</v-navigation-drawer>
<v-toolbar dark color="primary" app>
<v-toolbar-side-icon @click.stop="switchShowDrawer"></v-toolbar-side-icon>
<v-toolbar-title v-text="appName"></v-toolbar-title>
<v-spacer></v-spacer>
<v-menu bottom left offset-y>
<v-btn slot="activator" icon>
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
<v-list-tile to="/main/profile">
<v-list-tile-content>
<v-list-tile-title>Profile</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-icon>person</v-icon>
</v-list-tile-action>
</v-list-tile>
<v-list-tile @click="logout">
<v-list-tile-content>
<v-list-tile-title>Logout</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-icon>close</v-icon>
</v-list-tile-action>
</v-list-tile>
</v-list>
</v-menu>
</v-toolbar>
<v-content>
<router-view></router-view>
</v-content>
<v-footer class="pa-3" fixed app>
<v-spacer></v-spacer>
<span>&copy; {{appName}}</span>
</v-footer>
</div>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator';
import { appName } from '@/env';
import { readDashboardMiniDrawer, readDashboardShowDrawer, readHasAdminAccess } from '@/store/main/getters';
import { commitSetDashboardShowDrawer, commitSetDashboardMiniDrawer } from '@/store/main/mutations';
import { dispatchUserLogOut } from '@/store/main/actions';
const routeGuardMain = async (to, from, next) => {
if (to.path === '/main') {
next('/main/dashboard');
} else {
next();
}
};
@Component
export default class Main extends Vue {
public appName = appName;
public beforeRouteEnter(to, from, next) {
routeGuardMain(to, from, next);
}
public beforeRouteUpdate(to, from, next) {
routeGuardMain(to, from, next);
}
get miniDrawer() {
return readDashboardMiniDrawer(this.$store);
}
get showDrawer() {
return readDashboardShowDrawer(this.$store);
}
set showDrawer(value) {
commitSetDashboardShowDrawer(this.$store, value);
}
public switchShowDrawer() {
commitSetDashboardShowDrawer(
this.$store,
!readDashboardShowDrawer(this.$store),
);
}
public switchMiniDrawer() {
commitSetDashboardMiniDrawer(
this.$store,
!readDashboardMiniDrawer(this.$store),
);
}
public get hasAdminAccess() {
return readHasAdminAccess(this.$store);
}
public async logout() {
await dispatchUserLogOut(this.$store);
}
}
</script>

38
frontend/src/views/main/Start.vue

@ -1,38 +0,0 @@
<template>
<router-view></router-view>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { store } from '@/store';
import { dispatchCheckLoggedIn } from '@/store/main/actions';
import { readIsLoggedIn } from '@/store/main/getters';
const startRouteGuard = async (to, from, next) => {
await dispatchCheckLoggedIn(store);
if (readIsLoggedIn(store)) {
if (to.path === '/login' || to.path === '/') {
next('/main');
} else {
next();
}
} else if (readIsLoggedIn(store) === false) {
if (to.path === '/' || (to.path as string).startsWith('/main')) {
next('/login');
} else {
next();
}
}
};
@Component
export default class Start extends Vue {
public beforeRouteEnter(to, from, next) {
startRouteGuard(to, from, next);
}
public beforeRouteUpdate(to, from, next) {
startRouteGuard(to, from, next);
}
}
</script>

28
frontend/src/views/main/admin/Admin.vue

@ -1,28 +0,0 @@
<template>
<router-view></router-view>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { store } from '@/store';
import { readHasAdminAccess } from '@/store/main/getters';
const routeGuardAdmin = async (to, from, next) => {
if (!readHasAdminAccess(store)) {
next('/main');
} else {
next();
}
};
@Component
export default class Admin extends Vue {
public beforeRouteEnter(to, from, next) {
routeGuardAdmin(to, from, next);
}
public beforeRouteUpdate(to, from, next) {
routeGuardAdmin(to, from, next);
}
}
</script>

83
frontend/src/views/main/admin/AdminUsers.vue

@ -1,83 +0,0 @@
<template>
<div>
<v-toolbar light>
<v-toolbar-title>
Manage Users
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn color="primary" to="/main/admin/users/create">Create User</v-btn>
</v-toolbar>
<v-data-table :headers="headers" :items="users">
<template slot="items" slot-scope="props">
<td>{{ props.item.name }}</td>
<td>{{ props.item.email }}</td>
<td>{{ props.item.full_name }}</td>
<td><v-icon v-if="props.item.is_active">checkmark</v-icon></td>
<td><v-icon v-if="props.item.is_superuser">checkmark</v-icon></td>
<td class="justify-center layout px-0">
<v-tooltip top>
<span>Edit</span>
<v-btn slot="activator" flat :to="{name: 'main-admin-users-edit', params: {id: props.item.id}}">
<v-icon>edit</v-icon>
</v-btn>
</v-tooltip>
</td>
</template>
</v-data-table>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Store } from 'vuex';
import { IUserProfile } from '@/interfaces';
import { readAdminUsers } from '@/store/admin/getters';
import { dispatchGetUsers } from '@/store/admin/actions';
@Component
export default class AdminUsers extends Vue {
public headers = [
{
text: 'Name',
sortable: true,
value: 'name',
align: 'left',
},
{
text: 'Email',
sortable: true,
value: 'email',
align: 'left',
},
{
text: 'Full Name',
sortable: true,
value: 'full_name',
align: 'left',
},
{
text: 'Is Active',
sortable: true,
value: 'isActive',
align: 'left',
},
{
text: 'Is Superuser',
sortable: true,
value: 'isSuperuser',
align: 'left',
},
{
text: 'Actions',
value: 'id',
},
];
get users() {
return readAdminUsers(this.$store);
}
public async mounted() {
await dispatchGetUsers(this.$store);
}
}
</script>

97
frontend/src/views/main/admin/CreateUser.vue

@ -1,97 +0,0 @@
<template>
<v-container fluid>
<v-card class="ma-3 pa-3">
<v-card-title primary-title>
<div class="headline primary--text">Create User</div>
</v-card-title>
<v-card-text>
<template>
<v-form v-model="valid" ref="form" lazy-validation>
<v-text-field label="Full Name" v-model="fullName" required></v-text-field>
<v-text-field label="E-mail" type="email" v-model="email" v-validate="'required|email'" data-vv-name="email" :error-messages="errors.collect('email')" required></v-text-field>
<div class="subheading secondary--text text--lighten-2">User is superuser <span v-if="isSuperuser">(currently is a superuser)</span><span v-else>(currently is not a superuser)</span></div>
<v-checkbox label="Is Superuser" v-model="isSuperuser"></v-checkbox>
<div class="subheading secondary--text text--lighten-2">User is active <span v-if="isActive">(currently active)</span><span v-else>(currently not active)</span></div>
<v-checkbox label="Is Active" v-model="isActive"></v-checkbox>
<v-layout align-center>
<v-flex>
<v-text-field type="password" ref="password" label="Set Password" data-vv-name="password" data-vv-delay="100" v-validate="{required: true}" v-model="password1" :error-messages="errors.first('password')">
</v-text-field>
<v-text-field type="password" label="Confirm Password" data-vv-name="password_confirmation" data-vv-delay="100" data-vv-as="password" v-validate="{required: true, confirmed: 'password'}" v-model="password2" :error-messages="errors.first('password_confirmation')">
</v-text-field>
</v-flex>
</v-layout>
</v-form>
</template>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="cancel">Cancel</v-btn>
<v-btn @click="reset">Reset</v-btn>
<v-btn @click="submit" :disabled="!valid">
Save
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import {
IUserProfile,
IUserProfileUpdate,
IUserProfileCreate,
} from '@/interfaces';
import { dispatchGetUsers, dispatchCreateUser } from '@/store/admin/actions';
@Component
export default class CreateUser extends Vue {
public valid = false;
public fullName: string = '';
public email: string = '';
public isActive: boolean = true;
public isSuperuser: boolean = false;
public setPassword = false;
public password1: string = '';
public password2: string = '';
public async mounted() {
await dispatchGetUsers(this.$store);
this.reset();
}
public reset() {
this.password1 = '';
this.password2 = '';
this.fullName = '';
this.email = '';
this.isActive = true;
this.isSuperuser = false;
this.$validator.reset();
}
public cancel() {
this.$router.back();
}
public async submit() {
if (await this.$validator.validateAll()) {
const updatedProfile: IUserProfileCreate = {
email: this.email,
};
if (this.fullName) {
updatedProfile.full_name = this.fullName;
}
if (this.email) {
updatedProfile.email = this.email;
}
updatedProfile.is_active = this.isActive;
updatedProfile.is_superuser = this.isSuperuser;
updatedProfile.password = this.password1;
await dispatchCreateUser(this.$store, updatedProfile);
this.$router.push('/main/admin/users');
}
}
}
</script>

163
frontend/src/views/main/admin/EditUser.vue

@ -1,163 +0,0 @@
<template>
<v-container fluid>
<v-card class="ma-3 pa-3">
<v-card-title primary-title>
<div class="headline primary--text">Edit User</div>
</v-card-title>
<v-card-text>
<template>
<div class="my-3">
<div class="subheading secondary--text text--lighten-2">Username</div>
<div
class="title primary--text text--darken-2"
v-if="user"
>{{user.email}}</div>
<div
class="title primary--text text--darken-2"
v-else
>-----</div>
</div>
<v-form
v-model="valid"
ref="form"
lazy-validation
>
<v-text-field
label="Full Name"
v-model="fullName"
required
></v-text-field>
<v-text-field
label="E-mail"
type="email"
v-model="email"
v-validate="'required|email'"
data-vv-name="email"
:error-messages="errors.collect('email')"
required
></v-text-field>
<div class="subheading secondary--text text--lighten-2">User is superuser <span v-if="isSuperuser">(currently is a superuser)</span><span v-else>(currently is not a superuser)</span></div>
<v-checkbox
label="Is Superuser"
v-model="isSuperuser"
></v-checkbox>
<div class="subheading secondary--text text--lighten-2">User is active <span v-if="isActive">(currently active)</span><span v-else>(currently not active)</span></div>
<v-checkbox
label="Is Active"
v-model="isActive"
></v-checkbox>
<v-layout align-center>
<v-flex shrink>
<v-checkbox
v-model="setPassword"
class="mr-2"
></v-checkbox>
</v-flex>
<v-flex>
<v-text-field
:disabled="!setPassword"
type="password"
ref="password"
label="Set Password"
data-vv-name="password"
data-vv-delay="100"
v-validate="{required: setPassword}"
v-model="password1"
:error-messages="errors.first('password')"
>
</v-text-field>
<v-text-field
v-show="setPassword"
type="password"
label="Confirm Password"
data-vv-name="password_confirmation"
data-vv-delay="100"
data-vv-as="password"
v-validate="{required: setPassword, confirmed: 'password'}"
v-model="password2"
:error-messages="errors.first('password_confirmation')"
>
</v-text-field>
</v-flex>
</v-layout>
</v-form>
</template>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="cancel">Cancel</v-btn>
<v-btn @click="reset">Reset</v-btn>
<v-btn
@click="submit"
:disabled="!valid"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { IUserProfile, IUserProfileUpdate } from '@/interfaces';
import { dispatchGetUsers, dispatchUpdateUser } from '@/store/admin/actions';
import { readAdminOneUser } from '@/store/admin/getters';
@Component
export default class EditUser extends Vue {
public valid = true;
public fullName: string = '';
public email: string = '';
public isActive: boolean = true;
public isSuperuser: boolean = false;
public setPassword = false;
public password1: string = '';
public password2: string = '';
public async mounted() {
await dispatchGetUsers(this.$store);
this.reset();
}
public reset() {
this.setPassword = false;
this.password1 = '';
this.password2 = '';
this.$validator.reset();
if (this.user) {
this.fullName = this.user.full_name;
this.email = this.user.email;
this.isActive = this.user.is_active;
this.isSuperuser = this.user.is_superuser;
}
}
public cancel() {
this.$router.back();
}
public async submit() {
if (await this.$validator.validateAll()) {
const updatedProfile: IUserProfileUpdate = {};
if (this.fullName) {
updatedProfile.full_name = this.fullName;
}
if (this.email) {
updatedProfile.email = this.email;
}
updatedProfile.is_active = this.isActive;
updatedProfile.is_superuser = this.isSuperuser;
if (this.setPassword) {
updatedProfile.password = this.password1;
}
await dispatchUpdateUser(this.$store, { id: this.user!.id, user: updatedProfile });
this.$router.push('/main/admin/users');
}
}
get user() {
return readAdminOneUser(this.$store)(+this.$router.currentRoute.params.id);
}
}
</script>

46
frontend/src/views/main/profile/UserProfile.vue

@ -1,46 +0,0 @@
<template>
<v-container fluid>
<v-card class="ma-3 pa-3">
<v-card-title primary-title>
<div class="headline primary--text">User Profile</div>
</v-card-title>
<v-card-text>
<div class="my-4">
<div class="subheading secondary--text text--lighten-3">Full Name</div>
<div class="title primary--text text--darken-2" v-if="userProfile && userProfile.full_name">{{userProfile.full_name}}</div>
<div class="title primary--text text--darken-2" v-else>-----</div>
</div>
<div class="my-3">
<div class="subheading secondary--text text--lighten-3">Email</div>
<div class="title primary--text text--darken-2" v-if="userProfile && userProfile.email">{{userProfile.email}}</div>
<div class="title primary--text text--darken-2" v-else>-----</div>
</div>
</v-card-text>
<v-card-actions>
<v-btn to="/main/profile/edit">Edit</v-btn>
<v-btn to="/main/profile/password">Change password</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Store } from 'vuex';
import { readUserProfile } from '@/store/main/getters';
@Component
export default class UserProfile extends Vue {
get userProfile() {
return readUserProfile(this.$store);
}
public goToEdit() {
this.$router.push('/main/profile/edit');
}
public goToPassword() {
this.$router.push('/main/profile/password');
}
}
</script>

97
frontend/src/views/main/profile/UserProfileEdit.vue

@ -1,97 +0,0 @@
<template>
<v-container fluid>
<v-card class="ma-3 pa-3">
<v-card-title primary-title>
<div class="headline primary--text">Edit User Profile</div>
</v-card-title>
<v-card-text>
<template>
<v-form
v-model="valid"
ref="form"
lazy-validation
>
<v-text-field
label="Full Name"
v-model="fullName"
required
></v-text-field>
<v-text-field
label="E-mail"
type="email"
v-model="email"
v-validate="'required|email'"
data-vv-name="email"
:error-messages="errors.collect('email')"
required
></v-text-field>
</v-form>
</template>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="cancel">Cancel</v-btn>
<v-btn @click="reset">Reset</v-btn>
<v-btn
@click="submit"
:disabled="!valid"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Store } from 'vuex';
import { IUserProfileUpdate } from '@/interfaces';
import { readUserProfile } from '@/store/main/getters';
import { dispatchUpdateUserProfile } from '@/store/main/actions';
@Component
export default class UserProfileEdit extends Vue {
public valid = true;
public fullName: string = '';
public email: string = '';
public created() {
const userProfile = readUserProfile(this.$store);
if (userProfile) {
this.fullName = userProfile.full_name;
this.email = userProfile.email;
}
}
get userProfile() {
return readUserProfile(this.$store);
}
public reset() {
const userProfile = readUserProfile(this.$store);
if (userProfile) {
this.fullName = userProfile.full_name;
this.email = userProfile.email;
}
}
public cancel() {
this.$router.back();
}
public async submit() {
if ((this.$refs.form as any).validate()) {
const updatedProfile: IUserProfileUpdate = {};
if (this.fullName) {
updatedProfile.full_name = this.fullName;
}
if (this.email) {
updatedProfile.email = this.email;
}
await dispatchUpdateUserProfile(this.$store, updatedProfile);
this.$router.push('/main/profile');
}
}
}
</script>

86
frontend/src/views/main/profile/UserProfileEditPassword.vue

@ -1,86 +0,0 @@
<template>
<v-container fluid>
<v-card class="ma-3 pa-3">
<v-card-title primary-title>
<div class="headline primary--text">Set Password</div>
</v-card-title>
<v-card-text>
<template>
<div class="my-3">
<div class="subheading secondary--text text--lighten-2">User</div>
<div class="title primary--text text--darken-2" v-if="userProfile.full_name">{{userProfile.full_name}}</div>
<div class="title primary--text text--darken-2" v-else>{{userProfile.email}}</div>
</div>
<v-form ref="form">
<v-text-field
type="password"
ref="password"
label="Password"
data-vv-name="password"
data-vv-delay="100"
data-vv-rules="required"
v-validate="'required'"
v-model="password1"
:error-messages="errors.first('password')">
</v-text-field>
<v-text-field
type="password"
label="Confirm Password"
data-vv-name="password_confirmation"
data-vv-delay="100"
data-vv-rules="required|confirmed:$password"
data-vv-as="password"
v-validate="'required|confirmed:password'"
v-model="password2"
:error-messages="errors.first('password_confirmation')">
</v-text-field>
</v-form>
</template>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="cancel">Cancel</v-btn>
<v-btn @click="reset">Reset</v-btn>
<v-btn @click="submit" :disabled="!valid">Save</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Store } from 'vuex';
import { IUserProfileUpdate } from '@/interfaces';
import { readUserProfile } from '@/store/main/getters';
import { dispatchUpdateUserProfile } from '@/store/main/actions';
@Component
export default class UserProfileEdit extends Vue {
public valid = true;
public password1 = '';
public password2 = '';
get userProfile() {
return readUserProfile(this.$store);
}
public reset() {
this.password1 = '';
this.password2 = '';
this.$validator.reset();
}
public cancel() {
this.$router.back();
}
public async submit() {
if (await this.$validator.validateAll()) {
const updatedProfile: IUserProfileUpdate = {};
updatedProfile.password = this.password1;
await dispatchUpdateUserProfile(this.$store, updatedProfile);
this.$router.push('/main/profile');
}
}
}
</script>

15
frontend/tests/unit/upload-button.spec.ts

@ -1,15 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import UploadButton from '@/components/UploadButton.vue';
import '@/plugins/vuetify';
describe('UploadButton.vue', () => {
it('renders props.title when passed', () => {
const title = 'upload a file';
const wrapper = shallowMount(UploadButton, {
slots: {
default: title,
},
});
expect(wrapper.text()).toMatch(title);
});
});

41
frontend/tsconfig.json

@ -1,41 +0,0 @@
{
"compilerOptions": {
"noImplicitAny": false,
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

19
frontend/tslint.json

@ -1,19 +0,0 @@
{
"defaultSeverity": "warning",
"extends": [
"tslint:recommended"
],
"linterOptions": {
"exclude": [
"node_modules/**"
]
},
"rules": {
"quotemark": [true, "single"],
"indent": [true, "spaces", 2],
"interface-name": false,
"ordered-imports": false,
"object-literal-sort-keys": false,
"no-consecutive-blank-lines": false
}
}

35
frontend/vue.config.js

@ -1,35 +0,0 @@
module.exports = {
// Fix Vuex-typescript in prod: https://github.com/istrib/vuex-typescript/issues/13#issuecomment-409869231
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
config.optimization.minimizer[0].options.terserOptions = Object.assign(
{},
config.optimization.minimizer[0].options.terserOptions,
{
ecma: 5,
compress: {
keep_fnames: true,
},
warnings: false,
mangle: {
keep_fnames: true,
},
},
);
}
},
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => Object.assign(options, {
transformAssetUrls: {
'v-img': ['src', 'lazy-src'],
'v-card': 'src',
'v-card-media': 'src',
'v-responsive': 'src',
}
}));
},
}
Loading…
Cancel
Save