Use a pinia store to manage session state

Replace the convoluted useAccountSession composable with a pinia store
that in addition allows for the consolidation of all session related
functions to grouped into one module.
This commit is contained in:
Hornwitser 2025-05-24 17:53:33 +02:00
parent c47452a8b4
commit fae8b4e2e4
21 changed files with 181 additions and 118 deletions

View file

@ -5,4 +5,9 @@
<script setup lang="ts"> <script setup lang="ts">
import "~/assets/global.css"; import "~/assets/global.css";
const event = useRequestEvent();
const sessionStore = useSessionStore();
await callOnce("fetch-session", async () => {
await sessionStore.fetch(event);
})
</script> </script>

View file

@ -5,7 +5,7 @@
<p v-if="event.interested"> <p v-if="event.interested">
{{ event.interested }} interested {{ event.interested }} interested
</p> </p>
<p v-if="session"> <p v-if="sessionStore.account">
<button <button
class="interested" class="interested"
:class="{ active: interestedIds.has(event.id) }" :class="{ active: interestedIds.has(event.id) }"
@ -20,7 +20,7 @@
<li v-for="slot in event.slots" :key="slot.id"> <li v-for="slot in event.slots" :key="slot.id">
{{ formatTime(slot.start) }} - {{ formatTime(slot.end) }} {{ formatTime(slot.start) }} - {{ formatTime(slot.end) }}
<button <button
v-if="session && event.slots.length > 1" v-if="sessionStore.account && event.slots.length > 1"
class="interested" class="interested"
:disabled="interestedIds.has(event.id)" :disabled="interestedIds.has(event.id)"
:class="{ active: interestedIds.has(event.id) || interestedIds.has(slot.id) }" :class="{ active: interestedIds.has(event.id) || interestedIds.has(slot.id) }"
@ -49,9 +49,9 @@ defineProps<{
}>() }>()
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const { data: session, refresh: refreshSession } = await useAccountSession(); const sessionStore = useSessionStore();
const interestedIds = computed(() => new Set(session.value?.account.interestedIds ?? [])); const interestedIds = computed(() => new Set(sessionStore.account?.interestedIds ?? []));
const timezone = computed(() => session.value?.account?.timezone ?? runtimeConfig.public.defaultTimezone); const timezone = computed(() => sessionStore.account?.timezone ?? runtimeConfig.public.defaultTimezone);
const { data: accounts } = await useAccounts(); const { data: accounts } = await useAccounts();
const idToAccount = computed(() => new Map(accounts.value?.map(a => [a.id, a]))); const idToAccount = computed(() => new Map(accounts.value?.map(a => [a.id, a])));
@ -60,7 +60,7 @@ function formatTime(time: string) {
} }
async function toggle(id: string, slotIds?: string[]) { async function toggle(id: string, slotIds?: string[]) {
let newIds = [...session.value!.account.interestedIds ?? []]; let newIds = [...sessionStore.account!.interestedIds ?? []];
if (interestedIds.value.has(id)) { if (interestedIds.value.has(id)) {
newIds = newIds.filter(newId => newId !== id); newIds = newIds.filter(newId => newId !== id);
} else { } else {
@ -74,7 +74,7 @@ async function toggle(id: string, slotIds?: string[]) {
method: "PATCH", method: "PATCH",
body: { interestedIds: newIds }, body: { interestedIds: newIds },
}) })
await refreshSession(); await sessionStore.fetch();
} }
</script> </script>

View file

