| import { useRouter } from 'next/navigation' | import { useRouter } from 'next/navigation' | ||||
| import { useCallback, useEffect, useMemo, useState } from 'react' | import { useCallback, useEffect, useMemo, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react' | |||||
| import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react' | |||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import type { App } from '@/types/app' | import type { App } from '@/types/app' | ||||
| import Confirm from '@/app/components/base/confirm' | import Confirm from '@/app/components/base/confirm' | ||||
| </div> | </div> | ||||
| <div className='flex h-5 w-5 shrink-0 items-center justify-center'> | <div className='flex h-5 w-5 shrink-0 items-center justify-center'> | ||||
| {app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.anyone')}> | {app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.anyone')}> | ||||
| <RiGlobalLine className='h-4 w-4 text-text-accent' /> | |||||
| <RiGlobalLine className='h-4 w-4 text-text-quaternary' /> | |||||
| </Tooltip>} | </Tooltip>} | ||||
| {app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}> | {app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}> | ||||
| <RiLockLine className='h-4 w-4 text-text-quaternary' /> | <RiLockLine className='h-4 w-4 text-text-quaternary' /> | ||||
| {app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.organization')}> | {app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.organization')}> | ||||
| <RiBuildingLine className='h-4 w-4 text-text-quaternary' /> | <RiBuildingLine className='h-4 w-4 text-text-quaternary' /> | ||||
| </Tooltip>} | </Tooltip>} | ||||
| {app.access_mode === AccessMode.EXTERNAL_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.external')}> | |||||
| <RiVerifiedBadgeLine className='h-4 w-4 text-text-quaternary' /> | |||||
| </Tooltip>} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'> | <div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'> |
| import React from 'react' | |||||
| 'use client' | |||||
| import React, { useEffect, useState } from 'react' | |||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import type { Metadata } from 'next' | |||||
| export const metadata: Metadata = { | |||||
| icons: 'data:,', // prevent browser from using default favicon | |||||
| } | |||||
| import { usePathname, useSearchParams } from 'next/navigation' | |||||
| import Loading from '../components/base/loading' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import { AccessMode } from '@/models/access-control' | |||||
| import { getAppAccessModeByAppCode } from '@/service/share' | |||||
| const Layout: FC<{ | const Layout: FC<{ | ||||
| children: React.ReactNode | children: React.ReactNode | ||||
| }> = ({ children }) => { | }> = ({ children }) => { | ||||
| const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending) | |||||
| const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode) | |||||
| const pathname = usePathname() | |||||
| const searchParams = useSearchParams() | |||||
| const redirectUrl = searchParams.get('redirect_url') | |||||
| const [isLoading, setIsLoading] = useState(true) | |||||
| useEffect(() => { | |||||
| (async () => { | |||||
| let appCode: string | null = null | |||||
| if (redirectUrl) | |||||
| appCode = redirectUrl?.split('/').pop() || null | |||||
| else | |||||
| appCode = pathname.split('/').pop() || null | |||||
| if (!appCode) | |||||
| return | |||||
| setIsLoading(true) | |||||
| const ret = await getAppAccessModeByAppCode(appCode) | |||||
| setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC) | |||||
| setIsLoading(false) | |||||
| })() | |||||
| }, [pathname, redirectUrl, setWebAppAccessMode]) | |||||
| if (isLoading || isGlobalPending) { | |||||
| return <div className='flex h-full w-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| return ( | return ( | ||||
| <div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]"> | <div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]"> | ||||
| {children} | {children} |
| 'use client' | |||||
| import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useState } from 'react' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import { useContext } from 'use-context-selector' | |||||
| import Countdown from '@/app/components/signin/countdown' | |||||
| import Button from '@/app/components/base/button' | |||||
| import Input from '@/app/components/base/input' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common' | |||||
| import I18NContext from '@/context/i18n' | |||||
| export default function CheckCode() { | |||||
| const { t } = useTranslation() | |||||
| const router = useRouter() | |||||
| const searchParams = useSearchParams() | |||||
| const email = decodeURIComponent(searchParams.get('email') as string) | |||||
| const token = decodeURIComponent(searchParams.get('token') as string) | |||||
| const [code, setVerifyCode] = useState('') | |||||
| const [loading, setIsLoading] = useState(false) | |||||
| const { locale } = useContext(I18NContext) | |||||
| const verify = async () => { | |||||
| try { | |||||
| if (!code.trim()) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.checkCode.emptyCode'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| if (!/\d{6}/.test(code)) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.checkCode.invalidCode'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| setIsLoading(true) | |||||
| const ret = await verifyWebAppResetPasswordCode({ email, code, token }) | |||||
| if (ret.is_valid) { | |||||
| const params = new URLSearchParams(searchParams) | |||||
| params.set('token', encodeURIComponent(ret.token)) | |||||
| router.push(`/webapp-reset-password/set-password?${params.toString()}`) | |||||
| } | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| finally { | |||||
| setIsLoading(false) | |||||
| } | |||||
| } | |||||
| const resendCode = async () => { | |||||
| try { | |||||
| const res = await sendWebAppResetPasswordCode(email, locale) | |||||
| if (res.result === 'success') { | |||||
| const params = new URLSearchParams(searchParams) | |||||
| params.set('token', encodeURIComponent(res.data)) | |||||
| router.replace(`/webapp-reset-password/check-code?${params.toString()}`) | |||||
| } | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| } | |||||
| return <div className='flex flex-col gap-3'> | |||||
| <div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge text-text-accent-light-mode-only shadow-lg'> | |||||
| <RiMailSendFill className='h-6 w-6 text-2xl' /> | |||||
| </div> | |||||
| <div className='pb-4 pt-2'> | |||||
| <h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2> | |||||
| <p className='body-md-regular mt-2 text-text-secondary'> | |||||
| <span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email }) as string }}></span> | |||||
| <br /> | |||||
| {t('login.checkCode.validTime')} | |||||
| </p> | |||||
| </div> | |||||
| <form action=""> | |||||
| <input type='text' className='hidden' /> | |||||
| <label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label> | |||||
| <Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} /> | |||||
| <Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button> | |||||
| <Countdown onResend={resendCode} /> | |||||
| </form> | |||||
| <div className='py-2'> | |||||
| <div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div> | |||||
| </div> | |||||
| <div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'> | |||||
| <div className='bg-background-default-dimm inline-block rounded-full p-1'> | |||||
| <RiArrowLeftLine size={12} /> | |||||
| </div> | |||||
| <span className='system-xs-regular ml-2'>{t('login.back')}</span> | |||||
| </div> | |||||
| </div> | |||||
| } |
| 'use client' | |||||
| import Header from '@/app/signin/_header' | |||||
| import cn from '@/utils/classnames' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| export default function SignInLayout({ children }: any) { | |||||
| const { systemFeatures } = useGlobalPublicStore() | |||||
| return <> | |||||
| <div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}> | |||||
| <div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}> | |||||
| <Header /> | |||||
| <div className={ | |||||
| cn( | |||||
| 'flex w-full grow flex-col items-center justify-center', | |||||
| 'px-6', | |||||
| 'md:px-[108px]', | |||||
| ) | |||||
| }> | |||||
| <div className='flex w-[400px] flex-col'> | |||||
| {children} | |||||
| </div> | |||||
| </div> | |||||
| {!systemFeatures.branding.enabled && <div className='system-xs-regular px-8 py-6 text-text-tertiary'> | |||||
| © {new Date().getFullYear()} LangGenius, Inc. All rights reserved. | |||||
| </div>} | |||||
| </div> | |||||
| </div> | |||||
| </> | |||||
| } |
| 'use client' | |||||
| import Link from 'next/link' | |||||
| import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useState } from 'react' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import { useContext } from 'use-context-selector' | |||||
| import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' | |||||
| import { emailRegex } from '@/config' | |||||
| import Button from '@/app/components/base/button' | |||||
| import Input from '@/app/components/base/input' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import { sendResetPasswordCode } from '@/service/common' | |||||
| import I18NContext from '@/context/i18n' | |||||
| import { noop } from 'lodash-es' | |||||
| import useDocumentTitle from '@/hooks/use-document-title' | |||||
| export default function CheckCode() { | |||||
| const { t } = useTranslation() | |||||
| useDocumentTitle('') | |||||
| const searchParams = useSearchParams() | |||||
| const router = useRouter() | |||||
| const [email, setEmail] = useState('') | |||||
| const [loading, setIsLoading] = useState(false) | |||||
| const { locale } = useContext(I18NContext) | |||||
| const handleGetEMailVerificationCode = async () => { | |||||
| try { | |||||
| if (!email) { | |||||
| Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) | |||||
| return | |||||
| } | |||||
| if (!emailRegex.test(email)) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.error.emailInValid'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| setIsLoading(true) | |||||
| const res = await sendResetPasswordCode(email, locale) | |||||
| if (res.result === 'success') { | |||||
| localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) | |||||
| const params = new URLSearchParams(searchParams) | |||||
| params.set('token', encodeURIComponent(res.data)) | |||||
| params.set('email', encodeURIComponent(email)) | |||||
| router.push(`/webapp-reset-password/check-code?${params.toString()}`) | |||||
| } | |||||
| else if (res.code === 'account_not_found') { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.error.registrationNotAllowed'), | |||||
| }) | |||||
| } | |||||
| else { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: res.data, | |||||
| }) | |||||
| } | |||||
| } | |||||
| catch (error) { | |||||
| console.error(error) | |||||
| } | |||||
| finally { | |||||
| setIsLoading(false) | |||||
| } | |||||
| } | |||||
| return <div className='flex flex-col gap-3'> | |||||
| <div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'> | |||||
| <RiLockPasswordLine className='h-6 w-6 text-2xl text-text-accent-light-mode-only' /> | |||||
| </div> | |||||
| <div className='pb-4 pt-2'> | |||||
| <h2 className='title-4xl-semi-bold text-text-primary'>{t('login.resetPassword')}</h2> | |||||
| <p className='body-md-regular mt-2 text-text-secondary'> | |||||
| {t('login.resetPasswordDesc')} | |||||
| </p> | |||||
| </div> | |||||
| <form onSubmit={noop}> | |||||
| <input type='text' className='hidden' /> | |||||
| <div className='mb-2'> | |||||
| <label htmlFor="email" className='system-md-semibold my-2 text-text-secondary'>{t('login.email')}</label> | |||||
| <div className='mt-1'> | |||||
| <Input id='email' type="email" disabled={loading} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} /> | |||||
| </div> | |||||
| <div className='mt-3'> | |||||
| <Button loading={loading} disabled={loading} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.sendVerificationCode')}</Button> | |||||
| </div> | |||||
| </div> | |||||
| </form> | |||||
| <div className='py-2'> | |||||
| <div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div> | |||||
| </div> | |||||
| <Link href={`/webapp-signin?${searchParams.toString()}`} className='flex h-9 items-center justify-center text-text-tertiary hover:text-text-primary'> | |||||
| <div className='inline-block rounded-full bg-background-default-dimmed p-1'> | |||||
| <RiArrowLeftLine size={12} /> | |||||
| </div> | |||||
| <span className='system-xs-regular ml-2'>{t('login.backToLogin')}</span> | |||||
| </Link> | |||||
| </div> | |||||
| } |
| 'use client' | |||||
| import { useCallback, useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import cn from 'classnames' | |||||
| import { RiCheckboxCircleFill } from '@remixicon/react' | |||||
| import { useCountDown } from 'ahooks' | |||||
| import Button from '@/app/components/base/button' | |||||
| import { changeWebAppPasswordWithToken } from '@/service/common' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import Input from '@/app/components/base/input' | |||||
| const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ | |||||
| const ChangePasswordForm = () => { | |||||
| const { t } = useTranslation() | |||||
| const router = useRouter() | |||||
| const searchParams = useSearchParams() | |||||
| const token = decodeURIComponent(searchParams.get('token') || '') | |||||
| const [password, setPassword] = useState('') | |||||
| const [confirmPassword, setConfirmPassword] = useState('') | |||||
| const [showSuccess, setShowSuccess] = useState(false) | |||||
| const [showPassword, setShowPassword] = useState(false) | |||||
| const [showConfirmPassword, setShowConfirmPassword] = useState(false) | |||||
| const showErrorMessage = useCallback((message: string) => { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message, | |||||
| }) | |||||
| }, []) | |||||
| const getSignInUrl = () => { | |||||
| return `/webapp-signin?redirect_url=${searchParams.get('redirect_url') || ''}` | |||||
| } | |||||
| const AUTO_REDIRECT_TIME = 5000 | |||||
| const [leftTime, setLeftTime] = useState<number | undefined>(undefined) | |||||
| const [countdown] = useCountDown({ | |||||
| leftTime, | |||||
| onEnd: () => { | |||||
| router.replace(getSignInUrl()) | |||||
| }, | |||||
| }) | |||||
| const valid = useCallback(() => { | |||||
| if (!password.trim()) { | |||||
| showErrorMessage(t('login.error.passwordEmpty')) | |||||
| return false | |||||
| } | |||||
| if (!validPassword.test(password)) { | |||||
| showErrorMessage(t('login.error.passwordInvalid')) | |||||
| return false | |||||
| } | |||||
| if (password !== confirmPassword) { | |||||
| showErrorMessage(t('common.account.notEqual')) | |||||
| return false | |||||
| } | |||||
| return true | |||||
| }, [password, confirmPassword, showErrorMessage, t]) | |||||
| const handleChangePassword = useCallback(async () => { | |||||
| if (!valid()) | |||||
| return | |||||
| try { | |||||
| await changeWebAppPasswordWithToken({ | |||||
| url: '/forgot-password/resets', | |||||
| body: { | |||||
| token, | |||||
| new_password: password, | |||||
| password_confirm: confirmPassword, | |||||
| }, | |||||
| }) | |||||
| setShowSuccess(true) | |||||
| setLeftTime(AUTO_REDIRECT_TIME) | |||||
| } | |||||
| catch (error) { | |||||
| console.error(error) | |||||
| } | |||||
| }, [password, token, valid, confirmPassword]) | |||||
| return ( | |||||
| <div className={ | |||||
| cn( | |||||
| 'flex w-full grow flex-col items-center justify-center', | |||||
| 'px-6', | |||||
| 'md:px-[108px]', | |||||
| ) | |||||
| }> | |||||
| {!showSuccess && ( | |||||
| <div className='flex flex-col md:w-[400px]'> | |||||
| <div className="mx-auto w-full"> | |||||
| <h2 className="title-4xl-semi-bold text-text-primary"> | |||||
| {t('login.changePassword')} | |||||
| </h2> | |||||
| <p className='body-md-regular mt-2 text-text-secondary'> | |||||
| {t('login.changePasswordTip')} | |||||
| </p> | |||||
| </div> | |||||
| <div className="mx-auto mt-6 w-full"> | |||||
| <div className="bg-white"> | |||||
| {/* Password */} | |||||
| <div className='mb-5'> | |||||
| <label htmlFor="password" className="system-md-semibold my-2 text-text-secondary"> | |||||
| {t('common.account.newPassword')} | |||||
| </label> | |||||
| <div className='relative mt-1'> | |||||
| <Input | |||||
| id="password" type={showPassword ? 'text' : 'password'} | |||||
| value={password} | |||||
| onChange={e => setPassword(e.target.value)} | |||||
| placeholder={t('login.passwordPlaceholder') || ''} | |||||
| /> | |||||
| <div className="absolute inset-y-0 right-0 flex items-center"> | |||||
| <Button | |||||
| type="button" | |||||
| variant='ghost' | |||||
| onClick={() => setShowPassword(!showPassword)} | |||||
| > | |||||
| {showPassword ? '👀' : '😝'} | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| <div className='body-xs-regular mt-1 text-text-secondary'>{t('login.error.passwordInvalid')}</div> | |||||
| </div> | |||||
| {/* Confirm Password */} | |||||
| <div className='mb-5'> | |||||
| <label htmlFor="confirmPassword" className="system-md-semibold my-2 text-text-secondary"> | |||||
| {t('common.account.confirmPassword')} | |||||
| </label> | |||||
| <div className='relative mt-1'> | |||||
| <Input | |||||
| id="confirmPassword" | |||||
| type={showConfirmPassword ? 'text' : 'password'} | |||||
| value={confirmPassword} | |||||
| onChange={e => setConfirmPassword(e.target.value)} | |||||
| placeholder={t('login.confirmPasswordPlaceholder') || ''} | |||||
| /> | |||||
| <div className="absolute inset-y-0 right-0 flex items-center"> | |||||
| <Button | |||||
| type="button" | |||||
| variant='ghost' | |||||
| onClick={() => setShowConfirmPassword(!showConfirmPassword)} | |||||
| > | |||||
| {showConfirmPassword ? '👀' : '😝'} | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div> | |||||
| <Button | |||||
| variant='primary' | |||||
| className='w-full' | |||||
| onClick={handleChangePassword} | |||||
| > | |||||
| {t('login.changePasswordBtn')} | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| {showSuccess && ( | |||||
| <div className="flex flex-col md:w-[400px]"> | |||||
| <div className="mx-auto w-full"> | |||||
| <div className="mb-3 flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle font-bold shadow-lg"> | |||||
| <RiCheckboxCircleFill className='h-6 w-6 text-text-success' /> | |||||
| </div> | |||||
| <h2 className="title-4xl-semi-bold text-text-primary"> | |||||
| {t('login.passwordChangedTip')} | |||||
| </h2> | |||||
| </div> | |||||
| <div className="mx-auto mt-6 w-full"> | |||||
| <Button variant='primary' className='w-full' onClick={() => { | |||||
| setLeftTime(undefined) | |||||
| router.replace(getSignInUrl()) | |||||
| }}>{t('login.passwordChanged')} ({Math.round(countdown / 1000)}) </Button> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default ChangePasswordForm |
| 'use client' | |||||
| import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useCallback, useState } from 'react' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import { useContext } from 'use-context-selector' | |||||
| import Countdown from '@/app/components/signin/countdown' | |||||
| import Button from '@/app/components/base/button' | |||||
| import Input from '@/app/components/base/input' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common' | |||||
| import I18NContext from '@/context/i18n' | |||||
| import { setAccessToken } from '@/app/components/share/utils' | |||||
| import { fetchAccessToken } from '@/service/share' | |||||
| export default function CheckCode() { | |||||
| const { t } = useTranslation() | |||||
| const router = useRouter() | |||||
| const searchParams = useSearchParams() | |||||
| const email = decodeURIComponent(searchParams.get('email') as string) | |||||
| const token = decodeURIComponent(searchParams.get('token') as string) | |||||
| const [code, setVerifyCode] = useState('') | |||||
| const [loading, setIsLoading] = useState(false) | |||||
| const { locale } = useContext(I18NContext) | |||||
| const redirectUrl = searchParams.get('redirect_url') | |||||
| const getAppCodeFromRedirectUrl = useCallback(() => { | |||||
| const appCode = redirectUrl?.split('/').pop() | |||||
| if (!appCode) | |||||
| return null | |||||
| return appCode | |||||
| }, [redirectUrl]) | |||||
| const verify = async () => { | |||||
| try { | |||||
| const appCode = getAppCodeFromRedirectUrl() | |||||
| if (!code.trim()) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.checkCode.emptyCode'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| if (!/\d{6}/.test(code)) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.checkCode.invalidCode'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| if (!redirectUrl || !appCode) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.error.redirectUrlMissing'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| setIsLoading(true) | |||||
| const ret = await webAppEmailLoginWithCode({ email, code, token }) | |||||
| if (ret.result === 'success') { | |||||
| localStorage.setItem('webapp_access_token', ret.data.access_token) | |||||
| const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: ret.data.access_token }) | |||||
| await setAccessToken(appCode, tokenResp.access_token) | |||||
| router.replace(redirectUrl) | |||||
| } | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| finally { | |||||
| setIsLoading(false) | |||||
| } | |||||
| } | |||||
| const resendCode = async () => { | |||||
| try { | |||||
| const ret = await sendWebAppEMailLoginCode(email, locale) | |||||
| if (ret.result === 'success') { | |||||
| const params = new URLSearchParams(searchParams) | |||||
| params.set('token', encodeURIComponent(ret.data)) | |||||
| router.replace(`/webapp-signin/check-code?${params.toString()}`) | |||||
| } | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| } | |||||
| return <div className='flex w-[400px] flex-col gap-3'> | |||||
| <div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'> | |||||
| <RiMailSendFill className='h-6 w-6 text-2xl text-text-accent-light-mode-only' /> | |||||
| </div> | |||||
| <div className='pb-4 pt-2'> | |||||
| <h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2> | |||||
| <p className='body-md-regular mt-2 text-text-secondary'> | |||||
| <span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email }) as string }}></span> | |||||
| <br /> | |||||
| {t('login.checkCode.validTime')} | |||||
| </p> | |||||
| </div> | |||||
| <form action=""> | |||||
| <label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label> | |||||
| <Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} /> | |||||
| <Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button> | |||||
| <Countdown onResend={resendCode} /> | |||||
| </form> | |||||
| <div className='py-2'> | |||||
| <div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div> | |||||
| </div> | |||||
| <div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'> | |||||
| <div className='bg-background-default-dimm inline-block rounded-full p-1'> | |||||
| <RiArrowLeftLine size={12} /> | |||||
| </div> | |||||
| <span className='system-xs-regular ml-2'>{t('login.back')}</span> | |||||
| </div> | |||||
| </div> | |||||
| } |
| 'use client' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import React, { useCallback, useEffect } from 'react' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import { SSOProtocol } from '@/types/feature' | |||||
| import Loading from '@/app/components/base/loading' | |||||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||||
| const ExternalMemberSSOAuth = () => { | |||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||||
| const searchParams = useSearchParams() | |||||
| const router = useRouter() | |||||
| const redirectUrl = searchParams.get('redirect_url') | |||||
| const showErrorToast = (message: string) => { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message, | |||||
| }) | |||||
| } | |||||
| const getAppCodeFromRedirectUrl = useCallback(() => { | |||||
| const appCode = redirectUrl?.split('/').pop() | |||||
| if (!appCode) | |||||
| return null | |||||
| return appCode | |||||
| }, [redirectUrl]) | |||||
| const handleSSOLogin = useCallback(async () => { | |||||
| const appCode = getAppCodeFromRedirectUrl() | |||||
| if (!appCode || !redirectUrl) { | |||||
| showErrorToast('redirect url or app code is invalid.') | |||||
| return | |||||
| } | |||||
| switch (systemFeatures.webapp_auth.sso_config.protocol) { | |||||
| case SSOProtocol.SAML: { | |||||
| const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl) | |||||
| router.push(samlRes.url) | |||||
| break | |||||
| } | |||||
| case SSOProtocol.OIDC: { | |||||
| const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl) | |||||
| router.push(oidcRes.url) | |||||
| break | |||||
| } | |||||
| case SSOProtocol.OAuth2: { | |||||
| const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl) | |||||
| router.push(oauth2Res.url) | |||||
| break | |||||
| } | |||||
| case '': | |||||
| break | |||||
| default: | |||||
| showErrorToast('SSO protocol is not supported.') | |||||
| } | |||||
| }, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol]) | |||||
| useEffect(() => { | |||||
| handleSSOLogin() | |||||
| }, [handleSSOLogin]) | |||||
| if (!systemFeatures.webapp_auth.sso_config.protocol) { | |||||
| return <div className="flex h-full items-center justify-center"> | |||||
| <AppUnavailable code={403} unknownReason='sso protocol is invalid.' /> | |||||
| </div> | |||||
| } | |||||
| return ( | |||||
| <div className="flex h-full items-center justify-center"> | |||||
| <Loading /> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default React.memo(ExternalMemberSSOAuth) |
| import { useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import { useContext } from 'use-context-selector' | |||||
| import Input from '@/app/components/base/input' | |||||
| import Button from '@/app/components/base/button' | |||||
| import { emailRegex } from '@/config' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import { sendWebAppEMailLoginCode } from '@/service/common' | |||||
| import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' | |||||
| import I18NContext from '@/context/i18n' | |||||
| import { noop } from 'lodash-es' | |||||
| export default function MailAndCodeAuth() { | |||||
| const { t } = useTranslation() | |||||
| const router = useRouter() | |||||
| const searchParams = useSearchParams() | |||||
| const emailFromLink = decodeURIComponent(searchParams.get('email') || '') | |||||
| const [email, setEmail] = useState(emailFromLink) | |||||
| const [loading, setIsLoading] = useState(false) | |||||
| const { locale } = useContext(I18NContext) | |||||
| const handleGetEMailVerificationCode = async () => { | |||||
| try { | |||||
| if (!email) { | |||||
| Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) | |||||
| return | |||||
| } | |||||
| if (!emailRegex.test(email)) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.error.emailInValid'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| setIsLoading(true) | |||||
| const ret = await sendWebAppEMailLoginCode(email, locale) | |||||
| if (ret.result === 'success') { | |||||
| localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) | |||||
| const params = new URLSearchParams(searchParams) | |||||
| params.set('email', encodeURIComponent(email)) | |||||
| params.set('token', encodeURIComponent(ret.data)) | |||||
| router.push(`/webapp-signin/check-code?${params.toString()}`) | |||||
| } | |||||
| } | |||||
| catch (error) { | |||||
| console.error(error) | |||||
| } | |||||
| finally { | |||||
| setIsLoading(false) | |||||
| } | |||||
| } | |||||
| return (<form onSubmit={noop}> | |||||
| <input type='text' className='hidden' /> | |||||
| <div className='mb-2'> | |||||
| <label htmlFor="email" className='system-md-semibold my-2 text-text-secondary'>{t('login.email')}</label> | |||||
| <div className='mt-1'> | |||||
| <Input id='email' type="email" value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} /> | |||||
| </div> | |||||
| <div className='mt-3'> | |||||
| <Button loading={loading} disabled={loading || !email} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.continueWithCode')}</Button> | |||||
| </div> | |||||
| </div> | |||||
| </form> | |||||
| ) | |||||
| } |
| import Link from 'next/link' | |||||
| import { useCallback, useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import { useContext } from 'use-context-selector' | |||||
| import Button from '@/app/components/base/button' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import { emailRegex } from '@/config' | |||||
| import { webAppLogin } from '@/service/common' | |||||
| import Input from '@/app/components/base/input' | |||||
| import I18NContext from '@/context/i18n' | |||||
| import { noop } from 'lodash-es' | |||||
| import { setAccessToken } from '@/app/components/share/utils' | |||||
| import { fetchAccessToken } from '@/service/share' | |||||
| type MailAndPasswordAuthProps = { | |||||
| isEmailSetup: boolean | |||||
| } | |||||
| const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ | |||||
| export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) { | |||||
| const { t } = useTranslation() | |||||
| const { locale } = useContext(I18NContext) | |||||
| const router = useRouter() | |||||
| const searchParams = useSearchParams() | |||||
| const [showPassword, setShowPassword] = useState(false) | |||||
| const emailFromLink = decodeURIComponent(searchParams.get('email') || '') | |||||
| const [email, setEmail] = useState(emailFromLink) | |||||
| const [password, setPassword] = useState('') | |||||
| const [isLoading, setIsLoading] = useState(false) | |||||
| const redirectUrl = searchParams.get('redirect_url') | |||||
| const getAppCodeFromRedirectUrl = useCallback(() => { | |||||
| const appCode = redirectUrl?.split('/').pop() | |||||
| if (!appCode) | |||||
| return null | |||||
| return appCode | |||||
| }, [redirectUrl]) | |||||
| const handleEmailPasswordLogin = async () => { | |||||
| const appCode = getAppCodeFromRedirectUrl() | |||||
| if (!email) { | |||||
| Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) | |||||
| return | |||||
| } | |||||
| if (!emailRegex.test(email)) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.error.emailInValid'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| if (!password?.trim()) { | |||||
| Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') }) | |||||
| return | |||||
| } | |||||
| if (!passwordRegex.test(password)) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.error.passwordInvalid'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| if (!redirectUrl || !appCode) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('login.error.redirectUrlMissing'), | |||||
| }) | |||||
| return | |||||
| } | |||||
| try { | |||||
| setIsLoading(true) | |||||
| const loginData: Record<string, any> = { | |||||
| email, | |||||
| password, | |||||
| language: locale, | |||||
| remember_me: true, | |||||
| } | |||||
| const res = await webAppLogin({ | |||||
| url: '/login', | |||||
| body: loginData, | |||||
| }) | |||||
| if (res.result === 'success') { | |||||
| localStorage.setItem('webapp_access_token', res.data.access_token) | |||||
| const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: res.data.access_token }) | |||||
| await setAccessToken(appCode, tokenResp.access_token) | |||||
| router.replace(redirectUrl) | |||||
| } | |||||
| else { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: res.data, | |||||
| }) | |||||
| } | |||||
| } | |||||
| finally { | |||||
| setIsLoading(false) | |||||
| } | |||||
| } | |||||
| return <form onSubmit={noop}> | |||||
| <div className='mb-3'> | |||||
| <label htmlFor="email" className="system-md-semibold my-2 text-text-secondary"> | |||||
| {t('login.email')} | |||||
| </label> | |||||
| <div className="mt-1"> | |||||
| <Input | |||||
| value={email} | |||||
| onChange={e => setEmail(e.target.value)} | |||||
| id="email" | |||||
| type="email" | |||||
| autoComplete="email" | |||||
| placeholder={t('login.emailPlaceholder') || ''} | |||||
| tabIndex={1} | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| <div className='mb-3'> | |||||
| <label htmlFor="password" className="my-2 flex items-center justify-between"> | |||||
| <span className='system-md-semibold text-text-secondary'>{t('login.password')}</span> | |||||
| <Link | |||||
| href={`/webapp-reset-password?${searchParams.toString()}`} | |||||
| className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`} | |||||
| tabIndex={isEmailSetup ? 0 : -1} | |||||
| aria-disabled={!isEmailSetup} | |||||
| > | |||||
| {t('login.forget')} | |||||
| </Link> | |||||
| </label> | |||||
| <div className="relative mt-1"> | |||||
| <Input | |||||
| id="password" | |||||
| value={password} | |||||
| onChange={e => setPassword(e.target.value)} | |||||
| onKeyDown={(e) => { | |||||
| if (e.key === 'Enter') | |||||
| handleEmailPasswordLogin() | |||||
| }} | |||||
| type={showPassword ? 'text' : 'password'} | |||||
| autoComplete="current-password" | |||||
| placeholder={t('login.passwordPlaceholder') || ''} | |||||
| tabIndex={2} | |||||
| /> | |||||
| <div className="absolute inset-y-0 right-0 flex items-center"> | |||||
| <Button | |||||
| type="button" | |||||
| variant='ghost' | |||||
| onClick={() => setShowPassword(!showPassword)} | |||||
| > | |||||
| {showPassword ? '👀' : '😝'} | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div className='mb-2'> | |||||
| <Button | |||||
| tabIndex={2} | |||||
| variant='primary' | |||||
| onClick={handleEmailPasswordLogin} | |||||
| disabled={isLoading || !email || !password} | |||||
| className="w-full" | |||||
| >{t('login.signBtn')}</Button> | |||||
| </div> | |||||
| </form> | |||||
| } |
| 'use client' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import type { FC } from 'react' | |||||
| import { useCallback } from 'react' | |||||
| import { useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import Button from '@/app/components/base/button' | |||||
| import { SSOProtocol } from '@/types/feature' | |||||
| import { fetchMembersOAuth2SSOUrl, fetchMembersOIDCSSOUrl, fetchMembersSAMLSSOUrl } from '@/service/share' | |||||
| type SSOAuthProps = { | |||||
| protocol: SSOProtocol | '' | |||||
| } | |||||
| const SSOAuth: FC<SSOAuthProps> = ({ | |||||
| protocol, | |||||
| }) => { | |||||
| const router = useRouter() | |||||
| const { t } = useTranslation() | |||||
| const searchParams = useSearchParams() | |||||
| const redirectUrl = searchParams.get('redirect_url') | |||||
| const getAppCodeFromRedirectUrl = useCallback(() => { | |||||
| const appCode = redirectUrl?.split('/').pop() | |||||
| if (!appCode) | |||||
| return null | |||||
| return appCode | |||||
| }, [redirectUrl]) | |||||
| const [isLoading, setIsLoading] = useState(false) | |||||
| const handleSSOLogin = () => { | |||||
| const appCode = getAppCodeFromRedirectUrl() | |||||
| if (!redirectUrl || !appCode) { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: 'invalid redirect URL or app code', | |||||
| }) | |||||
| return | |||||
| } | |||||
| setIsLoading(true) | |||||
| if (protocol === SSOProtocol.SAML) { | |||||
| fetchMembersSAMLSSOUrl(appCode, redirectUrl).then((res) => { | |||||
| router.push(res.url) | |||||
| }).finally(() => { | |||||
| setIsLoading(false) | |||||
| }) | |||||
| } | |||||
| else if (protocol === SSOProtocol.OIDC) { | |||||
| fetchMembersOIDCSSOUrl(appCode, redirectUrl).then((res) => { | |||||
| router.push(res.url) | |||||
| }).finally(() => { | |||||
| setIsLoading(false) | |||||
| }) | |||||
| } | |||||
| else if (protocol === SSOProtocol.OAuth2) { | |||||
| fetchMembersOAuth2SSOUrl(appCode, redirectUrl).then((res) => { | |||||
| router.push(res.url) | |||||
| }).finally(() => { | |||||
| setIsLoading(false) | |||||
| }) | |||||
| } | |||||
| else { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: 'invalid SSO protocol', | |||||
| }) | |||||
| setIsLoading(false) | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <Button | |||||
| tabIndex={0} | |||||
| onClick={() => { handleSSOLogin() }} | |||||
| disabled={isLoading} | |||||
| className="w-full" | |||||
| > | |||||
| <Lock01 className='mr-2 h-5 w-5 text-text-accent-light-mode-only' /> | |||||
| <span className="truncate">{t('login.withSSO')}</span> | |||||
| </Button> | |||||
| ) | |||||
| } | |||||
| export default SSOAuth |
| 'use client' | |||||
| import cn from '@/utils/classnames' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import useDocumentTitle from '@/hooks/use-document-title' | |||||
| export default function SignInLayout({ children }: any) { | |||||
| const { systemFeatures } = useGlobalPublicStore() | |||||
| useDocumentTitle('') | |||||
| return <> | |||||
| <div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}> | |||||
| <div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}> | |||||
| {/* <Header /> */} | |||||
| <div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}> | |||||
| <div className='flex justify-center md:w-[440px] lg:w-[600px]'> | |||||
| {children} | |||||
| </div> | |||||
| </div> | |||||
| {systemFeatures.branding.enabled === false && <div className='system-xs-regular px-8 py-6 text-text-tertiary'> | |||||
| © {new Date().getFullYear()} LangGenius, Inc. All rights reserved. | |||||
| </div>} | |||||
| </div> | |||||
| </div> | |||||
| </> | |||||
| } |
| import React, { useCallback, useEffect, useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import Link from 'next/link' | |||||
| import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react' | |||||
| import Loading from '@/app/components/base/loading' | |||||
| import MailAndCodeAuth from './components/mail-and-code-auth' | |||||
| import MailAndPasswordAuth from './components/mail-and-password-auth' | |||||
| import SSOAuth from './components/sso-auth' | |||||
| import cn from '@/utils/classnames' | |||||
| import { LicenseStatus } from '@/types/feature' | |||||
| import { IS_CE_EDITION } from '@/config' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| const NormalForm = () => { | |||||
| const { t } = useTranslation() | |||||
| const [isLoading, setIsLoading] = useState(true) | |||||
| const { systemFeatures } = useGlobalPublicStore() | |||||
| const [authType, updateAuthType] = useState<'code' | 'password'>('password') | |||||
| const [showORLine, setShowORLine] = useState(false) | |||||
| const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) | |||||
| const init = useCallback(async () => { | |||||
| try { | |||||
| setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin) | |||||
| setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login)) | |||||
| updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code') | |||||
| } | |||||
| catch (error) { | |||||
| console.error(error) | |||||
| setAllMethodsAreDisabled(true) | |||||
| } | |||||
| finally { setIsLoading(false) } | |||||
| }, [systemFeatures]) | |||||
| useEffect(() => { | |||||
| init() | |||||
| }, [init]) | |||||
| if (isLoading) { | |||||
| return <div className={ | |||||
| cn( | |||||
| 'flex w-full grow flex-col items-center justify-center', | |||||
| 'px-6', | |||||
| 'md:px-[108px]', | |||||
| ) | |||||
| }> | |||||
| <Loading type='area' /> | |||||
| </div> | |||||
| } | |||||
| if (systemFeatures.license?.status === LicenseStatus.LOST) { | |||||
| return <div className='mx-auto mt-8 w-full'> | |||||
| <div className='relative'> | |||||
| <div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4"> | |||||
| <div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'> | |||||
| <RiContractLine className='h-5 w-5' /> | |||||
| <RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' /> | |||||
| </div> | |||||
| <p className='system-sm-medium text-text-primary'>{t('login.licenseLost')}</p> | |||||
| <p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseLostTip')}</p> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| } | |||||
| if (systemFeatures.license?.status === LicenseStatus.EXPIRED) { | |||||
| return <div className='mx-auto mt-8 w-full'> | |||||
| <div className='relative'> | |||||
| <div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4"> | |||||
| <div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'> | |||||
| <RiContractLine className='h-5 w-5' /> | |||||
| <RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' /> | |||||
| </div> | |||||
| <p className='system-sm-medium text-text-primary'>{t('login.licenseExpired')}</p> | |||||
| <p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseExpiredTip')}</p> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| } | |||||
| if (systemFeatures.license?.status === LicenseStatus.INACTIVE) { | |||||
| return <div className='mx-auto mt-8 w-full'> | |||||
| <div className='relative'> | |||||
| <div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4"> | |||||
| <div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'> | |||||
| <RiContractLine className='h-5 w-5' /> | |||||
| <RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' /> | |||||
| </div> | |||||
| <p className='system-sm-medium text-text-primary'>{t('login.licenseInactive')}</p> | |||||
| <p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseInactiveTip')}</p> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <div className="mx-auto mt-8 w-full"> | |||||
| <div className="mx-auto w-full"> | |||||
| <h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2> | |||||
| {!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>} | |||||
| </div> | |||||
| <div className="relative"> | |||||
| <div className="mt-6 flex flex-col gap-3"> | |||||
| {systemFeatures.sso_enforced_for_signin && <div className='w-full'> | |||||
| <SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} /> | |||||
| </div>} | |||||
| </div> | |||||
| {showORLine && <div className="relative mt-6"> | |||||
| <div className="absolute inset-0 flex items-center" aria-hidden="true"> | |||||
| <div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div> | |||||
| </div> | |||||
| <div className="relative flex justify-center"> | |||||
| <span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span> | |||||
| </div> | |||||
| </div>} | |||||
| { | |||||
| (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && <> | |||||
| {systemFeatures.enable_email_code_login && authType === 'code' && <> | |||||
| <MailAndCodeAuth /> | |||||
| {systemFeatures.enable_email_password_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('password') }}> | |||||
| <span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.usePassword')}</span> | |||||
| </div>} | |||||
| </>} | |||||
| {systemFeatures.enable_email_password_login && authType === 'password' && <> | |||||
| <MailAndPasswordAuth isEmailSetup={systemFeatures.is_email_setup} /> | |||||
| {systemFeatures.enable_email_code_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('code') }}> | |||||
| <span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.useVerificationCode')}</span> | |||||
| </div>} | |||||
| </>} | |||||
| </> | |||||
| } | |||||
| {allMethodsAreDisabled && <> | |||||
| <div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4"> | |||||
| <div className='shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'> | |||||
| <RiDoorLockLine className='h-5 w-5' /> | |||||
| </div> | |||||
| <p className='system-sm-medium text-text-primary'>{t('login.noLoginMethod')}</p> | |||||
| <p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.noLoginMethodTip')}</p> | |||||
| </div> | |||||
| <div className="relative my-2 py-2"> | |||||
| <div className="absolute inset-0 flex items-center" aria-hidden="true"> | |||||
| <div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div> | |||||
| </div> | |||||
| </div> | |||||
| </>} | |||||
| {!systemFeatures.branding.enabled && <> | |||||
| <div className="system-xs-regular mt-2 block w-full text-text-tertiary"> | |||||
| {t('login.tosDesc')} | |||||
| | |||||
| <Link | |||||
| className='system-xs-medium text-text-secondary hover:underline' | |||||
| target='_blank' rel='noopener noreferrer' | |||||
| href='https://dify.ai/terms' | |||||
| >{t('login.tos')}</Link> | |||||
| & | |||||
| <Link | |||||
| className='system-xs-medium text-text-secondary hover:underline' | |||||
| target='_blank' rel='noopener noreferrer' | |||||
| href='https://dify.ai/privacy' | |||||
| >{t('login.pp')}</Link> | |||||
| </div> | |||||
| {IS_CE_EDITION && <div className="w-hull system-xs-regular mt-2 block text-text-tertiary"> | |||||
| {t('login.goToInit')} | |||||
| | |||||
| <Link | |||||
| className='system-xs-medium text-text-secondary hover:underline' | |||||
| href='/install' | |||||
| >{t('login.setAdminAccount')}</Link> | |||||
| </div>} | |||||
| </>} | |||||
| </div> | |||||
| </div> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default NormalForm |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useCallback, useEffect } from 'react' | import React, { useCallback, useEffect } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { RiDoorLockLine } from '@remixicon/react' | |||||
| import cn from '@/utils/classnames' | |||||
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' | |||||
| import { setAccessToken } from '@/app/components/share/utils' | |||||
| import { removeAccessToken, setAccessToken } from '@/app/components/share/utils' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| import { SSOProtocol } from '@/types/feature' | |||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import AppUnavailable from '@/app/components/base/app-unavailable' | import AppUnavailable from '@/app/components/base/app-unavailable' | ||||
| import NormalForm from './normalForm' | |||||
| import { AccessMode } from '@/models/access-control' | |||||
| import ExternalMemberSsoAuth from './components/external-member-sso-auth' | |||||
| import { fetchAccessToken } from '@/service/share' | |||||
| const WebSSOForm: FC = () => { | const WebSSOForm: FC = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | ||||
| const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) | |||||
| const searchParams = useSearchParams() | const searchParams = useSearchParams() | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const tokenFromUrl = searchParams.get('web_sso_token') | const tokenFromUrl = searchParams.get('web_sso_token') | ||||
| const message = searchParams.get('message') | const message = searchParams.get('message') | ||||
| const showErrorToast = (message: string) => { | |||||
| const getSigninUrl = useCallback(() => { | |||||
| const params = new URLSearchParams(searchParams) | |||||
| params.delete('message') | |||||
| return `/webapp-signin?${params.toString()}` | |||||
| }, [searchParams]) | |||||
| const backToHome = useCallback(() => { | |||||
| removeAccessToken() | |||||
| const url = getSigninUrl() | |||||
| router.replace(url) | |||||
| }, [getSigninUrl, router]) | |||||
| const showErrorToast = (msg: string) => { | |||||
| Toast.notify({ | Toast.notify({ | ||||
| type: 'error', | type: 'error', | ||||
| message, | |||||
| message: msg, | |||||
| }) | }) | ||||
| } | } | ||||
| return appCode | return appCode | ||||
| }, [redirectUrl]) | }, [redirectUrl]) | ||||
| const processTokenAndRedirect = useCallback(async () => { | |||||
| const appCode = getAppCodeFromRedirectUrl() | |||||
| if (!appCode || !tokenFromUrl || !redirectUrl) { | |||||
| showErrorToast('redirect url or app code or token is invalid.') | |||||
| return | |||||
| } | |||||
| await setAccessToken(appCode, tokenFromUrl) | |||||
| router.push(redirectUrl) | |||||
| }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl]) | |||||
| const handleSSOLogin = useCallback(async () => { | |||||
| const appCode = getAppCodeFromRedirectUrl() | |||||
| if (!appCode || !redirectUrl) { | |||||
| showErrorToast('redirect url or app code is invalid.') | |||||
| return | |||||
| } | |||||
| switch (systemFeatures.webapp_auth.sso_config.protocol) { | |||||
| case SSOProtocol.SAML: { | |||||
| const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl) | |||||
| router.push(samlRes.url) | |||||
| break | |||||
| } | |||||
| case SSOProtocol.OIDC: { | |||||
| const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl) | |||||
| router.push(oidcRes.url) | |||||
| break | |||||
| } | |||||
| case SSOProtocol.OAuth2: { | |||||
| const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl) | |||||
| router.push(oauth2Res.url) | |||||
| break | |||||
| } | |||||
| case '': | |||||
| break | |||||
| default: | |||||
| showErrorToast('SSO protocol is not supported.') | |||||
| } | |||||
| }, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol]) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const init = async () => { | |||||
| if (message) { | |||||
| showErrorToast(message) | |||||
| (async () => { | |||||
| if (message) | |||||
| return | return | ||||
| } | |||||
| if (!tokenFromUrl) { | |||||
| await handleSSOLogin() | |||||
| const appCode = getAppCodeFromRedirectUrl() | |||||
| if (appCode && tokenFromUrl && redirectUrl) { | |||||
| localStorage.setItem('webapp_access_token', tokenFromUrl) | |||||
| const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl }) | |||||
| await setAccessToken(appCode, tokenResp.access_token) | |||||
| router.replace(redirectUrl) | |||||
| return | return | ||||
| } | } | ||||
| if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) { | |||||
| const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) | |||||
| await setAccessToken(appCode, tokenResp.access_token) | |||||
| router.replace(redirectUrl) | |||||
| } | |||||
| })() | |||||
| }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) | |||||
| await processTokenAndRedirect() | |||||
| } | |||||
| useEffect(() => { | |||||
| if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) | |||||
| router.replace(redirectUrl) | |||||
| }, [webAppAccessMode, router, redirectUrl]) | |||||
| init() | |||||
| }, [message, processTokenAndRedirect, tokenFromUrl, handleSSOLogin]) | |||||
| if (tokenFromUrl) | |||||
| return <div className='flex h-full items-center justify-center'><Loading /></div> | |||||
| if (message) { | |||||
| if (tokenFromUrl) { | |||||
| return <div className='flex h-full items-center justify-center'> | return <div className='flex h-full items-center justify-center'> | ||||
| <AppUnavailable code={'App Unavailable'} unknownReason={message} /> | |||||
| <Loading /> | |||||
| </div> | </div> | ||||
| } | } | ||||
| if (systemFeatures.webapp_auth.enabled) { | |||||
| if (systemFeatures.webapp_auth.allow_sso) { | |||||
| return ( | |||||
| <div className="flex h-full items-center justify-center"> | |||||
| <div className={cn('flex w-full grow flex-col items-center justify-center', 'px-6', 'md:px-[108px]')}> | |||||
| <Loading /> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| return <div className="flex h-full items-center justify-center"> | |||||
| <div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4"> | |||||
| <div className='shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'> | |||||
| <RiDoorLockLine className='h-5 w-5' /> | |||||
| </div> | |||||
| <p className='system-sm-medium text-text-primary'>{t('login.webapp.noLoginMethod')}</p> | |||||
| <p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.webapp.noLoginMethodTip')}</p> | |||||
| </div> | |||||
| <div className="relative my-2 py-2"> | |||||
| <div className="absolute inset-0 flex items-center" aria-hidden="true"> | |||||
| <div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div> | |||||
| </div> | |||||
| </div> | |||||
| if (message) { | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-4'> | |||||
| <AppUnavailable className='h-auto w-auto' code={t('share.common.appUnavailable')} unknownReason={message} /> | |||||
| <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span> | |||||
| </div> | |||||
| } | |||||
| if (!redirectUrl) { | |||||
| showErrorToast('redirect url is invalid.') | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' /> | |||||
| </div> | |||||
| } | |||||
| if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | </div> | ||||
| } | } | ||||
| else { | |||||
| if (!systemFeatures.webapp_auth.enabled) { | |||||
| return <div className="flex h-full items-center justify-center"> | return <div className="flex h-full items-center justify-center"> | ||||
| <p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p> | <p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p> | ||||
| </div> | </div> | ||||
| } | } | ||||
| if (webAppAccessMode && (webAppAccessMode === AccessMode.ORGANIZATION || webAppAccessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) { | |||||
| return <div className='w-full max-w-[400px]'> | |||||
| <NormalForm /> | |||||
| </div> | |||||
| } | |||||
| if (webAppAccessMode && webAppAccessMode === AccessMode.EXTERNAL_MEMBERS) | |||||
| return <ExternalMemberSsoAuth /> | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-4'> | |||||
| <AppUnavailable className='h-auto w-auto' isUnknownReason={true} /> | |||||
| <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span> | |||||
| </div> | |||||
| } | } | ||||
| export default React.memo(WebSSOForm) | export default React.memo(WebSSOForm) |
| 'use client' | 'use client' | ||||
| import { Dialog } from '@headlessui/react' | |||||
| import { RiBuildingLine, RiGlobalLine } from '@remixicon/react' | |||||
| import { Description as DialogDescription, DialogTitle } from '@headlessui/react' | |||||
| import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useCallback, useEffect } from 'react' | import { useCallback, useEffect } from 'react' | ||||
| import Button from '../../base/button' | import Button from '../../base/button' | ||||
| return <AccessControlDialog show onClose={onClose}> | return <AccessControlDialog show onClose={onClose}> | ||||
| <div className='flex flex-col gap-y-3'> | <div className='flex flex-col gap-y-3'> | ||||
| <div className='pb-3 pl-6 pr-14 pt-6'> | <div className='pb-3 pl-6 pr-14 pt-6'> | ||||
| <Dialog.Title className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</Dialog.Title> | |||||
| <Dialog.Description className='system-xs-regular mt-1 text-text-tertiary'>{t('app.accessControlDialog.description')}</Dialog.Description> | |||||
| <DialogTitle className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</DialogTitle> | |||||
| <DialogDescription className='system-xs-regular mt-1 text-text-tertiary'>{t('app.accessControlDialog.description')}</DialogDescription> | |||||
| </div> | </div> | ||||
| <div className='flex flex-col gap-y-1 px-6 pb-3'> | <div className='flex flex-col gap-y-1 px-6 pb-3'> | ||||
| <div className='leading-6'> | <div className='leading-6'> | ||||
| <RiBuildingLine className='h-4 w-4 text-text-primary' /> | <RiBuildingLine className='h-4 w-4 text-text-primary' /> | ||||
| <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.organization')}</p> | <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.organization')}</p> | ||||
| </div> | </div> | ||||
| {!hideTip && <WebAppSSONotEnabledTip />} | |||||
| </div> | </div> | ||||
| </AccessControlItem> | </AccessControlItem> | ||||
| <AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}> | <AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}> | ||||
| <SpecificGroupsOrMembers /> | <SpecificGroupsOrMembers /> | ||||
| </AccessControlItem> | </AccessControlItem> | ||||
| <AccessControlItem type={AccessMode.EXTERNAL_MEMBERS}> | |||||
| <div className='flex items-center p-3'> | |||||
| <div className='flex grow items-center gap-x-2'> | |||||
| <RiVerifiedBadgeLine className='h-4 w-4 text-text-primary' /> | |||||
| <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.external')}</p> | |||||
| </div> | |||||
| {!hideTip && <WebAppSSONotEnabledTip />} | |||||
| </div> | |||||
| </AccessControlItem> | |||||
| <AccessControlItem type={AccessMode.PUBLIC}> | <AccessControlItem type={AccessMode.PUBLIC}> | ||||
| <div className='flex items-center gap-x-2 p-3'> | <div className='flex items-center gap-x-2 p-3'> | ||||
| <RiGlobalLine className='h-4 w-4 text-text-primary' /> | <RiGlobalLine className='h-4 w-4 text-text-primary' /> |
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useCallback, useEffect } from 'react' | import { useCallback, useEffect } from 'react' | ||||
| import Avatar from '../../base/avatar' | import Avatar from '../../base/avatar' | ||||
| import Divider from '../../base/divider' | |||||
| import Tooltip from '../../base/tooltip' | import Tooltip from '../../base/tooltip' | ||||
| import Loading from '../../base/loading' | import Loading from '../../base/loading' | ||||
| import useAccessControlStore from '../../../../context/access-control-store' | import useAccessControlStore from '../../../../context/access-control-store' | ||||
| import AddMemberOrGroupDialog from './add-member-or-group-pop' | import AddMemberOrGroupDialog from './add-member-or-group-pop' | ||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control' | import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control' | ||||
| import { AccessMode } from '@/models/access-control' | import { AccessMode } from '@/models/access-control' | ||||
| import { useAppWhiteListSubjects } from '@/service/access-control' | import { useAppWhiteListSubjects } from '@/service/access-control' | ||||
| const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups) | const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups) | ||||
| const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers) | const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers) | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||||
| const hideTip = systemFeatures.webapp_auth.enabled | |||||
| && (systemFeatures.webapp_auth.allow_sso | |||||
| || systemFeatures.webapp_auth.allow_email_password_login | |||||
| || systemFeatures.webapp_auth.allow_email_code_login) | |||||
| const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) | const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) | ||||
| useEffect(() => { | useEffect(() => { | ||||
| <RiLockLine className='h-4 w-4 text-text-primary' /> | <RiLockLine className='h-4 w-4 text-text-primary' /> | ||||
| <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p> | <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p> | ||||
| </div> | </div> | ||||
| {!hideTip && <WebAppSSONotEnabledTip />} | |||||
| </div> | </div> | ||||
| } | } | ||||
| <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p> | <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p> | ||||
| </div> | </div> | ||||
| <div className='flex items-center gap-x-1'> | <div className='flex items-center gap-x-1'> | ||||
| {!hideTip && <> | |||||
| <WebAppSSONotEnabledTip /> | |||||
| <Divider className='ml-2 mr-0 h-[14px]' type="vertical" /> | |||||
| </>} | |||||
| <AddMemberOrGroupDialog /> | <AddMemberOrGroupDialog /> | ||||
| </div> | </div> | ||||
| </div> | </div> |
| import { | import { | ||||
| RiArrowDownSLine, | RiArrowDownSLine, | ||||
| RiArrowRightSLine, | RiArrowRightSLine, | ||||
| RiBuildingLine, | |||||
| RiGlobalLine, | |||||
| RiLockLine, | RiLockLine, | ||||
| RiPlanetLine, | RiPlanetLine, | ||||
| RiPlayCircleLine, | RiPlayCircleLine, | ||||
| RiPlayList2Line, | RiPlayList2Line, | ||||
| RiTerminalBoxLine, | RiTerminalBoxLine, | ||||
| RiVerifiedBadgeLine, | |||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import { useKeyPress } from 'ahooks' | import { useKeyPress } from 'ahooks' | ||||
| import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' | import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' | ||||
| setShowAppAccessControl(true) | setShowAppAccessControl(true) | ||||
| }}> | }}> | ||||
| <div className='flex grow items-center gap-x-1.5 pr-1'> | <div className='flex grow items-center gap-x-1.5 pr-1'> | ||||
| <RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| {appDetail?.access_mode === AccessMode.ORGANIZATION && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>} | |||||
| {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p>} | |||||
| {appDetail?.access_mode === AccessMode.PUBLIC && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>} | |||||
| {appDetail?.access_mode === AccessMode.ORGANIZATION | |||||
| && <> | |||||
| <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p> | |||||
| </> | |||||
| } | |||||
| {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS | |||||
| && <> | |||||
| <RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p> | |||||
| </> | |||||
| } | |||||
| {appDetail?.access_mode === AccessMode.PUBLIC | |||||
| && <> | |||||
| <RiGlobalLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p> | |||||
| </> | |||||
| } | |||||
| {appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS | |||||
| && <> | |||||
| <RiVerifiedBadgeLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.external')}</p> | |||||
| </> | |||||
| } | |||||
| </div> | </div> | ||||
| {!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>} | {!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>} | ||||
| <div className='flex h-4 w-4 shrink-0 items-center justify-center'> | <div className='flex h-4 w-4 shrink-0 items-center justify-center'> |
| import { | import { | ||||
| RiArrowRightSLine, | RiArrowRightSLine, | ||||
| RiBookOpenLine, | RiBookOpenLine, | ||||
| RiBuildingLine, | |||||
| RiEqualizer2Line, | RiEqualizer2Line, | ||||
| RiExternalLinkLine, | RiExternalLinkLine, | ||||
| RiGlobalLine, | |||||
| RiLockLine, | RiLockLine, | ||||
| RiPaintBrushLine, | RiPaintBrushLine, | ||||
| RiVerifiedBadgeLine, | |||||
| RiWindowLine, | RiWindowLine, | ||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import SettingsModal from './settings' | import SettingsModal from './settings' | ||||
| <div className='flex h-9 w-full cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2' | <div className='flex h-9 w-full cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2' | ||||
| onClick={handleClickAccessControl}> | onClick={handleClickAccessControl}> | ||||
| <div className='flex grow items-center gap-x-1.5 pr-1'> | <div className='flex grow items-center gap-x-1.5 pr-1'> | ||||
| <RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| {appDetail?.access_mode === AccessMode.ORGANIZATION && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>} | |||||
| {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p>} | |||||
| {appDetail?.access_mode === AccessMode.PUBLIC && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>} | |||||
| </div> | |||||
| {appDetail?.access_mode === AccessMode.ORGANIZATION | |||||
| && <> | |||||
| <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p> | |||||
| </> | |||||
| } | |||||
| {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS | |||||
| && <> | |||||
| <RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p> | |||||
| </> | |||||
| } | |||||
| {appDetail?.access_mode === AccessMode.PUBLIC | |||||
| && <> | |||||
| <RiGlobalLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p> | |||||
| </> | |||||
| } | |||||
| {appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS | |||||
| && <> | |||||
| <RiVerifiedBadgeLine className='h-4 w-4 shrink-0 text-text-secondary' /> | |||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.external')}</p> | |||||
| </> | |||||
| }</div> | |||||
| {!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>} | {!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>} | ||||
| <div className='flex h-4 w-4 shrink-0 items-center justify-center'> | <div className='flex h-4 w-4 shrink-0 items-center justify-center'> | ||||
| <RiArrowRightSLine className='h-4 w-4 text-text-quaternary' /> | <RiArrowRightSLine className='h-4 w-4 text-text-quaternary' /> |
| 'use client' | 'use client' | ||||
| import classNames from '@/utils/classnames' | |||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React from 'react' | import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| code?: number | string | code?: number | string | ||||
| isUnknownReason?: boolean | isUnknownReason?: boolean | ||||
| unknownReason?: string | unknownReason?: string | ||||
| className?: string | |||||
| } | } | ||||
| const AppUnavailable: FC<IAppUnavailableProps> = ({ | const AppUnavailable: FC<IAppUnavailableProps> = ({ | ||||
| code = 404, | code = 404, | ||||
| isUnknownReason, | isUnknownReason, | ||||
| unknownReason, | unknownReason, | ||||
| className, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| return ( | return ( | ||||
| <div className='flex h-screen w-screen items-center justify-center'> | |||||
| <div className={classNames('flex h-screen w-screen items-center justify-center', className)}> | |||||
| <h1 className='mr-5 h-[50px] pr-5 text-[24px] font-medium leading-[50px]' | <h1 className='mr-5 h-[50px] pr-5 text-[24px] font-medium leading-[50px]' | ||||
| style={{ | style={{ | ||||
| borderRight: '1px solid rgba(0,0,0,.3)', | borderRight: '1px solid rgba(0,0,0,.3)', |
| ConversationItem, | ConversationItem, | ||||
| } from '@/models/share' | } from '@/models/share' | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| import { AccessMode } from '@/models/access-control' | |||||
| export type ChatWithHistoryContextValue = { | export type ChatWithHistoryContextValue = { | ||||
| appInfoError?: any | appInfoError?: any | ||||
| appInfoLoading?: boolean | appInfoLoading?: boolean | ||||
| appMeta?: AppMeta | appMeta?: AppMeta | ||||
| appData?: AppData | appData?: AppData | ||||
| accessMode?: AccessMode | |||||
| userCanAccess?: boolean | userCanAccess?: boolean | ||||
| appParams?: ChatConfig | appParams?: ChatConfig | ||||
| appChatListDataLoading?: boolean | appChatListDataLoading?: boolean | ||||
| } | } | ||||
| export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ | export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ | ||||
| accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, | |||||
| userCanAccess: false, | userCanAccess: false, | ||||
| currentConversationId: '', | currentConversationId: '', | ||||
| appPrevChatTree: [], | appPrevChatTree: [], |
| import { InputVarType } from '@/app/components/workflow/types' | import { InputVarType } from '@/app/components/workflow/types' | ||||
| import { TransferMethod } from '@/types/app' | import { TransferMethod } from '@/types/app' | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' | |||||
| import { useGetUserCanAccessApp } from '@/service/access-control' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| import { AccessMode } from '@/models/access-control' | |||||
| function getFormattedChatList(messages: any[]) { | function getFormattedChatList(messages: any[]) { | ||||
| const newChatList: ChatItem[] = [] | const newChatList: ChatItem[] = [] | ||||
| const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) | const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) | ||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | ||||
| const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) | const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) | ||||
| const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ | |||||
| appId: installedAppInfo?.app.id || appInfo?.app_id, | |||||
| isInstalledApp, | |||||
| enabled: systemFeatures.webapp_auth.enabled, | |||||
| }) | |||||
| const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ | const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ | ||||
| appId: installedAppInfo?.app.id || appInfo?.app_id, | appId: installedAppInfo?.app.id || appInfo?.app_id, | ||||
| isInstalledApp, | isInstalledApp, | ||||
| return { | return { | ||||
| appInfoError, | appInfoError, | ||||
| appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), | |||||
| accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC, | |||||
| appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), | |||||
| userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, | userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, | ||||
| isInstalledApp, | isInstalledApp, | ||||
| appId, | appId, |
| const { | const { | ||||
| appInfoError, | appInfoError, | ||||
| appInfoLoading, | appInfoLoading, | ||||
| accessMode, | |||||
| userCanAccess, | userCanAccess, | ||||
| appData, | appData, | ||||
| appParams, | appParams, | ||||
| appInfoError, | appInfoError, | ||||
| appInfoLoading, | appInfoLoading, | ||||
| appData, | appData, | ||||
| accessMode, | |||||
| userCanAccess, | userCanAccess, | ||||
| appParams, | appParams, | ||||
| appMeta, | appMeta, |
| import DifyLogo from '@/app/components/base/logo/dify-logo' | import DifyLogo from '@/app/components/base/logo/dify-logo' | ||||
| import type { ConversationItem } from '@/models/share' | import type { ConversationItem } from '@/models/share' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { AccessMode } from '@/models/access-control' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| type Props = { | type Props = { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { | const { | ||||
| isInstalledApp, | isInstalledApp, | ||||
| accessMode, | |||||
| appData, | appData, | ||||
| handleNewConversation, | handleNewConversation, | ||||
| pinnedConversationList, | pinnedConversationList, | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| <div className='flex shrink-0 items-center justify-between p-3'> | <div className='flex shrink-0 items-center justify-between p-3'> | ||||
| <MenuDropdown hideLogout={isInstalledApp || accessMode === AccessMode.PUBLIC} placement='top-start' data={appData?.site} /> | |||||
| <MenuDropdown hideLogout={isInstalledApp} placement='top-start' data={appData?.site} /> | |||||
| {/* powered by */} | {/* powered by */} | ||||
| <div className='shrink-0'> | <div className='shrink-0'> | ||||
| {!appData?.custom_config?.remove_webapp_brand && ( | {!appData?.custom_config?.remove_webapp_brand && ( |
| ConversationItem, | ConversationItem, | ||||
| } from '@/models/share' | } from '@/models/share' | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| import { AccessMode } from '@/models/access-control' | |||||
| export type EmbeddedChatbotContextValue = { | export type EmbeddedChatbotContextValue = { | ||||
| accessMode?: AccessMode | |||||
| userCanAccess?: boolean | userCanAccess?: boolean | ||||
| appInfoError?: any | appInfoError?: any | ||||
| appInfoLoading?: boolean | appInfoLoading?: boolean | ||||
| export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ | export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ | ||||
| userCanAccess: false, | userCanAccess: false, | ||||
| accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, | |||||
| currentConversationId: '', | currentConversationId: '', | ||||
| appPrevChatList: [], | appPrevChatList: [], | ||||
| pinnedConversationList: [], | pinnedConversationList: [], |
| import { TransferMethod } from '@/types/app' | import { TransferMethod } from '@/types/app' | ||||
| import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' | import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' | |||||
| import { useGetUserCanAccessApp } from '@/service/access-control' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| import { AccessMode } from '@/models/access-control' | |||||
| function getFormattedChatList(messages: any[]) { | function getFormattedChatList(messages: any[]) { | ||||
| const newChatList: ChatItem[] = [] | const newChatList: ChatItem[] = [] | ||||
| const isInstalledApp = false | const isInstalledApp = false | ||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | ||||
| const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo) | const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo) | ||||
| const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ | |||||
| appId: appInfo?.app_id, | |||||
| isInstalledApp, | |||||
| enabled: systemFeatures.webapp_auth.enabled, | |||||
| }) | |||||
| const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ | const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ | ||||
| appId: appInfo?.app_id, | appId: appInfo?.app_id, | ||||
| isInstalledApp, | isInstalledApp, | ||||
| return { | return { | ||||
| appInfoError, | appInfoError, | ||||
| appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), | |||||
| accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC, | |||||
| appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), | |||||
| userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, | userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, | ||||
| isInstalledApp, | isInstalledApp, | ||||
| allowResetChat, | allowResetChat, |
| import { | import { | ||||
| RiEqualizer2Line, | RiEqualizer2Line, | ||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import { useRouter } from 'next/navigation' | |||||
| import { usePathname, useRouter } from 'next/navigation' | |||||
| import Divider from '../../base/divider' | import Divider from '../../base/divider' | ||||
| import { removeAccessToken } from '../utils' | |||||
| import InfoModal from './info-modal' | import InfoModal from './info-modal' | ||||
| import ActionButton from '@/app/components/base/action-button' | import ActionButton from '@/app/components/base/action-button' | ||||
| import { | import { | ||||
| import ThemeSwitcher from '@/app/components/base/theme-switcher' | import ThemeSwitcher from '@/app/components/base/theme-switcher' | ||||
| import type { SiteInfo } from '@/models/share' | import type { SiteInfo } from '@/models/share' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import { AccessMode } from '@/models/access-control' | |||||
| type Props = { | type Props = { | ||||
| data?: SiteInfo | data?: SiteInfo | ||||
| placement, | placement, | ||||
| hideLogout, | hideLogout, | ||||
| }) => { | }) => { | ||||
| const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) | |||||
| const router = useRouter() | const router = useRouter() | ||||
| const pathname = usePathname() | |||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const [open, doSetOpen] = useState(false) | const [open, doSetOpen] = useState(false) | ||||
| const openRef = useRef(open) | const openRef = useRef(open) | ||||
| }, [setOpen]) | }, [setOpen]) | ||||
| const handleLogout = useCallback(() => { | const handleLogout = useCallback(() => { | ||||
| removeAccessToken() | |||||
| router.replace(`/webapp-signin?redirect_url=${window.location.href}`) | |||||
| }, [router]) | |||||
| localStorage.removeItem('token') | |||||
| localStorage.removeItem('webapp_access_token') | |||||
| router.replace(`/webapp-signin?redirect_url=${pathname}`) | |||||
| }, [router, pathname]) | |||||
| const [show, setShow] = useState(false) | const [show, setShow] = useState(false) | ||||
| className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' | className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' | ||||
| >{t('common.userProfile.about')}</div> | >{t('common.userProfile.about')}</div> | ||||
| </div> | </div> | ||||
| {!(hideLogout || webAppAccessMode === AccessMode.EXTERNAL_MEMBERS || webAppAccessMode === AccessMode.PUBLIC) && ( | |||||
| <div className='p-1'> | |||||
| <div | |||||
| onClick={handleLogout} | |||||
| className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' | |||||
| > | |||||
| {t('common.userProfile.logout')} | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| </div> | </div> | ||||
| </PortalToFollowElemContent> | </PortalToFollowElemContent> | ||||
| </PortalToFollowElem> | </PortalToFollowElem> |
| version: 2, | version: 2, | ||||
| }) | }) | ||||
| export const checkOrSetAccessToken = async () => { | |||||
| const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] | |||||
| export const checkOrSetAccessToken = async (appCode?: string) => { | |||||
| const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0] | |||||
| const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id | const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id | ||||
| const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) | const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) | ||||
| let accessTokenJson = getInitialTokenV2() | let accessTokenJson = getInitialTokenV2() | ||||
| catch { | catch { | ||||
| } | } | ||||
| if (!accessTokenJson[sharedToken]?.[userId || 'DEFAULT']) { | if (!accessTokenJson[sharedToken]?.[userId || 'DEFAULT']) { | ||||
| const res = await fetchAccessToken(sharedToken, userId) | |||||
| const webAppAccessToken = localStorage.getItem('webapp_access_token') | |||||
| const res = await fetchAccessToken({ appCode: sharedToken, userId, webAppAccessToken }) | |||||
| accessTokenJson[sharedToken] = { | accessTokenJson[sharedToken] = { | ||||
| ...accessTokenJson[sharedToken], | ...accessTokenJson[sharedToken], | ||||
| [userId || 'DEFAULT']: res.access_token, | [userId || 'DEFAULT']: res.access_token, | ||||
| } | } | ||||
| } | } | ||||
| export const setAccessToken = async (sharedToken: string, token: string, user_id?: string) => { | |||||
| export const setAccessToken = (sharedToken: string, token: string, user_id?: string) => { | |||||
| const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) | const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) | ||||
| let accessTokenJson = getInitialTokenV2() | let accessTokenJson = getInitialTokenV2() | ||||
| try { | try { | ||||
| } | } | ||||
| localStorage.removeItem(CONVERSATION_ID_INFO) | localStorage.removeItem(CONVERSATION_ID_INFO) | ||||
| localStorage.removeItem('webapp_access_token') | |||||
| delete accessTokenJson[sharedToken] | delete accessTokenJson[sharedToken] | ||||
| localStorage.setItem('token', JSON.stringify(accessTokenJson)) | localStorage.setItem('token', JSON.stringify(accessTokenJson)) |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import classNames from '@/utils/classnames' | import classNames from '@/utils/classnames' | ||||
| import { useSelector } from '@/context/app-context' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| import { useTheme } from 'next-themes' | |||||
| type LoginLogoProps = { | type LoginLogoProps = { | ||||
| className?: string | className?: string | ||||
| className, | className, | ||||
| }) => { | }) => { | ||||
| const { systemFeatures } = useGlobalPublicStore() | const { systemFeatures } = useGlobalPublicStore() | ||||
| const { theme } = useSelector((s) => { | |||||
| return { | |||||
| theme: s.theme, | |||||
| } | |||||
| }) | |||||
| const { theme } = useTheme() | |||||
| let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` | let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` | ||||
| if (systemFeatures.branding.enabled) | if (systemFeatures.branding.enabled) |
| import { defaultSystemFeatures } from '@/types/feature' | import { defaultSystemFeatures } from '@/types/feature' | ||||
| import { getSystemFeatures } from '@/service/common' | import { getSystemFeatures } from '@/service/common' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import { AccessMode } from '@/models/access-control' | |||||
| type GlobalPublicStore = { | type GlobalPublicStore = { | ||||
| isPending: boolean | |||||
| setIsPending: (isPending: boolean) => void | |||||
| isGlobalPending: boolean | |||||
| setIsGlobalPending: (isPending: boolean) => void | |||||
| systemFeatures: SystemFeatures | systemFeatures: SystemFeatures | ||||
| setSystemFeatures: (systemFeatures: SystemFeatures) => void | setSystemFeatures: (systemFeatures: SystemFeatures) => void | ||||
| webAppAccessMode: AccessMode, | |||||
| setWebAppAccessMode: (webAppAccessMode: AccessMode) => void | |||||
| } | } | ||||
| export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ | export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ | ||||
| isPending: true, | |||||
| setIsPending: (isPending: boolean) => set(() => ({ isPending })), | |||||
| isGlobalPending: true, | |||||
| setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), | |||||
| systemFeatures: defaultSystemFeatures, | systemFeatures: defaultSystemFeatures, | ||||
| setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), | setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), | ||||
| webAppAccessMode: AccessMode.PUBLIC, | |||||
| setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })), | |||||
| })) | })) | ||||
| const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ | const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ | ||||
| queryKey: ['systemFeatures'], | queryKey: ['systemFeatures'], | ||||
| queryFn: getSystemFeatures, | queryFn: getSystemFeatures, | ||||
| }) | }) | ||||
| const { setSystemFeatures, setIsPending } = useGlobalPublicStore() | |||||
| const { setSystemFeatures, setIsGlobalPending: setIsPending } = useGlobalPublicStore() | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (data) | if (data) | ||||
| setSystemFeatures({ ...defaultSystemFeatures, ...data }) | setSystemFeatures({ ...defaultSystemFeatures, ...data }) |
| act(() => { | act(() => { | ||||
| useGlobalPublicStore.setState({ | useGlobalPublicStore.setState({ | ||||
| systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, | systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, | ||||
| isPending: true, | |||||
| isGlobalPending: true, | |||||
| }) | }) | ||||
| }) | }) | ||||
| it('document title should be empty if set title', () => { | it('document title should be empty if set title', () => { | ||||
| beforeEach(() => { | beforeEach(() => { | ||||
| act(() => { | act(() => { | ||||
| useGlobalPublicStore.setState({ | useGlobalPublicStore.setState({ | ||||
| isPending: false, | |||||
| isGlobalPending: false, | |||||
| systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, | systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, | ||||
| }) | }) | ||||
| }) | }) | ||||
| beforeEach(() => { | beforeEach(() => { | ||||
| act(() => { | act(() => { | ||||
| useGlobalPublicStore.setState({ | useGlobalPublicStore.setState({ | ||||
| isPending: false, | |||||
| isGlobalPending: false, | |||||
| systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } }, | systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } }, | ||||
| }) | }) | ||||
| }) | }) |
| import { useFavicon, useTitle } from 'ahooks' | import { useFavicon, useTitle } from 'ahooks' | ||||
| export default function useDocumentTitle(title: string) { | export default function useDocumentTitle(title: string) { | ||||
| const isPending = useGlobalPublicStore(s => s.isPending) | |||||
| const isPending = useGlobalPublicStore(s => s.isGlobalPending) | |||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | ||||
| const prefix = title ? `${title} - ` : '' | const prefix = title ? `${title} - ` : '' | ||||
| let titleStr = '' | let titleStr = '' |
| }, | }, | ||||
| accessControl: 'Web App Access Control', | accessControl: 'Web App Access Control', | ||||
| accessItemsDescription: { | accessItemsDescription: { | ||||
| anyone: 'Anyone can access the web app', | |||||
| specific: 'Only specific groups or members can access the web app', | |||||
| organization: 'Anyone in the organization can access the web app', | |||||
| anyone: 'Anyone can access the web app (no login required)', | |||||
| specific: 'Only specific members within the platform can access the Web application', | |||||
| organization: 'All members within the platform can access the Web application', | |||||
| external: 'Only authenticated external users can access the Web application', | |||||
| }, | }, | ||||
| accessControlDialog: { | accessControlDialog: { | ||||
| title: 'Web App Access Control', | title: 'Web App Access Control', | ||||
| accessLabel: 'Who has access', | accessLabel: 'Who has access', | ||||
| accessItems: { | accessItems: { | ||||
| anyone: 'Anyone with the link', | anyone: 'Anyone with the link', | ||||
| specific: 'Specific groups or members', | |||||
| organization: 'Only members within the enterprise', | |||||
| specific: 'Specific members within the platform', | |||||
| organization: 'All members within the platform', | |||||
| external: 'Authenticated external users', | |||||
| }, | }, | ||||
| groups_one: '{{count}} GROUP', | groups_one: '{{count}} GROUP', | ||||
| groups_other: '{{count}} GROUPS', | groups_other: '{{count}} GROUPS', | ||||
| members_one: '{{count}} MEMBER', | members_one: '{{count}} MEMBER', | ||||
| members_other: '{{count}} MEMBERS', | members_other: '{{count}} MEMBERS', | ||||
| noGroupsOrMembers: 'No groups or members selected', | noGroupsOrMembers: 'No groups or members selected', | ||||
| webAppSSONotEnabledTip: 'Please contact enterprise administrator to configure the web app authentication method.', | |||||
| webAppSSONotEnabledTip: 'Please contact your organization administrator to configure external authentication for the Web application.', | |||||
| operateGroupAndMember: { | operateGroupAndMember: { | ||||
| searchPlaceholder: 'Search groups and members', | searchPlaceholder: 'Search groups and members', | ||||
| allMembers: 'All members', | allMembers: 'All members', |
| atLeastOne: 'Please input at least one row in the uploaded file.', | atLeastOne: 'Please input at least one row in the uploaded file.', | ||||
| }, | }, | ||||
| }, | }, | ||||
| login: { | |||||
| backToHome: 'Back to Home', | |||||
| }, | |||||
| } | } | ||||
| export default translation | export default translation |
| }, | }, | ||||
| accessControl: 'Web アプリアクセス制御', | accessControl: 'Web アプリアクセス制御', | ||||
| accessItemsDescription: { | accessItemsDescription: { | ||||
| anyone: '誰でも Web アプリにアクセス可能', | |||||
| specific: '特定のグループまたはメンバーのみが Web アプリにアクセス可能', | |||||
| organization: '組織内の誰でも Web アプリにアクセス可能', | |||||
| anyone: '誰でもこの web アプリにアクセスできます(ログイン不要)', | |||||
| specific: '特定のプラットフォーム内メンバーのみがこの Web アプリにアクセスできます', | |||||
| organization: 'プラットフォーム内の全メンバーがこの Web アプリにアクセスできます', | |||||
| external: '認証済みの外部ユーザーのみがこの Web アプリにアクセスできます', | |||||
| }, | }, | ||||
| accessControlDialog: { | accessControlDialog: { | ||||
| title: 'アクセス権限', | title: 'アクセス権限', | ||||
| description: 'Web アプリのアクセス権限を設定します', | description: 'Web アプリのアクセス権限を設定します', | ||||
| accessLabel: '誰がアクセスできますか', | accessLabel: '誰がアクセスできますか', | ||||
| accessItemsDescription: { | |||||
| anyone: '誰でも Web アプリにアクセス可能です', | |||||
| specific: '特定のグループやメンバーが Web アプリにアクセス可能です', | |||||
| organization: '組織内の誰でも Web アプリにアクセス可能です', | |||||
| }, | |||||
| accessItems: { | accessItems: { | ||||
| anyone: 'すべてのユーザー', | |||||
| specific: '特定のグループメンバー', | |||||
| organization: 'グループ内の全員', | |||||
| anyone: 'リンクを知っているすべてのユーザー', | |||||
| specific: '特定のプラットフォーム内メンバー', | |||||
| organization: 'プラットフォーム内の全メンバー', | |||||
| external: '認証済みの外部ユーザー', | |||||
| }, | }, | ||||
| groups_one: '{{count}} グループ', | groups_one: '{{count}} グループ', | ||||
| groups_other: '{{count}} グループ', | groups_other: '{{count}} グループ', | ||||
| members_one: '{{count}} メンバー', | members_one: '{{count}} メンバー', | ||||
| members_other: '{{count}} メンバー', | members_other: '{{count}} メンバー', | ||||
| noGroupsOrMembers: 'グループまたはメンバーが選択されていません', | noGroupsOrMembers: 'グループまたはメンバーが選択されていません', | ||||
| webAppSSONotEnabledTip: 'Web アプリの認証方式設定については、企業管理者へご連絡ください。', | |||||
| webAppSSONotEnabledTip: 'Web アプリの外部認証方式を設定するには、組織の管理者にお問い合わせください。', | |||||
| operateGroupAndMember: { | operateGroupAndMember: { | ||||
| searchPlaceholder: 'グループやメンバーを検索', | searchPlaceholder: 'グループやメンバーを検索', | ||||
| allMembers: 'すべてのメンバー', | allMembers: 'すべてのメンバー', |
| atLeastOne: '1 行以上のデータが必要です', | atLeastOne: '1 行以上のデータが必要です', | ||||
| }, | }, | ||||
| }, | }, | ||||
| login: { | |||||
| backToHome: 'ホームに戻る', | |||||
| }, | |||||
| } | } | ||||
| export default translation | export default translation |
| }, | }, | ||||
| accessControl: 'Web 应用访问控制', | accessControl: 'Web 应用访问控制', | ||||
| accessItemsDescription: { | accessItemsDescription: { | ||||
| anyone: '任何人可以访问 web 应用', | |||||
| specific: '特定组或成员可以访问 web 应用', | |||||
| organization: '组织内任何人可以访问 web 应用', | |||||
| anyone: '任何人都可以访问该 web 应用(无需登录)', | |||||
| specific: '仅指定的平台内成员可访问该 Web 应用', | |||||
| organization: '平台内所有成员均可访问该 Web 应用', | |||||
| external: '仅经认证的外部用户可访问该 Web 应用', | |||||
| }, | }, | ||||
| accessControlDialog: { | accessControlDialog: { | ||||
| title: 'Web 应用访问权限', | title: 'Web 应用访问权限', | ||||
| description: '设置 web 应用访问权限。', | description: '设置 web 应用访问权限。', | ||||
| accessLabel: '谁可以访问', | accessLabel: '谁可以访问', | ||||
| accessItemsDescription: { | |||||
| anyone: '任何人可以访问 web 应用', | |||||
| specific: '特定组或成员可以访问 web 应用', | |||||
| organization: '组织内任何人可以访问 web 应用', | |||||
| }, | |||||
| accessItems: { | accessItems: { | ||||
| anyone: '任何人', | anyone: '任何人', | ||||
| specific: '特定组或成员', | |||||
| organization: '组织内任何人', | |||||
| specific: '平台内指定成员', | |||||
| organization: '平台内所有成员', | |||||
| external: '经认证的外部用户', | |||||
| }, | }, | ||||
| groups_one: '{{count}} 个组', | groups_one: '{{count}} 个组', | ||||
| groups_other: '{{count}} 个组', | groups_other: '{{count}} 个组', | ||||
| members_one: '{{count}} 个成员', | members_one: '{{count}} 个成员', | ||||
| members_other: '{{count}} 个成员', | members_other: '{{count}} 个成员', | ||||
| noGroupsOrMembers: '未选择分组或成员', | noGroupsOrMembers: '未选择分组或成员', | ||||
| webAppSSONotEnabledTip: '请联系企业管理员配置 web 应用的身份认证方式。', | |||||
| webAppSSONotEnabledTip: '请联系企业管理员配置 Web 应用外部认证方式。', | |||||
| operateGroupAndMember: { | operateGroupAndMember: { | ||||
| searchPlaceholder: '搜索组或成员', | searchPlaceholder: '搜索组或成员', | ||||
| allMembers: '所有成员', | allMembers: '所有成员', |
| atLeastOne: '上传文件的内容不能少于一条', | atLeastOne: '上传文件的内容不能少于一条', | ||||
| }, | }, | ||||
| }, | }, | ||||
| login: { | |||||
| backToHome: '返回首页', | |||||
| }, | |||||
| } | } | ||||
| export default translation | export default translation |
| PUBLIC = 'public', | PUBLIC = 'public', | ||||
| SPECIFIC_GROUPS_MEMBERS = 'private', | SPECIFIC_GROUPS_MEMBERS = 'private', | ||||
| ORGANIZATION = 'private_all', | ORGANIZATION = 'private_all', | ||||
| EXTERNAL_MEMBERS = 'sso_verified', | |||||
| } | } | ||||
| export type AccessControlGroup = { | export type AccessControlGroup = { |
| } | } | ||||
| function requiredWebSSOLogin(message?: string) { | function requiredWebSSOLogin(message?: string) { | ||||
| removeAccessToken() | |||||
| const params = new URLSearchParams() | const params = new URLSearchParams() | ||||
| params.append('redirect_url', globalThis.location.pathname) | params.append('redirect_url', globalThis.location.pathname) | ||||
| if (message) | if (message) |
| export const login: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => { | export const login: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => { | ||||
| return post(url, { body }) as Promise<LoginResponse> | return post(url, { body }) as Promise<LoginResponse> | ||||
| } | } | ||||
| export const webAppLogin: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => { | |||||
| return post(url, { body }, { isPublicAPI: true }) as Promise<LoginResponse> | |||||
| } | |||||
| export const fetchNewToken: Fetcher<CommonResponse & { data: { access_token: string; refresh_token: string } }, { body: Record<string, any> }> = ({ body }) => { | export const fetchNewToken: Fetcher<CommonResponse & { data: { access_token: string; refresh_token: string } }, { body: Record<string, any> }> = ({ body }) => { | ||||
| return post('/refresh-token', { body }) as Promise<CommonResponse & { data: { access_token: string; refresh_token: string } }> | return post('/refresh-token', { body }) as Promise<CommonResponse & { data: { access_token: string; refresh_token: string } }> | ||||
| export const changePasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => | export const changePasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => | ||||
| post<CommonResponse>(url, { body }) | post<CommonResponse>(url, { body }) | ||||
| export const sendWebAppForgotPasswordEmail: Fetcher<CommonResponse & { data: string }, { url: string; body: { email: string } }> = ({ url, body }) => | |||||
| post<CommonResponse & { data: string }>(url, { body }, { isPublicAPI: true }) | |||||
| export const verifyWebAppForgotPasswordToken: Fetcher<CommonResponse & { is_valid: boolean; email: string }, { url: string; body: { token: string } }> = ({ url, body }) => { | |||||
| return post(url, { body }, { isPublicAPI: true }) as Promise<CommonResponse & { is_valid: boolean; email: string }> | |||||
| } | |||||
| export const changeWebAppPasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => | |||||
| post<CommonResponse>(url, { body }, { isPublicAPI: true }) | |||||
| export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => { | export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => { | ||||
| return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic }) | return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic }) | ||||
| } | } | ||||
| export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) => | export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) => | ||||
| post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body }) | post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body }) | ||||
| export const sendWebAppEMailLoginCode = (email: string, language = 'en-US') => | |||||
| post<CommonResponse & { data: string }>('/email-code-login', { body: { email, language } }, { isPublicAPI: true }) | |||||
| export const webAppEmailLoginWithCode = (data: { email: string; code: string; token: string }) => | |||||
| post<LoginResponse>('/email-code-login/validity', { body: data }, { isPublicAPI: true }) | |||||
| export const sendWebAppResetPasswordCode = (email: string, language = 'en-US') => | |||||
| post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', { body: { email, language } }, { isPublicAPI: true }) | |||||
| export const verifyWebAppResetPasswordCode = (body: { email: string; code: string; token: string }) => | |||||
| post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body }, { isPublicAPI: true }) | |||||
| export const sendDeleteAccountCode = () => | export const sendDeleteAccountCode = () => | ||||
| get<CommonResponse & { data: string }>('/account/delete/verify') | get<CommonResponse & { data: string }>('/account/delete/verify') | ||||
| }) as Promise<{ url: string }> | }) as Promise<{ url: string }> | ||||
| } | } | ||||
| export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { | |||||
| return (getAction('get', false))(getUrl('/enterprise/sso/members/saml/login', false, ''), { | |||||
| params: { | |||||
| app_code: appCode, | |||||
| redirect_url: redirectUrl, | |||||
| }, | |||||
| }) as Promise<{ url: string }> | |||||
| } | |||||
| export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: string) => { | |||||
| return (getAction('get', false))(getUrl('/enterprise/sso/members/oidc/login', false, ''), { | |||||
| params: { | |||||
| app_code: appCode, | |||||
| redirect_url: redirectUrl, | |||||
| }, | |||||
| }) as Promise<{ url: string }> | |||||
| } | |||||
| export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => { | |||||
| return (getAction('get', false))(getUrl('/enterprise/sso/members/oauth2/login', false, ''), { | |||||
| params: { | |||||
| app_code: appCode, | |||||
| redirect_url: redirectUrl, | |||||
| }, | |||||
| }) as Promise<{ url: string }> | |||||
| } | |||||
| export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => { | export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => { | ||||
| return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise<AppMeta> | return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise<AppMeta> | ||||
| } | } | ||||
| return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true }) | return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true }) | ||||
| } | } | ||||
| export const fetchAccessToken = async (appCode: string, userId?: string) => { | |||||
| export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { appCode: string, userId?: string, webAppAccessToken?: string | null }) => { | |||||
| const headers = new Headers() | const headers = new Headers() | ||||
| headers.append('X-App-Code', appCode) | headers.append('X-App-Code', appCode) | ||||
| const url = userId ? `/passport?user_id=${encodeURIComponent(userId)}` : '/passport' | |||||
| const params = new URLSearchParams() | |||||
| webAppAccessToken && params.append('web_app_access_token', webAppAccessToken) | |||||
| userId && params.append('user_id', userId) | |||||
| const url = `/passport?${params.toString()}` | |||||
| return get(url, { headers }) as Promise<{ access_token: string }> | return get(url, { headers }) as Promise<{ access_token: string }> | ||||
| } | } | ||||
| return get<{ result: boolean }>(`/webapp/permission?appId=${appId}`) | return get<{ result: boolean }>(`/webapp/permission?appId=${appId}`) | ||||
| } | } | ||||
| export const getAppAccessModeByAppCode = (appCode: string) => { | |||||
| return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appCode=${appCode}`) | |||||
| } |
| import { useQuery } from '@tanstack/react-query' | |||||
| import { getAppAccessModeByAppCode } from './share' | |||||
| const NAME_SPACE = 'webapp' | |||||
| export const useAppAccessModeByCode = (code: string | null) => { | |||||
| return useQuery({ | |||||
| queryKey: [NAME_SPACE, 'appAccessMode', code], | |||||
| queryFn: () => { | |||||
| if (!code) | |||||
| return null | |||||
| return getAppAccessModeByAppCode(code) | |||||
| }, | |||||
| enabled: !!code, | |||||
| }) | |||||
| } |