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

refactor(identity-provider): use service hooks and resolvers #9033

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -582,7 +582,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 @@ -1444,7 +1444,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> {}
Loading
Loading