Vuex #126
|
@ -20,7 +20,8 @@
|
|||
"vue": "2.6.11",
|
||||
"vue-drag-resize": "1.3.2",
|
||||
"vue-easymde": "1.2.0",
|
||||
"vue-smooth-dnd": "0.8.1"
|
||||
"vue-smooth-dnd": "0.8.1",
|
||||
"vuex": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
||||
|
|
94
src/App.vue
94
src/App.vue
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<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 -->
|
||||
<div class="offline" style="height: 0;width: 0;"></div>
|
||||
<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">
|
||||
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
||||
|
@ -16,11 +16,11 @@
|
|||
<a @click="refreshApp()" class="button is-primary noshadow">Update Now</a>
|
||||
</div>
|
||||
<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-trigger">
|
||||
<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">
|
||||
<icon icon="chevron-down"/>
|
||||
</span>
|
||||
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<icon icon="bars"></icon>
|
||||
</a>
|
||||
|
@ -107,7 +107,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<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
|
||||
</fancycheckbox>
|
||||
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
|
||||
|
@ -168,8 +168,7 @@
|
|||
</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="user.authenticated && (userInfo && userInfo.type === authTypes.LINK_SHARE)">
|
||||
<div v-else-if="userAuthenticated && (userInfo && userInfo.type === authTypes.LINK_SHARE)">
|
||||
<div class="container has-text-centered link-share-view">
|
||||
<div class="column is-10 is-offset-1">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja" class="logo"/>
|
||||
|
@ -215,8 +214,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from './auth'
|
||||
import router from './router'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import NamespaceService from './services/namespace'
|
||||
import authTypes from './models/authTypes'
|
||||
|
@ -224,6 +223,7 @@
|
|||
import swEvents from './ServiceWorker/events'
|
||||
import Notification from './components/global/notification'
|
||||
import Fancycheckbox from './components/global/fancycheckbox'
|
||||
import {IS_FULLPAGE, ONLINE} from './store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
|
@ -233,16 +233,11 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
user: auth.user,
|
||||
namespaces: [],
|
||||
namespaceService: NamespaceService,
|
||||
mobileMenuActive: false,
|
||||
fullpage: false,
|
||||
currentDate: new Date(),
|
||||
userMenuActive: false,
|
||||
authTypes: authTypes,
|
||||
isOnline: true,
|
||||
motd: '',
|
||||
showArchived: false,
|
||||
|
||||
// Service Worker stuff
|
||||
|
@ -253,9 +248,9 @@
|
|||
},
|
||||
beforeMount() {
|
||||
// Check if the user is offline, show a message then
|
||||
this.isOnline = navigator.onLine
|
||||
window.addEventListener('online', () => this.isOnline = navigator.onLine);
|
||||
window.addEventListener('offline', () => this.isOnline = navigator.onLine);
|
||||
this.$store.commit(ONLINE, navigator.onLine)
|
||||
window.addEventListener('online', () => this.$store.commit(ONLINE, navigator.onLine));
|
||||
window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine));
|
||||
|
||||
// Password reset
|
||||
if (this.$route.query.userPasswordReset !== undefined) {
|
||||
|
@ -271,12 +266,15 @@
|
|||
}
|
||||
},
|
||||
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()
|
||||
}
|
||||
|
||||
// Service worker communication
|
||||
document.addEventListener(swEvents.SW_UPDATED, this.showRefreshUI, { once: true })
|
||||
document.addEventListener(swEvents.SW_UPDATED, this.showRefreshUI, {once: true})
|
||||
|
||||
navigator.serviceWorker.addEventListener(
|
||||
'controllerchange', () => {
|
||||
|
@ -288,72 +286,54 @@
|
|||
|
||||
// Schedule a token renew every minute
|
||||
setTimeout(() => {
|
||||
auth.renewToken()
|
||||
this.$store.dispatch('auth/renewToken')
|
||||
}, 1000 * 60)
|
||||
|
||||
// Set the motd
|
||||
this.setMotd()
|
||||
},
|
||||
watch: {
|
||||
// call the method again if the route changes
|
||||
'$route': 'doStuffAfterRoute',
|
||||
},
|
||||
computed: {
|
||||
userInfo() {
|
||||
return auth.getUserInfos()
|
||||
}
|
||||
},
|
||||
computed: mapState({
|
||||
userInfo: state => state.auth.info,
|
||||
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: {
|
||||
logout() {
|
||||
auth.logout()
|
||||
this.$store.dispatch('auth/logout')
|
||||
},
|
||||
loadNamespaces() {
|
||||
this.namespaceService = new NamespaceService()
|
||||
this.namespaceService.getAll({}, {isArchived: this.showArchived})
|
||||
.then(r => {
|
||||
this.$set(this, 'namespaces', r)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
this.$store.dispatch('namespaces/loadNamespaces')
|
||||
},
|
||||
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()
|
||||
}
|
||||
},
|
||||
doStuffAfterRoute(e) {
|
||||
this.fullpage = false;
|
||||
this.$store.commit(IS_FULLPAGE, false)
|
||||
this.loadNamespacesIfNeeded(e)
|
||||
this.mobileMenuActive = false
|
||||
this.userMenuActive = false
|
||||
},
|
||||
setFullPage() {
|
||||
this.fullpage = true;
|
||||
},
|
||||
showRefreshUI (e) {
|
||||
showRefreshUI(e) {
|
||||
console.log('recieved refresh event', e)
|
||||
this.registration = e.detail;
|
||||
this.updateAvailable = true;
|
||||
},
|
||||
refreshApp () {
|
||||
refreshApp() {
|
||||
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
|
||||
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>
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
<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"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../auth'
|
||||
import router from '../router'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: "Home",
|
||||
data() {
|
||||
return {
|
||||
user: auth.user,
|
||||
loading: false,
|
||||
currentDate: new Date(),
|
||||
tasks: []
|
||||
|
@ -23,14 +28,14 @@
|
|||
},
|
||||
beforeMount() {
|
||||
// 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'})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
auth.logout()
|
||||
},
|
||||
},
|
||||
computed: mapState({
|
||||
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
|
||||
authenticated: state => state.auth.authenticated,
|
||||
userInfo: state => state.auth.info,
|
||||
}),
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
<span
|
||||
v-for="l in labels" :key="l.id"
|
||||
class="tag"
|
||||
:class="{'disabled': user.infos.id !== l.createdBy.id}"
|
||||
:class="{'disabled': userInfo.id !== l.createdBy.id}"
|
||||
:style="{'background': l.hexColor, 'color': l.textColor}"
|
||||
>
|
||||
<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.'">
|
||||
{{ l.title }}
|
||||
</span>
|
||||
|
@ -25,7 +25,7 @@
|
|||
v-else>
|
||||
{{ l.title }}
|
||||
</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>
|
||||
</div>
|
||||
<div class="column is-4" v-if="isLabelEdit">
|
||||
|
@ -102,10 +102,10 @@
|
|||
<script>
|
||||
import verte from 'verte'
|
||||
import 'verte/dist/verte.css'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import LabelService from '../../services/label'
|
||||
import LabelModel from '../../models/label'
|
||||
import auth from '../../auth'
|
||||
|
||||
export default {
|
||||
name: 'ListLabels',
|
||||
|
@ -118,7 +118,6 @@
|
|||
labels: [],
|
||||
labelEditLabel: LabelModel,
|
||||
isLabelEdit: false,
|
||||
user: auth.user,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
@ -126,6 +125,9 @@
|
|||
this.labelEditLabel = new LabelModel()
|
||||
this.loadLabels()
|
||||
},
|
||||
computed: mapState({
|
||||
userInfo: state => state.auth.info
|
||||
}),
|
||||
methods: {
|
||||
loadLabels() {
|
||||
const getAllLabels = (page = 1) => {
|
||||
|
@ -183,7 +185,7 @@
|
|||
})
|
||||
},
|
||||
editLabel(label) {
|
||||
if (label.createdBy.id !== this.user.infos.id) {
|
||||
if (label.createdBy.id !== this.userInfo.id) {
|
||||
return
|
||||
}
|
||||
this.labelEditLabel = label
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
<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>
|
||||
|
||||
<link-sharing :list-id="$route.params.id"/>
|
||||
<link-sharing :list-id="$route.params.id" v-if="linkSharingEnabled"/>
|
||||
|
||||
<modal
|
||||
v-if="showDeleteModal"
|
||||
|
@ -85,7 +85,6 @@
|
|||
import verte from 'verte'
|
||||
import 'verte/dist/verte.css'
|
||||
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import manageSharing from '../sharing/userTeam'
|
||||
import LinkSharing from '../sharing/linkSharing'
|
||||
|
@ -102,8 +101,6 @@
|
|||
listService: ListService,
|
||||
|
||||
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: '',
|
||||
manageTeamsComponent: '',
|
||||
|
@ -115,12 +112,6 @@
|
|||
manageSharing,
|
||||
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() {
|
||||
this.listService = new ListService()
|
||||
this.loadList()
|
||||
|
@ -129,15 +120,20 @@
|
|||
// call again the method if the route changes
|
||||
'$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: {
|
||||
loadList() {
|
||||
let list = new ListModel({id: this.$route.params.id})
|
||||
this.listService.get(list)
|
||||
.then(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.manageTeamsComponent = 'manageSharing'
|
||||
this.manageUsersComponent = 'manageSharing'
|
||||
|
@ -149,15 +145,7 @@
|
|||
submit() {
|
||||
this.listService.update(this.list)
|
||||
.then(r => {
|
||||
// Update the list in the parent
|
||||
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.$store.commit('namespaces/setListInNamespaceById', r)
|
||||
this.success({message: 'The list was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import ListService from '../../services/list'
|
||||
import ListModel from '../../models/list'
|
||||
import {IS_FULLPAGE} from '../../store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: "NewList",
|
||||
|
@ -46,16 +46,10 @@
|
|||
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() {
|
||||
this.list = new ListModel()
|
||||
this.listService = new ListService()
|
||||
this.$parent.setFullPage();
|
||||
this.$store.commit(IS_FULLPAGE, true)
|
||||
},
|
||||
methods: {
|
||||
newList() {
|
||||
|
@ -68,7 +62,8 @@
|
|||
this.list.namespaceId = this.$route.params.id
|
||||
this.listService.create(this.list)
|
||||
.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)
|
||||
router.push({name: 'list.index', params: {listId: response.id}})
|
||||
})
|
||||
|
|
|
@ -22,12 +22,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
|
||||
import ListModel from '../../models/list'
|
||||
import ListService from '../../services/list'
|
||||
import authType from '../../models/authTypes'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -37,12 +35,6 @@
|
|||
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() {
|
||||
this.listService = new ListService()
|
||||
this.list = new ListModel()
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
<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>
|
||||
<div class="migration-services-overview">
|
||||
<router-link :to="{name: 'migrateWunderlist'}">
|
||||
<img src="/images/migration/wunderlist.png" alt="Wunderlist"/>
|
||||
Wunderlist
|
||||
<router-link :to="{name: 'migrate.'+m}" v-for="m in availableMigrators" :key="m">
|
||||
<img :src="`/images/migration/${m}.png`" :alt="m"/>
|
||||
{{ m }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,6 +13,11 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'migrate'
|
||||
name: 'migrate',
|
||||
computed: {
|
||||
availableMigrators() {
|
||||
return this.$store.state.config.availableMigrators
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -83,7 +83,6 @@
|
|||
import verte from 'verte'
|
||||
import 'verte/dist/verte.css'
|
||||
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import manageSharing from '../sharing/userTeam'
|
||||
|
||||
|
@ -96,13 +95,11 @@
|
|||
data() {
|
||||
return {
|
||||
namespaceService: NamespaceService,
|
||||
userIsAdmin: false,
|
||||
manageUsersComponent: '',
|
||||
manageTeamsComponent: '',
|
||||
|
||||
namespace: NamespaceModel,
|
||||
showDeleteModal: false,
|
||||
user: auth.user,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -111,11 +108,6 @@
|
|||
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'})
|
||||
}
|
||||
|
||||
this.namespace.id = this.$route.params.id
|
||||
},
|
||||
created() {
|
||||
|
@ -127,15 +119,17 @@
|
|||
// call again the method if the route changes
|
||||
'$route': 'loadNamespace'
|
||||
},
|
||||
computed: {
|
||||
userIsAdmin() {
|
||||
return this.namespace.owner && this.namespace.owner.id === this.$store.state.auth.info.id
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadNamespace() {
|
||||
let namespace = new NamespaceModel({id: this.$route.params.id})
|
||||
this.namespaceService.get(namespace)
|
||||
.then(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.manageTeamsComponent = 'manageSharing'
|
||||
this.manageUsersComponent = 'manageSharing'
|
||||
|
@ -148,12 +142,7 @@
|
|||
this.namespaceService.update(this.namespace)
|
||||
.then(r => {
|
||||
// Update the namespace in the parent
|
||||
for (const n in this.$parent.namespaces) {
|
||||
if (this.$parent.namespaces[n].id === r.id) {
|
||||
r.lists = this.$parent.namespaces[n].lists
|
||||
this.$set(this.$parent.namespaces, n, r)
|
||||
}
|
||||
}
|
||||
this.$store.commit('namespaces/setNamespaceById', r)
|
||||
this.success({message: 'The namespace was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -34,10 +34,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import NamespaceModel from "../../models/namespace";
|
||||
import NamespaceService from "../../services/namespace";
|
||||
import {IS_FULLPAGE} from '../../store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: "NewNamespace",
|
||||
|
@ -48,16 +48,10 @@
|
|||
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() {
|
||||
this.namespace = new NamespaceModel()
|
||||
this.namespaceService = new NamespaceService()
|
||||
this.$parent.setFullPage();
|
||||
this.$store.commit(IS_FULLPAGE, true)
|
||||
},
|
||||
methods: {
|
||||
newNamespace() {
|
||||
|
@ -68,10 +62,10 @@
|
|||
this.showError = false
|
||||
|
||||
this.namespaceService.create(this.namespace)
|
||||
.then(() => {
|
||||
this.$parent.loadNamespaces()
|
||||
.then(r => {
|
||||
this.$store.commit('namespaces/addNamespace', r)
|
||||
this.success({message: 'The namespace was successfully created.'}, this)
|
||||
router.push({name: 'home'})
|
||||
router.back()
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
</template>
|
||||
</td>
|
||||
<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">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
|
@ -106,6 +106,7 @@
|
|||
import LinkShareModel from '../../models/linkShare'
|
||||
|
||||
import copy from 'copy-to-clipboard'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'linkSharing',
|
||||
|
@ -138,6 +139,9 @@
|
|||
this.load()
|
||||
}
|
||||
},
|
||||
computed: mapState({
|
||||
frontendUrl: state => state.config.frontendUrl,
|
||||
}),
|
||||
methods: {
|
||||
load() {
|
||||
// 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)
|
||||
},
|
||||
getShareLink(hash) {
|
||||
return this.$config.frontend_url + 'share/' + hash + '/auth'
|
||||
return this.frontendUrl + 'share/' + hash + '/auth'
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
|
||||
export default {
|
||||
|
@ -25,7 +24,7 @@
|
|||
},
|
||||
methods: {
|
||||
auth() {
|
||||
auth.linkShareAuth(this.$route.params.share)
|
||||
this.$store.dispatch('auth/linkShareAuth', this.$route.params.share)
|
||||
.then((r) => {
|
||||
this.loading = false
|
||||
router.push({name: 'list.list', params: {listId: r.list_id}})
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<template v-if="shareType === 'user'">
|
||||
<td>{{s.username}}</td>
|
||||
<td>
|
||||
<template v-if="s.id === currentUser.id">
|
||||
<template v-if="s.id === userInfo.id">
|
||||
<b class="is-success">You</b>
|
||||
</template>
|
||||
</td>
|
||||
|
@ -105,8 +105,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import multiselect from 'vue-multiselect'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import UserNamespaceService from '../../services/userNamespace'
|
||||
import UserNamespaceModel from '../../models/userNamespace'
|
||||
|
@ -156,7 +156,6 @@
|
|||
rights: rights,
|
||||
selectedRight: rights.READ,
|
||||
|
||||
currentUser: auth.user.infos,
|
||||
typeString: '',
|
||||
sharables: [], // This holds either teams or users who this namepace or list is shared with
|
||||
showDeleteModal: false,
|
||||
|
@ -165,6 +164,9 @@
|
|||
components: {
|
||||
multiselect
|
||||
},
|
||||
computed: mapState({
|
||||
userInfo: state => state.auth.info
|
||||
}),
|
||||
created() {
|
||||
|
||||
if (this.shareType === 'user') {
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
</div>
|
||||
<div class="media comment">
|
||||
<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>
|
||||
<div class="media-content">
|
||||
<div class="form">
|
||||
|
@ -67,7 +67,6 @@
|
|||
<script>
|
||||
import TaskCommentService from '../../../services/taskComment'
|
||||
import TaskCommentModel from '../../../models/taskComment'
|
||||
import auth from '../../../auth'
|
||||
|
||||
export default {
|
||||
name: 'comments',
|
||||
|
@ -80,7 +79,6 @@
|
|||
data() {
|
||||
return {
|
||||
comments: [],
|
||||
user: auth.user,
|
||||
|
||||
showDeleteModal: false,
|
||||
commentToDelete: TaskCommentModel,
|
||||
|
@ -107,6 +105,11 @@
|
|||
this.loadComments()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userAvatar() {
|
||||
return this.$store.state.auth.info.getAvatarUrl(48)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadComments() {
|
||||
this.taskCommentService.getAll({taskId: this.taskId})
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
<tr v-for="m in team.members" :key="m.id">
|
||||
<td>{{m.username}}</td>
|
||||
<td>
|
||||
<template v-if="m.id === user.infos.id">
|
||||
<template v-if="m.id === userInfo.id">
|
||||
<b class="is-success">You</b>
|
||||
</template>
|
||||
</td>
|
||||
|
@ -127,7 +127,7 @@
|
|||
</td>
|
||||
<td class="actions" v-if="userIsAdmin">
|
||||
<button @click="toggleUserType(m)" class="button buttonright is-primary"
|
||||
v-if="m.id !== user.infos.id">
|
||||
v-if="m.id !== userInfo.id">
|
||||
Make
|
||||
<template v-if="!m.admin">
|
||||
Admin
|
||||
|
@ -137,7 +137,7 @@
|
|||
</template>
|
||||
</button>
|
||||
<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">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
|
@ -173,9 +173,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import multiselect from 'vue-multiselect'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import TeamService from '../../services/team'
|
||||
import TeamModel from '../../models/team'
|
||||
|
@ -196,7 +196,6 @@
|
|||
|
||||
showDeleteModal: false,
|
||||
showUserDeleteModal: false,
|
||||
user: auth.user,
|
||||
userIsAdmin: false,
|
||||
|
||||
newMember: UserModel,
|
||||
|
@ -209,12 +208,6 @@
|
|||
components: {
|
||||
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() {
|
||||
this.teamService = new TeamService()
|
||||
this.teamMemberService = new TeamMemberService()
|
||||
|
@ -225,9 +218,11 @@
|
|||
// call again the method if the route changes
|
||||
'$route': 'loadTeam'
|
||||
},
|
||||
computed: mapState({
|
||||
userInfo: state => state.auth.info,
|
||||
}),
|
||||
methods: {
|
||||
loadTeam() {
|
||||
// this.member = new TeamMemberModel({teamId: this.teamId})
|
||||
this.team = new TeamModel({id: this.teamId})
|
||||
this.teamService.get(this.team)
|
||||
.then(response => {
|
||||
|
@ -235,7 +230,7 @@
|
|||
let members = response.members
|
||||
for (const m in members) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import TeamService from '../../services/team'
|
||||
|
||||
export default {
|
||||
|
@ -30,12 +28,6 @@
|
|||
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() {
|
||||
this.teamService = new TeamService()
|
||||
this.loadTeams()
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import TeamModel from '../../models/team'
|
||||
import TeamService from '../../services/team'
|
||||
import {IS_FULLPAGE} from '../../store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: "NewTeam",
|
||||
|
@ -46,16 +46,10 @@
|
|||
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() {
|
||||
this.teamService = new TeamService()
|
||||
this.team = new TeamModel()
|
||||
this.$parent.setFullPage();
|
||||
this.$store.commit(IS_FULLPAGE, true)
|
||||
},
|
||||
methods: {
|
||||
newTeam() {
|
||||
|
|
|
@ -9,31 +9,61 @@
|
|||
<div class="field">
|
||||
<label class="label" for="username">Username</label>
|
||||
<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 class="field">
|
||||
<label class="label" for="password">Password</label>
|
||||
<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 class="field" v-if="needsTotpPasscode">
|
||||
<label class="label" for="totpPasscode">Two Factor Authentication Code</label>
|
||||
<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 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">
|
||||
<button type="submit" class="button is-primary" v-bind:class="{ 'is-loading': loading}">Login</button>
|
||||
<router-link :to="{ name: 'register' }" class="button">Register</router-link>
|
||||
<router-link :to="{ name: 'getPasswordReset' }" class="reset-password-link">Reset your password</router-link>
|
||||
<router-link :to="{ name: 'getPasswordReset' }" class="reset-password-link">Reset your
|
||||
password
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification is-danger" v-if="errorMsg">
|
||||
{{ errorMsg }}
|
||||
<div class="notification is-danger" v-if="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -41,18 +71,17 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
import {ERROR_MESSAGE, LOADING} from '../../store/mutation-types'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
errorMsg: '',
|
||||
confirmedEmailSuccess: false,
|
||||
loading: false,
|
||||
needsTotpPasscode: false,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
@ -69,19 +98,25 @@
|
|||
})
|
||||
.catch(e => {
|
||||
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
|
||||
if (auth.user.authenticated) {
|
||||
if (this.authenticated) {
|
||||
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: {
|
||||
submit() {
|
||||
this.loading = true
|
||||
this.errorMsg = ''
|
||||
this.$store.commit(ERROR_MESSAGE, '')
|
||||
// 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.
|
||||
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
||||
|
@ -90,11 +125,15 @@
|
|||
password: this.$refs.password.value,
|
||||
}
|
||||
|
||||
if(this.needsTotpPasscode) {
|
||||
if (this.needsTotpPasscode) {
|
||||
credentials.totpPasscode = this.$refs.totpPasscode.value
|
||||
}
|
||||
|
||||
auth.login(this, credentials, 'home')
|
||||
this.$store.dispatch('auth/login', credentials)
|
||||
.then(() => {
|
||||
router.push({name: 'home'})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +144,7 @@
|
|||
margin: 0 0.4em 0 0;
|
||||
}
|
||||
|
||||
.reset-password-link{
|
||||
.reset-password-link {
|
||||
display: inline-block;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
|
|
@ -37,8 +37,8 @@
|
|||
<div class="notification is-info" v-if="loading">
|
||||
Loading...
|
||||
</div>
|
||||
<div class="notification is-danger" v-if="errorMsg !== ''">
|
||||
{{ errorMsg }}
|
||||
<div class="notification is-danger" v-if="errorMessage !== ''">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -46,8 +46,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import {mapState} from 'vuex'
|
||||
import {ERROR_MESSAGE, LOADING} from '../../store/mutation-types'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -58,35 +59,37 @@
|
|||
password: '',
|
||||
password2: '',
|
||||
},
|
||||
errorMsg: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
// 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'})
|
||||
}
|
||||
},
|
||||
computed: mapState({
|
||||
authenticated: state => state.auth.authenticated,
|
||||
loading: LOADING,
|
||||
errorMessage: ERROR_MESSAGE,
|
||||
}),
|
||||
methods: {
|
||||
submit() {
|
||||
this.loading = true
|
||||
|
||||
this.errorMsg = ''
|
||||
this.$store.commit(LOADING, true)
|
||||
this.$store.commit(ERROR_MESSAGE, '')
|
||||
|
||||
if (this.credentials.password2 !== this.credentials.password) {
|
||||
this.loading = false
|
||||
this.errorMsg = 'Passwords don\'t match.'
|
||||
this.$store.commit(ERROR_MESSAGE, 'Passwords don\'t match.')
|
||||
this.$store.commit(LOADING, false)
|
||||
return
|
||||
}
|
||||
|
||||
let credentials = {
|
||||
const credentials = {
|
||||
username: this.credentials.username,
|
||||
email: this.credentials.email,
|
||||
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 App from './App.vue'
|
||||
import router from './router'
|
||||
import auth from './auth'
|
||||
|
||||
// Register the modal
|
||||
import Modal from './components/modal/Modal'
|
||||
|
@ -20,12 +19,6 @@ Vue.config.productionTip = false
|
|||
import Notifications from 'vue-notification'
|
||||
Vue.use(Notifications)
|
||||
|
||||
import config from './config'
|
||||
config.initConfig()
|
||||
.then(() => {
|
||||
Vue.prototype.$config = config.getConfig()
|
||||
})
|
||||
|
||||
// Icons
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
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
|
||||
import message from './message'
|
||||
import {format, formatDistance} from 'date-fns'
|
||||
|
@ -165,7 +155,11 @@ Vue.mixin({
|
|||
}
|
||||
})
|
||||
|
||||
// Vuex
|
||||
import {store} from './store'
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { register } from 'register-service-worker'
|
||||
import swEvents from './ServiceWorker/events'
|
||||
import auth from './auth'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
register(`${process.env.BASE_URL}sw.js`, {
|
||||
|
@ -45,7 +44,7 @@ if(navigator && navigator.serviceWorker) {
|
|||
if(action === 'getBearerToken') {
|
||||
console.debug('Token request from sw');
|
||||
port.postMessage({
|
||||
authToken: auth.getToken(),
|
||||
authToken: localStorage.getItem('token'),
|
||||
})
|
||||
} else {
|
||||
console.error('Unknown event', event);
|
||||
|
|
|
@ -197,7 +197,7 @@ export default new Router({
|
|||
},
|
||||
{
|
||||
path: '/migrate/wunderlist',
|
||||
name: 'migrateWunderlist',
|
||||
name: 'migrate.wunderlist',
|
||||
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 {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
text-transform: capitalize;
|
||||
|
||||
img {
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"
|
||||
|
|
Reference in New Issue
Block a user