Vuex (#126)
Merge branch 'master' into feature/vuex Cleanup Move namespaces handling to store Move fullpage setting to store Move online/offline handling to store Remove debug log Fix loading namepaces Rename errorMsg Handle registering through the store Use store to determine wether or not a user is authenticated on login page Use store in edit team Use store to get the current user's avatar Use store to figure out if the current user belongs to a team Use store to figure out if the current user is list admin Use store to get user info when listing labels Use store to get username on home Use store to figure out if the user is namespace admin Use the store for configuration Use the store to check if a user is authenticated everywhere Only renew token if the user is logged in Fix renewing auth from jwt token Move logout to store Move renew token to store Move log in to store Only show enabled migrators Only show registration button if registration is enabled Put all config information from the backend in the store Remove old config handler Move config state to seperate module Add vuex and get the motd through it Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/126
This commit is contained in:
parent
2ba6b7ef3a
commit
5724b98358
31 changed files with 501 additions and 393 deletions
|
@ -20,7 +20,8 @@
|
||||||
"vue": "2.6.11",
|
"vue": "2.6.11",
|
||||||
"vue-drag-resize": "1.3.2",
|
"vue-drag-resize": "1.3.2",
|
||||||
"vue-easymde": "1.2.0",
|
"vue-easymde": "1.2.0",
|
||||||
"vue-smooth-dnd": "0.8.1"
|
"vue-smooth-dnd": "0.8.1",
|
||||||
|
"vuex": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
||||||
|
|
86
src/App.vue
86
src/App.vue
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="isOnline">
|
<div v-if="online">
|
||||||
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
|
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
|
||||||
<div class="offline" style="height: 0;width: 0;"></div>
|
<div class="offline" style="height: 0;width: 0;"></div>
|
||||||
<nav class="navbar main-theme is-fixed-top" role="navigation" aria-label="main navigation"
|
<nav class="navbar main-theme is-fixed-top" role="navigation" aria-label="main navigation"
|
||||||
v-if="user.authenticated && (userInfo && userInfo.type === authTypes.USER)">
|
v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||||
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
||||||
|
@ -16,11 +16,11 @@
|
||||||
<a @click="refreshApp()" class="button is-primary noshadow">Update Now</a>
|
<a @click="refreshApp()" class="button is-primary noshadow">Update Now</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="user">
|
<div class="user">
|
||||||
<img :src="user.infos.getAvatarUrl()" class="avatar" alt=""/>
|
<img :src="userInfo.getAvatarUrl()" class="avatar" alt=""/>
|
||||||
<div class="dropdown is-right is-active">
|
<div class="dropdown is-right is-active">
|
||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
<button class="button noshadow" @click="userMenuActive = !userMenuActive">
|
<button class="button noshadow" @click="userMenuActive = !userMenuActive">
|
||||||
<span class="username">{{user.infos.username}}</span>
|
<span class="username">{{userInfo.username}}</span>
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<icon icon="chevron-down"/>
|
<icon icon="chevron-down"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div v-if="user.authenticated && (userInfo && userInfo.type === authTypes.USER)">
|
<div v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)">
|
||||||
<a @click="mobileMenuActive = true" class="mobilemenu-show-button" v-if="!mobileMenuActive">
|
<a @click="mobileMenuActive = true" class="mobilemenu-show-button" v-if="!mobileMenuActive">
|
||||||
<icon icon="bars"></icon>
|
<icon icon="bars"></icon>
|
||||||
</a>
|
</a>
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<aside class="menu namespaces-lists">
|
<aside class="menu namespaces-lists">
|
||||||
<fancycheckbox v-model="showArchived" @change="loadNamespaces()" class="show-archived-check">
|
<fancycheckbox v-model="showArchived" class="show-archived-check">
|
||||||
Show Archived
|
Show Archived
|
||||||
</fancycheckbox>
|
</fancycheckbox>
|
||||||
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
|
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
|
||||||
|
@ -168,8 +168,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- FIXME: This will only be triggered when the root component is already loaded before doing link share auth. Will "fix" itself once we use vuex. -->
|
<div v-else-if="userAuthenticated && (userInfo && userInfo.type === authTypes.LINK_SHARE)">
|
||||||
<div v-else-if="user.authenticated && (userInfo && userInfo.type === authTypes.LINK_SHARE)">
|
|
||||||
<div class="container has-text-centered link-share-view">
|
<div class="container has-text-centered link-share-view">
|
||||||
<div class="column is-10 is-offset-1">
|
<div class="column is-10 is-offset-1">
|
||||||
<img src="/images/logo-full.svg" alt="Vikunja" class="logo"/>
|
<img src="/images/logo-full.svg" alt="Vikunja" class="logo"/>
|
||||||
|
@ -215,8 +214,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from './auth'
|
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
import NamespaceService from './services/namespace'
|
import NamespaceService from './services/namespace'
|
||||||
import authTypes from './models/authTypes'
|
import authTypes from './models/authTypes'
|
||||||
|
@ -224,6 +223,7 @@
|
||||||
import swEvents from './ServiceWorker/events'
|
import swEvents from './ServiceWorker/events'
|
||||||
import Notification from './components/global/notification'
|
import Notification from './components/global/notification'
|
||||||
import Fancycheckbox from './components/global/fancycheckbox'
|
import Fancycheckbox from './components/global/fancycheckbox'
|
||||||
|
import {IS_FULLPAGE, ONLINE} from './store/mutation-types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
@ -233,16 +233,11 @@
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: auth.user,
|
|
||||||
namespaces: [],
|
|
||||||
namespaceService: NamespaceService,
|
namespaceService: NamespaceService,
|
||||||
mobileMenuActive: false,
|
mobileMenuActive: false,
|
||||||
fullpage: false,
|
|
||||||
currentDate: new Date(),
|
currentDate: new Date(),
|
||||||
userMenuActive: false,
|
userMenuActive: false,
|
||||||
authTypes: authTypes,
|
authTypes: authTypes,
|
||||||
isOnline: true,
|
|
||||||
motd: '',
|
|
||||||
showArchived: false,
|
showArchived: false,
|
||||||
|
|
||||||
// Service Worker stuff
|
// Service Worker stuff
|
||||||
|
@ -253,9 +248,9 @@
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
// Check if the user is offline, show a message then
|
// Check if the user is offline, show a message then
|
||||||
this.isOnline = navigator.onLine
|
this.$store.commit(ONLINE, navigator.onLine)
|
||||||
window.addEventListener('online', () => this.isOnline = navigator.onLine);
|
window.addEventListener('online', () => this.$store.commit(ONLINE, navigator.onLine));
|
||||||
window.addEventListener('offline', () => this.isOnline = navigator.onLine);
|
window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine));
|
||||||
|
|
||||||
// Password reset
|
// Password reset
|
||||||
if (this.$route.query.userPasswordReset !== undefined) {
|
if (this.$route.query.userPasswordReset !== undefined) {
|
||||||
|
@ -271,7 +266,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (auth.user.authenticated && auth.user.infos.type === authTypes.USER && (this.$route.params.name === 'home' || this.namespaces.length === 0)) {
|
this.$store.dispatch('config/update')
|
||||||
|
this.$store.dispatch('auth/checkAuth')
|
||||||
|
|
||||||
|
if (this.userAuthenticated && this.userInfo.type === authTypes.USER && (this.$route.params.name === 'home' || this.namespaces.length === 0)) {
|
||||||
this.loadNamespaces()
|
this.loadNamespaces()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,49 +286,41 @@
|
||||||
|
|
||||||
// Schedule a token renew every minute
|
// Schedule a token renew every minute
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
auth.renewToken()
|
this.$store.dispatch('auth/renewToken')
|
||||||
}, 1000 * 60)
|
}, 1000 * 60)
|
||||||
|
|
||||||
// Set the motd
|
|
||||||
this.setMotd()
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
// call the method again if the route changes
|
// call the method again if the route changes
|
||||||
'$route': 'doStuffAfterRoute',
|
'$route': 'doStuffAfterRoute',
|
||||||
},
|
},
|
||||||
computed: {
|
computed: mapState({
|
||||||
userInfo() {
|
userInfo: state => state.auth.info,
|
||||||
return auth.getUserInfos()
|
userAuthenticated: state => state.auth.authenticated,
|
||||||
}
|
motd: state => state.config.motd,
|
||||||
|
online: ONLINE,
|
||||||
|
fullpage: IS_FULLPAGE,
|
||||||
|
namespaces(state) {
|
||||||
|
return state.namespaces.namespaces.filter(n => this.showArchived ? true : !n.isArchived)
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
logout() {
|
logout() {
|
||||||
auth.logout()
|
this.$store.dispatch('auth/logout')
|
||||||
},
|
},
|
||||||
loadNamespaces() {
|
loadNamespaces() {
|
||||||
this.namespaceService = new NamespaceService()
|
this.$store.dispatch('namespaces/loadNamespaces')
|
||||||
this.namespaceService.getAll({}, {isArchived: this.showArchived})
|
|
||||||
.then(r => {
|
|
||||||
this.$set(this, 'namespaces', r)
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
this.error(e, this)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
loadNamespacesIfNeeded(e) {
|
loadNamespacesIfNeeded(e) {
|
||||||
if (auth.user.authenticated && (this.userInfo && this.userInfo.type === authTypes.USER) && (e.name === 'home' || this.namespaces.length === 0)) {
|
if (this.userAuthenticated && (this.userInfo && this.userInfo.type === authTypes.USER) && (e.name === 'home' || this.namespaces.length === 0)) {
|
||||||
this.loadNamespaces()
|
this.loadNamespaces()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doStuffAfterRoute(e) {
|
doStuffAfterRoute(e) {
|
||||||
this.fullpage = false;
|
this.$store.commit(IS_FULLPAGE, false)
|
||||||
this.loadNamespacesIfNeeded(e)
|
this.loadNamespacesIfNeeded(e)
|
||||||
this.mobileMenuActive = false
|
this.mobileMenuActive = false
|
||||||
this.userMenuActive = false
|
this.userMenuActive = false
|
||||||
},
|
},
|
||||||
setFullPage() {
|
|
||||||
this.fullpage = true;
|
|
||||||
},
|
|
||||||
showRefreshUI(e) {
|
showRefreshUI(e) {
|
||||||
console.log('recieved refresh event', e)
|
console.log('recieved refresh event', e)
|
||||||
this.registration = e.detail;
|
this.registration = e.detail;
|
||||||
|
@ -338,22 +328,12 @@
|
||||||
},
|
},
|
||||||
refreshApp() {
|
refreshApp() {
|
||||||
this.updateExists = false;
|
this.updateExists = false;
|
||||||
if (!this.registration || !this.registration.waiting) { return; }
|
if (!this.registration || !this.registration.waiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Notify the service worker to actually do the update
|
// Notify the service worker to actually do the update
|
||||||
this.registration.waiting.postMessage('skipWaiting');
|
this.registration.waiting.postMessage('skipWaiting');
|
||||||
},
|
},
|
||||||
setMotd() {
|
|
||||||
let cancel = () => {};
|
|
||||||
// Since the config may not be initialized when we're calling this, we need to retry until it is ready.
|
|
||||||
if (typeof this.$config === 'undefined') {
|
|
||||||
cancel = setTimeout(() => {
|
|
||||||
this.setMotd()
|
|
||||||
}, 150)
|
|
||||||
} else {
|
|
||||||
cancel()
|
|
||||||
this.motd = this.$config.motd
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,148 +0,0 @@
|
||||||
import {HTTP} from '../http-common'
|
|
||||||
import router from '../router'
|
|
||||||
import UserModel from '../models/user'
|
|
||||||
// const API_URL = 'http://localhost:8082/api/v1/'
|
|
||||||
// const LOGIN_URL = 'http://localhost:8082/login'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
user: {
|
|
||||||
authenticated: false,
|
|
||||||
infos: {},
|
|
||||||
},
|
|
||||||
|
|
||||||
login(context, credentials, redirect = '') {
|
|
||||||
localStorage.removeItem('token') // Delete an eventually preexisting old token
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
username: credentials.username,
|
|
||||||
password: credentials.password
|
|
||||||
}
|
|
||||||
|
|
||||||
if(credentials.totpPasscode) {
|
|
||||||
data.totp_passcode = credentials.totpPasscode
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP.post('login', data)
|
|
||||||
.then(response => {
|
|
||||||
// Save the token to local storage for later use
|
|
||||||
localStorage.setItem('token', response.data.token)
|
|
||||||
|
|
||||||
// Tell others the user is autheticated
|
|
||||||
this.user.authenticated = true
|
|
||||||
this.user.isLinkShareAuth = false
|
|
||||||
|
|
||||||
// Redirect if nessecary
|
|
||||||
if (redirect !== '') {
|
|
||||||
router.push({name: redirect})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
if (e.response) {
|
|
||||||
if (e.response.data.code === 1017 && !credentials.totpPasscode) {
|
|
||||||
context.needsTotpPasscode = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
context.errorMsg = e.response.data.message
|
|
||||||
if (e.response.status === 401) {
|
|
||||||
context.errorMsg = 'Wrong username or password.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
context.loading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
register(context, creds, redirect) {
|
|
||||||
HTTP.post('register', {
|
|
||||||
username: creds.username,
|
|
||||||
email: creds.email,
|
|
||||||
password: creds.password
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.login(context, creds, redirect)
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
// Hide the loader
|
|
||||||
context.loading = false
|
|
||||||
if (e.response) {
|
|
||||||
context.errorMsg = e.response.data.message
|
|
||||||
if (e.response.status === 401) {
|
|
||||||
context.errorMsg = 'Wrong username or password.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
router.push({name: 'login'})
|
|
||||||
this.user.authenticated = false
|
|
||||||
},
|
|
||||||
|
|
||||||
linkShareAuth(hash) {
|
|
||||||
return HTTP.post('/shares/' + hash + '/auth')
|
|
||||||
.then(r => {
|
|
||||||
localStorage.setItem('token', r.data.token)
|
|
||||||
this.getUserInfos()
|
|
||||||
return Promise.resolve(r.data)
|
|
||||||
}).catch(e => {
|
|
||||||
return Promise.reject(e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
renewToken() {
|
|
||||||
HTTP.post('user/token', null, {
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('token'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(r => {
|
|
||||||
localStorage.setItem('token', r.data.token)
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.log('Error renewing token: ', e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
checkAuth() {
|
|
||||||
let jwt = localStorage.getItem('token')
|
|
||||||
this.getUserInfos()
|
|
||||||
this.user.authenticated = false
|
|
||||||
if (jwt) {
|
|
||||||
let ts = Math.round((new Date()).getTime() / 1000)
|
|
||||||
if (this.user.infos.exp >= ts) {
|
|
||||||
this.user.authenticated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getUserInfos() {
|
|
||||||
let jwt = localStorage.getItem('token')
|
|
||||||
if (jwt) {
|
|
||||||
this.user.infos = new UserModel(this.parseJwt(localStorage.getItem('token')))
|
|
||||||
return this.user.infos
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
parseJwt(token) {
|
|
||||||
let base64Url = token.split('.')[1]
|
|
||||||
let base64 = base64Url.replace('-', '+').replace('_', '/')
|
|
||||||
return JSON.parse(window.atob(base64))
|
|
||||||
},
|
|
||||||
|
|
||||||
getAuthHeader() {
|
|
||||||
return {
|
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getToken() {
|
|
||||||
return localStorage.getItem('token')
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
<h2>Hi {{user.infos.username}}!</h2>
|
<h2>Hi {{userInfo.username}}!</h2>
|
||||||
<p>Click on a list or namespace on the left to get started.</p>
|
<p>Click on a list or namespace on the left to get started.</p>
|
||||||
<router-link class="button is-primary is-right noshadow is-outlined" :to="{name: 'migrateStart'}">Import your data into Vikunja</router-link>
|
<router-link
|
||||||
|
class="button is-primary is-right noshadow is-outlined"
|
||||||
|
:to="{name: 'migrateStart'}"
|
||||||
|
v-if="migratorsEnabled"
|
||||||
|
>
|
||||||
|
Import your data into Vikunja
|
||||||
|
</router-link>
|
||||||
<TaskOverview :show-all="true"/>
|
<TaskOverview :show-all="true"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../auth'
|
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Home",
|
name: "Home",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: auth.user,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
currentDate: new Date(),
|
currentDate: new Date(),
|
||||||
tasks: []
|
tasks: []
|
||||||
|
@ -23,14 +28,14 @@
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
// Check if the user is already logged in, if so, redirect him to the homepage
|
||||||
if (!auth.user.authenticated) {
|
if (!this.authenticated) {
|
||||||
router.push({name: 'login'})
|
router.push({name: 'login'})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
computed: mapState({
|
||||||
logout() {
|
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
|
||||||
auth.logout()
|
authenticated: state => state.auth.authenticated,
|
||||||
},
|
userInfo: state => state.auth.info,
|
||||||
},
|
}),
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,11 +11,11 @@
|
||||||
<span
|
<span
|
||||||
v-for="l in labels" :key="l.id"
|
v-for="l in labels" :key="l.id"
|
||||||
class="tag"
|
class="tag"
|
||||||
:class="{'disabled': user.infos.id !== l.createdBy.id}"
|
:class="{'disabled': userInfo.id !== l.createdBy.id}"
|
||||||
:style="{'background': l.hexColor, 'color': l.textColor}"
|
:style="{'background': l.hexColor, 'color': l.textColor}"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="user.infos.id !== l.createdBy.id"
|
v-if="userInfo.id !== l.createdBy.id"
|
||||||
v-tooltip.bottom="'You are not allowed to edit this label because you dont own it.'">
|
v-tooltip.bottom="'You are not allowed to edit this label because you dont own it.'">
|
||||||
{{ l.title }}
|
{{ l.title }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
v-else>
|
v-else>
|
||||||
{{ l.title }}
|
{{ l.title }}
|
||||||
</a>
|
</a>
|
||||||
<a class="delete is-small" @click="deleteLabel(l)" v-if="user.infos.id === l.createdBy.id"></a>
|
<a class="delete is-small" @click="deleteLabel(l)" v-if="userInfo.id === l.createdBy.id"></a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4" v-if="isLabelEdit">
|
<div class="column is-4" v-if="isLabelEdit">
|
||||||
|
@ -102,10 +102,10 @@
|
||||||
<script>
|
<script>
|
||||||
import verte from 'verte'
|
import verte from 'verte'
|
||||||
import 'verte/dist/verte.css'
|
import 'verte/dist/verte.css'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
import LabelService from '../../services/label'
|
import LabelService from '../../services/label'
|
||||||
import LabelModel from '../../models/label'
|
import LabelModel from '../../models/label'
|
||||||
import auth from '../../auth'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ListLabels',
|
name: 'ListLabels',
|
||||||
|
@ -118,7 +118,6 @@
|
||||||
labels: [],
|
labels: [],
|
||||||
labelEditLabel: LabelModel,
|
labelEditLabel: LabelModel,
|
||||||
isLabelEdit: false,
|
isLabelEdit: false,
|
||||||
user: auth.user,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -126,6 +125,9 @@
|
||||||
this.labelEditLabel = new LabelModel()
|
this.labelEditLabel = new LabelModel()
|
||||||
this.loadLabels()
|
this.loadLabels()
|
||||||
},
|
},
|
||||||
|
computed: mapState({
|
||||||
|
userInfo: state => state.auth.info
|
||||||
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
loadLabels() {
|
loadLabels() {
|
||||||
const getAllLabels = (page = 1) => {
|
const getAllLabels = (page = 1) => {
|
||||||
|
@ -183,7 +185,7 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
editLabel(label) {
|
editLabel(label) {
|
||||||
if (label.createdBy.id !== this.user.infos.id) {
|
if (label.createdBy.id !== this.userInfo.id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.labelEditLabel = label
|
this.labelEditLabel = label
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
<component :is="manageUsersComponent" :id="list.id" type="list" shareType="user" :userIsAdmin="userIsAdmin"></component>
|
<component :is="manageUsersComponent" :id="list.id" type="list" shareType="user" :userIsAdmin="userIsAdmin"></component>
|
||||||
<component :is="manageTeamsComponent" :id="list.id" type="list" shareType="team" :userIsAdmin="userIsAdmin"></component>
|
<component :is="manageTeamsComponent" :id="list.id" type="list" shareType="team" :userIsAdmin="userIsAdmin"></component>
|
||||||
|
|
||||||
<link-sharing :list-id="$route.params.id"/>
|
<link-sharing :list-id="$route.params.id" v-if="linkSharingEnabled"/>
|
||||||
|
|
||||||
<modal
|
<modal
|
||||||
v-if="showDeleteModal"
|
v-if="showDeleteModal"
|
||||||
|
@ -85,7 +85,6 @@
|
||||||
import verte from 'verte'
|
import verte from 'verte'
|
||||||
import 'verte/dist/verte.css'
|
import 'verte/dist/verte.css'
|
||||||
|
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import manageSharing from '../sharing/userTeam'
|
import manageSharing from '../sharing/userTeam'
|
||||||
import LinkSharing from '../sharing/linkSharing'
|
import LinkSharing from '../sharing/linkSharing'
|
||||||
|
@ -102,8 +101,6 @@
|
||||||
listService: ListService,
|
listService: ListService,
|
||||||
|
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
user: auth.user,
|
|
||||||
userIsAdmin: false, // FIXME: we should be able to know somehow if the user is admin, not only based on if he's the owner
|
|
||||||
|
|
||||||
manageUsersComponent: '',
|
manageUsersComponent: '',
|
||||||
manageTeamsComponent: '',
|
manageTeamsComponent: '',
|
||||||
|
@ -115,12 +112,6 @@
|
||||||
manageSharing,
|
manageSharing,
|
||||||
verte,
|
verte,
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
|
||||||
if (!auth.user.authenticated) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
this.listService = new ListService()
|
this.listService = new ListService()
|
||||||
this.loadList()
|
this.loadList()
|
||||||
|
@ -129,15 +120,20 @@
|
||||||
// call again the method if the route changes
|
// call again the method if the route changes
|
||||||
'$route': 'loadList'
|
'$route': 'loadList'
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
linkSharingEnabled() {
|
||||||
|
return this.$store.state.config.linkSharingEnabled
|
||||||
|
},
|
||||||
|
userIsAdmin() {
|
||||||
|
return this.list.owner && this.list.owner.id === this.$store.state.auth.info.id
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadList() {
|
loadList() {
|
||||||
let list = new ListModel({id: this.$route.params.id})
|
let list = new ListModel({id: this.$route.params.id})
|
||||||
this.listService.get(list)
|
this.listService.get(list)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
this.$set(this, 'list', r)
|
this.$set(this, 'list', r)
|
||||||
if (r.owner.id === this.user.infos.id) {
|
|
||||||
this.userIsAdmin = true
|
|
||||||
}
|
|
||||||
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
|
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
|
||||||
this.manageTeamsComponent = 'manageSharing'
|
this.manageTeamsComponent = 'manageSharing'
|
||||||
this.manageUsersComponent = 'manageSharing'
|
this.manageUsersComponent = 'manageSharing'
|
||||||
|
@ -149,15 +145,7 @@
|
||||||
submit() {
|
submit() {
|
||||||
this.listService.update(this.list)
|
this.listService.update(this.list)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
// Update the list in the parent
|
this.$store.commit('namespaces/setListInNamespaceById', r)
|
||||||
for (const n in this.$parent.namespaces) {
|
|
||||||
let lists = this.$parent.namespaces[n].lists
|
|
||||||
for (const l in lists) {
|
|
||||||
if (lists[l].id === r.id) {
|
|
||||||
this.$set(this.$parent.namespaces[n].lists, l, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.success({message: 'The list was successfully updated.'}, this)
|
this.success({message: 'The list was successfully updated.'}, this)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
|
|
@ -32,10 +32,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import ListService from '../../services/list'
|
import ListService from '../../services/list'
|
||||||
import ListModel from '../../models/list'
|
import ListModel from '../../models/list'
|
||||||
|
import {IS_FULLPAGE} from '../../store/mutation-types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NewList",
|
name: "NewList",
|
||||||
|
@ -46,16 +46,10 @@
|
||||||
listService: ListService,
|
listService: ListService,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
|
||||||
if (!auth.user.authenticated) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
this.list = new ListModel()
|
this.list = new ListModel()
|
||||||
this.listService = new ListService()
|
this.listService = new ListService()
|
||||||
this.$parent.setFullPage();
|
this.$store.commit(IS_FULLPAGE, true)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newList() {
|
newList() {
|
||||||
|
@ -68,7 +62,8 @@
|
||||||
this.list.namespaceId = this.$route.params.id
|
this.list.namespaceId = this.$route.params.id
|
||||||
this.listService.create(this.list)
|
this.listService.create(this.list)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$parent.loadNamespaces()
|
response.namespaceId = this.list.namespaceId
|
||||||
|
this.$store.commit('namespaces/addListToNamespace', response)
|
||||||
this.success({message: 'The list was successfully created.'}, this)
|
this.success({message: 'The list was successfully created.'}, this)
|
||||||
router.push({name: 'list.index', params: {listId: response.id}})
|
router.push({name: 'list.index', params: {listId: response.id}})
|
||||||
})
|
})
|
||||||
|
|
|
@ -22,12 +22,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
|
|
||||||
import ListModel from '../../models/list'
|
import ListModel from '../../models/list'
|
||||||
import ListService from '../../services/list'
|
import ListService from '../../services/list'
|
||||||
import authType from '../../models/authTypes'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
@ -37,12 +35,6 @@
|
||||||
listLoaded: 0,
|
listLoaded: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
|
||||||
if (!auth.user.authenticated && auth.user.infos.type !== authType.LINK_SHARE) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
this.listService = new ListService()
|
this.listService = new ListService()
|
||||||
this.list = new ListModel()
|
this.list = new ListModel()
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
<h1>Import your data from other services to Vikunja</h1>
|
<h1>Import your data from other services to Vikunja</h1>
|
||||||
<p>Click on the logo of one of the third-party services below to get started.</p>
|
<p>Click on the logo of one of the third-party services below to get started.</p>
|
||||||
<div class="migration-services-overview">
|
<div class="migration-services-overview">
|
||||||
<router-link :to="{name: 'migrateWunderlist'}">
|
<router-link :to="{name: 'migrate.'+m}" v-for="m in availableMigrators" :key="m">
|
||||||
<img src="/images/migration/wunderlist.png" alt="Wunderlist"/>
|
<img :src="`/images/migration/${m}.png`" :alt="m"/>
|
||||||
Wunderlist
|
{{ m }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'migrate'
|
name: 'migrate',
|
||||||
|
computed: {
|
||||||
|
availableMigrators() {
|
||||||
|
return this.$store.state.config.availableMigrators
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -83,7 +83,6 @@
|
||||||
import verte from 'verte'
|
import verte from 'verte'
|
||||||
import 'verte/dist/verte.css'
|
import 'verte/dist/verte.css'
|
||||||
|
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import manageSharing from '../sharing/userTeam'
|
import manageSharing from '../sharing/userTeam'
|
||||||
|
|
||||||
|
@ -96,13 +95,11 @@
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
namespaceService: NamespaceService,
|
namespaceService: NamespaceService,
|
||||||
userIsAdmin: false,
|
|
||||||
manageUsersComponent: '',
|
manageUsersComponent: '',
|
||||||
manageTeamsComponent: '',
|
manageTeamsComponent: '',
|
||||||
|
|
||||||
namespace: NamespaceModel,
|
namespace: NamespaceModel,
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
user: auth.user,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -111,11 +108,6 @@
|
||||||
verte,
|
verte,
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
|
||||||
if (!auth.user.authenticated) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.namespace.id = this.$route.params.id
|
this.namespace.id = this.$route.params.id
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -127,15 +119,17 @@
|
||||||
// call again the method if the route changes
|
// call again the method if the route changes
|
||||||
'$route': 'loadNamespace'
|
'$route': 'loadNamespace'
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
userIsAdmin() {
|
||||||
|
return this.namespace.owner && this.namespace.owner.id === this.$store.state.auth.info.id
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadNamespace() {
|
loadNamespace() {
|
||||||
let namespace = new NamespaceModel({id: this.$route.params.id})
|
let namespace = new NamespaceModel({id: this.$route.params.id})
|
||||||
this.namespaceService.get(namespace)
|
this.namespaceService.get(namespace)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
this.$set(this, 'namespace', r)
|
this.$set(this, 'namespace', r)
|
||||||
if (r.owner.id === this.user.infos.id) {
|
|
||||||
this.userIsAdmin = true
|
|
||||||
}
|
|
||||||
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
|
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
|
||||||
this.manageTeamsComponent = 'manageSharing'
|
this.manageTeamsComponent = 'manageSharing'
|
||||||
this.manageUsersComponent = 'manageSharing'
|
this.manageUsersComponent = 'manageSharing'
|
||||||
|
@ -148,12 +142,7 @@
|
||||||
this.namespaceService.update(this.namespace)
|
this.namespaceService.update(this.namespace)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
// Update the namespace in the parent
|
// Update the namespace in the parent
|
||||||
for (const n in this.$parent.namespaces) {
|
this.$store.commit('namespaces/setNamespaceById', r)
|
||||||
if (this.$parent.namespaces[n].id === r.id) {
|
|
||||||
r.lists = this.$parent.namespaces[n].lists
|
|
||||||
this.$set(this.$parent.namespaces, n, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.success({message: 'The namespace was successfully updated.'}, this)
|
this.success({message: 'The namespace was successfully updated.'}, this)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
|
|
@ -34,10 +34,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import NamespaceModel from "../../models/namespace";
|
import NamespaceModel from "../../models/namespace";
|
||||||
import NamespaceService from "../../services/namespace";
|
import NamespaceService from "../../services/namespace";
|
||||||
|
import {IS_FULLPAGE} from '../../store/mutation-types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NewNamespace",
|
name: "NewNamespace",
|
||||||
|
@ -48,16 +48,10 @@
|
||||||
namespaceService: NamespaceService,
|
namespaceService: NamespaceService,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
|
||||||
if (!auth.user.authenticated) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
this.namespace = new NamespaceModel()
|
this.namespace = new NamespaceModel()
|
||||||
this.namespaceService = new NamespaceService()
|
this.namespaceService = new NamespaceService()
|
||||||
this.$parent.setFullPage();
|
this.$store.commit(IS_FULLPAGE, true)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newNamespace() {
|
newNamespace() {
|
||||||
|
@ -68,10 +62,10 @@
|
||||||
this.showError = false
|
this.showError = false
|
||||||
|
|
||||||
this.namespaceService.create(this.namespace)
|
this.namespaceService.create(this.namespace)
|
||||||
.then(() => {
|
.then(r => {
|
||||||
this.$parent.loadNamespaces()
|
this.$store.commit('namespaces/addNamespace', r)
|
||||||
this.success({message: 'The namespace was successfully created.'}, this)
|
this.success({message: 'The namespace was successfully created.'}, this)
|
||||||
router.push({name: 'home'})
|
router.back()
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button @click="linkIdToDelete = s.id; showDeleteModal = true" class="button is-danger icon-only">
|
<button @click="() => {linkIdToDelete = s.id; showDeleteModal = true}" class="button is-danger icon-only">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<icon icon="trash-alt"/>
|
<icon icon="trash-alt"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -106,6 +106,7 @@
|
||||||
import LinkShareModel from '../../models/linkShare'
|
import LinkShareModel from '../../models/linkShare'
|
||||||
|
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'linkSharing',
|
name: 'linkSharing',
|
||||||
|
@ -138,6 +139,9 @@
|
||||||
this.load()
|
this.load()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: mapState({
|
||||||
|
frontendUrl: state => state.config.frontendUrl,
|
||||||
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
load() {
|
load() {
|
||||||
// If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here
|
// If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here
|
||||||
|
@ -183,7 +187,7 @@
|
||||||
copy(text)
|
copy(text)
|
||||||
},
|
},
|
||||||
getShareLink(hash) {
|
getShareLink(hash) {
|
||||||
return this.$config.frontend_url + 'share/' + hash + '/auth'
|
return this.frontendUrl + 'share/' + hash + '/auth'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -25,7 +24,7 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
auth() {
|
auth() {
|
||||||
auth.linkShareAuth(this.$route.params.share)
|
this.$store.dispatch('auth/linkShareAuth', this.$route.params.share)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
router.push({name: 'list.list', params: {listId: r.list_id}})
|
router.push({name: 'list.list', params: {listId: r.list_id}})
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
<template v-if="shareType === 'user'">
|
<template v-if="shareType === 'user'">
|
||||||
<td>{{s.username}}</td>
|
<td>{{s.username}}</td>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="s.id === currentUser.id">
|
<template v-if="s.id === userInfo.id">
|
||||||
<b class="is-success">You</b>
|
<b class="is-success">You</b>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
|
@ -105,8 +105,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import multiselect from 'vue-multiselect'
|
import multiselect from 'vue-multiselect'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
import UserNamespaceService from '../../services/userNamespace'
|
import UserNamespaceService from '../../services/userNamespace'
|
||||||
import UserNamespaceModel from '../../models/userNamespace'
|
import UserNamespaceModel from '../../models/userNamespace'
|
||||||
|
@ -156,7 +156,6 @@
|
||||||
rights: rights,
|
rights: rights,
|
||||||
selectedRight: rights.READ,
|
selectedRight: rights.READ,
|
||||||
|
|
||||||
currentUser: auth.user.infos,
|
|
||||||
typeString: '',
|
typeString: '',
|
||||||
sharables: [], // This holds either teams or users who this namepace or list is shared with
|
sharables: [], // This holds either teams or users who this namepace or list is shared with
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
|
@ -165,6 +164,9 @@
|
||||||
components: {
|
components: {
|
||||||
multiselect
|
multiselect
|
||||||
},
|
},
|
||||||
|
computed: mapState({
|
||||||
|
userInfo: state => state.auth.info
|
||||||
|
}),
|
||||||
created() {
|
created() {
|
||||||
|
|
||||||
if (this.shareType === 'user') {
|
if (this.shareType === 'user') {
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="media comment">
|
<div class="media comment">
|
||||||
<figure class="media-left">
|
<figure class="media-left">
|
||||||
<img class="image is-avatar" :src="user.infos.getAvatarUrl(48)" alt="" width="48" height="48"/>
|
<img class="image is-avatar" :src="userAvatar" alt="" width="48" height="48"/>
|
||||||
</figure>
|
</figure>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
|
@ -67,7 +67,6 @@
|
||||||
<script>
|
<script>
|
||||||
import TaskCommentService from '../../../services/taskComment'
|
import TaskCommentService from '../../../services/taskComment'
|
||||||
import TaskCommentModel from '../../../models/taskComment'
|
import TaskCommentModel from '../../../models/taskComment'
|
||||||
import auth from '../../../auth'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'comments',
|
name: 'comments',
|
||||||
|
@ -80,7 +79,6 @@
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
comments: [],
|
comments: [],
|
||||||
user: auth.user,
|
|
||||||
|
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
commentToDelete: TaskCommentModel,
|
commentToDelete: TaskCommentModel,
|
||||||
|
@ -107,6 +105,11 @@
|
||||||
this.loadComments()
|
this.loadComments()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
userAvatar() {
|
||||||
|
return this.$store.state.auth.info.getAvatarUrl(48)
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadComments() {
|
loadComments() {
|
||||||
this.taskCommentService.getAll({taskId: this.taskId})
|
this.taskCommentService.getAll({taskId: this.taskId})
|
||||||
|
|
|
@ -107,7 +107,7 @@
|
||||||
<tr v-for="m in team.members" :key="m.id">
|
<tr v-for="m in team.members" :key="m.id">
|
||||||
<td>{{m.username}}</td>
|
<td>{{m.username}}</td>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="m.id === user.infos.id">
|
<template v-if="m.id === userInfo.id">
|
||||||
<b class="is-success">You</b>
|
<b class="is-success">You</b>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
|
@ -127,7 +127,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="actions" v-if="userIsAdmin">
|
<td class="actions" v-if="userIsAdmin">
|
||||||
<button @click="toggleUserType(m)" class="button buttonright is-primary"
|
<button @click="toggleUserType(m)" class="button buttonright is-primary"
|
||||||
v-if="m.id !== user.infos.id">
|
v-if="m.id !== userInfo.id">
|
||||||
Make
|
Make
|
||||||
<template v-if="!m.admin">
|
<template v-if="!m.admin">
|
||||||
Admin
|
Admin
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
<button @click="() => {member = m; showUserDeleteModal = true}" class="button is-danger"
|
<button @click="() => {member = m; showUserDeleteModal = true}" class="button is-danger"
|
||||||
v-if="m.id !== user.infos.id">
|
v-if="m.id !== userInfo.id">
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<icon icon="trash-alt"/>
|
<icon icon="trash-alt"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -173,9 +173,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import multiselect from 'vue-multiselect'
|
import multiselect from 'vue-multiselect'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
import TeamService from '../../services/team'
|
import TeamService from '../../services/team'
|
||||||
import TeamModel from '../../models/team'
|
import TeamModel from '../../models/team'
|
||||||
|
@ -196,7 +196,6 @@
|
||||||
|
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
showUserDeleteModal: false,
|
showUserDeleteModal: false,
|
||||||
user: auth.user,
|
|
||||||
userIsAdmin: false,
|
userIsAdmin: false,
|
||||||
|
|
||||||
newMember: UserModel,
|
newMember: UserModel,
|
||||||
|
@ -209,12 +208,6 @@
|
||||||
components: {
|
components: {
|
||||||
multiselect,
|
multiselect,
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
|
||||||
if (!auth.user.authenticated) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
this.teamService = new TeamService()
|
this.teamService = new TeamService()
|
||||||
this.teamMemberService = new TeamMemberService()
|
this.teamMemberService = new TeamMemberService()
|
||||||
|
@ -225,9 +218,11 @@
|
||||||
// call again the method if the route changes
|
// call again the method if the route changes
|
||||||
'$route': 'loadTeam'
|
'$route': 'loadTeam'
|
||||||
},
|
},
|
||||||
|
computed: mapState({
|
||||||
|
userInfo: state => state.auth.info,
|
||||||
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
loadTeam() {
|
loadTeam() {
|
||||||
// this.member = new TeamMemberModel({teamId: this.teamId})
|
|
||||||
this.team = new TeamModel({id: this.teamId})
|
this.team = new TeamModel({id: this.teamId})
|
||||||
this.teamService.get(this.team)
|
this.teamService.get(this.team)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
@ -235,7 +230,7 @@
|
||||||
let members = response.members
|
let members = response.members
|
||||||
for (const m in members) {
|
for (const m in members) {
|
||||||
members[m].teamId = this.teamId
|
members[m].teamId = this.teamId
|
||||||
if (members[m].id === this.user.infos.id && members[m].admin) {
|
if (members[m].id === this.userInfo.id && members[m].admin) {
|
||||||
this.userIsAdmin = true
|
this.userIsAdmin = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
|
||||||
import TeamService from '../../services/team'
|
import TeamService from '../../services/team'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -30,12 +28,6 @@
|
||||||
teams: [],
|
teams: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
|
||||||
if (!auth.user.authenticated) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
this.teamService = new TeamService()
|
this.teamService = new TeamService()
|
||||||
this.loadTeams()
|
this.loadTeams()
|
||||||
|
|
|
@ -32,10 +32,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import TeamModel from '../../models/team'
|
import TeamModel from '../../models/team'
|
||||||
import TeamService from '../../services/team'
|
import TeamService from '../../services/team'
|
||||||
|
import {IS_FULLPAGE} from '../../store/mutation-types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NewTeam",
|
name: "NewTeam",
|
||||||
|
@ -46,16 +46,10 @@
|
||||||
showError: false,
|
showError: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
|
||||||
if (!auth.user.authenticated) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
this.teamService = new TeamService()
|
this.teamService = new TeamService()
|
||||||
this.team = new TeamModel()
|
this.team = new TeamModel()
|
||||||
this.$parent.setFullPage();
|
this.$store.commit(IS_FULLPAGE, true)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newTeam() {
|
newTeam() {
|
||||||
|
|
|
@ -9,31 +9,61 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="username">Username</label>
|
<label class="label" for="username">Username</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input v-focus type="text" id="username" class="input" name="username" placeholder="e.g. frederick" ref="username" required/>
|
<input
|
||||||
|
v-focus type="text"
|
||||||
|
id="username"
|
||||||
|
class="input"
|
||||||
|
name="username"
|
||||||
|
placeholder="e.g. frederick"
|
||||||
|
ref="username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="password">Password</label>
|
<label class="label" for="password">Password</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="password" class="input" id="password" name="password" placeholder="e.g. ••••••••••••" ref="password" required/>
|
<input
|
||||||
|
type="password"
|
||||||
|
class="input"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="e.g. ••••••••••••"
|
||||||
|
ref="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" v-if="needsTotpPasscode">
|
<div class="field" v-if="needsTotpPasscode">
|
||||||
<label class="label" for="totpPasscode">Two Factor Authentication Code</label>
|
<label class="label" for="totpPasscode">Two Factor Authentication Code</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="text" class="input" id="totpPasscode" placeholder="e.g. 123456" ref="totpPasscode" required v-focus/>
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
id="totpPasscode"
|
||||||
|
placeholder="e.g. 123456"
|
||||||
|
ref="totpPasscode"
|
||||||
|
required
|
||||||
|
v-focus
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<button type="submit" class="button is-primary" v-bind:class="{ 'is-loading': loading}">Login
|
||||||
|
</button>
|
||||||
|
<router-link :to="{ name: 'register' }" class="button" v-if="registrationEnabled">Register
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="submit" class="button is-primary" v-bind:class="{ 'is-loading': loading}">Login</button>
|
<router-link :to="{ name: 'getPasswordReset' }" class="reset-password-link">Reset your
|
||||||
<router-link :to="{ name: 'register' }" class="button">Register</router-link>
|
password
|
||||||
<router-link :to="{ name: 'getPasswordReset' }" class="reset-password-link">Reset your password</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification is-danger" v-if="errorMsg">
|
<div class="notification is-danger" v-if="errorMessage">
|
||||||
{{ errorMsg }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,18 +71,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
import {HTTP} from '../../http-common'
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
|
import {ERROR_MESSAGE, LOADING} from '../../store/mutation-types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
errorMsg: '',
|
|
||||||
confirmedEmailSuccess: false,
|
confirmedEmailSuccess: false,
|
||||||
loading: false,
|
|
||||||
needsTotpPasscode: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
@ -69,19 +98,25 @@
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
cancel()
|
||||||
this.errorMsg = e.response.data.message
|
this.$store.commit(ERROR_MESSAGE, e.response.data.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
// Check if the user is already logged in, if so, redirect him to the homepage
|
||||||
if (auth.user.authenticated) {
|
if (this.authenticated) {
|
||||||
router.push({name: 'home'})
|
router.push({name: 'home'})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: mapState({
|
||||||
|
registrationEnabled: state => state.config.registrationEnabled,
|
||||||
|
loading: LOADING,
|
||||||
|
errorMessage: ERROR_MESSAGE,
|
||||||
|
needsTotpPasscode: state => state.auth.needsTotpPasscode,
|
||||||
|
authenticated: state => state.auth.authenticated,
|
||||||
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
this.loading = true
|
this.$store.commit(ERROR_MESSAGE, '')
|
||||||
this.errorMsg = ''
|
|
||||||
// Some browsers prevent Vue bindings from working with autofilled values.
|
// Some browsers prevent Vue bindings from working with autofilled values.
|
||||||
// To work around this, we're manually getting the values here instead of relying on vue bindings.
|
// To work around this, we're manually getting the values here instead of relying on vue bindings.
|
||||||
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
||||||
|
@ -94,7 +129,11 @@
|
||||||
credentials.totpPasscode = this.$refs.totpPasscode.value
|
credentials.totpPasscode = this.$refs.totpPasscode.value
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.login(this, credentials, 'home')
|
this.$store.dispatch('auth/login', credentials)
|
||||||
|
.then(() => {
|
||||||
|
router.push({name: 'home'})
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
<div class="notification is-info" v-if="loading">
|
<div class="notification is-info" v-if="loading">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div class="notification is-danger" v-if="errorMsg !== ''">
|
<div class="notification is-danger" v-if="errorMessage !== ''">
|
||||||
{{ errorMsg }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,8 +46,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
import {ERROR_MESSAGE, LOADING} from '../../store/mutation-types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
@ -58,35 +59,37 @@
|
||||||
password: '',
|
password: '',
|
||||||
password2: '',
|
password2: '',
|
||||||
},
|
},
|
||||||
errorMsg: '',
|
|
||||||
loading: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
// Check if the user is already logged in, if so, redirect him to the homepage
|
// Check if the user is already logged in, if so, redirect him to the homepage
|
||||||
if (auth.user.authenticated) {
|
if (this.authenticated) {
|
||||||
router.push({name: 'home'})
|
router.push({name: 'home'})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: mapState({
|
||||||
|
authenticated: state => state.auth.authenticated,
|
||||||
|
loading: LOADING,
|
||||||
|
errorMessage: ERROR_MESSAGE,
|
||||||
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
this.loading = true
|
this.$store.commit(LOADING, true)
|
||||||
|
this.$store.commit(ERROR_MESSAGE, '')
|
||||||
this.errorMsg = ''
|
|
||||||
|
|
||||||
if (this.credentials.password2 !== this.credentials.password) {
|
if (this.credentials.password2 !== this.credentials.password) {
|
||||||
this.loading = false
|
this.$store.commit(ERROR_MESSAGE, 'Passwords don\'t match.')
|
||||||
this.errorMsg = 'Passwords don\'t match.'
|
this.$store.commit(LOADING, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let credentials = {
|
const credentials = {
|
||||||
username: this.credentials.username,
|
username: this.credentials.username,
|
||||||
email: this.credentials.email,
|
email: this.credentials.email,
|
||||||
password: this.credentials.password
|
password: this.credentials.password
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.register(this, credentials, 'home')
|
this.$store.dispatch('auth/register', credentials)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import {HTTP} from './http-common'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
config: null,
|
|
||||||
|
|
||||||
getConfig() {
|
|
||||||
return this.config
|
|
||||||
},
|
|
||||||
|
|
||||||
initConfig() {
|
|
||||||
return HTTP.get('info')
|
|
||||||
.then(r => {
|
|
||||||
this.config = r.data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
14
src/main.js
14
src/main.js
|
@ -1,7 +1,6 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import auth from './auth'
|
|
||||||
|
|
||||||
// Register the modal
|
// Register the modal
|
||||||
import Modal from './components/modal/Modal'
|
import Modal from './components/modal/Modal'
|
||||||
|
@ -20,12 +19,6 @@ Vue.config.productionTip = false
|
||||||
import Notifications from 'vue-notification'
|
import Notifications from 'vue-notification'
|
||||||
Vue.use(Notifications)
|
Vue.use(Notifications)
|
||||||
|
|
||||||
import config from './config'
|
|
||||||
config.initConfig()
|
|
||||||
.then(() => {
|
|
||||||
Vue.prototype.$config = config.getConfig()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
@ -138,9 +131,6 @@ Vue.directive('focus', {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check the user's auth status when the app starts
|
|
||||||
auth.checkAuth()
|
|
||||||
|
|
||||||
// Mixins
|
// Mixins
|
||||||
import message from './message'
|
import message from './message'
|
||||||
import {format, formatDistance} from 'date-fns'
|
import {format, formatDistance} from 'date-fns'
|
||||||
|
@ -165,7 +155,11 @@ Vue.mixin({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Vuex
|
||||||
|
import {store} from './store'
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
|
store,
|
||||||
render: h => h(App)
|
render: h => h(App)
|
||||||
}).$mount('#app')
|
}).$mount('#app')
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { register } from 'register-service-worker'
|
import { register } from 'register-service-worker'
|
||||||
import swEvents from './ServiceWorker/events'
|
import swEvents from './ServiceWorker/events'
|
||||||
import auth from './auth'
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
register(`${process.env.BASE_URL}sw.js`, {
|
register(`${process.env.BASE_URL}sw.js`, {
|
||||||
|
@ -45,7 +44,7 @@ if(navigator && navigator.serviceWorker) {
|
||||||
if(action === 'getBearerToken') {
|
if(action === 'getBearerToken') {
|
||||||
console.debug('Token request from sw');
|
console.debug('Token request from sw');
|
||||||
port.postMessage({
|
port.postMessage({
|
||||||
authToken: auth.getToken(),
|
authToken: localStorage.getItem('token'),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.error('Unknown event', event);
|
console.error('Unknown event', event);
|
||||||
|
|
|
@ -197,7 +197,7 @@ export default new Router({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/migrate/wunderlist',
|
path: '/migrate/wunderlist',
|
||||||
name: 'migrateWunderlist',
|
name: 'migrate.wunderlist',
|
||||||
component: WunderlistMigrationComponent,
|
component: WunderlistMigrationComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
36
src/store/index.js
Normal file
36
src/store/index.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
import config from './modules/config'
|
||||||
|
import auth from './modules/auth'
|
||||||
|
import namespaces from './modules/namespaces'
|
||||||
|
import {ERROR_MESSAGE, IS_FULLPAGE, LOADING, ONLINE} from './mutation-types'
|
||||||
|
|
||||||
|
export const store = new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
config,
|
||||||
|
auth,
|
||||||
|
namespaces,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
loading: false,
|
||||||
|
errorMessage: '',
|
||||||
|
online: true,
|
||||||
|
isFullpage: false,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
[LOADING](state, loading) {
|
||||||
|
state.loading = loading
|
||||||
|
},
|
||||||
|
[ERROR_MESSAGE](state, error) {
|
||||||
|
state.errorMessage = error
|
||||||
|
},
|
||||||
|
[ONLINE](state, online) {
|
||||||
|
state.online = online
|
||||||
|
},
|
||||||
|
[IS_FULLPAGE](state, fullpage) {
|
||||||
|
state.isFullpage = fullpage
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
148
src/store/modules/auth.js
Normal file
148
src/store/modules/auth.js
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import {HTTP} from '../../http-common'
|
||||||
|
import {ERROR_MESSAGE, LOADING} from "../mutation-types";
|
||||||
|
import UserModel from "../../models/user";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: () => ({
|
||||||
|
authenticated: false,
|
||||||
|
isLinkShareAuth: false,
|
||||||
|
info: {},
|
||||||
|
needsTotpPasscode: false,
|
||||||
|
}),
|
||||||
|
mutations: {
|
||||||
|
info(state, info) {
|
||||||
|
state.info = info
|
||||||
|
},
|
||||||
|
authenticated(state, authenticated) {
|
||||||
|
state.authenticated = authenticated
|
||||||
|
},
|
||||||
|
isLinkShareAuth(state, is) {
|
||||||
|
state.isLinkShareAuth = is
|
||||||
|
},
|
||||||
|
needsTotpPasscode(state, needs) {
|
||||||
|
state.needsTotpPasscode = needs
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Logs a user in with a set of credentials.
|
||||||
|
login(ctx, credentials) {
|
||||||
|
ctx.commit(LOADING, true, {root: true})
|
||||||
|
|
||||||
|
// Delete an eventually preexisting old token
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password
|
||||||
|
}
|
||||||
|
|
||||||
|
if(credentials.totpPasscode) {
|
||||||
|
data.totp_passcode = credentials.totpPasscode
|
||||||
|
}
|
||||||
|
|
||||||
|
return HTTP.post('login', data)
|
||||||
|
.then(response => {
|
||||||
|
// Save the token to local storage for later use
|
||||||
|
localStorage.setItem('token', response.data.token)
|
||||||
|
|
||||||
|
// Tell others the user is autheticated
|
||||||
|
ctx.commit('isLinkShareAuth', false)
|
||||||
|
ctx.dispatch('checkAuth')
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
if (e.response) {
|
||||||
|
if (e.response.data.code === 1017 && !credentials.totpPasscode) {
|
||||||
|
ctx.commit('needsTotpPasscode', true)
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorMsg = e.response.data.message
|
||||||
|
if (e.response.status === 401) {
|
||||||
|
errorMsg = 'Wrong username or password.'
|
||||||
|
}
|
||||||
|
ctx.commit(ERROR_MESSAGE, errorMsg, {root: true})
|
||||||
|
}
|
||||||
|
return Promise.reject()
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
ctx.commit(LOADING, false, {root: true})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// Registers a new user and logs them in.
|
||||||
|
// Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
|
||||||
|
register(ctx, credentials) {
|
||||||
|
return HTTP.post('register', {
|
||||||
|
username: credentials.username,
|
||||||
|
email: credentials.email,
|
||||||
|
password: credentials.password
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return ctx.dispatch('login', credentials)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
if (e.response) {
|
||||||
|
ctx.commit(ERROR_MESSAGE, e.response.data.message, {root: true})
|
||||||
|
}
|
||||||
|
return Promise.reject()
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
ctx.commit(LOADING, false, {root: true})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
linkShareAuth(ctx, hash) {
|
||||||
|
return HTTP.post('/shares/' + hash + '/auth')
|
||||||
|
.then(r => {
|
||||||
|
localStorage.setItem('token', r.data.token)
|
||||||
|
ctx.dispatch('checkAuth')
|
||||||
|
return Promise.resolve(r.data)
|
||||||
|
}).catch(e => {
|
||||||
|
return Promise.reject(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// Populates user information from jwt token saved in local storage in store
|
||||||
|
checkAuth(ctx) {
|
||||||
|
const jwt = localStorage.getItem('token')
|
||||||
|
let authenticated = false
|
||||||
|
if (jwt) {
|
||||||
|
const base64 = jwt
|
||||||
|
.split('.')[1]
|
||||||
|
.replace('-', '+')
|
||||||
|
.replace('_', '/')
|
||||||
|
const info = new UserModel(JSON.parse(window.atob(base64)))
|
||||||
|
const ts = Math.round((new Date()).getTime() / 1000)
|
||||||
|
if (info.exp >= ts) {
|
||||||
|
authenticated = true
|
||||||
|
}
|
||||||
|
ctx.commit('info', info)
|
||||||
|
}
|
||||||
|
ctx.commit('authenticated', authenticated)
|
||||||
|
},
|
||||||
|
// Renews the api token and saves it to local storage
|
||||||
|
renewToken(ctx) {
|
||||||
|
if (!ctx.state.authenticated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP.post('user/token', null, {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + localStorage.getItem('token'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
localStorage.setItem('token', r.data.token)
|
||||||
|
ctx.dispatch('checkAuth')
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error renewing token: ', e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
logout(ctx) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
ctx.dispatch('checkAuth')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
37
src/store/modules/config.js
Normal file
37
src/store/modules/config.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import {CONFIG} from '../mutation-types'
|
||||||
|
import {HTTP} from '../../http-common'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: () => ({
|
||||||
|
// These are the api defaults.
|
||||||
|
version: '',
|
||||||
|
frontendUrl: '',
|
||||||
|
motd: '',
|
||||||
|
linkSharingEnabled: true,
|
||||||
|
maxFileSize: '20MB',
|
||||||
|
registrationEnabled: true,
|
||||||
|
availableMigrators: [],
|
||||||
|
taskAttachmentsEnabled: true,
|
||||||
|
}),
|
||||||
|
mutations: {
|
||||||
|
[CONFIG](state, config) {
|
||||||
|
state.version = config.version
|
||||||
|
state.frontendUrl = config.frontend_url
|
||||||
|
state.motd = config.motd
|
||||||
|
state.linkSharingEnabled = config.link_sharing_enabled
|
||||||
|
state.maxFileSize = config.max_file_size
|
||||||
|
state.registrationEnabled = config.registration_enabled
|
||||||
|
state.availableMigrators = config.available_migrators
|
||||||
|
state.taskAttachmentsEnabled = config.task_attachments_enabled
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
update(ctx) {
|
||||||
|
HTTP.get('info')
|
||||||
|
.then(r => {
|
||||||
|
ctx.commit(CONFIG, r.data)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
63
src/store/modules/namespaces.js
Normal file
63
src/store/modules/namespaces.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
import NamespaceService from '../../services/namespace'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: () => ({
|
||||||
|
namespaces: [],
|
||||||
|
}),
|
||||||
|
mutations: {
|
||||||
|
namespaces(state, namespaces) {
|
||||||
|
state.namespaces = namespaces
|
||||||
|
},
|
||||||
|
setNamespaceById(state, namespace) {
|
||||||
|
for (const n in state.namespaces) {
|
||||||
|
if (state.namespaces[n].id === namespace.id) {
|
||||||
|
namespace.lists = state.namespaces[n].lists
|
||||||
|
Vue.set(state.namespaces, n, namespace)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setListInNamespaceById(state, list) {
|
||||||
|
for (const n in state.namespaces) {
|
||||||
|
// We don't have the namespace id on the list which means we need to loop over all lists until we find it.
|
||||||
|
// FIXME: Not ideal at all - we should fix that at the api level.
|
||||||
|
for (const l in state.namespaces[n].lists) {
|
||||||
|
if (state.namespaces[n].lists[l].id === list.id) {
|
||||||
|
const namespace = state.namespaces[n]
|
||||||
|
namespace.lists[l] = list
|
||||||
|
Vue.set(state.namespaces, n, namespace)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addNamespace(state, namespace) {
|
||||||
|
state.namespaces.push(namespace)
|
||||||
|
},
|
||||||
|
addListToNamespace(state, list) {
|
||||||
|
for (const n in state.namespaces) {
|
||||||
|
if (state.namespaces[n].id === list.namespaceId) {
|
||||||
|
state.namespaces[n].lists.push(list)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
loadNamespaces(ctx) {
|
||||||
|
const namespaceService = new NamespaceService()
|
||||||
|
// We always load all namespaces and filter them on the frontend
|
||||||
|
return namespaceService.getAll({}, {is_archived: true})
|
||||||
|
.then(r => {
|
||||||
|
ctx.commit('namespaces', r)
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
return Promise.reject(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
7
src/store/mutation-types.js
Normal file
7
src/store/mutation-types.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const LOADING = 'loading'
|
||||||
|
export const ERROR_MESSAGE = 'errorMessage'
|
||||||
|
export const ONLINE = 'online'
|
||||||
|
export const IS_FULLPAGE = 'isFullpage'
|
||||||
|
|
||||||
|
export const CONFIG = 'config'
|
||||||
|
export const AUTH = 'auth'
|
|
@ -4,6 +4,7 @@
|
||||||
a {
|
a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -12403,6 +12403,11 @@ vue@2.6.11, vue@^2.6.11:
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
|
||||||
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
|
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
|
||||||
|
|
||||||
|
vuex@^3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.3.0.tgz#665b4630ea1347317139fcc5cb495aab3ec5e513"
|
||||||
|
integrity sha512-1MfcBt+YFd20DPwKe0ThhYm1UEXZya4gVKUvCy7AtS11YAOUR+9a6u4fsv1Rr6ePZCDNxW/M1zuIaswp6nNv8Q==
|
||||||
|
|
||||||
watch@^1.0.2:
|
watch@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"
|
resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"
|
||||||
|
|
Loading…
Reference in a new issue