Skip to content

Commit 26eac45

Browse files
committed
Merge branch 'develop' of https://github.com/BidingCC/BuildingAI into develop
2 parents 41588d5 + b7f0df4 commit 26eac45

309 files changed

Lines changed: 536 additions & 395 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/@buildingai/web/http/src/core/http-client.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ function toHttpError(error: unknown): HttpError {
9494
});
9595
}
9696

97+
function isAccessError(error: HttpError): boolean {
98+
const responseData =
99+
error.details && typeof error.details === "object" && "data" in error.details
100+
? (error.details.data as { code?: unknown } | undefined)
101+
: undefined;
102+
const businessCode = error.code ?? responseData?.code;
103+
return String(businessCode) === "40203";
104+
}
105+
97106
export class HttpClient {
98107
private readonly axios: AxiosInstance;
99108
private readonly options: HttpClientOptions;
@@ -189,7 +198,11 @@ export class HttpClient {
189198
}
190199

191200
const httpError = toHttpError(error);
192-
if (!silent) this.options.hooks?.onError?.(httpError);
201+
if (!silent && isAccessError(httpError)) {
202+
await this.options.hooks?.onAccessError?.(httpError);
203+
} else if (!silent) {
204+
this.options.hooks?.onError?.(httpError);
205+
}
193206
throw httpError;
194207
}
195208
}

