From 0d4611052e75cbe9b2dc9309b60435178dcab663 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 13 Jun 2023 00:39:29 +0800 Subject: [PATCH 1/3] feat: white url list for openai security --- app/api/openai/[...path]/route.ts | 18 ++++++++++++++++++ app/client/platforms/openai.ts | 13 +++++-------- app/constant.ts | 6 ++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/api/openai/[...path]/route.ts b/app/api/openai/[...path]/route.ts index 981749e7e..04f3b6da8 100644 --- a/app/api/openai/[...path]/route.ts +++ b/app/api/openai/[...path]/route.ts @@ -1,14 +1,32 @@ +import { OpenaiPath } from "@/app/constant"; import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "../../auth"; import { requestOpenai } from "../../common"; +const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); + async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { console.log("[OpenAI Route] params ", params); + const subpath = params.path.join("/"); + + if (!ALLOWD_PATH.has(subpath)) { + console.log("[OpenAI Route] forbidden path ", subpath); + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + subpath, + }, + { + status: 403, + }, + ); + } + const authResult = auth(req); if (authResult.error) { return NextResponse.json(authResult, { diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 84c4a2df0..fd4c33655 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -1,4 +1,4 @@ -import { REQUEST_TIMEOUT_MS } from "@/app/constant"; +import { OpenaiPath, REQUEST_TIMEOUT_MS } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; @@ -10,10 +10,6 @@ import { import { prettyObject } from "@/app/utils/format"; export class ChatGPTApi implements LLMApi { - public ChatPath = "v1/chat/completions"; - public UsagePath = "dashboard/billing/usage"; - public SubsPath = "dashboard/billing/subscription"; - path(path: string): string { let openaiUrl = useAccessStore.getState().openaiUrl; if (openaiUrl.endsWith("/")) { @@ -55,7 +51,7 @@ export class ChatGPTApi implements LLMApi { options.onController?.(controller); try { - const chatPath = this.path(this.ChatPath); + const chatPath = this.path(OpenaiPath.ChatPath); const chatPayload = { method: "POST", body: JSON.stringify(requestPayload), @@ -177,14 +173,14 @@ export class ChatGPTApi implements LLMApi { const [used, subs] = await Promise.all([ fetch( this.path( - `${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`, + `${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`, ), { method: "GET", headers: getHeaders(), }, ), - fetch(this.path(this.SubsPath), { + fetch(this.path(OpenaiPath.SubsPath), { method: "GET", headers: getHeaders(), }), @@ -228,3 +224,4 @@ export class ChatGPTApi implements LLMApi { } as LLMUsage; } } +export { OpenaiPath }; diff --git a/app/constant.ts b/app/constant.ts index b640919e5..0f8052753 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -45,3 +45,9 @@ export const LAST_INPUT_KEY = "last-input"; export const REQUEST_TIMEOUT_MS = 60000; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; + +export const OpenaiPath = { + ChatPath: "v1/chat/completions", + UsagePath: "dashboard/billing/usage", + SubsPath: "dashboard/billing/subscription", +}; From 88df4a2223beb86d8c9d4fe0285732152f0b372a Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 13 Jun 2023 02:27:39 +0800 Subject: [PATCH 2/3] feat: close #1762 add hover text for chat input actions --- app/components/chat.module.scss | 25 +++++++ app/components/chat.tsx | 125 +++++++++++++++++++++----------- app/locales/cn.ts | 13 ++++ app/locales/en.ts | 13 ++++ 4 files changed, 135 insertions(+), 41 deletions(-) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 0e2741e70..3a8d3cda4 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -17,10 +17,35 @@ transition: all ease 0.3s; margin-bottom: 10px; align-items: center; + height: 16px; &:not(:last-child) { margin-right: 5px; } + + .text { + white-space: nowrap; + padding-left: 5px; + opacity: 0; + transform: translateX(-5px); + transition: all ease 0.3s; + transition-delay: 0.1s; + pointer-events: none; + } + + &:hover { + .text { + opacity: 1; + transform: translate(0); + } + } + + .text, + .icon { + display: flex; + align-items: center; + justify-content: center; + } } } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 70fd462d9..15784861e 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -279,6 +279,52 @@ function ClearContextDivider() { ); } +function ChatAction(props: { + text: string; + icon: JSX.Element; + onClick: () => void; +}) { + const iconRef = useRef(null); + const textRef = useRef(null); + const [hovering, setHovering] = useState(false); + const [width, setWidth] = useState(20); + + const updateWidth = () => { + if (!iconRef.current || !textRef.current) return; + const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width; + const textWidth = getWidth(textRef.current); + const iconWidth = getWidth(iconRef.current); + setWidth(hovering ? textWidth + iconWidth : iconWidth); + }; + + useEffect(() => { + updateWidth(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hovering]); + + return ( +
setHovering(true)} + onMouseLeave={() => setHovering(false)} + style={{ + width, + }} + onClick={() => { + props.onClick(); + setTimeout(updateWidth, 1); + }} + > +
+ {props.icon} +
+
+ {props.text} +
+
+ ); +} + function useScrollToBottom() { // for auto-scroll const scrollRef = useRef(null); @@ -330,61 +376,60 @@ export function ChatActions(props: { return (
{couldStop && ( -
- -
+ text={Locale.Chat.InputActions.Stop} + icon={} + /> )} {!props.hitBottom && ( -
- -
+ text={Locale.Chat.InputActions.ToBottom} + icon={} + /> )} {props.hitBottom && ( -
- -
+ text={Locale.Chat.InputActions.Settings} + icon={} + /> )} -
- {theme === Theme.Auto ? ( - - ) : theme === Theme.Light ? ( - - ) : theme === Theme.Dark ? ( - - ) : null} -
+ text={Locale.Chat.InputActions.Theme[theme]} + icon={ + <> + {theme === Theme.Auto ? ( + + ) : theme === Theme.Light ? ( + + ) : theme === Theme.Dark ? ( + + ) : null} + + } + /> -
- -
+ text={Locale.Chat.InputActions.Prompt} + icon={} + /> -
{ navigate(Path.Masks); }} - > - -
+ text={Locale.Chat.InputActions.Masks} + icon={} + /> -
} onClick={() => { chatStore.updateCurrentSession((session) => { if (session.clearContextIndex === session.messages.length) { @@ -395,9 +440,7 @@ export function ChatActions(props: { } }); }} - > - -
+ />
); } diff --git a/app/locales/cn.ts b/app/locales/cn.ts index c3cd8f457..d33ba101b 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -27,6 +27,19 @@ const cn = { Retry: "重试", Delete: "删除", }, + InputActions: { + Stop: "停止响应", + ToBottom: "滚到最新", + Theme: { + auto: "自动主题", + light: "亮色模式", + dark: "深色模式", + }, + Prompt: "快捷指令", + Masks: "所有面具", + Clear: "清除聊天", + Settings: "对话设置", + }, Rename: "重命名对话", Typing: "正在输入…", Input: (submitKey: string) => { diff --git a/app/locales/en.ts b/app/locales/en.ts index 068b2e583..9c8bc2a79 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -28,6 +28,19 @@ const en: RequiredLocaleType = { Retry: "Retry", Delete: "Delete", }, + InputActions: { + Stop: "Stop", + ToBottom: "To Latest", + Theme: { + auto: "Auto", + light: "Light Theme", + dark: "Dark Theme", + }, + Prompt: "Prompts", + Masks: "Masks", + Clear: "Clear Context", + Settings: "Settings", + }, Rename: "Rename Chat", Typing: "Typing…", Input: (submitKey: string) => { From a7e9356c16469ff75d2a162e2923edaac81c65e2 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 13 Jun 2023 03:04:09 +0800 Subject: [PATCH 3/3] fixup: #1762 optimize style on mobile screen --- app/components/chat.module.scss | 3 +++ app/components/chat.tsx | 31 ++++++++++++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 3a8d3cda4..644c917a1 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -18,6 +18,7 @@ margin-bottom: 10px; align-items: center; height: 16px; + width: var(--icon-width); &:not(:last-child) { margin-right: 5px; @@ -34,6 +35,8 @@ } &:hover { + width: var(--full-width); + .text { opacity: 1; transform: translate(0); diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 15784861e..ffd2b7d29 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,5 +1,5 @@ import { useDebouncedCallback } from "use-debounce"; -import { useState, useRef, useEffect, useLayoutEffect } from "react"; +import React, { useState, useRef, useEffect, useLayoutEffect } from "react"; import SendWhiteIcon from "../icons/send-white.svg"; import BrainIcon from "../icons/brain.svg"; @@ -286,34 +286,39 @@ function ChatAction(props: { }) { const iconRef = useRef(null); const textRef = useRef(null); - const [hovering, setHovering] = useState(false); - const [width, setWidth] = useState(20); + const [width, setWidth] = useState({ + full: 20, + icon: 20, + }); - const updateWidth = () => { + function updateWidth() { if (!iconRef.current || !textRef.current) return; const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width; const textWidth = getWidth(textRef.current); const iconWidth = getWidth(iconRef.current); - setWidth(hovering ? textWidth + iconWidth : iconWidth); - }; + setWidth({ + full: textWidth + iconWidth, + icon: iconWidth, + }); + } useEffect(() => { updateWidth(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hovering]); + }, []); return (
setHovering(true)} - onMouseLeave={() => setHovering(false)} - style={{ - width, - }} onClick={() => { props.onClick(); setTimeout(updateWidth, 1); }} + style={ + { + "--icon-width": `${width.icon}px`, + "--full-width": `${width.full}px`, + } as React.CSSProperties + } >
{props.icon}