Skip to content

State Management ​

Pinia store patterns.

Overview ​

The application uses 10 Pinia stores for global state:

StoreFilePurpose
Authauth.tsSession state, user info, login/logout, session timeout
Authorizationauthorization.tsUser permissions, roles, menu visibility
Brandingbranding.tsInstitution branding (logo, colors, name)
Holdsholds.tsCached hold state for the hold banner, which renders on every page
Instanceinstance.tsDeployment kind — restricted is the /records instance that refuses most writes
Lookupslookups.tsCached lookup table values
Menu LayoutmenuLayout.tsDynamic sidebar menu configuration
Recent PagesrecentPages.tsThe last pages visited, scoped per active role
Scroll PositionsscrollPositions.tsScroll position per page, restored on a "back to results" router.push as well as browser back
Settingssettings.tsApplication-wide settings

Store Pattern ​

All stores use the Composition API (setup function) style:

typescript
// stores/lookups.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { LookupValue } from '@/api/types'
import { lookupsApi } from '@/api/services/lookups.api'

export const useLookupsStore = defineStore('lookups', () => {
  // State
  const cache = ref<Record<string, LookupValue[]>>({})
  const loading = ref(false)

  // Getters
  const getByType = computed(() => {
    return (typeCode: string) => cache.value[typeCode] || []
  })

  // Actions
  async function fetchLookup(typeCode: string) {
    if (cache.value[typeCode]) return // Already cached

    loading.value = true
    try {
      const response = await lookupsApi.getByType(typeCode)
      cache.value[typeCode] = response.data.results || response.data
    } finally {
      loading.value = false
    }
  }

  function clearCache() {
    cache.value = {}
  }

  return {
    cache,
    loading,
    getByType,
    fetchLookup,
    clearCache
  }
})

Using Stores ​

In Components ​

vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useLookupsStore } from '@/stores/lookups'

const lookupsStore = useLookupsStore()

onMounted(() => {
  lookupsStore.fetchLookup('COURSE_STATUS')
})
</script>

<template>
  <Select
    :options="lookupsStore.getByType('COURSE_STATUS')"
    optionLabel="display_value"
    optionValue="id"
  />
</template>

With Destructuring ​

Use storeToRefs to preserve reactivity when destructuring:

vue
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useLookupsStore } from '@/stores/lookups'

const lookupsStore = useLookupsStore()
const { cache, loading } = storeToRefs(lookupsStore)
const { fetchLookup } = lookupsStore // Actions don't need storeToRefs
</script>

Auth Store ​

The auth store manages session-based authentication. The server sets HttpOnly cookies; the frontend only tracks the user object and session activity.

typescript
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, LoginCredentials } from '@/api/types'
import { authApi } from '@/api/services/auth.api'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null)
  const lastActivity = ref<number>(Date.now())
  const isLoading = ref(false)
  const sessionTimeoutMs = ref<number>(240 * 60 * 1000) // 4 hours default

  // Getters
  const isAuthenticated = computed(() => !!user.value)
  const isAdmin = computed(() => user.value?.is_superuser || user.value?.is_staff || false)
  const isStudent = computed(() => user.value?.is_student || false)
  const isFaculty = computed(() => user.value?.is_faculty || false)

  // Actions

  /**
   * Login with credentials.
   * Server sets HttpOnly cookies; we just store the user.
   */
  async function login(credentials: LoginCredentials): Promise<boolean> {
    isLoading.value = true
    try {
      const response = await authApi.login(credentials)
      if (response.success && response.data) {
        user.value = response.data.user
        lastActivity.value = Date.now()
        return true
      }
      return false
    } finally {
      isLoading.value = false
    }
  }

  /**
   * Fetch current user from session.
   * Called on app init to check if session cookie is still valid.
   */
  async function fetchUser() {
    try {
      const response = await authApi.me()
      user.value = response.data
    } catch {
      user.value = null
    }
  }

  /**
   * Logout - tells server to destroy session.
   */
  async function logout() {
    try {
      await authApi.logout()
    } finally {
      user.value = null
    }
  }

  return {
    user,
    isAuthenticated,
    isAdmin,
    isStudent,
    isFaculty,
    isLoading,
    login,
    fetchUser,
    logout
  }
})

INFO

There are no tokens stored in localStorage. Authentication is handled entirely via HttpOnly session cookies (sis_session) set by the server. The CSRF token (sis_csrf) is read from a non-HttpOnly cookie by the Axios interceptor.

Session Persistence ​

The auth store checks for an active session on app startup by calling GET /api/v1/auth/me. If the session cookie is valid, the user object is populated. If the session has expired, the user is redirected to the login page.

typescript
// In App.vue or router guard
const authStore = useAuthStore()
await authStore.fetchUser()

if (!authStore.isAuthenticated) {
  router.push('/login')
}

Student Information System Documentation