@ -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', |
|||
} |
|||
})); |
|||
}, |
|||
} |