Co-authored-by: Garfield Dai <dai.hai@foxmail.com>tags/0.11.2
| @@ -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 = () => { | |||
| </div> | |||
| )} | |||
| <div className='flex items-center flex-shrink-0'> | |||
| <LicenseNav /> | |||
| <EnvNav /> | |||
| {enableBilling && ( | |||
| <div className='mr-3 select-none'> | |||
| @@ -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 <div className='px-2 py-1 mr-4 rounded-full bg-util-colors-orange-orange-50 border-util-colors-orange-orange-100 system-xs-medium text-util-colors-orange-orange-600'> | |||
| {count <= 1 && <span>{t('common.license.expiring', { count })}</span>} | |||
| {count > 1 && <span>{t('common.license.expiring_plural', { count })}</span>} | |||
| </div> | |||
| } | |||
| if (systemFeatures.license.status === LicenseStatus.ACTIVE) { | |||
| return <div className='px-2 py-1 mr-4 rounded-md bg-util-colors-indigo-indigo-50 border-util-colors-indigo-indigo-100 system-xs-medium text-util-colors-indigo-indigo-600'> | |||
| Enterprise | |||
| </div> | |||
| } | |||
| return null | |||
| } | |||
| export default LicenseNav | |||
| @@ -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 = () => { | |||
| <Loading type='area' /> | |||
| </div> | |||
| } | |||
| if (systemFeatures.license?.status === LicenseStatus.LOST) { | |||
| return <div className='w-full mx-auto mt-8'> | |||
| <div className='bg-white'> | |||
| <div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2"> | |||
| <div className='flex items-center justify-center w-10 h-10 rounded-xl bg-components-card-bg shadow shadows-shadow-lg mb-2 relative'> | |||
| <RiContractLine className='w-5 h-5' /> | |||
| <RiErrorWarningFill className='absolute w-4 h-4 text-text-warning-secondary -top-1 -right-1' /> | |||
| </div> | |||
| <p className='system-sm-medium text-text-primary'>{t('login.licenseLost')}</p> | |||
| <p className='system-xs-regular text-text-tertiary mt-1'>{t('login.licenseLostTip')}</p> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| } | |||
| if (systemFeatures.license?.status === LicenseStatus.EXPIRED) { | |||
| return <div className='w-full mx-auto mt-8'> | |||
| <div className='bg-white'> | |||
| <div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2"> | |||
| <div className='flex items-center justify-center w-10 h-10 rounded-xl bg-components-card-bg shadow shadows-shadow-lg mb-2 relative'> | |||
| <RiContractLine className='w-5 h-5' /> | |||
| <RiErrorWarningFill className='absolute w-4 h-4 text-text-warning-secondary -top-1 -right-1' /> | |||
| </div> | |||
| <p className='system-sm-medium text-text-primary'>{t('login.licenseExpired')}</p> | |||
| <p className='system-xs-regular text-text-tertiary mt-1'>{t('login.licenseExpiredTip')}</p> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| } | |||
| if (systemFeatures.license?.status === LicenseStatus.INACTIVE) { | |||
| return <div className='w-full mx-auto mt-8'> | |||
| <div className='bg-white'> | |||
| <div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2"> | |||
| <div className='flex items-center justify-center w-10 h-10 rounded-xl bg-components-card-bg shadow shadows-shadow-lg mb-2 relative'> | |||
| <RiContractLine className='w-5 h-5' /> | |||
| <RiErrorWarningFill className='absolute w-4 h-4 text-text-warning-secondary -top-1 -right-1' /> | |||
| </div> | |||
| <p className='system-sm-medium text-text-primary'>{t('login.licenseInactive')}</p> | |||
| <p className='system-xs-regular text-text-tertiary mt-1'>{t('login.licenseInactiveTip')}</p> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| } | |||
| return ( | |||
| <> | |||
| @@ -144,7 +144,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => | |||
| theme, | |||
| setTheme: handleSetTheme, | |||
| apps: appList.data, | |||
| systemFeatures, | |||
| systemFeatures: { ...defaultSystemFeatures, ...systemFeatures }, | |||
| mutateApps, | |||
| userProfile, | |||
| mutateUserProfile, | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -591,6 +591,10 @@ const translation = { | |||
| created: '标签创建成功', | |||
| failed: '标签创建失败', | |||
| }, | |||
| license: { | |||
| expiring: '许可证还有 1 天到期', | |||
| expiring_plural: '许可证还有 {{count}} 天到期', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -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 | |||
| @@ -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 = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => { | |||
| return new Promise<T>((resolve, reject) => { | |||
| export const request = async<T>(url: string, options = {}, otherOptions?: IOtherOptions) => { | |||
| try { | |||
| const otherOptionsForBaseFetch = otherOptions || {} | |||
| baseFetch<T>(url, options, otherOptionsForBaseFetch).then(resolve).catch((errResp) => { | |||
| if (errResp?.status === 401) { | |||
| return refreshAccessTokenOrRelogin(TIME_OUT).then(() => { | |||
| baseFetch<T>(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() | |||
| } | |||
| const [err, resp] = await asyncRunSafe<T>(baseFetch(url, options, otherOptionsForBaseFetch)) | |||
| if (err === null) | |||
| return resp | |||
| const errResp: Response = err as any | |||
| if (errResp.status === 401) { | |||
| const [parseErr, errRespData] = await asyncRunSafe<ResponseError>(errResp.json()) | |||
| const loginUrl = `${globalThis.location.origin}/signin` | |||
| if (parseErr) { | |||
| globalThis.location.href = loginUrl | |||
| return Promise.reject(err) | |||
| } | |||
| // 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) | |||
| } | |||
| 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 | |||
| }) | |||
| }) | |||
| // refresh token | |||
| const [refreshErr] = await asyncRunSafe(refreshAccessTokenOrRelogin(TIME_OUT)) | |||
| if (refreshErr === null) | |||
| return baseFetch<T>(url, options, otherOptionsForBaseFetch) | |||
| if (location.pathname !== '/signin' || !IS_CE_EDITION) { | |||
| globalThis.location.href = loginUrl | |||
| return Promise.reject(err) | |||
| } | |||
| else { | |||
| reject(errResp) | |||
| 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 | |||
| @@ -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: '', | |||
| }, | |||
| } | |||
| @@ -8,10 +8,8 @@ export async function asyncRunSafe<T = any>(fn: Promise<T>): 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')] | |||
| } | |||
| } | |||