diff --git a/backend/apps/system/api/login.py b/backend/apps/system/api/login.py index 3697cf58c..41a87a5e7 100644 --- a/backend/apps/system/api/login.py +++ b/backend/apps/system/api/login.py @@ -2,21 +2,29 @@ from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.security import OAuth2PasswordRequestForm from apps.system.schemas.logout_schema import LogoutSchema -from apps.system.schemas.system_schema import BaseUserDTO +from apps.system.schemas.system_schema import BaseUserDTO, LoginPwdEditor from common.core.deps import SessionDep, Trans from common.utils.crypto import sqlbot_decrypt -from ..crud.user import authenticate -from common.core.security import create_access_token +from ..crud.user import authenticate, check_pwd_format, clean_user_cache, get_db_user +from common.core.security import create_access_token, default_pwd, md5pwd, verify_md5pwd from datetime import timedelta from common.core.config import settings from common.core.schemas import Token from sqlbot_xpack.authentication.manage import logout as xpack_logout +from sqlbot_xpack.config.arg_manage import get_group_args from common.audit.models.log_model import OperationType, OperationModules from common.audit.schemas.logger_decorator import system_log, LogConfig router = APIRouter(tags=["login"], prefix="/login") + +async def initial_pwd_disabled(session) -> bool: + login_args = await get_group_args(session=session, flag='login') + disabled_arg = next((a for a in login_args if a.pkey == 'login.initial_pwd_disabled'), None) + return bool(disabled_arg and str(disabled_arg.pval).strip().lower() == 'true') + + @router.post("/access-token") @system_log(LogConfig( operation_type=OperationType.LOGIN, @@ -39,13 +47,46 @@ async def local_login( raise HTTPException(status_code=400, detail=trans('i18n_login.user_disable', msg = trans('i18n_concat_admin'))) if user.origin is not None and user.origin != 0: raise HTTPException(status_code=400, detail=trans('i18n_login.origin_error')) + if ( + user.id != 1 + and await initial_pwd_disabled(session) + and verify_md5pwd(default_pwd(), user.password) + ): + return Token(access_token='', need_change_pwd=True) access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) user_dict = user.to_dict() - return Token(access_token=create_access_token( - user_dict, expires_delta=access_token_expires - )) + return Token( + access_token=create_access_token(user_dict, expires_delta=access_token_expires), + need_change_pwd=False, + ) + + +@router.post("/change-pwd") +@system_log(LogConfig( + operation_type=OperationType.UPDATE_PWD, + module=OperationModules.USER, + result_id_expr="id" +)) +async def login_change_pwd(session: SessionDep, trans: Trans, editor: LoginPwdEditor): + origin_account = await sqlbot_decrypt(editor.account) + origin_pwd = await sqlbot_decrypt(editor.pwd) + new_pwd = await sqlbot_decrypt(editor.new_pwd) + user: BaseUserDTO = authenticate(session=session, account=origin_account, password=origin_pwd) + if not user: + raise HTTPException(status_code=400, detail=trans('i18n_login.account_pwd_error')) + if user.origin is not None and user.origin != 0: + raise HTTPException(status_code=400, detail=trans('i18n_login.origin_error')) + if not check_pwd_format(new_pwd): + raise HTTPException(status_code=400, detail=trans('i18n_format_invalid', key = trans('i18n_user.password'))) + if await initial_pwd_disabled(session) and new_pwd == default_pwd(): + raise HTTPException(status_code=400, detail=trans('i18n_login.new_pwd_is_initial')) + db_user = get_db_user(session=session, user_id=user.id) + db_user.password = md5pwd(new_pwd) + session.add(db_user) + await clean_user_cache(user.id) + return db_user -@router.post("/logout") +@router.post("/logout") async def logout(session: SessionDep, request: Request, dto: LogoutSchema): if dto.origin != 0: return await xpack_logout(session, request, dto) diff --git a/backend/apps/system/api/user.py b/backend/apps/system/api/user.py index 0386f3a1c..d383fe597 100644 --- a/backend/apps/system/api/user.py +++ b/backend/apps/system/api/user.py @@ -18,6 +18,7 @@ from common.core.sqlbot_cache import clear_cache from common.core.config import settings from apps.swagger.i18n import PLACEHOLDER_PREFIX +from sqlbot_xpack.config.arg_manage import get_group_args router = APIRouter(tags=["system_user"], prefix="/user") @@ -45,7 +46,11 @@ async def user_info(current_user: CurrentUser) -> UserInfoDTO: @router.get("/defaultPwd", include_in_schema=False) @require_permissions(permission=SqlbotPermission(role=['admin'])) -async def default_pwd() -> str: +async def default_pwd(session: SessionDep) -> str: + login_args = await get_group_args(session=session, flag='login') + hidden_arg = next((a for a in login_args if a.pkey == 'login.initial_pwd_hidden'), None) + if hidden_arg and str(hidden_arg.pval).strip().lower() == 'true': + return '' return settings.DEFAULT_PWD @router.get("/pager/{pageNum}/{pageSize}", response_model=PaginatedResponse[UserGrid], summary=f"{PLACEHOLDER_PREFIX}system_user_grid", description=f"{PLACEHOLDER_PREFIX}system_user_grid") diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py index db2f255b4..1e75bdfbc 100644 --- a/backend/apps/system/schemas/system_schema.py +++ b/backend/apps/system/schemas/system_schema.py @@ -83,6 +83,12 @@ class PwdEditor(BaseModel): new_pwd: str = Field(description=f"{PLACEHOLDER_PREFIX}new_pwd") +class LoginPwdEditor(BaseModel): + account: str = Field(description=f"{PLACEHOLDER_PREFIX}user_account") + pwd: str = Field(description=f"{PLACEHOLDER_PREFIX}origin_pwd") + new_pwd: str = Field(description=f"{PLACEHOLDER_PREFIX}new_pwd") + + class UserWsBase(BaseModel): uid_list: list[int] = Field(description=f"{PLACEHOLDER_PREFIX}uid") oid: Optional[int] = Field(default=None, description=f"{PLACEHOLDER_PREFIX}oid") diff --git a/backend/common/core/schemas.py b/backend/common/core/schemas.py index 04c3d5eaa..2c60fa0e9 100644 --- a/backend/common/core/schemas.py +++ b/backend/common/core/schemas.py @@ -16,6 +16,7 @@ class Token(SQLModel): access_token: str token_type: str = "bearer" platform_info: Optional[dict] = None + need_change_pwd: Optional[bool] = None class XOAuth2PasswordBearer(OAuth2PasswordBearer): async def __call__(self, request: Request) -> Optional[str]: diff --git a/backend/locales/en.json b/backend/locales/en.json index 43db335b8..08ab4bcca 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -14,7 +14,8 @@ "user_disable": "Account is disabled, {msg}", "origin_error": "Invalid login method", "prohibit_auto_create": "Automatically creating users is prohibited. Please synchronize users first", - "no_platform_user": "Account does not exist. Please sync the user first." + "no_platform_user": "Account does not exist. Please sync the user first.", + "new_pwd_is_initial": "The new password cannot be the initial password" }, "i18n_user": { "account": "Account", diff --git a/backend/locales/ko-KR.json b/backend/locales/ko-KR.json index 186c2a4d6..d12b70d31 100644 --- a/backend/locales/ko-KR.json +++ b/backend/locales/ko-KR.json @@ -14,7 +14,8 @@ "user_disable": "계정이 비활성화되었습니다, {msg}", "origin_error": "잘못된 로그인 방식입니다", "prohibit_auto_create": "사용자 자동 생성이 금지되어 있습니다. 먼저 사용자를 동기화해 주세요", - "no_platform_user": "계정이 존재하지 않습니다. 먼저 사용자를 동기화하세요." + "no_platform_user": "계정이 존재하지 않습니다. 먼저 사용자를 동기화하세요.", + "new_pwd_is_initial": "새 비밀번호는 초기 비밀번호로 설정할 수 없습니다" }, "i18n_user": { "account": "계정", diff --git a/backend/locales/zh-CN.json b/backend/locales/zh-CN.json index 788e911e7..2fa8744cf 100644 --- a/backend/locales/zh-CN.json +++ b/backend/locales/zh-CN.json @@ -14,7 +14,8 @@ "user_disable": "账号已禁用,{msg}", "origin_error": "登录方式错误", "prohibit_auto_create": "禁止自动创建用户,请先同步用户", - "no_platform_user": "账号不存在,请先同步用户" + "no_platform_user": "账号不存在,请先同步用户", + "new_pwd_is_initial": "新密码不能为初始密码" }, "i18n_user": { "account": "账号", diff --git a/backend/locales/zh-TW.json b/backend/locales/zh-TW.json index 6b37aca9f..3a5358ab5 100644 --- a/backend/locales/zh-TW.json +++ b/backend/locales/zh-TW.json @@ -14,7 +14,8 @@ "user_disable": "帳號已禁用,{msg}", "origin_error": "登入方式錯誤", "prohibit_auto_create": "禁止自動建立用戶,請先同步用戶", - "no_platform_user": "帳號不存在,請先同步用戶" + "no_platform_user": "帳號不存在,請先同步用戶", + "new_pwd_is_initial": "新密碼不能為初始密碼" }, "i18n_user": { "account": "帳號", diff --git a/frontend/src/api/login.ts b/frontend/src/api/login.ts index 7dfdb0ec4..72ae72c3d 100644 --- a/frontend/src/api/login.ts +++ b/frontend/src/api/login.ts @@ -14,6 +14,14 @@ export const AuthApi = { }, }) }, + changePwd: (data: { account: string; pwd: string; new_pwd: string }) => { + const entry = { + account: LicenseGenerator.sqlbotEncrypt(data.account), + pwd: LicenseGenerator.sqlbotEncrypt(data.pwd), + new_pwd: LicenseGenerator.sqlbotEncrypt(data.new_pwd), + } + return request.post('/login/change-pwd', entry) + }, logout: (data: any) => request.post('/login/logout', data), info: () => request.get('/user/info'), } diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 3680a773d..430fa369c 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -96,7 +96,9 @@ "closed_by_default": "In the Question Count window, control whether the model thinking process is expanded or closed by default.", "and_platform_integration": "Authentication-related", "login_settings": "Login Settings", - "default_login": "Default Login Method" + "default_login": "Default Login Method", + "disable_initial_password": "Disable Initial Password", + "hide_initial_password": "Hide Initial Password on Page" }, "prompt": { "default_password": "Default password:{msg}", @@ -932,6 +934,9 @@ "default_login": "Default", "ldap_login": "LDAP Login", "account_login": "Account Login", + "force_change_pwd_title": "Change Initial Password", + "force_change_pwd_tips": "Your current password is the system initial password. Please change it and log in again.", + "force_change_pwd_success": "Password changed successfully", "other_login": "Other Login Methods", "pwd_invalid_error": "Password has expired, please contact administrator to modify or reset", "pwd_exp_tips": "Password will expire in {0} days, please change it as soon as possible", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 332e303f6..850102f9b 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -96,7 +96,9 @@ "closed_by_default": "질문 수 창에서 모델 사고 프로세스를 기본적으로 확장할지 또는 닫을지 여부를 제어합니다.", "and_platform_integration": "인증 관련", "login_settings": "로그인 설정", - "default_login": "기본 로그인 방식" + "default_login": "기본 로그인 방식", + "disable_initial_password": "초기 비밀번호 비활성화", + "hide_initial_password": "페이지에서 초기 비밀번호 숨기기" }, "prompt": { "default_password:": "기본 비밀번호:{msg}", @@ -932,6 +934,9 @@ "default_login": "기본값", "ldap_login": "LDAP 로그인", "account_login": "계정 로그인", + "force_change_pwd_title": "초기 비밀번호 변경", + "force_change_pwd_tips": "현재 비밀번호는 시스템 초기 비밀번호입니다. 비밀번호를 변경한 후 로그인해 주세요.", + "force_change_pwd_success": "비밀번호가 변경되었습니다", "other_login": "기타 로그인 방식", "pwd_invalid_error": "비밀번호가 만료되었습니다. 관리자에게 문의하여 수정 또는 재설정해 주세요", "pwd_exp_tips": "비밀번호가 {0}일 후에 만료됩니다.尽快尽快 비밀번호를 변경해 주세요", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 06697bd26..30cea8f2d 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -96,7 +96,9 @@ "closed_by_default": "在问数窗口中,控制模型思考过程默认展开或者关闭", "and_platform_integration": "登录认证相关", "login_settings": "登录设置", - "default_login": "默认登录方式" + "default_login": "默认登录方式", + "disable_initial_password": "禁用初始密码", + "hide_initial_password": "页面隐藏初始密码" }, "prompt": { "default_password": "默认密码:{msg}", @@ -932,6 +934,9 @@ "default_login": "默认", "ldap_login": "LDAP 登录", "account_login": "账号登录", + "force_change_pwd_title": "修改初始密码", + "force_change_pwd_tips": "当前密码为系统初始密码,请修改密码后再登录", + "force_change_pwd_success": "密码修改成功", "other_login": "其他登录方式", "pwd_invalid_error": "密码已过期请联系管理员修改或重置", "pwd_exp_tips": "密码在 {0} 天后过期,请尽快修改密码", diff --git a/frontend/src/i18n/zh-TW.json b/frontend/src/i18n/zh-TW.json index 86991ae44..295d2d016 100644 --- a/frontend/src/i18n/zh-TW.json +++ b/frontend/src/i18n/zh-TW.json @@ -96,7 +96,9 @@ "closed_by_default": "在問數視窗中,控制模型思考過程預設展開或者關閉", "and_platform_integration": "登入認證相關", "login_settings": "登入設定", - "default_login": "預設登入方式" + "default_login": "預設登入方式", + "disable_initial_password": "禁用初始密碼", + "hide_initial_password": "頁面隱藏初始密碼" }, "prompt": { "default_password": "預設密碼:{msg}", @@ -932,6 +934,9 @@ "default_login": "預設", "ldap_login": "LDAP 登入", "account_login": "帳號登入", + "force_change_pwd_title": "修改初始密碼", + "force_change_pwd_tips": "目前密碼為系統初始密碼,請修改密碼後再登入", + "force_change_pwd_success": "密碼修改成功", "other_login": "其他登入方式", "pwd_invalid_error": "密碼已過期請聯繫管理員修改或重設", "pwd_exp_tips": "密碼在 {0} 天後過期,請盡快修改密碼", diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index b72e061d7..30ba4f3a8 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -83,7 +83,10 @@ export const UserStore = defineStore('user', { actions: { async login(formData: { username: string; password: string }) { const res: any = await AuthApi.login(formData) - this.setToken(res.access_token) + if (res.access_token) { + this.setToken(res.access_token) + } + return res }, async logout() { diff --git a/frontend/src/views/login/InitialPwdDialog.vue b/frontend/src/views/login/InitialPwdDialog.vue new file mode 100644 index 000000000..af0b95360 --- /dev/null +++ b/frontend/src/views/login/InitialPwdDialog.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue index 87de40e7a..a52838f12 100644 --- a/frontend/src/views/login/index.vue +++ b/frontend/src/views/login/index.vue @@ -71,6 +71,12 @@ jsname="L2NvbXBvbmVudC9sb2dpbi9IYW5kbGVy" @switch-tab="switchTab" /> + @@ -88,6 +94,7 @@ import login_image from '@/assets/embedded/login_image.png' import { useAppearanceStoreWithOut } from '@/stores/appearance' import loginImage from '@/assets/blue/login-image_blue.png' import Handler from './xpack/Handler.vue' +import InitialPwdDialog from './InitialPwdDialog.vue' import { toLoginSuccess } from '@/utils/utils' import elementResizeDetectorMaker from 'element-resize-detector' @@ -134,12 +141,20 @@ const loginFormRef = ref() const submitForm = () => { loginFormRef.value.validate((valid: boolean) => { if (valid) { - userStore.login(loginForm.value).then(() => { + userStore.login(loginForm.value).then((res: any) => { + if (res?.need_change_pwd) { + initPwdDialogVisible.value = true + return + } toLoginSuccess(router) }) } }) } +const initPwdDialogVisible = ref(false) +const onInitPwdSaved = (newPwd: string) => { + loginForm.value.password = newPwd +} const switchTab = (name: string) => { activeName.value = name || 'simple' } diff --git a/frontend/src/views/system/parameter/xpack/PlatformParam.vue b/frontend/src/views/system/parameter/xpack/PlatformParam.vue index 8590157d2..b770255e3 100644 --- a/frontend/src/views/system/parameter/xpack/PlatformParam.vue +++ b/frontend/src/views/system/parameter/xpack/PlatformParam.vue @@ -63,11 +63,11 @@ -
+
{{ t('parameter.login_settings') }}
-
+
{{ t('parameter.default_login') }}
@@ -79,6 +79,20 @@
+ +
+
{{ t('parameter.disable_initial_password') }}
+
+ +
+
+ +
+
{{ t('parameter.hide_initial_password') }}
+
+ +
+
@@ -97,6 +111,8 @@ const defaultForm = reactive>({ 'platform.oid': 1, 'platform.rid': 1, 'login.default_login': 0, + 'login.initial_pwd_disabled': false, + 'login.initial_pwd_hidden': false, }) const loginTypeOptions = shallowRef([{ value: 0, label: t('login.account_login') }]) diff --git a/frontend/src/views/system/user/User.vue b/frontend/src/views/system/user/User.vue index 3dcc911b3..5db892ecc 100644 --- a/frontend/src/views/system/user/User.vue +++ b/frontend/src/views/system/user/User.vue @@ -203,7 +203,7 @@ {{ t('datasource.the_original_one') }}
-
+
{{ defaultPwd }} {{ t('datasource.copy') @@ -284,7 +284,7 @@ size="600px" :before-close="onFormClose" > -
+
@@ -584,6 +584,7 @@ import { request } from '@/utils/request' import { workspaceList } from '@/api/workspace' import { variablesApi } from '@/api/variables' import { formatTimestamp } from '@/utils/date' +import { formatArg } from '@/utils/utils' import { ClickOutside as vClickOutside } from 'element-plus-secondary' import icon_warning_filled from '@/assets/svg/icon_warning_filled.svg' import { useClipboard } from '@vueuse/core' @@ -1291,6 +1292,21 @@ const loadDefaultPwd = () => { } }) } +const showInitPwd = ref(true) +const loadInitPwdHidden = () => { + request + .get('/system/parameter/login') + .then((res: any) => { + const argObj: Record = {} + ;(res || []).forEach((item: any) => { + argObj[item.pkey] = item.pval + }) + showInitPwd.value = !formatArg(argObj['login.initial_pwd_hidden']) + }) + .catch(() => { + showInitPwd.value = true + }) +} const formatUserOrigin = (origin?: number) => { if (!origin) { return t('user.local_creation') @@ -1327,6 +1343,7 @@ onMounted(() => { handleCurrentChange(1) loadDefaultPwd() + loadInitPwdHidden() }) const downErrorExcel = (dataKey: any) => { userApi.errorRecord(dataKey).then((res: any) => {