Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Admin Route Extensibility #9043

Merged
merged 2 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions .env.local.default
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,6 @@ CHARGEBEE_SITE=etherealengine-test
CHARGEBEE_API_KEY=
# --------------------------------

# Coil web payment config
# Optional ILP/Interledger payment pointer (such as from an Uphold wallet)
COIL_PAYMENT_POINTER=
# Optional COIL API OAuth2 client registration id
COIL_API_CLIENT_ID=
# Optional COIL API OAuth2 client registration client_secret
COIL_API_CLIENT_SECRET=

# Redish variables ---------------
REDIS_ENABLED=true
Expand All @@ -212,16 +205,6 @@ REDIS_PORT=6379
# REDIS_PASSWORD=
# --------------------------------

#define user scope
DEFAULT_USER_SCOPES=editor:write,location:read,location:write

#define Blockchain url
BLOCKCHAIN_URL=http://localhost:8080/api/v1
BLOCKCHAIN_URL_SECRET=secret

# IPFS
USE_IPFS=

FRONTEND_SERVICE_URL=https://local-matchmaking.etherealengine.io/v1/frontendservice

#define logging url
Expand Down
41 changes: 41 additions & 0 deletions packages/client-core/src/admin/AllowedAdminRoutesState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
CPAL-1.0 License

