diff --git a/.env b/.env new file mode 100644 index 0000000..a2e552a --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +# API基础URL +NEXT_PUBLIC_API_BASE_URL=/ + +# 开发服务器主机地址 +NEXT_PUBLIC_DEV_SERVER_HOST=https://p29.ngsk.tech:7001 \ No newline at end of file diff --git a/next.config.js b/next.config.js index 5d009b5..d3a199a 100644 --- a/next.config.js +++ b/next.config.js @@ -9,21 +9,23 @@ const withTM = require('next-transpile-modules')([ '@xyflow/system' ]); -const setting = require("./src/settings.json"); +const setting = require('./src/settings.json'); + +const target = process.env.NEXT_PUBLIC_DEV_SERVER_HOST; module.exports = withLess( withTM({ lessLoaderOptions: { lessOptions: { modifyVars: { - 'arcoblue-6': setting.themeColor, - }, - }, + 'arcoblue-6': setting.themeColor + } + } }, webpack: (config) => { config.module.rules.push({ test: /\.svg$/, - use: ['@svgr/webpack'], + use: ['@svgr/webpack'] }); config.resolve.alias['@/assets'] = path.resolve( @@ -42,10 +44,23 @@ module.exports = withLess( { source: '/', destination: '/dashboard/workplace', - permanent: true, - }, + permanent: true + } + ]; + }, + // 添加代理配置 + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${target}/api/:path*` // api代理 + } + // { + // source: '/ws/:path*', + // destination: `${target.replace('http', 'ws')}/WS/:path*` // WebSocket 代理 + // } ]; }, - pageExtensions: ['tsx'], + pageExtensions: ['tsx'] }) ); \ No newline at end of file diff --git a/package.json b/package.json index 3f43bfc..c850e6b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", "install": "^0.13.0", + "js-md5": "^0.8.3", "lodash": "^4.17.21", "mockjs": "^1.1.0", "next": "12.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c52b612..4581fdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: install: specifier: ^0.13.0 version: 0.13.0 + js-md5: + specifier: ^0.8.3 + version: 0.8.3 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -3018,6 +3021,9 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + js-md5@0.8.3: + resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -8480,6 +8486,8 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + js-md5@0.8.3: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..f050a7f --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,57 @@ +import axios from 'axios'; +import type { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { Message } from '@arco-design/web-react'; +import { getToken } from '@/utils/auth'; +import useUser from '@/hooks/user'; + +export interface HttpResponse { + status: number; + msg: string; + code: number; + data: T; +} + +if (process.env.NEXT_API_BASE_URL) { + axios.defaults.baseURL = process.env.NEXT_API_BASE_URL; +} + +axios.interceptors.request.use( + (config: AxiosRequestConfig) => { + const token = getToken(); + if (token) { + if (!config.headers) { + config.headers = {}; + } + // config.headers.Authorization = `Bearer ${token}` + } + return config; + }, + error => { + return Promise.reject(error); + } +); +// add response interceptors +axios.interceptors.response.use( + (response: AxiosResponse) => { + const res = response.data; + if (res.code && res.code !== 200) { + Message.error({ + content: res.msg || 'Error', + duration: 5 * 1000 + }); + if ([-401].includes(res.code)) { + const { logout } = useUser(); + logout(); + } + return Promise.reject(new Error(res.msg || 'Error')); + } + return res; + }, + error => { + Message.error({ + content: error.msg || '系统异常', + duration: 5 * 1000 + }); + return Promise.reject(error); + } +); \ No newline at end of file diff --git a/src/api/interface/index.ts b/src/api/interface/index.ts new file mode 100644 index 0000000..e3455d1 --- /dev/null +++ b/src/api/interface/index.ts @@ -0,0 +1,225 @@ +// application +export interface CronModel { + appId: string; + cron: string; +} + +export interface applicationModel { + id?: string; + name?: string; + description?: string; + identify?: string; + logo?: string; + path?: string; + published?: boolean | number; + sceneId?: string; + createBy?: string; + createTime?: string | number; + updateBy?: string; + updateTime?: string | number; + compNum?: number; + instanceNum?: number; + nodeNum?: number; + cron?: string; + scheduled?: number; + createUser?: string; + instanceId?: string | number; +} + +export interface queryParams { + sceneId?: string; + resId?: string; + name?: string; + currPage?: number; + pageSize?: number; + total?: number; +} + +export interface apiResData { + list: applicationModel[]; +} + +export interface FlowDefinition { + id: string; + lineConfigs: any[]; + nodeConfigs: any[]; + name?: string; +} + +export interface appFlowModel { + id?: string; + name?: string; + compIds?: string[]; + description?: string; + nodeNum?: number; + main?: FlowDefinition; + subs?: any; +} + +export interface publishType { + appId?: string; + flowId?: string; + isMain?: number; + name?: string; + published?: number | string; + tag?: string; + description?: string; +} + +export interface publishApi { + appId: string; + needAuthor: number; + time?: number; +} + +export interface paramsT { + code: number; + msg: string; + data?: any; + traceId: string | null; +} + +export interface flowType { + id: string; + type?: string; +} + +// aichat +export interface RecommendResponse { + code: number; + compent_list: string[]; +} + +// dashboard +export interface ContentDataRecord { + x: string; + y: number; +} + +export interface PopularRecord { + key: number; + clickNumber: string; + title: string; + increases: number; +} + +// scene +export interface monitorCfg { + label: string; + url: string; +} + +export interface sceneModel { + id?: string; + name?: string; + description?: string; + identify?: string; + logo?: string; + published?: boolean | number; + createBy?: string; + createTime?: string; + updateBy?: string; + updateTime?: string; + createUser?: string; + configUrls?: Array; +} + +export interface querySceneParams { + currPage?: number; + name?: string; + pageSize?: number; +} + +export interface sceneApiResData { + list: sceneModel[]; + totalCount: number; +} + +// user +export interface LoginRes { + token: string; + + [key: string]: any; +} + +export interface authParams { + domain: string; + path: string; +} + +export interface getTokenParams extends authParams { + authCode: string; + callbackUrl: string; +} + +export interface getLinkParams { + callbackUrl: string; +} + +export interface UserState { + account: string; + avatar?: string; + realName: string; + userId: string; + username: string; +} + +// message +export interface MessageRecord { + id: number; + type: string; + title: string; + subTitle: string; + avatar?: string; + content: string; + time: string; + status: 0 | 1; + messageType?: number; +} + +export type MessageListType = MessageRecord[]; + +export interface MessageStatus { + ids: number[]; +} + +export interface ChatRecord { + id: number; + username: string; + content: string; + time: string; + isCollect: boolean; +} + +// event +export interface AddEventParams { + name: string, + topic: string, + description: string, + sceneId: string, +} + +export interface DeleteEventParams { + id: string; +} + +export interface DeleteEventForAppParams { + appId: string, + eventIds: string[] +} + +// runtime +export interface StepRunEventParams { + eventId: string; +} + +export interface ExecuteCurrentEventParams { + appId: string, + nodeId: string, + socketId: string +} + +export interface ReconnectRunParams { + instanceId: string, + newSocketId: string +} \ No newline at end of file diff --git a/src/api/sso.ts b/src/api/sso.ts new file mode 100644 index 0000000..6c9760f --- /dev/null +++ b/src/api/sso.ts @@ -0,0 +1,51 @@ +import { md5 } from 'js-md5'; +import { ssoHost } from '@/utils/env'; +import { LoginRes } from '@/api/interface'; + +export function post(url: string, params: Record) { + const temp = document.createElement('form'); + temp.action = url; + temp.method = 'post'; + temp.style.display = 'none'; + Object.keys(params).forEach(key => { + const opt = document.createElement('textarea'); + opt.name = key; + opt.value = params[key]; + temp.appendChild(opt); + }); + document.body.appendChild(temp); + temp.submit(); + return temp; +} + +export const ssoLogin = ({ username, password }: Record) => { + // 模拟表单提交 + const ssoUri = `${ssoHost}/api/blade-auth/oauth/form`; + return post(ssoUri, { + tenant_id: '000000', + username, + password: md5(password), + client_id: 'isdp2', + // 统一认证的地址 + sso_uri: `${ssoHost || window.location.origin}/api/blade-auth`, + // 必须加/ + redirect_uri: `${window.location.origin}/dashboard/workplace` + }); +}; + +export const verify = ({ username, password }): Promise => { + return fetch('/api/user-center/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username, password: md5(password) }) + }).then(async (res) => { + const data = await res.json(); + // 登录成功后设置用户状态 + if (data && res.status === 200) { + localStorage.setItem('userStatus', 'login'); + } + return data; + }); +}; \ No newline at end of file diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..c3e4cde --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,45 @@ +import axios from 'axios'; +import { + getTokenParams, + getLinkParams, + UserState, + LoginRes, + authParams +} from '@/api/interface'; + +// 修复window is not defined错误,确保只在浏览器环境中访问window对象 +const defaultReqData: authParams = typeof window !== 'undefined' ? { + domain: window.location.hostname, + path: '/' +} : { + domain: 'localhost', + path: '/' +}; + +const urlPrefix = '/api/v1/bpms-workbench'; + +export function getToken(params: getTokenParams) { + return axios.get(`${urlPrefix}/sessions/callback`, { + params: { ...params, ...defaultReqData } + }); +} + +export function getAuthLink(params: getLinkParams) { + return axios.get(`${urlPrefix}/sessions/auth/url`, { + params + }); +} + +export function getUserInfo() { + return axios.get(`${urlPrefix}/sessions/info`); +} + +export function logout() { + return axios.get(`${urlPrefix}/sessions/logout`, { + params: defaultReqData + }); +} + +export function getUserToken() { + return axios.get(`${urlPrefix}/sessions/validate`, {}); +} \ No newline at end of file diff --git a/src/hooks/user.ts b/src/hooks/user.ts new file mode 100644 index 0000000..97f42f8 --- /dev/null +++ b/src/hooks/user.ts @@ -0,0 +1,44 @@ +import { Message } from '@arco-design/web-react'; +import { useDispatch } from 'react-redux'; +import axios from 'axios'; + +interface UseUserReturnType { + logout: () => Promise; +} + +export default function useUser(): UseUserReturnType { + const dispatch = useDispatch(); + + const logout = async () => { + try { + // 调用后端登出接口 + await axios.post('/api/user/logout'); + } catch (error) { + // 即使后端登出失败,也要清除本地状态 + console.error('Logout error:', error); + } finally { + // 清除用户信息状态 + dispatch({ + type: 'update-userInfo', + payload: { + userInfo: { permissions: {} }, + userLoading: false + } + }); + + // 清除本地存储的用户状态 + localStorage.removeItem('userStatus'); + + // 显示成功消息 + Message.success('退出成功'); + + // 重定向到登录页面 + const { protocol, host } = window.location; + window.location.href = `${protocol}//${host}/login`; + } + }; + + return { + logout + }; +} \ No newline at end of file diff --git a/src/pages/login/form.tsx b/src/pages/login/form.tsx index 4135218..26d4d42 100644 --- a/src/pages/login/form.tsx +++ b/src/pages/login/form.tsx @@ -9,7 +9,7 @@ import { import { FormInstance } from '@arco-design/web-react/es/Form'; import { IconLock, IconUser } from '@arco-design/web-react/icon'; import React, { useEffect, useRef, useState } from 'react'; -import axios from 'axios'; +import { ssoLogin, verify } from '@/api/sso'; import useStorage from '@/utils/useStorage'; import useLocale from '@/utils/useLocale'; import locale from './locale'; @@ -26,7 +26,7 @@ export default function LoginForm() { const [rememberPassword, setRememberPassword] = useState(!!loginParams); - function afterLoginSuccess(params) { + function afterLoginSuccess(params, token?: string) { // 记住密码 if (rememberPassword) { setLoginParams(JSON.stringify(params)); @@ -37,31 +37,29 @@ export default function LoginForm() { // 记录登录状态 localStorage.setItem('userStatus', 'login'); // 跳转首页 - window.location.href = '/dashboard/workplace'; + // window.location.href = '/dashboard/workplace'; } - function login(params) { + async function loginRequest(params) { setErrorMessage(''); setLoading(true); - axios - .post('/api/user/login', params) - .then((res) => { - const { status, msg } = res.data; - if (status === 'ok') { - afterLoginSuccess(params); - } - else { - setErrorMessage(msg || t['login.form.login.errMsg']); - } - }) - .finally(() => { - setLoading(false); - }); + try { + const res = await verify(params as any); + if (res.code === 200) { + await ssoLogin(params); + afterLoginSuccess(params); + } + else { + setErrorMessage(res.msg); + } + } finally { + setLoading(false); + } } function onSubmitClick() { formRef.current.validate().then((values) => { - login(values); + loginRequest(values); }); } @@ -86,10 +84,10 @@ export default function LoginForm() { className={styles['login-form']} layout="vertical" ref={formRef} - initialValues={{ userName: 'admin', password: 'admin' }} + initialValues={{ username: 'admin', password: 'admin' }} > ); -} +} \ No newline at end of file diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 5dea4c9..8c3db32 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -1,14 +1,64 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; import Footer from '@/components/Footer'; import Logo from '@/assets/logo.svg'; import LoginForm from './form'; import LoginBanner from './banner'; import styles from './style/index.module.less'; +import { getToken } from '@/api/user'; +import { setToken } from '@/utils/auth'; +import { localGet, localRemove, openWindow } from '@/utils/common'; function Login() { + const router = useRouter(); + const [isNeedLogin, setIsNeedLogin] = useState(false); + const handleLogin = async () => { + const url = new URL(window.location.href); + const code = url.searchParams.get('code'); + const { origin } = window.location; + + setIsNeedLogin(code === null); + console.log('code:', code); + + if (code) { + const callbackUrl = `${origin}`; + try { + // 根据code向后端获取token + const res: any = await getToken({ + authCode: code, + callbackUrl + } as any); + + if (res && res.code === 200) { + // 保存Token + setToken(res.data as string); + // 保存用户信息 + // await userStore.info(); // 如果有对应的Redux操作,可以在这里dispatch + + if (localGet('system_name') === 'pc') { + openWindow(`${window.location.protocol}//${window.location.hostname}:${window.location.port}/isdp/`, { + target: '_self' + }); + localRemove('system_name'); + return; + } + else { + // 使用Next.js标准路由跳转方式 + router.push('/dashboard/workplace'); + } + } + } catch (error) { + console.error('Login error:', error); + } + } + }; + useEffect(() => { document.body.setAttribute('arco-theme', 'light'); - }, []); + + // 模拟Vue的onMounted行为,在组件挂载后执行 + handleLogin(); + }, [router]); // 依赖数组包含router,确保router变化时能正确执行 return (
@@ -32,6 +82,7 @@ function Login() {
); } + Login.displayName = 'LoginPage'; -export default Login; +export default Login; \ No newline at end of file diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..07d64ba --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,37 @@ +const TOKEN_KEY = 'token'; +const ISDP_KEY = 'saber-token'; + +const isLogin = () => { + return !!localStorage.getItem(TOKEN_KEY); +}; + +const getToken = () => { + return localStorage.getItem(TOKEN_KEY); +}; + +const setToken = (token: string) => { + localStorage.setItem(TOKEN_KEY, token); + const obj = { + dataType: typeof token, + content: token, + datetime: new Date().getTime() + }; + localStorage.setItem(ISDP_KEY, JSON.stringify(obj)); + document.cookie = `saber-access-token=${token};`; +}; + +const clearToken = () => { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(ISDP_KEY); +}; + +const USER_INFO_KEY = 'userInfo'; +export const setSessionUserInfo = data => { + sessionStorage.setItem(USER_INFO_KEY, JSON.stringify(data)); +}; +export const getSessionUserInfo = () => { + const data = sessionStorage.getItem(USER_INFO_KEY); + return data ? JSON.parse(data) : null; +}; + +export { isLogin, getToken, setToken, clearToken }; diff --git a/src/utils/checkLogin.tsx b/src/utils/checkLogin.tsx index df76c68..60ee7c0 100644 --- a/src/utils/checkLogin.tsx +++ b/src/utils/checkLogin.tsx @@ -1,3 +1,32 @@ +import { isLogin, getToken } from '@/utils/auth'; + export default function checkLogin() { - return localStorage.getItem('userStatus') === 'login'; -} + // 检查基础登录状态 + const userStatus = localStorage.getItem('userStatus'); + if (!userStatus || userStatus !== 'login') { + return false; + } + + // 检查token是否存在 + if (!isLogin()) { + return false; + } + + // 检查token是否过期(如果有过期时间的话) + const token = getToken(); + if (token) { + try { + // 如果token是JWT,可以解析并检查过期时间 + // 这里只是一个示例,实际实现取决于token格式 + // const payload = JSON.parse(atob(token.split('.')[1])); + // if (payload.exp && Date.now() >= payload.exp * 1000) { + // return false; // token已过期 + // } + } catch (e) { + // 如果token不是JWT格式,忽略解析错误 + console.warn('Failed to parse token', e); + } + } + + return true; +} \ No newline at end of file diff --git a/src/utils/common.ts b/src/utils/common.ts index 8430c26..c27f924 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -23,6 +23,47 @@ export function openWindow(url: string, opts?: OpenWindowOptions) { window.open(fullUrl, target); } +/** + * @description 获取localStorage + * @param {String} key Storage名称 + * @returns {String} + */ +export function localGet(key: string) { + const value = window.localStorage.getItem(key); + try { + return JSON.parse(window.localStorage.getItem(key) as string); + } catch (error) { + return value; + } +} + +/** + * @description 存储localStorage + * @param {String} key Storage名称 + * @param {*} value Storage值 + * @returns {void} + */ +export function localSet(key: string, value: any) { + window.localStorage.setItem(key, JSON.stringify(value)); +} + +/** + * @description 清除localStorage + * @param {String} key Storage名称 + * @returns {void} + */ +export function localRemove(key: string) { + window.localStorage.removeItem(key); +} + +/** + * @description 清除所有localStorage + * @returns {void} + */ +export function localClear() { + window.localStorage.clear(); +} + /** * 从URL中获取查询参数 * @param url - 要解析的URL diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 0000000..5e41c95 --- /dev/null +++ b/src/utils/env.ts @@ -0,0 +1,5 @@ +const debug = process.env.NODE_ENV !== 'production'; + +// 使用NEXT_PUBLIC_前缀的环境变量才能在客户端访问 +export const ssoHost = debug ? process.env.NEXT_PUBLIC_DEV_SERVER_HOST : ''; +export default debug; \ No newline at end of file