diff --git a/apps/web-antd/.env.development b/apps/web-antd/.env.development index c138f48..39fd1b2 100644 --- a/apps/web-antd/.env.development +++ b/apps/web-antd/.env.development @@ -4,10 +4,13 @@ VITE_PORT=5666 VITE_BASE=/ # 接口地址 -VITE_GLOB_API_URL=/api +VITE_GLOB_API_URL=https://api-tenant-dev.laosankeji.com/api/tenant/v1 + +# 可选:默认租户标识(若登录不使用 账号@手机号,可在此配置) +VITE_TENANT_ID=806357433394921472 # 是否开启 Nitro Mock服务,true 为开启,false 为关闭 -VITE_NITRO_MOCK=true +VITE_NITRO_MOCK=false # 是否打开 devtools,true 为打开,false 为关闭 VITE_DEVTOOLS=false diff --git a/apps/web-antd/src/api/core/auth.ts b/apps/web-antd/src/api/core/auth.ts index 71d9f99..567f71a 100644 --- a/apps/web-antd/src/api/core/auth.ts +++ b/apps/web-antd/src/api/core/auth.ts @@ -1,20 +1,43 @@ import { baseRequestClient, requestClient } from '#/api/request'; export namespace AuthApi { + export interface CurrentUserProfile { + account?: string; + avatar?: string; + displayName?: string; + merchantId?: null | number | string; + permissions?: string[]; + roles?: string[]; + tenantId?: number | string; + userId?: number | string; + } + /** 登录接口参数 */ export interface LoginParams { + account?: string; password?: string; username?: string; } /** 登录接口返回值 */ export interface LoginResult { - accessToken: string; + accessToken?: string; + accessTokenExpiresAt?: string; + isNewUser?: boolean; + refreshToken?: string; + refreshTokenExpiresAt?: string; + user?: CurrentUserProfile; } - export interface RefreshTokenResult { - data: string; - status: number; + export interface RefreshTokenParams { + refreshToken: string; + } + + export interface ApiResponse { + code: number; + data: T; + message?: string; + success: boolean; } } @@ -22,30 +45,33 @@ export namespace AuthApi { * 登录 */ export async function loginApi(data: AuthApi.LoginParams) { - return requestClient.post('/auth/login', data); + const account = data.account ?? data.username; + return requestClient.post('/auth/login', { + account, + password: data.password, + }); } /** * 刷新accessToken */ -export async function refreshTokenApi() { - return baseRequestClient.post('/auth/refresh', { - withCredentials: true, - }); +export async function refreshTokenApi(data: AuthApi.RefreshTokenParams) { + return baseRequestClient.post>( + '/auth/refresh', + data, + ); } /** * 退出登录 */ export async function logoutApi() { - return baseRequestClient.post('/auth/logout', { - withCredentials: true, - }); + return baseRequestClient.post('/auth/logout'); } /** * 获取用户权限码 */ export async function getAccessCodesApi() { - return requestClient.get('/auth/codes'); + return requestClient.get('/auth/permissions'); } diff --git a/apps/web-antd/src/api/core/menu.ts b/apps/web-antd/src/api/core/menu.ts index 9ef60b1..8b1d5a2 100644 --- a/apps/web-antd/src/api/core/menu.ts +++ b/apps/web-antd/src/api/core/menu.ts @@ -6,5 +6,5 @@ import { requestClient } from '#/api/request'; * 获取用户所有菜单 */ export async function getAllMenusApi() { - return requestClient.get('/menu/all'); + return requestClient.get('/auth/menu'); } diff --git a/apps/web-antd/src/api/core/user.ts b/apps/web-antd/src/api/core/user.ts index 7e28ea8..319f821 100644 --- a/apps/web-antd/src/api/core/user.ts +++ b/apps/web-antd/src/api/core/user.ts @@ -2,9 +2,46 @@ import type { UserInfo } from '@vben/types'; import { requestClient } from '#/api/request'; +const TENANT_STORAGE_KEY = 'sys-tenant-id'; + +interface TenantUserProfile { + account?: string; + avatar?: string; + displayName?: string; + merchantId?: null | number | string; + permissions?: string[]; + roles?: string[]; + tenantId?: number | string; + userId?: number | string; +} + +function normalizeId(value: null | number | string | undefined) { + if (value === null || value === undefined) { + return ''; + } + return String(value); +} + +function mapProfileToUserInfo(profile: TenantUserProfile): UserInfo { + return { + avatar: profile.avatar ?? '', + desc: '', + homePath: '', + realName: profile.displayName ?? profile.account ?? '', + roles: profile.roles ?? [], + token: '', + userId: normalizeId(profile.userId), + username: profile.account ?? '', + }; +} + /** * 获取用户信息 */ export async function getUserInfoApi() { - return requestClient.get('/user/info'); + const profile = await requestClient.get('/auth/profile'); + if (profile.tenantId !== null && profile.tenantId !== undefined) { + localStorage.setItem(TENANT_STORAGE_KEY, String(profile.tenantId)); + } + return mapProfileToUserInfo(profile); } diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts index 288dddd..a425032 100644 --- a/apps/web-antd/src/api/request.ts +++ b/apps/web-antd/src/api/request.ts @@ -20,6 +20,46 @@ import { useAuthStore } from '#/store'; import { refreshTokenApi } from './core'; const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); +const TENANT_HEADER_KEY = 'X-Tenant-Id'; +const TENANT_STORAGE_KEY = 'sys-tenant-id'; +const DEV_TENANT_ID = import.meta.env.DEV ? import.meta.env.VITE_TENANT_ID : ''; + +function getHeaderValue(headers: any, key: string) { + if (!headers) { + return ''; + } + + if (typeof headers.get === 'function') { + return headers.get(key) || headers.get(key.toLowerCase()) || ''; + } + + return headers[key] || headers[key.toLowerCase()] || ''; +} + +function setHeaderValue(headers: any, key: string, value: string) { + if (!headers) { + return; + } + + if (typeof headers.set === 'function') { + headers.set(key, value); + return; + } + + headers[key] = value; +} + +function resolveTenantId() { + const hostname = window.location.hostname; + const hostnameParts = hostname.split('.').filter(Boolean); + const isIpAddress = /^\d+\.\d+\.\d+\.\d+$/.test(hostname); + const subdomainTenantId = + hostnameParts.length > 2 && !isIpAddress ? hostnameParts[0] : ''; + + const storageTenantId = localStorage.getItem(TENANT_STORAGE_KEY) || ''; + + return subdomainTenantId || storageTenantId || DEV_TENANT_ID || ''; +} function createRequestClient(baseURL: string, options?: RequestClientOptions) { const client = new RequestClient({ @@ -50,10 +90,27 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { */ async function doRefreshToken() { const accessStore = useAccessStore(); - const resp = await refreshTokenApi(); - const newToken = resp.data; - accessStore.setAccessToken(newToken); - return newToken; + const currentRefreshToken = accessStore.refreshToken; + + if (!currentRefreshToken) { + throw new Error('refresh token is required'); + } + + const resp = await refreshTokenApi({ + refreshToken: currentRefreshToken, + }); + + const tokenData = resp?.data; + const newAccessToken = tokenData?.accessToken; + const newRefreshToken = tokenData?.refreshToken ?? currentRefreshToken; + + if (!newAccessToken) { + throw new Error('access token is missing in refresh response'); + } + + accessStore.setAccessToken(newAccessToken); + accessStore.setRefreshToken(newRefreshToken); + return newAccessToken; } function formatToken(token: null | string) { @@ -65,8 +122,20 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { fulfilled: async (config) => { const accessStore = useAccessStore(); - config.headers.Authorization = formatToken(accessStore.accessToken); - config.headers['Accept-Language'] = preferences.app.locale; + const authorization = formatToken(accessStore.accessToken); + if (authorization) { + setHeaderValue(config.headers, 'Authorization', authorization); + } + setHeaderValue(config.headers, 'Accept-Language', preferences.app.locale); + + const headerTenantId = getHeaderValue(config.headers, TENANT_HEADER_KEY); + if (!headerTenantId) { + const tenantId = resolveTenantId(); + if (tenantId) { + setHeaderValue(config.headers, TENANT_HEADER_KEY, String(tenantId)); + } + } + return config; }, }); @@ -76,7 +145,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { defaultResponseInterceptor({ codeField: 'code', dataField: 'data', - successCode: 0, + successCode: 200, }), ); diff --git a/apps/web-antd/src/main.ts b/apps/web-antd/src/main.ts index 5d728a0..22a0006 100644 --- a/apps/web-antd/src/main.ts +++ b/apps/web-antd/src/main.ts @@ -1,4 +1,4 @@ -import { initPreferences } from '@vben/preferences'; +import { initPreferences, updatePreferences } from '@vben/preferences'; import { unmountGlobalLoading } from '@vben/utils'; import { overridesPreferences } from './preferences'; @@ -19,6 +19,13 @@ async function initApplication() { overrides: overridesPreferences, }); + updatePreferences({ + app: { + accessMode: 'backend', + defaultHomePath: '/dashboard/console', + }, + }); + // 启动应用并挂载 // vue应用主要逻辑及视图 const { bootstrap } = await import('./bootstrap'); diff --git a/apps/web-antd/src/preferences.ts b/apps/web-antd/src/preferences.ts index b2e9ace..d94cd3a 100644 --- a/apps/web-antd/src/preferences.ts +++ b/apps/web-antd/src/preferences.ts @@ -8,6 +8,11 @@ import { defineOverridesPreferences } from '@vben/preferences'; export const overridesPreferences = defineOverridesPreferences({ // overrides app: { + accessMode: 'backend', + defaultHomePath: '/dashboard/console', name: import.meta.env.VITE_APP_TITLE, }, + theme: { + mode: 'light', + }, }); diff --git a/apps/web-antd/src/store/auth.ts b/apps/web-antd/src/store/auth.ts index bd496d1..018f721 100644 --- a/apps/web-antd/src/store/auth.ts +++ b/apps/web-antd/src/store/auth.ts @@ -33,11 +33,12 @@ export const useAuthStore = defineStore('auth', () => { let userInfo: null | UserInfo = null; try { loginLoading.value = true; - const { accessToken } = await loginApi(params); + const { accessToken, refreshToken } = await loginApi(params); // 如果成功获取到 accessToken if (accessToken) { accessStore.setAccessToken(accessToken); + accessStore.setRefreshToken(refreshToken ?? null); // 获取用户信息并存储到 accessStore 中 const [fetchUserInfoResult, accessCodes] = await Promise.all([ diff --git a/apps/web-antd/src/views/_core/authentication/login.vue b/apps/web-antd/src/views/_core/authentication/login.vue index 099e4c8..31c59c9 100644 --- a/apps/web-antd/src/views/_core/authentication/login.vue +++ b/apps/web-antd/src/views/_core/authentication/login.vue @@ -1,6 +1,5 @@