This commit is contained in:
dataeaseShu
2025-07-25 18:12:29 +08:00
11 changed files with 107 additions and 28 deletions
+1 -12
View File
@@ -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))
+6
View File
@@ -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,
+28 -6
View File
@@ -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
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33333 5.3335V13.3335H10V5.3335H3.33333ZM11.3333 4.66683V14.0742C11.3333 14.4015 11.0548 14.6668 10.7111 14.6668H2.62222C2.27858 14.6668 2 14.4015 2 14.0742V4.59276C2 4.26548 2.27858 4.00016 2.62222 4.00016H10.6667C11.0349 4.00016 11.3333 4.29864 11.3333 4.66683ZM13.8047 1.52876C13.9254 1.6494 14 1.81607 14 2.00016V10.3335C14 10.5176 13.8508 10.6668 13.6667 10.6668H13C12.8159 10.6668 12.6667 10.5176 12.6667 10.3335V2.66683H6.33333C6.14924 2.66683 6 2.51759 6 2.3335V1.66683C6 1.48273 6.14924 1.3335 6.33333 1.3335H13.3333C13.5174 1.3335 13.6841 1.40812 13.8047 1.52876Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 706 B

+3 -2
View File
@@ -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?"
}
}
}
+3 -2
View File
@@ -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": "确定还原至社区版?"
}
}
}
+14 -2
View File
@@ -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
@@ -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'))
}
}
</script>
<template>
@@ -318,12 +326,17 @@ function addToDashboard() {
direction="rtl"
body-class="chart-sql-drawer-body"
>
<div>
<div class="sql-block">
<SQLComponent
v-if="message.record?.sql"
:sql="message.record?.sql"
style="margin-top: 12px"
/>
<el-button v-if="message.record?.sql" circle class="input-icon" @click="copy">
<el-icon size="16">
<icon_copy_outlined />
</el-icon>
</el-button>
</div>
</el-drawer>
</div>
@@ -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;
}
}
</style>
@@ -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' },
@@ -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' },
@@ -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,