Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple session management of tokens from user data #354

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
alt="Profile"
/>
</div>
<div class="ml-3">
<div class="ml-3" v-if="!isUserLoading">
<div class="text-base font-medium text-gray-800">
{{ user.firstName }} {{ user.lastName }}
</div>
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { useAlert } from '@/composables/CommonAlerts'
import {
AccountForm,
EmailForgotPasswordForm,
LoginForm,
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({}))
Expand All @@ -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)
},
Expand All @@ -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({
Expand All @@ -79,40 +83,42 @@ 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
},
})

return {
isUserLoading: isPending,
user,
loginForm,
forgotPasswordForm,
resetPasswordForm,
loading,
login,
requestPasswordReset,
resetPassword,
user,
register,
registerForm,
getCodeUidFromRoute,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useUserStore } from '@/stores/user'
import { useAuthStore } from '@/stores/auth'

/**
* Route Guard.
* Only logged in users can access the route.
* 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 },
Expand All @@ -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',
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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}/`, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './forms'
export * from './api'
export * from './models'
export * from './queries'
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof userWithTokenShape>
export type UserShape = GetInferredFromRaw<typeof userShape>

export const userCreateShape = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
}),
}
36 changes: 36 additions & 0 deletions {{cookiecutter.project_slug}}/clients/web/vue3/src/stores/auth.ts
Copy link
Member

@lakardion lakardion Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved store to setup store which very much looks like a composobale

Original file line number Diff line number Diff line change
@@ -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<string | null>(null)
const token = ref<string | null>(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,
},
},
)
35 changes: 0 additions & 35 deletions {{cookiecutter.project_slug}}/clients/web/vue3/src/stores/user.ts

This file was deleted.

Loading