diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/NavBar.vue b/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/NavBar.vue index ce9ce28ba..296426bf5 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/NavBar.vue +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/NavBar.vue @@ -96,7 +96,7 @@ alt="Profile" /> -
+
{{ user.firstName }} {{ user.lastName }}
@@ -134,15 +134,19 @@ import { userApi } from '@/services/users' import { computed, ref } from 'vue' +import { useUsers } from '@/composables/Users' +import { useAuthStore } from '@/stores/auth' +import { useQueryClient } from '@tanstack/vue-query' import { useRouter } from 'vue-router' -import { useUserStore } from '@/stores/user' export default { setup() { - const userStore = useUserStore() + const authStore = useAuthStore() + const { user, isUserLoading } = useUsers() const router = useRouter() - let mobileMenuOpen = ref(false) - let profileMenuOpen = ref(false) + const mobileMenuOpen = ref(false) + const profileMenuOpen = ref(false) + const queryClient = useQueryClient() async function logout() { try { @@ -155,15 +159,17 @@ export default { } finally { profileMenuOpen.value = false mobileMenuOpen.value = false - userStore.clearUser() + authStore.clearStore() + queryClient.removeQueries() router.push({ name: 'Home' }) } } return { logout, - isLoggedIn: computed(() => userStore.isLoggedIn), - user: computed(() => userStore.user), + isLoggedIn: computed(() => authStore.isLoggedIn), + isUserLoading, + user, mobileMenuOpen, profileMenuOpen, } diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts index c2d1310a3..0dc24398a 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts @@ -1,3 +1,4 @@ +import { useAlert } from '@/composables/CommonAlerts' import { AccountForm, EmailForgotPasswordForm, @@ -5,18 +6,17 @@ import { LoginShape, ResetPasswordForm, ResetPasswordShape, - UserCreateShape, - UserShape, + UserWithTokenShape, userApi, + userQueries, } from '@/services/users' -import { useMutation, useQueryClient } from '@tanstack/vue-query' +import { useAuthStore } from '@/stores/auth' +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' import { reactive, ref } from 'vue' import { useRouter } from 'vue-router' -import { useUserStore } from '@/stores/user' -import { useAlert } from '@/composables/CommonAlerts' export function useUsers() { - const userStore = useUserStore() + const authStore = useAuthStore() const router = useRouter() const qc = useQueryClient() const loginForm = reactive(new LoginForm({})) @@ -26,12 +26,14 @@ export function useUsers() { const loading = ref(false) const { errorAlert, successAlert } = useAlert() + const { data: user , isPending} = useQuery(userQueries.retrieve(authStore.userId ?? '')) + const getCodeUidFromRoute = () => { const { uid, token } = router.currentRoute.value.params return { uid, token } } - const { data: user, mutate: login } = useMutation({ + const { mutate: login } = useMutation({ mutationFn: async (user: LoginShape) => { return await userApi.csc.login(user) }, @@ -43,16 +45,18 @@ export function useUsers() { console.log(error) errorAlert('Invalid email or password') }, - onSuccess: (data: UserShape) => { + onSuccess: (data) => { loading.value = false - userStore.updateUser(data) + const { token, ...user } = data + authStore.updateAuth({ userId: user.id, token }) + qc.setQueryData(userQueries.retrieve(user.id).queryKey, user) + const redirectPath = router.currentRoute.value.query.redirect if (redirectPath) { router.push({ path: redirectPath as string }) } else { router.push({ name: 'Dashboard' }) } - qc.invalidateQueries({ queryKey: ['user'] }) }, }) const { mutate: requestPasswordReset } = useMutation({ @@ -79,25 +83,26 @@ export function useUsers() { console.log(error) errorAlert('There was an error attempting to reset password') }, - onSuccess: (data: UserShape) => { + onSuccess: (data) => { loading.value = false - userStore.updateUser(data) + const { token, ...user } = data + authStore.updateAuth({ userId: user.id, token }) + qc.setQueryData(userQueries.retrieve(user.id).queryKey, user) router.push({ name: 'Dashboard' }) - qc.invalidateQueries({ queryKey: ['user'] }) }, }) const { mutate: register } = useMutation({ - mutationFn: async (data: UserCreateShape) => { - return await userApi.create(data) - }, + mutationFn: userApi.create, onError: (error: Error) => { loading.value = false console.log(error) errorAlert('There was an error attempting to register') }, - onSuccess: (data: UserShape) => { - userStore.updateUser(data) + onSuccess: (data) => { + const { token, ...user } = data as UserWithTokenShape + authStore.updateAuth({ userId: user.id, token }) + qc.setQueryData(userQueries.retrieve(user.id).queryKey, user) router.push({ name: 'Dashboard' }) qc.invalidateQueries({ queryKey: ['user'] }) loading.value = false @@ -105,6 +110,8 @@ export function useUsers() { }) return { + isUserLoading: isPending, + user, loginForm, forgotPasswordForm, resetPasswordForm, @@ -112,7 +119,6 @@ export function useUsers() { login, requestPasswordReset, resetPassword, - user, register, registerForm, getCodeUidFromRoute, diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js index d8ad9395b..e49f04634 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/AxiosClient.js @@ -1,5 +1,5 @@ import axios from 'axios' -import { useUserStore } from '@/stores/user' +import { useAuthStore } from '@/stores/auth' import CSRF from '@/services/csrf' import qs from 'qs' @@ -34,9 +34,9 @@ class ApiService { }) ApiService.session.interceptors.request.use( async (config) => { - const userStore = useUserStore() - if (userStore.isLoggedIn) { - config.headers['Authorization'] = `Token ${userStore.token}` + const { token, isLoggedIn } = useAuthStore() + if (isLoggedIn) { + config.headers['Authorization'] = `Token ${token}` } return config }, diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/auth.js b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/auth.js index 79978a00e..a40ce7710 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/auth.js +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/auth.js @@ -1,4 +1,4 @@ -import { useUserStore } from '@/stores/user' +import { useAuthStore } from '@/stores/auth' /** * Route Guard. @@ -6,8 +6,8 @@ import { useUserStore } from '@/stores/user' * If not logged in, a user will be redirected to the login page. */ export function requireAuth(to, from, next) { - const userStore = useUserStore() - if (!userStore.isLoggedIn) { + const authStore = useAuthStore() + if (!authStore.isLoggedIn) { next({ name: 'Login', query: { redirect: to.fullPath }, @@ -23,8 +23,8 @@ export function requireAuth(to, from, next) { * If logged in, a user will be redirected to the dashboard page. */ export function requireNoAuth(to, from, next) { - const userStore = useUserStore() - if (userStore.isLoggedIn) { + const authStore = useAuthStore() + if (authStore.isLoggedIn) { next({ name: 'Dashboard', }) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/api.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/api.ts index 572005dca..55a3bec29 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/api.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/api.ts @@ -6,11 +6,12 @@ import { resetPasswordShape, userCreateShape, userShape, + userWithTokenShape, } from './models' const login = createCustomServiceCall({ inputShape: loginShape, - outputShape: userShape, + outputShape: userWithTokenShape, cb: async ({ client, input, utils }) => { const res = await client.post('/login/', utils.toApi(input)) return utils.fromApi(res.data) @@ -26,7 +27,7 @@ const requestPasswordReset = createCustomServiceCall({ const resetPassword = createCustomServiceCall({ inputShape: resetPasswordShape, - outputShape: userShape, + outputShape: userWithTokenShape, cb: async ({ client, input, utils }) => { const { password } = utils.toApi(input) const res = await client.post(`/password/reset/confirm/${input.uid}/${input.token}/`, { diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/index.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/index.ts index af348c986..73f19dfc4 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/index.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/index.ts @@ -1,3 +1,4 @@ export * from './forms' export * from './api' export * from './models' +export * from './queries' diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/models.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/models.ts index ebc1bfa82..a164c5b5b 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/models.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/models.ts @@ -14,9 +14,13 @@ export const userShape = { email: z.string().email(), firstName: z.string(), lastName: z.string(), - //TODO:add back `readonly` https://github.com/thinknimble/tn-models-fp/issues/161 - token: z.string().nullable(), } + +export const userWithTokenShape = { + ...userShape, + token: z.string(), +} +export type UserWithTokenShape = GetInferredFromRaw export type UserShape = GetInferredFromRaw export const userCreateShape = { diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/queries.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/queries.ts new file mode 100644 index 000000000..a6a9d570e --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/queries.ts @@ -0,0 +1,15 @@ +import { queryOptions } from '@tanstack/vue-query' +import { userApi } from './api' +import { useAuthStore } from '@/stores/auth' + +export const userQueries = { + all: ['users'], + retrieve: (id: string) => + queryOptions({ + queryKey: [...userQueries.all, id], + queryFn: async () => { + return userApi.retrieve(id) + }, + enabled: Boolean(id && useAuthStore().token), + }), +} diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/auth.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/auth.ts new file mode 100644 index 000000000..a0f1eb338 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/auth.ts @@ -0,0 +1,36 @@ +// Pinia Store +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +const STORAGE_HASH = 'aYlBAJpGke' +export const STORAGE_KEY = `{{ cookiecutter.project_slug }}-${STORAGE_HASH}` + +export const useAuthStore = defineStore( + 'auth', + () => { + const userId = ref(null) + const token = ref(null) + + const isLoggedIn = computed(() => Boolean(token.value)) + const updateAuth = (payload: { userId: string; token: string }) => { + userId.value = payload.userId + token.value = payload.token + } + const clearStore = () => { + userId.value = null + token.value = null + } + return { + userId, + token, + isLoggedIn, + updateAuth, + clearStore, + } + }, + { + persist: { + key: STORAGE_KEY, + }, + }, +) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/user.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/user.ts deleted file mode 100644 index 3ac0bb819..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/stores/user.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Pinia Store -import { defineStore } from 'pinia' -import { UserShape } from '@/services/users' - -const STORAGE_HASH = '{{ random_ascii_string(10) }}' -export const STORAGE_KEY = `{{ cookiecutter.project_slug }}-${STORAGE_HASH}` - -interface State { - user: UserShape | null -} - -export const useUserStore = defineStore('user', { - state: (): State => ({ - user: null, - }), - persist: { - key: STORAGE_KEY, - }, - getters: { - isLoggedIn: (state) => { - return !!state.user - }, - token: (state) => { - return state.user ? state.user.token : null - }, - }, - actions: { - updateUser(payload: UserShape) { - this.user = payload - }, - clearUser() { - this.$reset() - }, - }, -})