Files
huly-platform/dev/tool/src/github.ts
T
Artyom SavchenkoandGitHub 35cd8a3d86 Fix admin integrations (#10076)
* Get your own integrations for admin

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Remove admin check

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Allow to add social id by admin

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Use service token for integrations test

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Update version

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Fix tokens

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Update allowed services

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Use workspace service

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Fix service client

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Get rid of admin service name

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
2025-10-14 17:01:10 +07:00

238 lines
7.5 KiB
TypeScript

import core, {
buildSocialIdString,
DOMAIN_MODEL_TX,
isArchivingMode,
isDeletingMode,
MeasureMetricsContext,
systemAccountUuid,
TxProcessor,
type Doc,
type PersonId,
type Ref,
type Tx,
type TxCUD,
type WorkspaceUuid,
type IntegrationKind
} from '@hcengineering/core'
import { getAccountsFromTxes, getSocialKeyByOldEmail } from '@hcengineering/model-core'
import { getAccountClient } from '@hcengineering/server-client'
import { createDummyStorageAdapter, wrapPipeline, type PipelineFactory } from '@hcengineering/server-core'
import { createBackupPipeline, createEmptyBroadcastOps } from '@hcengineering/server-pipeline'
import { generateToken } from '@hcengineering/server-token'
import type { Db } from 'mongodb'
/**
* @public
*/
export interface GithubIntegrationRecord {
installationId: number
workspace: string
accountId: string // Ref<Account>
}
/**
* @public
*/
export interface GithubUserRecord {
_id: string // login
code?: string | null
token?: string
expiresIn?: number | null // seconds
refreshToken?: string | null
refreshTokenExpiresIn?: number | null
authorized?: boolean
state?: string
scope?: string
error?: string | null
accounts: Record<string, string /* Ref<Account> */>
}
export async function performGithubAccountMigrations (
db: Db,
dbUrl: string,
txes: Tx[],
region: string | null
): Promise<void> {
const token = generateToken(systemAccountUuid, undefined, { service: 'tool', admin: 'true' })
const githubToken = generateToken(systemAccountUuid, undefined, { service: 'github' })
const accountClient = getAccountClient(token)
const githubAccountClient = getAccountClient(githubToken)
const usersCollection = db.collection<GithubUserRecord>('users')
const integrationCollection = db.collection<GithubIntegrationRecord>('installations')
const integrations = await integrationCollection.find({}).toArray()
// Check and apply migrations
// We need to update all workspace information accordingly
const allWorkpaces = await accountClient.listWorkspaces(region)
const byId = new Map(allWorkpaces.map((it) => [it.uuid, it]))
const oldNewIds = new Map(allWorkpaces.map((it) => [it.dataId ?? it.uuid, it]))
const allAuthorizations = await usersCollection.find({}).toArray()
const wsToAuth = new Map<WorkspaceUuid, GithubUserRecord[]>()
for (const it of allAuthorizations) {
for (const ws of Object.keys(it.accounts)) {
const wsId = oldNewIds.get(ws as WorkspaceUuid) ?? byId.get(ws as WorkspaceUuid)
if (wsId !== undefined) {
wsToAuth.set(wsId.uuid, (wsToAuth.get(wsId.uuid) ?? []).concat(it))
}
}
}
const processed = new Set<string>()
const metricsContext = new MeasureMetricsContext('github-migrate', {})
const factory: PipelineFactory = createBackupPipeline(metricsContext, dbUrl, txes, {
externalStorage: createDummyStorageAdapter(),
usePassedCtx: true
})
const replaces = new Map<string, WorkspaceUuid>()
const failedPersonIds = new Set<PersonId>()
for (const it of integrations) {
const ws = oldNewIds.get(it.workspace as any) ?? byId.get(it.workspace as any)
if (ws != null) {
if (isArchivingMode(ws.mode) || isDeletingMode(ws.mode)) {
continue
}
console.info('processing workspace', ws.uuid, ws.name, ws.url)
it.workspace = ws.uuid
replaces.set(it.workspace, ws.uuid)
const pipeline = await factory(metricsContext, ws, createEmptyBroadcastOps(), null)
const client = wrapPipeline(metricsContext, pipeline, ws, false)
const systemAccounts = [core.account.System, core.account.ConfigUser]
const accountsTxes: TxCUD<Doc>[] = []
let idx: number | undefined
while (true) {
const info = await client.loadChunk(DOMAIN_MODEL_TX, idx)
idx = info.idx
const ids = Array.from(info.docs.map((it) => it.id as Ref<Doc>))
const docs = (await client.loadDocs(DOMAIN_MODEL_TX, ids)).filter((it) =>
TxProcessor.isExtendsCUD(it._class)
) as TxCUD<Doc>[]
for (const tx of docs) {
if (tx.objectClass === 'core:class:Account' || tx.objectClass === 'contact:class:PersonAccount') {
accountsTxes.push(tx)
}
}
if (info.finished && idx !== undefined) {
await client.closeChunk(info.idx)
break
}
}
await client.close()
const accounts: (Doc & { email?: string })[] = getAccountsFromTxes(accountsTxes)
const socialKeyByAccount: Record<string, string> = {}
for (const account of accounts) {
if (account.email === undefined) {
continue
}
if (systemAccounts.includes(account._id as any)) {
;(socialKeyByAccount as any)[account._id] = account._id
} else {
socialKeyByAccount[account._id] = buildSocialIdString(getSocialKeyByOldEmail(account.email)) as any
}
}
const sid = socialKeyByAccount[it.accountId]
const kind: IntegrationKind = 'github' as any
const userKind: IntegrationKind = 'github-user' as any
const person = sid !== undefined ? await accountClient.findSocialIdBySocialKey(sid) : undefined
if (person !== undefined) {
// Check/create integeration in account
const existing = await githubAccountClient.getIntegration({
kind,
workspaceUuid: ws?.uuid,
socialId: person
})
if (existing == null) {
try {
await githubAccountClient.createIntegration({
kind,
workspaceUuid: ws?.uuid,
socialId: person,
data: {
installationId: it.installationId
}
})
} catch (err: any) {
failedPersonIds.add(person)
console.log(err)
}
}
}
const users = wsToAuth.get(ws.uuid)
for (const u of users ?? []) {
if (processed.has(u._id)) {
continue
}
processed.add(u._id)
const sid = socialKeyByAccount[u.accounts[ws.dataId ?? ws.uuid]]
if (sid !== undefined) {
const person = await accountClient.findSocialIdBySocialKey(sid)
if (person !== undefined) {
const { _id, accounts, ...data } = u
const existing = await githubAccountClient.getIntegration({
kind: userKind,
workspaceUuid: null,
socialId: person
})
if (existing == null) {
try {
await githubAccountClient.createIntegration({
kind: userKind,
workspaceUuid: null,
socialId: person,
data: {
login: u._id
}
})
// Check/create integeration in account
await githubAccountClient.addIntegrationSecret({
kind: userKind,
workspaceUuid: null,
socialId: person,
key: u._id, // github login
secret: JSON.stringify(data)
})
} catch (err: any) {
failedPersonIds.add(person)
console.error(err)
}
}
}
}
}
}
}
console.log('Failed to create integrations for', failedPersonIds.size, 'people')
if (failedPersonIds.size > 0) {
// We need to remove all integrations for failed persons
for (const person of failedPersonIds) {
console.log('Fialed person id', person)
}
}
}