feat: 重构用户认证逻辑,添加导航重定向功能,更新相关组件和路由

This commit is contained in:
2025-12-21 02:37:01 +07:00
parent b957eb7cc2
commit 7fcb2555a3
18 changed files with 104 additions and 67 deletions

4
auto-imports.d.ts vendored
View File

@@ -234,6 +234,7 @@ declare global {
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
const useMousePressed: typeof import('@vueuse/core').useMousePressed const useMousePressed: typeof import('@vueuse/core').useMousePressed
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
const useNavigateToRedirect: typeof import('./src/composables/useNavigateToRedirect').useNavigateToRedirect
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
const useNetwork: typeof import('@vueuse/core').useNetwork const useNetwork: typeof import('@vueuse/core').useNetwork
const useNow: typeof import('@vueuse/core').useNow const useNow: typeof import('@vueuse/core').useNow
@@ -372,7 +373,6 @@ declare module 'vue' {
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']> readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']> readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']> readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly beforeApp: UnwrapRef<typeof import('./src/composables/beforeApp')['beforeApp']>
readonly clearExpiredCaches: UnwrapRef<typeof import('./src/composables/useStorageCache')['clearExpiredCaches']> readonly clearExpiredCaches: UnwrapRef<typeof import('./src/composables/useStorageCache')['clearExpiredCaches']>
readonly computed: UnwrapRef<typeof import('vue')['computed']> readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']> readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
@@ -516,7 +516,6 @@ declare module 'vue' {
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']> readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']> readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']> readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAuth: UnwrapRef<typeof import('./src/composables/useAuth')['useAuth']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']> readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']> readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']> readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
@@ -595,6 +594,7 @@ declare module 'vue' {
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']> readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']> readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']> readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigateToRedirect: UnwrapRef<typeof import('./src/composables/useNavigateToRedirect')['useNavigateToRedirect']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']> readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']> readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']> readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>

View File

