From 1f87676d52d4c490962ba6d98e4d854113511b9e Mon Sep 17 00:00:00 2001 From: NFish Date: Fri, 15 Nov 2024 17:59:48 +0800 Subject: [PATCH] Supports display license status (#10408) Co-authored-by: Garfield Dai --- web/app/components/header/index.tsx | 2 + .../components/header/license-env/index.tsx | 29 +++++ web/app/signin/normalForm.tsx | 46 ++++++- web/context/app-context.tsx | 2 +- web/i18n/en-US/common.ts | 4 + web/i18n/en-US/login.ts | 6 + web/i18n/zh-Hans/common.ts | 4 + web/i18n/zh-Hans/login.ts | 6 + web/service/base.ts | 116 +++++++++++------- web/types/feature.ts | 19 +++ web/utils/index.ts | 6 +- 11 files changed, 187 insertions(+), 53 deletions(-) create mode 100644 web/app/components/header/license-env/index.tsx diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index 2b020b81e7..3757d552df 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -12,6 +12,7 @@ import EnvNav from './env-nav' import ExploreNav from './explore-nav' import ToolsNav from './tools-nav' import GithubStar from './github-star' +import LicenseNav from './license-env' import { WorkspaceProvider } from '@/context/workspace-context' import { useAppContext } from '@/context/app-context' import LogoSite from '@/app/components/base/logo/logo-site' @@ -79,6 +80,7 @@ const Header = () => { )}
+ {enableBilling && (
diff --git a/web/app/components/header/license-env/index.tsx b/web/app/components/header/license-env/index.tsx new file mode 100644 index 0000000000..800d86d2b8 --- /dev/null +++ b/web/app/components/header/license-env/index.tsx @@ -0,0 +1,29 @@ +'use client' + +import AppContext from '@/context/app-context' +import { LicenseStatus } from '@/types/feature' +import { useTranslation } from 'react-i18next' +import { useContextSelector } from 'use-context-selector' +import dayjs from 'dayjs' + +const LicenseNav = () => { + const { t } = useTranslation() + const systemFeatures = useContextSelector(AppContext, s => s.systemFeatures) + + if (systemFeatures.license?.status === LicenseStatus.EXPIRING) { + const expiredAt = systemFeatures.license?.expired_at + const count = dayjs(expiredAt).diff(dayjs(), 'days') + return
+ {count <= 1 && {t('common.license.expiring', { count })}} + {count > 1 && {t('common.license.expiring_plural', { count })}} +
+ } + if (systemFeatures.license.status === LicenseStatus.ACTIVE) { + return
+ Enterprise +
+ } + return null +} + +export default LicenseNav diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index f4f46c68ba..783d8ac507 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' -import { RiDoorLockLine } from '@remixicon/react' +import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react' import Loading from '../components/base/loading' import MailAndCodeAuth from './components/mail-and-code-auth' import MailAndPasswordAuth from './components/mail-and-password-auth' @@ -10,7 +10,7 @@ import SocialAuth from './components/social-auth' import SSOAuth from './components/sso-auth' import cn from '@/utils/classnames' import { getSystemFeatures, invitationCheck } from '@/service/common' -import { defaultSystemFeatures } from '@/types/feature' +import { LicenseStatus, defaultSystemFeatures } from '@/types/feature' import Toast from '@/app/components/base/toast' import { IS_CE_EDITION } from '@/config' @@ -83,6 +83,48 @@ const NormalForm = () => {
} + if (systemFeatures.license?.status === LicenseStatus.LOST) { + return
+
+
+
+ + +
+

{t('login.licenseLost')}

+

{t('login.licenseLostTip')}

+
+
+
+ } + if (systemFeatures.license?.status === LicenseStatus.EXPIRED) { + return
+
+
+
+ + +
+

{t('login.licenseExpired')}

+

{t('login.licenseExpiredTip')}

+
+
+
+ } + if (systemFeatures.license?.status === LicenseStatus.INACTIVE) { + return
+
+
+
+ + +
+

{t('login.licenseInactive')}

+

{t('login.licenseInactiveTip')}

+
+
+
+ } return ( <> diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 78ac1c9848..369fe5af19 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -144,7 +144,7 @@ export const AppContextProvider: FC = ({ children }) => theme, setTheme: handleSetTheme, apps: appList.data, - systemFeatures, + systemFeatures: { ...defaultSystemFeatures, ...systemFeatures }, mutateApps, userProfile, mutateUserProfile, diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 3390280e8a..f9d815be2c 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -591,6 +591,10 @@ const translation = { created: 'Tag created successfully', failed: 'Tag creation failed', }, + license: { + expiring: 'Expiring in one day', + expiring_plural: 'Expiring in {{count}} days', + }, } export default translation diff --git a/web/i18n/en-US/login.ts b/web/i18n/en-US/login.ts index b47d7bd69a..5ff7b80b4e 100644 --- a/web/i18n/en-US/login.ts +++ b/web/i18n/en-US/login.ts @@ -98,6 +98,12 @@ const translation = { back: 'Back', noLoginMethod: 'Authentication method not configured', noLoginMethodTip: 'Please contact the system admin to add an authentication method.', + licenseExpired: 'License Expired', + licenseExpiredTip: 'The Dify Enterprise license for your workspace has expired. Please contact your administrator to continue using Dify.', + licenseLost: 'License Lost', + licenseLostTip: 'Failed to connect Dify license server. Please contact your administrator to continue using Dify.', + licenseInactive: 'License Inactive', + licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', } export default translation diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 58d56a8331..207beaab8c 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -591,6 +591,10 @@ const translation = { created: '标签创建成功', failed: '标签创建失败', }, + license: { + expiring: '许可证还有 1 天到期', + expiring_plural: '许可证还有 {{count}} 天到期', + }, } export default translation diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index 40697701da..7f64c954b1 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -99,6 +99,12 @@ const translation = { back: '返回', noLoginMethod: '未配置身份认证方式', noLoginMethodTip: '请联系系统管理员添加身份认证方式', + licenseExpired: '许可证已过期', + licenseExpiredTip: '您所在空间的 Dify Enterprise 许可证已过期,请联系管理员以继续使用 Dify。', + licenseLost: '许可证丢失', + licenseLostTip: '无法连接 Dify 许可证服务器,请联系管理员以继续使用 Dify。', + licenseInactive: '许可证未激活', + licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', } export default translation diff --git a/web/service/base.ts b/web/service/base.ts index 6cb732c55d..9ee3033d8e 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -17,6 +17,7 @@ import type { WorkflowStartedResponse, } from '@/types/workflow' import { removeAccessToken } from '@/app/components/share/utils' +import { asyncRunSafe } from '@/utils' const TIME_OUT = 100000 const ContentType = { @@ -550,55 +551,78 @@ export const ssePost = ( } // base request -export const request = (url: string, options = {}, otherOptions?: IOtherOptions) => { - return new Promise((resolve, reject) => { +export const request = async(url: string, options = {}, otherOptions?: IOtherOptions) => { + try { const otherOptionsForBaseFetch = otherOptions || {} - baseFetch(url, options, otherOptionsForBaseFetch).then(resolve).catch((errResp) => { - if (errResp?.status === 401) { - return refreshAccessTokenOrRelogin(TIME_OUT).then(() => { - baseFetch(url, options, otherOptionsForBaseFetch).then(resolve).catch(reject) - }).catch(() => { - const { - isPublicAPI = false, - silent, - } = otherOptionsForBaseFetch - const bodyJson = errResp.json() - if (isPublicAPI) { - return bodyJson.then((data: ResponseError) => { - if (data.code === 'web_sso_auth_required') - requiredWebSSOLogin() - - if (data.code === 'unauthorized') { - removeAccessToken() - globalThis.location.reload() - } - - return Promise.reject(data) - }) - } - const loginUrl = `${globalThis.location.origin}/signin` - bodyJson.then((data: ResponseError) => { - if (data.code === 'init_validate_failed' && IS_CE_EDITION && !silent) - Toast.notify({ type: 'error', message: data.message, duration: 4000 }) - else if (data.code === 'not_init_validated' && IS_CE_EDITION) - globalThis.location.href = `${globalThis.location.origin}/init` - else if (data.code === 'not_setup' && IS_CE_EDITION) - globalThis.location.href = `${globalThis.location.origin}/install` - else if (location.pathname !== '/signin' || !IS_CE_EDITION) - globalThis.location.href = loginUrl - else if (!silent) - Toast.notify({ type: 'error', message: data.message }) - }).catch(() => { - // Handle any other errors - globalThis.location.href = loginUrl - }) - }) + const [err, resp] = await asyncRunSafe(baseFetch(url, options, otherOptionsForBaseFetch)) + if (err === null) + return resp + const errResp: Response = err as any + if (errResp.status === 401) { + const [parseErr, errRespData] = await asyncRunSafe(errResp.json()) + const loginUrl = `${globalThis.location.origin}/signin` + if (parseErr) { + globalThis.location.href = loginUrl + return Promise.reject(err) } - else { - reject(errResp) + // special code + const { code, message } = errRespData + // webapp sso + if (code === 'web_sso_auth_required') { + requiredWebSSOLogin() + return Promise.reject(err) } - }) - }) + if (code === 'unauthorized_and_force_logout') { + localStorage.removeItem('console_token') + localStorage.removeItem('refresh_token') + globalThis.location.reload() + return Promise.reject(err) + } + const { + isPublicAPI = false, + silent, + } = otherOptionsForBaseFetch + if (isPublicAPI && code === 'unauthorized') { + removeAccessToken() + globalThis.location.reload() + return Promise.reject(err) + } + if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) { + Toast.notify({ type: 'error', message, duration: 4000 }) + return Promise.reject(err) + } + if (code === 'not_init_validated' && IS_CE_EDITION) { + globalThis.location.href = `${globalThis.location.origin}/init` + return Promise.reject(err) + } + if (code === 'not_setup' && IS_CE_EDITION) { + globalThis.location.href = `${globalThis.location.origin}/install` + return Promise.reject(err) + } + + // refresh token + const [refreshErr] = await asyncRunSafe(refreshAccessTokenOrRelogin(TIME_OUT)) + if (refreshErr === null) + return baseFetch(url, options, otherOptionsForBaseFetch) + if (location.pathname !== '/signin' || !IS_CE_EDITION) { + globalThis.location.href = loginUrl + return Promise.reject(err) + } + if (!silent) { + Toast.notify({ type: 'error', message }) + return Promise.reject(err) + } + globalThis.location.href = loginUrl + return Promise.reject(err) + } + else { + return Promise.reject(err) + } + } + catch (error) { + console.error(error) + return Promise.reject(error) + } } // request methods diff --git a/web/types/feature.ts b/web/types/feature.ts index 0d9b2ec18d..47e8e1aad1 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -4,6 +4,20 @@ export enum SSOProtocol { OAuth2 = 'oauth2', } +export enum LicenseStatus { + NONE = 'none', + INACTIVE = 'inactive', + ACTIVE = 'active', + EXPIRING = 'expiring', + EXPIRED = 'expired', + LOST = 'lost', +} + +type License = { + status: LicenseStatus + expired_at: string | null +} + export type SystemFeatures = { sso_enforced_for_signin: boolean sso_enforced_for_signin_protocol: SSOProtocol | '' @@ -15,6 +29,7 @@ export type SystemFeatures = { enable_social_oauth_login: boolean is_allow_create_workspace: boolean is_allow_register: boolean + license: License } export const defaultSystemFeatures: SystemFeatures = { @@ -28,4 +43,8 @@ export const defaultSystemFeatures: SystemFeatures = { enable_social_oauth_login: false, is_allow_create_workspace: false, is_allow_register: false, + license: { + status: LicenseStatus.NONE, + expired_at: '', + }, } diff --git a/web/utils/index.ts b/web/utils/index.ts index 7aa6fef0a8..cabad6c35c 100644 --- a/web/utils/index.ts +++ b/web/utils/index.ts @@ -8,10 +8,8 @@ export async function asyncRunSafe(fn: Promise): Promise<[Error] | [ try { return [null, await fn] } - catch (e) { - if (e instanceof Error) - return [e] - return [new Error('unknown error')] + catch (e: any) { + return [e || new Error('unknown error')] } }