diff --git a/backend/apps/system/crud/user.py b/backend/apps/system/crud/user.py
index 4c96d124..ae6ed887 100644
--- a/backend/apps/system/crud/user.py
+++ b/backend/apps/system/crud/user.py
@@ -3,7 +3,7 @@ from typing import Optional
from sqlmodel import Session, func, select, delete as sqlmodel_delete
from apps.system.models.system_model import UserWsModel, WorkspaceModel
from apps.system.schemas.auth import CacheName, CacheNamespace
-from apps.system.schemas.system_schema import BaseUserDTO, UserInfoDTO, UserWs
+from apps.system.schemas.system_schema import EMAIL_REGEX, PWD_REGEX, BaseUserDTO, UserInfoDTO, UserWs
from common.core.deps import SessionDep
from common.core.sqlbot_cache import cache, clear_cache
from common.utils.locale import I18n
@@ -78,18 +78,7 @@ def check_email_exists(*, session: Session, email: str) -> bool:
return session.exec(select(func.count()).select_from(UserModel).where(UserModel.email == email)).one() > 0
-# 预编译正则表达式,提高效率
-EMAIL_REGEX = re.compile(
- r"^[a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*@"
- r"([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+"
- r"[a-zA-Z]{2,}$"
-)
-PWD_REGEX = re.compile(
- r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)"
- r"(?=.*[~!@#$%^&*()_+\-={}|:\"<>?`\[\];',./])"
- r"[A-Za-z\d~!@#$%^&*()_+\-={}|:\"<>?`\[\];',./]{8,20}$"
-)
def check_email_format(email: str) -> bool:
return bool(EMAIL_REGEX.fullmatch(email))
diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py
index 23bdd512..8c3a1f59 100644
--- a/backend/apps/system/middleware/auth.py
+++ b/backend/apps/system/middleware/auth.py
@@ -62,7 +62,13 @@ class TokenMiddleware(BaseHTTPMiddleware):
try:
with Session(engine) as session:
session_user = await get_user_info(session = session, user_id = token_data.id)
+ if not session_user:
+ raise Exception(f"User not found with id: {token_data.id}")
session_user = UserInfoDTO.model_validate(session_user)
+ if session_user.status != 1:
+ raise Exception(f"User is not active!")
+ if not session_user.oid or session_user.oid == 0:
+ raise Exception(f"User default space is not set!")
""" if token_data.oid != session_user.oid:
raise HTTPException(
status_code=401,
diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py
index cb0c757f..ecec9ed2 100644
--- a/backend/apps/system/schemas/system_schema.py
+++ b/backend/apps/system/schemas/system_schema.py
@@ -1,8 +1,19 @@
from typing import Optional
-from pydantic import BaseModel
+from pydantic import BaseModel, Field, field_validator
from common.core.schemas import BaseCreatorDTO
+import re
+EMAIL_REGEX = re.compile(
+ r"^[a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*@"
+ r"([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+"
+ r"[a-zA-Z]{2,}$"
+)
+PWD_REGEX = re.compile(
+ r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)"
+ r"(?=.*[~!@#$%^&*()_+\-={}|:\"<>?`\[\];',./])"
+ r"[A-Za-z\d~!@#$%^&*()_+\-={}|:\"<>?`\[\];',./]{8,20}$"
+)
class model_status(BaseModel):
status: bool
ids: list[int]
@@ -13,11 +24,11 @@ class UserLanguage(BaseModel):
class BaseUser(BaseModel):
- account: str
+ account: str = Field(min_length=1, max_length=100, description="用户账号")
oid: int
-
+
class BaseUserDTO(BaseUser, BaseCreatorDTO):
- language: str
+ language: str = Field(pattern=r"^(zh-CN|en)$", default="zh-CN", description="用户语言")
password: str
status: int = 1
def to_dict(self):
@@ -26,12 +37,23 @@ class BaseUserDTO(BaseUser, BaseCreatorDTO):
"account": self.account,
"oid": self.oid
}
+ @field_validator("language")
+ def validate_language(cls, lang: str) -> str:
+ if not re.fullmatch(r"^(zh-CN|en)$", lang):
+ raise ValueError("Language must be 'zh-CN' or 'en'")
+ return lang
class UserCreator(BaseUser):
- name: str
- email: str
+ name: str = Field(min_length=1, max_length=100, description="用户名")
+ email: str = Field(min_length=1, max_length=100, description="用户邮箱")
status: int = 1
oid_list: Optional[list[int]] = None
+
+ """ @field_validator("email")
+ def validate_email(cls, lang: str) -> str:
+ if not re.fullmatch(EMAIL_REGEX, lang):
+ raise ValueError("Email format is invalid!")
+ return lang """
class UserEditor(UserCreator, BaseCreatorDTO):
pass
diff --git a/frontend/src/assets/svg/icon_copy_outlined.svg b/frontend/src/assets/svg/icon_copy_outlined.svg
new file mode 100644
index 00000000..acdd1f66
--- /dev/null
+++ b/frontend/src/assets/svg/icon_copy_outlined.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json
index c869eb99..d16e667e 100644
--- a/frontend/src/i18n/en.json
+++ b/frontend/src/i18n/en.json
@@ -118,7 +118,8 @@
"earlier": "Earlier",
"no_time": "No Time",
"rename_conversation_title": "Rename conversation title",
- "conversation_title": "Conversation title"
+ "conversation_title": "Conversation title",
+ "copied": "Copied"
},
"ds": {
"title": "Data Source",
@@ -513,4 +514,4 @@
"back_community": "Restore to Community Edition",
"confirm_tips": "Are you sure to restore to Community Edition?"
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json
index 5d3d7861..4234340a 100644
--- a/frontend/src/i18n/zh-CN.json
+++ b/frontend/src/i18n/zh-CN.json
@@ -121,7 +121,8 @@
"earlier": "更早以前",
"no_time": "没有时间",
"rename_conversation_title": "重命名对话标题",
- "conversation_title": "对话标题"
+ "conversation_title": "对话标题",
+ "copied": "已复制"
},
"ds": {
"title": "数据源",
@@ -525,4 +526,4 @@
"back_community": "还原至社区版",
"confirm_tips": "确定还原至社区版?"
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts
index 9aee595d..2851979c 100644
--- a/frontend/src/utils/request.ts
+++ b/frontend/src/utils/request.ts
@@ -168,9 +168,21 @@ class HttpService {
errorMessage = 'Invalid request parameters'
break
case 401:
- errorMessage = 'Unauthorized, please login again'
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ errorMessage = error.response?.data?.data?.msg || 'Unauthorized, please login again'
// Redirect to login page if needed
- break
+ ElMessage({
+ message: errorMessage,
+ type: 'error',
+ showClose: true,
+ })
+ setTimeout(() => {
+ wsCache.delete('user.token')
+ window.location.reload()
+ }, 1000)
+ return
+ // break
case 403:
errorMessage = 'Access denied'
break
diff --git a/frontend/src/views/chat/chat-block/ChartBlock.vue b/frontend/src/views/chat/chat-block/ChartBlock.vue
index a6d8546e..52e59969 100644
--- a/frontend/src/views/chat/chat-block/ChartBlock.vue
+++ b/frontend/src/views/chat/chat-block/ChartBlock.vue
@@ -15,6 +15,7 @@ import icon_export_outlined from '@/assets/svg/icon_export_outlined.svg'
import icon_into_item_outlined from '@/assets/svg/icon_into-item_outlined.svg'
import icon_window_max_outlined from '@/assets/svg/icon_window-max_outlined.svg'
import icon_window_mini_outlined from '@/assets/svg/icon_window-mini_outlined.svg'
+import icon_copy_outlined from '@/assets/svg/icon_copy_outlined.svg'
import { useI18n } from 'vue-i18n'
import SQLComponent from '@/views/chat/component/SQLComponent.vue'
@@ -189,6 +190,13 @@ function showSql() {
function addToDashboard() {
console.log('todo')
}
+
+function copy() {
+ if (props.message?.record?.sql) {
+ navigator.clipboard.writeText(props.message.record.sql)
+ ElMessage.info(t('qa.copied'))
+ }
+}
@@ -318,12 +326,17 @@ function addToDashboard() {
direction="rtl"
body-class="chart-sql-drawer-body"
>
-
@@ -490,4 +503,18 @@ function addToDashboard() {
margin-top: 16px;
}
}
+
+.sql-block {
+ position: relative;
+
+ .input-icon {
+ min-width: unset;
+ position: absolute;
+ top: 12px;
+ right: 12px;
+
+ border-color: #dee0e3;
+ box-shadow: 0px 4px 8px 0px #1f23291a;
+ }
+}
diff --git a/frontend/src/views/chat/component/charts/Bar.ts b/frontend/src/views/chat/component/charts/Bar.ts
index 031b9605..f54fece9 100644
--- a/frontend/src/views/chat/component/charts/Bar.ts
+++ b/frontend/src/views/chat/component/charts/Bar.ts
@@ -106,7 +106,13 @@ export class Bar extends BaseG2Chart {
},
labels: [
{
- text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
+ text: (data: any) => {
+ const value = data[y[0].value]
+ if (value === undefined || value === null) {
+ return ''
+ }
+ return `${value}${_data.isPercent ? '%' : ''}`
+ },
transform: [
{ type: 'contrastReverse' },
{ type: 'exceedAdjust' },
diff --git a/frontend/src/views/chat/component/charts/Column.ts b/frontend/src/views/chat/component/charts/Column.ts
index 4a4d8209..ad437abb 100644
--- a/frontend/src/views/chat/component/charts/Column.ts
+++ b/frontend/src/views/chat/component/charts/Column.ts
@@ -95,7 +95,13 @@ export class Column extends BaseG2Chart {
},
labels: [
{
- text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
+ text: (data: any) => {
+ const value = data[y[0].value]
+ if (value === undefined || value === null) {
+ return ''
+ }
+ return `${value}${_data.isPercent ? '%' : ''}`
+ },
position: 'top',
transform: [
{ type: 'contrastReverse' },
diff --git a/frontend/src/views/chat/component/charts/Line.ts b/frontend/src/views/chat/component/charts/Line.ts
index d28f29b9..43ed6c0a 100644
--- a/frontend/src/views/chat/component/charts/Line.ts
+++ b/frontend/src/views/chat/component/charts/Line.ts
@@ -61,7 +61,13 @@ export class Line extends BaseG2Chart {
},
labels: [
{
- text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
+ text: (data: any) => {
+ const value = data[y[0].value]
+ if (value === undefined || value === null) {
+ return ''
+ }
+ return `${value}${_data.isPercent ? '%' : ''}`
+ },
style: {
dx: -10,
dy: -12,