feature/feat-pinia-auth-store | pinia 6/9 #2398

Merged
konrad merged 4 commits from dpschen/frontend:feature/feat-pinia-auth-store into main 2022-09-29 11:20:23 +00:00
44 changed files with 349 additions and 276 deletions

View File

@ -36,15 +36,17 @@ import AccountDeleteService from '@/services/accountDelete'
import {useColorScheme} from '@/composables/useColorScheme'
import {useBodyClass} from '@/composables/useBodyClass'
import {useAuthStore} from './stores/auth'
const store = useStore()
const authStore = useAuthStore()
const router = useRouter()
useBodyClass('is-touch', isTouchDevice())
const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive)
const authUser = computed(() => store.getters['auth/authUser'])
const authLinkShare = computed(() => store.getters['auth/authLinkShare'])
const authUser = computed(() => authStore.authUser)
const authLinkShare = computed(() => authStore.authLinkShare)
dpschen marked this conversation as resolved
Review

Don't we need to call that function?

Don't we need to call that function?
Review

No, that would only be necessary if the getter would return a function that we want to call.

No, that would only be necessary if the getter [would return a function that we want to call](https://pinia.vuejs.org/core-concepts/getters.html#passing-arguments-to-getters).
const {t} = useI18n({useScope: 'global'})
@ -58,7 +60,7 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
const accountDeletionService = new AccountDeleteService()
await accountDeletionService.confirm(accountDeletionConfirm)
success({message: t('user.deletion.confirmSuccess')})
store.dispatch('auth/refreshUserInfo')
authStore.refreshUserInfo()
}, { immediate: true })
// setup password reset redirect

View File

@ -71,7 +71,6 @@
<script lang="ts" setup>
import {computed, ref, watch} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component'
@ -81,8 +80,9 @@ import Popup from '@/components/misc/popup.vue'
import {DATE_RANGES} from '@/components/date/dateRanges'
import BaseButton from '@/components/base/BaseButton.vue'
import DatemathHelp from '@/components/date/datemathHelp.vue'
import {useAuthStore} from '@/stores/auth'
const store = useStore()
const authStore = useAuthStore()
const {t} = useI18n({useScope: 'global'})
const emit = defineEmits(['update:modelValue'])
@ -93,7 +93,7 @@ const props = defineProps({
})
// FIXME: This seems to always contain the default value - that breaks the picker
const weekStart = computed<number>(() => store.state.auth.settings.weekStart ?? 0)
const weekStart = computed(() => authStore.settings.weekStart ?? 0)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,

View File