@@ -1,10 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { App as CapacitorApp } from "@capacitor/app"; import { App as CapacitorApp } from "@capacitor/app";
const { isAuthenticated } = useAuth(); const userStore = useUserStore();
const { isAuthenticated } = storeToRefs(userStore);
const { locale, loadSavedLanguage } = useLanguage(); const { locale, loadSavedLanguage } = useLanguage();
const { initializeWallet } = useWalletStore();
const { updateProfile } = useUserStore();
onMounted(() => { onMounted(() => {
updateProfile();
initializeWallet();
CapacitorApp.addListener("appStateChange", async ({ isActive }) => { CapacitorApp.addListener("appStateChange", async ({ isActive }) => {
if (isActive && isAuthenticated.value) { if (isActive && isAuthenticated.value) {
await userStore.updateProfile(); await userStore.updateProfile();

View File

@@ -8,6 +8,13 @@ const client = treaty<App>(window.location.origin, {
fetch: { fetch: {
credentials: "include", credentials: "include",
}, },
headers() {
const token = localStorage.getItem("user-token") || "";
return {
"Content-Type": "application/json",
"Authorization": token ? `Bearer ${token}` : "",
};
},
}); });
export interface SafeClientOptions { export interface SafeClientOptions {

View File

@@ -1,15 +0,0 @@
export function beforeApp() {
const { updateProfile } = useUserStore();
const { initializeWallet } = useWalletStore();
return new Promise<void>((resolve) => {
useAuth().then((session) => {
if (session) {
updateProfile();
initializeWallet();
}
resolve();
});
});
}

View File

@@ -1,30 +0,0 @@
import type { UnwrapRef } from "vue";
import { safeClient } from "@/api";
import { authClient } from "@/auth";
type User = UnwrapRef<ReturnType<typeof authClient.useSession> extends Promise<infer R extends { data: any }> ? R["data"] : never>;
export function useAuth() {
const session = ref<User | null>(null);
if (session.value === undefined) {
safeClient(authClient.getSession()).then((res) => {
session.value = res.data.value;
});
}
const user = computed(() => session.value?.user);
const isAuthenticated = computed(() => !!session.value);
return {
user,
session,
isAuthenticated,
then(onfulfilled: (value: User | null) => void) {
return safeClient(authClient.getSession()).then((res) => {
session.value = res.data.value;
onfulfilled(session.value);
});
},
};
}

View File

@@ -0,0 +1,12 @@
import type { LocationQueryValue } from "vue-router";
import { router } from "@/router";
export function useNavigateToRedirect(redirect: LocationQueryValue): void;
export function useNavigateToRedirect(redirect: LocationQueryValue[], index: number): void;
export function useNavigateToRedirect(redirect: LocationQueryValue | LocationQueryValue[], index?: number) {
const _redirect = Array.isArray(redirect) ? redirect[index || 0] as string : redirect as string;
const path = decodeURIComponent(_redirect || "/");
router.replace(path);
}

View File

@@ -47,8 +47,6 @@ const app = createApp(App)
.use(router) .use(router)
.use(i18n); .use(i18n);
beforeApp().then(() => { router.isReady().then(() => {
router.isReady().then(() => {
app.mount("#app"); app.mount("#app");
});
}); });

View File

@@ -2,6 +2,11 @@ import type { Router } from "vue-router";
export function createRouterGuard(router: Router) { export function createRouterGuard(router: Router) {
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
const redirect = encodeURIComponent(to.fullPath);
return next({ path: "/auth/login", query: { redirect } });
}
next(); next();
}); });
} }

View File

@@ -36,40 +36,49 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: "user", path: "user",
component: () => import("@/views/user/index.vue"), component: () => import("@/views/user/index.vue"),
meta: { requiresAuth: true },
}, },
], ],
}, },
{ {
path: "/onchain-address", path: "/onchain-address",
component: () => import("@/views/onchain-address/index.vue"), component: () => import("@/views/onchain-address/index.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "/deposit/fiat", path: "/deposit/fiat",
component: () => import("@/views/deposit/fiat.vue"), component: () => import("@/views/deposit/fiat.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "/withdraw/index", path: "/withdraw/index",
component: () => import("@/views/withdraw/index.vue"), component: () => import("@/views/withdraw/index.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "/user/settings", path: "/user/settings",
component: () => import("@/views/user-settings/outlet.vue"), component: () => import("@/views/user-settings/outlet.vue"),
meta: { requiresAuth: true },
children: [ children: [
{ {
path: "", path: "",
component: () => import("@/views/user-settings/index.vue"), component: () => import("@/views/user-settings/index.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "username", path: "username",
component: () => import("@/views/user-settings/username.vue"), component: () => import("@/views/user-settings/username.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "nickname", path: "nickname",
component: () => import("@/views/user-settings/nickname.vue"), component: () => import("@/views/user-settings/nickname.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "email", path: "email",
component: () => import("@/views/user-settings/email.vue"), component: () => import("@/views/user-settings/email.vue"),
meta: { requiresAuth: true },
}, },
], ],
}, },
@@ -95,23 +104,28 @@ const routes: Array<RouteRecordRaw> = [
path: "/issue/issuing-apply", path: "/issue/issuing-apply",
props: ({ query, params }) => ({ query, params }), props: ({ query, params }) => ({ query, params }),
component: () => import("@/views/issue/issuing-apply/index.vue"), component: () => import("@/views/issue/issuing-apply/index.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "/trade-settings/bank-management", path: "/trade-settings/bank-management",
component: () => import("@/views/trade-settings/bank-management/index.vue"), component: () => import("@/views/trade-settings/bank-management/index.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "/trade-settings/my-subscribe", path: "/trade-settings/my-subscribe",
component: () => import("@/views/trade-settings/my-subscribe/index.vue"), component: () => import("@/views/trade-settings/my-subscribe/index.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "/trade-settings/bank-management/add", path: "/trade-settings/bank-management/add",
component: () => import("@/views/trade-settings/bank-management/add.vue"), component: () => import("@/views/trade-settings/bank-management/add.vue"),
meta: { requiresAuth: true },
}, },
{ {
path: "/trade-rwa/:id", path: "/trade-rwa/:id",
props: true, props: true,
component: () => import("@/views/trade-rwa/index.vue"), component: () => import("@/views/trade-rwa/index.vue"),
meta: { requiresAuth: true },
}, },
]; ];

View File

@@ -2,24 +2,41 @@ import type { UserData, UserProfileData } from "@/api/types";
import { client, safeClient } from "@/api"; import { client, safeClient } from "@/api";
interface State { interface State {
user: UserData | null;
userProfile: UserProfileData | null; userProfile: UserProfileData | null;
session: UserData | null;
} }
export const useUserStore = defineStore("user", () => { export const useUserStore = defineStore("user", () => {
const token = useStorage<string | null>("user-token", null);
const state = reactive<State>({ const state = reactive<State>({
user: null,
userProfile: null, userProfile: null,
session: null,
}); });
const isAuthenticated = computed(() => token.value !== null);
async function updateProfile() { async function updateProfile() {
const { data } = await safeClient(client.api.user.profile.get(), { silent: true }); const { data } = await safeClient(client.api.user.profile.get(), { silent: true });
state.userProfile = data.value?.userProfile || null; state.userProfile = data.value?.userProfile || null;
state.session = data.value?.user || null; state.user = data.value?.user || null;
}
function setToken(value: string) {
token.value = value;
}
function signOut() {
token.value = null;
state.userProfile = null;
state.user = null;
} }
return { return {
state, ...toRefs(state),
token,
isAuthenticated,
signOut,
setToken,
updateProfile, updateProfile,
}; };
}); });

View File

@@ -1,11 +1,14 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { EmailVerifyClient, PhoneNumberVerifyClient } from "@/api/types"; import type { EmailVerifyClient, PhoneNumberVerifyClient } from "@/api/types";
import { closeOutline } from "ionicons/icons";
import { authClient } from "@/auth"; import { authClient } from "@/auth";
import EmailLogin from "./components/email.vue"; import EmailLogin from "./components/email.vue";
import PhoneNumberLogin from "./components/phone-number.vue"; import PhoneNumberLogin from "./components/phone-number.vue";
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
async function handleSignInEmail(value: EmailVerifyClient) { async function handleSignInEmail(value: EmailVerifyClient) {
const { data } = await authClient.signIn.emailOtp({ const { data } = await authClient.signIn.emailOtp({
@@ -13,7 +16,8 @@ async function handleSignInEmail(value: EmailVerifyClient) {
otp: value.otp, otp: value.otp,
}); });
if (data?.token) { if (data?.token) {
router.back(); userStore.setToken(data.token);
useNavigateToRedirect(route.query.redirect as string);
} }
} }
async function handleSignInPhoneNumber(value: PhoneNumberVerifyClient) { async function handleSignInPhoneNumber(value: PhoneNumberVerifyClient) {
@@ -24,7 +28,8 @@ async function handleSignInPhoneNumber(value: PhoneNumberVerifyClient) {
updatePhoneNumber: false, updatePhoneNumber: false,
}); });
if (data?.token) { if (data?.token) {
router.back(); userStore.setToken(data.token);
useNavigateToRedirect(route.query.redirect as string);
} }
} }
</script> </script>
@@ -33,7 +38,11 @@ async function handleSignInPhoneNumber(value: PhoneNumberVerifyClient) {
<IonPage> <IonPage>
<IonHeader class="ion-no-border"> <IonHeader class="ion-no-border">
<IonToolbar class="ui-toolbar"> <IonToolbar class="ui-toolbar">
<ion-back-button slot="start" /> <ion-buttons slot="start">
<ion-button @click="$router.back()">
<ion-icon slot="icon-only" :icon="closeOutline" />
</ion-button>
</ion-buttons>
<ion-button slot="end" fill="clear" @click="router.push('/auth/signup')"> <ion-button slot="end" fill="clear" @click="router.push('/auth/signup')">
{{ t('auth.login.signupButton') }} {{ t('auth.login.signupButton') }}
</ion-button> </ion-button>

View File

@@ -1,7 +1,8 @@
<script lang='ts' setup> <script lang='ts' setup>
import { copyOutline, qrCodeOutline, shareOutline } from "ionicons/icons"; import { copyOutline, qrCodeOutline, shareOutline } from "ionicons/icons";
const { user } = useAuth(); const userStore = useUserStore();
const { user } = storeToRefs(userStore);
</script> </script>
<template> <template>

View File

@@ -7,7 +7,8 @@ import * as yup from "yup";
import { safeClient } from "@/api"; import { safeClient } from "@/api";
import { authClient, emailSchema } from "@/auth"; import { authClient, emailSchema } from "@/auth";
const { user } = useAuth(); const userStore = useUserStore();
const { user } = storeToRefs(userStore);
const email = ref(user.value?.email || ""); const email = ref(user.value?.email || "");
const { updateProfile } = useUserStore(); const { updateProfile } = useUserStore();
const { t } = useI18n(); const { t } = useI18n();

View File

@@ -6,6 +6,7 @@ import { authClient } from "@/auth";
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const { user, userProfile } = storeToRefs(userStore);
async function handleSignOut() { async function handleSignOut() {
const alert = await alertController.create({ const alert = await alertController.create({
@@ -20,6 +21,7 @@ async function handleSignOut() {
text: "Sign Out", text: "Sign Out",
role: "destructive", role: "destructive",
handler: async () => { handler: async () => {
userStore.signOut();
authClient.signOut(); authClient.signOut();
router.replace("/layout/riwa"); router.replace("/layout/riwa");
}, },
@@ -88,7 +90,7 @@ async function handleSignOut() {
</div> </div>
</div> </div>
<div class="end"> <div class="end">
{{ userStore.state.userProfile?.nickname }} {{ userProfile?.nickname }}
</div> </div>
</div> </div>
</ion-item> </ion-item>
@@ -101,7 +103,7 @@ async function handleSignOut() {
</div> </div>
</div> </div>
<div class="end"> <div class="end">
{{ userStore.state.session?.email }} {{ user?.email }}
</div> </div>
</div> </div>
</ion-item> </ion-item>

View File

@@ -2,10 +2,10 @@
import { toastController } from "@ionic/vue"; import { toastController } from "@ionic/vue";
import { arrowBackOutline } from "ionicons/icons"; import { arrowBackOutline } from "ionicons/icons";
import { client, safeClient } from "@/api"; import { client, safeClient } from "@/api";
import { authClient } from "@/auth";
const userStore = useUserStore(); const userStore = useUserStore();
const nickname = ref(userStore.state.userProfile?.nickname || ""); const { userProfile } = storeToRefs(userStore);
const nickname = ref(userProfile.value?.nickname || "");
const { updateProfile } = useUserStore(); const { updateProfile } = useUserStore();
async function handleSave() { async function handleSave() {

View File

@@ -4,7 +4,8 @@ import { arrowBackOutline } from "ionicons/icons";
import { safeClient } from "@/api"; import { safeClient } from "@/api";
import { authClient } from "@/auth"; import { authClient } from "@/auth";
const { user } = useAuth(); const userStore = useUserStore();
const { user } = storeToRefs(userStore);
const username = ref(user.value?.username || ""); const username = ref(user.value?.username || "");
const { updateProfile } = useUserStore(); const { updateProfile } = useUserStore();

View File

@@ -1,7 +1,8 @@
<script lang='ts' setup> <script lang='ts' setup>
import { chevronForwardOutline, copyOutline, qrCodeOutline } from "ionicons/icons"; import { chevronForwardOutline, copyOutline, qrCodeOutline } from "ionicons/icons";
const { user } = useAuth(); const userStore = useUserStore();
const { user } = storeToRefs(userStore);
</script> </script>
<template> <template>

9
src/vite-env.d.ts vendored
View File

@@ -7,6 +7,9 @@ import {
DefineLocaleMessage, DefineLocaleMessage,
DefineNumberFormat, DefineNumberFormat,
} from "vue-i18n"; } from "vue-i18n";
import "vue-router";
export {};
declare module "vue-i18n" { declare module "vue-i18n" {
// define the locale messages schema // define the locale messages schema
@@ -21,3 +24,9 @@ declare module "vue-i18n" {
export interface DefineNumberFormat { export interface DefineNumberFormat {
} }
} }
declare module "vue-router" {
interface RouteMeta {
requiresAuth: ? boolean;
}
}