Skip to content

Commit

Permalink
refactor(identity-provider): use service hooks and resolvers (#9033)
Browse files Browse the repository at this point in the history
* 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 <kbaran@bitscoop.com>
  • Loading branch information
aditya-mitra and barankyle committed Oct 22, 2023
1 parent ab09ca9 commit 4fa898e
Show file tree
Hide file tree
Showing 18 changed files with 228 additions and 392 deletions.
2 changes: 1 addition & 1 deletion packages/client-core/src/user/services/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
5 changes: 3 additions & 2 deletions packages/engine/src/common/functions/checkScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<string[]>((result, sc) => {
if (sc.type.split(':')[0] === currentType) result.push(sc.type.split(':')[1])
Expand Down
2 changes: 1 addition & 1 deletion packages/instanceserver/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions packages/server-core/src/social/invite/invite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,270 +23,22 @@ 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,
IdentityProviderPatch,
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<IdentityProviderQuery> {
authentication?: any
}

/**
* A class for IdentityProvider service
*/

export class IdentityProviderService<
T = IdentityProviderType,
ServiceParams extends Params = IdentityProviderParams
> extends KnexAdapter<IdentityProviderType, IdentityProviderData, IdentityProviderParams, IdentityProviderPatch> {
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<ScopeType>

const avatars = (await this.app
.service(avatarPath)
.find({ isInternal: true, query: { $limit: 1000 } })) as Paginated<AvatarType>

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<IdentityProviderType, IdentityProviderData, IdentityProviderParams, IdentityProviderPatch> {}

0 comments on commit 4fa898e

Please sign in to comment.