Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supports display license status #10408

Merged
merged 11 commits into from
Nov 15, 2024
2 changes: 2 additions & 0 deletions web/app/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -79,6 +80,7 @@ const Header = () => {
</div>
)}
<div className='flex items-center flex-shrink-0'>
<LicenseNav />
<EnvNav />
{enableBilling && (
<div className='mr-3 select-none'>
Expand Down
29 changes: 29 additions & 0 deletions web/app/components/header/license-env/index.tsx
Original file line number Diff line number Diff line change
@@ -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
46 changes: 44 additions & 2 deletions web/app/signin/normalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ 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'
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'

Expand Down Expand Up @@ -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 (
<>
Expand Down
2 changes: 1 addition & 1 deletion web/context/app-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
theme,
setTheme: handleSetTheme,
apps: appList.data,
systemFeatures,
systemFeatures: { ...defaultSystemFeatures, ...systemFeatures },
mutateApps,
userProfile,
mutateUserProfile,
Expand Down
4 changes: 4 additions & 0 deletions web/i18n/en-US/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions web/i18n/en-US/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions web/i18n/zh-Hans/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,10 @@ const translation = {
created: '标签创建成功',
failed: '标签创建失败',
},
license: {
expiring: '许可证还有 1 天到期',
expiring_plural: '许可证还有 {{count}} 天到期',
},
}

export default translation
6 changes: 6 additions & 0 deletions web/i18n/zh-Hans/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
114 changes: 69 additions & 45 deletions web/service/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions web/types/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 | ''
Expand All @@ -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 = {
Expand All @@ -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: '',
},
}
6 changes: 2 additions & 4 deletions web/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
}
}

Expand Down
Loading