From 4fa898e6de25db73b0e5e1493d122042486cd7fa Mon Sep 17 00:00:00 2001 From: Aditya Mitra <55396651+aditya-mitra@users.noreply.github.com> Date: Sun, 22 Oct 2023 11:18:00 +0530 Subject: [PATCH] refactor(identity-provider): use service hooks and resolvers (#9033) * refactor(identity-provider): use service hooks and resolvers * fix: do not throw error if no scopes are available * tests: test more result properties * improve: use getFreeInviteCode in user resolvers * improve: identity provider hooks * fix: use existing set-logged-in-user hook * fix: add the correct typing in HookContext * Replaced internal method calls Changed order of identity-provider create hooks. validateAuthParams needs to occur after createNewUser, or else it won't have a userId to act on. * Reversed rearrangement, changed tests * fix: allow guest to be created using idenity provider --------- Co-authored-by: Kyle Baran --- .../src/user/services/AuthService.ts | 2 +- .../engine/src/common/functions/checkScope.ts | 5 +- packages/instanceserver/src/channels.ts | 2 +- .../src/projects/project/project-helper.ts | 2 +- .../src/social/invite/invite.test.ts | 10 + .../identity-provider.class.ts | 254 +----------------- .../identity-provider.hooks.ts | 195 ++++++++++---- .../identity-provider.resolvers.ts | 4 - .../identity-provider.test.ts | 104 ++++--- .../identity-provider/identity-provider.ts | 2 +- .../server-core/src/user/login/login.class.ts | 2 +- .../src/user/strategies/discord.ts | 6 +- .../src/user/strategies/facebook.ts | 6 +- .../server-core/src/user/strategies/google.ts | 6 +- .../src/user/strategies/linkedin.ts | 6 +- .../src/user/strategies/twitter.ts | 6 +- .../src/user/user/user.resolvers.ts | 5 +- .../src/util/get-free-invite-code.ts | 3 +- 18 files changed, 228 insertions(+), 392 deletions(-) diff --git a/packages/client-core/src/user/services/AuthService.ts b/packages/client-core/src/user/services/AuthService.ts index 5b0cf99829f..94cfd5f6d4d 100755 --- a/packages/client-core/src/user/services/AuthService.ts +++ b/packages/client-core/src/user/services/AuthService.ts @@ -583,7 +583,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 Engine.instance.api.service(identityProviderPath).remove(identityProviderId) return AuthService.loadUserData(userId) } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) diff --git a/packages/engine/src/common/functions/checkScope.ts b/packages/engine/src/common/functions/checkScope.ts index 62d7f910b2b..feba74933b2 100644 --- a/packages/engine/src/common/functions/checkScope.ts +++ b/packages/engine/src/common/functions/checkScope.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 { NotFound } from '@feathersjs/errors' import { Engine } from '../../ecs/classes/Engine' import { ScopeType, scopePath } from '../../schemas/scope/scope.schema' import { UserType } from '../../schemas/user/user.schema' @@ -36,7 +35,9 @@ export const checkScope = async (user: UserType, currentType: string, scopeToVer } })) as any as ScopeType[] - if (!scopes || scopes.length === 0) throw new NotFound('No scope available for the current user.') + if (!scopes || scopes.length === 0) { + return false + } const currentScopes = scopes.reduce((result, sc) => { if (sc.type.split(':')[0] === currentType) result.push(sc.type.split(':')[1]) diff --git a/packages/instanceserver/src/channels.ts b/packages/instanceserver/src/channels.ts index 74fdc4574a4..d8dbee27e59 100755 --- a/packages/instanceserver/src/channels.ts +++ b/packages/instanceserver/src/channels.ts @@ -662,7 +662,7 @@ const onDisconnection = (app: Application) => async (connection: PrimusConnectio } catch (err) { if (err.code === 401 && err.data.name === 'TokenExpiredError') { const jwtDecoded = decode(token)! - const idProvider = await app.service(identityProviderPath)._get(jwtDecoded.sub as string) + const idProvider = await app.service(identityProviderPath).get(jwtDecoded.sub as string) authResult = { [identityProviderPath]: idProvider } diff --git a/packages/server-core/src/projects/project/project-helper.ts b/packages/server-core/src/projects/project/project-helper.ts index fd17009181a..191dc4a25e8 100644 --- a/packages/server-core/src/projects/project/project-helper.ts +++ b/packages/server-core/src/projects/project/project-helper.ts @@ -1448,7 +1448,7 @@ export const updateProject = async ( const userId = params!.user?.id || project?.updateUserId if (!userId) throw new BadRequest('No user ID from call or existing project owner') - const githubIdentityProvider = (await app.service(identityProviderPath)._find({ + const githubIdentityProvider = (await app.service(identityProviderPath).find({ query: { userId: userId, type: 'github', diff --git a/packages/server-core/src/social/invite/invite.test.ts b/packages/server-core/src/social/invite/invite.test.ts index 1d2ccc3d811..dd581caf93e 100755 --- a/packages/server-core/src/social/invite/invite.test.ts +++ b/packages/server-core/src/social/invite/invite.test.ts @@ -27,6 +27,7 @@ import { inviteTypes } from '@etherealengine/engine/src/schemas/social/invite-ty import { InviteType, invitePath } from '@etherealengine/engine/src/schemas/social/invite.schema' import { LocationType, locationPath } from '@etherealengine/engine/src/schemas/social/location.schema' import { avatarPath } from '@etherealengine/engine/src/schemas/user/avatar.schema' +import { identityProviderPath } from '@etherealengine/engine/src/schemas/user/identity-provider.schema' import { UserType, userPath } from '@etherealengine/engine/src/schemas/user/user.schema' import assert from 'assert' import { v1 } from 'uuid' @@ -83,6 +84,15 @@ describe('invite.service', () => { ) }) + after(async () => { + // Remove test user + await app.service(identityProviderPath).remove(null, { + query: { + userId: testUser.id + } + }) + }) + inviteTypes.forEach((inviteType) => { it(`should create an invite with type ${inviteType}`, async () => { const inviteType = 'friend' diff --git a/packages/server-core/src/user/identity-provider/identity-provider.class.ts b/packages/server-core/src/user/identity-provider/identity-provider.class.ts index 06ad6ae0ad8..a8a76dcee43 100755 --- a/packages/server-core/src/user/identity-provider/identity-provider.class.ts +++ b/packages/server-core/src/user/identity-provider/identity-provider.class.ts @@ -23,9 +23,8 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import type { Id, NullableId, Params } from '@feathersjs/feathers' -import type { KnexAdapterOptions } from '@feathersjs/knex' -import { KnexAdapter } from '@feathersjs/knex' +import type { Params } from '@feathersjs/feathers' +import { KnexService } from '@feathersjs/knex' import { IdentityProviderData, @@ -33,260 +32,13 @@ import { IdentityProviderQuery, IdentityProviderType } from '@etherealengine/engine/src/schemas/user/identity-provider.schema' -import { Paginated } from '@feathersjs/feathers' -import { random } from 'lodash' -import { v1 as uuidv1 } from 'uuid' -import { isDev } from '@etherealengine/common/src/config' -import { avatarPath, AvatarType } from '@etherealengine/engine/src/schemas/user/avatar.schema' - -import { scopePath, ScopeType } from '@etherealengine/engine/src/schemas/scope/scope.schema' -import { userPath, UserType } from '@etherealengine/engine/src/schemas/user/user.schema' -import { Application } from '../../../declarations' - -import { scopeTypePath } from '@etherealengine/engine/src/schemas/scope/scope-type.schema' import { KnexAdapterParams } from '@feathersjs/knex' -import appConfig from '../../appconfig' -import getFreeInviteCode from '../../util/get-free-invite-code' export interface IdentityProviderParams extends KnexAdapterParams { authentication?: any } - -/** - * A class for IdentityProvider service - */ - export class IdentityProviderService< T = IdentityProviderType, ServiceParams extends Params = IdentityProviderParams -> extends KnexAdapter { - app: Application - - constructor(options: KnexAdapterOptions, app: Application) { - super(options) - this.app = app - } - - async create(data: IdentityProviderData, params?: IdentityProviderParams) { - if (!params) params = {} - let { token, type } = data - let user - let authResult - - if (params?.authentication) { - authResult = await (this.app.service('authentication') as any).strategies.jwt.authenticate( - { accessToken: params?.authentication.accessToken }, - {} - ) - if (authResult[appConfig.authentication.entity]?.userId) { - user = await this.app.service(userPath).get(authResult[appConfig.authentication.entity]?.userId) - } - } - if ( - (!user || !user.scopes || !user.scopes.find((scope) => scope.type === 'admin:admin')) && - params?.provider && - type !== 'password' && - type !== 'email' && - type !== 'sms' - ) - type = 'guest' //Non-password/magiclink create requests must always be for guests - - let userId = data.userId || (authResult ? authResult[appConfig.authentication.entity]?.userId : null) - let identityProvider: IdentityProviderData = { ...data } - - switch (type) { - case 'email': - identityProvider = { - ...identityProvider, - token, - type - } - break - case 'sms': - identityProvider = { - ...identityProvider, - token, - type - } - break - case 'password': - identityProvider = { - ...identityProvider, - token, - type - } - break - case 'github': - identityProvider = { - ...identityProvider, - token: token, - type - } - break - case 'facebook': - identityProvider = { - ...identityProvider, - token: token, - type - } - break - case 'google': - identityProvider = { - ...identityProvider, - token: token, - type - } - break - case 'twitter': - identityProvider = { - ...identityProvider, - token: token, - type - } - break - case 'linkedin': - identityProvider = { - ...identityProvider, - token: token, - type - } - break - case 'discord': - identityProvider = { - ...identityProvider, - token: token, - type - } - break - case 'guest': - identityProvider = { - ...identityProvider, - token: token, - type: type - } - break - case 'auth0': - break - } - - // if userId is not defined, then generate userId - if (!userId) { - userId = uuidv1() - } - - // check if there is a user with userId - let foundUser - try { - foundUser = await this.app.service(userPath).get(userId) - } catch (err) { - // - } - - if (foundUser != null) { - // if there is the user with userId, then we add the identity provider to the user - return await super._create( - { - ...identityProvider, - userId - }, - params - ) - } - - const code = await getFreeInviteCode(this.app) - // if there is no user with userId, then we create a user and a identity provider. - const adminCount = (await this.app.service(scopePath).find({ - query: { - type: 'admin:admin' - } - })) as Paginated - - const avatars = (await this.app - .service(avatarPath) - .find({ isInternal: true, query: { $limit: 1000 } })) as Paginated - - const isGuest = type === 'guest' - - if (adminCount.data.length === 0) { - // in dev mode make the first guest an admin - // otherwise make the first logged in user an admin - if (isDev || !isGuest) { - type = 'admin' - } - } - - let result: IdentityProviderType - try { - const newUser = (await this.app.service(userPath).create({ - id: userId, - isGuest, - inviteCode: type === 'guest' ? '' : code, - avatarId: avatars.data[random(avatars.data.length - 1)].id - })) as UserType - - result = await super._create( - { - ...identityProvider, - userId: newUser.id - }, - params - ) - } catch (err) { - console.error(err) - await this.app.service(userPath).remove(userId) - throw err - } - // DRC - - if (type === 'guest') { - if (appConfig.scopes.guest.length) { - const data = appConfig.scopes.guest.map((el) => { - return { - type: el, - userId - } - }) - await this.app.service(scopePath).create(data) - } - - result.accessToken = await this.app - .service('authentication') - .createAccessToken({}, { subject: result.id.toString() }) - } else if (isDev && type === 'admin') { - // in dev mode, add all scopes to the first user made an admin - const scopeTypes = await this.app.service(scopeTypePath).find({ - paginate: false - }) - - const data = scopeTypes.map(({ type }) => { - return { userId, type } - }) - await this.app.service(scopePath).create(data) - - result.accessToken = await this.app - .service('authentication') - .createAccessToken({}, { subject: result.id.toString() }) - } - - return result - } - - async find(params?: IdentityProviderParams) { - const loggedInUser = params!.user as UserType - if (params!.provider) params!.query!.userId = loggedInUser.id - return super._find(params) - } - - async get(id: Id, params?: IdentityProviderParams) { - return super._get(id, params) - } - - async patch(id: Id, data: IdentityProviderData, params?: IdentityProviderParams) { - return super._patch(id, data, params) - } - - async remove(id: NullableId, params?: IdentityProviderParams) { - return super._remove(id, params) - } -} +> extends KnexService {} diff --git a/packages/server-core/src/user/identity-provider/identity-provider.hooks.ts b/packages/server-core/src/user/identity-provider/identity-provider.hooks.ts index ab70fd40550..c97baed91af 100755 --- a/packages/server-core/src/user/identity-provider/identity-provider.hooks.ts +++ b/packages/server-core/src/user/identity-provider/identity-provider.hooks.ts @@ -23,19 +23,29 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { hooks as schemaHooks } from '@feathersjs/schema' -import { iff, isProvider } from 'feathers-hooks-common' - import { + IdentityProviderData, IdentityProviderType, identityProviderDataValidator, identityProviderPatchValidator, identityProviderPath, identityProviderQueryValidator } from '@etherealengine/engine/src/schemas/user/identity-provider.schema' -import { Forbidden, MethodNotAllowed, NotFound } from '@feathersjs/errors' -import { HookContext } from '@feathersjs/feathers' - +import { BadRequest, Forbidden, MethodNotAllowed, NotFound } from '@feathersjs/errors' +import { hooks as schemaHooks } from '@feathersjs/schema' +import { iff, isProvider } from 'feathers-hooks-common' +import appConfig from '../../appconfig' + +import { isDev } from '@etherealengine/common/src/config' +import { checkScope } from '@etherealengine/engine/src/common/functions/checkScope' +import { scopePath } from '@etherealengine/engine/src/schemas/scope/scope.schema' +import { avatarPath } from '@etherealengine/engine/src/schemas/user/avatar.schema' +import { userPath } from '@etherealengine/engine/src/schemas/user/user.schema' +import { random } from 'lodash' +import { HookContext } from '../../../declarations' +import setLoggedinUserInQuery from '../../hooks/set-loggedin-user-in-query' +import { scopeTypeSeed } from '../../scope/scope-type/scope-type.seed' +import { IdentityProviderService } from './identity-provider.class' import { identityProviderDataResolver, identityProviderExternalResolver, @@ -44,56 +54,122 @@ import { identityProviderResolver } from './identity-provider.resolvers' -const checkIdentityProvider = (): any => { - return async (context: HookContext): Promise => { - if (context.id) { - // If trying to CRUD a specific identity-provider, throw 404 if the user doesn't own it - const thisIdentityProvider = (await context.app - .service(identityProviderPath) - .get(context.id)) as IdentityProviderType - if ( - !context.params.user || - !thisIdentityProvider || - (context.params.user && thisIdentityProvider && context.params.user.id !== thisIdentityProvider.userId) - ) - throw new NotFound() - } else { - // If trying to CRUD multiple identity-providers, e.g. patch all IP's belonging to a user, make params.query.userId - // the ID of the calling user, so no one can alter anyone else's IPs. - const userId = context.params[identityProviderPath]?.userId - if (!userId) throw new NotFound() - if (!context.params.query) context.params.query = {} - context.params.query.userId = userId +/** + * If trying to CRUD multiple identity-providers (e.g. patch all IP's belonging to a user), + * make `params.query.userId` the ID of the calling user, so no one can alter anyone else's IPs. + */ +async function checkIdentityProvider(context: HookContext): Promise { + if (context.id) { + const thisIdentityProvider = await context.app.service(identityProviderPath).get(context.id) + if ( + !context.params.user || + !thisIdentityProvider || + (context.params.user && thisIdentityProvider && context.params.user.id !== thisIdentityProvider.userId) + ) + throw new MethodNotAllowed('authenticated user is not owner of this identity provider') + } else { + const userId = context.params[identityProviderPath]?.userId + if (!userId) throw new NotFound() + if (!context.params.query) context.params.query = {} + context.params.query.userId = userId + } + return context +} + +/** + * do not allow to remove the identity providers in bulk + * and we want to disallow removing the last identity provider for non-guest users + */ +async function checkOnlyIdentityProvider(context: HookContext) { + if (!context.id) { + throw new MethodNotAllowed('Cannot remove multiple providers together') + } + const thisIdentityProvider = await context.app.service(identityProviderPath).get(context.id) + + if (!thisIdentityProvider) throw new Forbidden('You do not have any identity provider') + + if (thisIdentityProvider.type === 'guest') return context + + const providers = await context.app + .service(identityProviderPath) + .find({ query: { userId: thisIdentityProvider.userId } }) + + if (providers.total <= 1) { + throw new MethodNotAllowed('Cannot remove the only identity provider on a user') + } + return context +} + +/* (BEFORE) CREATE HOOKS */ + +async function validateAuthParams(context: HookContext) { + let userId = (context.data as IdentityProviderData).userId + + if (context.params.authentication) { + const authResult = await context.app.service('authentication').strategies.jwt.authenticate!( + { accessToken: context.params.authentication.accessToken }, + {} + ) + userId = userId || authResult[appConfig.authentication.entity]?.userId + } + + if (!userId) { + if ((context.data as IdentityProviderData).type === 'guest') { + return } - if (context.data) context.data = { password: context.data.password } //If patching externally, should only be able to change password - return context + throw new BadRequest('userId not found') } + + context.existingUser = await context.app.service(userPath).get(userId) } -const checkOnlyIdentityProvider = () => { - return async (context: HookContext): Promise => { - if (!context.id) { - // do not allow to remove identity providers in bulk - throw new MethodNotAllowed('Cannot remove multiple providers together') +async function addIdentityProviderType(context: HookContext) { + const isAdmin = context.existingUser && (await checkScope(context.existingUser, 'admin', 'admin')) + if ( + !isAdmin && + context.params!.provider && + !['password', 'email', 'sms'].includes((context!.data as IdentityProviderData).type) + ) { + ;(context.data as IdentityProviderData).type = 'guest' + } + + const adminScopes = await context.app.service(scopePath).find({ + query: { + type: 'admin:admin' } - const thisIdentityProvider = (await context.app - .service(identityProviderPath) - .get(context.id)) as IdentityProviderType + }) - if (!thisIdentityProvider) throw new Forbidden('You do not have any identity provider') + if (adminScopes.total === 0 && isDev && (context.data as IdentityProviderData).type !== 'guest') { + ;(context.data as IdentityProviderData).type = 'admin' + } +} - // we only want to disallow removing the last identity provider if it is not a guest - // since the guest user will be destroyed once they log in - if (thisIdentityProvider.type === 'guest') return context +async function createNewUser(context: HookContext) { + const isGuest = (context.data as IdentityProviderType).type === 'guest' + const avatars = await context.app.service(avatarPath).find({ isInternal: true, query: { $limit: 1000 } }) - const providers = await context.app - .service(identityProviderPath) - .find({ query: { userId: thisIdentityProvider.userId } }) + const newUser = await context.app.service(userPath).create({ + isGuest, + avatarId: avatars.data[random(avatars.data.length - 1)].id + }) - if (providers.total <= 1) { - throw new MethodNotAllowed('Cannot remove the only identity provider on a user') - } - return context + context.existingUser = newUser +} + +/* (AFTER) CREATE HOOKS */ + +async function addScopes(context: HookContext) { + if (isDev && (context.data as IdentityProviderType).type === 'admin') { + const data = scopeTypeSeed.map(({ type }) => ({ userId: context.existingUser!.id, type })) + await context.app.service(scopePath).create(data) + } +} + +async function createAccessToken(context: HookContext) { + if (!(context.result as IdentityProviderType).accessToken) { + ;(context.result as IdentityProviderType).accessToken = await context.app + .service('authentication') + .createAccessToken({}, { subject: (context.result as IdentityProviderType).id.toString() }) } } @@ -110,25 +186,36 @@ export default { () => schemaHooks.validateQuery(identityProviderQueryValidator), schemaHooks.resolveQuery(identityProviderQueryResolver) ], - find: [], - get: [iff(isProvider('external'), checkIdentityProvider())], + find: [iff(isProvider('external'), setLoggedinUserInQuery('userId'))], + get: [iff(isProvider('external'), checkIdentityProvider)], create: [ + iff( + (context: HookContext) => Array.isArray(context.data), + () => { + throw new MethodNotAllowed('identity-provider create works only with singular entries') + } + ), () => schemaHooks.validateData(identityProviderDataValidator), - schemaHooks.resolveData(identityProviderDataResolver) + schemaHooks.resolveData(identityProviderDataResolver), + validateAuthParams, + addIdentityProviderType, + iff((context: HookContext) => !context.existingUser, createNewUser), + (context: HookContext) => + ((context.data as IdentityProviderData).userId = context.existingUser!.id) ], - update: [iff(isProvider('external'), checkIdentityProvider())], + update: [iff(isProvider('external'), checkIdentityProvider)], patch: [ - iff(isProvider('external'), checkIdentityProvider()), + iff(isProvider('external'), checkIdentityProvider), () => schemaHooks.validateData(identityProviderPatchValidator), schemaHooks.resolveData(identityProviderPatchResolver) ], - remove: [iff(isProvider('external'), checkIdentityProvider()), checkOnlyIdentityProvider()] + remove: [iff(isProvider('external'), checkIdentityProvider), checkOnlyIdentityProvider] }, after: { all: [], find: [], get: [], - create: [], + create: [addScopes, createAccessToken], update: [], patch: [], remove: [] diff --git a/packages/server-core/src/user/identity-provider/identity-provider.resolvers.ts b/packages/server-core/src/user/identity-provider/identity-provider.resolvers.ts index 1993ece6547..2e9bf82c16c 100644 --- a/packages/server-core/src/user/identity-provider/identity-provider.resolvers.ts +++ b/packages/server-core/src/user/identity-provider/identity-provider.resolvers.ts @@ -33,7 +33,6 @@ import { } from '@etherealengine/engine/src/schemas/user/identity-provider.schema' import type { HookContext } from '@etherealengine/server-core/declarations' -import { UserID } from '@etherealengine/engine/src/schemas/user/user.schema' import { fromDateTimeSql, getDateTimeSql } from '../../util/datetime-sql' export const identityProviderResolver = resolve({ @@ -47,9 +46,6 @@ export const identityProviderDataResolver = resolve { return v4() }, - userId: async (userId) => { - return userId || (v4() as UserID) - }, createdAt: getDateTimeSql, updatedAt: getDateTimeSql }) diff --git a/packages/server-core/src/user/identity-provider/identity-provider.test.ts b/packages/server-core/src/user/identity-provider/identity-provider.test.ts index 39654e5a5d5..56e9287be03 100755 --- a/packages/server-core/src/user/identity-provider/identity-provider.test.ts +++ b/packages/server-core/src/user/identity-provider/identity-provider.test.ts @@ -34,13 +34,11 @@ import { } from '@etherealengine/engine/src/schemas/user/identity-provider.schema' import { UserID } from '@etherealengine/engine/src/schemas/user/user.schema' -import { Paginated } from '@feathersjs/feathers' import { Application } from '../../../declarations' import { createFeathersKoaApp } from '../../createApp' -let userId: UserID - -describe('identity-provider service', () => { +describe('identity-provider.service', () => { + let userId: UserID let app: Application let providers: IdentityProviderType[] = [] @@ -53,94 +51,84 @@ describe('identity-provider service', () => { return destroyEngine() }) - it('registered the service', async () => { - const service = await app.service(identityProviderPath) - assert.ok(service, 'Registered the service') - }) - it('should create an identity provider for guest', async () => { const type = 'guest' const token = v1() - const item = await app.service(identityProviderPath).create( - { - type, - token, - userId: '' as UserID - }, - {} - ) + const createdIdentityProvider = await app.service(identityProviderPath).create({ + type, + token, + userId: '' as UserID + }) - providers.push(item) + providers.push(createdIdentityProvider) - userId = item.userId + userId = createdIdentityProvider.userId - assert.equal(item.type, type) - assert.equal(item.token, token) - assert.ok(item.userId) + assert.equal(createdIdentityProvider.type, type) + assert.equal(createdIdentityProvider.token, token) + assert.ok(createdIdentityProvider.accessToken) + assert.equal(createdIdentityProvider.userId, userId) }) it('should create an identity provider for email', async () => { const type = 'email' const token = v1() - const item = await app.service(identityProviderPath).create( - { - type, - token, - userId - }, - {} - ) + const createdIdentityProvider = await app.service(identityProviderPath).create({ + type, + token, + userId + }) - providers.push(item) + providers.push(createdIdentityProvider) - assert.equal(item.type, type) - assert.equal(item.token, token) - assert.ok(item.userId) + assert.equal(createdIdentityProvider.type, type) + assert.equal(createdIdentityProvider.token, token) + assert.ok(createdIdentityProvider.accessToken) + assert.equal(createdIdentityProvider.userId, userId) }) it('should create an identity provider for password', async () => { const type = 'password' const token = v1() - const item = await app.service(identityProviderPath).create( - { - type, - token, - userId - }, - {} - ) + const createdIdentityProvider = await app.service(identityProviderPath).create({ + type, + token, + userId + }) - providers.push(item) + providers.push(createdIdentityProvider) - assert.equal(item.type, type) - assert.equal(item.token, token) - assert.ok(item.userId) + assert.equal(createdIdentityProvider.type, type) + assert.equal(createdIdentityProvider.token, token) + assert.ok(createdIdentityProvider.accessToken) + assert.equal(createdIdentityProvider.userId, userId) }) it('should find identity providers', async () => { - const item = (await app.service(identityProviderPath).find({ + const foundIdentityProviders = await app.service(identityProviderPath).find({ query: { userId - } - })) as Paginated + }, + isInternal: true + }) - assert.ok(item, 'Identity provider item is found') - assert.equal(item.total, providers.length) + assert.ok(foundIdentityProviders) + assert.equal(foundIdentityProviders.total, providers.length) }) it('should remove an identity provider by id', async () => { await app.service(identityProviderPath).remove(providers[0].id) - const item = (await app.service(identityProviderPath).find({ + const foundIdentityProviders = await app.service(identityProviderPath).find({ query: { id: providers[0].id } - })) as Paginated + }) - assert.equal(item.total, 0) + assert.equal(foundIdentityProviders.total, 0) }) it('should not be able to remove identity providers by user id', async () => { @@ -161,7 +149,7 @@ describe('identity-provider service', () => { const type = 'guest' const token = v1() - const item = await app.service(identityProviderPath).create( + const foundIdentityProvider = await app.service(identityProviderPath).create( { type, token, @@ -170,14 +158,14 @@ describe('identity-provider service', () => { {} ) - assert.ok(() => app.service(identityProviderPath).remove(item.id)) + assert.ok(() => app.service(identityProviderPath).remove(foundIdentityProvider.id)) }) it('should not be able to remove the only identity provider as a user', async () => { const type = 'user' const token = v1() - const item = await app.service(identityProviderPath).create( + const foundIdentityProvider = await app.service(identityProviderPath).create( { type, token, @@ -186,7 +174,7 @@ describe('identity-provider service', () => { {} ) - assert.rejects(() => app.service(identityProviderPath).remove(item.id), { + assert.rejects(() => app.service(identityProviderPath).remove(foundIdentityProvider.id), { name: 'MethodNotAllowed' }) }) diff --git a/packages/server-core/src/user/identity-provider/identity-provider.ts b/packages/server-core/src/user/identity-provider/identity-provider.ts index d5ac5ce0587..9cb685c1080 100755 --- a/packages/server-core/src/user/identity-provider/identity-provider.ts +++ b/packages/server-core/src/user/identity-provider/identity-provider.ts @@ -47,7 +47,7 @@ export default (app: Application): void => { multi: true } - app.use(identityProviderPath, new IdentityProviderService(options, app), { + app.use(identityProviderPath, new IdentityProviderService(options), { // A list of all methods this service exposes externally methods: identityProviderMethods, // You can add additional custom events to be sent to clients here diff --git a/packages/server-core/src/user/login/login.class.ts b/packages/server-core/src/user/login/login.class.ts index 84523fab3c9..01ebbc907e2 100755 --- a/packages/server-core/src/user/login/login.class.ts +++ b/packages/server-core/src/user/login/login.class.ts @@ -78,7 +78,7 @@ export class LoginService implements ServiceInterface { logger.info('Login Token has expired') return { error: 'Login link has expired' } } - const identityProvider = await this.app.service(identityProviderPath)._get(result.data[0].identityProviderId) + const identityProvider = await this.app.service(identityProviderPath).get(result.data[0].identityProviderId) await makeInitialAdmin(this.app, identityProvider.userId) const apiKey = (await this.app.service(userApiKeyPath).find({ query: { diff --git a/packages/server-core/src/user/strategies/discord.ts b/packages/server-core/src/user/strategies/discord.ts index 1f4206df391..eac5b3954c3 100755 --- a/packages/server-core/src/user/strategies/discord.ts +++ b/packages/server-core/src/user/strategies/discord.ts @@ -80,7 +80,7 @@ export class DiscordStrategy extends CustomOAuthStrategy { scopes: [] }) entity.userId = newUser.id - await this.app.service(identityProviderPath)._patch(entity.id, { + await this.app.service(identityProviderPath).patch(entity.id, { userId: newUser.id }) } @@ -101,7 +101,7 @@ export class DiscordStrategy extends CustomOAuthStrategy { userId: entity.userId }) if (entity.type !== 'guest' && identityProvider.type === 'guest') { - await this.app.service(identityProviderPath)._remove(identityProvider.id) + await this.app.service(identityProviderPath).remove(identityProvider.id) await this.app.service(userPath).remove(identityProvider.userId) return super.updateEntity(entity, profile, params) } @@ -109,7 +109,7 @@ export class DiscordStrategy extends CustomOAuthStrategy { if (!existingEntity) { profile.userId = user.id const newIP = await super.createEntity(profile, params) - if (entity.type === 'guest') await this.app.service(identityProviderPath)._remove(entity.id) + if (entity.type === 'guest') await this.app.service(identityProviderPath).remove(entity.id) return newIP } else if (existingEntity.userId === identityProvider.userId) return existingEntity else { diff --git a/packages/server-core/src/user/strategies/facebook.ts b/packages/server-core/src/user/strategies/facebook.ts index 4107edb6c5a..68361b30a0f 100755 --- a/packages/server-core/src/user/strategies/facebook.ts +++ b/packages/server-core/src/user/strategies/facebook.ts @@ -80,7 +80,7 @@ export class FacebookStrategy extends CustomOAuthStrategy { scopes: [] }) entity.userId = newUser.id - await this.app.service(identityProviderPath)._patch(entity.id, { + await this.app.service(identityProviderPath).patch(entity.id, { userId: newUser.id }) } @@ -101,7 +101,7 @@ export class FacebookStrategy extends CustomOAuthStrategy { userId: entity.userId }) if (entity.type !== 'guest' && identityProvider.type === 'guest') { - await this.app.service(identityProviderPath)._remove(identityProvider.id) + await this.app.service(identityProviderPath).remove(identityProvider.id) await this.app.service(userPath).remove(identityProvider.userId) return super.updateEntity(entity, profile, params) } @@ -109,7 +109,7 @@ export class FacebookStrategy extends CustomOAuthStrategy { if (!existingEntity) { profile.userId = user.id const newIP = await super.createEntity(profile, params) - if (entity.type === 'guest') await this.app.service(identityProviderPath)._remove(entity.id) + if (entity.type === 'guest') await this.app.service(identityProviderPath).remove(entity.id) return newIP } else if (existingEntity.userId === identityProvider.userId) return existingEntity else { diff --git a/packages/server-core/src/user/strategies/google.ts b/packages/server-core/src/user/strategies/google.ts index 9a466b9146f..5741b2ce0be 100755 --- a/packages/server-core/src/user/strategies/google.ts +++ b/packages/server-core/src/user/strategies/google.ts @@ -80,7 +80,7 @@ export class Googlestrategy extends CustomOAuthStrategy { scopes: [] }) entity.userId = newUser.id - await this.app.service(identityProviderPath)._patch(entity.id, { + await this.app.service(identityProviderPath).patch(entity.id, { userId: newUser.id }) } @@ -101,7 +101,7 @@ export class Googlestrategy extends CustomOAuthStrategy { userId: entity.userId }) if (entity.type !== 'guest' && identityProvider.type === 'guest') { - await this.app.service(identityProviderPath)._remove(identityProvider.id) + await this.app.service(identityProviderPath).remove(identityProvider.id) await this.app.service(userPath).remove(identityProvider.userId) return super.updateEntity(entity, profile, params) } @@ -109,7 +109,7 @@ export class Googlestrategy extends CustomOAuthStrategy { if (!existingEntity) { profile.userId = user.id const newIP = await super.createEntity(profile, params) - if (entity.type === 'guest') await this.app.service(identityProviderPath)._remove(entity.id) + if (entity.type === 'guest') await this.app.service(identityProviderPath).remove(entity.id) return newIP } else if (existingEntity.userId === identityProvider.userId) return existingEntity else { diff --git a/packages/server-core/src/user/strategies/linkedin.ts b/packages/server-core/src/user/strategies/linkedin.ts index 0cc35ed09bd..35037a20f39 100755 --- a/packages/server-core/src/user/strategies/linkedin.ts +++ b/packages/server-core/src/user/strategies/linkedin.ts @@ -80,7 +80,7 @@ export class LinkedInStrategy extends CustomOAuthStrategy { scopes: [] }) entity.userId = newUser.id - await this.app.service(identityProviderPath)._patch(entity.id, { + await this.app.service(identityProviderPath).patch(entity.id, { userId: newUser.id }) } @@ -101,7 +101,7 @@ export class LinkedInStrategy extends CustomOAuthStrategy { userId: entity.userId }) if (entity.type !== 'guest' && identityProvider.type === 'guest') { - await this.app.service(identityProviderPath)._remove(identityProvider.id) + await this.app.service(identityProviderPath).remove(identityProvider.id) await this.app.service(userPath).remove(identityProvider.userId) return super.updateEntity(entity, profile, params) } @@ -109,7 +109,7 @@ export class LinkedInStrategy extends CustomOAuthStrategy { if (!existingEntity) { profile.userId = user.id const newIP = await super.createEntity(profile, params) - if (entity.type === 'guest') await this.app.service(identityProviderPath)._remove(entity.id) + if (entity.type === 'guest') await this.app.service(identityProviderPath).remove(entity.id) return newIP } else if (existingEntity.userId === identityProvider.userId) return existingEntity else { diff --git a/packages/server-core/src/user/strategies/twitter.ts b/packages/server-core/src/user/strategies/twitter.ts index fc3e3067a55..d2c7c609d49 100755 --- a/packages/server-core/src/user/strategies/twitter.ts +++ b/packages/server-core/src/user/strategies/twitter.ts @@ -81,7 +81,7 @@ export class TwitterStrategy extends CustomOAuthStrategy { scopes: [] }) entity.userId = newUser.id - await this.app.service(identityProviderPath)._patch(entity.id, { + await this.app.service(identityProviderPath).patch(entity.id, { userId: newUser.id }) } @@ -102,7 +102,7 @@ export class TwitterStrategy extends CustomOAuthStrategy { userId: entity.userId }) if (entity.type !== 'guest' && identityProvider.type === 'guest') { - await this.app.service(identityProviderPath)._remove(identityProvider.id) + await this.app.service(identityProviderPath).remove(identityProvider.id) await this.app.service(userPath).remove(identityProvider.userId) return super.updateEntity(entity, profile, params) } @@ -110,7 +110,7 @@ export class TwitterStrategy extends CustomOAuthStrategy { if (!existingEntity) { profile.userId = user.id const newIP = await super.createEntity(profile, params) - if (entity.type === 'guest') await this.app.service(identityProviderPath)._remove(entity.id) + if (entity.type === 'guest') await this.app.service(identityProviderPath).remove(entity.id) return newIP } else if (existingEntity.userId === identityProvider.userId) return existingEntity else { diff --git a/packages/server-core/src/user/user/user.resolvers.ts b/packages/server-core/src/user/user/user.resolvers.ts index 5693a0dd13d..fc743e0281e 100644 --- a/packages/server-core/src/user/user/user.resolvers.ts +++ b/packages/server-core/src/user/user/user.resolvers.ts @@ -47,6 +47,7 @@ import { import { UserApiKeyType, userApiKeyPath } from '@etherealengine/engine/src/schemas/user/user-api-key.schema' import { UserSettingType, userSettingPath } from '@etherealengine/engine/src/schemas/user/user-setting.schema' import { fromDateTimeSql, getDateTimeSql } from '../../util/datetime-sql' +import getFreeInviteCode from '../../util/get-free-invite-code' export const userResolver = resolve({ identityProviders: virtual(async (user, context) => { @@ -142,8 +143,8 @@ export const userDataResolver = resolve({ name: async (name) => { return name || 'Guest #' + Math.floor(Math.random() * (999 - 100 + 1) + 100) }, - inviteCode: async (inviteCode) => { - return inviteCode || Math.random().toString(36).slice(2) + inviteCode: async (inviteCode, _, context) => { + return inviteCode || (await getFreeInviteCode(context.app)) }, avatarId: async (avatarId) => { return avatarId || undefined diff --git a/packages/server-core/src/util/get-free-invite-code.ts b/packages/server-core/src/util/get-free-invite-code.ts index 957b593cfd1..c6f1f218237 100755 --- a/packages/server-core/src/util/get-free-invite-code.ts +++ b/packages/server-core/src/util/get-free-invite-code.ts @@ -25,8 +25,9 @@ Ethereal Engine. All Rights Reserved. import { userPath } from '@etherealengine/engine/src/schemas/user/user.schema' import crypto from 'crypto' +import { Application } from '../../declarations' -const getFreeInviteCode = async (app): Promise => { +const getFreeInviteCode = async (app: Application): Promise => { const code = crypto.randomBytes(4).toString('hex') const users = await app.service(userPath).find({ query: {