From a8f236256db4111fe9337f043a2a64e4f500d553 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Tue, 4 Nov 2025 17:23:29 +0800 Subject: [PATCH] feat(X-Pack): Add CAS Authentication Mechanism --- .../versions/048_authentication_ddl.py | 40 ++ .../alembic/versions/049_user_platform_ddl.py | 42 ++ backend/apps/system/api/login.py | 12 +- backend/apps/system/models/system_model.py | 14 +- backend/apps/system/models/user.py | 11 + backend/apps/system/schemas/auth.py | 4 +- backend/apps/system/schemas/logout_schema.py | 9 + backend/apps/system/schemas/system_schema.py | 3 +- backend/common/utils/http_utils.py | 31 ++ backend/common/utils/whitelist.py | 5 +- frontend/src/api/login.ts | 2 +- frontend/src/assets/svg/logo_cas.svg | 16 + frontend/src/assets/svg/logo_dingtalk.svg | 5 + frontend/src/assets/svg/logo_lark.svg | 20 + frontend/src/assets/svg/logo_ldap.svg | 14 + frontend/src/assets/svg/logo_oauth.svg | 3 + frontend/src/assets/svg/logo_saml.svg | 1 + frontend/src/components/layout/Person.vue | 4 +- frontend/src/components/layout/index.vue | 4 +- frontend/src/i18n/en.json | 23 + frontend/src/i18n/ko-KR.json | 25 +- frontend/src/i18n/zh-CN.json | 25 +- frontend/src/router/index.ts | 7 + frontend/src/stores/user.ts | 20 +- frontend/src/utils/RemoteJs.ts | 22 + frontend/src/utils/utils.ts | 34 ++ frontend/src/views/WelcomeView.vue | 4 +- frontend/src/views/login/index.vue | 8 +- frontend/src/views/login/xpack/Cas.vue | 52 +++ frontend/src/views/login/xpack/Handler.vue | 423 ++++++++++++++++++ .../src/views/login/xpack/PlatformClient.ts | 184 ++++++++ .../views/system/authentication/CasEditor.vue | 273 +++++++++++ .../system/authentication/LdapEditor.vue | 288 ++++++++++++ .../system/authentication/Oauth2Editor.vue | 354 +++++++++++++++ .../system/authentication/OidcEditor.vue | 333 ++++++++++++++ .../system/authentication/SAML2Editor.vue | 259 +++++++++++ .../src/views/system/authentication/index.vue | 254 +++++++++++ 37 files changed, 2808 insertions(+), 20 deletions(-) create mode 100644 backend/alembic/versions/048_authentication_ddl.py create mode 100644 backend/alembic/versions/049_user_platform_ddl.py create mode 100644 backend/apps/system/schemas/logout_schema.py create mode 100644 backend/common/utils/http_utils.py create mode 100644 frontend/src/assets/svg/logo_cas.svg create mode 100644 frontend/src/assets/svg/logo_dingtalk.svg create mode 100644 frontend/src/assets/svg/logo_lark.svg create mode 100644 frontend/src/assets/svg/logo_ldap.svg create mode 100644 frontend/src/assets/svg/logo_oauth.svg create mode 100644 frontend/src/assets/svg/logo_saml.svg create mode 100644 frontend/src/utils/RemoteJs.ts create mode 100644 frontend/src/views/login/xpack/Cas.vue create mode 100644 frontend/src/views/login/xpack/Handler.vue create mode 100644 frontend/src/views/login/xpack/PlatformClient.ts create mode 100644 frontend/src/views/system/authentication/CasEditor.vue create mode 100644 frontend/src/views/system/authentication/LdapEditor.vue create mode 100644 frontend/src/views/system/authentication/Oauth2Editor.vue create mode 100644 frontend/src/views/system/authentication/OidcEditor.vue create mode 100644 frontend/src/views/system/authentication/SAML2Editor.vue create mode 100644 frontend/src/views/system/authentication/index.vue diff --git a/backend/alembic/versions/048_authentication_ddl.py b/backend/alembic/versions/048_authentication_ddl.py new file mode 100644 index 00000000..fe7e7042 --- /dev/null +++ b/backend/alembic/versions/048_authentication_ddl.py @@ -0,0 +1,40 @@ +"""048_authentication_ddl + +Revision ID: 073bf544b373 +Revises: c1b794a961ce +Create Date: 2025-10-30 14:11:29.786938 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + +# revision identifiers, used by Alembic. +revision = '073bf544b373' +down_revision = 'c1b794a961ce' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('sys_authentication', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('name', sa.String(255), nullable=False), + sa.Column('type', sa.Integer(), nullable=False), + sa.Column('config', sa.Text(), nullable=True), + sa.Column('enable', sa.Boolean(), nullable=False), + sa.Column('valid', sa.Boolean(), nullable=False), + sa.Column('create_time', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_sys_authentication_id'), 'sys_authentication', ['id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_sys_authentication_id'), table_name='sys_authentication') + op.drop_table('sys_authentication') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/049_user_platform_ddl.py b/backend/alembic/versions/049_user_platform_ddl.py new file mode 100644 index 00000000..0e5ed35a --- /dev/null +++ b/backend/alembic/versions/049_user_platform_ddl.py @@ -0,0 +1,42 @@ +"""049_user_platform_ddl + +Revision ID: b58a71ca6ae3 +Revises: 073bf544b373 +Create Date: 2025-11-04 12:31:56.481582 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + +# revision identifiers, used by Alembic. +revision = 'b58a71ca6ae3' +down_revision = '073bf544b373' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('sys_user_platform', + sa.Column('uid', sa.BigInteger(), nullable=False), + sa.Column('origin', sa.Integer(), server_default='0', nullable=False), + sa.Column('platform_uid', sa.String(255), nullable=False), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_sys_user_platform_id'), 'sys_user_platform', ['id'], unique=False) + + op.add_column('sys_user', sa.Column('origin', sa.Integer(), server_default='0', nullable=False)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + op.drop_column('sys_user', 'origin') + + op.drop_index(op.f('ix_sys_user_platform_id'), table_name='sys_user_platform') + op.drop_table('sys_user_platform') + # ### end Alembic commands ### diff --git a/backend/apps/system/api/login.py b/backend/apps/system/api/login.py index 70d85146..426dc4cd 100644 --- a/backend/apps/system/api/login.py +++ b/backend/apps/system/api/login.py @@ -1,6 +1,7 @@ from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException +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 common.core.deps import SessionDep, Trans from common.utils.crypto import sqlbot_decrypt @@ -9,6 +10,7 @@ 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 router = APIRouter(tags=["login"], prefix="/login") @router.post("/access-token") @@ -30,4 +32,10 @@ async def local_login( user_dict = user.to_dict() return Token(access_token=create_access_token( user_dict, expires_delta=access_token_expires - )) \ No newline at end of file + )) + +@router.post("/logout") +async def logout(session: SessionDep, request: Request, dto: LogoutSchema): + if dto.origin != 0: + return await xpack_logout(session, request, dto) + return None \ No newline at end of file diff --git a/backend/apps/system/models/system_model.py b/backend/apps/system/models/system_model.py index 8d782af0..281b737c 100644 --- a/backend/apps/system/models/system_model.py +++ b/backend/apps/system/models/system_model.py @@ -55,4 +55,16 @@ class AssistantBaseModel(SQLModel): class AssistantModel(SnowflakeBase, AssistantBaseModel, table=True): __tablename__ = "sys_assistant" - \ No newline at end of file + + +class AuthenticationBaseModel(SQLModel): + name: str = Field(max_length=255, nullable=False) + type: int = Field(nullable=False, default=0) + config: Optional[str] = Field(sa_type = Text(), nullable=True) + + +class AuthenticationModel(SnowflakeBase, AuthenticationBaseModel, table=True): + __tablename__ = "sys_authentication" + create_time: Optional[int] = Field(default=0, sa_type=BigInteger()) + enable: bool = Field(default=False, nullable=False) + valid: bool = Field(default=False, nullable=False) \ No newline at end of file diff --git a/backend/apps/system/models/user.py b/backend/apps/system/models/user.py index 078f02d2..a6ed524f 100644 --- a/backend/apps/system/models/user.py +++ b/backend/apps/system/models/user.py @@ -15,9 +15,20 @@ class BaseUserPO(SQLModel): password: str = Field(default_factory=default_md5_pwd, max_length=255) email: str = Field(max_length=255) status: int = Field(default=0, nullable=False) + origin: int = Field(nullable=False, default=0) create_time: int = Field(default_factory=get_timestamp, sa_type=BigInteger(), nullable=False) language: str = Field(max_length=255, default="zh-CN") class UserModel(SnowflakeBase, BaseUserPO, table=True): __tablename__ = "sys_user" + +class UserPlatformBase(SQLModel): + uid: int = Field(nullable=False, sa_type=BigInteger()) + origin: int = Field(nullable=False, default=0) + platform_uid: str = Field(max_length=255, nullable=False) + +class UserPlatformModel(SnowflakeBase, UserPlatformBase, table=True): + __tablename__ = "sys_user_platform" + + diff --git a/backend/apps/system/schemas/auth.py b/backend/apps/system/schemas/auth.py index f67b15f0..3b3816ab 100644 --- a/backend/apps/system/schemas/auth.py +++ b/backend/apps/system/schemas/auth.py @@ -1,4 +1,5 @@ +from typing import Optional from pydantic import BaseModel from enum import Enum @@ -16,4 +17,5 @@ class CacheName(Enum): ASSISTANT_INFO = "assistant:info" ASSISTANT_DS = "assistant:ds" def __str__(self): - return self.value \ No newline at end of file + return self.value + diff --git a/backend/apps/system/schemas/logout_schema.py b/backend/apps/system/schemas/logout_schema.py new file mode 100644 index 00000000..b6b3d492 --- /dev/null +++ b/backend/apps/system/schemas/logout_schema.py @@ -0,0 +1,9 @@ +from typing import Optional +from pydantic import BaseModel + + +class LogoutSchema(BaseModel): + token: Optional[str] = None + flag: Optional[str] = 'default' + origin: Optional[int] = 0 + data: Optional[str] = None \ No newline at end of file diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py index 6505e8fd..52dc20c9 100644 --- a/backend/apps/system/schemas/system_schema.py +++ b/backend/apps/system/schemas/system_schema.py @@ -53,6 +53,7 @@ class UserCreator(BaseUser): name: str = Field(min_length=1, max_length=100, description="用户名") email: str = Field(min_length=1, max_length=100, description="用户邮箱") status: int = 1 + origin: Optional[int] = 0 oid_list: Optional[list[int]] = None """ @field_validator("email") @@ -70,7 +71,7 @@ class UserGrid(UserEditor): create_time: int language: str = "zh-CN" # space_name: Optional[str] = None - origin: str = '' + # origin: str = '' class PwdEditor(BaseModel): diff --git a/backend/common/utils/http_utils.py b/backend/common/utils/http_utils.py new file mode 100644 index 00000000..61f944fa --- /dev/null +++ b/backend/common/utils/http_utils.py @@ -0,0 +1,31 @@ +from typing import Tuple +import requests +from urllib.parse import urlparse +from requests.exceptions import RequestException, Timeout + +def verify_url(url: str, timeout: int = 5) -> Tuple[bool, str]: + try: + parsed = urlparse(url) + if not all([parsed.scheme, parsed.netloc]): + return False, "无效的 URL 格式" + + if parsed.scheme not in ['http', 'https']: + return False, "URL 必须以 http 或 https 开头" + + response = requests.get( + url, + timeout=timeout, + verify=False # 忽略 SSL 证书验证 + ) + + if response.status_code < 400: + return True, "URL 可达" + else: + return False, f"服务器返回错误状态码: {response.status_code}" + + except Timeout: + return False, f"连接超时 (>{timeout}秒)" + except RequestException as e: + return False, f"连接失败: {str(e)}" + except Exception as e: + return False, f"验证过程发生错误: {str(e)}" \ No newline at end of file diff --git a/backend/common/utils/whitelist.py b/backend/common/utils/whitelist.py index beef0b5b..c880fe53 100644 --- a/backend/common/utils/whitelist.py +++ b/backend/common/utils/whitelist.py @@ -33,7 +33,10 @@ "/system/assistant/info/*", "/system/assistant/app/*", "/system/assistant/picture/*", - "/datasource/uploadExcel" + "/datasource/uploadExcel", + "/system/authentication/platform/status", + "/system/authentication/login/*", + "/system/authentication/sso/*", ] class WhitelistChecker: diff --git a/frontend/src/api/login.ts b/frontend/src/api/login.ts index efe894e2..7dfdb0ec 100644 --- a/frontend/src/api/login.ts +++ b/frontend/src/api/login.ts @@ -14,6 +14,6 @@ export const AuthApi = { }, }) }, - logout: () => request.post('/auth/logout'), + logout: (data: any) => request.post('/login/logout', data), info: () => request.get('/user/info'), } diff --git a/frontend/src/assets/svg/logo_cas.svg b/frontend/src/assets/svg/logo_cas.svg new file mode 100644 index 00000000..72242460 --- /dev/null +++ b/frontend/src/assets/svg/logo_cas.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/svg/logo_dingtalk.svg b/frontend/src/assets/svg/logo_dingtalk.svg new file mode 100644 index 00000000..c392456b --- /dev/null +++ b/frontend/src/assets/svg/logo_dingtalk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/svg/logo_lark.svg b/frontend/src/assets/svg/logo_lark.svg new file mode 100644 index 00000000..0eaf8df8 --- /dev/null +++ b/frontend/src/assets/svg/logo_lark.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/svg/logo_ldap.svg b/frontend/src/assets/svg/logo_ldap.svg new file mode 100644 index 00000000..57deedf2 --- /dev/null +++ b/frontend/src/assets/svg/logo_ldap.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/assets/svg/logo_oauth.svg b/frontend/src/assets/svg/logo_oauth.svg new file mode 100644 index 00000000..89ef68c1 --- /dev/null +++ b/frontend/src/assets/svg/logo_oauth.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/svg/logo_saml.svg b/frontend/src/assets/svg/logo_saml.svg new file mode 100644 index 00000000..6b682117 --- /dev/null +++ b/frontend/src/assets/svg/logo_saml.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/layout/Person.vue b/frontend/src/components/layout/Person.vue index 94651432..d7972033 100644 --- a/frontend/src/components/layout/Person.vue +++ b/frontend/src/components/layout/Person.vue @@ -82,8 +82,8 @@ const toAbout = () => { const savePwdHandler = () => { pwdFormRef.value?.submit() } -const logout = () => { - userStore.logout() +const logout = async () => { + await userStore.logout() router.push('/login') } diff --git a/frontend/src/components/layout/index.vue b/frontend/src/components/layout/index.vue index 0aa85842..de8c2ee7 100644 --- a/frontend/src/components/layout/index.vue +++ b/frontend/src/components/layout/index.vue @@ -235,8 +235,8 @@ const resolveIcon = (iconName: any) => { const menuSelect = (e: any) => { router.push(e.index) } -const logout = () => { - userStore.logout() +const logout = async () => { + await userStore.logout() router.push('/login') } const toSystem = () => { diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 253c76a3..40993925 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -664,6 +664,7 @@ "system": { "system_settings": "System Settings", "appearance_settings": "Appearance Settings", + "authentication_settings": "Authentication Settings", "platform_display_theme": "Platform Display Theme", "default_turquoise": "Default (Turquoise)", "tech_blue": "Tech Blue", @@ -692,5 +693,27 @@ "save_and_apply": "Save and Apply", "setting_successfully": "Setting Successfully", "customize_theme_color": "Customize theme color" + }, + "authentication": { + "invalid": "Invalid", + "valid": "Valid", + "cas_settings": "CAS Settings", + "callback_domain_name": "Callback Domain", + "field_mapping": "User Attribute Mapping", + "field_mapping_placeholder": "Example: {'{'}\"account\": \"saml2Account\", \"name\": \"saml2Name\", \"email\": \"email\"{'}'}", + "incorrect_please_re_enter": "Incorrect format, please re-enter", + "in_json_format": "Please enter JSON format" + }, + "login": { + "default_login": "Default", + "ldap_login": "LDAP Login", + "account_login": "Account Login", + "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", + "qr_code": "QR Code", + "platform_disable": "{0} settings are not enabled!", + "input_account": "Please enter account", + "redirect_2_auth": "Redirecting to {0} authentication, {1} seconds..." } } diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 52c642ca..1ff22322 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -664,6 +664,7 @@ "system": { "system_settings": "시스템 설정", "appearance_settings": "외관 설정", + "authentication_settings": "인증 설정", "platform_display_theme": "플랫폼 표시 테마", "default_turquoise": "기본 (터키석 녹색)", "tech_blue": "테크 블루", @@ -692,5 +693,27 @@ "save_and_apply": "저장 및 적용", "setting_successfully": "설정 성공", "customize_theme_color": "사용자 정의 테마 색상" + }, + "authentication": { + "invalid": "유효하지 않음", + "valid": "유효함", + "cas_settings": "CAS 설정", + "callback_domain_name": "콜백 도메인", + "field_mapping": "사용자 속성 매핑", + "field_mapping_placeholder": "예: {'{'}\"account\": \"saml2Account\", \"name\": \"saml2Name\", \"email\": \"email\"{'}'}", + "incorrect_please_re_enter": "형식이 잘못되었습니다. 다시 입력해 주세요", + "in_json_format": "JSON 형식으로 입력해 주세요" + }, + "login": { + "default_login": "기본값", + "ldap_login": "LDAP 로그인", + "account_login": "계정 로그인", + "other_login": "기타 로그인 방식", + "pwd_invalid_error": "비밀번호가 만료되었습니다. 관리자에게 문의하여 수정 또는 재설정해 주세요", + "pwd_exp_tips": "비밀번호가 {0}일 후에 만료됩니다.尽快尽快 비밀번호를 변경해 주세요", + "qr_code": "QR 코드", + "platform_disable": "{0} 설정이 활성화되지 않았습니다!", + "input_account": "계정을 입력해 주세요", + "redirect_2_auth": "{0} 인증으로 리디렉션 중입니다, {1}초..." } -} \ No newline at end of file +} diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 46b8da20..bda92d77 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -664,6 +664,7 @@ "system": { "system_settings": "系统设置", "appearance_settings": "外观设置", + "authentication_settings": "登录认证", "platform_display_theme": "平台显示主题", "default_turquoise": "默认 (松石绿)", "tech_blue": "科技蓝", @@ -692,5 +693,27 @@ "save_and_apply": "保存并应用", "setting_successfully": "设置成功", "customize_theme_color": "自定义主题色" + }, + "authentication": { + "invalid": "无效", + "valid": "有效", + "cas_settings": "CAS 设置", + "callback_domain_name": "回调地址", + "field_mapping": "用户属性映射", + "field_mapping_placeholder": "例如:{'{'}\"account\": \"saml2Account\", \"name\": \"saml2Name\", \"email\": \"email\"{'}'}", + "incorrect_please_re_enter": "格式错误,请重新填写", + "in_json_format": "请输入json格式" + }, + "login": { + "default_login": "默认", + "ldap_login": "LDAP 登录", + "account_login": "账号登录", + "other_login": "其他登录方式", + "pwd_invalid_error": "密码已过期请联系管理员修改或重置", + "pwd_exp_tips": "密码在 {0} 天后过期,请尽快修改密码", + "qr_code": "二维码", + "platform_disable": "{0}设置未开启!", + "input_account": "请输入账号", + "redirect_2_auth": "正在跳转至 {0} 认证,{1} 秒..." } -} \ No newline at end of file +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 84e29859..4ab80003 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -19,6 +19,7 @@ import Professional from '@/views/system/professional/index.vue' import Training from '@/views/system/training/index.vue' import Prompt from '@/views/system/prompt/index.vue' import Appearance from '@/views/system/appearance/index.vue' +import Authentication from '@/views/system/authentication/index.vue' import Permission from '@/views/system/permission/index.vue' import User from '@/views/system/user/User.vue' import Workspace from '@/views/system/workspace/index.vue' @@ -195,6 +196,12 @@ export const routes = [ component: Appearance, meta: { title: t('system.appearance_settings') }, }, + { + path: 'authentication', + name: 'authentication', + component: Authentication, + meta: { title: t('system.authentication_settings') }, + }, ], }, ], diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index 576e9060..0e21fc17 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -17,7 +17,8 @@ interface UserState { exp: number time: number weight: number - [key: string]: string | number + platformInfo: any | null + [key: string]: string | number | any | null } export const UserStore = defineStore('user', { @@ -32,6 +33,7 @@ export const UserStore = defineStore('user', { exp: 0, time: 0, weight: 0, + platformInfo: null, } }, getters: { @@ -68,6 +70,9 @@ export const UserStore = defineStore('user', { isSpaceAdmin(): boolean { return this.uid === '1' || !!this.weight }, + getPlatformInfo(): any | null { + return this.platformInfo + }, }, actions: { async login(formData: { username: string; password: string }) { @@ -75,8 +80,14 @@ export const UserStore = defineStore('user', { this.setToken(res.access_token) }, - logout() { + async logout() { + const param = { ...{ token: this.token }, ...this.platformInfo } + const res: any = await AuthApi.logout(param) this.clear() + if (res) { + window.location.href = res + window.open(res, '_self') + } }, async info() { @@ -145,6 +156,10 @@ export const UserStore = defineStore('user', { wsCache.set('user.weight', weight) this.weight = weight }, + setPlatformInfo(info: any | null) { + wsCache.set('user.platformInfo', info) + this.platformInfo = info + }, clear() { const keys: string[] = [ 'token', @@ -156,6 +171,7 @@ export const UserStore = defineStore('user', { 'exp', 'time', 'weight', + 'platformInfo', ] keys.forEach((key) => wsCache.delete('user.' + key)) this.$reset() diff --git a/frontend/src/utils/RemoteJs.ts b/frontend/src/utils/RemoteJs.ts new file mode 100644 index 00000000..289fca89 --- /dev/null +++ b/frontend/src/utils/RemoteJs.ts @@ -0,0 +1,22 @@ +export const loadScript = (url: string, jsId?: string) => { + return new Promise(function (resolve, reject) { + const scriptId = jsId || 'de-fit2cloud-script-id' + let dom = document.getElementById(scriptId) + if (dom) { + dom.parentElement?.removeChild(dom) + dom = null + } + const script = document.createElement('script') + + script.id = scriptId + script.onload = function () { + return resolve(null) + } + script.onerror = function () { + return reject(new Error('Load script from '.concat(url, ' failed'))) + } + script.src = url + const head = document.head || document.getElementsByTagName('head')[0] + ;(document.body || head).appendChild(script) + }) +} diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 2a2babf4..082a3647 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -204,3 +204,37 @@ export const setCurrentColor = (color: any, element: HTMLElement = document.docu .toRGB() ) } +export const getQueryString = (name: string) => { + const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i') + const r = window.location.search.substr(1).match(reg) + if (r != null) { + return unescape(r[2]) + } + return null +} + +export const isLarkPlatform = () => { + return !!getQueryString('state') && !!getQueryString('code') +} + +export const isPlatformClient = () => { + return !!getQueryString('client') || getQueryString('state')?.includes('client') +} + +export const checkPlatform = () => { + const flagArray = ['/casbi', 'oidcbi'] + const pathname = window.location.pathname + if ( + !flagArray.some((flag) => pathname.includes(flag)) && + !isLarkPlatform() && + !isPlatformClient() + ) { + return cleanPlatformFlag() + } + return true +} +export const cleanPlatformFlag = () => { + const platformKey = 'out_auth_platform' + wsCache.delete(platformKey) + return false +} diff --git a/frontend/src/views/WelcomeView.vue b/frontend/src/views/WelcomeView.vue index bb4624b4..fb382c86 100644 --- a/frontend/src/views/WelcomeView.vue +++ b/frontend/src/views/WelcomeView.vue @@ -17,8 +17,8 @@ import { useUserStore } from '@/stores/user' const router = useRouter() const userStore = useUserStore() -const logout = () => { - userStore.logout() +const logout = async () => { + await userStore.logout() router.push('/login') } diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue index ce005ef4..eaf9e17c 100644 --- a/frontend/src/views/login/index.vue +++ b/frontend/src/views/login/index.vue @@ -6,8 +6,8 @@
@@ -68,12 +69,13 @@ import LOGO_fold from '@/assets/LOGO-fold.svg' 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' const router = useRouter() const userStore = useUserStore() const appearanceStore = useAppearanceStoreWithOut() const { t } = useI18n() - +const xpackLoginHandler = ref(null) const loginForm = ref({ username: '', password: '', diff --git a/frontend/src/views/login/xpack/Cas.vue b/frontend/src/views/login/xpack/Cas.vue new file mode 100644 index 00000000..b42b086e --- /dev/null +++ b/frontend/src/views/login/xpack/Cas.vue @@ -0,0 +1,52 @@ + + + + diff --git a/frontend/src/views/login/xpack/Handler.vue b/frontend/src/views/login/xpack/Handler.vue new file mode 100644 index 00000000..f665b7e1 --- /dev/null +++ b/frontend/src/views/login/xpack/Handler.vue @@ -0,0 +1,423 @@ + + + + + diff --git a/frontend/src/views/login/xpack/PlatformClient.ts b/frontend/src/views/login/xpack/PlatformClient.ts new file mode 100644 index 00000000..9d7c804b --- /dev/null +++ b/frontend/src/views/login/xpack/PlatformClient.ts @@ -0,0 +1,184 @@ +import { loadScript } from '@/utils/RemoteJs' +import { getQueryString } from '@/utils/utils' +import { ElMessage, ElMessageBox } from 'element-plus-secondary' +import { request } from '@/utils/request' +// import { useI18n } from 'vue-i18n' +import { i18n } from '@/i18n' +declare global { + interface Window { + tt: any + h5sdk: any + dd: any + } +} +export interface LoginCategory { + oidc?: boolean + cas?: boolean + ldap?: boolean + oauth2?: boolean + saml2?: boolean + qrcode?: boolean + lark?: boolean + dingtalk?: boolean + wecom?: boolean + larksuite?: boolean +} +const t = i18n.global.t +const flagArray = ['dingtalk', 'lark', 'larksuite'] +const urlArray = [ + 'https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.25/dingtalk.open.js', + 'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.26.js', + 'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.16.js', +] +export const loadClient = (category: LoginCategory) => { + const type = getQueryString('client') + const corpid = getQueryString('corpid') + if (type && !category[type as keyof LoginCategory]) { + ElMessageBox.confirm(t('login.platform_disable', [t(`threshold.${type}`)]), { + confirmButtonType: 'danger', + type: 'warning', + showCancelButton: false, + confirmButtonText: t('commons.refresh'), + cancelButtonText: t('dataset.cancel'), + autofocus: false, + showClose: false, + }) + .then(() => { + window.location.reload() + }) + .catch(() => {}) + return false + } + if ( + !type || + !flagArray.includes(type) || + !category[type as keyof LoginCategory] || + (type === 'dingtalk' && !corpid) + ) { + return false + } + const index = flagArray.indexOf(type) + const jsId = `fit2cloud-dataease-v2-platform-client-${type}` + const awaitMethod = loadScript(urlArray[index], jsId) + awaitMethod + .then(() => { + if (index === 0 && corpid) { + dingtalkClientRequest(corpid) + } + if (index === 1) { + larkClientRequest() + } + if (index === 2) { + larksuiteClientRequest() + } + }) + .catch(() => { + ElMessage.error('加载失败') + }) + return true +} + +const dingtalkClientRequest = (id?: string) => { + const dd = window['dd'] + if (id && dd?.runtime?.permission?.requestAuthCode) { + dd.runtime.permission.requestAuthCode({ + corpId: id, + onSuccess: function (result: any) { + const code = result.code + const state = `fit2cloud-dingtalk-client` + toUrl(`?code=${code}&state=${state}`) + }, + onFail: function (err: any) { + console.error(err) + }, + }) + } else { + ElMessage.error('not success') + } +} + +const larkClientRequest = async () => { + if (!window['tt']) { + ElMessage.error('load remote lark js error') + return + } + const res = await queryAppid('lark') + if (!res?.data?.appId) { + ElMessage.error('get appId error') + return + } + const appId = res.data.appId + const callRequestAuthCode = () => { + window['tt'].requestAuthCode({ + appId: appId, + success: (res: any) => { + const { code } = res + const state = `fit2cloud-lark-client` + toUrl(`?code=${code}&state=${state}`) + }, + fail: (error: any) => { + const { errno, errString } = error + ElMessage.error(`error code: ${errno}, error msg: ${errString}`) + }, + }) + } + if (window['tt'].requestAccess) { + window['tt'].requestAccess({ + appID: appId, + scopeList: [], + success: (res: any) => { + const { code } = res + const state = `fit2cloud-lark-client` + toUrl(`?code=${code}&state=${state}`) + }, + fail: (error: any) => { + const { errno, errString } = error + if (errno === 103) { + callRequestAuthCode() + } else { + ElMessage.error(`error code: ${errno}, error msg: ${errString}`) + } + }, + }) + } else { + callRequestAuthCode() + } +} + +const larksuiteClientRequest = async () => { + if (!window['tt'] || !window['h5sdk']) { + ElMessage.error('load remote lark js error') + return + } + const res = await queryAppid('larksuite') + if (!res?.data?.appId) { + ElMessage.error('get appId error') + return + } + const appId = res.data.appId + + window['h5sdk'].ready(() => { + window['tt'].requestAuthCode({ + appId: appId, + success(res: any) { + const code = res?.code || res + const state = `fit2cloud-larksuite-client` + toUrl(`?code=${code}&state=${state}`) + }, + fail(error: any) { + const { errno, errString } = error + ElMessage.error(`error code: ${errno}, error msg: ${errString}`) + }, + }) + }) +} + +const queryAppid = (type: string) => { + const url = `/${type}/qrinfo` + return request.get(url) +} + +const toUrl = (url: string) => { + const { origin, pathname } = window.location + window.location.href = origin + pathname + url +} diff --git a/frontend/src/views/system/authentication/CasEditor.vue b/frontend/src/views/system/authentication/CasEditor.vue new file mode 100644 index 00000000..4ffe0613 --- /dev/null +++ b/frontend/src/views/system/authentication/CasEditor.vue @@ -0,0 +1,273 @@ + + + + + + diff --git a/frontend/src/views/system/authentication/LdapEditor.vue b/frontend/src/views/system/authentication/LdapEditor.vue new file mode 100644 index 00000000..cf4e0713 --- /dev/null +++ b/frontend/src/views/system/authentication/LdapEditor.vue @@ -0,0 +1,288 @@ + + + + + + diff --git a/frontend/src/views/system/authentication/Oauth2Editor.vue b/frontend/src/views/system/authentication/Oauth2Editor.vue new file mode 100644 index 00000000..573ecfd8 --- /dev/null +++ b/frontend/src/views/system/authentication/Oauth2Editor.vue @@ -0,0 +1,354 @@ + + + + + + diff --git a/frontend/src/views/system/authentication/OidcEditor.vue b/frontend/src/views/system/authentication/OidcEditor.vue new file mode 100644 index 00000000..3f868a49 --- /dev/null +++ b/frontend/src/views/system/authentication/OidcEditor.vue @@ -0,0 +1,333 @@ + + + + + + diff --git a/frontend/src/views/system/authentication/SAML2Editor.vue b/frontend/src/views/system/authentication/SAML2Editor.vue new file mode 100644 index 00000000..9dca76e9 --- /dev/null +++ b/frontend/src/views/system/authentication/SAML2Editor.vue @@ -0,0 +1,259 @@ + + + + + + diff --git a/frontend/src/views/system/authentication/index.vue b/frontend/src/views/system/authentication/index.vue new file mode 100644 index 00000000..69573001 --- /dev/null +++ b/frontend/src/views/system/authentication/index.vue @@ -0,0 +1,254 @@ + + + +