The contents of this file are subject to the Common Public Attribution License
Version 1.0. (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE.
The License is based on the Mozilla Public License Version 1.1, but Sections 14
and 15 have been added to cover use of software over a computer network and
provide for limited attribution for the Original Developer. In addition,
Exhibit A has been modified to be consistent with Exhibit B.

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
specific language governing rights and limitations under the License.

The Original Code is Ethereal Engine.

The Original Developer is the Initial Developer. The Initial Developer of the
Original Code is the Ethereal Engine team.

All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023
Ethereal Engine. All Rights Reserved.
*/

import { defineState } from '@etherealengine/hyperflux'
import React from 'react'

export type AdminRouteStateType = {
name: string
scope: string
redirect?: string
component: React.LazyExoticComponent<() => JSX.Element>
access: boolean
hanzlamateen marked this conversation as resolved.
Show resolved Hide resolved
icon: JSX.Element
}

export const AllowedAdminRoutesState = defineState({
name: 'AllowedAdminRoutesState',
initial: {} as Record<string, AdminRouteStateType>
})
Original file line number Diff line number Diff line change
Expand Up @@ -23,84 +23,130 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
Ethereal Engine. All Rights Reserved.
*/

import React from 'react'

import Icon from '@etherealengine/ui/src/primitives/mui/Icon'
import React, { lazy } from 'react'
import { AdminRouteStateType } from './AllowedAdminRoutesState'

const Avatars = lazy(() => import('./components/Avatars'))
const Benchmarking = lazy(() => import('./components/Benchmarking'))
const BotSetting = lazy(() => import('./components/Bots'))
const Instance = lazy(() => import('./components/Instance'))
const Invites = lazy(() => import('./components/Invite'))
const Locations = lazy(() => import('./components/Location'))
const Channels = lazy(() => import('./components/Channels'))
const Projects = lazy(() => import('./components/Project'))
const Recordings = lazy(() => import('./components/Recordings'))
const Resources = lazy(() => import('./components/Resources'))
const RoutesComp = lazy(() => import('./components/Routes'))
const Server = lazy(() => import('./components/Server'))
const Setting = lazy(() => import('./components/Setting'))
const Users = lazy(() => import('./components/Users'))

export const SidebarItems = (allowedRoutes) => [
allowedRoutes.analytics && {
export const DefaultAdminRoutes: Record<string, AdminRouteStateType> = {
analytics: {
name: 'user:dashboard.dashboard',
path: '/admin',
scope: '',
redirect: '/admin',
component: Avatars,
access: false,
icon: <Icon type="Dashboard" style={{ color: 'white' }} />
},
allowedRoutes.server && {
server: {
name: 'user:dashboard.server',
path: '/admin/server',
scope: 'server',
component: Server,
access: false,
icon: <Icon type="Storage" style={{ color: 'white' }} />
},
allowedRoutes.projects && {
projects: {
name: 'user:dashboard.projects',
path: '/admin/projects',
scope: 'projects',
component: Projects,
access: false,
icon: <Icon type="Code" style={{ color: 'white' }} />
},
allowedRoutes.routes && {
routes: {
name: 'user:dashboard.routes',
path: '/admin/routes',
scope: 'routes',
component: RoutesComp,
access: false,
icon: <Icon type="Shuffle" style={{ color: 'white' }} />
},
allowedRoutes.location && {
locations: {
name: 'user:dashboard.locations',
path: '/admin/locations',
scope: 'location',
component: Locations,
access: false,
icon: <Icon type="NearMe" style={{ color: 'white' }} />
},
allowedRoutes.instance && {
instance: {
name: 'user:dashboard.instances',
path: '/admin/instance',
scope: 'instance',
component: Instance,
access: false,
icon: <Icon type="DirectionsRun" style={{ color: 'white' }} />
},
allowedRoutes.channel && {
avatars: {
name: 'user:dashboard.avatars',
scope: 'globalAvatars',
component: Avatars,
access: false,
icon: <Icon type="Accessibility" style={{ color: 'white' }} />
},
benchmarking: {
name: 'user:dashboard.benchmarking',
scope: 'benchmarking',
component: Benchmarking,
access: false,
icon: <Icon type="Timeline" style={{ color: 'white' }} />
},
bots: {
name: 'user:dashboard.bots',
scope: 'bot',
component: BotSetting,
access: false,
icon: <Icon type="SmartToy" style={{ color: 'white' }} />
},
channel: {
name: 'user:dashboard.channels',
path: '/admin/channel',
scope: 'channel',
component: Channels,
access: false,
icon: <Icon type="CalendarViewDay" style={{ color: 'white' }} />
},
allowedRoutes.user && {
name: 'user:dashboard.users',
path: '/admin/users',
icon: <Icon type="SupervisorAccount" style={{ color: 'white' }} />
},
allowedRoutes.invite && {
invites: {
name: 'user:dashboard.invites',
path: '/admin/invites',
scope: 'invite',
component: Invites,
access: false,
icon: <Icon type="PersonAdd" style={{ color: 'white' }} />
},
allowedRoutes.globalAvatars && {
name: 'user:dashboard.avatars',
path: '/admin/avatars',
icon: <Icon type="Accessibility" style={{ color: 'white' }} />
recordings: {
name: 'user:dashboard.recordings',
scope: 'recording',
component: Recordings,
access: false,
icon: <Icon type="Videocam" style={{ color: 'white' }} />
},
allowedRoutes.static_resource && {
resources: {
name: 'user:dashboard.resources',
path: '/admin/resources',
scope: 'static_resource',
component: Resources,
access: false,
icon: <Icon type="PermMedia" style={{ color: 'white' }} />
},
allowedRoutes.benchmarking && {
name: 'user:dashboard.benchmarking',
path: '/admin/benchmarking',
icon: <Icon type="Timeline" style={{ color: 'white' }} />
},
allowedRoutes.settings && {
settings: {
name: 'user:dashboard.setting',
path: '/admin/settings',
scope: 'settings',
hanzlamateen marked this conversation as resolved.
Show resolved Hide resolved
component: Setting,
access: false,
icon: <Icon type="Settings" style={{ color: 'white' }} />
},
allowedRoutes.bot && {
name: 'user:dashboard.bots',
path: '/admin/bots',
icon: <Icon type="SmartToy" style={{ color: 'white' }} />
},
allowedRoutes.recording && {
name: 'user:dashboard.recordings',
path: '/admin/recordings',
icon: <Icon type="Videocam" style={{ color: 'white' }} />
users: {
name: 'user:dashboard.users',
scope: 'user',
component: Users,
access: false,
icon: <Icon type="SupervisorAccount" style={{ color: 'white' }} />
}
]
}
46 changes: 17 additions & 29 deletions packages/client-core/src/admin/adminRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import Dashboard from '@etherealengine/ui/src/primitives/mui/Dashboard'
import { LoadingCircle } from '../components/LoadingCircle'
import { AuthState } from '../user/services/AuthService'
import { UserUISystem } from '../user/UserUISystem'
import { AllowedAdminRoutesState } from './AllowedAdminRoutesState'
import Analytics from './components/Analytics'
import { DefaultAdminRoutes } from './DefaultAdminRoutes'

const $allowed = lazy(() => import('@etherealengine/client-core/src/admin/allowedRoutes'))

Expand All @@ -47,41 +49,27 @@ const AdminRoutes = () => {
const location = useLocation()
const admin = useHookstate(getMutableState(AuthState)).user

let allowedRoutes = {
analytics: false,
location: false,
user: false,
bot: false,
scene: false,
channel: false,
instance: false,
invite: false,
globalAvatars: false,
static_resource: false,
benchmarking: false,
routes: false,
projects: false,
settings: false,
server: false,
recording: false
}
const scopes = admin?.scopes?.value || []
const allowedRoutes = useHookstate(getMutableState(AllowedAdminRoutesState))

const scopes = admin?.scopes?.value

useEffect(() => {
AdminSystemInjection()
dispatchAction(EngineActions.initializeEngine({ initialised: true }))
allowedRoutes.set(DefaultAdminRoutes)
}, [])

scopes.forEach((scope) => {
if (Object.keys(allowedRoutes).includes(scope.type.split(':')[0])) {
if (scope.type.split(':')[1] === 'read') {
allowedRoutes = {
...allowedRoutes,
[scope.type.split(':')[0]]: true
}
}
useEffect(() => {
for (const [route, state] of Object.entries(allowedRoutes)) {
const hasScope =
state.scope.value === '' ||
scopes?.find((scope) => {
const [scopeKey, type] = scope.type.split(':')
return scopeKey === state.scope.value
})
state.access.set(!!hasScope)
}
})
}, [scopes])

if (admin?.id?.value?.length! > 0 && !admin?.scopes?.value?.find((scope) => scope.type === 'admin:admin')) {
return <Navigate to={{ pathname: '/' }} />
Expand All @@ -91,7 +79,7 @@ const AdminRoutes = () => {
<Dashboard>
<Suspense fallback={<LoadingCircle message={`Loading ${location.pathname.split('/')[2]}...`} />}>
<Routes>
<Route path="/*" element={<$allowed allowedRoutes={allowedRoutes} />} />
<Route path="/*" element={<$allowed />} />
{<Route path="/" element={<Analytics />} />}
</Routes>
</Suspense>
Expand Down
Loading