feat: HttpOnly Cookie 登入狀態初始化

This commit is contained in:
skytek_xinliang
2026-08-31 09:26:47 +08:00
parent db089ce267
commit 28cda7be1c
5 changed files with 60 additions and 7 deletions
+7
View File
@@ -9,6 +9,7 @@ import { createApp } from "vue";
// Plugins
import { registerPlugins } from "@/plugins";
import { useAppStore } from "@/stores/app";
// Components
import App from "./App.vue";
@@ -20,4 +21,10 @@ const app = createApp(App);
registerPlugins(app);
async function bootstrap() {
const store = useAppStore();
await store.initializeAuthentication();
app.mount("#app");
}
void bootstrap();
+3 -3
View File
@@ -6,8 +6,8 @@
<v-card-title class="text-primary font-weight-bold"
>個人資料修改</v-card-title
>
<v-card-subtitle class="text-grey-darken-3"
>更新您的基本資料與緊急聯絡資訊。</v-card-subtitle
<v-card-subtitle class="text-grey-darken-3">
更新您的基本資料與緊急聯絡資訊。</v-card-subtitle
>
</v-card>
@@ -59,7 +59,7 @@
icon="mdi-lock-outline"
variant="tonal"
>
手機門號不可由個人資料修改
手機門號不可自行修改
</v-alert>
<v-text-field
@@ -513,7 +513,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { computed, onMounted, reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import type {
ChronicDisease,
@@ -687,16 +687,25 @@ function scrollToPageTop() {
window.scrollTo({ top: 0 });
}
function clearErrorMessage() {
errorMessage.value = "";
}
function goToPreviousStep() {
clearErrorMessage();
currentStep.value -= 1;
scrollToPageTop();
}
function goToNextStep() {
clearErrorMessage();
currentStep.value += 1;
scrollToPageTop();
}
watch(profileForm, clearErrorMessage);
watch(assessmentForm, clearErrorMessage);
function buildAssessmentRequest(): SelfAssessmentRequest | null {
if (
!profileForm.name.trim() ||
+4 -2
View File
@@ -78,8 +78,9 @@ const router = createRouter({
});
// 簡單的導航守衛:只有標記需要登入的頁面會導向登入頁
router.beforeEach((to) => {
router.beforeEach(async (to) => {
const store = useAppStore();
await store.initializeAuthentication();
if (to.meta.requiresAuth && !store.isLoggedIn) {
return {
@@ -99,9 +100,10 @@ if (typeof window !== "undefined") {
window.addEventListener("auth-expired", () => {
const store = useAppStore();
const currentRoute = router.currentRoute.value;
const wasLoggedIn = store.isLoggedIn;
store.clearAuthentication();
if (currentRoute.name !== "login") {
if (wasLoggedIn && currentRoute.name !== "login") {
void router.replace({
name: "login",
query: { redirect: currentRoute.fullPath },
+35
View File
@@ -317,6 +317,8 @@ function createDefaultRadarData() {
export const useAppStore = defineStore("app", () => {
const isLoggedIn = ref(false);
const isAuthInitialized = ref(false);
let authInitializationPromise: Promise<boolean> | null = null;
/** 僅供前端顯示或其他使用者資料用途;course/group 不再傳送此值。 */
const userID = ref("");
const userProfile = ref<UserProfile>(createEmptyUserProfile());
@@ -368,6 +370,37 @@ export const useAppStore = defineStore("app", () => {
};
}
/** 以 HttpOnly Cookie 還原頁面重新整理後的登入狀態。 */
async function initializeAuthentication(): Promise<boolean> {
if (isAuthInitialized.value) {
return isLoggedIn.value;
}
if (authInitializationPromise) {
return authInitializationPromise;
}
authInitializationPromise = (async () => {
try {
if (!USE_MOCK) {
const profile = await apiGetProfile();
isLoggedIn.value = true;
userProfile.value = applyProfileToStore(profile);
}
return isLoggedIn.value;
} catch {
// 未登入時 profile API 會回 401,維持訪客狀態即可。
clearAuthentication();
return false;
} finally {
isAuthInitialized.value = true;
}
})();
return authInitializationPromise;
}
/** 使用手機門號與密碼登入,成功後只保存非敏感的回應資料。 */
async function login(phone: string, password: string): Promise<AuthResponse> {
if (!password) {
@@ -666,6 +699,7 @@ export const useAppStore = defineStore("app", () => {
return {
isLoggedIn,
isAuthInitialized,
userID,
userProfile,
selfAssessment,
@@ -679,6 +713,7 @@ export const useAppStore = defineStore("app", () => {
progressPercent,
login,
register,
initializeAuthentication,
logout,
clearAuthentication,
updateProfile,