From 13752f9906778749772bbb12e3d373a8ba3d01cc Mon Sep 17 00:00:00 2001 From: wikiboss Date: Mon, 21 Sep 2026 11:21:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=AE=9E=E7=8E=B0=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E8=B6=85=E7=AE=A1=E4=B8=8E=E5=A4=9A=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90=E6=B7=B1=E5=BA=A6=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=E3=80=81=E5=89=8D=E7=AB=AF=E5=8A=A8=E6=80=81=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E7=99=BD=E5=90=8D=E5=8D=95=E5=8F=8ADeepSeek=E9=85=8D?= =?UTF-8?q?=E9=A2=9D=E5=AE=88=E5=8D=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/layout.tsx | 6 +- admin/src/main.tsx | 14 ++- admin/src/routes.ts | 101 ++++++++++++++-- admin/src/store/index.ts | 3 + admin/tests/permission-isolation.test.ts | 70 +++++++++++ .../modules/ai/controller/admin/deepseek.ts | 71 +++++++++-- backend/src/modules/ai/service/deepseek.ts | 19 ++- .../src/modules/base/middleware/authority.ts | 5 +- .../modules/ai/admin-deepseek-tenant.test.ts | 113 ++++++++++++++++++ mpuniapp/common/request.uts | 4 +- mpuniapp/pages/common/login.uvue | 2 +- 11 files changed, 380 insertions(+), 28 deletions(-) create mode 100644 admin/tests/permission-isolation.test.ts create mode 100644 backend/test/modules/ai/admin-deepseek-tenant.test.ts diff --git a/admin/src/layout.tsx b/admin/src/layout.tsx index 20ddcd4..a983d4a 100644 --- a/admin/src/layout.tsx +++ b/admin/src/layout.tsx @@ -105,7 +105,11 @@ function PageLayout() { (state: GlobalState) => state ); - const [routes, defaultRoute] = useRoute(userInfo?.permissions); + const [routes, defaultRoute] = useRoute(userInfo?.permissions, { + isSuper: userInfo?.isSuper, + role: userInfo?.role, + tenantId: userInfo?.tenantId, + }); const defaultSelectedKeys = [currentComponent || defaultRoute]; const paths = (currentComponent || defaultRoute).split('/'); const defaultOpenKeys = paths.slice(0, paths.length - 1); diff --git a/admin/src/main.tsx b/admin/src/main.tsx index b7dd031..c1806ea 100644 --- a/admin/src/main.tsx +++ b/admin/src/main.tsx @@ -27,6 +27,9 @@ interface CoolPerson { username?: string; email?: string; headImg?: string; + isSuper?: boolean; + role?: string; + tenantId?: string | number; } interface CoolResponse { @@ -59,6 +62,8 @@ function Index() { .get>('/api/admin/base/comm/person') .then((res) => { const person = res.data?.code === COOL_OK ? res.data.data : undefined; + const isSuper = Boolean(person?.isSuper); + const role = person?.role || (isSuper ? 'super_admin' : 'tenant_owner'); store.dispatch({ type: 'update-userInfo', payload: { @@ -67,7 +72,10 @@ function Index() { name: person?.name || person?.nickName || person?.username || '', email: person?.email || '', avatar: person?.headImg || '', - permissions: generatePermission('admin'), + isSuper, + role, + tenantId: person?.tenantId, + permissions: generatePermission(role), }, }, }); @@ -78,7 +86,9 @@ function Index() { payload: { userLoading: false, userInfo: { - permissions: generatePermission('admin'), + isSuper: false, + role: 'sales_rep', + permissions: generatePermission('sales_rep'), }, }, }); diff --git a/admin/src/routes.ts b/admin/src/routes.ts index 581ffa3..e74f7ca 100644 --- a/admin/src/routes.ts +++ b/admin/src/routes.ts @@ -9,8 +9,20 @@ export type IRoute = AuthParams & { children?: IRoute[]; // 当前路由是否渲染菜单项,为 true 的话不会在菜单中显示,但可通过路由地址访问。 ignore?: boolean; + // 是否仅平台超级管理员可见 + superAdminOnly?: boolean; + // 是否仅租户可见 + tenantOnly?: boolean; + // 角色白名单访问限制 + roles?: string[]; }; +export interface RoleContext { + isSuper?: boolean; + role?: string; + tenantId?: string | number; +} + export const routes: IRoute[] = [ { name: 'menu.dashboard', @@ -27,6 +39,7 @@ export const routes: IRoute[] = [ { name: 'menu.dashboard.financial', key: 'dashboard/financial', + superAdminOnly: true, }, ], }, @@ -73,22 +86,39 @@ export const routes: IRoute[] = [ key: 'team', children: [ { name: 'menu.team.members', key: 'team/members' }, - { name: 'menu.team.brandKit', key: 'team/brand-kit' }, - { name: 'menu.team.handover', key: 'team/handover' }, + { + name: 'menu.team.brandKit', + key: 'team/brand-kit', + roles: ['super_admin', 'tenant_owner', 'admin'], + }, + { + name: 'menu.team.handover', + key: 'team/handover', + roles: ['super_admin', 'tenant_owner', 'admin'], + }, ], }, { name: 'menu.settingsBiz', key: 'settings-biz', children: [ - { name: 'menu.settings.tenant', key: 'settings/tenant' }, - { name: 'menu.settings.billing', key: 'settings/billing' }, + { + name: 'menu.settings.tenant', + key: 'settings/tenant', + roles: ['super_admin', 'tenant_owner', 'admin'], + }, + { + name: 'menu.settings.billing', + key: 'settings/billing', + roles: ['super_admin', 'tenant_owner', 'admin'], + }, { name: 'menu.settings.auditLogs', key: 'settings/audit-logs' }, ], }, { name: 'menu.platform', key: 'platform', + superAdminOnly: true, children: [ { name: 'menu.platform.subscribers', key: 'platform/subscribers' }, { name: 'menu.platform.renewals', key: 'platform/renewals' }, @@ -127,8 +157,57 @@ export const getName = (path: string, routes) => { }); }; +export const filterRoutesByRole = ( + routesList: IRoute[], + roleCtx?: RoleContext +): IRoute[] => { + if (!routesList || !routesList.length) { + return []; + } + const isSuper = Boolean(roleCtx?.isSuper || roleCtx?.role === 'super_admin'); + const role = roleCtx?.role || (isSuper ? 'super_admin' : 'sales_rep'); + + const result: IRoute[] = []; + + for (const route of routesList) { + // 1. 平台超级管理员专属路由隔离 + if (route.superAdminOnly && !isSuper) { + continue; + } + + // 2. 租户专属路由隔离 + if (route.tenantOnly && isSuper) { + continue; + } + + // 3. 角色白名单访问限制 + if (route.roles && route.roles.length > 0) { + const isAllowed = isSuper || route.roles.includes(role); + if (!isAllowed) { + continue; + } + } + + // 4. 递归处理子路由 + if (route.children && route.children.length > 0) { + const filteredChildren = filterRoutesByRole(route.children, roleCtx); + if (filteredChildren.length === 0) { + continue; + } + result.push({ + ...route, + children: filteredChildren, + }); + } else { + result.push({ ...route }); + } + } + + return result; +}; + export const generatePermission = (role: string) => { - const actions = role === 'admin' ? ['*'] : ['read']; + const actions = role === 'admin' || role === 'super_admin' ? ['*'] : ['read']; const result = {}; routes.forEach((item) => { if (item.children) { @@ -140,7 +219,10 @@ export const generatePermission = (role: string) => { return result; }; -const useRoute = (userPermission): [IRoute[], string] => { +const useRoute = ( + userPermission?: Record, + roleCtx?: RoleContext +): [IRoute[], string] => { const filterRoute = (routes: IRoute[], arr = []): IRoute[] => { if (!routes.length) { return []; @@ -148,7 +230,7 @@ const useRoute = (userPermission): [IRoute[], string] => { for (const route of routes) { const { requiredPermissions, oneOfPerm } = route; let visible = true; - if (requiredPermissions) { + if (requiredPermissions && userPermission) { visible = auth({ requiredPermissions, oneOfPerm }, userPermission); } @@ -172,9 +254,10 @@ const useRoute = (userPermission): [IRoute[], string] => { const [permissionRoute, setPermissionRoute] = useState(routes); useEffect(() => { - const newRoutes = filterRoute(routes); + const roleFiltered = filterRoutesByRole(routes, roleCtx); + const newRoutes = filterRoute(roleFiltered); setPermissionRoute(newRoutes); - }, [JSON.stringify(userPermission)]); + }, [JSON.stringify(userPermission), JSON.stringify(roleCtx)]); const defaultRoute = useMemo(() => { const first = permissionRoute[0]; diff --git a/admin/src/store/index.ts b/admin/src/store/index.ts index 84f0e03..eca1dd0 100644 --- a/admin/src/store/index.ts +++ b/admin/src/store/index.ts @@ -8,6 +8,9 @@ export interface GlobalState { organization?: string; location?: string; email?: string; + isSuper?: boolean; + role?: string; + tenantId?: string | number; permissions: Record; }; userLoading?: boolean; diff --git a/admin/tests/permission-isolation.test.ts b/admin/tests/permission-isolation.test.ts new file mode 100644 index 0000000..09e30fd --- /dev/null +++ b/admin/tests/permission-isolation.test.ts @@ -0,0 +1,70 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { routes, filterRoutesByRole } from '../src/routes'; + +test('1. 平台超级管理员 (Super Admin) 应拥有包含平台总控与全局财务大屏的完整菜单', () => { + const adminRoutes = filterRoutesByRole(routes, { + isSuper: true, + role: 'super_admin', + }); + + const keys = adminRoutes.map((r) => r.key); + assert.ok(keys.includes('platform'), '超管必须拥有 platform 平台总控菜单'); + assert.ok(keys.includes('dashboard'), '超管必须拥有 dashboard 菜单'); + + const dashboardRoute = adminRoutes.find((r) => r.key === 'dashboard'); + const dashChildrenKeys = dashboardRoute?.children?.map((c) => c.key) || []; + assert.ok( + dashChildrenKeys.includes('dashboard/financial'), + '超管必须拥有 dashboard/financial 全局财务大屏' + ); +}); + +test('2. 租户企业管理员 (Tenant Owner) 必须完全物理剔除【平台总控】与【全局财务大屏】', () => { + const tenantRoutes = filterRoutesByRole(routes, { + isSuper: false, + role: 'tenant_owner', + }); + + const keys = tenantRoutes.map((r) => r.key); + assert.ok(!keys.includes('platform'), '普通租户侧边栏绝对不得出现 platform 平台总控'); + + const dashboardRoute = tenantRoutes.find((r) => r.key === 'dashboard'); + const dashChildrenKeys = dashboardRoute?.children?.map((c) => c.key) || []; + assert.ok( + !dashChildrenKeys.includes('dashboard/financial'), + '普通租户绝不得访问或看到 dashboard/financial 全局财务大屏' + ); + + // 但租户必须能看到自己的名片、线索、自身订阅账单 + assert.ok(keys.includes('card'), '租户必须有名片微官网菜单'); + assert.ok(keys.includes('crm'), '租户必须有 CRM 菜单'); + assert.ok(keys.includes('settings-biz'), '租户必须有 企业设置 菜单'); + + const settingsRoute = tenantRoutes.find((r) => r.key === 'settings-biz'); + const settingsKeys = settingsRoute?.children?.map((c) => c.key) || []; + assert.ok( + settingsKeys.includes('settings/billing'), + '租户必须能访问自身的订阅账单 settings/billing' + ); +}); + +test('3. 租户一线销售员 (Sales Rep) 必须进一步限制企业高层设置与敏感人事交接', () => { + const repRoutes = filterRoutesByRole(routes, { + isSuper: false, + role: 'sales_rep', + }); + + const keys = repRoutes.map((r) => r.key); + assert.ok(!keys.includes('platform'), '销售员不得看到 platform'); + + const teamRoute = repRoutes.find((r) => r.key === 'team'); + const teamKeys = teamRoute?.children?.map((c) => c.key) || []; + assert.ok(!teamKeys.includes('team/handover'), '销售员不得看到离职交接'); + assert.ok(!teamKeys.includes('team/brand-kit'), '销售员不得篡改企业统一品牌规范'); + + const settingsRoute = repRoutes.find((r) => r.key === 'settings-biz'); + const settingsKeys = settingsRoute?.children?.map((c) => c.key) || []; + assert.ok(!settingsKeys.includes('settings/tenant'), '销售员不得修改企业主域名与证书'); + assert.ok(!settingsKeys.includes('settings/billing'), '销售员不得查看企业订阅账单'); +}); diff --git a/backend/src/modules/ai/controller/admin/deepseek.ts b/backend/src/modules/ai/controller/admin/deepseek.ts index ecbe8ba..1a49952 100644 --- a/backend/src/modules/ai/controller/admin/deepseek.ts +++ b/backend/src/modules/ai/controller/admin/deepseek.ts @@ -1,13 +1,15 @@ import { Body, Inject, Post } from '@midwayjs/core'; -import { BaseController, CoolController } from '@cool-midway/core'; +import { Context } from '@midwayjs/koa'; +import { BaseController, CoolController, CoolCommException } from '@cool-midway/core'; import { DeepSeekService } from '../../service/deepseek'; import { DeepSeekChatCompletionRequest, DeepSeekConfig, } from '../../../../comm/types/omni'; +import { assertSuperAdmin } from '../../../../comm/guard/super-admin'; /** - * DeepSeek 大模型后台管理与调试控制器 + * DeepSeek 大模型后台管理与调试控制器 (支持多租户 BYOK 与 Token 计量) */ @CoolController({ api: [], @@ -20,18 +22,32 @@ import { ], }) export class AdminDeepSeekController extends BaseController { + @Inject() + ctx: Context; + @Inject() deepSeekService: DeepSeekService; /** - * 获取当前有效配置(脱敏 API Key) + * 获取当前有效配置(脱敏 API Key,支持租户 BYOK 隔离) */ @Post('/config', { summary: '获取或保存 DeepSeek 配置' }) async config(@Body() body?: Partial) { + const tenantId = this.ctx?.admin?.tenantId; + if (body && Object.keys(body).length > 0) { - this.deepSeekService.updateConfig(body); + if (tenantId !== undefined && tenantId !== null) { + this.deepSeekService.updateTenantConfig(tenantId, body); + } else { + this.deepSeekService.updateConfig(body); + } } - const current = this.deepSeekService.getConfig(); + + const current = + tenantId !== undefined && tenantId !== null + ? this.deepSeekService.getTenantConfig(tenantId) + : this.deepSeekService.getConfig(); + const maskedKey = current.apiKey ? current.apiKey.length > 8 ? `${current.apiKey.slice(0, 4)}****${current.apiKey.slice(-4)}` @@ -41,16 +57,51 @@ export class AdminDeepSeekController extends BaseController { return this.ok({ ...current, apiKey: maskedKey, - isConfigured: this.deepSeekService.isConfigured(), + isConfigured: this.deepSeekService.isConfigured(tenantId), }); } + /** + * 获取当前租户的 Token 消耗统计与额度进度 + */ + @Post('/usage', { summary: '租户 Token 消耗与额度统计' }) + async usage() { + const tenantId = this.ctx?.admin?.tenantId || 0; + const usage = this.deepSeekService.getTenantTokenUsage(tenantId); + const quota = this.deepSeekService.getTenantTokenQuota(tenantId); + const percent = + quota > 0 ? Math.min(100, Math.round((usage.totalTokens / quota) * 100)) : 0; + + return this.ok({ + ...usage, + quota, + percent, + }); + } + + /** + * 超级管理员设置租户的 Token 配额上限 + */ + @Post('/quota', { summary: '设置租户 Token 配额上限' }) + async quota(@Body() body: { tenantId: string | number; maxTokens: number }) { + assertSuperAdmin(this.ctx); + if (!body?.tenantId) { + throw new CoolCommException('缺少租户标识 tenantId'); + } + this.deepSeekService.setTenantTokenQuota(body.tenantId, Number(body.maxTokens) || 0); + return this.ok({ success: true }); + } + /** * 测试与 DeepSeek 官方端点连通性 */ @Post('/test-connection', { summary: '测试连通性' }) async testConnection(@Body('apiKey') apiKey?: string) { - const res = await this.deepSeekService.testConnection(apiKey); + const tenantId = this.ctx?.admin?.tenantId; + const res = + tenantId !== undefined && tenantId !== null + ? await this.deepSeekService.testConnection(apiKey, tenantId) + : await this.deepSeekService.testConnection(apiKey); return this.ok(res); } @@ -59,7 +110,11 @@ export class AdminDeepSeekController extends BaseController { */ @Post('/chat', { summary: '调试对话补全' }) async chat(@Body() body: DeepSeekChatCompletionRequest) { - const reply = await this.deepSeekService.chatCompletion(body); + const tenantId = this.ctx?.admin?.tenantId; + const reply = + tenantId !== undefined && tenantId !== null + ? await this.deepSeekService.chatCompletion(body, undefined, tenantId) + : await this.deepSeekService.chatCompletion(body); return this.ok({ reply }); } } diff --git a/backend/src/modules/ai/service/deepseek.ts b/backend/src/modules/ai/service/deepseek.ts index a3165d6..76e646a 100644 --- a/backend/src/modules/ai/service/deepseek.ts +++ b/backend/src/modules/ai/service/deepseek.ts @@ -148,6 +148,13 @@ export class DeepSeekService extends BaseService { this.tenantQuotas.set(tenantId, Math.max(0, Number(maxTotalTokens) || 0)); } + /** + * 获取指定租户的 Token 最大额度上限 + */ + getTenantTokenQuota(tenantId: string | number): number { + return this.tenantQuotas.get(tenantId) || 0; + } + /** * 断言当前租户 Token 配额充足,超限则抛出 429 阻断 */ @@ -339,8 +346,12 @@ export class DeepSeekService extends BaseService { /** * 测试与 DeepSeek 官方端点连通性 */ - async testConnection(testApiKey?: string): Promise { - const key = (testApiKey || this.config.apiKey || '').trim(); + async testConnection( + testApiKey?: string, + tenantId?: number | string + ): Promise { + const cfg = this.getTenantConfig(tenantId); + const key = (testApiKey || cfg.apiKey || '').trim(); if (!key) { return { success: false, @@ -349,8 +360,8 @@ export class DeepSeekService extends BaseService { } const startTime = Date.now(); - const endpoint = `${this.config.baseUrl || 'https://api.deepseek.com'}/chat/completions`; - const model = this.config.model || 'deepseek-chat'; + const endpoint = `${cfg.baseUrl || 'https://api.deepseek.com'}/chat/completions`; + const model = cfg.model || 'deepseek-chat'; try { await this.httpClient.post( diff --git a/backend/src/modules/base/middleware/authority.ts b/backend/src/modules/base/middleware/authority.ts index c2cea8a..9bff591 100644 --- a/backend/src/modules/base/middleware/authority.ts +++ b/backend/src/modules/base/middleware/authority.ts @@ -49,7 +49,10 @@ export class BaseAuthorityMiddleware let statusCode = 200; let { url } = ctx; url = url.replace(this.prefix, '').split('?')[0]; - const token = ctx.get('Authorization'); + let token = ctx.get('Authorization') || ''; + if (token.startsWith('Bearer ')) { + token = token.substring(7); + } const adminUrl = '/admin/'; // 路由地址为 admin前缀的 需要权限校验 if (_.startsWith(url, adminUrl)) { diff --git a/backend/test/modules/ai/admin-deepseek-tenant.test.ts b/backend/test/modules/ai/admin-deepseek-tenant.test.ts new file mode 100644 index 0000000..3312da8 --- /dev/null +++ b/backend/test/modules/ai/admin-deepseek-tenant.test.ts @@ -0,0 +1,113 @@ +import { AdminDeepSeekController } from '../../../src/modules/ai/controller/admin/deepseek'; +import { DeepSeekService } from '../../../src/modules/ai/service/deepseek'; +import { CoolCommException } from '@cool-midway/core'; + +describe('AdminDeepSeekController 多租户 BYOK 与 Token 用量控制台 (TDD)', () => { + let controller: AdminDeepSeekController; + let service: DeepSeekService; + + beforeEach(() => { + service = new DeepSeekService(); + controller = new AdminDeepSeekController(); + controller.deepSeekService = service; + (controller as any).ok = (data: any) => ({ code: 1000, message: 'success', data }); + }); + + describe('1. 租户管理员私有 BYOK 配置隔离', () => { + it('租户管理员保存私有 API Key,仅生效于该租户,不污染全局配置', async () => { + // 模拟租户 888 的登录上下文 + (controller as any).ctx = { + admin: { username: 'tenant_admin_888', tenantId: 888 }, + }; + + // 租户保存私有 Key + const saveRes: any = await controller.config({ + apiKey: 'sk-tenant-888-secret-key-123456', + model: 'deepseek-reasoner', + }); + + expect(saveRes.code).toBe(1000); + expect(saveRes.data.isConfigured).toBe(true); + expect(saveRes.data.model).toBe('deepseek-reasoner'); + // 验证脱敏显示 + expect(saveRes.data.apiKey).toBe('sk-t****3456'); + + // 验证底层 service 中该租户已配置独立 Key + expect(service.isConfigured(888)).toBe(true); + expect(service.getTenantConfig(888).apiKey).toBe('sk-tenant-888-secret-key-123456'); + + // 验证全局公共池并未被污染 + expect(service.isConfigured()).toBe(false); + expect(service.getConfig().apiKey).toBeFalsy(); + }); + + it('超管访问 config 操作平台公共资金池配置', async () => { + // 模拟平台超管上下文 + (controller as any).ctx = { + admin: { username: 'admin', isSuper: true }, + }; + + await controller.config({ + apiKey: 'sk-platform-master-key-999999', + model: 'deepseek-chat', + }); + + // 平台全局已配置 + expect(service.isConfigured()).toBe(true); + expect(service.getConfig().apiKey).toBe('sk-platform-master-key-999999'); + }); + }); + + describe('2. 租户 Token 消耗统计与用量查询 (POST /usage)', () => { + it('租户可准确查询自身的 Token 消耗与配额进度', async () => { + // 模拟记录租户 666 的 Token 消耗 + service.recordTokenUsage(666, { + promptTokens: 120, + completionTokens: 80, + totalTokens: 200, + }); + service.setTenantTokenQuota(666, 10000); + + (controller as any).ctx = { + admin: { username: 'tenant_user_666', tenantId: 666 }, + }; + + const usageRes = await (controller as any).usage(); + expect(usageRes.code).toBe(1000); + expect(usageRes.data.totalTokens).toBe(200); + expect(usageRes.data.promptTokens).toBe(120); + expect(usageRes.data.completionTokens).toBe(80); + expect(usageRes.data.callCount).toBe(1); + expect(usageRes.data.quota).toBe(10000); + expect(usageRes.data.percent).toBe(2); // 200 / 10000 = 2% + }); + }); + + describe('3. 超管设置租户配额 (POST /quota)', () => { + it('超级管理员可成功为指定租户设置额度上限', async () => { + (controller as any).ctx = { + admin: { username: 'admin', isSuper: true }, + }; + + const quotaRes = await (controller as any).quota({ + tenantId: 777, + maxTokens: 50000, + }); + + expect(quotaRes.code).toBe(1000); + // 验证生效 + expect(() => service.assertTenantTokenQuota(777, 40000)).not.toThrow(); + expect(() => service.assertTenantTokenQuota(777, 60000)).toThrow(CoolCommException); + }); + + it('非超级管理员调用 quota 必须抛出 403 阻断越权', async () => { + (controller as any).ctx = { + admin: { username: 'tenant_rep', tenantId: 10 }, + }; + + await expect( + (controller as any).quota({ tenantId: 10, maxTokens: 999999 }) + ).rejects.toThrow(CoolCommException); + }); + }); +}); diff --git a/mpuniapp/common/request.uts b/mpuniapp/common/request.uts index e42ebf0..683d429 100644 --- a/mpuniapp/common/request.uts +++ b/mpuniapp/common/request.uts @@ -150,10 +150,10 @@ export function request(config: RequestConfig): Promise { 'content-type': 'application/json', }; - // 自动挂载 Authorization 与 x-tenant-id + // 自动挂载 Authorization 与 x-tenant-id (对齐 Cool-Admin 原生 JWT 格式) const token = getAuthToken(); if (token) { - headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`; + headers['Authorization'] = token.startsWith('Bearer ') ? token.substring(7) : token; } const tenantId = getTenantId(); diff --git a/mpuniapp/pages/common/login.uvue b/mpuniapp/pages/common/login.uvue index 170905c..8cdcbdc 100644 --- a/mpuniapp/pages/common/login.uvue +++ b/mpuniapp/pages/common/login.uvue @@ -44,10 +44,10 @@