mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-21 03:42:24 +02:00
* fix(github): honour the force flag in checkRefreshToken `checkRefreshToken` accepts a `force` parameter but never reads it, so the only caller that passes `force = true` (worker.ts, when re-syncing a user) behaves exactly like the non-forced call and skips the refresh entirely. Gate the refresh on `force || expired` instead of on expiry alone. As a side effect a record with a `refreshToken` but a null `expiresIn` is now validated when forced, instead of being reported as valid unchecked. Signed-off-by: koreanjoker <namug014@gmail.com> * fix(github): return undefined from getOctokit when the token is dead When `checkRefreshToken` reports failure, `getOctokit` cleared `record.octokit` and then fell straight through to constructing a new Octokit from the very token that was just rejected. The method therefore never returned `undefined` for a revoked user, so the `(await getOctokit(...)) ?? container.container.octokit` installation-token fallback that the sync code already writes at 13 call sites was unreachable. Return `undefined` after clearing the client so the existing fallback can take effect. Signed-off-by: koreanjoker <namug014@gmail.com> * fix(github): preserve accounts when deserialising a user secret `updateUser` serialises the whole `GithubUserRecord` — `accounts` included — into the integration secret, but `secretToUserRecord` placed a literal `accounts: {}` after the spread of the parsed payload, discarding whatever was stored. Every consumer of a record loaded through `getAccount` therefore saw an empty map. `revokeUserAuth` iterates `Object.entries(record.accounts)`, so its body never ran and the re-authorisation notice was never written to any workspace. Read `accounts` back from the parsed payload, keeping `{}` as the fallback for records written before the field existed. Signed-off-by: koreanjoker <namug014@gmail.com> --------- Signed-off-by: koreanjoker <namug014@gmail.com>
163 lines
4.6 KiB
TypeScript
163 lines
4.6 KiB
TypeScript
import type { AccountClient, IntegrationSecret } from '@hcengineering/account-client'
|
|
import core, { systemAccountUuid, type MeasureContext, type PersonId, type WorkspaceUuid } from '@hcengineering/core'
|
|
import { getAccountClient } from '@hcengineering/server-client'
|
|
import { generateToken } from '@hcengineering/server-token'
|
|
import { githubUserIntegrationKind } from '@hcengineering/github'
|
|
import type { GithubUserRecord } from './types'
|
|
|
|
export class UserManager {
|
|
userCache = new Map<string, GithubUserRecord>()
|
|
refUserCache = new Map<string, GithubUserRecord>()
|
|
|
|
accountClient: AccountClient
|
|
|
|
constructor () {
|
|
const sysToken = generateToken(systemAccountUuid, undefined, { service: 'github' })
|
|
this.accountClient = getAccountClient(sysToken, 30000)
|
|
}
|
|
|
|
async getAccount (login: string): Promise<GithubUserRecord | undefined> {
|
|
let res = this.userCache.get(login)
|
|
if (res !== undefined) {
|
|
return res
|
|
}
|
|
const secrets = await this.accountClient.listIntegrationsSecrets({ kind: githubUserIntegrationKind, key: login })
|
|
if (secrets.length === 0) {
|
|
return
|
|
}
|
|
res = this.secretToUserRecord(secrets[0], login)
|
|
if (res !== undefined) {
|
|
if (this.userCache.size > 1000) {
|
|
this.userCache.clear()
|
|
}
|
|
this.userCache.set(login, res)
|
|
}
|
|
return res
|
|
}
|
|
|
|
private secretToUserRecord (secret: IntegrationSecret, login: string): GithubUserRecord | undefined {
|
|
const parsed = JSON.parse(secret.secret) ?? {} // TODO: Add security
|
|
return {
|
|
...parsed,
|
|
account: secret.socialId,
|
|
_id: login,
|
|
accounts: parsed.accounts ?? {}
|
|
}
|
|
}
|
|
|
|
failedRefs = new Set<string>()
|
|
|
|
cacheRecord (workspace: WorkspaceUuid, ref: PersonId, record: GithubUserRecord): void {
|
|
if (this.refUserCache.size > 1000) {
|
|
this.refUserCache.clear()
|
|
}
|
|
const key = `${workspace}.${ref}`
|
|
this.refUserCache.set(key, record)
|
|
}
|
|
|
|
async getAccountByRef (
|
|
ctx: MeasureContext,
|
|
workspace: WorkspaceUuid,
|
|
ref: PersonId
|
|
): Promise<GithubUserRecord | undefined> {
|
|
const key = `${workspace}.${ref}`
|
|
let rec = this.refUserCache.get(key)
|
|
if (rec !== undefined) {
|
|
return rec
|
|
}
|
|
if (ref === core.account.System || ref === core.account.ConfigUser) {
|
|
return undefined
|
|
}
|
|
|
|
try {
|
|
if (this.failedRefs.has(ref)) {
|
|
// Ignore failed refs
|
|
return
|
|
}
|
|
const secrets = await this.accountClient.listIntegrationsSecrets({
|
|
kind: githubUserIntegrationKind,
|
|
socialId: ref
|
|
})
|
|
if (secrets.length === 0) {
|
|
return
|
|
}
|
|
|
|
rec = this.secretToUserRecord(secrets[0], secrets[0].key)
|
|
if (rec !== undefined) {
|
|
if (this.refUserCache.size > 1000) {
|
|
this.refUserCache.clear()
|
|
}
|
|
this.refUserCache.set(key, rec)
|
|
}
|
|
} catch (err: any) {
|
|
this.failedRefs.add(ref)
|
|
ctx.warn('failed to get user by ref', { workspace, ref, err })
|
|
}
|
|
return rec
|
|
}
|
|
|
|
async updateUser (dta: GithubUserRecord, clear: boolean = true): Promise<void> {
|
|
if (clear) {
|
|
this.userCache.clear()
|
|
this.refUserCache.clear()
|
|
}
|
|
|
|
// Need to check if user integeration exists.
|
|
const existing = await this.accountClient.getIntegration({
|
|
kind: githubUserIntegrationKind,
|
|
workspaceUuid: null,
|
|
socialId: dta.account
|
|
})
|
|
|
|
if (existing == null) {
|
|
await this.accountClient.createIntegration({
|
|
kind: githubUserIntegrationKind,
|
|
workspaceUuid: null,
|
|
socialId: dta.account,
|
|
data: {
|
|
login: dta._id
|
|
}
|
|
})
|
|
}
|
|
|
|
const exists = await this.accountClient.getIntegrationSecret({
|
|
key: dta._id,
|
|
kind: githubUserIntegrationKind,
|
|
socialId: dta.account,
|
|
workspaceUuid: null
|
|
})
|
|
|
|
if (exists !== null) {
|
|
await this.accountClient.updateIntegrationSecret({
|
|
key: dta._id,
|
|
kind: githubUserIntegrationKind,
|
|
socialId: dta.account,
|
|
secret: JSON.stringify(dta),
|
|
workspaceUuid: null
|
|
})
|
|
} else {
|
|
await this.accountClient.addIntegrationSecret({
|
|
key: dta._id,
|
|
kind: githubUserIntegrationKind,
|
|
socialId: dta.account,
|
|
secret: JSON.stringify(dta),
|
|
workspaceUuid: null
|
|
})
|
|
}
|
|
}
|
|
|
|
async insertUser (dta: GithubUserRecord): Promise<void> {
|
|
await this.updateUser(dta, false)
|
|
}
|
|
|
|
async removeUser (login: string): Promise<void> {
|
|
this.userCache.clear()
|
|
this.refUserCache.clear()
|
|
|
|
const secerts = await this.accountClient.listIntegrationsSecrets({ kind: githubUserIntegrationKind, key: login })
|
|
for (const s of secerts) {
|
|
await this.accountClient.deleteIntegrationSecret(s)
|
|
}
|
|
}
|
|
}
|