initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
---
task: Fix homepage fragment persisting from previous login when switching account types
cycles: 5
context: true
private: false
started: 2026-05-17T16:22:34Z
finished: 2026-05-17T16:24:00Z
---
## files
- resources/js/stores/user.js [lines 63-95] — fetchCurrentUser() has a guard that skips role re-fetch when acctType is already set; this allows stale sessionStorage role to persist
- resources/js/Pages/Home.vue [lines 1-21] — renders role fragments before userStore loading is complete, causing flash of wrong fragment
- resources/js/composables/Core/useAuth.js [lines 7-53] — module-level globalRole ref; module-level fetchRole() guard prevents re-fetch when sessionStorage already has a non-public role
- resources/js/Pages/Auth/Login.vue [lines 13-38] — sessionStorage.clear() in onMounted runs synchronously, but app.js fetchCurrentUser() is async and can set sessionStorage AFTER the clear
## steps
### Step 1 — Fix stale-role guard in `fetchCurrentUser()` (user.js:78-84)
The guard `if (!this.acctType || this.acctType === 'public')` prevents re-fetching the real server-side role when `acctType` is already populated from sessionStorage. This is the primary bug: after login, if sessionStorage has the old user's role (e.g. 'ultimate'), `acctType` initialises to 'ultimate' from the store state factory, the guard is false, and the role is never corrected against the server — even though the session now belongs to a completely different user.
Remove the guard entirely so that `fetchCurrentUser()` **always** re-fetches `acct_type` from `/get/user/acct-type` after successfully loading the user object.
```js
// user.js fetchCurrentUser(), after setting this.user:
// REMOVE the `if (!this.acctType || this.acctType === 'public')` wrapper.
// Always fetch and overwrite acctType from the server.
const resRole = await axios.get('/get/user/acct-type')
if (resRole.data && resRole.data.acct_type) {
this.acctType = resRole.data.acct_type
sessionStorage.setItem('user_acct_type', this.acctType)
}
```
Keep the 401/419 catch path unchanged — it already sets `acctType = 'public'`.
### Step 2 — Add loading guard to Home.vue before rendering role fragments
While `userStore.loading` is true (fetchCurrentUser in flight), `userStore.acctType` may still hold the stale sessionStorage value. Guard all role-specific `<template>` blocks with a top-level loading state so no fragment renders until auth is confirmed.
In Home.vue add a `userStore` import and wrap the inner content:
```js
import { useUserStore } from '../stores/user.js';
const userStore = useUserStore();
```
In the template, wrap the role switch inside a `v-if="!userStore.loading || userStore.user"` block. While loading and no user yet, show a minimal centered spinner (reuse the existing spinner pattern in the codebase — `<div class="spinner-border text-primary">`). Once `userStore.user` is resolved (non-null), the correct fragment renders reactively.
### Step 3 — Reset globalRole in useAuth.js when Login.vue mounts
Export a `resetRole()` function from `useAuth.js` that sets `globalRole.value = UserTypes.PUBLIC` and removes `sessionStorage.user_acct_type`. Call it from Login.vue's `onMounted` AFTER `sessionStorage.clear()`.
This ensures the module-level `globalRole` is synchronously reset before any async work sets it again, eliminating any window where the stale in-memory value could be read.
```js
// useAuth.js add export:
export function resetRole() {
globalRole.value = UserTypes.PUBLIC;
isFetching.value = false;
sessionStorage.removeItem('user_acct_type');
}
```
```js
// Login.vue onMounted, after sessionStorage.clear():
import { resetRole } from '../../composables/Core/useAuth.js';
import { useUserStore } from '../../stores/user.js';
onMounted(() => {
sessionStorage.clear();
localStorage.clear();
resetRole();
useUserStore().resetCurrentUser(); // clears user + acctType
// existing SW unregistration code...
});
```
### Step 4 — Extend `resetCurrentUser()` in user.js to also clear acctType
Currently `resetCurrentUser()` only sets `this.user = null`. Extend it to also reset `acctType` and remove the sessionStorage key so the role computed falls back to PUBLIC on next render.
```js
resetCurrentUser() {
this.user = null;
this.acctType = null;
sessionStorage.removeItem('user_acct_type');
},
```
## context
### Root cause trace
On a fresh page load at `/` after logging in as a different user:
1. `user.js` Pinia store state factory runs: `acctType = sessionStorage.getItem('user_acct_type') || null`
2. If sessionStorage still has the PREVIOUS user's type (e.g. 'ultimate') — possible because `fetchCurrentUser()` on the login page ran AFTER `Login.vue` cleared sessionStorage and re-set it — `acctType = 'ultimate'`
3. `app.js` calls `userStore.fetchCurrentUser()` (async)
4. Guard at user.js:78 fires: `!this.acctType` = false, `this.acctType !== 'public'` = true → **guard is false → role NOT re-fetched from server**
5. `userStore.user` is updated to the NEW user's data but `acctType` stays 'ultimate'
6. `role` computed in `useAuth.js` returns `userStore.acctType = 'ultimate'`
7. Home.vue renders `<HomeUltimate />` for the new STORE_OWNER user
### Key reactive chain
```
role (computed) = userStore.acctType || currentUser.value?.acct_type || globalRole.value
↑ populated from sessionStorage on store init → stale
```
### Affected files key refs
- `user.js:12``acctType: sessionStorage.getItem('user_acct_type') || null`
- `user.js:78-84` — the stale guard (primary bug)
- `user.js:190``resetCurrentUser()` only clears `user`, not `acctType`
- `useAuth.js:7``globalRole` module-level ref
- `useAuth.js:51-53` — module-level fetchRole() call (won't fire if sessionStorage is non-public)
- `useAuth.js:76-86``role` computed priority: userStore.acctType first
- `Home.vue:15-20` — destructures from `useAuth()` with no loading guard
## notes
- dictionary: ai-docs/dictionary.md
- linters: eslint, tsc (via vite)
- constraints: Hypervel SPA — full page reloads happen after login/logout; sessionStorage persists within a tab across reloads. The guard removal in step 1 adds one extra request per page load but is required for correctness. The loading guard in step 2 may briefly show a spinner before the dashboard; this is acceptable and far better than showing the wrong dashboard.