@ -135,8 +135,8 @@ defineProps<{
}>(); }>();
const schedule = await useSchedule(); const schedule = await useSchedule();
const { data: session } = await useAccountSession(); const sessionStore = useSessionStore();
const canEditPublic = computed(() => session.value?.account.type === "admin"); const canEditPublic = computed(() => sessionStore.account?.type === "admin");
function canEdit(event: ScheduleEvent) { function canEdit(event: ScheduleEvent) {
return event.crew || canEditPublic.value; return event.crew || canEditPublic.value;

View file

@ -8,18 +8,18 @@
<li> <li>
<NuxtLink to="/schedule">Schedule</NuxtLink> <NuxtLink to="/schedule">Schedule</NuxtLink>
</li> </li>
<li v-if="session?.account?.type === 'admin' || session?.account?.type === 'crew'"> <li v-if="sessionStore.account?.type === 'admin' || sessionStore.account?.type === 'crew'">
<NuxtLink to="/edit">Edit</NuxtLink> <NuxtLink to="/edit">Edit</NuxtLink>
</li> </li>
</ul> </ul>
</nav> </nav>
<div class="account"> <div class="account">
<template v-if="session?.account"> <template v-if="sessionStore.account">
{{ session.account.name }} {{ sessionStore.account.name }}
(s:{{ session.id }} a:{{ session.account.id }}{{ session.push ? " push" : null }}) (s:{{ sessionStore.id }} a:{{ sessionStore.account.id }}{{ sessionStore.push ? " push" : null }})
{{ session.account.type }} {{ sessionStore.account.type }}
<NuxtLink to="/account/settings">Settings</NuxtLink> <NuxtLink to="/account/settings">Settings</NuxtLink>
<LogOutButton v-if="session.account.type !== 'anonymous'"/> <LogOutButton v-if="sessionStore.account.type !== 'anonymous'"/>
</template> </template>
<template v-else> <template v-else>
<NuxtLink to="/login">Log In</NuxtLink> <NuxtLink to="/login">Log In</NuxtLink>
@ -29,7 +29,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const { data: session } = await useAccountSession(); const sessionStore = useSessionStore();
</script> </script>
<style scoped> <style scoped>

View file

@ -1,19 +1,7 @@
<template> <template>
<button type="button" @click="logOut">Log out</button> <button type="button" @click="sessionStore.logOut">Log out</button>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { refresh: sessionRefresh } = useAccountSession(); const sessionStore = useSessionStore();
async function logOut() {
try {
await $fetch.raw("/api/auth/session", {
method: "DELETE",
});
await sessionRefresh();
} catch (err: any) {
alert(`Log out failed: ${err.statusCode} ${err.statusMessage}`);
}
}
</script> </script>

View file

@ -18,15 +18,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { data: session, refresh: refreshSession } = await useAccountSession(); const sessionStore = useSessionStore();
const { supported, subscription, getSubscription, subscribe, unsubscribe } = usePushNotification(); const { supported, subscription, getSubscription, subscribe, unsubscribe } = usePushNotification();
const subscribed = computed(() => Boolean(subscription.value && session.value?.push)) const subscribed = computed(() => Boolean(subscription.value && sessionStore.push));
async function onClick() { async function onClick() {
if (!subscribed.value) if (!subscribed.value)
await subscribe(); await subscribe();
else else
await unsubscribe(); await unsubscribe();
await refreshSession(); await sessionStore.fetch();
} }
onMounted(() => { onMounted(() => {

View file

@ -424,11 +424,11 @@ function removeSlot(eventChanges: ChangeRecord<ScheduleEvent>[], event: Schedule
return eventChanges; return eventChanges;
} }
const { data: session } = await useAccountSession(); const sessionStore = useSessionStore();
const schedule = await useSchedule(); const schedule = await useSchedule();
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const timezone = computed( const timezone = computed(
() => session.value?.account?.timezone ?? runtimeConfig.public.defaultTimezone () => sessionStore.account?.timezone ?? runtimeConfig.public.defaultTimezone
); );
type EventSlotChange = { op: "set" | "del", data: EventSlot } ; type EventSlotChange = { op: "set" | "del", data: EventSlot } ;

View file

@ -394,11 +394,11 @@ function removeSlot(eventChanges: ChangeRecord<Shift>[], shift: Shift, shiftSlot
return eventChanges; return eventChanges;
} }
const { data: session } = await useAccountSession(); const sessionStore = useSessionStore();
const schedule = await useSchedule(); const schedule = await useSchedule();
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const timezone = computed( const timezone = computed(
() => session.value?.account?.timezone ?? runtimeConfig.public.defaultTimezone () => sessionStore.account?.timezone ?? runtimeConfig.public.defaultTimezone
); );
type ShiftSlotChange = { op: "set" | "del", data: ShiftSlot } ; type ShiftSlotChange = { op: "set" | "del", data: ShiftSlot } ;

View file

@ -504,10 +504,10 @@ const stretches = computed(() => [
]) ])
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const { data: session } = await useAccountSession(); const sessionStore = useSessionStore();
const debugTimezone = ref<undefined | string>(); const debugTimezone = ref<undefined | string>();
const timezone = computed({ const timezone = computed({
get: () => debugTimezone.value ?? session.value?.account?.timezone ?? runtimeConfig.public.defaultTimezone, get: () => debugTimezone.value ?? sessionStore.account?.timezone ?? runtimeConfig.public.defaultTimezone,
set: (value: string) => { debugTimezone.value = value }, set: (value: string) => { debugTimezone.value = value },
}); });

View file

@ -4,7 +4,7 @@ let source: EventSource | null = null;
let sourceRefs = 0; let sourceRefs = 0;
let sourceSessionId: number | undefined = undefined; let sourceSessionId: number | undefined = undefined;
export const useSchedule = () => { export const useSchedule = () => {
const { data: session } = useAccountSession(); const sessionStore = useSessionStore();
const requestFetch = useRequestFetch(); const requestFetch = useRequestFetch();
const asyncData = useAsyncData<Schedule>( const asyncData = useAsyncData<Schedule>(
'schedule', 'schedule',
@ -14,8 +14,8 @@ export const useSchedule = () => {
const { data: schedule } = asyncData; const { data: schedule } = asyncData;
function connect() { function connect() {
console.log("Opening event source sid:", session.value?.id); console.log("Opening event source sid:", sessionStore.id);
sourceSessionId = session.value?.id; sourceSessionId = sessionStore.id;
source = new EventSource("/api/events"); source = new EventSource("/api/events");
source.addEventListener("message", (message) => { source.addEventListener("message", (message) => {
console.log("Message", message.data); console.log("Message", message.data);
@ -43,11 +43,11 @@ export const useSchedule = () => {
connect(); connect();
}) })
watch(() => session.value?.id, () => { watch(() => sessionStore.id, () => {
if (sourceSessionId === session.value?.id) { if (sourceSessionId === sessionStore.id) {
return; return;
} }
sourceSessionId = session.value?.id; sourceSessionId = sessionStore.id;
console.log("Session changed, refetching schedule") console.log("Session changed, refetching schedule")
$fetch("/api/schedule").then( $fetch("/api/schedule").then(
data => { schedule.value = data; }, data => { schedule.value = data; },

View file

@ -1,34 +0,0 @@
import { appendResponseHeader } from "h3";
import type { H3Event } from "h3";
const fetchWithCookie = async (url: string, event?: H3Event) => {
if (!event) {
return $fetch(url);
}
const cookie = useRequestHeader("cookie");
const res = await $fetch.raw(url, {
headers: cookie ? { cookie } : undefined
});
for (const cookie of res.headers.getSetCookie()) {
appendResponseHeader(event, "set-cookie", cookie);
}
return res._data;
}
export const useAccountSession = () => {
const event = useRequestEvent();
return useAsyncData(
"session",
async () => await fetchWithCookie("/api/auth/session", event),
{
transform: (input) => input === undefined ? false as any as null: input,
getCachedData(key, nuxtApp, context) {
if (context.cause === "refresh:manual")
return undefined
return nuxtApp.payload.data[key];
},
dedupe: "defer",
}
);
}

View file

@ -1,14 +1,14 @@
export default defineNuxtRouteMiddleware(async (to, from) => { export default defineNuxtRouteMiddleware(async (to, from) => {
const { data: session } = await useAccountSession(); const sessionStore = useSessionStore();
if (!session.value) { if (!sessionStore.account) {
console.log("Not logged in, redirecting to /login"); console.log("Not logged in, redirecting to /login");
return navigateTo("/login"); return navigateTo("/login");
} }
if ( if (
to.meta.allowedAccountTypes to.meta.allowedAccountTypes
&& !to.meta.allowedAccountTypes.includes(session.value.account.type) && !to.meta.allowedAccountTypes.includes(sessionStore.account.type)
) { ) {
throw createError({ throw createError({
status: 403, status: 403,

View file

@ -10,5 +10,8 @@ export default defineNuxtConfig({
defaultTimezone: "Europe/Oslo", defaultTimezone: "Europe/Oslo",
vapidPublicKey: "", vapidPublicKey: "",
} }
} },
modules: [
"@pinia/nuxt",
],
}) })

View file

@ -10,8 +10,10 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@pinia/nuxt": "0.11.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"nuxt": "^3.17.4", "nuxt": "^3.17.4",
"pinia": "^3.0.2",
"vue": "latest", "vue": "latest",
"vue-router": "latest", "vue-router": "latest",
"web-push": "^3.6.7" "web-push": "^3.6.7"

View file

@ -1,10 +1,10 @@
<template> <template>
<main> <main>
<h1>Account Settings</h1> <h1>Account Settings</h1>
<p v-if="session?.account.type !== 'anonymous'"> <p v-if="sessionStore.account?.type !== 'anonymous'">
Name: {{ session?.account.name }} Name: {{ sessionStore.account?.name }}
</p> </p>
<p>Access: {{ session?.account.type }}</p> <p>Access: {{ sessionStore.account?.type }}</p>
<form @submit.prevent="changeSettings"> <form @submit.prevent="changeSettings">
<label> <label>
Timezone Timezone
@ -34,9 +34,9 @@ definePageMeta({
middleware: ["authenticated"], middleware: ["authenticated"],
}); });
const { data: session, refresh: sessionRefresh } = await useAccountSession(); const sessionStore = useSessionStore();
const timezone = ref(session.value?.account.timezone ?? ""); const timezone = ref(sessionStore.account?.timezone ?? "");
async function changeSettings() { async function changeSettings() {
try { try {
@ -46,7 +46,7 @@ async function changeSettings() {
timezone: timezone.value, timezone: timezone.value,
} }
}); });
await sessionRefresh(); await sessionStore.fetch();
} catch (err: any) { } catch (err: any) {
alert(err.data.message); alert(err.data.message);
} }
@ -57,7 +57,7 @@ async function deleteAccount() {
await $fetch.raw("/api/account", { await $fetch.raw("/api/account", {
method: "DELETE", method: "DELETE",
}); });
await sessionRefresh(); await sessionStore.fetch();
await navigateTo("/"); await navigateTo("/");
} catch (err: any) { } catch (err: any) {

View file

@ -77,6 +77,7 @@ definePageMeta({
const schedule = await useSchedule(); const schedule = await useSchedule();
const { data: accounts } = await useAccounts(); const { data: accounts } = await useAccounts();
const sessionStore = useSessionStore();
const route = useRoute(); const route = useRoute();
const crewFilter = computed({ const crewFilter = computed({
@ -90,14 +91,14 @@ const crewFilter = computed({
}), }),
}); });
const eventSlotFilter = computed(() => { const eventSlotFilter = computed(() => {
if (crewFilter.value === undefined || !session.value) { if (crewFilter.value === undefined || !sessionStore.account) {
return () => true; return () => true;
} }
const cid = parseInt(crewFilter.value); const cid = parseInt(crewFilter.value);
return (slot: TimeSlot) => slot.assigned?.some(id => id === cid) || false; return (slot: TimeSlot) => slot.assigned?.some(id => id === cid) || false;
}); });
const shiftSlotFilter = computed(() => { const shiftSlotFilter = computed(() => {
if (crewFilter.value === undefined || !session.value) { if (crewFilter.value === undefined || !sessionStore.account) {
return () => true; return () => true;
} }
const cid = parseInt(crewFilter.value); const cid = parseInt(crewFilter.value);
@ -126,6 +127,5 @@ const roleFilter = computed({
}), }),
}); });
const { data: session } = await useAccountSession(); const isAdmin = computed(() => sessionStore.account?.type === "admin")
const isAdmin = computed(() => session.value?.account.type === "admin")
</script> </script>

View file

@ -5,13 +5,13 @@
<li> <li>
<NuxtLink to="/schedule">View Schedule</NuxtLink> <NuxtLink to="/schedule">View Schedule</NuxtLink>
</li> </li>
<li v-if="session?.account?.type === 'admin' || session?.account?.type === 'crew'"> <li v-if="sessionStore.account?.type === 'admin' || sessionStore.account?.type === 'crew'">
<NuxtLink to="/edit">Edit Schedule</NuxtLink> <NuxtLink to="/edit">Edit Schedule</NuxtLink>
</li> </li>
<li v-if="session"> <li v-if="sessionStore.account">
<NuxtLink to="/account/settings">Account Settings</NuxtLink> <NuxtLink to="/account/settings">Account Settings</NuxtLink>
</li> </li>
<li v-if="!session"> <li v-if="!sessionStore.account">
<NuxtLink to="/login">Log In / Create Account</NuxtLink> <NuxtLink to="/login">Log In / Create Account</NuxtLink>
</li> </li>
</ul> </ul>
@ -19,5 +19,5 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const { data: session } = await useAccountSession(); const sessionStore = useSessionStore();
</script> </script>

View file

@ -25,29 +25,26 @@
<button type="button" @click="createAnonymousAccount">Create anonymous account</button> <button type="button" @click="createAnonymousAccount">Create anonymous account</button>
</fieldset> </fieldset>
<pre><code>{{ result }}</code></pre> <pre><code>{{ result }}</code></pre>
<pre><code>Session: {{ session }}</code></pre> <pre><code>Session: {{ ({ id: sessionStore.id, account: sessionStore.account, push: sessionStore.push }) }}</code></pre>
</main> </main>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const { data: session, refresh: sessionRefresh } = await useAccountSession(); const sessionStore = useSessionStore();
const { getSubscription, subscribe } = usePushNotification(); const { getSubscription, subscribe } = usePushNotification();
const name = ref(""); const name = ref("");
const result = ref("") const result = ref("")
async function logIn() { async function logIn() {
try { try {
const res = await $fetch.raw("/api/auth/login", { result.value = await sessionStore.logIn(name.value);
method: "POST",
body: { name: name.value },
});
result.value = `Server replied: ${res.status} ${res.statusText}`;
// Resubscribe push notifications if the user was subscribed before. // Resubscribe push notifications if the user was subscribed before.
const subscription = await getSubscription(); const subscription = await getSubscription();
if (subscription) { if (subscription) {
await subscribe(); await subscribe();
} }
await sessionRefresh(); // XXX Remove the need for this.
await sessionStore.fetch();
} catch (err: any) { } catch (err: any) {
console.log(err); console.log(err);
@ -67,7 +64,7 @@ async function createAccount() {
body: new URLSearchParams({ name: createName.value }) body: new URLSearchParams({ name: createName.value })
}); });
result.value = `Server replied: ${res.status} ${res.statusText}`; result.value = `Server replied: ${res.status} ${res.statusText}`;
await sessionRefresh(); await sessionStore.fetch();
} catch (err: any) { } catch (err: any) {
console.log(err); console.log(err);
@ -84,7 +81,7 @@ async function createAnonymousAccount() {
}, },
}); });
result.value = `Server replied: ${res.status} ${res.statusText}`; result.value = `Server replied: ${res.status} ${res.statusText}`;
await sessionRefresh(); await sessionStore.fetch();
} catch (err: any) { } catch (err: any) {
console.log(err); console.log(err);

View file

@ -4,7 +4,7 @@
<p> <p>
Study carefully, we only hold these events once a year. Study carefully, we only hold these events once a year.
</p> </p>
<p v-if="!session"> <p v-if="!sessionStore.account">
<NuxtLink to="/login">Login</NuxtLink> or <NuxtLink to="/login#create-account">Create an account</NuxtLink> <NuxtLink to="/login">Login</NuxtLink> or <NuxtLink to="/login#create-account">Create an account</NuxtLink>
to get notified about updates to the schedule. to get notified about updates to the schedule.
</p> </p>
@ -12,7 +12,7 @@
Check out your <NuxtLink to="/account/settings">Account Setting</NuxtLink> to set up notifications for changes to schedule. Check out your <NuxtLink to="/account/settings">Account Setting</NuxtLink> to set up notifications for changes to schedule.
</p> </p>
<h2>Schedule</h2> <h2>Schedule</h2>
<label v-if="session"> <label v-if="sessionStore.account">
Filter: Filter:
<select <select
v-model="filter" v-model="filter"
@ -57,12 +57,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ShiftSlot, TimeSlot } from '~/shared/types/schedule'; import type { ShiftSlot, TimeSlot } from '~/shared/types/schedule';
const { data: session } = await useAccountSession(); const sessionStore = useSessionStore();
const { data: accounts } = await useAccounts(); const { data: accounts } = await useAccounts();
const schedule = await useSchedule(); const schedule = await useSchedule();
const isCrew = computed(() => ( const isCrew = computed(() => (
session.value?.account?.type === "crew" sessionStore.account?.type === "crew"
|| session.value?.account?.type === "admin" || sessionStore.account?.type === "admin"
)); ));
const route = useRoute(); const route = useRoute();
@ -78,12 +78,12 @@ const filter = computed({
}); });
const eventSlotFilter = computed(() => { const eventSlotFilter = computed(() => {
if (filter.value === undefined || !session.value) { if (filter.value === undefined || !sessionStore.account) {
return () => true; return () => true;
} }
const aid = session.value?.account?.id; const aid = sessionStore.account?.id;
if (filter.value === "my-schedule") { if (filter.value === "my-schedule") {
const ids = new Set(session.value?.account?.interestedIds); const ids = new Set(sessionStore.account?.interestedIds);
for (const event of schedule.value.events) { for (const event of schedule.value.events) {
if (ids.has(event.id)) { if (ids.has(event.id)) {
for (const slot of event.slots) { for (const slot of event.slots) {
@ -103,11 +103,11 @@ const eventSlotFilter = computed(() => {
return () => false; return () => false;
}); });
const shiftSlotFilter = computed(() => { const shiftSlotFilter = computed(() => {
if (filter.value === undefined || !session.value) { if (filter.value === undefined || !sessionStore.account) {
return () => true; return () => true;
} }
if (filter.value === "my-schedule" || filter.value === "assigned") { if (filter.value === "my-schedule" || filter.value === "assigned") {
const aid = session.value?.account?.id; const aid = sessionStore.account?.id;
return (slot: ShiftSlot) => slot.assigned?.some(id => id === aid) || false; return (slot: ShiftSlot) => slot.assigned?.some(id => id === aid) || false;
} }
if (filter.value.startsWith("crew-")) { if (filter.value.startsWith("crew-")) {

41
pnpm-lock.yaml generated
View file

@ -8,12 +8,18 @@ importers:
.: .:
dependencies: dependencies:
'@pinia/nuxt':
specifier: 0.11.0
version: 0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.2)(vue@3.5.14(typescript@5.8.2)))
luxon: luxon:
specifier: ^3.5.0 specifier: ^3.5.0
version: 3.5.0 version: 3.5.0
nuxt: nuxt:
specifier: ^3.17.4 specifier: ^3.17.4
version: 3.17.4(@parcel/watcher@2.5.1)(@types/node@22.13.8)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.41.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) version: 3.17.4(@parcel/watcher@2.5.1)(@types/node@22.13.8)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.41.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
pinia:
specifier: ^3.0.2
version: 3.0.2(typescript@5.8.2)(vue@3.5.14(typescript@5.8.2))
vue: vue:
specifier: latest specifier: latest
version: 3.5.14(typescript@5.8.2) version: 3.5.14(typescript@5.8.2)
@ -633,6 +639,11 @@ packages:
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
'@pinia/nuxt@0.11.0':
resolution: {integrity: sha512-QGFlUAkeVAhPCTXacrtNP4ti24sGEleVzmxcTALY9IkS6U5OUox7vmNL1pkqBeW39oSNq/UC5m40ofDEPHB1fg==}
peerDependencies:
pinia: ^3.0.2
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -949,6 +960,9 @@ packages:
'@vue/devtools-api@6.6.4': '@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
'@vue/devtools-api@7.7.6':
resolution: {integrity: sha512-b2Xx0KvXZObePpXPYHvBRRJLDQn5nhKjXh7vUhMEtWxz1AYNFOVIsh5+HLP8xDGL7sy+Q7hXeUxPHB/KgbtsPw==}
'@vue/devtools-core@7.7.6': '@vue/devtools-core@7.7.6':
resolution: {integrity: sha512-ghVX3zjKPtSHu94Xs03giRIeIWlb9M+gvDRVpIZ/cRIxKHdW6HE/sm1PT3rUYS3aV92CazirT93ne+7IOvGUWg==} resolution: {integrity: sha512-ghVX3zjKPtSHu94Xs03giRIeIWlb9M+gvDRVpIZ/cRIxKHdW6HE/sm1PT3rUYS3aV92CazirT93ne+7IOvGUWg==}
peerDependencies: peerDependencies:
@ -2478,6 +2492,15 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'} engines: {node: '>=12'}
pinia@3.0.2:
resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==}
peerDependencies:
typescript: '>=4.4.4'
vue: ^2.7.0 || ^3.5.11
peerDependenciesMeta:
typescript:
optional: true
pkg-types@1.3.1: pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@ -4240,6 +4263,13 @@ snapshots:
'@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-ia32': 2.5.1
'@parcel/watcher-win32-x64': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1
'@pinia/nuxt@0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.2)(vue@3.5.14(typescript@5.8.2)))':
dependencies:
'@nuxt/kit': 3.17.4(magicast@0.3.5)
pinia: 3.0.2(typescript@5.8.2)(vue@3.5.14(typescript@5.8.2))
transitivePeerDependencies:
- magicast
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@ -4554,6 +4584,10 @@ snapshots:
'@vue/devtools-api@6.6.4': {} '@vue/devtools-api@6.6.4': {}
'@vue/devtools-api@7.7.6':
dependencies:
'@vue/devtools-kit': 7.7.6
'@vue/devtools-core@7.7.6(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.14(typescript@5.8.2))': '@vue/devtools-core@7.7.6(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.14(typescript@5.8.2))':
dependencies: dependencies:
'@vue/devtools-kit': 7.7.6 '@vue/devtools-kit': 7.7.6
@ -6256,6 +6290,13 @@ snapshots:
picomatch@4.0.2: {} picomatch@4.0.2: {}
pinia@3.0.2(typescript@5.8.2)(vue@3.5.14(typescript@5.8.2)):
dependencies:
'@vue/devtools-api': 7.7.6
vue: 3.5.14(typescript@5.8.2)
optionalDependencies:
typescript: 5.8.2
pkg-types@1.3.1: pkg-types@1.3.1:
dependencies: dependencies:
confbox: 0.1.8 confbox: 0.1.8

61
stores/session.ts Normal file
View file

@ -0,0 +1,61 @@
import { appendResponseHeader } from "h3";
import type { H3Event } from "h3";
import type { Account } from "~/shared/types/account";
const fetchSessionWithCookie = async (event?: H3Event) => {
// Client side
if (!event) {
return $fetch("/api/auth/session");
}
// Server side
const cookie = useRequestHeader("cookie");
const res = await $fetch.raw("/api/auth/session", {
headers: cookie ? { cookie } : undefined
});
for (const cookie of res.headers.getSetCookie()) {
appendResponseHeader(event, "set-cookie", cookie);
}
return res._data;
}
export const useSessionStore = defineStore("session", () => {
const state = {
account: ref<Account>(),
id: ref<number>(),
push: ref<boolean>(false),
};
const actions = {
async fetch(event?: H3Event) {
const session = await fetchSessionWithCookie(event)
state.account.value = session?.account;
state.id.value = session?.id;
state.push.value = session?.push ?? false;
},
async logIn(name: string) {
const res = await $fetch.raw("/api/auth/login", {
method: "POST",
body: { name },
});
await actions.fetch();
return `/api/auth/login replied: ${res.status} ${res.statusText}`;
},
async logOut() {
try {
await $fetch.raw("/api/auth/session", {
method: "DELETE",
});
await actions.fetch();
} catch (err: any) {
alert(`Log out failed: ${err.statusCode} ${err.statusMessage}`);
}
},
};
return {
...state,
...actions,
};
});