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')) + } +}