packages/@buildingai/web/http/src/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export interface RetryOptions {
6969
export interface HttpHooks {
7070
getAccessToken?: () => string | undefined | Promise<string | undefined>;
7171
onAuthError?: (error: unknown) => void | Promise<void>;
72+
onAccessError?: (error: HttpError) => void | Promise<void>;
7273
/**
7374
* Called when request received 401 and you want to refresh token.
7475
* Return true to indicate refresh succeeded and request should be retried.

packages/@buildingai/web/services/src/base.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { useAuthStore } from "@buildingai/stores";
33
import type { AxiosError } from "axios";
44
import { toast } from "sonner";
55

6+
import { getFirstConsoleMenuPath, hasConsoleAccess, WEB_HOME_PATH } from "./shared/console-access";
7+
68
const isDev = import.meta.env.DEV;
79
const devBase = import.meta.env.VITE_DEVELOP_APP_BASE_URL;
810
const prodBase = import.meta.env.VITE_PRODUCTION_APP_BASE_URL;
@@ -33,6 +35,17 @@ async function handleAuthError(error: unknown): Promise<void> {
3335
location.replace(`/login?redirect=${location.pathname}`);
3436
}
3537

38+
async function handleAccessError(): Promise<void> {
39+
const userInfo = useAuthStore.getState().auth.userInfo;
40+
const target = hasConsoleAccess(userInfo)
41+
? getFirstConsoleMenuPath(userInfo?.menus ?? [])
42+
: WEB_HOME_PATH;
43+
44+
if (location.pathname !== target) {
45+
location.replace(target);
46+
}
47+
}
48+
3649
export const apiHttpClient = createHttpClient({
3750
baseURL: isDev ? devBase : prodBase,
3851
pathPrefix: import.meta.env.VITE_APP_WEB_API_PREFIX || "/api",
@@ -42,6 +55,7 @@ export const apiHttpClient = createHttpClient({
4255
return useAuthStore.getState().auth.token || "";
4356
},
4457
onAuthError: handleAuthError,
58+
onAccessError: handleAccessError,
4559
onError: handleHttpError,
4660
},
4761
});
@@ -55,6 +69,7 @@ export const consoleHttpClient = createHttpClient({
5569
return useAuthStore.getState().auth.token || "";
5670
},
5771
onAuthError: handleAuthError,
72+
onAccessError: handleAccessError,
5873
onError: handleHttpError,
5974
},
6075
});
@@ -106,6 +121,7 @@ export function createPluginHttpClients(pluginIdentifier?: string) {
106121
return useAuthStore.getState().auth.token || "";
107122
},
108123
onAuthError: handlePluginAuthError,
124+
onAccessError: handleAccessError,
109125
onError: handleHttpError,
110126
},
111127
});
@@ -119,6 +135,7 @@ export function createPluginHttpClients(pluginIdentifier?: string) {
119135
return useAuthStore.getState().auth.token || "";
120136
},
121137
onAuthError: handlePluginAuthError,
138+
onAccessError: handleAccessError,
122139
onError: handleHttpError,
123140
},
124141
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { MenuItem, UserInfo } from "@buildingai/web-types";
2+
3+
export const CONSOLE_HOME_PATH = "/console/dashboard";
4+
export const WEB_HOME_PATH = "/";
5+
6+
function isEnabled(status: number | string | boolean | undefined | null): boolean {
7+
if (typeof status === "boolean") return status;
8+
if (typeof status === "number") return status === 1;
9+
if (typeof status === "string") {
10+
return ["1", "true", "yes", "y", "on", "enabled", "enable", "active"].includes(
11+
status.toLowerCase().trim(),
12+
);
13+
}
14+
return false;
15+
}
16+
17+
export function hasConsoleAccess(userInfo?: Pick<UserInfo, "permissions" | "isRoot"> | null) {
18+
return !!userInfo && (isEnabled(userInfo.permissions) || isEnabled(userInfo.isRoot));
19+
}
20+
21+
function normalizePath(path: string) {
22+
return path.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
23+
}
24+
25+
function collectConsoleMenuPaths(items: MenuItem[], parentPath = "", out: string[] = []) {
26+
for (const item of items) {
27+
const currentPath = item.path
28+
? [parentPath, item.path].filter(Boolean).join("/")
29+
: parentPath;
30+
31+
if (item.type === 2 && item.path && item.path !== "#") {
32+
out.push(normalizePath(`/console/${currentPath}`));
33+
}
34+
35+
if (item.children?.length) {
36+
collectConsoleMenuPaths(item.children, currentPath, out);
37+
}
38+
}
39+
40+
return out;
41+
}
42+
43+
export function getConsoleMenuPaths(menus: MenuItem[] = []) {
44+
return collectConsoleMenuPaths(menus);
45+
}
46+
47+
export function getFirstConsoleMenuPath(menus: MenuItem[] = []) {
48+
const paths = getConsoleMenuPaths(menus);
49+
return paths.find((path) => path === CONSOLE_HOME_PATH) ?? paths[0] ?? CONSOLE_HOME_PATH;
50+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { createPluginHttpClients } from "../base";
22
export * from "./config";
3+
export * from "./console-access";
34
export * from "./upload";
45
export * from "./upload-oss";
56
export * from "./user";

packages/client/src/components/ask-assistant-ui/hooks/use-chat-stream.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,14 @@ export function useChatStream(options: UseChatStreamOptions): UseChatStreamRetur
239239
) => {
240240
if (status === "streaming") return;
241241
if (!content.trim() && (!files || files.length === 0)) return;
242+
if (!token) {
243+
const redirect = `${location.pathname}${location.search}`;
244+
navigate(`/login?redirect=${encodeURIComponent(redirect)}`, {
245+
replace: true,
246+
state: { redirect },
247+
});
248+
return;
249+
}
242250
setStatusOverride(null);
243251
pendingParentIdRef.current = parentId !== undefined ? parentId : lastMessageDbIdRef.current;
244252

@@ -257,7 +265,7 @@ export function useChatStream(options: UseChatStreamOptions): UseChatStreamRetur
257265
...(fileParts && { files: fileParts }),
258266
});
259267
},
260-
[sendMessage, status, lastMessageDbIdRef],
268+
[sendMessage, status, token, location.pathname, location.search, navigate, lastMessageDbIdRef],
261269
);
262270

263271
const streamingMessageId =

packages/client/src/layouts/console/index.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import {
2+
getConsoleMenuPaths,
3+
getFirstConsoleMenuPath,
4+
hasConsoleAccess,
5+
WEB_HOME_PATH,
6+
} from "@buildingai/services/shared";
17
import { useAuthStore } from "@buildingai/stores";
28
import NotFoundPage from "@buildingai/ui/components/exception/not-found-page";
39
import { ScrollArea } from "@buildingai/ui/components/ui/scroll-area";
@@ -6,7 +12,7 @@ import type { MenuItem } from "@buildingai/web-types";
612
import type { ComponentType } from "react";
713
import { useMemo } from "react";
814
import type { RouteObject } from "react-router-dom";
9-
import { useRoutes } from "react-router-dom";
15+
import { Navigate, useLocation, useRoutes } from "react-router-dom";
1016

1117
import AccessMenuIndexPage from "@/pages/console/access/menu";
1218
import AccessPermissionIndexPage from "@/pages/console/access/permission";
@@ -267,6 +273,28 @@ function ConsoleRoutes() {
267273
}
268274

269275
export default function ConsoleLayout({ children }: { children?: React.ReactNode }) {
276+
const location = useLocation();
277+
const { userInfo } = useAuthStore((state) => state.auth);
278+
const menuPaths = useMemo(() => getConsoleMenuPaths(userInfo?.menus ?? []), [userInfo?.menus]);
279+
const firstConsolePath = useMemo(
280+
() => getFirstConsoleMenuPath(userInfo?.menus ?? []),
281+
[userInfo?.menus],
282+
);
283+
284+
if (!userInfo) return null;
285+
286+
if (!hasConsoleAccess(userInfo)) {
287+
return <Navigate to={WEB_HOME_PATH} replace />;
288+
}
289+
290+
const currentPath = location.pathname.replace(/\/$/, "") || WEB_HOME_PATH;
291+
const shouldRedirectToFirstMenu =
292+
currentPath === "/console" ||
293+
(menuPaths.length > 0 ? !menuPaths.includes(currentPath) : currentPath !== firstConsolePath);
294+
295+
if (shouldRedirectToFirstMenu && currentPath !== firstConsolePath) {
296+
return <Navigate to={firstConsolePath} replace />;
297+
}
270298
return (
271299
<SidebarProvider storageKey="layout-console-sidebar" className="bd-console-layout h-dvh">
272300
<AppSidebar />

packages/client/src/pages/login/index.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
1+
import {
2+
getFirstConsoleMenuPath,
3+
hasConsoleAccess,
4+
WEB_HOME_PATH,
5+
} from "@buildingai/services/shared";
16
import { useAuthStore, useConfigStore } from "@buildingai/stores";
27
import SvgIcons from "@buildingai/ui/components/svg-icons";
38
import { Navigate, useSearchParams } from "react-router-dom";
49

510
import { LoginForm } from "./_components/login-form";
611

12+
function isAbsoluteHttpUrl(target: string) {
13+
return /^https?:\/\//i.test(target);
14+
}
15+
16+
function isConsoleTarget(target: string) {
17+
if (!target) return false;
18+
const pathname = isAbsoluteHttpUrl(target) ? new URL(target).pathname : target;
19+
return pathname === "/console" || pathname.startsWith("/console/");
20+
}
21+
722
const LoginPage = () => {
823
const [searchParams] = useSearchParams();
24+
const { userInfo } = useAuthStore((state) => state.auth);
925
const { isLogin } = useAuthStore((state) => state.authActions);
1026
const { websiteConfig } = useConfigStore((state) => state.config);
1127
const redirect = searchParams.get("redirect") ?? "";
1228

1329
if (isLogin()) {
14-
const target = redirect || "/console/dashboard";
15-
if (target.startsWith("http")) {
30+
if (!userInfo) return null;
31+
32+
if (!hasConsoleAccess(userInfo)) {
33+
const target = redirect && !isConsoleTarget(redirect) ? redirect : WEB_HOME_PATH;
34+
return <Navigate to={target} replace />;
35+
}
36+
37+
const target = redirect || getFirstConsoleMenuPath(userInfo.menus ?? []);
38+
if (isAbsoluteHttpUrl(target)) {
1639
const url = new URL(target);
1740
if (url.port && url.pathname.includes("/extension/")) {
1841
const token = useAuthStore.getState().auth.token;

public/web/assets/_layouts-53VJ_WTQ.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)