diff --git a/ui/api-templates/http-client.ejs b/ui/api-templates/http-client.ejs index b7046a9..19b8903 100644 --- a/ui/api-templates/http-client.ejs +++ b/ui/api-templates/http-client.ejs @@ -37,6 +37,10 @@ export enum ContentType { Text = "text/plain", } + +const whitePathnameList = ['/user/login', '/login']; +const whiteApiList = ['/api/v1/user/profile', '/api/v1/admin/profile']; + const redirectToLogin = () => { const redirectAfterLogin = encodeURIComponent(location.href); const search = `redirect=${redirectAfterLogin}`; @@ -72,6 +76,9 @@ export class HttpClient { }, (err) => { if (err?.response?.status === 401) { + if (whitePathnameList.includes(location.pathname)) { + return; + } Message.error('尚未登录') redirectToLogin(); return diff --git a/ui/src/api/Admin.ts b/ui/src/api/Admin.ts index 4eaed3c..fbb2765 100644 --- a/ui/src/api/Admin.ts +++ b/ui/src/api/Admin.ts @@ -175,6 +175,51 @@ export const getAdminLoginHistory = ( ...params, }); +/** + * @description 管理员登出 + * + * @tags Admin + * @name PostAdminLogout + * @summary 管理员登出 + * @request POST:/api/v1/admin/logout + * @response `200` `WebResp` OK + */ + +export const postAdminLogout = (params: RequestParams = {}) => + request({ + path: `/api/v1/admin/logout`, + method: "POST", + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 管理员信息 + * + * @tags Admin + * @name GetAdminProfile + * @summary 管理员信息 + * @request GET:/api/v1/admin/profile + * @response `200` `(WebResp & { + data?: DomainAdminUser, + +})` OK + */ + +export const getAdminProfile = (params: RequestParams = {}) => + request< + WebResp & { + data?: DomainAdminUser; + } + >({ + path: `/api/v1/admin/profile`, + method: "GET", + type: ContentType.Json, + format: "json", + ...params, + }); + /** * @description 获取系统设置 * diff --git a/ui/src/api/OpenAiv1.ts b/ui/src/api/OpenAiv1.ts index 52ad1e6..f3409af 100644 --- a/ui/src/api/OpenAiv1.ts +++ b/ui/src/api/OpenAiv1.ts @@ -125,7 +125,7 @@ export const getModelList = (params: RequestParams = {}) => }); /** - * @description 报告 + * @description 报告,支持多种操作:accept(接受补全)、suggest(建议)、reject(拒绝补全并记录用户输入)、file_written(文件写入) * * @tags OpenAIV1 * @name PostReport diff --git a/ui/src/api/User.ts b/ui/src/api/User.ts index 7ebbd1a..f099503 100644 --- a/ui/src/api/User.ts +++ b/ui/src/api/User.ts @@ -189,6 +189,25 @@ export const getLoginHistory = ( ...params, }); +/** + * @description 用户登出 + * + * @tags User + * @name PostLogout + * @summary 用户登出 + * @request POST:/api/v1/user/logout + * @response `200` `WebResp` OK + */ + +export const postLogout = (params: RequestParams = {}) => + request({ + path: `/api/v1/user/logout`, + method: "POST", + type: ContentType.Json, + format: "json", + ...params, + }); + /** * @description 用户 OAuth 回调 * diff --git a/ui/src/api/httpClient.ts b/ui/src/api/httpClient.ts index 8042895..750955e 100644 --- a/ui/src/api/httpClient.ts +++ b/ui/src/api/httpClient.ts @@ -58,6 +58,9 @@ export enum ContentType { Text = "text/plain", } +const whitePathnameList = ["/user/login", "/login"]; +const whiteApiList = ["/api/v1/user/profile", "/api/v1/admin/profile"]; + const redirectToLogin = () => { const redirectAfterLogin = encodeURIComponent(location.href); const search = `redirect=${redirectAfterLogin}`; @@ -101,6 +104,9 @@ export class HttpClient { }, (err) => { if (err?.response?.status === 401) { + if (whitePathnameList.includes(location.pathname)) { + return; + } Message.error("尚未登录"); redirectToLogin(); return; diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 20df103..5d287a7 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -26,6 +26,7 @@ export enum ConstsReportAction { ReportActionAccept = "accept", ReportActionSuggest = "suggest", ReportActionFileWritten = "file_written", + ReportActionReject = "reject", } export enum ConstsModelType { @@ -367,8 +368,11 @@ export interface DomainLoginReq { password?: string; /** 会话Id插件登录时必填 */ session_id?: string; - /** 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin */ - source?: ConstsLoginSource; + /** + * 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin + * @default "plugin" + */ + source: ConstsLoginSource; /** 用户名 */ username?: string; } @@ -511,10 +515,16 @@ export interface DomainReportReq { action?: ConstsReportAction; /** 内容 */ content?: string; + /** 光标位置(用于reject action) */ + cursor_position?: number; /** task_id or resp_id */ id?: string; + /** 当前文件的原文(用于reject action) */ + source_code?: string; /** 工具 */ tool?: string; + /** 用户输入的新文本(用于reject action) */ + user_input?: string; } export interface DomainSetting { @@ -1070,6 +1080,9 @@ export interface GetUserOauthSignupOrInParams { redirect_url?: string; /** 会话ID */ session_id?: string; - /** 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin */ - source?: "plugin" | "browser"; + /** + * 登录来源 plugin: 插件 browser: 浏览器; 默认为 plugin + * @default "plugin" + */ + source: "plugin" | "browser"; } diff --git a/ui/src/components/header/index.tsx b/ui/src/components/header/index.tsx index 38f7177..678f3ae 100644 --- a/ui/src/components/header/index.tsx +++ b/ui/src/components/header/index.tsx @@ -1,14 +1,24 @@ -import { Button, IconButton, Stack } from '@mui/material'; -import { Icon, message } from '@c-x/ui'; +import { Button, IconButton, Stack, Tooltip } from '@mui/material'; +import { message } from '@c-x/ui'; +import { postLogout } from '@/api/User'; +import { postAdminLogout } from '@/api/Admin'; import { useNavigate, useLocation } from 'react-router-dom'; import LogoutIcon from '@mui/icons-material/Logout'; import DownloadIcon from '@mui/icons-material/Download'; -import { Box } from '@mui/material'; import Bread from './Bread'; const Header = () => { const navigate = useNavigate(); const { pathname } = useLocation(); + const onLogout = async () => { + if (pathname.startsWith('/user')) { + await postLogout(); + } else { + await postAdminLogout(); + } + message.success('退出登录成功'); + navigate(pathname.startsWith('/user') ? '/user/login' : '/login'); + }; return ( { > 下载客户端 - + { - message.success('退出登录成功'); - navigate(pathname.startsWith('/user/') ? '/user/login' : '/login', { - replace: true, - }); - }} - > - - + '&:hover': { + color: 'primary.main', + }, + }} + onClick={onLogout} + > + + + ); diff --git a/ui/src/context/index.tsx b/ui/src/context/index.tsx new file mode 100644 index 0000000..c650625 --- /dev/null +++ b/ui/src/context/index.tsx @@ -0,0 +1,25 @@ +import { createContext } from 'react'; +import { DomainUser, DomainAdminUser } from '@/api/types'; + +export const AuthContext = createContext< + [ + DomainUser | DomainAdminUser | null, + { + loading: boolean; + setUser: (user: DomainUser | DomainAdminUser) => void; + refreshUser: () => void; + } + ] +>([ + null, + { + setUser: () => {}, + loading: true, + refreshUser: () => {}, + }, +]); + +export const CommonContext = createContext<{ + contactModalOpen: boolean; + setContactModalOpen: React.Dispatch>; +}>({ contactModalOpen: false, setContactModalOpen: () => {} }); diff --git a/ui/src/hooks/context.ts b/ui/src/hooks/context.ts new file mode 100644 index 0000000..c0903bd --- /dev/null +++ b/ui/src/hooks/context.ts @@ -0,0 +1,10 @@ +import { use } from 'react'; +import { AuthContext, CommonContext } from '@/context'; + +export const useAuthContext = () => { + return use(AuthContext); +}; + +export const useCommonContext = () => { + return use(CommonContext); +}; diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 3cb1003..06c0be9 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -1,4 +1,5 @@ import ReactDOM from 'react-dom/client'; +import { useState, useEffect } from 'react'; import 'dayjs/locale/zh-cn'; import { RouterProvider } from 'react-router-dom'; import '@/assets/fonts/font.css'; @@ -6,22 +7,86 @@ import '@/assets/fonts/iconfont'; import './index.css'; import '@/assets/styles/markdown.css'; import { ThemeProvider } from '@c-x/ui'; +import { getUserProfile } from '@/api/UserManage'; +import { getAdminProfile } from '@/api/Admin'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import relativeTime from 'dayjs/plugin/relativeTime'; - +import { AuthContext } from './context'; +import { DomainUser, DomainAdminUser } from './api/types'; import { lightTheme } from './theme'; import router from './router'; +import { getRedirectUrl } from './utils'; +import { Loading } from '@c-x/ui'; dayjs.locale('zh-cn'); dayjs.extend(duration); dayjs.extend(relativeTime); +const App = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const onGotoRedirect = (source: 'user' | 'admin') => { + const redirectUrl = getRedirectUrl(source); + window.location.href = redirectUrl.href; + }; + + const getUser = () => { + setLoading(true); + if (location.pathname.startsWith('/user')) { + return getUserProfile() + .then((res) => { + setUser(res); + if (location.pathname.startsWith('/user/login')) { + onGotoRedirect('user'); + } + }) + .finally(() => { + setLoading(false); + }); + } else { + return getAdminProfile() + .then((res) => { + console.log(res); + setUser(res); + if (location.pathname.startsWith('/login')) { + onGotoRedirect('admin'); + } + }) + .finally(() => { + setLoading(false); + }); + } + }; + + useEffect(() => { + getUser(); + }, []); + + if (loading) { + return null; + } + + return ( + + + + + + ); +}; + const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); -root.render( - - - -); +root.render(); diff --git a/ui/src/pages/auth/index.tsx b/ui/src/pages/auth/index.tsx index ab1ab7b..63088b2 100644 --- a/ui/src/pages/auth/index.tsx +++ b/ui/src/pages/auth/index.tsx @@ -147,6 +147,7 @@ const AuthPage = () => { const loginResult = await postLogin({ ...data, session_id: sessionId, + source: ConstsLoginSource.LoginSourcePlugin, }); if (!loginResult.redirect_url) { diff --git a/ui/src/pages/invite/index.tsx b/ui/src/pages/invite/index.tsx index 9dc76b7..1428fc6 100644 --- a/ui/src/pages/invite/index.tsx +++ b/ui/src/pages/invite/index.tsx @@ -26,7 +26,7 @@ import { useRequest } from 'ahooks'; import { postRegister, getUserOauthSignupOrIn } from '@/api/User'; import { getGetSetting } from '@/api/Admin'; import { Icon } from '@c-x/ui'; -import { DomainSetting } from '@/api/types'; +import { ConstsLoginSource, DomainSetting } from '@/api/types'; import DownloadIcon from '@mui/icons-material/Download'; import MenuBookIcon from '@mui/icons-material/MenuBook'; @@ -140,6 +140,7 @@ const Invite = () => { platform, redirect_url: `${window.location.origin}/invite/${id}/2`, inviate_code: id, + source: 'plugin', }).then((res) => { if (res.url) { window.location.href = res.url; diff --git a/ui/src/pages/login/index.tsx b/ui/src/pages/login/index.tsx index 6a2263c..cce67dd 100644 --- a/ui/src/pages/login/index.tsx +++ b/ui/src/pages/login/index.tsx @@ -16,6 +16,7 @@ import { postAdminLogin } from '@/api/Admin'; import { useForm, Controller } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { styled } from '@mui/material/styles'; +import { ConstsLoginSource } from '@/api/types'; import { Icon } from '@c-x/ui'; import Logo from '@/assets/images/logo.png'; import { getRedirectUrl } from '@/utils'; @@ -71,6 +72,7 @@ const LoginPage = () => { return postAdminLogin({ username: data.username, password: data.password, + source: ConstsLoginSource.LoginSourceBrowser, }); };