feat: HttpOnly Cookie 登入狀態初始化
This commit is contained in:
@@ -9,6 +9,7 @@ import { createApp } from "vue";
|
|||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
import { registerPlugins } from "@/plugins";
|
import { registerPlugins } from "@/plugins";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
@@ -20,4 +21,10 @@ const app = createApp(App);
|
|||||||
|
|
||||||
registerPlugins(app);
|
registerPlugins(app);
|
||||||
|
|
||||||
app.mount("#app");
|
async function bootstrap() {
|
||||||
|
const store = useAppStore();
|
||||||
|
await store.initializeAuthentication();
|
||||||
|
app.mount("#app");
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootstrap();
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<v-card-title class="text-primary font-weight-bold"
|
<v-card-title class="text-primary font-weight-bold"
|
||||||
>個人資料修改</v-card-title
|
>個人資料修改</v-card-title
|
||||||
>
|
>
|
||||||
<v-card-subtitle class="text-grey-darken-3"
|
<v-card-subtitle class="text-grey-darken-3">
|
||||||
>更新您的基本資料與緊急聯絡資訊。</v-card-subtitle
|
更新您的基本資料與緊急聯絡資訊。</v-card-subtitle
|
||||||
>
|
>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
icon="mdi-lock-outline"
|
icon="mdi-lock-outline"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
>
|
>
|
||||||
手機門號不可由個人資料修改
|
手機門號不可自行修改
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
|||||||
@@ -513,7 +513,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRouter } from "vue-router";
|
||||||
import type {
|
import type {
|
||||||
ChronicDisease,
|
ChronicDisease,
|
||||||
@@ -687,16 +687,25 @@ function scrollToPageTop() {
|
|||||||
window.scrollTo({ top: 0 });
|
window.scrollTo({ top: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearErrorMessage() {
|
||||||
|
errorMessage.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
function goToPreviousStep() {
|
function goToPreviousStep() {
|
||||||
|
clearErrorMessage();
|
||||||
currentStep.value -= 1;
|
currentStep.value -= 1;
|
||||||
scrollToPageTop();
|
scrollToPageTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToNextStep() {
|
function goToNextStep() {
|
||||||
|
clearErrorMessage();
|
||||||
currentStep.value += 1;
|
currentStep.value += 1;
|
||||||
scrollToPageTop();
|
scrollToPageTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(profileForm, clearErrorMessage);
|
||||||
|
watch(assessmentForm, clearErrorMessage);
|
||||||
|
|
||||||
function buildAssessmentRequest(): SelfAssessmentRequest | null {
|
function buildAssessmentRequest(): SelfAssessmentRequest | null {
|
||||||
if (
|
if (
|
||||||
!profileForm.name.trim() ||
|
!profileForm.name.trim() ||
|
||||||
|
|||||||
@@ -78,8 +78,9 @@ const router = createRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 簡單的導航守衛:只有標記需要登入的頁面會導向登入頁
|
// 簡單的導航守衛:只有標記需要登入的頁面會導向登入頁
|
||||||
router.beforeEach((to) => {
|
router.beforeEach(async (to) => {
|
||||||
const store = useAppStore();
|
const store = useAppStore();
|
||||||
|
await store.initializeAuthentication();
|
||||||
|
|
||||||
if (to.meta.requiresAuth && !store.isLoggedIn) {
|
if (to.meta.requiresAuth && !store.isLoggedIn) {
|
||||||
return {
|
return {
|
||||||
@@ -99,9 +100,10 @@ if (typeof window !== "undefined") {
|
|||||||
window.addEventListener("auth-expired", () => {
|
window.addEventListener("auth-expired", () => {
|
||||||
const store = useAppStore();
|
const store = useAppStore();
|
||||||
const currentRoute = router.currentRoute.value;
|
const currentRoute = router.currentRoute.value;
|
||||||
|
const wasLoggedIn = store.isLoggedIn;
|
||||||
store.clearAuthentication();
|
store.clearAuthentication();
|
||||||
|
|
||||||
if (currentRoute.name !== "login") {
|
if (wasLoggedIn && currentRoute.name !== "login") {
|
||||||
void router.replace({
|
void router.replace({
|
||||||
name: "login",
|
name: "login",
|
||||||
query: { redirect: currentRoute.fullPath },
|
query: { redirect: currentRoute.fullPath },
|
||||||
|
|||||||
@@ -317,6 +317,8 @@ function createDefaultRadarData() {
|
|||||||
|
|
||||||
export const useAppStore = defineStore("app", () => {
|
export const useAppStore = defineStore("app", () => {
|
||||||
const isLoggedIn = ref(false);
|
const isLoggedIn = ref(false);
|
||||||
|
const isAuthInitialized = ref(false);
|
||||||
|
let authInitializationPromise: Promise<boolean> | null = null;
|
||||||
/** 僅供前端顯示或其他使用者資料用途;course/group 不再傳送此值。 */
|
/** 僅供前端顯示或其他使用者資料用途;course/group 不再傳送此值。 */
|
||||||
const userID = ref("");
|
const userID = ref("");
|
||||||
const userProfile = ref<UserProfile>(createEmptyUserProfile());
|
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> {
|
async function login(phone: string, password: string): Promise<AuthResponse> {
|
||||||
if (!password) {
|
if (!password) {
|
||||||
@@ -666,6 +699,7 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
isAuthInitialized,
|
||||||
userID,
|
userID,
|
||||||
userProfile,
|
userProfile,
|
||||||
selfAssessment,
|
selfAssessment,
|
||||||
@@ -679,6 +713,7 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
progressPercent,
|
progressPercent,
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
|
initializeAuthentication,
|
||||||
logout,
|
logout,
|
||||||
clearAuthentication,
|
clearAuthentication,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
|
|||||||
Reference in New Issue
Block a user