From afbd7af36f22c0525536466b9794870f9124b83f Mon Sep 17 00:00:00 2001 From: Kyle Baran Date: Thu, 25 Jan 2024 17:53:07 -0800 Subject: [PATCH] Added support for cross-domain login. For multi-domain setups, having to log in on each different domain is annoying. This saves the user's login information in the 'root' domain of the deployment, and on all domains sources the login credentials from an iframe running in the root domain using postMessage. This only gets skipped if a user has expressly denied cross-domain sharing of their login information, in which case localStorage will be used as the source for login credentials. The iframe loads a new non-vite page in client/public, and does so in the root domain. Some user authorization flow is required on most browsers to enable this, using the requestStorageAccess API. A new backend service, allowed-domains, takes in a domain on a query paramter and returns true if that domain is part of the deployment, and a 204 if not. By default this just returns the root domain, but this is extensible via projects' hooks to add other domains to the allowed list. It can also just be passed a variable isAllowed from a hook if fetching all domains would be cumbersome, and simply querying whether the domain is in a table would be simpler. The accessor iframe uses the response to determine whether to even attempt to access the cookies or prompt the user with requestStorageAccess. --- Dockerfile | 2 + packages/client-core/src/API.ts | 5 +- .../src/social/services/LocationService.ts | 9 +- .../UserMenu/menus/LocationMenu.tsx | 4 +- .../src/user/services/AuthService.ts | 268 +++++++++++++----- .../src/util/wait-for-client-authenticated.ts | 1 - packages/client/index.html | 20 ++ packages/client/public/cookie-accessor.css | 33 +++ .../public/root-cookie-accessor-template.html | 133 +++++++++ packages/client/vite.config.ts | 29 +- .../networking/allowed-domains.schema.ts | 35 +++ packages/ecs/src/Engine.ts | 6 +- .../components/assets/ImageConvertPanel.tsx | 4 +- .../properties/PortalNodeEditor.tsx | 4 +- .../editor/src/functions/assetFunctions.ts | 3 +- .../hyperflux/functions/StateFunctions.ts | 5 +- packages/server-core/src/appconfig.ts | 2 + .../server-core/src/hooks/authenticate.ts | 4 +- .../allowed-domains/allowed-domains.class.ts | 38 +++ .../allowed-domains/allowed-domains.docs.ts | 37 +++ .../allowed-domains/allowed-domains.hooks.ts | 79 ++++++ .../allowed-domains/allowed-domains.ts | 52 ++++ .../server-core/src/networking/services.ts | 2 + .../ui/src/pages/Capture/index.stories.tsx | 2 +- scripts/build_minikube.sh | 11 +- 25 files changed, 686 insertions(+), 102 deletions(-) create mode 100644 packages/client/public/cookie-accessor.css create mode 100644 packages/client/public/root-cookie-accessor-template.html create mode 100644 packages/common/src/schemas/networking/allowed-domains.schema.ts create mode 100755 packages/server-core/src/networking/allowed-domains/allowed-domains.class.ts create mode 100755 packages/server-core/src/networking/allowed-domains/allowed-domains.docs.ts create mode 100755 packages/server-core/src/networking/allowed-domains/allowed-domains.hooks.ts create mode 100755 packages/server-core/src/networking/allowed-domains/allowed-domains.ts diff --git a/Dockerfile b/Dockerfile index fb50d79048..96289a669e 100755 --- a/Dockerfile +++ b/Dockerfile @@ -62,6 +62,7 @@ ARG VITE_READY_PLAYER_ME_URL ARG VITE_DISABLE_LOG ARG VITE_AVATURN_URL ARG VITE_AVATURN_API +ARG VITE_FEATHERS_STORE_KEY ENV MYSQL_HOST=$MYSQL_HOST ENV MYSQL_PORT=$MYSQL_PORT ENV MYSQL_USER=$MYSQL_USER @@ -85,6 +86,7 @@ ENV VITE_READY_PLAYER_ME_URL=$VITE_READY_PLAYER_ME_URL ENV VITE_DISABLE_LOG=$VITE_DISABLE_LOG ENV VITE_AVATURN_URL=$VITE_AVATURN_URL ENV VITE_AVATURN_API=$VITE_AVATURN_API +ENV VITE_FEATHERS_STORE_KEY=$VITE_FEATHERS_STORE_KEY ARG CACHE_DATE RUN npx cross-env ts-node --swc scripts/check-db-exists.ts diff --git a/packages/client-core/src/API.ts b/packages/client-core/src/API.ts index 88332748ec..84e292954e 100755 --- a/packages/client-core/src/API.ts +++ b/packages/client-core/src/API.ts @@ -61,10 +61,7 @@ export class API { }) ) - primus.on('reconnected', () => API.instance.client.reAuthenticate(true)) - - API.instance = new API() - API.instance.client = feathersClient as any + primus.on('reconnected', () => feathersClient.reAuthenticate(true)) Engine.instance.api = feathersClient } diff --git a/packages/client-core/src/social/services/LocationService.ts b/packages/client-core/src/social/services/LocationService.ts index 3ceaddae07..2e02fa675a 100755 --- a/packages/client-core/src/social/services/LocationService.ts +++ b/packages/client-core/src/social/services/LocationService.ts @@ -30,7 +30,6 @@ import { Engine } from '@etherealengine/ecs/src/Engine' import { defineState, getMutableState } from '@etherealengine/hyperflux' import { locationBanPath, SceneID, UserID } from '@etherealengine/common/src/schema.type.module' -import { API } from '../../API' import { NotificationService } from '../../common/services/NotificationService' export const LocationSeed: LocationType = { @@ -129,7 +128,7 @@ export const LocationService = { getLocation: async (locationId: LocationID) => { try { LocationState.fetchingCurrentSocialLocation() - const location = await API.instance.client.service(locationPath).get(locationId) + const location = await Engine.instance.api.service(locationPath).get(locationId) LocationState.socialLocationRetrieved(location) } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) @@ -137,7 +136,7 @@ export const LocationService = { }, getLocationByName: async (locationName: string) => { LocationState.fetchingCurrentSocialLocation() - const locationResult = (await API.instance.client.service(locationPath).find({ + const locationResult = (await Engine.instance.api.service(locationPath).find({ query: { slugifiedName: locationName } @@ -155,7 +154,7 @@ export const LocationService = { } }, getLobby: async () => { - const lobbyResult = (await API.instance.client.service(locationPath).find({ + const lobbyResult = (await Engine.instance.api.service(locationPath).find({ query: { isLobby: true, $limit: 1 @@ -170,7 +169,7 @@ export const LocationService = { }, banUserFromLocation: async (userId: UserID, locationId: LocationID) => { try { - await API.instance.client.service(locationBanPath).create({ + await Engine.instance.api.service(locationBanPath).create({ userId: userId, locationId: locationId }) diff --git a/packages/client-core/src/user/components/UserMenu/menus/LocationMenu.tsx b/packages/client-core/src/user/components/UserMenu/menus/LocationMenu.tsx index 7550119c55..2117a4013e 100755 --- a/packages/client-core/src/user/components/UserMenu/menus/LocationMenu.tsx +++ b/packages/client-core/src/user/components/UserMenu/menus/LocationMenu.tsx @@ -41,7 +41,7 @@ import TextField from '@etherealengine/ui/src/primitives/mui/TextField' import Typography from '@etherealengine/ui/src/primitives/mui/Typography' import { SceneID } from '@etherealengine/common/src/schema.type.module' -import { API } from '../../../../API' +import { Engine } from '@etherealengine/ecs/src/Engine' import { LocationSeed } from '../../../../social/services/LocationService' import styles from '../index.module.scss' @@ -69,7 +69,7 @@ const LocationMenu = (props: Props) => { }, []) const fetchLocations = (page: number, rows: number, search?: string) => { - API.instance.client + Engine.instance.api .service(locationPath) .find({ query: { diff --git a/packages/client-core/src/user/services/AuthService.ts b/packages/client-core/src/user/services/AuthService.ts index d16d0bbf22..5571a1f6c3 100755 --- a/packages/client-core/src/user/services/AuthService.ts +++ b/packages/client-core/src/user/services/AuthService.ts @@ -32,7 +32,15 @@ import config, { validateEmail, validatePhoneNumber } from '@etherealengine/comm import { AuthUserSeed, resolveAuthUser } from '@etherealengine/common/src/interfaces/AuthUser' import multiLogger from '@etherealengine/common/src/logger' import { AuthStrategiesType } from '@etherealengine/common/src/schema.type.module' -import { defineState, getMutableState, getState, syncStateWithLocalStorage } from '@etherealengine/hyperflux' +import { + defineState, + getMutableState, + getState, + stateNamespaceKey, + syncStateWithLocalStorage +} from '@etherealengine/hyperflux' + +import { Static, Type } from '@feathersjs/typebox' import { AvatarID, @@ -61,13 +69,15 @@ import { } from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs/src/Engine' import { AuthenticationResult } from '@feathersjs/authentication' -import { API } from '../../API' +import { FeathersClient } from '../../API' import { NotificationService } from '../../common/services/NotificationService' import { LocationState } from '../../social/services/LocationService' export const logger = multiLogger.child({ component: 'client-core:AuthService' }) export const TIMEOUT_INTERVAL = 50 // ms per interval of waiting for authToken to be updated +const api = Engine.instance.api as FeathersClient + export const UserSeed: UserType = { id: '' as UserID, name: '' as UserName, @@ -109,6 +119,13 @@ export const UserSeed: UserType = { lastLogin: null } +export const hasAccessSchema = Type.Object({ + hasStorageAccess: Type.Boolean(), + cookieSet: Type.Boolean() +}) + +export interface HasAccessType extends Static {} + const resolveWalletUser = (credentials: any): UserType => { return { ...UserSeed, @@ -120,6 +137,114 @@ const resolveWalletUser = (credentials: any): UserType => { } } +const waitForToken = async (win, clientUrl): Promise => { + return new Promise((resolve) => { + win.postMessage( + JSON.stringify({ + key: `${stateNamespaceKey}.AuthState.authUser`, + method: 'get' + }), + clientUrl + ) + const getIframeResponse = function (e) { + if (e.origin !== clientUrl) return + if (e?.data) { + try { + const value = JSON.parse(e.data) + if (value?.accessToken != null) { + window.removeEventListener('message', getIframeResponse) + resolve(value?.accessToken) + } + } catch { + resolve('') + } + } else resolve(e) + } + window.addEventListener('message', getIframeResponse) + }) +} + +const getToken = async (): Promise => { + let gotResponse = false + const iframe = document.getElementById('root-cookie-accessor') as HTMLFrameElement + let win + try { + win = iframe!.contentWindow + } catch (e) { + win = iframe!.contentWindow + } + + window.addEventListener('message', (e) => { + if (e?.data) { + try { + const value = JSON.parse(e.data) + if (value?.invalidDomain != null) { + localStorage.setItem('invalidCrossOriginDomain', 'true') + } + } catch (err) { + // + } + } + }) + + const clientUrl = config.client.clientUrl + let iteration = 0 + const hasAccess = (await new Promise((resolve) => { + const checkAccessInterval = setInterval(() => { + if (iteration > 4) { + clearInterval(checkAccessInterval) + resolve({ cookieSet: false, hasStorageAccess: false }) + } + if (!gotResponse) { + iteration++ + win.postMessage(JSON.stringify({ method: 'checkAccess' }), clientUrl) + } else clearInterval(checkAccessInterval) + }, 100) + const hasAccessListener = async function (e) { + gotResponse = true + window.removeEventListener('message', hasAccessListener) + if (!e.data) resolve({ hasStorageAccess: false, cookieSet: false }) + const data = JSON.parse(e.data) + resolve(data) + } + window.addEventListener('message', hasAccessListener) + })) as HasAccessType + + if (!hasAccess.cookieSet || !hasAccess.hasStorageAccess) { + const skipCheck = localStorage.getItem('skipCrossOriginCookieCheck') + const invalidCrossOriginDomain = localStorage.getItem('invalidCrossOriginDomain') + if (skipCheck === 'true' || invalidCrossOriginDomain === 'true') { + const authState = getMutableState(AuthState) + const accessToken = authState?.authUser?.accessToken?.value + return Promise.resolve(accessToken?.length > 0 ? accessToken : '') + } else { + iframe.style.display = 'block' + return await new Promise((resolve) => { + const clickResponseListener = async function (e) { + try { + window.removeEventListener('message', clickResponseListener) + const parsed = !e.data ? {} : JSON.parse(e.data) + if (parsed.skipCrossOriginCookieCheck != null) { + localStorage.setItem('skipCrossOriginCookieCheck', parsed.skipCrossOriginCookieCheck) + iframe.style.display = 'none' + resolve('') + } else { + const token = await waitForToken(win, clientUrl) + iframe.style.display = 'none' + resolve(token) + } + } catch (err) { + //Do nothing + } + } + window.addEventListener('message', clickResponseListener) + }) + } + } else { + return waitForToken(win, clientUrl) + } +} + export const AuthState = defineState({ name: 'AuthState', initial: () => ({ @@ -152,21 +277,40 @@ export interface LinkedInLoginForm { email: string } +export const writeAuthUserToIframe = () => { + const iframe = document.getElementById('root-cookie-accessor') as HTMLFrameElement + let win + try { + win = iframe!.contentWindow + } catch (e) { + win = iframe!.contentWindow + } + + win.postMessage( + JSON.stringify({ + key: `${stateNamespaceKey}.${AuthState.name}.authUser`, + method: 'set', + data: getState(AuthState).authUser + }), + config.client.clientUrl + ) +} + /** * Resets the current user's accessToken to a new random guest token. */ async function _resetToGuestToken(options = { reset: true }) { if (options.reset) { - await API.instance.client.authentication.reset() + await api.authentication.reset() } - const newProvider = await Engine.instance.api.service(identityProviderPath).create({ + const newProvider = await api.service(identityProviderPath).create({ type: 'guest', token: v1(), userId: '' as UserID }) const accessToken = newProvider.accessToken! - console.log(`Created new guest accessToken: ${accessToken}`) - await API.instance.client.authentication.setAccessToken(accessToken as string) + await api.authentication.setAccessToken(accessToken as string) + writeAuthUserToIframe() return accessToken } @@ -175,29 +319,25 @@ export const AuthService = { // Oauth callbacks may be running when a guest identity-provider has been deleted. // This would normally cause doLoginAuto to make a guest user, which we do not want. // Instead, just skip it on oauth callbacks, and the callback handler will log them in. - // The client and auth settigns will not be needed on these routes + // The client and auth settings will not be needed on these routes if (/auth\/oauth/.test(location.pathname)) return const authState = getMutableState(AuthState) try { - const accessToken = !forceClientAuthReset && authState?.authUser?.accessToken?.value + const rootDomainToken = await getToken() - if (forceClientAuthReset) { - await API.instance.client.authentication.reset() - } - if (accessToken) { - await API.instance.client.authentication.setAccessToken(accessToken as string) - } else { - await _resetToGuestToken({ reset: false }) - } + if (forceClientAuthReset) await api.authentication.reset() + + if (rootDomainToken?.length > 0) await api.authentication.setAccessToken(rootDomainToken as string) + else await _resetToGuestToken({ reset: false }) let res: AuthenticationResult try { - res = await API.instance.client.reAuthenticate() + res = await api.reAuthenticate() } catch (err) { if (err.className === 'not-found' || (err.className === 'not-authenticated' && err.message === 'jwt expired')) { authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed }) await _resetToGuestToken() - res = await API.instance.client.reAuthenticate() + res = await api.reAuthenticate() } else { logger.error(err, 'Error re-authenticating') throw err @@ -209,18 +349,20 @@ export const AuthService = { if (!identityProvider?.id) { authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed }) await _resetToGuestToken() - res = await API.instance.client.reAuthenticate() + res = await api.reAuthenticate() } const authUser = resolveAuthUser(res) // authUser is now { accessToken, authentication, identityProvider } authState.merge({ authUser }) - await AuthService.loadUserData(authUser.identityProvider?.userId) + writeAuthUserToIframe() + await AuthService.loadUserData(authUser.identityProvider.userId) } else { logger.warn('No response received from reAuthenticate()!') } } catch (err) { logger.error(err, 'Error on resolving auth user in doLoginAuto, logging out') authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed }) + writeAuthUserToIframe() // if (window.location.pathname !== '/') { // window.location.href = '/'; @@ -230,15 +372,14 @@ export const AuthService = { async loadUserData(userId: UserID) { try { - const client = API.instance.client - const user = await client.service(userPath).get(userId) + const user = await api.service(userPath).get(userId) if (!user.userSetting) { - const settingsRes = (await client + const settingsRes = (await api .service(userSettingPath) .find({ query: { userId: userId } })) as Paginated if (settingsRes.total === 0) { - user.userSetting = await client.service(userSettingPath).create({ userId: userId }) + user.userSetting = await api.service(userSettingPath).create({ userId: userId }) } else { user.userSetting = settingsRes.data[0] } @@ -262,7 +403,7 @@ export const AuthService = { authState.merge({ isProcessing: true, error: '' }) try { - const authenticationResult = await API.instance.client.authenticate({ + const authenticationResult = await api.authenticate({ strategy: 'local', email: form.email, password: form.password @@ -307,7 +448,6 @@ export const AuthService = { authState.merge({ isProcessing: true, error: '' }) const credentials: any = parseUserWalletCredentials(vprResult) - console.log(credentials) const walletUser = resolveWalletUser(credentials) const authUser = { @@ -324,8 +464,7 @@ export const AuthService = { } // TODO: This is temp until we move completely to XR wallet #6453 - const oldId = authState.user.id.value - walletUser.id = oldId + walletUser.id = authState.user.id.value // loadXRAvatarForUpdatedUser(walletUser) authState.merge({ isLoggedIn: true, user: walletUser, authUser }) @@ -362,24 +501,24 @@ export const AuthService = { }, async removeUserOAuth(service: string) { - const ipResult = (await Engine.instance.api.service(identityProviderPath).find()) as Paginated + const ipResult = (await api.service(identityProviderPath).find()) as Paginated const ipToRemove = ipResult.data.find((ip) => ip.type === service) if (ipToRemove) { if (ipResult.total === 1) { NotificationService.dispatchNotify('You can not remove your last login method.', { variant: 'warning' }) } else { const otherIp = ipResult.data.find((ip) => ip.type !== service) - const newTokenResult = await Engine.instance.api.service(generateTokenPath).create({ + const newTokenResult = await api.service(generateTokenPath).create({ type: otherIp!.type, token: otherIp!.token }) if (newTokenResult?.token) { getMutableState(AuthState).merge({ isProcessing: true, error: '' }) - await API.instance.client.authentication.setAccessToken(newTokenResult.token) - const res = await API.instance.client.reAuthenticate(true) + await api.authentication.setAccessToken(newTokenResult.token) + const res = await api.reAuthenticate(true) const authUser = resolveAuthUser(res) - await Engine.instance.api.service(identityProviderPath).remove(ipToRemove.id) + await api.service(identityProviderPath).remove(ipToRemove.id) const authState = getMutableState(AuthState) authState.merge({ authUser }) await AuthService.loadUserData(authUser.identityProvider.userId) @@ -393,14 +532,15 @@ export const AuthService = { const authState = getMutableState(AuthState) authState.merge({ isProcessing: true, error: '' }) try { - await API.instance.client.authentication.setAccessToken(accessToken as string) - const res = await API.instance.client.authenticate({ + await api.authentication.setAccessToken(accessToken as string) + const res = await api.authenticate({ strategy: 'jwt', accessToken }) const authUser = resolveAuthUser(res) authState.merge({ authUser }) + writeAuthUserToIframe() await AuthService.loadUserData(authUser.identityProvider?.userId) authState.merge({ isProcessing: false, error: '' }) let timeoutTimer = 0 @@ -409,8 +549,7 @@ export const AuthService = { // in properly. This interval waits to make sure the token has been updated before redirecting const waitForTokenStored = setInterval(() => { timeoutTimer += TIMEOUT_INTERVAL - const authData = authState - const storedToken = authData.authUser?.accessToken?.value + const storedToken = authState.authUser?.accessToken?.value if (storedToken === accessToken) { clearInterval(waitForTokenStored) window.location.href = redirectSuccess @@ -430,7 +569,7 @@ export const AuthService = { async loginUserMagicLink(token, redirectSuccess, redirectError) { try { - const res = await Engine.instance.api.service(loginPath).get(token) + const res = await api.service(loginPath).get(token) await AuthService.loginUserByJwt(res.token!, '/', '/') } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) @@ -443,12 +582,13 @@ export const AuthService = { const authState = getMutableState(AuthState) authState.merge({ isProcessing: true, error: '' }) try { - await API.instance.client.logout() + await api.logout() authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed }) } catch (_) { authState.merge({ isLoggedIn: false, user: UserSeed, authUser: AuthUserSeed }) } finally { authState.merge({ isProcessing: false, error: '' }) + writeAuthUserToIframe() AuthService.doLoginAuto(true) } }, @@ -457,7 +597,7 @@ export const AuthService = { const authState = getMutableState(AuthState) authState.merge({ isProcessing: true, error: '' }) try { - const identityProvider: any = await Engine.instance.api.service(identityProviderPath).create({ + const identityProvider: any = await api.service(identityProviderPath).create({ token: form.email, type: 'password', userId: '' as UserID @@ -516,7 +656,7 @@ export const AuthService = { } try { - await Engine.instance.api.service(magicLinkPath).create({ type, [paramName]: emailPhone }) + await api.service(magicLinkPath).create({ type, [paramName]: emailPhone }) NotificationService.dispatchNotify(i18n.t('user:auth.magiklink.success-msg'), { variant: 'success' }) } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) @@ -530,7 +670,7 @@ export const AuthService = { authState.merge({ isProcessing: true, error: '' }) try { - const identityProvider = await Engine.instance.api.service(identityProviderPath).create({ + const identityProvider = await api.service(identityProviderPath).create({ token: form.email, type: 'password', userId: '' as UserID @@ -548,7 +688,7 @@ export const AuthService = { const authState = getMutableState(AuthState) authState.merge({ isProcessing: true, error: '' }) try { - const identityProvider = (await Engine.instance.api.service(magicLinkPath).create({ + const identityProvider = (await api.service(magicLinkPath).create({ email, type: 'email', userId @@ -574,7 +714,7 @@ export const AuthService = { } try { - const identityProvider = (await Engine.instance.api.service(magicLinkPath).create({ + const identityProvider = (await api.service(magicLinkPath).create({ mobile: sendPhone, type: 'sms', userId @@ -600,7 +740,7 @@ export const AuthService = { async removeConnection(identityProviderId: number, userId: UserID) { getMutableState(AuthState).merge({ isProcessing: true, error: '' }) try { - await Engine.instance.api.service(identityProviderPath).remove(identityProviderId) + await api.service(identityProviderPath).remove(identityProviderId) return AuthService.loadUserData(userId) } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) @@ -614,45 +754,41 @@ export const AuthService = { }, async updateUserSettings(id: UserSettingID, data: UserSettingPatch) { - const response = await Engine.instance.api.service(userSettingPath).patch(id, data) + const response = await api.service(userSettingPath).patch(id, data) getMutableState(AuthState).user.userSetting.merge(response) }, async removeUser(userId: UserID) { - await Engine.instance.api.service(userPath).remove(userId) + await api.service(userPath).remove(userId) AuthService.logoutUser() }, async updateApiKey() { - const userApiKey = (await Engine.instance.api.service(userApiKeyPath).find()) as Paginated + const userApiKey = (await api.service(userApiKeyPath).find()) as Paginated let apiKey: UserApiKeyType | undefined if (userApiKey.data.length > 0) { - apiKey = await Engine.instance.api.service(userApiKeyPath).patch(userApiKey.data[0].id, {}) + apiKey = await api.service(userApiKeyPath).patch(userApiKey.data[0].id, {}) } else { - apiKey = await Engine.instance.api.service(userApiKeyPath).create({}) + apiKey = await api.service(userApiKeyPath).create({}) } getMutableState(AuthState).user.merge({ apiKey }) }, async updateUsername(userId: UserID, name: UserName) { - const { name: updatedName } = (await Engine.instance.api - .service(userPath) - .patch(userId, { name: name })) as UserType + const { name: updatedName } = (await api.service(userPath).patch(userId, { name: name })) as UserType NotificationService.dispatchNotify(i18n.t('user:usermenu.profile.update-msg'), { variant: 'success' }) getMutableState(AuthState).user.merge({ name: updatedName }) }, async createLoginToken() { - return Engine.instance.api.service(loginTokenPath).create({}) + return api.service(loginTokenPath).create({}) }, useAPIListeners: () => { useEffect(() => { const userPatchedListener = (user: UserPublicPatch | UserPatch) => { - console.log('USER PATCHED %o', user) - if (!user.id) return const selfUser = getMutableState(AuthState).user @@ -663,14 +799,12 @@ export const AuthService = { } const userAvatarPatchedListener = async (userAvatar: UserAvatarPatch) => { - console.log('USER AVATAR PATCHED %o', userAvatar) - if (!userAvatar.userId) return const selfUser = getMutableState(AuthState).user if (selfUser.id.value === userAvatar.userId) { - const user = await Engine.instance.api.service(userPath).get(userAvatar.userId) + const user = await api.service(userPath).get(userAvatar.userId) getMutableState(AuthState).user.merge(user) } } @@ -681,19 +815,19 @@ export const AuthService = { const locationBan = params.locationBan if (selfUser.id === locationBan.userId && currentLocation.id === locationBan.locationId) { const userId = selfUser.id ?? '' - const user = await Engine.instance.api.service(userPath).get(userId) + const user = await api.service(userPath).get(userId) getMutableState(AuthState).merge({ user }) } } - Engine.instance.api.service(userPath).on('patched', userPatchedListener) - Engine.instance.api.service(userAvatarPath).on('patched', userAvatarPatchedListener) - Engine.instance.api.service(locationBanPath).on('created', locationBanCreatedListener) + api.service(userPath).on('patched', userPatchedListener) + api.service(userAvatarPath).on('patched', userAvatarPatchedListener) + api.service(locationBanPath).on('created', locationBanCreatedListener) return () => { - Engine.instance.api.service(userPath).off('patched', userPatchedListener) - Engine.instance.api.service(userAvatarPath).off('patched', userAvatarPatchedListener) - Engine.instance.api.service(locationBanPath).off('created', locationBanCreatedListener) + api.service(userPath).off('patched', userPatchedListener) + api.service(userAvatarPath).off('patched', userAvatarPatchedListener) + api.service(locationBanPath).off('created', locationBanCreatedListener) } }, []) } @@ -703,8 +837,6 @@ export const AuthService = { * @param vprResult {any} See `loginUserByXRWallet()`'s docstring. */ function parseUserWalletCredentials(vprResult: any) { - console.log('PARSING:', vprResult) - const { data: { presentation: vp } } = vprResult diff --git a/packages/client-core/src/util/wait-for-client-authenticated.ts b/packages/client-core/src/util/wait-for-client-authenticated.ts index b52082e8dc..832fb96011 100644 --- a/packages/client-core/src/util/wait-for-client-authenticated.ts +++ b/packages/client-core/src/util/wait-for-client-authenticated.ts @@ -24,7 +24,6 @@ Ethereal Engine. All Rights Reserved. */ import { Engine } from '@etherealengine/ecs/src/Engine' - import { FeathersClient } from '../API' async function waitForClientAuthenticated(): Promise { diff --git a/packages/client/index.html b/packages/client/index.html index aa77cae2c1..da3379d4bd 100755 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -47,10 +47,30 @@ }) }) } + + //Base height and width of cross-origin iframe + const w = 400 + const h = 400 + + let width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; + let height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; + + const left = (width - w) / 2 + const topSetting = (height - h) / 2 + + document.addEventListener('DOMContentLoaded', async (event) => { + const rootIframe = document.getElementById('root-cookie-accessor') + rootIframe.style.width = `${w}px` + rootIframe.style.height = `${h}px` + rootIframe.style.top = `${topSetting}px` + rootIframe.style.left = `${left}px` + })
+ + diff --git a/packages/client/public/cookie-accessor.css b/packages/client/public/cookie-accessor.css new file mode 100644 index 0000000000..d3276fabcf --- /dev/null +++ b/packages/client/public/cookie-accessor.css @@ -0,0 +1,33 @@ +body { + display: flex; + flex-direction: column; + margin: auto; + text-align: center; + background-image: linear-gradient(150deg, #00022e, rgba(255, 0, 125, 0.2901960784)); + color: white; +} + +.button-container { + display: flex; + justify-content: space-around; +} + +button { + width: 150px; + border-radius: 5px; + border: 2px solid transparent; + color: white; + cursor: pointer; +} + +button:hover { + border-color: black; +} + +#confirmButton { + background-color: darkgreen; +} + +#cancelButton { + background-color: darkred; +} \ No newline at end of file diff --git a/packages/client/public/root-cookie-accessor-template.html b/packages/client/public/root-cookie-accessor-template.html new file mode 100644 index 0000000000..1ee4c1ef81 --- /dev/null +++ b/packages/client/public/root-cookie-accessor-template.html @@ -0,0 +1,133 @@ + + + + Local Storage Accessor + + + + +

This site runs on Ethereal Engine. To automatically use your login across all Ethereal Engine-powered sites, please confirm below. If you decline, then each Ethereal Engine-powered site will log in separately, and you will receive this prompt on each site.

+
+ + +
+ + \ No newline at end of file diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index d2f6e9c570..b8c0995592 100755 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -26,7 +26,7 @@ Ethereal Engine. All Rights Reserved. import { viteCommonjs } from '@originjs/vite-plugin-commonjs' import packageRoot from 'app-root-path' import dotenv from 'dotenv' -import fs from 'fs' +import fs, { readFileSync, writeFileSync } from 'fs' import lodash from 'lodash' import path from 'path' import { UserConfig, defineConfig } from 'vite' @@ -36,6 +36,7 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills' import svgr from 'vite-plugin-svgr' const { isArray, mergeWith } = lodash +import appRootPath from 'app-root-path' import manifest from './manifest.default.json' import PWA from './pwa.config' import { getClientSetting } from './scripts/getClientSettings' @@ -230,6 +231,23 @@ const resetSWFiles = () => { deleteDirFilesUsingPattern(/workbox-/, './public/') } +const updateRootCookieAccessorDomain = (isDevOrLocal) => { + const localStorageAccessor = readFileSync( + path.join(appRootPath.path, 'packages', 'client', 'public', 'root-cookie-accessor-template.html') + ).toString() + + const apiUrl = + isDevOrLocal && process.env.VITE_LOCAL_NGINX !== 'true' + ? `https://${process.env.VITE_SERVER_HOST}:${process.env.VITE_SERVER_PORT}` + : `https://${process.env.VITE_SERVER_HOST}` + const updated = localStorageAccessor.replace(//g, apiUrl) + + writeFileSync( + path.join(appRootPath.path, 'packages', 'client', 'public', 'root-cookie-accessor.html'), + Buffer.from(updated) + ) +} + export default defineConfig(async () => { dotenv.config({ path: packageRoot.path + '/.env.local' @@ -241,6 +259,8 @@ export default defineConfig(async () => { const isDevOrLocal = process.env.APP_ENV === 'development' || process.env.VITE_LOCAL_BUILD === 'true' + updateRootCookieAccessorDomain(isDevOrLocal) + let base = `https://${process.env['APP_HOST'] ? process.env['APP_HOST'] : process.env['VITE_APP_HOST']}/` if (process.env.SERVE_CLIENT_FROM_STORAGE_PROVIDER === 'true') { @@ -260,7 +280,7 @@ export default defineConfig(async () => { define: define, server: { proxy: {}, - cors: isDevOrLocal ? false : true, + cors: !isDevOrLocal, hmr: process.env.VITE_HMR === 'true' ? { @@ -316,7 +336,8 @@ export default defineConfig(async () => { ? 'dev-sw.js?dev-sw' : 'service-worker.js' : '', - paymentPointer: coilSetting?.paymentPointer || '' + paymentPointer: coilSetting?.paymentPointer || '', + rootCookieAccessor: `${clientSetting.url}/root-cookie-accessor.html` }), viteCompression({ filter: /\.(js|mjs|json|css)$/i, @@ -334,7 +355,7 @@ export default defineConfig(async () => { }, build: { target: 'esnext', - sourcemap: process.env.VITE_SOURCEMAPS === 'true' ? true : false, + sourcemap: process.env.VITE_SOURCEMAPS === 'true', minify: 'terser', dynamicImportVarsOptions: { warnOnError: true diff --git a/packages/common/src/schemas/networking/allowed-domains.schema.ts b/packages/common/src/schemas/networking/allowed-domains.schema.ts new file mode 100644 index 0000000000..5c4d68d684 --- /dev/null +++ b/packages/common/src/schemas/networking/allowed-domains.schema.ts @@ -0,0 +1,35 @@ +/* +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 { Type } from '@feathersjs/typebox' + +// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html + +export const allowedDomainsPath = 'allowed-domains' + +export const allowedDomainsMethods = ['find'] as const + +export const allowedDomainsSchema = Type.Boolean() +export interface AllowedDomainsType {} diff --git a/packages/ecs/src/Engine.ts b/packages/ecs/src/Engine.ts index 9afe49ced4..5c1aafb17c 100755 --- a/packages/ecs/src/Engine.ts +++ b/packages/ecs/src/Engine.ts @@ -29,10 +29,8 @@ import { ReactorReconciler, createHyperStore, getState } from '@etherealengine/h import { HyperFlux, HyperStore, disposeStore } from '@etherealengine/hyperflux/functions/StoreFunctions' import * as bitECS from 'bitecs' -import type { FeathersApplication } from '@feathersjs/feathers' - -import type { ServiceTypes } from '@etherealengine/common/declarations' - +import { ServiceTypes } from '@etherealengine/common/declarations' +import { FeathersApplication } from '@feathersjs/feathers' import { getAllEntities } from 'bitecs' import { Group, Scene } from 'three' import { ECSState } from './ECSState' diff --git a/packages/editor/src/components/assets/ImageConvertPanel.tsx b/packages/editor/src/components/assets/ImageConvertPanel.tsx index 03cba074e8..24f58a750b 100644 --- a/packages/editor/src/components/assets/ImageConvertPanel.tsx +++ b/packages/editor/src/components/assets/ImageConvertPanel.tsx @@ -25,7 +25,7 @@ Ethereal Engine. All Rights Reserved. import React from 'react' -import { API } from '@etherealengine/client-core/src/API' +import { Engine } from '@etherealengine/ecs/src/Engine' import { ImageConvertParms } from '@etherealengine/engine/src/assets/constants/ImageConvertParms' import { State } from '@etherealengine/hyperflux' @@ -51,7 +51,7 @@ export default function ImageConvertPanel({ function convertImage() { const props = fileProperties.value convertProperties.src.set(props.type === 'folder' ? `${props.url}/${props.key}` : props.url) - API.instance.client + Engine.instance.api .service('image-convert') .create(convertProperties.value) .then(() => { diff --git a/packages/editor/src/components/properties/PortalNodeEditor.tsx b/packages/editor/src/components/properties/PortalNodeEditor.tsx index e4c0e9c126..3a25c54c80 100755 --- a/packages/editor/src/components/properties/PortalNodeEditor.tsx +++ b/packages/editor/src/components/properties/PortalNodeEditor.tsx @@ -27,9 +27,9 @@ import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Euler, Quaternion, Vector3 } from 'three' -import { API } from '@etherealengine/client-core/src/API' import { UUIDComponent } from '@etherealengine/ecs' import { getComponent, useComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { Engine } from '@etherealengine/ecs/src/Engine' import { PortalComponent, PortalEffects, @@ -95,7 +95,7 @@ export const PortalNodeEditor: EditorComponentType = (props) => { const portalsDetail: PortalType[] = [] try { portalsDetail.push( - ...((await API.instance.client.service(portalPath).find({ query: { paginate: false } })) as PortalType[]) + ...((await Engine.instance.api.service(portalPath).find({ query: { paginate: false } })) as PortalType[]) ) console.log('portalsDetail', portalsDetail, getComponent(props.entity, UUIDComponent)) } catch (error) { diff --git a/packages/editor/src/functions/assetFunctions.ts b/packages/editor/src/functions/assetFunctions.ts index 4dc6ecf9df..ae6f9c109a 100644 --- a/packages/editor/src/functions/assetFunctions.ts +++ b/packages/editor/src/functions/assetFunctions.ts @@ -23,7 +23,6 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { API } from '@etherealengine/client-core/src/API' import { FileBrowserService } from '@etherealengine/client-core/src/common/services/FileBrowserService' import { CancelableUploadPromiseArrayReturnType, @@ -123,7 +122,7 @@ export const uploadProjectFiles = (projectName: string, files: File[], isAsset = export async function clearModelResources(projectName: string, modelName: string) { const resourcePath = `projects/${projectName}/assets/${modelResourcesPath(modelName)}` - const exists = await API.instance.client.service(fileBrowserPath).get(resourcePath) + const exists = await Engine.instance.api.service(fileBrowserPath).get(resourcePath) if (exists) { await FileBrowserService.deleteContent(resourcePath) } diff --git a/packages/hyperflux/functions/StateFunctions.ts b/packages/hyperflux/functions/StateFunctions.ts index 8b1888c329..cdc47aad80 100644 --- a/packages/hyperflux/functions/StateFunctions.ts +++ b/packages/hyperflux/functions/StateFunctions.ts @@ -27,7 +27,6 @@ import { createState, SetInitialStateAction, State, useHookstate } from '@hookst import type { Object as _Object, Function, String } from 'ts-toolbelt' import { DeepReadonly } from '@etherealengine/common/src/DeepReadonly' -import multiLogger from '@etherealengine/common/src/logger' import { isClient } from '@etherealengine/common/src/utils/getEnvironment' import { resolveObject } from '@etherealengine/common/src/utils/resolveObject' @@ -37,8 +36,6 @@ import { HyperFlux, HyperStore } from './StoreFunctions' export * from '@hookstate/core' -const logger = multiLogger.child({ component: 'hyperflux:State' }) - export const NO_PROXY = { noproxy: true } export const NO_PROXY_STEALTH = { noproxy: true, stealth: true } @@ -103,7 +100,7 @@ export function useMutableState( return useHookstate(resolvedState) as any } -const stateNamespaceKey = 'ee.hyperflux' +export const stateNamespaceKey = 'ee.hyperflux' /** * Automatically synchronises specific root paths of a hyperflux state definition with the localStorage. diff --git a/packages/server-core/src/appconfig.ts b/packages/server-core/src/appconfig.ts index e0b0eb5715..90145919f8 100755 --- a/packages/server-core/src/appconfig.ts +++ b/packages/server-core/src/appconfig.ts @@ -30,6 +30,7 @@ import path from 'path' import url from 'url' import { oembedPath } from '@etherealengine/common/src/schemas/media/oembed.schema' +import { allowedDomainsPath } from '@etherealengine/common/src/schemas/networking/allowed-domains.schema' import { routePath } from '@etherealengine/common/src/schemas/route/route.schema' import { acceptInvitePath } from '@etherealengine/common/src/schemas/user/accept-invite.schema' import { discordBotAuthPath } from '@etherealengine/common/src/schemas/user/discord-bot-auth.schema' @@ -260,6 +261,7 @@ const authentication = { 'auth', 'oauth/:provider', 'authentication', + allowedDomainsPath, oembedPath, githubRepoAccessWebhookPath, { path: identityProviderPath, methods: ['create'] }, diff --git a/packages/server-core/src/hooks/authenticate.ts b/packages/server-core/src/hooks/authenticate.ts index 49c8e3c068..1509283270 100644 --- a/packages/server-core/src/hooks/authenticate.ts +++ b/packages/server-core/src/hooks/authenticate.ts @@ -31,8 +31,8 @@ import { UserType, userPath } from '@etherealengine/common/src/schemas/user/user import { toDateTimeSql } from '@etherealengine/common/src/utils/datetime-sql' import { AsyncLocalStorage } from 'async_hooks' import { isProvider } from 'feathers-hooks-common' +import { Application } from '../../declarations' import config from '../appconfig' -import { Application } from './../../declarations' const { authenticate } = authentication.hooks @@ -112,7 +112,7 @@ export default async (context: HookContext, next: NextFunction): Pr /** * A method to check if the service requesting is whitelisted. - * In that scenario we dont need to perform authentication check. + * In that scenario we don't need to perform authentication check. */ const checkWhitelist = (context: HookContext): boolean => { for (const item of config.authentication.whiteList) { diff --git a/packages/server-core/src/networking/allowed-domains/allowed-domains.class.ts b/packages/server-core/src/networking/allowed-domains/allowed-domains.class.ts new file mode 100755 index 0000000000..8b51b58854 --- /dev/null +++ b/packages/server-core/src/networking/allowed-domains/allowed-domains.class.ts @@ -0,0 +1,38 @@ +/* +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 { AllowedDomainsType } from '@etherealengine/common/src/schemas/networking/allowed-domains.schema' +import { Params } from '@feathersjs/feathers' +import { KnexAdapterParams } from '@feathersjs/knex' +import { BaseService } from '../../BaseService' +export interface AllowedDomainParams extends KnexAdapterParams { + additionalDomains?: string[] + isAllowed?: boolean +} + +export class AllowedDomainsService< + T = AllowedDomainsType, + ServiceParams extends Params = AllowedDomainParams +> extends BaseService {} diff --git a/packages/server-core/src/networking/allowed-domains/allowed-domains.docs.ts b/packages/server-core/src/networking/allowed-domains/allowed-domains.docs.ts new file mode 100755 index 0000000000..9ed46f24ea --- /dev/null +++ b/packages/server-core/src/networking/allowed-domains/allowed-domains.docs.ts @@ -0,0 +1,37 @@ +/* +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 { allowedDomainsSchema } from '@etherealengine/common/src/schemas/networking/allowed-domains.schema' +import { createSwaggerServiceOptions } from 'feathers-swagger' + +export default createSwaggerServiceOptions({ + schemas: { + allowedDomainsSchema + }, + docs: { + description: 'Allowed-domain service description', + securities: ['all'] + } +}) diff --git a/packages/server-core/src/networking/allowed-domains/allowed-domains.hooks.ts b/packages/server-core/src/networking/allowed-domains/allowed-domains.hooks.ts new file mode 100755 index 0000000000..cddd633541 --- /dev/null +++ b/packages/server-core/src/networking/allowed-domains/allowed-domains.hooks.ts @@ -0,0 +1,79 @@ +/* +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 { disallow } from 'feathers-hooks-common' + +import { createSkippableHooks } from '../../hooks/createSkippableHooks' + +import logRequest from '@etherealengine/server-core/src/hooks/log-request' +import { HookContext } from '../../../declarations' +import appConfig from '../../appconfig' +import { AllowedDomainsService } from './allowed-domains.class' + +// Don't remove this comment. It's needed to format import lines nicely. + +const checkDomain = async (context: HookContext) => { + const { params } = context + const domainToCheck = params?.query?.domainToCheck + const additionalDomains = params.additionalDomains + const isAllowed = params.isAllowed + let allowedDomains = [`https://${appConfig.server.clientHost}`] + + if (additionalDomains && Array.isArray(additionalDomains)) allowedDomains = allowedDomains.concat(additionalDomains) + + context.result = isAllowed || allowedDomains.indexOf(domainToCheck) > -1 +} + +export default createSkippableHooks({ + before: { + all: [logRequest()], + find: [checkDomain], + get: [disallow()], + create: [disallow()], + update: [disallow()], + patch: [disallow()], + remove: [disallow()] + }, + + after: { + all: [], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + }, + + error: { + all: [logRequest()], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + } +}) diff --git a/packages/server-core/src/networking/allowed-domains/allowed-domains.ts b/packages/server-core/src/networking/allowed-domains/allowed-domains.ts new file mode 100755 index 0000000000..186f974609 --- /dev/null +++ b/packages/server-core/src/networking/allowed-domains/allowed-domains.ts @@ -0,0 +1,52 @@ +/* +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 { + allowedDomainsMethods, + allowedDomainsPath +} from '@etherealengine/common/src/schemas/networking/allowed-domains.schema' +import { Application } from '../../../declarations' +import { AllowedDomainsService } from './allowed-domains.class' +import allowedDomainsDocs from './allowed-domains.docs' +import hooks from './allowed-domains.hooks' + +declare module '@etherealengine/common/declarations' { + interface ServiceTypes { + [allowedDomainsPath]: AllowedDomainsService + } +} + +export default (app: Application): void => { + app.use(allowedDomainsPath, new AllowedDomainsService(), { + // A list of all methods this service exposes externally + methods: allowedDomainsMethods, + // You can add additional custom events to be sent to clients here + events: [], + docs: allowedDomainsDocs + }) + + const service = app.service(allowedDomainsPath) + service.hooks(hooks) +} diff --git a/packages/server-core/src/networking/services.ts b/packages/server-core/src/networking/services.ts index 016126ee55..16f98974c4 100755 --- a/packages/server-core/src/networking/services.ts +++ b/packages/server-core/src/networking/services.ts @@ -23,6 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import AllowedDomains from './allowed-domains/allowed-domains' import InstanceActive from './instance-active/instance-active' import InstanceAttendance from './instance-attendance/instance-attendance' import InstanceAuthorizedUser from './instance-authorized-user/instance-authorized-user' @@ -32,6 +33,7 @@ import InstanceServerLoad from './instanceserver-load/instanceserver-load.servic import InstanceServerProvision from './instanceserver-provision/instanceserver-provision.service' export default [ + AllowedDomains, Instance, InstanceServerLoad, InstanceServerProvision, diff --git a/packages/ui/src/pages/Capture/index.stories.tsx b/packages/ui/src/pages/Capture/index.stories.tsx index f1f987b848..5886603686 100644 --- a/packages/ui/src/pages/Capture/index.stories.tsx +++ b/packages/ui/src/pages/Capture/index.stories.tsx @@ -51,7 +51,7 @@ import 'tailwindcss/tailwind.css' // import { useLocation } from 'react-router-dom' const initializeEngineForRecorder = async () => { - // const projects = API.instance.client.service(projectsPath).find() + // const projects = Engine.instance.api.service(projectsPath).find() // await loadEngineInjection(await projects) getMutableState(SceneState).sceneLoaded.set(true) } diff --git a/scripts/build_minikube.sh b/scripts/build_minikube.sh index ed4792905e..de1f8deb46 100755 --- a/scripts/build_minikube.sh +++ b/scripts/build_minikube.sh @@ -121,6 +121,14 @@ else NODE_ENV=$NODE_ENV fi +if [ -z "$VITE_FEATHERS_STORE_KEY" ] +then + VITE_FEATHERS_STORE_KEY=EtherealEngine-Auth-Store +else + VITE_FEATHERS_STORE_KEY=VITE_FEATHERS_STORE_KEY +fi + +echo $VITE_APP_HOST # ./generate-certs.sh @@ -152,6 +160,7 @@ docker buildx build \ --build-arg VITE_8TH_WALL=$VITE_8TH_WALL \ --build-arg VITE_LOGIN_WITH_WALLET=$VITE_LOGIN_WITH_WALLET \ --build-arg VITE_AVATURN_URL=$VITE_AVATURN_URL \ - --build-arg VITE_AVATURN_API=$VITE_AVATURN_API . + --build-arg VITE_AVATURN_API=$VITE_AVATURN_API \ + --build-arg VITE_FEATHERS_STORE_KEY=$VITE_FEATHERS_STORE_KEY . #DOCKER_BUILDKIT=1 docker build -t etherealengine-testbot -f ./dockerfiles/testbot/Dockerfile-testbot . \ No newline at end of file