@xrengine/server-core
Version:
Shared components for XREngine server
149 lines (136 loc) • 5.35 kB
text/typescript
import { NotAuthenticated } from '@feathersjs/errors'
import { Paginated } from '@feathersjs/feathers/lib'
import crypto from 'crypto'
import { iff, isProvider } from 'feathers-hooks-common'
import { ServerSettingInterface } from '@xrengine/common/src/dbmodels/ServerSetting'
import logger from '@xrengine/common/src/logger'
import { Application } from '../../../declarations'
import authenticate from '../../hooks/authenticate'
import setResponseStatusCode from '../../hooks/set-response-status-code'
import { getUserRepos } from '../../projects/project/github-helper'
import { UnauthorizedException } from '../../util/exceptions/exception'
import { GithubRepoAccess } from './github-repo-access.class'
import githubRepoAccessDocs from './github-repo-access.docs'
import hooks from './github-repo-access.hooks'
import createModel from './github-repo-access.model'
declare module '@xrengine/common/declarations' {
interface ServiceTypes {
'github-repo-access': any
'github-repo-access-webhook': any
'github-repo-access-refresh': any
}
}
const SIG_HEADER_NAME = 'x-hub-signature-256'
const SIG_HASH_ALGORITHM = 'sha256'
/**
* Initialize our service with any options it requires and docs
*/
export default (app: Application): void => {
const options = {
Model: createModel(app),
paginate: app.get('paginate'),
multi: true
}
const event = new GithubRepoAccess(options, app)
event.docs = githubRepoAccessDocs
app.use('github-repo-access', event)
const service = app.service('github-repo-access')
service.hooks(hooks)
app.use('github-repo-access-webhook', {
create: async (data, params): Promise<string> => {
try {
const secret = ((await app.service('server-setting').find()) as Paginated<ServerSettingInterface>).data[0]
.githubWebhookSecret
const sig = Buffer.from(params.headers[SIG_HEADER_NAME] || '', 'utf8')
const hmac = crypto.createHmac(SIG_HASH_ALGORITHM, secret)
const digest = Buffer.from(SIG_HASH_ALGORITHM + '=' + hmac.update(JSON.stringify(data)).digest('hex'), 'utf8')
if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) {
throw new UnauthorizedException('Invalid secret')
}
const { blocked_user, member, membership } = data
const ghUser = member
? member.login
: membership
? membership.user.login
: blocked_user
? blocked_user.login
: null
if (!ghUser) return ''
const githubIdentityProvider = await app.service('identity-provider').Model.findOne({
where: {
type: 'github',
accountIdentifier: ghUser
}
})
if (!githubIdentityProvider) return ''
const user = await app.service('user').get(githubIdentityProvider.userId)
// GitHub's API doesn't always reflect changes to user repo permissions right when a webhook is sent.
// 10 seconds should be more than enough time for the changes to propagate.
setTimeout(() => {
app.service('github-repo-access-refresh').find({ user })
}, 10000)
return ''
} catch (err) {
console.error(err)
throw err
}
}
})
app.service('github-repo-access-webhook').hooks({
after: {
create: [setResponseStatusCode(200)]
}
})
app.use('github-repo-access-refresh', {
find: async (params): Promise<void> => {
try {
const githubIdentityProvider = await (app.service('identity-provider') as any).Model.findOne({
where: {
userId: params.user.id,
type: 'github'
}
})
if (githubIdentityProvider) {
const existingGithubRepoAccesses = await app.service('github-repo-access').Model.findAll({
paginate: false,
where: {
identityProviderId: githubIdentityProvider.id
}
})
const githubRepos = await getUserRepos(githubIdentityProvider.oauthToken)
await Promise.all(
githubRepos.map(async (repo) => {
const matchingAccess = existingGithubRepoAccesses.find((access) => access.repo === repo.html_url)
const hasWriteAccess = repo.permissions.admin || repo.permissions.maintain || repo.permissions.push
if (!matchingAccess)
await app.service('github-repo-access').create({
repo: repo.html_url,
identityProviderId: githubIdentityProvider.id,
hasWriteAccess
})
else
await app.service('github-repo-access').patch(matchingAccess.id, {
hasWriteAccess
})
})
)
const urlsOnly = githubRepos.map((repo) => repo.html_url)
await Promise.all(
existingGithubRepoAccesses.map(async (repoAccess) => {
if (urlsOnly.indexOf(repoAccess.repo) < 0) await app.service('github-repo-access').remove(repoAccess.id)
})
)
}
return
} catch (err) {
logger.error(err)
throw err
}
}
})
app.service('github-repo-access-refresh').hooks({
before: {
find: [iff(isProvider('external'), authenticate() as any)]
}
})
}