Skip to content

Commit 7a5b57e

Browse files
committed
UBERF-9724: Use updated accounts
Signed-off-by: Andrey Sobolev <[email protected]>
1 parent 1b4103e commit 7a5b57e

File tree

28 files changed

+1549
-1196
lines changed

28 files changed

+1549
-1196
lines changed

.vscode/launch.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@
424424
"name": "Debug tool upgrade",
425425
"type": "node",
426426
"request": "launch",
427-
"args": ["src/__start.ts", "upgrade-workspace", "mongo-1000-1"],
427+
"args": ["src/__start.ts", "migrate-github-account", "--region", "cockroach", "--db", "%github"],
428428
"env": {
429429
"SERVER_SECRET": "secret",
430430
"MINIO_ACCESS_KEY": "minioadmin",
@@ -433,7 +433,7 @@
433433
"TRANSACTOR_URL": "ws://localhost:3333",
434434
"MONGO_URL": "mongodb://localhost:27017",
435435
"DB_URL": "mongodb://localhost:27017",
436-
"ACCOUNTS_URL": "http://localhost:3000",
436+
"ACCOUNTS_URL": "http://127.0.0.1:3000",
437437
"ACCOUNT_DB_URL": "mongodb://localhost:27017",
438438
"TELEGRAM_DATABASE": "telegram-service",
439439
"REKONI_URL": "http://localhost:4004",
@@ -565,21 +565,21 @@
565565
"request": "launch",
566566
"args": ["src/index.ts"],
567567
"env": {
568-
"MONGO_URL": "mongodb://localhost:27018",
568+
"MONGO_URL": "mongodb://localhost:27017",
569569
"SERVER_SECRET": "secret",
570-
"ACCOUNTS_URL": "http://localhost:3003",
570+
"ACCOUNTS_URL": "http://localhost:3000",
571571
"APP_ID": "${env:POD_GITHUB_APPID}",
572572
"CLIENT_ID": "${env:POD_GITHUB_CLIENTID}",
573573
"CLIENT_SECRET": "${env:POD_GITHUB_CLIENT_SECRET}",
574574
"PRIVATE_KEY": "${env:POD_GITHUB_PRIVATE_KEY}",
575-
"COLLABORATOR_URL": "ws://huly.local:3079",
575+
"COLLABORATOR_URL": "ws://huly.local:3078",
576576
"MINIO_ENDPOINT": "localhost",
577577
"MINIO_ACCESS_KEY": "minioadmin",
578578
"MINIO_SECRET_KEY": "minioadmin",
579579
"PLATFORM_OPERATION_LOGGING": "true",
580580
"FRONT_URL": "http://localhost:8080",
581581
"PORT": "3500",
582-
"STATS_URL": "http://huly.local:4901"
582+
"STATS_URL": "http://huly.local:4900"
583583
},
584584
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
585585
"sourceMaps": true,

dev/tool/src/github.ts

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import core, {
2+
buildSocialIdString,
3+
DOMAIN_MODEL_TX,
4+
systemAccountUuid,
5+
TxProcessor,
6+
type BackupClient,
7+
type Client,
8+
type Doc,
9+
type Ref,
10+
type TxCUD,
11+
type WorkspaceUuid
12+
} from '@hcengineering/core'
13+
import { getAccountsFromTxes, getSocialKeyByOldEmail } from '@hcengineering/model-core'
14+
import { createClient, getAccountClient, getTransactorEndpoint } from '@hcengineering/server-client'
15+
import { generateToken } from '@hcengineering/server-token'
16+
import type { Db } from 'mongodb'
17+
18+
/**
19+
* @public
20+
*/
21+
export interface GithubIntegrationRecord {
22+
installationId: number
23+
workspace: string
24+
accountId: string // Ref<Account>
25+
}
26+
27+
/**
28+
* @public
29+
*/
30+
export interface GithubUserRecord {
31+
_id: string // login
32+
code?: string | null
33+
token?: string
34+
expiresIn?: number | null // seconds
35+
refreshToken?: string | null
36+
refreshTokenExpiresIn?: number | null
37+
authorized?: boolean
38+
state?: string
39+
scope?: string
40+
error?: string | null
41+
42+
accounts: Record<string, string /* Ref<Account> */>
43+
}
44+
45+
export async function performGithubAccountMigrations (db: Db, region: string | null): Promise<void> {
46+
const token = generateToken(systemAccountUuid, '' as WorkspaceUuid, { service: 'admin', admin: 'true' })
47+
const githubToken = generateToken(systemAccountUuid, '' as WorkspaceUuid, { service: 'github' })
48+
const accountClient = getAccountClient(token)
49+
50+
const githubAccountClient = getAccountClient(githubToken)
51+
52+
const usersCollection = db.collection<GithubUserRecord>('users')
53+
54+
const integrationCollection = db.collection<GithubIntegrationRecord>('installations')
55+
56+
const integrations = await integrationCollection.find({}).toArray()
57+
// Check and apply migrations
58+
// We need to update all workspace information accordingly
59+
60+
const allWorkpaces = await accountClient.listWorkspaces(region)
61+
const byId = new Map(allWorkpaces.map((it) => [it.uuid, it]))
62+
const oldNewIds = new Map(allWorkpaces.map((it) => [it.dataId ?? it.uuid, it]))
63+
64+
const allAuthorizations = await usersCollection.find({}).toArray()
65+
66+
const wsToAuth = new Map<WorkspaceUuid, GithubUserRecord[]>()
67+
68+
for (const it of allAuthorizations) {
69+
for (const ws of Object.keys(it.accounts)) {
70+
const wsId = oldNewIds.get(ws as WorkspaceUuid) ?? byId.get(ws as WorkspaceUuid)
71+
if (wsId !== undefined) {
72+
wsToAuth.set(wsId.uuid, (wsToAuth.get(wsId.uuid) ?? []).concat(it))
73+
}
74+
}
75+
}
76+
const processed = new Set<string>()
77+
78+
const replaces = new Map<string, WorkspaceUuid>()
79+
for (const it of integrations) {
80+
const ws = oldNewIds.get(it.workspace as any) ?? byId.get(it.workspace as any)
81+
if (ws != null) {
82+
// Need to connect to workspace to get account mapping
83+
84+
it.workspace = ws.uuid
85+
replaces.set(it.workspace, ws.uuid)
86+
87+
const wsToken = generateToken(systemAccountUuid, ws.uuid, { service: 'github', mode: 'backup' })
88+
const endpoint = await getTransactorEndpoint(wsToken, 'external')
89+
const client = (await createClient(endpoint, wsToken)) as BackupClient & Client
90+
91+
const systemAccounts = [core.account.System, core.account.ConfigUser]
92+
const accountsTxes: TxCUD<Doc>[] = []
93+
94+
let idx: number | undefined
95+
96+
while (true) {
97+
const info = await client.loadChunk(DOMAIN_MODEL_TX, idx)
98+
idx = info.idx
99+
const ids = Array.from(info.docs.map((it) => it.id as Ref<Doc>))
100+
const docs = (await client.loadDocs(DOMAIN_MODEL_TX, ids)).filter((it) =>
101+
TxProcessor.isExtendsCUD(it._class)
102+
) as TxCUD<Doc>[]
103+
accountsTxes.push(...docs)
104+
if (info.finished && idx !== undefined) {
105+
await client.closeChunk(info.idx)
106+
break
107+
}
108+
}
109+
await client.close()
110+
111+
// await client.loadChunk(DOMAIN_MODEL_TX, {
112+
// objectClass: { $in: ['core:class:Account', 'contact:class:PersonAccount'] as Ref<Class<Doc>>[] }
113+
// })
114+
const accounts: (Doc & { email?: string })[] = getAccountsFromTxes(accountsTxes)
115+
116+
const socialKeyByAccount: Record<string, string> = {}
117+
for (const account of accounts) {
118+
if (account.email === undefined) {
119+
continue
120+
}
121+
122+
if (systemAccounts.includes(account._id as any)) {
123+
;(socialKeyByAccount as any)[account._id] = account._id
124+
} else {
125+
socialKeyByAccount[account._id] = buildSocialIdString(getSocialKeyByOldEmail(account.email)) as any
126+
}
127+
}
128+
129+
const sid = socialKeyByAccount[it.accountId]
130+
131+
const person = sid !== undefined ? await accountClient.findSocialIdBySocialKey(sid) : undefined
132+
if (person !== undefined) {
133+
// Check/create integeration in account
134+
135+
const existing = await githubAccountClient.getIntegration({
136+
kind: 'github',
137+
workspaceUuid: ws?.uuid,
138+
socialId: person
139+
})
140+
141+
if (existing == null) {
142+
await githubAccountClient.createIntegration({
143+
kind: 'github',
144+
workspaceUuid: ws?.uuid,
145+
socialId: person,
146+
data: {
147+
installationId: it.installationId
148+
}
149+
})
150+
}
151+
}
152+
153+
const users = wsToAuth.get(ws.uuid)
154+
for (const u of users ?? []) {
155+
if (processed.has(u._id)) {
156+
continue
157+
}
158+
processed.add(u._id)
159+
160+
const sid = socialKeyByAccount[u.accounts[ws.dataId ?? ws.uuid]]
161+
if (sid !== undefined) {
162+
const person = await accountClient.findSocialIdBySocialKey(sid)
163+
if (person !== undefined) {
164+
const { _id, accounts, ...data } = u
165+
166+
const existing = await githubAccountClient.getIntegration({
167+
kind: 'github-user',
168+
workspaceUuid: null,
169+
socialId: person
170+
})
171+
172+
if (existing == null) {
173+
await githubAccountClient.createIntegration({
174+
kind: 'github-user',
175+
workspaceUuid: null,
176+
socialId: person,
177+
data: {
178+
login: u._id
179+
}
180+
})
181+
// Check/create integeration in account
182+
await githubAccountClient.addIntegrationSecret({
183+
kind: 'github-user',
184+
workspaceUuid: null,
185+
socialId: person,
186+
key: u._id, // github login
187+
secret: JSON.stringify(data)
188+
})
189+
}
190+
}
191+
}
192+
}
193+
}
194+
}
195+
}

dev/tool/src/index.ts

+16
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import {
7171
createMongoAdapter,
7272
createMongoDestroyAdapter,
7373
createMongoTxAdapter,
74+
getMongoClient,
7475
shutdownMongo
7576
} from '@hcengineering/mongo'
7677
import { backupDownload } from '@hcengineering/server-backup/src/backup'
@@ -93,6 +94,7 @@ import { getAccountDBUrl, getMongoDBUrl } from './__start'
9394
import { changeConfiguration } from './configuration'
9495

9596
import { moveAccountDbFromMongoToPG } from './db'
97+
import { performGithubAccountMigrations } from './github'
9698
import { getToolToken, getWorkspace, getWorkspaceTransactorEndpoint } from './utils'
9799

98100
const colorConstants = {
@@ -2399,6 +2401,20 @@ export function devTool (
23992401
// })
24002402
// })
24012403

2404+
program
2405+
.command('migrate-github-account')
2406+
.option('--db <db>', 'Github DB', '%github')
2407+
.option('--region <region>', 'Github DB')
2408+
.action(async (cmd: { db: string, region?: string }) => {
2409+
const mongodbUri = getMongoDBUrl()
2410+
const client = getMongoClient(mongodbUri)
2411+
const _client = await client.getClient()
2412+
2413+
await performGithubAccountMigrations(_client.db(cmd.db), cmd.region ?? null)
2414+
await _client.close()
2415+
client.close()
2416+
})
2417+
24022418
program
24032419
.command('queue-init-topics')
24042420
.description('create required kafka topics')

models/core/src/migration.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ export async function getSocialKeyByOldAccount (client: MigrationClient): Promis
316316
})
317317
const accounts = getAccountsFromTxes(accountsTxes)
318318

319-
const socialKeyByAccount: Record<string, PersonId> = {}
319+
const socialKeyByAccount: Record<string, string> = {}
320320
for (const account of accounts) {
321321
if (account.email === undefined) {
322322
continue

packages/account-client/src/client.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,12 @@ export interface AccountClient {
169169
}
170170

171171
/** @public */
172-
export function getClient (accountsUrl?: string, token?: string): AccountClient {
172+
export function getClient (accountsUrl?: string, token?: string, retryTimeoutMs?: number): AccountClient {
173173
if (accountsUrl === undefined) {
174174
throw new Error('Accounts url not specified')
175175
}
176176

177-
return new AccountClientImpl(accountsUrl, token)
177+
return new AccountClientImpl(accountsUrl, token, retryTimeoutMs)
178178
}
179179

180180
interface Request {
@@ -188,7 +188,8 @@ class AccountClientImpl implements AccountClient {
188188

189189
constructor (
190190
private readonly url: string,
191-
private readonly token?: string
191+
private readonly token?: string,
192+
retryTimeoutMs?: number
192193
) {
193194
if (url === '') {
194195
throw new Error('Accounts url not specified')
@@ -207,7 +208,7 @@ class AccountClientImpl implements AccountClient {
207208
},
208209
...(isBrowser ? { credentials: 'include' } : {})
209210
}
210-
this.rpc = withRetryUntilTimeout(this._rpc.bind(this))
211+
this.rpc = withRetryUntilTimeout(this._rpc.bind(this), retryTimeoutMs ?? 5000)
211212
}
212213

213214
async getProviders (): Promise<string[]> {
@@ -907,7 +908,7 @@ class AccountClientImpl implements AccountClient {
907908
function withRetry<T, F extends (...args: any[]) => Promise<T>> (
908909
f: F,
909910
shouldFail: (err: any, attempt: number) => boolean,
910-
intervalMs: number = 1000
911+
intervalMs: number = 25
911912
): F {
912913
return async function (...params: any[]): Promise<T> {
913914
let attempt = 0
@@ -921,6 +922,9 @@ function withRetry<T, F extends (...args: any[]) => Promise<T>> (
921922

922923
attempt++
923924
await new Promise<void>((resolve) => setTimeout(resolve, intervalMs))
925+
if (intervalMs < 1000) {
926+
intervalMs += 100
927+
}
924928
}
925929
}
926930
} as F

plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte

+15-11
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,37 @@
1414
-->
1515
<script lang="ts">
1616
import activity, {
17+
ActivityMessage,
1718
ActivityMessageViewlet,
18-
DisplayActivityMessage,
1919
ActivityMessageViewType,
20-
ActivityMessage
20+
DisplayActivityMessage
2121
} from '@hcengineering/activity'
2222
import { Person } from '@hcengineering/contact'
2323
import { Avatar, SystemAvatar } from '@hcengineering/contact-resources'
24-
import core, { Ref } from '@hcengineering/core'
24+
import core, { Ref, type SocialId } from '@hcengineering/core'
25+
import notification from '@hcengineering/notification'
26+
import { Asset } from '@hcengineering/platform'
2527
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
2628
import { Action, Icon, Label } from '@hcengineering/ui'
27-
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
28-
import { Asset } from '@hcengineering/platform'
2929
import { Action as ViewAction } from '@hcengineering/view'
30-
import notification from '@hcengineering/notification'
30+
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
3131
32-
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
33-
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
34-
import ActivityMessageActions from '../ActivityMessageActions.svelte'
35-
import { isReactionMessage } from '../../activityMessagesUtils'
3632
import { savedMessagesStore } from '../../activity'
33+
import { isReactionMessage } from '../../activityMessagesUtils'
34+
import { MessageInlineAction } from '../../types'
35+
import ActivityMessageActions from '../ActivityMessageActions.svelte'
3736
import MessageTimestamp from '../MessageTimestamp.svelte'
37+
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
3838
import Replies from '../Replies.svelte'
39-
import { MessageInlineAction } from '../../types'
39+
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
4040
import InlineAction from './InlineAction.svelte'
4141
4242
export let message: DisplayActivityMessage
4343
export let parentMessage: DisplayActivityMessage | undefined = undefined
4444
4545
export let viewlet: ActivityMessageViewlet | undefined = undefined
4646
export let person: Person | undefined = undefined
47+
export let socialId: SocialId | undefined = undefined
4748
export let actions: Action[] = []
4849
export let showNotify: boolean = false
4950
export let isHighlighted: boolean = false
@@ -219,6 +220,9 @@
219220
<div class="username">
220221
<ComponentExtensions extension={activity.extension.ActivityEmployeePresenter} props={{ person }} />
221222
</div>
223+
{#if socialId !== undefined}
224+
({socialId.type})
225+
{/if}
222226
{:else}
223227
<div class="strong">
224228
<Label label={core.string.System} />

0 commit comments

Comments
 (0)