@ -1 +0,0 @@ |
|||||
node_modules |
|
@ -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 |
|
@ -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 +0,0 @@ |
|||||
18.12.1 |
|
@ -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 |
|
@ -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 |
|
||||
``` |
|
@ -1,10 +0,0 @@ |
|||||
module.exports = { |
|
||||
"presets": [ |
|
||||
[ |
|
||||
"@vue/cli-plugin-babel/preset", |
|
||||
{ |
|
||||
"useBuiltIns": "entry" |
|
||||
} |
|
||||
] |
|
||||
] |
|
||||
} |
|
@ -1,9 +0,0 @@ |
|||||
location /api { |
|
||||
return 404; |
|
||||
} |
|
||||
location /docs { |
|
||||
return 404; |
|
||||
} |
|
||||
location /redoc { |
|
||||
return 404; |
|
||||
} |
|
@ -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; |
|
||||
} |
|
@ -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/" |
|
||||
} |
|
||||
} |
|
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 10 KiB |
@ -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> |
|
@ -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" |
|
||||
} |
|
@ -1,2 +0,0 @@ |
|||||
User-agent: * |
|
||||
Disallow: |
|
@ -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> |
|
@ -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, |
|
||||
}); |
|
||||
}, |
|
||||
}; |
|
Before Width: | Height: | Size: 6.7 KiB |
@ -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+
|
|
||||
]); |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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; |
|
@ -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; |
|
||||
} |
|
@ -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'); |
|
@ -1,4 +0,0 @@ |
|||||
import Vue from 'vue'; |
|
||||
import VeeValidate from 'vee-validate'; |
|
||||
|
|
||||
Vue.use(VeeValidate); |
|
@ -1,6 +0,0 @@ |
|||||
import Vue from 'vue'; |
|
||||
import Vuetify from 'vuetify'; |
|
||||
|
|
||||
Vue.use(Vuetify, { |
|
||||
iconfont: 'md', |
|
||||
}); |
|
@ -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); |
|
||||
}, |
|
||||
}); |
|
||||
} |
|
@ -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: '/', |
|
||||
}, |
|
||||
], |
|
||||
}); |
|
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,4 +0,0 @@ |
|||||
declare module '*.vue' { |
|
||||
import Vue from 'vue'; |
|
||||
export default Vue; |
|
||||
} |
|
@ -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); |
|
@ -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); |
|
@ -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, |
|
||||
}; |
|
@ -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); |
|
@ -1,5 +0,0 @@ |
|||||
import { IUserProfile } from '@/interfaces'; |
|
||||
|
|
||||
export interface AdminState { |
|
||||
users: IUserProfile[]; |
|
||||
} |
|
@ -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; |
|
@ -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); |
|
@ -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); |
|
@ -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, |
|
||||
}; |
|
@ -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); |
|
@ -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[]; |
|
||||
} |
|
@ -1,5 +0,0 @@ |
|||||
import { MainState } from './main/state'; |
|
||||
|
|
||||
export interface State { |
|
||||
main: MainState; |
|
||||
} |
|
@ -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'); |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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>© {{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> |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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> |
|
@ -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); |
|
||||
}); |
|
||||
}); |
|
@ -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" |
|
||||
] |
|
||||
} |
|
@ -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 |
|
||||
} |
|
||||
} |
|
@ -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', |
|
||||
} |
|
||||
})); |
|
||||
}, |
|
||||
} |
|