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

View File

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

View File

@@ -8,6 +8,13 @@ const client = treaty<App>(window.location.origin, {
fetch: {
credentials: "include",
},
headers() {
const token = localStorage.getItem("user-token") || "";
return {
"Content-Type": "application/json",
"Authorization": token ? `Bearer ${token}` : "",
};
},
});
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(i18n);
beforeApp().then(() => {
router.isReady().then(() => {
app.mount("#app");
});
router.isReady().then(() => {
app.mount("#app");
});

View File

@@ -2,6 +2,11 @@ import type { Router } from "vue-router";
export function createRouterGuard(router: Router) {
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();
});
}

View File

@@ -36,40 +36,49 @@ const routes: Array<RouteRecordRaw> = [
{
path: "user",
component: () => import("@/views/user/index.vue"),
meta: { requiresAuth: true },
},
],
},
{
path: "/onchain-address",
component: () => import("@/views/onchain-address/index.vue"),
meta: { requiresAuth: true },
},
{
path: "/deposit/fiat",
component: () => import("@/views/deposit/fiat.vue"),
meta: { requiresAuth: true },
},
{
path: "/withdraw/index",
component: () => import("@/views/withdraw/index.vue"),
meta: { requiresAuth: true },
},
{
path: "/user/settings",
component: () => import("@/views/user-settings/outlet.vue"),
meta: { requiresAuth: true },
children: [
{
path: "",
component: () => import("@/views/user-settings/index.vue"),
meta: { requiresAuth: true },
},
{
path: "username",
component: () => import("@/views/user-settings/username.vue"),
meta: { requiresAuth: true },
},
{
path: "nickname",
component: () => import("@/views/user-settings/nickname.vue"),
meta: { requiresAuth: true },
},
{
path: "email",
component: () => import("@/views/user-settings/email.vue"),
meta: { requiresAuth: true },
},
],
},
@@ -95,23 +104,28 @@ const routes: Array<RouteRecordRaw> = [
path: "/issue/issuing-apply",
props: ({ query, params }) => ({ query, params }),
component: () => import("@/views/issue/issuing-apply/index.vue"),
meta: { requiresAuth: true },
},
{
path: "/trade-settings/bank-management",
component: () => import("@/views/trade-settings/bank-management/index.vue"),
meta: { requiresAuth: true },
},
{
path: "/trade-settings/my-subscribe",
component: () => import("@/views/trade-settings/my-subscribe/index.vue"),
meta: { requiresAuth: true },
},
{
path: "/trade-settings/bank-management/add",
component: () => import("@/views/trade-settings/bank-management/add.vue"),
meta: { requiresAuth: true },
},
{
path: "/trade-rwa/:id",
props: true,
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";
interface State {
user: UserData | null;
userProfile: UserProfileData | null;
session: UserData | null;
}
export const useUserStore = defineStore("user", () => {
const token = useStorage<string | null>("user-token", null);
const state = reactive<State>({
user: null,
userProfile: null,
session: null,
});
const isAuthenticated = computed(() => token.value !== null);
async function updateProfile() {
const { data } = await safeClient(client.api.user.profile.get(), { silent: true });
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 {
state,
...toRefs(state),
token,
isAuthenticated,
signOut,
setToken,
updateProfile,
};
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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