fix: frontend permission check (#784)

This commit is contained in:
Matri
2023-08-15 13:35:47 +08:00
committed by GitHub
parent 440cf63317
commit 2c77a74c40
19 changed files with 186 additions and 74 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import {
Cog8ToothIcon,
DocumentTextIcon,
@@ -22,6 +22,7 @@ import Switch from '@/app/components/base/switch'
import type { AppDetailResponse } from '@/models/app'
import './style.css'
import { AppType } from '@/types/app'
import { useAppContext } from '@/context/app-context'
export type IAppCardProps = {
className?: string
@@ -48,22 +49,30 @@ function AppCard({
}: IAppCardProps) {
const router = useRouter()
const pathname = usePathname()
const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
const [showSettingsModal, setShowSettingsModal] = useState(false)
const [showShareModal, setShowShareModal] = useState(false)
const [showEmbedded, setShowEmbedded] = useState(false)
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
const { t } = useTranslation()
const OPERATIONS_MAP = {
webapp: [
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon },
appInfo.mode === AppType.chat ? { opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon } : false,
{ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon },
].filter(item => !!item),
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
app: [],
}
const OPERATIONS_MAP = useMemo(() => {
const operationsMap = {
webapp: [
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon },
] as { opName: string; opIcon: any }[],
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
app: [],
}
if (appInfo.mode === AppType.chat)
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon })
if (isCurrentWorkspaceManager)
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon })
return operationsMap
}, [isCurrentWorkspaceManager, appInfo, t])
const isApp = cardType === 'app' || cardType === 'webapp'
const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title')
@@ -129,7 +138,7 @@ function AppCard({
<Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}>
{runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')}
</Tag>
<Switch defaultValue={runningStatus} onChange={onChangeStatus} />
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={currentWorkspace?.role === 'normal'} />
</div>
</div>
<div className="flex flex-col justify-center py-2">
@@ -200,6 +209,7 @@ function AppCard({
onClose={() => setShowShareModal(false)}
linkUrl={appUrl}
onGenerateCode={onGenerateCode}
regeneratable={isCurrentWorkspaceManager}
/>
<SettingsModal
appInfo={appInfo}

View File

@@ -17,6 +17,7 @@ type IShareLinkProps = {
onClose: () => void
onGenerateCode: () => Promise<void>
linkUrl: string
regeneratable?: boolean
}
const prefixShare = 'appOverview.overview.appInfo.share'
@@ -26,6 +27,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
isShow,
onClose,
onGenerateCode,
regeneratable,
}) => {
const [genLoading, setGenLoading] = useState(false)
const [isCopied, setIsCopied] = useState(false)
@@ -51,7 +53,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
<LinkIcon className='w-4 h-4 mr-2' />
{ t(`${prefixShare}.${isCopied ? 'linkCopied' : 'copyLink'}`) }
</Button>
<Button className='w-32 !px-0' onClick={async () => {
{regeneratable && <Button className='w-32 !px-0' onClick={async () => {
setGenLoading(true)
await onGenerateCode()
setGenLoading(false)
@@ -59,7 +61,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
}}>
<ArrowPathIcon className={`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`} />
{t(`${prefixShare}.regenerate`)}
</Button>
</Button>}
</div>
</Modal>
}

View File

@@ -18,6 +18,7 @@ import Tooltip from '@/app/components/base/tooltip'
import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
import I18n from '@/context/i18n'
import { useAppContext } from '@/context/app-context'
type ISecretKeyModalProps = {
isShow: boolean
@@ -31,6 +32,7 @@ const SecretKeyModal = ({
onClose,
}: ISecretKeyModalProps) => {
const { t } = useTranslation()
const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [isVisible, setVisible] = useState(false)
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
@@ -118,11 +120,13 @@ const SecretKeyModal = ({
setCopyValue(api.token)
}}></div>
</Tooltip>
<div className={`flex items-center justify-center flex-shrink-0 w-6 h-6 rounded-lg cursor-pointer ${s.trashIcon}`} onClick={() => {
setDelKeyId(api.id)
setShowConfirmDelete(true)
}}>
</div>
{ isCurrentWorkspaceManager
&& <div className={`flex items-center justify-center flex-shrink-0 w-6 h-6 rounded-lg cursor-pointer ${s.trashIcon}`} onClick={() => {
setDelKeyId(api.id)
setShowConfirmDelete(true)
}}>
</div>
}
</div>
</div>
))}
@@ -131,9 +135,7 @@ const SecretKeyModal = ({
)
}
<div className='flex'>
<Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={() =>
onCreate()
}>
<Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={onCreate} disabled={ !currentWorkspace || currentWorkspace.role === 'normal'}>
<PlusIcon className='flex flex-shrink-0 w-4 h-4' />
<div className='text-xs font-medium text-gray-800'>{t('appApi.apiKeyModal.createNewSecretKey')}</div>
</Button>

View File

@@ -8,6 +8,7 @@ import s from './style.module.css'
import NotionIcon from '@/app/components/base/notion-icon'
import { apiPrefix } from '@/config'
import type { DataSourceNotion as TDataSourceNotion } from '@/models/common'
import { useAppContext } from '@/context/app-context'
type DataSourceNotionProps = {
workspaces: TDataSourceNotion[]
@@ -16,6 +17,8 @@ const DataSourceNotion = ({
workspaces,
}: DataSourceNotionProps) => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const connected = !!workspaces.length
return (
@@ -35,18 +38,25 @@ const DataSourceNotion = ({
}
</div>
{
!connected
connected
? (
<Link
className='flex items-center ml-3 px-3 h-7 bg-white border border-gray-200 rounded-md text-xs font-medium text-gray-700 cursor-pointer'
href={`${apiPrefix}/oauth/data-source/notion`}>
className={
`flex items-center ml-3 px-3 h-7 bg-white border border-gray-200
rounded-md text-xs font-medium text-gray-700
${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
}
href={isCurrentWorkspaceManager ? `${apiPrefix}/oauth/data-source/notion` : '/'}>
{t('common.dataSource.connect')}
</Link>
)
: (
<Link
href={`${apiPrefix}/oauth/data-source/notion`}
className='flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md cursor-pointer'>
href={isCurrentWorkspaceManager ? `${apiPrefix}/oauth/data-source/notion` : '/' }
className={
`flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md
${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
}>
<PlusIcon className='w-[14px] h-[14px] mr-[5px]' />
{t('common.dataSource.notion.addWorkspace')}
</Link>

View File

@@ -5,6 +5,7 @@ import type { Status } from './declarations'
type OperateProps = {
isOpen: boolean
status: Status
disabled?: boolean
onCancel: () => void
onSave: () => void
onAdd: () => void
@@ -14,6 +15,7 @@ type OperateProps = {
const Operate = ({
isOpen,
status,
disabled,
onCancel,
onSave,
onAdd,
@@ -44,10 +46,10 @@ const Operate = ({
if (status === 'add') {
return (
<div className='
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
' onClick={onAdd}>
<div className={
`px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center ${disabled && 'opacity-50 cursor-default'}}`
} onClick={() => !disabled && onAdd()}>
{t('common.provider.addKey')}
</div>
)
@@ -69,10 +71,10 @@ const Operate = ({
<Indicator color='green' className='mr-4' />
)
}
<div className='
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
' onClick={onEdit}>
<div className={
`px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center ${disabled && 'opacity-50 cursor-default'}}`
} onClick={() => !disabled && onEdit()}>
{t('common.provider.editKey')}
</div>
</div>

View File

@@ -13,6 +13,7 @@ export type KeyValidatorProps = {
forms: Form[]
keyFrom: KeyFrom
onSave: (v: ValidateValue) => Promise<boolean | undefined>
disabled?: boolean
}
const KeyValidator = ({
@@ -22,6 +23,7 @@ const KeyValidator = ({
forms,
keyFrom,
onSave,
disabled,
}: KeyValidatorProps) => {
const triggerKey = `plugins/${type}`
const { eventEmitter } = useEventEmitterContextContext()
@@ -85,10 +87,11 @@ const KeyValidator = ({
onSave={handleSave}
onAdd={handleAdd}
onEdit={handleEdit}
disabled={disabled}
/>
</div>
{
isOpen && (
isOpen && !disabled && (
<div className='px-4 py-3'>
{
forms.map(form => (

View File

@@ -16,9 +16,9 @@ import { fetchMembers } from '@/service/common'
import I18n from '@/context/i18n'
import { useAppContext } from '@/context/app-context'
import Avatar from '@/app/components/base/avatar'
import { useWorkspacesContext } from '@/context/workspace-context'
dayjs.extend(relativeTime)
const MembersPage = () => {
const { t } = useTranslation()
const RoleMap = {
@@ -27,15 +27,13 @@ const MembersPage = () => {
normal: t('common.members.normal'),
}
const { locale } = useContext(I18n)
const { userProfile } = useAppContext()
const { userProfile, currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers)
const [inviteModalVisible, setInviteModalVisible] = useState(false)
const [invitationLink, setInvitationLink] = useState('')
const [invitedModalVisible, setInvitedModalVisible] = useState(false)
const accounts = data?.accounts || []
const owner = accounts.filter(account => account.role === 'owner')?.[0]?.email === userProfile.email
const { workspaces } = useWorkspacesContext()
const currentWrokspace = workspaces.filter(item => item.current)?.[0]
return (
<>
@@ -43,14 +41,14 @@ const MembersPage = () => {
<div className='flex items-center mb-4 p-3 bg-gray-50 rounded-2xl'>
<div className={cn(s['logo-icon'], 'shrink-0')}></div>
<div className='grow mx-2'>
<div className='text-sm font-medium text-gray-900'>{currentWrokspace?.name}</div>
<div className='text-sm font-medium text-gray-900'>{currentWorkspace?.name}</div>
<div className='text-xs text-gray-500'>{t('common.userProfile.workspace')}</div>
</div>
<div className='
shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
<div className={
`shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
text-[13px] font-medium text-primary-600 bg-white
shadow-xs rounded-lg cursor-pointer
' onClick={() => setInviteModalVisible(true)}>
shadow-xs rounded-lg ${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
} onClick={() => isCurrentWorkspaceManager && setInviteModalVisible(true)}>
<UserPlusIcon className='w-4 h-4 mr-2 ' />
{t('common.members.invite')}
</div>

View File

@@ -6,6 +6,7 @@ import type { Form, ValidateValue } from '../key-validator/declarations'
import { updatePluginKey, validatePluginKey } from './utils'
import { useToastContext } from '@/app/components/base/toast'
import type { PluginProvider } from '@/models/common'
import { useAppContext } from '@/context/app-context'
type SerpapiPluginProps = {
plugin: PluginProvider
@@ -16,6 +17,7 @@ const SerpapiPlugin = ({
onUpdate,
}: SerpapiPluginProps) => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const { notify } = useToastContext()
const forms: Form[] = [{
@@ -70,6 +72,7 @@ const SerpapiPlugin = ({
link: 'https://serpapi.com/manage-api-key',
}}
onSave={handleSave}
disabled={!isCurrentWorkspaceManager}
/>
)
}

View File

@@ -8,6 +8,7 @@ import Indicator from '../indicator'
import type { AppDetailResponse } from '@/models/app'
import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog'
import AppIcon from '@/app/components/base/app-icon'
import { useAppContext } from '@/context/app-context'
type IAppSelectorProps = {
appItems: AppDetailResponse[]
@@ -16,6 +17,7 @@ type IAppSelectorProps = {
export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
const router = useRouter()
const { isCurrentWorkspaceManager } = useAppContext()
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
const { t } = useTranslation()
@@ -77,7 +79,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
))
}
</div>)}
<Menu.Item>
{isCurrentWorkspaceManager && <Menu.Item>
<div className='p-1' onClick={() => setShowNewAppDialog(true)}>
<div
className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100'
@@ -95,6 +97,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
</div>
</div>
</Menu.Item>
}
</Menu.Items>
</Transition>
</Menu>

View File

@@ -1,3 +1,5 @@
'use client'
import Link from 'next/link'
import AccountDropdown from './account-dropdown'
import AppNav from './app-nav'
@@ -8,6 +10,7 @@ import GithubStar from './github-star'
import PluginNav from './plugin-nav'
import s from './index.module.css'
import { WorkspaceProvider } from '@/context/workspace-context'
import { useAppContext } from '@/context/app-context'
const navClassName = `
flex items-center relative mr-3 px-3 h-8 rounded-xl
@@ -16,6 +19,7 @@ const navClassName = `
`
const Header = () => {
const { isCurrentWorkspaceManager } = useAppContext()
return (
<>
<div className='flex items-center'>
@@ -29,7 +33,7 @@ const Header = () => {
<ExploreNav className={navClassName} />
<AppNav />
<PluginNav className={navClassName} />
<DatasetNav />
{isCurrentWorkspaceManager && <DatasetNav />}
</div>
<div className='flex items-center flex-shrink-0'>
<EnvNav />

View File

@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'
import { debounce } from 'lodash-es'
import Indicator from '../../indicator'
import AppIcon from '@/app/components/base/app-icon'
import { useAppContext } from '@/context/app-context'
type NavItem = {
id: string
@@ -29,6 +30,7 @@ const itemClassName = `
const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSelectorProps) => {
const router = useRouter()
const { isCurrentWorkspaceManager } = useAppContext()
const handleScroll = useCallback(debounce((e) => {
if (typeof onLoadmore === 'function') {
@@ -81,7 +83,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel
))
}
</div>
<Menu.Item>
{isCurrentWorkspaceManager && <Menu.Item>
<div className='p-1' onClick={onCreate}>
<div
className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100'
@@ -98,7 +100,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel
<div className='font-normal text-[14px] text-gray-700'>{createText}</div>
</div>
</div>
</Menu.Item>
</Menu.Item>}
</Menu.Items>
</Menu>
</div>