@ -93,7 +93,6 @@
<script setup lang="ts">
import {ref, computed, onMounted, nextTick} from 'vue'
import {useStore} from '@/store'
import {useRouter} from 'vue-router'
import {QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import {RIGHTS as Rights} from '@/constants/rights'
@ -109,12 +108,14 @@ import MenuButton from '@/components/home/MenuButton.vue'
import {getListTitle} from '@/helpers/getListTitle'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
const store = useStore()
const authStore = useAuthStore()
const configStore = useConfigStore()
const userInfo = computed(() => store.state.auth.info)
const userAvatar = computed(() => store.state.auth.avatarUrl)
const userInfo = computed(() => authStore.info)
const userAvatar = computed(() => authStore.avatarUrl)
const currentList = computed(() => store.state.currentList)
const background = computed(() => store.state.background)
const imprintUrl = computed(() => configStore.legal.imprintUrl)
@ -134,11 +135,8 @@ onMounted(async () => {
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
})
const router = useRouter()
function logout() {
store.dispatch('auth/logout')
router.push({name: 'user.login'})
authStore.logout()
}
function openQuickActions() {

View File

@ -70,6 +70,7 @@ import {useLabelStore} from '@/stores/labels'
import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {useAuthStore} from '@/stores/auth'
function useRouteWithModal() {
const router = useRouter()
@ -165,13 +166,15 @@ watch(() => route.name as string, (routeName) => {
function useRenewTokenOnFocus() {
const router = useRouter()
const authStore = useAuthStore()
const userInfo = computed(() => store.state.auth.info)
const authenticated = computed(() => store.state.auth.authenticated)
const userInfo = computed(() => authStore.info)
const authenticated = computed(() => authStore.authenticated)
// Try renewing the token every time vikunja is loaded initially
// (When opening the browser the focus event is not fired)
store.dispatch('auth/renewToken')
authStore.renewToken()
// Check if the token is still valid if the window gets focus again to maybe renew it
useEventListener('focus', () => {
@ -184,14 +187,14 @@ function useRenewTokenOnFocus() {
// If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page
if (expiresIn < 0) {
store.dispatch('auth/checkAuth')
authStore.checkAuth()
router.push({name: 'user.login'})
return
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
store.dispatch('auth/renewToken')
authStore.renewToken()
console.debug('renewed token')
}
})

View File

@ -102,6 +102,8 @@ import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {createDateFromString} from '@/helpers/time/createDateFromString'
import {mapState} from 'pinia'
import {useAuthStore} from '@/stores/auth'
export default defineComponent({
name: 'datepicker',
@ -145,6 +147,9 @@ export default defineComponent({
},
},
computed: {
...mapState(useAuthStore, {
weekStart: (state) => state.settings.weekStart,
}),
flatPickerConfig() {
return {
altFormat: this.$t('date.altFormatLong'),
@ -154,7 +159,7 @@ export default defineComponent({
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: this.$store.state.auth.settings.weekStart,
firstDayOfWeek: this.weekStart,
},
}
},

View File

@ -2,34 +2,39 @@
<div :class="{'is-inline': isInline}" class="user">
<img
:height="avatarSize"
:src="user.getAvatarUrl(avatarSize)"
:src="getAvatarUrl(user, avatarSize)"
:width="avatarSize"
alt=""
class="avatar"
v-tooltip="user.getDisplayName()"/>
<span class="username" v-if="showUsername">{{ user.getDisplayName() }}</span>
v-tooltip="getDisplayName(user)"/>
<span class="username" v-if="showUsername">{{ getDisplayName(user) }}</span>
</div>
</template>
<script lang="ts" setup>
import type {PropType} from 'vue'
import {getAvatarUrl, getDisplayName} from '@/models/user'
import type {IUser} from '@/modelTypes/IUser'
defineProps({
user: {
type: Object as PropType<IUser>,
dpschen marked this conversation as resolved Outdated

Please fix the indention here.

Please fix the indention here.

Done

Done
required: true,
type: Object,
},
showUsername: {
required: false,
type: Boolean,
required: false,
default: true,
},
avatarSize: {
required: false,
type: Number,
required: false,
default: 50,
},
isInline: {
required: false,
type: Boolean,
required: false,
default: false,
},
})

View File

@ -24,7 +24,7 @@
<div class="detail">
<div>
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
{{ n.notification.doer.getDisplayName() }}
{{ getDisplayName(n.notification.doer) }}
</span>
<BaseButton @click="() => to(n, index)()">
{{ n.toText(userInfo) }}
@ -48,19 +48,20 @@
<script lang="ts" setup>
import {computed, onMounted, onUnmounted, ref} from 'vue'
import {useRouter} from 'vue-router'
import NotificationService from '@/services/notification'
import BaseButton from '@/components/base/BaseButton.vue'
import User from '@/components/misc/user.vue'
import { NOTIFICATION_NAMES as names, type INotification} from '@/modelTypes/INotification'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {useStore} from '@/store'
import {useRouter} from 'vue-router'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import {getDisplayName} from '@/models/user'
import {useAuthStore} from '@/stores/auth'
const LOAD_NOTIFICATIONS_INTERVAL = 10000
const store = useStore()
const authStore = useAuthStore()
const router = useRouter()
const allNotifications = ref<INotification[]>([])
@ -73,7 +74,7 @@ const unreadNotifications = computed(() => {
const notifications = computed(() => {
return allNotifications.value ? allNotifications.value.filter(n => n.name !== '') : []
})
const userInfo = computed(() => store.state.auth.info)
const userInfo = computed(() => authStore.info)
let interval: number

View File

@ -93,7 +93,7 @@
<p class="mb-2">
<i18n-t keypath="list.share.links.sharedBy" scope="global">
<strong>{{ s.sharedBy.getDisplayName() }}</strong>
<strong>{{ getDisplayName(s.sharedBy) }}</strong>
</i18n-t>
</p>
@ -201,6 +201,7 @@ import LinkShareService from '@/services/linkShare'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message'
import {getDisplayName} from '@/models/user'
import type {ListView} from '@/types/ListView'
import {LIST_VIEWS} from '@/types/ListView'
import {useConfigStore} from '@/stores/config'

View File

@ -28,7 +28,7 @@
<tbody>
<tr :key="s.id" v-for="s in sharables">
<template v-if="shareType === 'user'">
<td>{{ s.getDisplayName() }}</td>
<td>{{ getDisplayName(s) }}</td>
<td>
<template v-if="s.id === userInfo.id">
<b class="is-success">{{ $t('list.share.userTeam.you') }}</b>
@ -139,7 +139,6 @@ export default {name: 'userTeamShare'}
<script setup lang="ts">
import {ref, reactive, computed, shallowReactive, type Ref} from 'vue'
import type {PropType} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import UserNamespaceService from '@/services/userNamespace'
@ -151,7 +150,7 @@ import UserListModel from '@/models/userList'
import type {IUserList} from '@/modelTypes/IUserList'
import UserService from '@/services/user'
import UserModel from '@/models/user'
import UserModel, { getDisplayName } from '@/models/user'
import type {IUser} from '@/modelTypes/IUser'
import TeamNamespaceService from '@/services/teamNamespace'
@ -171,6 +170,7 @@ import {RIGHTS} from '@/constants/rights'
import Multiselect from '@/components/input/multiselect.vue'
import Nothing from '@/components/misc/nothing.vue'
import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
const props = defineProps({
type: {
@ -208,8 +208,8 @@ const sharables = ref([])
const showDeleteModal = ref(false)
const store = useStore()
const userInfo = computed(() => store.state.auth.info)
const authStore = useAuthStore()
const userInfo = computed(() => authStore.info)
function createShareTypeNameComputed(count: number) {
return computed(() => {
@ -365,7 +365,7 @@ async function toggleType(sharable) {
const found = ref([])
const currentUserId = computed(() => store.state.auth.info.id)
const currentUserId = computed(() => authStore.info.id)
async function find(query: string) {
if (query === '') {
found.value = []

View File

@ -48,6 +48,7 @@ import {tryOnMounted, debouncedWatch, useWindowSize, type MaybeRef} from '@vueus
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import {useAuthStore} from '@/stores/auth'
function cleanupTitle(title: string) {
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '')
@ -135,6 +136,7 @@ const newTaskInput = useAutoHeightTextarea(newTaskTitle)
const {t} = useI18n({useScope: 'global'})
const store = useStore()
const authStore = useAuthStore()
const errorMessage = ref('')
@ -168,7 +170,7 @@ async function addTask() {
const task = await store.dispatch('tasks/createNewTask', {
title,
listId: store.state.auth.settings.defaultListId,
listId: authStore.settings.defaultListId,
position: props.defaultPosition,
})
emit('taskAdded', task)

View File

@ -17,7 +17,7 @@
<div :key="c.id" class="media comment" v-for="c in comments">
<figure class="media-left is-hidden-mobile">
<img
:src="c.author.getAvatarUrl(48)"
:src="getAvatarUrl(c.author, 48)"
alt=""
class="image is-avatar"
height="48"
@ -27,13 +27,13 @@
<div class="media-content">
<div class="comment-info">
<img
:src="c.author.getAvatarUrl(20)"
:src="getAvatarUrl(c.author, 20)"
alt=""
class="image is-avatar d-print-none"
height="20"
width="20"
/>
<strong>{{ c.author.getDisplayName() }}</strong>&nbsp;
<strong>{{ getDisplayName(c.author) }}</strong>&nbsp;
<span v-tooltip="formatDateLong(c.created)" class="has-text-grey">
{{ formatDateSince(c.created) }}
</span>
@ -153,7 +153,6 @@
<script setup lang="ts">
import {ref, reactive, computed, shallowReactive, watch, nextTick} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor'
@ -167,7 +166,9 @@ import type {ITask} from '@/modelTypes/ITask'
import {uploadFile} from '@/helpers/attachments'
import {success} from '@/message'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import {getAvatarUrl, getDisplayName} from '@/models/user'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
const props = defineProps({
taskId: {
@ -180,8 +181,8 @@ const props = defineProps({
})
const {t} = useI18n({useScope: 'global'})
const store = useStore()
const configStore = useConfigStore()
const authStore = useAuthStore()
const comments = ref<ITaskComment[]>([])
@ -196,8 +197,8 @@ const newComment = reactive(new TaskCommentModel())
const saved = ref<ITask['id'] | null>(null)
const saving = ref<ITask['id'] | null>(null)
const userAvatar = computed(() => store.state.auth.info.getAvatarUrl(48))
const currentUserId = computed(() => store.state.auth.info.id)
const userAvatar = computed(() => getAvatarUrl(authStore.info, 48))
const currentUserId = computed(() => authStore.info.id)
const enabled = computed(() => configStore.taskCommentsEnabled)
const actions = computed(() => {
if (!props.canWrite) {

View File

@ -3,7 +3,7 @@
<time :datetime="formatISO(task.created)" v-tooltip="formatDateLong(task.created)">
<i18n-t keypath="task.detail.created" scope="global">
<span>{{ formatDateSince(task.created) }}</span>
{{ task.createdBy.getDisplayName() }}
{{ getDisplayName(task.createdBy) }}
</i18n-t>
</time>
<template v-if="+new Date(task.created) !== +new Date(task.updated)">
@ -30,6 +30,7 @@
import {computed, toRefs, type PropType} from 'vue'
import type {ITask} from '@/modelTypes/ITask'
import {formatISO, formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import {getDisplayName} from '@/models/user'
const props = defineProps({
task: {

View File

@ -38,13 +38,13 @@
</template>
<script setup lang="ts">
import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount, type PropType} from 'vue'
import {useStore} from '@/store'
import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount, toRef, type PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component'
import TaskService from '@/services/task'
import type {ITask} from '@/modelTypes/ITask'
import {useAuthStore} from '@/stores/auth'
const props = defineProps({
modelValue: {
@ -55,7 +55,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const {t} = useI18n({useScope: 'global'})
const store = useStore()
const authStore = useAuthStore()
const taskService = shallowReactive(new TaskService())
const task = ref<ITask>()
@ -63,12 +63,12 @@ const task = ref<ITask>()
// We're saving the due date seperately to prevent null errors in very short periods where the task is null.
const dueDate = ref<Date>()
const lastValue = ref<Date>()
const changeInterval = ref<number>()
const changeInterval = ref<ReturnType<typeof setInterval>>()
watch(
() => props.modelValue,
toRef(props, 'modelValue'),
(value) => {
task.value = value
task.value = { ...value }
dueDate.value = value.dueDate
lastValue.value = value.dueDate
},
@ -103,7 +103,7 @@ const flatPickerConfig = computed(() => ({
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: store.state.auth.settings.weekStart,
firstDayOfWeek: authStore.settings.weekStart,
},
}))
@ -123,9 +123,10 @@ async function updateDueDate() {
return
}
// FIXME: direct prop manipulation
task.value.dueDate = new Date(dueDate.value)
const newTask = await taskService.update(task.value)
const newTask = await taskService.update({
dpschen marked this conversation as resolved Outdated

Is that still true?

Is that still true?

I changed the watcher of the modelValue now so that we create a shallow copy of the task. Maybe a deep copy would be even better here.

My reasoning here is:
If the modelValue is provided by a value that is bound from store e.g. via a getter we the task would have been a direct reference to that. Now that we create a shallow copy this shouldn't be the case anymore.

I changed the watcher of the modelValue now so that we create a shallow copy of the task. Maybe a deep copy would be even better here. My reasoning here is: If the modelValue is provided by a value that is bound from store e.g. via a getter we the task would have been a direct reference to that. Now that we create a shallow copy this shouldn't be the case anymore.

Makes sense!

Makes sense!
...task.value,
dueDate: new Date(dueDate.value),
})
lastValue.value = newTask.dueDate
task.value = newTask
emit('update:modelValue', newTask)

View File

@ -1,11 +1,21 @@
import type {IAbstract} from './IAbstract'
import type {IUserSettings} from './IUserSettings'
export const AUTH_TYPES = {
'UNKNOWN': 0,
'USER': 1,
'LINK_SHARE': 2,
} as const
type AuthType = typeof AUTH_TYPES[keyof typeof AUTH_TYPES]
export interface IUser extends IAbstract {
id: number
email: string
username: string
name: string
exp: number
type: AuthType
created: Date
updated: Date

View File

@ -11,4 +11,5 @@ export interface IUserSettings extends IAbstract {
defaultListId: undefined | IList['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string
language: string
}

View File

@ -1,12 +1,13 @@
import AbstractModel from './abstractModel'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import UserModel from '@/models/user'
import UserModel, {getDisplayName} from '@/models/user'
import TaskModel from '@/models/task'
import TaskCommentModel from '@/models/taskComment'
import ListModel from '@/models/list'
import TeamModel from '@/models/team'
import {NOTIFICATION_NAMES, type INotification} from '@/modelTypes/INotification'
import type { IUser } from '@/modelTypes/IUser'
export default class NotificationModel extends AbstractModel<INotification> implements INotification {
id = 0
@ -61,14 +62,14 @@ export default class NotificationModel extends AbstractModel<INotification> impl
this.readAt = parseDateOrNull(this.readAt)
}
toText(user = null) {
toText(user: IUser | null = null) {
let who = ''
switch (this.name) {
case NOTIFICATION_NAMES.TASK_COMMENT:
return `commented on ${this.notification.task.getTextIdentifier()}`
case NOTIFICATION_NAMES.TASK_ASSIGNED:
who = `${this.notification.assignee.getDisplayName()}`
who = `${getDisplayName(this.notification.assignee)}`
if (user !== null && user.id === this.notification.assignee.id) {
who = 'you'
@ -80,7 +81,7 @@ export default class NotificationModel extends AbstractModel<INotification> impl
case NOTIFICATION_NAMES.LIST_CREATED:
return `created ${this.notification.list.title}`
case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED:
who = `${this.notification.member.getDisplayName()}`
who = `${getDisplayName(this.notification.member)}`
if (user !== null && user.id === this.notification.member.id) {
who = 'you'

View File

@ -1,18 +1,32 @@
import AbstractModel from './abstractModel'
import UserSettingsModel from '@/models/userSettings'
import type { IUser } from '@/modelTypes/IUser'
import { AUTH_TYPES, type IUser } from '@/modelTypes/IUser'
import type { IUserSettings } from '@/modelTypes/IUserSettings'
export function getAvatarUrl(user: IUser, size = 50) {
return `${window.API_URL}/avatar/${user.username}?size=${size}`
}
export function getDisplayName(user: IUser) {
if (user.name !== '') {
return user.name
}
return user.username
}
export default class UserModel extends AbstractModel<IUser> implements IUser {
id = 0
email = ''
username = ''
name = ''
exp = 0
type = AUTH_TYPES.UNKNOWN
created: Date = null
updated: Date = null
settings: IUserSettings = null
created: Date
updated: Date
settings: IUserSettings
constructor(data: Partial<IUser> = {}) {
super()
@ -21,20 +35,6 @@ export default class UserModel extends AbstractModel<IUser> implements IUser {
this.created = new Date(this.created)
this.updated = new Date(this.updated)
if (this.settings !== null) {
this.settings = new UserSettingsModel(this.settings)
}
}
getAvatarUrl(size = 50) {
return `${window.API_URL}/avatar/${this.username}?size=${size}`
}
getDisplayName() {
if (this.name !== '') {
return this.name
}
return this.username
this.settings = new UserSettingsModel(this.settings || {})
}
}

View File

@ -1,8 +1,7 @@
import AbstractModel from './abstractModel'
import type {IUserSettings} from '@/modelTypes/IUserSettings'
import type {IList} from '@/modelTypes/IList'
import {getCurrentLanguage} from '@/i18n'
export default class UserSettingsModel extends AbstractModel<IUserSettings> implements IUserSettings {
name = ''
@ -10,11 +9,12 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
discoverableByName = false
discoverableByEmail = false
overdueTasksRemindersEnabled = true
defaultListId: undefined | IList['id'] = undefined
weekStart: IUserSettings['weekStart'] = 0
defaultListId = undefined
weekStart = 0 as IUserSettings['weekStart']
timezone = ''
language = getCurrentLanguage()
constructor(data: Partial<IUserSettings>) {
constructor(data: Partial<IUserSettings> = {}) {
super()
this.assignData(data)
}

View File

@ -1,7 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import {saveLastVisited} from '@/helpers/saveLastVisited'
import {store} from '@/store'
import {saveListView, getListView} from '@/helpers/saveListView'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
@ -9,6 +8,7 @@ import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
import {setTitle} from '@/helpers/setTitle'
import {useListStore} from '@/stores/lists'
import {useAuthStore} from '@/stores/auth'
import HomeComponent from '../views/Home.vue'
import NotFoundComponent from '../views/404.vue'
@ -464,10 +464,8 @@ const router = createRouter({
})
export function getAuthForRoute(route: RouteLocation) {
const authUser = store.getters['auth/authUser']
const authLinkShare = store.getters['auth/authLinkShare']
if (authUser || authLinkShare) {
const authStore = useAuthStore()
if (authStore.authUser || authStore.authLinkShare) {
return
}

View File

@ -13,7 +13,6 @@ import {
MENU_ACTIVE,
QUICK_ACTIONS_ACTIVE,
} from './mutation-types'
import auth from './modules/auth'
import kanban from './modules/kanban'
import tasks from './modules/tasks'
@ -23,6 +22,7 @@ import ListService from '../services/list'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import type { RootStoreState, StoreState } from './types'
import {useAuthStore} from '@/stores/auth'
export const key: InjectionKey<Store<StoreState>> = Symbol()
@ -34,7 +34,6 @@ export function useStore () {
export const store = createStore<RootStoreState>({
strict: import.meta.env.DEV,
modules: {
auth,
kanban,
tasks,
},
@ -131,9 +130,10 @@ export const store = createStore<RootStoreState>({
commit(CURRENT_LIST, list)
},
async loadApp({dispatch}) {
async loadApp() {
await checkAndSetApiUrl(window.API_URL)
await dispatch('auth/checkAuth')
const authStore = useAuthStore()
await authStore.checkAuth()
},
},
})

View File

@ -4,6 +4,7 @@ import type { IList } from '@/modelTypes/IList'
import type { IAttachment } from '@/modelTypes/IAttachment'
import type { ILabel } from '@/modelTypes/ILabel'
import type { INamespace } from '@/modelTypes/INamespace'
import type { IUser } from '@/modelTypes/IUser'
export interface RootStoreState {
loading: boolean,
@ -22,29 +23,16 @@ export interface AttachmentState {
attachments: IAttachment[],
}
export const AUTH_TYPES = {
'UNKNOWN': 0,
'USER': 1,
'LINK_SHARE': 2,
} as const
export interface Info {
id: number // what kind of id is this?
type: typeof AUTH_TYPES[keyof typeof AUTH_TYPES],
getAvatarUrl: () => string
settings: IUserSettings
name: string
email: string
exp: any
}
export interface AuthState {
authenticated: boolean,
isLinkShareAuth: boolean,
info: Info | null,
info: IUser | null,
needsTotpPasscode: boolean,
avatarUrl: string,
lastUserInfoRefresh: Date | null,
settings: IUserSettings,
isLoading: boolean,
isLoadingGeneralSettings: boolean
}
export interface ConfigState {

View File

@ -1,38 +1,35 @@
import type { Module } from 'vuex'
import {defineStore, acceptHMRUpdate} from 'pinia'
import {HTTPFactory, AuthenticatedHTTPFactory} from '@/http-common'
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
import {objectToSnakeCase} from '@/helpers/case'
import {LOADING} from '../mutation-types'
import UserModel from '@/models/user'
import UserModel, { getAvatarUrl } from '@/models/user'
import UserSettingsService from '@/services/userSettings'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setLoading} from '@/store/helper'
import {setLoadingPinia} from '@/store/helper'
import {success} from '@/message'
import {redirectToProvider} from '@/helpers/redirectToProvider'
import type { RootStoreState, AuthState, Info} from '@/store/types'
import {AUTH_TYPES} from '@/store/types'
import type { IUserSettings } from '@/modelTypes/IUserSettings'
import {AUTH_TYPES, type IUser} from '@/modelTypes/IUser'
import type {AuthState} from '@/store/types'
import type {IUserSettings} from '@/modelTypes/IUserSettings'
import router from '@/router'
import {useConfigStore} from '@/stores/config'
import UserSettingsModel from '@/models/userSettings'
import {store} from '@/store'
function defaultSettings(settings: Partial<IUserSettings>) {
if (typeof settings.weekStart === 'undefined' || settings.weekStart === '') {
settings.weekStart = 0
}
return settings
}
const authStore : Module<AuthState, RootStoreState> = {
namespaced: true,
state: () => ({
export const useAuthStore = defineStore('auth', {
state: () : AuthState => ({
authenticated: false,
isLinkShareAuth: false,
info: null,
needsTotpPasscode: false,
info: null,
avatarUrl: '',
settings: new UserSettingsModel(),
lastUserInfoRefresh: null,
settings: {}, // should be IUserSettings
isLoading: false,
isLoadingGeneralSettings: false,
}),
getters: {
authUser(state) {
@ -48,47 +45,56 @@ const authStore : Module<AuthState, RootStoreState> = {
)
},
},
mutations: {
info(state, info: Info) {
state.info = info
actions: {
setIsLoading(isLoading: boolean) {
this.isLoading = isLoading
},
setIsLoadingGeneralSettings(isLoading: boolean) {
this.isLoadingGeneralSettings = isLoading
},
setUser(info: IUser | null) {
this.info = info
if (info !== null) {
state.avatarUrl = info.getAvatarUrl()
this.reloadAvatar()
if (info.settings) {
state.settings = defaultSettings(info.settings)
this.settings = new UserSettingsModel(info.settings)
}
state.isLinkShareAuth = info.id < 0
this.isLinkShareAuth = info.id < 0
}
},
setUserSettings(state, settings: IUserSettings) {
state.settings = defaultSettings(settings)
const info = state.info !== null ? state.info : {} as Info
info.name = settings.name
state.info = info
setUserSettings(settings: IUserSettings) {
this.settings = new UserSettingsModel(settings)
this.info = new UserModel({
...this.info !== null ? this.info : {},
name: settings.name,
})
},
authenticated(state, authenticated: boolean) {
state.authenticated = authenticated
setAuthenticated(authenticated: boolean) {
this.authenticated = authenticated
},
isLinkShareAuth(state, isLinkShareAuth: boolean) {
state.isLinkShareAuth = isLinkShareAuth
setIsLinkShareAuth(isLinkShareAuth: boolean) {
this.isLinkShareAuth = isLinkShareAuth
},
needsTotpPasscode(state, needsTotpPasscode: boolean) {
state.needsTotpPasscode = needsTotpPasscode
setNeedsTotpPasscode(needsTotpPasscode: boolean) {
this.needsTotpPasscode = needsTotpPasscode
},
reloadAvatar(state) {
if (!state.info) return
state.avatarUrl = `${state.info.getAvatarUrl()}&=${+new Date()}`
reloadAvatar() {
if (!this.info) return
this.avatarUrl = `${getAvatarUrl(this.info)}&=${+new Date()}`
},
lastUserRefresh(state) {
state.lastUserInfoRefresh = new Date()
updateLastUserRefresh() {
this.lastUserInfoRefresh = new Date()
},
},
actions: {
// Logs a user in with a set of credentials.
async login(ctx, credentials) {
async login(credentials) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
store.commit('loading', true)
this.setIsLoading(true)
// Delete an eventually preexisting old token
removeToken()
@ -99,30 +105,32 @@ const authStore : Module<AuthState, RootStoreState> = {
saveToken(response.data.token, true)
// Tell others the user is autheticated
ctx.dispatch('checkAuth')
this.checkAuth()
} catch (e) {
if (
e.response &&
e.response.data.code === 1017 &&
!credentials.totpPasscode
) {
ctx.commit('needsTotpPasscode', true)
this.setNeedsTotpPasscode(true)
}
throw e
} finally {
ctx.commit(LOADING, false, {root: true})
store.commit('loading', false)
this.setIsLoading(false)
}
},
// 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.
async register(ctx, credentials) {
async register(credentials) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
store.commit('loading', true)
this.setIsLoading(true)
try {
await HTTP.post('register', credentials)
return ctx.dispatch('login', credentials)
return this.login(credentials)
} catch (e) {
if (e.response?.data?.message) {
throw e.response.data
@ -130,13 +138,15 @@ const authStore : Module<AuthState, RootStoreState> = {
throw e
} finally {
ctx.commit(LOADING, false, {root: true})
store.commit('loading', false)
this.setIsLoading(false)
}
},
async openIdAuth(ctx, {provider, code}) {
async openIdAuth({provider, code}) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
store.commit('loading', true)
this.setIsLoading(true)
const data = {
code: code,
@ -150,28 +160,32 @@ const authStore : Module<AuthState, RootStoreState> = {
saveToken(response.data.token, true)
// Tell others the user is autheticated
ctx.dispatch('checkAuth')
this.checkAuth()
} finally {
ctx.commit(LOADING, false, {root: true})
store.commit('loading', false)
this.setIsLoading(false)
}
},
async linkShareAuth(ctx, {hash, password}) {
async linkShareAuth({hash, password}) {
const HTTP = HTTPFactory()
const response = await HTTP.post('/shares/' + hash + '/auth', {
password: password,
})
saveToken(response.data.token, false)
ctx.dispatch('checkAuth')
this.checkAuth()
return response.data
},
// Populates user information from jwt token saved in local storage in store
checkAuth(ctx) {
checkAuth() {
// This function can be called from multiple places at the same time and shortly after one another.
// To prevent hitting the api too frequently or race conditions, we check at most once per minute.
if (ctx.state.lastUserInfoRefresh !== null && ctx.state.lastUserInfoRefresh > (new Date()).setMinutes((new Date()).getMinutes() + 1)) {
if (
this.lastUserInfoRefresh !== null &&
this.lastUserInfoRefresh > (new Date()).setMinutes((new Date()).getMinutes() + 1)
) {
return
}
@ -185,17 +199,17 @@ const authStore : Module<AuthState, RootStoreState> = {
const info = new UserModel(JSON.parse(atob(base64)))
const ts = Math.round((new Date()).getTime() / 1000)
authenticated = info.exp >= ts
ctx.commit('info', info)
this.setUser(info)
if (authenticated) {
ctx.dispatch('refreshUserInfo')
this.refreshUserInfo()
}
}
ctx.commit('authenticated', authenticated)
this.setAuthenticated(authenticated)
if (!authenticated) {
ctx.commit('info', null)
ctx.dispatch('redirectToProviderIfNothingElseIsEnabled')
this.setUser(null)
this.redirectToProviderIfNothingElseIsEnabled()
}
},
@ -211,7 +225,7 @@ const authStore : Module<AuthState, RootStoreState> = {
}
},
async refreshUserInfo({state, commit, dispatch}) {
async refreshUserInfo() {
const jwt = getToken()
if (!jwt) {
return
@ -220,19 +234,27 @@ const authStore : Module<AuthState, RootStoreState> = {
const HTTP = AuthenticatedHTTPFactory()
try {
const response = await HTTP.get('user')
const info = new UserModel(response.data)
info.type = state.info.type
info.email = state.info.email
info.exp = state.info.exp
const info = new UserModel({
...response.data,
...(this.info?.type && {type: this.info?.type}),
...(this.info?.email && {email: this.info?.email}),
...(this.info?.exp && {exp: this.info?.exp}),
})
commit('info', info)
commit('lastUserRefresh')
this.setUser(info)
this.updateLastUserRefresh()
if (info.type === AUTH_TYPES.USER && (typeof info.settings.language === 'undefined' || info.settings.language === '')) {
if (
info.type === AUTH_TYPES.USER &&
(
typeof info.settings.language === 'undefined' ||
info.settings.language === ''
)
) {
// save current language
await dispatch('saveUserSettings', {
await this.saveUserSettings({
settings: {
...state.settings,
...this.settings,
language: getCurrentLanguage(),
},
showMessage: false,
@ -242,23 +264,28 @@ const authStore : Module<AuthState, RootStoreState> = {
return info
} catch (e) {
if(e?.response?.data?.message === 'invalid or expired jwt') {
dispatch('logout')
this.logout()
return
}
throw new Error('Error while refreshing user info:', {cause: e})
}
},
async saveUserSettings(ctx, payload) {
const {settings} = payload
const showMessage = payload.showMessage ?? true
async saveUserSettings({
settings,
showMessage = true,
}: {
settings: IUserSettings
showMessage : boolean
}) {
const userSettingsService = new UserSettingsService()
const cancel = setLoading(ctx, 'general-settings')
// FIXME
const cancel = setLoadingPinia(this, this.setIsLoadingGeneralSettings)
try {
saveLanguage(settings.language)
await userSettingsService.update(settings)
ctx.commit('setUserSettings', {...settings})
this.setUserSettings({...settings})
if (showMessage) {
success({message: i18n.global.t('user.settings.general.savedSuccess')})
}
@ -270,34 +297,38 @@ const authStore : Module<AuthState, RootStoreState> = {
},
// Renews the api token and saves it to local storage
renewToken(ctx) {
renewToken() {
// FIXME: Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a
// link share in another tab. Without the timeout both the token renew and link share auth are executed at
// the same time and one might win over the other.
setTimeout(async () => {
if (!ctx.state.authenticated) {
if (!this.authenticated) {
return
}
try {
await refreshToken(!ctx.state.isLinkShareAuth)
ctx.dispatch('checkAuth')
await refreshToken(!this.isLinkShareAuth)
this.checkAuth()
} catch (e) {
// Don't logout on network errors as the user would then get logged out if they don't have
// internet for a short period of time - such as when the laptop is still reconnecting
if (e?.request?.status) {
ctx.dispatch('logout')
this.logout()
}
}
}, 5000)
},
logout(ctx) {
logout() {
removeToken()
window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
router.push({name: 'user.login'})
ctx.dispatch('checkAuth')
this.checkAuth()
},
},
}
})
export default authStore
// support hot reloading
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot))
}

View File

@ -80,7 +80,7 @@ export const useLabelStore = defineStore('label', {
return
}
const cancel = setLoadingPinia(useLabelStore, this.setIsLoading)
const cancel = setLoadingPinia(this)
try {
const labels = await getAllLabels()
@ -92,7 +92,7 @@ export const useLabelStore = defineStore('label', {
},
async deleteLabel(label: ILabel) {
const cancel = setLoadingPinia(useLabelStore)
const cancel = setLoadingPinia(this)
const labelService = new LabelService()
try {
@ -106,7 +106,7 @@ export const useLabelStore = defineStore('label', {
},
async updateLabel(label: ILabel) {
const cancel = setLoadingPinia(useLabelStore)
const cancel = setLoadingPinia(this)
const labelService = new LabelService()
try {
@ -120,7 +120,7 @@ export const useLabelStore = defineStore('label', {
},
async createLabel(label: ILabel) {
const cancel = setLoadingPinia(useLabelStore)
const cancel = setLoadingPinia(this)
const labelService = new LabelService()
try {

View File

@ -87,7 +87,7 @@ export const useListStore = defineStore('list', {
},
async createList(list: IList) {
const cancel = setLoadingPinia(useListStore)
const cancel = setLoadingPinia(this)
const listService = new ListService()
try {
@ -103,7 +103,7 @@ export const useListStore = defineStore('list', {
},
async updateList(list: IList) {
const cancel = setLoadingPinia(useListStore)
const cancel = setLoadingPinia(this)
const listService = new ListService()
try {
@ -139,7 +139,7 @@ export const useListStore = defineStore('list', {
},
async deleteList(list: IList) {
const cancel = setLoadingPinia(useListStore)
const cancel = setLoadingPinia(this)
const listService = new ListService()
try {

View File

@ -74,16 +74,18 @@ import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
import {useListStore} from '@/stores/lists'
import {useConfigStore} from '@/stores/config'
import {useNamespaceStore} from '@/stores/namespaces'
import {useAuthStore} from '@/stores/auth'
const welcome = useDateTimeSalutation()
const store = useStore()
const authStore = useAuthStore()
const configStore = useConfigStore()
const namespaceStore = useNamespaceStore()
const listStore = useListStore()
const listHistory = computed(() => {
// If we don't check this, it tries to load the list background right after logging out
if(!store.state.auth.authenticated) {
if(!authStore.authenticated) {
return []
}
@ -93,13 +95,13 @@ const listHistory = computed(() => {
})
const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0)
const userInfo = computed(() => store.state.auth.info)
const userInfo = computed(() => authStore.info)
const hasTasks = computed(() => store.state.hasTasks)
const defaultListId = computed(() => store.state.auth.settings.defaultListId)
const defaultListId = computed(() => authStore.settings.defaultListId)
const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0)
const hasLists = computed(() => namespaceStore.namespaces?.[0]?.lists.length > 0)
const loading = computed(() => store.state.loading && store.state.loadingModule === 'tasks')
const deletionScheduledAt = computed(() => parseDateOrNull(store.state.auth.info?.deletionScheduledAt))
const deletionScheduledAt = computed(() => parseDateOrNull(authStore.info?.deletionScheduledAt))
// This is to reload the tasks list after adding a new task through the global task add.
// FIXME: Should use vuex (somehow?)

View File

@ -119,10 +119,10 @@ import ColorPicker from '@/components/input/colorPicker.vue'
import LabelModel from '@/models/label'
import type {ILabel} from '@/modelTypes/ILabel'
import {useAuthStore} from '@/stores/auth'
import {useLabelStore} from '@/stores/labels'
import { useTitle } from '@/composables/useTitle'
import { useStore } from '@/store'
const {t} = useI18n({useScope: 'global'})
@ -134,25 +134,23 @@ const labelToDelete = ref<ILabel>(null)
useTitle(() => t('label.title'))
const store = useStore()
const userInfo = computed(() => store.state.auth.info)
const authStore = useAuthStore()
const userInfo = computed(() => authStore.info)
const labelStore = useLabelStore()
labelStore.loadAllLabels()
// Alphabetically sort the labels
const labels = computed(() => Object.values(labelStore.labels).sort((f, s) => f.title > s.title ? 1 : -1))
const loading = computed(() =>labelStore.isLoading)
const loading = computed(() => labelStore.isLoading)
function deleteLabel(label: ILabel) {
showDeleteModal.value = false
isLabelEdit.value = false
const labelStore = useLabelStore()
return labelStore.deleteLabel(label)
}
function editLabelSubmit() {
const labelStore = useLabelStore()
return labelStore.updateLabel(labelEditLabel.value)
}

View File

@ -67,9 +67,9 @@
<script setup lang="ts">
import {ref, computed} from 'vue'
import flatPickr from 'vue-flatpickr-component'
import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import {useAuthStore} from '@/stores/auth'
import ListWrapper from './ListWrapper.vue'
import GanttChart from '@/components/tasks/gantt-component.vue'
@ -92,14 +92,14 @@ const dateFrom = ref(new Date((new Date()).setDate(now.value.getDate() - 15)))
const dateTo = ref(new Date((new Date()).setDate(now.value.getDate() + 30)))
const {t} = useI18n({useScope: 'global'})
const store = useStore()
const authStore = useAuthStore()
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatShort'),
altInput: true,
dateFormat: 'Y-m-d',
enableTime: false,
locale: {
firstDayOfWeek: store.state.auth.settings.weekStart,
firstDayOfWeek: authStore.settings.weekStart,
},
}))
</script>

View File

@ -41,6 +41,7 @@ import CreateEdit from '@/components/misc/create-edit.vue'
import LinkSharing from '@/components/sharing/linkSharing.vue'
import userTeam from '@/components/sharing/userTeam.vue'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'})
@ -52,10 +53,11 @@ const title = computed(() => list.value?.title
useTitle(title)
const store = useStore()
const authStore = useAuthStore()
const configStore = useConfigStore()
const linkSharingEnabled = computed(() => configStore.linkSharingEnabled)
const userIsAdmin = computed(() => 'owner' in list.value && list.value.owner.id === store.state.auth.info.id)
const userIsAdmin = computed(() => 'owner' in list.value && list.value.owner.id === authStore.info.id)
async function loadList(listId: number) {
const listService = new ListService()

View File

@ -26,20 +26,21 @@ export default { name: 'namespace-setting-share' }
<script lang="ts" setup>
import {ref, computed, watchEffect} from 'vue'
import {useStore} from '@/store'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {useTitle} from '@vueuse/core'
import NamespaceService from '@/services/namespace'
import NamespaceModel from '@/models/namespace'
import CreateEdit from '@/components/misc/create-edit.vue'
import manageSharing from '@/components/sharing/userTeam.vue'
import {useTitle} from '@/composables/useTitle'
import {useAuthStore} from '@/stores/auth'
import type {INamespace} from '@/modelTypes/INamespace'
const {t} = useI18n({useScope: 'global'})
const namespace = ref()
const namespace = ref<INamespace>()
const title = computed(() => namespace.value?.title
? t('namespace.share.title', { namespace: namespace.value.title })
@ -47,8 +48,8 @@ const title = computed(() => namespace.value?.title
)
useTitle(title)
const store = useStore()
const userIsAdmin = computed(() => 'owner' in namespace.value && namespace.value.owner.id === store.state.auth.info.id)
const authStore = useAuthStore()
const userIsAdmin = computed(() => 'owner' in namespace.value && namespace.value.owner.id === authStore.info.id)
async function loadNamespace(namespaceId: number) {
if (!namespaceId) return

View File

@ -42,12 +42,14 @@ import {useTitle} from '@vueuse/core'
import Message from '@/components/misc/message.vue'
import {LOGO_VISIBLE} from '@/store/mutation-types'
import {LIST_VIEWS, type ListView} from '@/types/ListView'
import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'})
useTitle(t('sharing.authenticating'))
function useAuth() {
const store = useStore()
const authStore = useAuthStore()
const route = useRoute()
const router = useRouter()
@ -56,7 +58,7 @@ function useAuth() {
const errorMessage = ref('')
const password = ref('')
const authLinkShare = computed(() => store.getters['auth/authLinkShare'])
const authLinkShare = computed(() => authStore.authLinkShare)
async function authenticate() {
authenticateWithPassword.value = false
@ -72,7 +74,7 @@ function useAuth() {
loading.value = true
try {
const {list_id: listId} = await store.dispatch('auth/linkShareAuth', {
const {list_id: listId} = await authStore.linkShareAuth({
hash: route.params.share,
password: password.value,
})

View File

@ -59,8 +59,10 @@ import {DATE_RANGES} from '@/components/date/dateRanges'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import LlamaCool from '@/assets/llama-cool.svg?component'
import type {ITask} from '@/modelTypes/ITask'
import {useAuthStore} from '@/stores/auth'
const store = useStore()
const authStore = useAuthStore()
const route = useRoute()
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
@ -104,7 +106,7 @@ const pageTitle = computed(() => {
})
})
const hasTasks = computed(() => tasks.value && tasks.value.length > 0)
const userAuthenticated = computed(() => store.state.auth.authenticated)
const userAuthenticated = computed(() => authStore.authenticated)
const loading = computed(() => store.state[LOADING] && store.state[LOADING_MODULE] === 'tasks')
interface dateStrings {

View File

@ -85,7 +85,7 @@
<table class="table has-actions is-striped is-hoverable is-fullwidth">
<tbody>
<tr :key="m.id" v-for="m in team?.members">
<td>{{ m.getDisplayName() }}</td>
<td>{{ getDisplayName(m) }}</td>
<td>
<template v-if="m.id === userInfo.id">
<b class="is-success">You</b>
@ -163,9 +163,10 @@
<script lang="ts" setup>
import {computed, ref} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRoute, useRouter} from 'vue-router'
import Editor from '@/components/input/AsyncEditor'
import {useStore} from '@/store'
import Multiselect from '@/components/input/multiselect.vue'
import TeamService from '@/services/team'
import TeamMemberService from '@/services/teamMember'
@ -173,15 +174,16 @@ import UserService from '@/services/user'
import {RIGHTS as Rights} from '@/constants/rights'
import Multiselect from '@/components/input/multiselect.vue'
import {useRoute, useRouter} from 'vue-router'
import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import {getDisplayName} from '@/models/user'
import {useAuthStore} from '@/stores/auth'
import type {ITeam} from '@/modelTypes/ITeam'
import type {IUser} from '@/modelTypes/IUser'
import type {ITeamMember} from '@/modelTypes/ITeamMember'
const store = useStore()
const authStore = useAuthStore()
const route = useRoute()
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
@ -193,7 +195,7 @@ const userIsAdmin = computed(() => {
team.value.maxRight > Rights.READ
)
})
const userInfo = computed(() => store.state.auth.info)
const userInfo = computed(() => authStore.info)
const teamService = ref<TeamService>(new TeamService())
const teamMemberService = ref<TeamMemberService>(new TeamMemberService())

View File

@ -38,14 +38,15 @@
<script setup lang="ts">
import {ref, computed, reactive} from 'vue'
import DataExportService from '@/services/dataExport'
import {store} from '@/store'
import {useAuthStore} from '@/stores/auth'
const dataExportService = reactive(new DataExportService())
const password = ref('')
const errPasswordRequired = ref(false)
const passwordInput = ref(null)
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
const authStore = useAuthStore()
const isLocalUser = computed(() => authStore.info?.isLocalUser)
function download() {
if (password.value === '' && isLocalUser.value) {

View File

@ -116,6 +116,7 @@ import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
import Password from '@/components/input/password.vue'
import { setTitle } from '@/helpers/setTitle'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
export default defineComponent({
components: {
@ -173,8 +174,11 @@ export default defineComponent({
},
...mapStateVuex({
loading: LOADING,
needsTotpPasscode: state => state.auth.needsTotpPasscode,
authenticated: state => state.auth.authenticated,
}),
...mapState(useAuthStore, {
needsTotpPasscode: state => state.needsTotpPasscode,
authenticated: state => state.authenticated,
}),
...mapState(useConfigStore, {
@ -224,8 +228,9 @@ export default defineComponent({
}
try {
await this.$store.dispatch('auth/login', credentials)
this.$store.commit('auth/needsTotpPasscode', false)
const authStore = useAuthStore()
await authStore.login(credentials)
authStore.setNeedsTotpPasscode(false)
} catch (e) {
if (e.response?.data.code === 1017 && !this.credentials.totpPasscode) {
return

View File

@ -22,6 +22,7 @@ import {useI18n} from 'vue-i18n'
import {getErrorText} from '@/message'
import Message from '@/components/misc/message.vue'
import {clearLastVisited, getLastVisited} from '@/helpers/saveLastVisited'
import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'})
@ -29,6 +30,7 @@ const router = useRouter()
const route = useRoute()
const store = useStore()
const authStore = useAuthStore()
const loading = computed(() => store.state.loading)
const errorMessage = ref('')
@ -65,7 +67,7 @@ async function authenticateWithCode() {
}
try {
await store.dispatch('auth/openIdAuth', {
await authStore.openIdAuth({
provider: route.params.provider,
code: route.query.code,
})

View File

@ -77,11 +77,14 @@ import {store} from '@/store'
import Message from '@/components/misc/message.vue'
import {isEmail} from '@/helpers/isEmail'
import Password from '@/components/input/password.vue'
import {useAuthStore} from '@/stores/auth'
const authStore = useAuthStore()
// FIXME: use the `beforeEnter` hook of vue-router
// Check if the user is already logged in, if so, redirect them to the homepage
onBeforeMount(() => {
if (store.state.auth.authenticated) {
if (authStore.authenticated) {
router.push({name: 'home'})
}
})
@ -126,7 +129,7 @@ async function submit() {
}
try {
await store.dispatch('auth/register', toRaw(credentials))
await authStore.register(toRaw(credentials))
} catch (e) {
errorMessage.value = e.message
}

View File

@ -19,19 +19,21 @@
<script setup lang="ts">
import {computed} from 'vue'
import { store } from '@/store'
import { useI18n } from 'vue-i18n'
import { useTitle } from '@/composables/useTitle'
import { useConfigStore } from '@/stores/config'
import { useAuthStore } from '@/stores/auth'
const { t } = useI18n({useScope: 'global'})
useTitle(() => t('user.settings.title'))
const configStore = useConfigStore()
const authStore = useAuthStore()
const totpEnabled = computed(() => configStore.totpEnabled)
const caldavEnabled = computed(() => configStore.caldavEnabled)
const migratorsEnabled = computed(() => configStore.migratorsEnabled)
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
const isLocalUser = computed(() => authStore.info?.isLocalUser)
const navigationItems = computed(() => {
const items = [

View File

@ -64,17 +64,17 @@ export default { name: 'user-settings-avatar' }
<script setup lang="ts">
import {computed, ref, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import {Cropper} from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
import AvatarService from '@/services/avatar'
import AvatarModel from '@/models/avatar'
import { useTitle } from '@/composables/useTitle'
import { success } from '@/message'
import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'})
const store = useStore()
const authStore = useAuthStore()
const AVATAR_PROVIDERS = computed(() => ({
default: t('misc.default'),
@ -102,7 +102,7 @@ avatarStatus()
async function updateAvatarStatus() {
await avatarService.update(new AvatarModel({avatarProvider: avatarProvider.value}))
success({message: t('user.settings.avatar.statusUpdateSuccess')})
store.commit('auth/reloadAvatar')
authStore.reloadAvatar()
}
const cropper = ref()
@ -121,7 +121,7 @@ async function uploadAvatar() {
const blob = await new Promise(resolve => canvas.toBlob(blob => resolve(blob)))
await avatarService.create(blob)
success({message: t('user.settings.avatar.setSuccess')})
store.commit('auth/reloadAvatar')
authStore.reloadAvatar()
} finally {
loading.value = false
isCropAvatar.value = false

View File

@ -68,7 +68,6 @@
<script lang="ts" setup>
import {computed, ref, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import {CALDAV_DOCS} from '@/urls'
import {useTitle} from '@/composables/useTitle'
@ -80,6 +79,7 @@ import CaldavTokenService from '@/services/caldavToken'
import { formatDateShort } from '@/helpers/time/formatDate'
import type {ICaldavToken} from '@/modelTypes/ICaldavToken'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
const copy = useCopyToClipboard()
@ -105,10 +105,10 @@ async function deleteToken(token: ICaldavToken) {
success(r)
}
const store = useStore()
const authStore = useAuthStore()
const configStore = useConfigStore()
const username = computed(() => store.state.auth.info?.username)
const username = computed(() => authStore.info?.username)
const caldavUrl = computed(() => `${configStore.apiBase}/dav/principals/${username.value}/`)
const caldavEnabled = computed(() => configStore.caldavEnabled)
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
const isLocalUser = computed(() => authStore.info?.isLocalUser)
</script>

View File

@ -44,22 +44,22 @@ export default {name: 'user-settings-data-export'}
<script setup lang="ts">
import {ref, computed, shallowReactive} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import DataExportService from '@/services/dataExport'
import { useTitle } from '@/composables/useTitle'
import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'})
const store = useStore()
const authStore = useAuthStore()
useTitle(() => `${t('user.export.title')} - ${t('user.settings.title')}`)
const dataExportService = shallowReactive(new DataExportService())
const password = ref('')
const errPasswordRequired = ref(false)
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
const isLocalUser = computed(() => authStore.info?.isLocalUser)
const passwordInput = ref()
async function requestDataExport() {

View File

@ -88,7 +88,6 @@ export default { name: 'user-settings-deletion' }
<script setup lang="ts">
import {ref, shallowReactive, computed} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import AccountDeleteService from '@/services/accountDelete'
@ -96,6 +95,7 @@ import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
import {useConfigStore} from '@/stores/config'
const {t} = useI18n({useScope: 'global'})
@ -105,11 +105,11 @@ const accountDeleteService = shallowReactive(new AccountDeleteService())
const password = ref('')
const errPasswordRequired = ref(false)
const store = useStore()
const authStore = useAuthStore()
const configStore = useConfigStore()
const userDeletionEnabled = computed(() => configStore.userDeletionEnabled)
const deletionScheduledAt = computed(() => parseDateOrNull(store.state.auth.info?.deletionScheduledAt))
const deletionScheduledAt = computed(() => parseDateOrNull(authStore.info?.deletionScheduledAt))
const passwordInput = ref()
async function deleteAccount() {
@ -133,7 +133,7 @@ async function cancelDeletion() {
await accountDeleteService.cancel(password.value)
success({message: t('user.deletion.scheduledCancelSuccess')})
store.dispatch('auth/refreshUserInfo')
authStore.refreshUserInfo()
password.value = ''
}
</script>

View File

@ -43,18 +43,18 @@ export default { name: 'user-settings-update-email' }
<script setup lang="ts">
import {reactive, computed, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import EmailUpdateService from '@/services/emailUpdate'
import EmailUpdateModel from '@/models/emailUpdate'
import {success} from '@/message'
import {useTitle} from '@/composables/useTitle'
import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'})
useTitle(() => `${t('user.settings.updateEmailTitle')} - ${t('user.settings.title')}`)
const store = useStore()
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
const authStore = useAuthStore()
const isLocalUser = computed(() => authStore.info?.isLocalUser)
const emailUpdate = reactive(new EmailUpdateModel())
const emailUpdateService = shallowReactive(new EmailUpdateService())

View File

@ -149,6 +149,7 @@
<script lang="ts">
import {defineComponent} from 'vue'
import { useListStore } from '@/stores/lists'
import { useAuthStore } from '@/stores/auth'
export default defineComponent({
name: 'user-settings-general',
@ -227,7 +228,8 @@ const playSoundWhenDone = ref(getPlaySoundWhenDoneSetting())
const quickAddMagicMode = ref(getQuickAddMagicMode())
const store = useStore()
const settings = ref({...store.state.auth.settings})
const authStore = useAuthStore()
const settings = ref({...authStore.settings})
const id = ref(createRandomID())
const availableLanguageOptions = ref(
Object.entries(availableLanguages)
@ -236,13 +238,13 @@ const availableLanguageOptions = ref(
)
watch(
() => store.state.auth.settings,
() => authStore.settings,
() => {
// Only setting if we don't have values set yet to avoid overriding edited values
if (!objectIsEmpty(settings.value)) {
return
}
settings.value = {...store.state.auth.settings}
settings.value = {...authStore.settings}
},
{immediate: true},
)
@ -265,7 +267,7 @@ async function updateSettings() {
localStorage.setItem(playSoundWhenDoneKey, playSoundWhenDone.value ? 'true' : 'false')
setQuickAddMagicMode(quickAddMagicMode.value)
await store.dispatch('auth/saveUserSettings', {
await authStore.saveUserSettings({
settings: {...settings.value},
})
}

View File

@ -57,24 +57,24 @@ export default {name: 'user-settings-password-update'}
<script setup lang="ts">
import {ref, reactive, shallowReactive, computed} from 'vue'
import { useI18n } from 'vue-i18n'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import PasswordUpdateService from '@/services/passwordUpdateService'
import PasswordUpdateModel from '@/models/passwordUpdate'
import {useTitle} from '@/composables/useTitle'
import {success, error} from '@/message'
import {useAuthStore} from '@/stores/auth'
const passwordUpdateService = shallowReactive(new PasswordUpdateService())
const passwordUpdate = reactive(new PasswordUpdateModel())
const passwordConfirm = ref('')
const {t} = useI18n({useScope: 'global'})
const store = useStore()
useTitle(() => `${t('user.settings.newPasswordTitle')} - ${t('user.settings.title')}`)
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
const authStore = useAuthStore()
const isLocalUser = computed(() => authStore.info?.isLocalUser)
async function updatePassword() {
if (passwordConfirm.value !== passwordUpdate.newPassword) {