UBERF-6180: Fix account issues (#5063)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-03-26 22:07:58 +07:00 committed by GitHub
parent 352e8a4f3f
commit 5716c053fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 504 additions and 173 deletions

View File

@ -30,8 +30,11 @@ export default async () => {
for (const op of migrateOperations) {
console.log('Migrate', op[0])
await op[1].upgrade(client, {
log (...data) {
console.log(...data)
log (msg, data) {
console.log(msg, data)
},
error (msg, data) {
console.error(msg, data)
}
})
}

View File

@ -0,0 +1,83 @@
import { dropWorkspace, setWorkspaceDisabled, type Workspace } from '@hcengineering/account'
import core, { AccountRole, MeasureMetricsContext, SortingOrder } from '@hcengineering/core'
import contact from '@hcengineering/model-contact'
import { getWorkspaceDB } from '@hcengineering/mongo'
import { type StorageAdapter } from '@hcengineering/server-core'
import { connect } from '@hcengineering/server-tool'
import { type Db, type MongoClient } from 'mongodb'
export async function checkOrphanWorkspaces (
workspaces: Workspace[],
transactorUrl: string,
productId: string,
cmd: { remove: boolean, disable: boolean },
db: Db,
client: MongoClient,
storageAdapter: StorageAdapter,
excludes: string[]
): Promise<void> {
for (const ws of workspaces) {
if (excludes.includes(ws.workspace) || (ws.workspaceUrl != null && excludes.includes(ws.workspaceUrl))) {
continue
}
if ((ws.accounts ?? []).length === 0) {
// Potential orhpan workspace
// Let's connect and check activity.
const connection = await connect(transactorUrl, { name: ws.workspace, productId }, undefined, { admin: 'true' })
const accounts = await connection.findAll(contact.class.PersonAccount, {})
const employees = await connection.findAll(contact.mixin.Employee, {})
let activeOwners = 0
for (const person of employees) {
const account = accounts.find((it) => it.person === person._id)
if (account !== undefined) {
if (account.role === AccountRole.Owner && person.active) {
activeOwners++
}
// console.log('-----------', person.name, person.active, account.email, account.role)
}
}
// Find last transaction index:
const wspace = { name: ws.workspace, productId }
const hasBucket = await storageAdapter.exists(wspace)
const [lastTx] = await connection.findAll(
core.class.Tx,
{
objectSpace: { $ne: core.space.Model },
createdBy: { $nin: [core.account.System, core.account.ConfigUser] },
modifiedBy: { $ne: core.account.System }
},
{ limit: 1, sort: { modifiedOn: SortingOrder.Descending } }
)
await connection.close()
const lastTxHours = Math.floor((Date.now() - (lastTx?.modifiedOn ?? 0)) / 1000 / 60 / 60)
if (((activeOwners === 0 || lastTx == null) && lastTxHours > 1000) || !hasBucket) {
const createdOn = (ws.createdOn ?? 0) !== 0 ? new Date(ws.createdOn).toDateString() : ''
console.log(
'Found orhpan workspace',
`'${ws.workspaceName}' id: '${ws.workspace}' url:${ws.workspaceUrl} by: ${ws.createdBy ?? ''} on: '${createdOn}'`,
lastTxHours + ' hours without modifications',
hasBucket
)
if (cmd.disable) {
await setWorkspaceDisabled(db, ws._id, true)
}
if (cmd.remove) {
await dropWorkspace(new MeasureMetricsContext('tool', {}), db, productId, ws.workspace)
const workspaceDb = getWorkspaceDB(client, { name: ws.workspace, productId })
await workspaceDb.dropDatabase()
if (storageAdapter !== undefined && hasBucket) {
const docs = await storageAdapter.list(wspace)
await storageAdapter.remove(
wspace,
docs.map((it) => it.name)
)
await storageAdapter?.delete(wspace)
}
}
}
}
}
}

View File

@ -26,6 +26,7 @@ import {
getWorkspaceById,
listAccounts,
listWorkspaces,
listWorkspacesPure,
listWorkspacesRaw,
replacePassword,
setAccountAdmin,
@ -51,7 +52,15 @@ import { MongoClient, type Db } from 'mongodb'
import { clearTelegramHistory } from './telegram'
import { diffWorkspace, updateField } from './workspace'
import { RateLimiter, getWorkspaceId, type AccountRole, type Data, type Tx, type Version } from '@hcengineering/core'
import {
getWorkspaceId,
MeasureMetricsContext,
RateLimiter,
type AccountRole,
type Data,
type Tx,
type Version
} from '@hcengineering/core'
import { consoleModelLogger, type MigrateOperation } from '@hcengineering/model'
import { openAIConfigDefaults } from '@hcengineering/openai'
import { type StorageAdapter } from '@hcengineering/server-core'
@ -66,6 +75,7 @@ import {
fixSkills,
optimizeModel
} from './clean'
import { checkOrphanWorkspaces } from './cleanOrphan'
import { changeConfiguration } from './configuration'
import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin'
import { openAIConfig } from './openai'
@ -84,6 +94,7 @@ export function devTool (
productId: string,
extendProgram?: (prog: Command) => void
): void {
const toolCtx = new MeasureMetricsContext('tool', {})
const serverSecret = process.env.SERVER_SECRET
if (serverSecret === undefined) {
console.error('please provide server secret')
@ -135,7 +146,7 @@ export function devTool (
const { mongodbUri } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
console.log(`creating account ${cmd.first as string} ${cmd.last as string} (${email})...`)
await createAcc(db, productId, email, cmd.password, cmd.first, cmd.last, true)
await createAcc(toolCtx, db, productId, email, cmd.password, cmd.first, cmd.last, true)
})
})
@ -164,7 +175,7 @@ export function devTool (
}
console.log('assigning to workspace', workspaceInfo)
try {
await assignWorkspace(db, productId, email, workspaceInfo.workspace)
await assignWorkspace(toolCtx, db, productId, email, workspaceInfo.workspace)
} catch (err: any) {
console.error(err)
}
@ -215,6 +226,7 @@ export function devTool (
const { mongodbUri, txes, version, migrateOperations } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
const { client } = await createWorkspace(
toolCtx,
version,
txes,
migrateOperations,
@ -292,6 +304,8 @@ export function devTool (
}
const withError: string[] = []
let toProcess = workspaces.length
const st = Date.now()
async function _upgradeWorkspace (ws: WorkspaceInfo): Promise<void> {
if (ws.disabled === true) {
@ -301,7 +315,18 @@ export function devTool (
const logger = cmd.console
? consoleModelLogger
: new FileModelLogger(path.join(cmd.logs, `${ws.workspace}.log`))
console.log('---UPGRADING----', ws.workspace, !cmd.console ? (logger as FileModelLogger).file : '')
const avgTime = (Date.now() - st) / (workspaces.length - toProcess + 1)
console.log(
'---UPGRADING----',
ws.workspace,
!cmd.console ? (logger as FileModelLogger).file : '',
'pending: ',
toProcess,
'ETA:',
avgTime * toProcess
)
toProcess--
try {
await upgradeWorkspace(
version,
@ -347,6 +372,33 @@ export function devTool (
})
})
program
.command('remove-unused-workspaces')
.description(
'remove unused workspaces, please pass --remove to really delete them. Without it will only mark them disabled'
)
.option('-r|--remove [remove]', 'Force remove', false)
.option('-d|--disable [disable]', 'Force disable', false)
.option('-e|--exclude [exclude]', 'A comma separated list of workspaces to exclude', '')
.action(async (cmd: { remove: boolean, disable: boolean, exclude: string }) => {
const { mongodbUri, storageAdapter } = prepareTools()
await withDatabase(mongodbUri, async (db, client) => {
const workspaces = await listWorkspacesPure(db, productId)
// We need to update workspaces with missing workspaceUrl
await checkOrphanWorkspaces(
workspaces,
transactorUrl,
productId,
cmd,
db,
client,
storageAdapter,
cmd.exclude.split(',')
)
})
})
program
.command('drop-workspace <name>')
.description('drop workspace')
@ -358,7 +410,7 @@ export function devTool (
console.log('no workspace exists')
return
}
await dropWorkspace(db, productId, workspace)
await dropWorkspace(toolCtx, db, productId, workspace)
})
})
@ -368,7 +420,7 @@ export function devTool (
.action(async () => {
const { mongodbUri, version } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
const workspacesJSON = JSON.stringify(await listWorkspaces(db, productId), null, 2)
const workspacesJSON = JSON.stringify(await listWorkspaces(toolCtx, db, productId), null, 2)
console.info(workspacesJSON)
console.log('latest model version:', JSON.stringify(version))
@ -392,7 +444,7 @@ export function devTool (
.action(async (email: string, cmd) => {
const { mongodbUri } = prepareTools()
await withDatabase(mongodbUri, async (db) => {
await dropAccount(db, productId, email)
await dropAccount(toolCtx, db, productId, email)
})
})
@ -434,26 +486,24 @@ export function devTool (
.description('dump workspace transactions and minio resources')
.action(async (bucketName: string, dirName: string, workspace: string, cmd) => {
const { storageAdapter } = prepareTools()
const wsId = getWorkspaceId(workspace, productId)
const storage = await createStorageBackupStorage(storageAdapter, wsId, dirName)
await backup(transactorUrl, wsId, storage)
const storage = await createStorageBackupStorage(storageAdapter, getWorkspaceId(bucketName, productId), dirName)
await backup(transactorUrl, getWorkspaceId(workspace, productId), storage)
})
program
.command('backup-s3-restore <bucketName>, <dirName> <workspace> [date]')
.command('backup-s3-restore <bucketName> <dirName> <workspace> [date]')
.description('dump workspace transactions and minio resources')
.action(async (bucketName: string, dirName: string, workspace: string, date, cmd) => {
const { storageAdapter } = prepareTools()
const wsId = getWorkspaceId(bucketName, productId)
const storage = await createStorageBackupStorage(storageAdapter, wsId, dirName)
await restore(transactorUrl, wsId, storage, parseInt(date ?? '-1'))
const storage = await createStorageBackupStorage(storageAdapter, getWorkspaceId(bucketName), dirName)
await restore(transactorUrl, getWorkspaceId(workspace, productId), storage, parseInt(date ?? '-1'))
})
program
.command('backup-s3-list <bucketName> <dirName>')
.description('list snaphost ids for backup')
.action(async (bucketName: string, dirName: string, cmd) => {
const { storageAdapter } = prepareTools()
const wsId = getWorkspaceId(bucketName, productId)
const storage = await createStorageBackupStorage(storageAdapter, wsId, dirName)
const storage = await createStorageBackupStorage(storageAdapter, getWorkspaceId(bucketName, productId), dirName)
await backupList(storage)
})
@ -510,7 +560,7 @@ export function devTool (
process.exit(1)
}
const workspaces = await listWorkspaces(db, productId)
const workspaces = await listWorkspaces(toolCtx, db, productId)
for (const w of workspaces) {
console.log(`clearing ${w.workspace} history:`)

View File

@ -56,14 +56,18 @@ export async function createOrUpdate<T extends Doc> (
* @public
*/
export interface ModelLogger {
log: (...data: any[]) => void
log: (msg: string, data: any) => void
error: (msg: string, err: any) => void
}
/**
* @public
*/
export const consoleModelLogger: ModelLogger = {
log (...data: any[]): void {
console.log(...data)
log (msg: string, data: any): void {
console.log(msg, data)
},
error (msg: string, data: any): void {
console.error(msg, data)
}
}

View File

@ -206,7 +206,12 @@ export class LiveQuery implements WithTx, Client {
}
}
}
if (options?.limit === 1 && options.total !== true) {
if (
options?.limit === 1 &&
options.total !== true &&
options?.sort === undefined &&
options?.projection === undefined
) {
const docs = this.documentRefs.get(classKey)
if (docs !== undefined) {
const _docs = Array.from(docs.values()).map((it) => it.doc)

View File

@ -619,7 +619,7 @@
$: modern = currentApplication?.modern ?? false
</script>
{#if employee && !employee.active}
{#if employee && !employee.active && !isAdminUser()}
<div class="flex-col-center justify-center h-full flex-grow">
<h1><Label label={workbench.string.AccountDisabled} /></h1>
<Label label={workbench.string.AccountDisabledDescr} />

View File

@ -14,8 +14,8 @@
//
import { getMethods } from '@hcengineering/account'
import { type Tx } from '@hcengineering/core'
import builder, { migrateOperations, getModelVersion } from '@hcengineering/model-all'
import { MeasureMetricsContext, newMetrics, type Tx } from '@hcengineering/core'
import builder, { getModelVersion, migrateOperations } from '@hcengineering/model-all'
import { serveAccount } from '.'
const enabled = (process.env.MODEL_ENABLED ?? '*').split(',').map((it) => it.trim())
@ -23,4 +23,6 @@ const disabled = (process.env.MODEL_DISABLED ?? '').split(',').map((it) => it.tr
const txes = JSON.parse(JSON.stringify(builder(enabled, disabled).getTxes())) as Tx[]
serveAccount(getMethods(getModelVersion(), txes, migrateOperations))
const metricsContext = new MeasureMetricsContext('account', {}, {}, newMetrics())
serveAccount(metricsContext, getMethods(getModelVersion(), txes, migrateOperations))

View File

@ -17,6 +17,8 @@
import account, { ACCOUNT_DB, type AccountMethod, accountId } from '@hcengineering/account'
import accountEn from '@hcengineering/account/lang/en.json'
import accountRu from '@hcengineering/account/lang/ru.json'
import { registerProviders } from '@hcengineering/auth-providers'
import { type MeasureContext } from '@hcengineering/core'
import platform, { Severity, Status, addStringsLoader, setMetadata } from '@hcengineering/platform'
import serverToken from '@hcengineering/server-token'
import toolPlugin from '@hcengineering/server-tool'
@ -26,12 +28,11 @@ import Koa from 'koa'
import bodyParser from 'koa-bodyparser'
import Router from 'koa-router'
import { MongoClient } from 'mongodb'
import { registerProviders } from '@hcengineering/auth-providers'
/**
* @public
*/
export function serveAccount (methods: Record<string, AccountMethod>, productId = ''): void {
export function serveAccount (measureCtx: MeasureContext, methods: Record<string, AccountMethod>, productId = ''): void {
const ACCOUNT_PORT = parseInt(process.env.ACCOUNT_PORT ?? '3000')
const dbUri = process.env.MONGO_URL
if (dbUri === undefined) {
@ -89,9 +90,9 @@ export function serveAccount (methods: Record<string, AccountMethod>, productId
const app = new Koa()
const router = new Router()
void client.then((p) => {
void client.then((p: MongoClient) => {
const db = p.db(ACCOUNT_DB)
registerProviders(app, router, db, productId, serverSecret, frontURL)
registerProviders(measureCtx, app, router, db, productId, serverSecret, frontURL)
})
const extractToken = (header: IncomingHttpHeaders): string | undefined => {
@ -120,7 +121,7 @@ export function serveAccount (methods: Record<string, AccountMethod>, productId
client = await client
}
const db = client.db(ACCOUNT_DB)
const result = await method(db, productId, request, token)
const result = await method(measureCtx, db, productId, request, token)
ctx.body = result
})

View File

@ -1,11 +1,12 @@
import { concatLink } from '@hcengineering/core'
import Router from 'koa-router'
import { Strategy as GitHubStrategy } from 'passport-github2'
import { joinWithProvider, loginWithProvider } from '@hcengineering/account'
import { concatLink, MeasureContext } from '@hcengineering/core'
import Router from 'koa-router'
import { Db } from 'mongodb'
import { Strategy as GitHubStrategy } from 'passport-github2'
import { Passport } from '.'
export function registerGithub (
ctx: MeasureContext,
passport: Passport,
router: Router<any, any>,
accountsUrl: string,
@ -45,14 +46,16 @@ export function registerGithub (
const [first, last] = ctx.state.user.displayName.split(' ')
if (email !== undefined) {
if (ctx.query?.state != null) {
const loginInfo = await joinWithProvider(db, productId, email, first, last, ctx.query.state, {
const loginInfo = await joinWithProvider(ctx, db, productId, email, first, last, ctx.query.state, {
githubId: ctx.state.user.id
})
if (ctx.session != null) {
ctx.session.loginInfo = loginInfo
}
} else {
const loginInfo = await loginWithProvider(db, productId, email, first, last, { githubId: ctx.state.user.id })
const loginInfo = await loginWithProvider(ctx, db, productId, email, first, last, {
githubId: ctx.state.user.id
})
if (ctx.session != null) {
ctx.session.loginInfo = loginInfo
}

View File

@ -1,11 +1,12 @@
import { concatLink } from '@hcengineering/core'
import Router from 'koa-router'
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
import { joinWithProvider, loginWithProvider } from '@hcengineering/account'
import { concatLink, MeasureContext } from '@hcengineering/core'
import Router from 'koa-router'
import { Db } from 'mongodb'
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
import { Passport } from '.'
export function registerGoogle (
ctx: MeasureContext,
passport: Passport,
router: Router<any, any>,
accountsUrl: string,
@ -46,12 +47,12 @@ export function registerGoogle (
const last = ctx.state.user.name.familyName
if (email !== undefined) {
if (ctx.query?.state != null) {
const loginInfo = await joinWithProvider(db, productId, email, first, last, ctx.query.state)
const loginInfo = await joinWithProvider(ctx, db, productId, email, first, last, ctx.query.state)
if (ctx.session != null) {
ctx.session.loginInfo = loginInfo
}
} else {
const loginInfo = await loginWithProvider(db, productId, email, first, last)
const loginInfo = await loginWithProvider(ctx, db, productId, email, first, last)
if (ctx.session != null) {
ctx.session.loginInfo = loginInfo
}

View File

@ -5,10 +5,12 @@ import session from 'koa-session'
import { Db } from 'mongodb'
import { registerGithub } from './github'
import { registerGoogle } from './google'
import { MeasureContext } from '@hcengineering/core'
export type Passport = typeof passport
export type AuthProvider = (
ctx: MeasureContext,
passport: Passport,
router: Router<any, any>,
accountsUrl: string,
@ -18,6 +20,7 @@ export type AuthProvider = (
) => string | undefined
export function registerProviders (
ctx: MeasureContext,
app: Koa<Koa.DefaultState, Koa.DefaultContext>,
router: Router<any, any>,
db: Db,
@ -57,7 +60,7 @@ export function registerProviders (
const res: string[] = []
const providers: AuthProvider[] = [registerGoogle, registerGithub]
for (const provider of providers) {
const value = provider(passport, router, accountsUrl, db, productId, frontUrl)
const value = provider(ctx, passport, router, accountsUrl, db, productId, frontUrl)
if (value !== undefined) res.push(value)
}

View File

@ -19,11 +19,14 @@ import { randomBytes } from 'crypto'
import { Db, MongoClient } from 'mongodb'
import accountPlugin, { getAccount, getMethods, getWorkspaceByUrl } from '..'
import { setMetadata } from '@hcengineering/platform'
import { MeasureMetricsContext } from '@hcengineering/core'
const DB_NAME = 'test_accounts'
const methods = getMethods(getModelVersion(), builder().getTxes(), migrateOperations)
const metricsContext = new MeasureMetricsContext('account', {})
describe('server', () => {
const dbUri = process.env.MONGO_URL ?? 'mongodb://localhost:27017'
let conn: MongoClient
@ -48,7 +51,7 @@ describe('server', () => {
params: [workspace, 'ООО Рога и Копыта']
}
const result = await methods.createWorkspace(db, '', request)
const result = await methods.createWorkspace(metricsContext, db, '', request)
expect(result.result).toBeDefined()
workspace = result.result as string
})
@ -59,12 +62,12 @@ describe('server', () => {
params: ['andrey2', '123']
}
const result = await methods.createAccount(db, '', request)
const result = await methods.createAccount(metricsContext, db, '', request)
expect(result.result).toBeDefined()
})
it('should not create, duplicate account', async () => {
await methods.createAccount(db, '', {
await methods.createAccount(metricsContext, db, '', {
method: 'createAccount',
params: ['andrey', '123']
})
@ -74,20 +77,20 @@ describe('server', () => {
params: ['andrey', '123']
}
const result = await methods.createAccount(db, '', request)
const result = await methods.createAccount(metricsContext, db, '', request)
expect(result.error).toBeDefined()
})
it('should login', async () => {
await methods.createAccount(db, '', {
await methods.createAccount(metricsContext, db, '', {
method: 'createAccount',
params: ['andrey', '123']
})
await methods.createWorkspace(db, '', {
await methods.createWorkspace(metricsContext, db, '', {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
})
await methods.assignWorkspace(db, '', {
await methods.assignWorkspace(metricsContext, db, '', {
method: 'assignWorkspace',
params: ['andrey', workspace]
})
@ -97,7 +100,7 @@ describe('server', () => {
params: ['andrey', '123', workspace]
}
const result = await methods.login(db, '', request)
const result = await methods.login(metricsContext, db, '', request)
expect(result.result).toBeDefined()
})
@ -107,7 +110,7 @@ describe('server', () => {
params: ['andrey', '123555', workspace]
}
const result = await methods.login(db, '', request)
const result = await methods.login(metricsContext, db, '', request)
expect(result.error).toBeDefined()
})
@ -117,7 +120,7 @@ describe('server', () => {
params: ['andrey1', '123555', workspace]
}
const result = await methods.login(db, '', request)
const result = await methods.login(metricsContext, db, '', request)
expect(result.error).toBeDefined()
})
@ -127,20 +130,20 @@ describe('server', () => {
params: ['andrey', '123', 'non-existent-workspace']
}
const result = await methods.login(db, '', request)
const result = await methods.login(metricsContext, db, '', request)
expect(result.error).toBeDefined()
})
it('do remove workspace', async () => {
await methods.createAccount(db, '', {
await methods.createAccount(metricsContext, db, '', {
method: 'createAccount',
params: ['andrey', '123']
})
await methods.createWorkspace(db, '', {
await methods.createWorkspace(metricsContext, db, '', {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
})
await methods.assignWorkspace(db, '', {
await methods.assignWorkspace(metricsContext, db, '', {
method: 'assignWorkspace',
params: ['andrey', workspace]
})
@ -149,7 +152,7 @@ describe('server', () => {
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(1)
expect((await getWorkspaceByUrl(db, '', workspace))?.accounts.length).toEqual(1)
await methods.removeWorkspace(db, '', {
await methods.removeWorkspace(metricsContext, db, '', {
method: 'removeWorkspace',
params: ['andrey', workspace]
})

View File

@ -30,6 +30,8 @@ import core, {
Data,
generateId,
getWorkspaceId,
MeasureContext,
RateLimiter,
Ref,
systemAccountEmail,
Tx,
@ -108,6 +110,8 @@ export interface Workspace {
workspaceName?: string // An displayed workspace name
createdOn: number
lastVisit: number
createdBy: string
}
/**
@ -227,7 +231,7 @@ function toAccountInfo (account: Account): AccountInfo {
return result
}
async function getAccountInfo (db: Db, email: string, password: string): Promise<AccountInfo> {
async function getAccountInfo (ctx: MeasureContext, db: Db, email: string, password: string): Promise<AccountInfo> {
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
@ -241,11 +245,17 @@ async function getAccountInfo (db: Db, email: string, password: string): Promise
return toAccountInfo(account)
}
async function getAccountInfoByToken (db: Db, productId: string, token: string): Promise<LoginInfo> {
async function getAccountInfoByToken (
ctx: MeasureContext,
db: Db,
productId: string,
token: string
): Promise<LoginInfo> {
let email: string = ''
try {
email = decodeToken(token)?.email
} catch (err: any) {
await ctx.error('Invalid token', { token })
throw new PlatformError(new Status(Severity.ERROR, platform.status.Unauthorized, {}))
}
const account = await getAccount(db, email)
@ -270,17 +280,28 @@ async function getAccountInfoByToken (db: Db, productId: string, token: string):
* @param workspace -
* @returns
*/
export async function login (db: Db, productId: string, _email: string, password: string): Promise<LoginInfo> {
export async function login (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
password: string
): Promise<LoginInfo> {
const email = cleanEmail(_email)
console.log(`login attempt:${email}`)
const info = await getAccountInfo(db, email, password)
const result = {
endpoint: getEndpoint(),
email,
confirmed: info.confirmed ?? true,
token: generateToken(email, getWorkspaceId('', productId), getExtra(info))
try {
const info = await getAccountInfo(ctx, db, email, password)
const result = {
endpoint: getEndpoint(),
email,
confirmed: info.confirmed ?? true,
token: generateToken(email, getWorkspaceId('', productId), getExtra(info))
}
await ctx.info('login success', { email, productId })
return result
} catch (err: any) {
await ctx.error('login failed', { email, productId, _email, err })
throw err
}
return result
}
/**
@ -299,6 +320,7 @@ function getExtra (info: Account | AccountInfo | null, rec?: Record<string, any>
* @public
*/
export async function selectWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
token: string,
@ -328,6 +350,7 @@ export async function selectWorkspace (
if (workspaceInfo !== null) {
if (workspaceInfo.disabled === true) {
await ctx.error('workspace disabled', { workspaceUrl, email })
throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl })
)
@ -347,7 +370,7 @@ export async function selectWorkspace (
}
}
}
await ctx.error('workspace error', { workspaceUrl, email })
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
@ -386,6 +409,7 @@ export async function useInvite (db: Db, inviteId: ObjectId): Promise<void> {
* @public
*/
export async function join (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
@ -395,11 +419,11 @@ export async function join (
const email = cleanEmail(_email)
const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email)
console.log(`join attempt:${email}, ${workspace.name}`)
const ws = await assignWorkspace(db, productId, email, workspace.name)
await ctx.info(`join attempt:${email}, ${workspace.name}`)
const ws = await assignWorkspace(ctx, db, productId, email, workspace.name)
const token = (await login(db, productId, email, password)).token
const result = await selectWorkspace(db, productId, token, ws.workspaceUrl ?? ws.workspace)
const token = (await login(ctx, db, productId, email, password)).token
const result = await selectWorkspace(ctx, db, productId, token, ws.workspaceUrl ?? ws.workspace)
await useInvite(db, inviteId)
return result
}
@ -427,10 +451,11 @@ export async function confirmEmail (db: Db, _email: string): Promise<Account> {
/**
* @public
*/
export async function confirm (db: Db, productId: string, token: string): Promise<LoginInfo> {
export async function confirm (ctx: MeasureContext, db: Db, productId: string, token: string): Promise<LoginInfo> {
const decode = decodeToken(token)
const _email = decode.extra?.confirm
if (_email === undefined) {
await ctx.error('confirm email invalid', { token: decode })
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: _email }))
}
const email = cleanEmail(_email)
@ -441,6 +466,7 @@ export async function confirm (db: Db, productId: string, token: string): Promis
email,
token: generateToken(email, getWorkspaceId('', productId), getExtra(account))
}
await ctx.info('confirm success', { email, productId })
return result
}
@ -491,6 +517,7 @@ async function sendConfirmation (productId: string, account: Account): Promise<v
* @public
*/
export async function signUpJoin (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
@ -505,6 +532,7 @@ export async function signUpJoin (
const workspace = await checkInvite(invite, email)
const sesURL = getMetadata(accountPlugin.metadata.SES_URL)
await createAcc(
ctx,
db,
productId,
email,
@ -513,10 +541,10 @@ export async function signUpJoin (
last,
invite?.emailMask === email || sesURL === undefined || sesURL === ''
)
const ws = await assignWorkspace(db, productId, email, workspace.name)
const ws = await assignWorkspace(ctx, db, productId, email, workspace.name)
const token = (await login(db, productId, email, password)).token
const result = await selectWorkspace(db, productId, token, ws.workspaceUrl ?? ws.workspace)
const token = (await login(ctx, db, productId, email, password)).token
const result = await selectWorkspace(ctx, db, productId, token, ws.workspaceUrl ?? ws.workspace)
await useInvite(db, inviteId)
return result
}
@ -525,6 +553,7 @@ export async function signUpJoin (
* @public
*/
export async function createAcc (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
@ -540,6 +569,7 @@ export async function createAcc (
const systemEmails = [systemAccountEmail]
if (systemEmails.includes(email)) {
await ctx.error('system email used for account', { email })
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountAlreadyExists, { account: email }))
}
@ -570,10 +600,11 @@ export async function createAcc (
if (sesURL !== undefined && sesURL !== '') {
await sendConfirmation(productId, newAccount)
} else {
console.info('Please provide email service url to enable email confirmations.')
await ctx.info('Please provide email service url to enable email confirmations.')
await confirmEmail(db, email)
}
}
await ctx.info('account created', { account: email })
return newAccount
}
@ -581,6 +612,7 @@ export async function createAcc (
* @public
*/
export async function createAccount (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
@ -590,7 +622,16 @@ export async function createAccount (
): Promise<LoginInfo> {
const email = cleanEmail(_email)
const sesURL = getMetadata(accountPlugin.metadata.SES_URL)
const account = await createAcc(db, productId, email, password, first, last, sesURL === undefined || sesURL === '')
const account = await createAcc(
ctx,
db,
productId,
email,
password,
first,
last,
sesURL === undefined || sesURL === ''
)
const result = {
endpoint: getEndpoint(),
@ -603,7 +644,7 @@ export async function createAccount (
/**
* @public
*/
export async function listWorkspaces (db: Db, productId: string): Promise<WorkspaceInfo[]> {
export async function listWorkspaces (ctx: MeasureContext, db: Db, productId: string): Promise<WorkspaceInfo[]> {
return (await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray())
.map((it) => ({ ...it, productId }))
.filter((it) => it.disabled !== true)
@ -619,6 +660,22 @@ export async function listWorkspacesRaw (db: Db, productId: string): Promise<Wor
.filter((it) => it.disabled !== true)
}
/**
* @public
*/
export async function listWorkspacesPure (db: Db, productId: string): Promise<Workspace[]> {
return (await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray()).map(
(it) => ({ ...it, productId })
)
}
/**
* @public
*/
export async function setWorkspaceDisabled (db: Db, workspaceId: Workspace['_id'], disabled: boolean): Promise<void> {
await db.collection<Workspace>(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $set: { disabled } })
}
/**
* @public
*/
@ -680,9 +737,10 @@ async function generateWorkspaceRecord (
version,
workspaceName,
accounts: [],
disabled: false,
disabled: true,
createdOn: Date.now(),
lastVisit: Date.now()
lastVisit: Date.now(),
createdBy: email
}
// Add fixed workspace
const id = await coll.insertOne(data)
@ -709,9 +767,10 @@ async function generateWorkspaceRecord (
version,
workspaceName,
accounts: [],
disabled: false,
disabled: true,
createdOn: Date.now(),
lastVisit: Date.now()
lastVisit: Date.now(),
createdBy: email
}
// Nice we do not have a workspace or workspaceUrl duplicated.
const id = await coll.insertOne(data)
@ -736,10 +795,13 @@ async function generateWorkspaceRecord (
let searchPromise: Promise<Workspace> | undefined
const rateLimiter = new RateLimiter(3)
/**
* @public
*/
export async function createWorkspace (
ctx: MeasureContext,
version: Data<Version>,
txes: Tx[],
migrationOperation: [string, MigrateOperation][],
@ -749,33 +811,53 @@ export async function createWorkspace (
workspaceName: string,
workspace?: string
): Promise<{ workspaceInfo: Workspace, err?: any, client?: Client }> {
// We need to search for duplicate workspaceUrl
await searchPromise
return await rateLimiter.exec(async () => {
// We need to search for duplicate workspaceUrl
await searchPromise
// Safe generate workspace record.
searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace)
// Safe generate workspace record.
searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace)
const workspaceInfo = await searchPromise
let client: Client
try {
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
const wsId = getWorkspaceId(workspaceInfo.workspace, productId)
if (initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null) {
client = await initModel(getTransactor(), wsId, txes, [])
await client.close()
await cloneWorkspace(
getTransactor(),
getWorkspaceId(initWS, productId),
getWorkspaceId(workspaceInfo.workspace, productId)
)
client = await upgradeModel(getTransactor(), wsId, txes, migrationOperation)
} else {
client = await initModel(getTransactor(), wsId, txes, migrationOperation)
const workspaceInfo = await searchPromise
let client: Client
const childLogger = ctx.newChild(
'createWorkspace',
{ workspace: workspaceInfo.workspace },
{},
ctx.logger.childLogger?.(workspaceInfo.workspace, {}) ?? ctx.logger
)
const ctxModellogger: ModelLogger = {
log: (msg, data) => {
void childLogger.info(msg, data)
},
error: (msg, data) => {
void childLogger.error(msg, data)
}
}
} catch (err: any) {
return { workspaceInfo, err, client: {} as any }
}
return { workspaceInfo, client }
try {
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
const wsId = getWorkspaceId(workspaceInfo.workspace, productId)
if (initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null) {
client = await initModel(getTransactor(), wsId, txes, [], ctxModellogger)
await client.close()
await cloneWorkspace(
getTransactor(),
getWorkspaceId(initWS, productId),
getWorkspaceId(workspaceInfo.workspace, productId)
)
client = await upgradeModel(getTransactor(), wsId, txes, migrationOperation, ctxModellogger)
} else {
client = await initModel(getTransactor(), wsId, txes, migrationOperation, ctxModellogger)
}
} catch (err: any) {
return { workspaceInfo, err, client: {} as any }
}
// Workspace is created, we need to clear disabled flag.
await db
.collection<Omit<Workspace, '_id'>>(WORKSPACE_COLLECTION)
.updateOne({ _id: workspaceInfo._id }, { $set: { disabled: false } })
return { workspaceInfo, client }
})
}
/**
@ -828,10 +910,10 @@ export async function upgradeWorkspace (
*/
export const createUserWorkspace =
(version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
async (db: Db, productId: string, token: string, workspaceName: string): Promise<LoginInfo> => {
async (ctx: MeasureContext, db: Db, productId: string, token: string, workspaceName: string): Promise<LoginInfo> => {
const { email } = decodeToken(token)
console.log(`Creating workspace for "${workspaceName}" for ${email}`)
await ctx.info('Creating workspace', { workspaceName, email })
const info = await getAccount(db, email)
@ -851,6 +933,7 @@ export const createUserWorkspace =
}
const { workspaceInfo, err, client } = await createWorkspace(
ctx,
version,
txes,
migrationOperation,
@ -861,7 +944,7 @@ export const createUserWorkspace =
)
if (err != null) {
console.error(err)
await ctx.error('failed to create workspace', { err, workspaceName, email })
// We need to drop workspace, to prevent wrong data usage.
await db.collection(WORKSPACE_COLLECTION).updateOne(
@ -880,7 +963,7 @@ export const createUserWorkspace =
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null
await assignWorkspace(db, productId, email, workspaceInfo.workspace, shouldUpdateAccount, client)
await assignWorkspace(ctx, db, productId, email, workspaceInfo.workspace, shouldUpdateAccount, client)
await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner, client)
} finally {
await client?.close()
@ -892,7 +975,7 @@ export const createUserWorkspace =
productId,
workspace: workspaceInfo.workspaceUrl
}
console.log(`Creating workspace "${workspaceName}" Done`)
await ctx.info('Creating workspace done', { workspaceName, email })
return result
}
@ -900,6 +983,7 @@ export const createUserWorkspace =
* @public
*/
export async function getInviteLink (
ctx: MeasureContext,
db: Db,
productId: string,
token: string,
@ -907,13 +991,15 @@ export async function getInviteLink (
emailMask: string,
limit: number
): Promise<ObjectId> {
const { workspace } = decodeToken(token)
const { workspace, email } = decodeToken(token)
const wsPromise = await getWorkspaceById(db, productId, workspace.name)
if (wsPromise === null) {
await ctx.error('workspace not found', { workspace, email })
throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspace.name })
)
}
await ctx.info('Getting invite link', { workspace: workspace.name, emailMask, limit })
const result = await db.collection(INVITE_COLLECTION).insertOne({
workspace,
exp: Date.now() + exp,
@ -946,10 +1032,18 @@ function trimWorkspaceInfo (ws: Workspace): WorkspaceInfo {
/**
* @public
*/
export async function getUserWorkspaces (db: Db, productId: string, token: string): Promise<ClientWorkspaceInfo[]> {
export async function getUserWorkspaces (
ctx: MeasureContext,
db: Db,
productId: string,
token: string
): Promise<ClientWorkspaceInfo[]> {
const { email } = decodeToken(token)
const account = await getAccount(db, email)
if (account === null) return []
if (account === null) {
await ctx.error('account not found', { email })
return []
}
return (
await db
.collection<Workspace>(WORKSPACE_COLLECTION)
@ -964,6 +1058,7 @@ export async function getUserWorkspaces (db: Db, productId: string, token: strin
* @public
*/
export async function getWorkspaceInfo (
ctx: MeasureContext,
db: Db,
productId: string,
token: string,
@ -1021,6 +1116,7 @@ async function updateLastVisit (db: Db, ws: Workspace, account: Account): Promis
}
async function getWorkspaceAndAccount (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
@ -1072,6 +1168,7 @@ export async function setRole (
* @public
*/
export async function assignWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
@ -1083,9 +1180,10 @@ export async function assignWorkspace (
const email = cleanEmail(_email)
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
if (initWS !== undefined && initWS === workspaceId) {
await ctx.error('assign-workspace failed', { email, workspaceId, reason: 'initWs === workspaceId' })
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
const workspaceInfo = await getWorkspaceAndAccount(db, productId, email, workspaceId)
const workspaceInfo = await getWorkspaceAndAccount(ctx, db, productId, email, workspaceId)
if (workspaceInfo.account !== null) {
await createPersonAccount(
@ -1107,6 +1205,8 @@ export async function assignWorkspace (
await db
.collection(ACCOUNT_COLLECTION)
.updateOne({ _id: workspaceInfo.account._id }, { $addToSet: { workspaces: workspaceInfo.workspace._id } })
await ctx.info('assign-workspace success', { email, workspaceId })
return workspaceInfo.workspace
}
@ -1255,6 +1355,7 @@ async function createPersonAccount (
* @public
*/
export async function changePassword (
ctx: MeasureContext,
db: Db,
productId: string,
token: string,
@ -1262,12 +1363,13 @@ export async function changePassword (
password: string
): Promise<void> {
const { email } = decodeToken(token)
const account = await getAccountInfo(db, email, oldPassword)
const account = await getAccountInfo(ctx, db, email, oldPassword)
const salt = randomBytes(32)
const hash = hashWithSalt(password, salt)
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $set: { salt, hash } })
await ctx.info('change-password success', { email })
}
/**
@ -1288,11 +1390,12 @@ export async function replacePassword (db: Db, productId: string, email: string,
/**
* @public
*/
export async function requestPassword (db: Db, productId: string, _email: string): Promise<void> {
export async function requestPassword (ctx: MeasureContext, db: Db, productId: string, _email: string): Promise<void> {
const email = cleanEmail(_email)
const account = await getAccount(db, email)
if (account === null) {
await ctx.info('account not found', { email })
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
}
@ -1332,12 +1435,19 @@ export async function requestPassword (db: Db, productId: string, _email: string
to
})
})
await ctx.info('recovery email sent', { email, accountEmail: account.email })
}
/**
* @public
*/
export async function restorePassword (db: Db, productId: string, token: string, password: string): Promise<LoginInfo> {
export async function restorePassword (
ctx: MeasureContext,
db: Db,
productId: string,
token: string,
password: string
): Promise<LoginInfo> {
const decode = decodeToken(token)
const email = decode.extra?.restore
if (email === undefined) {
@ -1351,7 +1461,7 @@ export async function restorePassword (db: Db, productId: string, token: string,
await updatePassword(db, account, password)
return await login(db, productId, email, password)
return await login(ctx, db, productId, email, password)
}
async function updatePassword (db: Db, account: Account, password: string | null): Promise<void> {
@ -1364,20 +1474,28 @@ async function updatePassword (db: Db, account: Account, password: string | null
/**
* @public
*/
export async function removeWorkspace (db: Db, productId: string, email: string, workspaceId: string): Promise<void> {
const { workspace, account } = await getWorkspaceAndAccount(db, productId, email, workspaceId)
export async function removeWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
email: string,
workspaceId: string
): Promise<void> {
const { workspace, account } = await getWorkspaceAndAccount(ctx, db, productId, email, workspaceId)
// Add account into workspace.
await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspace._id }, { $pull: { accounts: account._id } })
// Add account a workspace
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $pull: { workspaces: workspace._id } })
await ctx.info('Workspace removed', { email, workspace })
}
/**
* @public
*/
export async function checkJoin (
ctx: MeasureContext,
db: Db,
productId: string,
token: string,
@ -1388,17 +1506,23 @@ export async function checkJoin (
const workspace = await checkInvite(invite, email)
const ws = await getWorkspaceById(db, productId, workspace.name)
if (ws === null) {
await ctx.error('workspace not found', { name: workspace.name, email, inviteId })
throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspace.name })
)
}
return await selectWorkspace(db, productId, token, ws?.workspaceUrl ?? ws.workspace, false)
return await selectWorkspace(ctx, db, productId, token, ws?.workspaceUrl ?? ws.workspace, false)
}
/**
* @public
*/
export async function dropWorkspace (db: Db, productId: string, workspaceId: string): Promise<void> {
export async function dropWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
workspaceId: string
): Promise<void> {
const ws = await getWorkspaceById(db, productId, workspaceId)
if (ws === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceId }))
@ -1407,12 +1531,14 @@ export async function dropWorkspace (db: Db, productId: string, workspaceId: str
await db
.collection<Account>(ACCOUNT_COLLECTION)
.updateMany({ _id: { $in: ws.accounts ?? [] } }, { $pull: { workspaces: ws._id } })
await ctx.info('Workspace dropped', { workspace: ws.workspace })
}
/**
* @public
*/
export async function dropAccount (db: Db, productId: string, email: string): Promise<void> {
export async function dropAccount (ctx: MeasureContext, db: Db, productId: string, email: string): Promise<void> {
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
@ -1425,7 +1551,7 @@ export async function dropAccount (db: Db, productId: string, email: string): Pr
await Promise.all(
workspaces.map(async (ws) => {
await deactivatePersonAccount(account.email, ws.workspace, productId)
await deactivatePersonAccount(ctx, account.email, ws.workspace, productId)
})
)
@ -1433,12 +1559,19 @@ export async function dropAccount (db: Db, productId: string, email: string): Pr
await db
.collection<Workspace>(WORKSPACE_COLLECTION)
.updateMany({ _id: { $in: account.workspaces } }, { $pull: { accounts: account._id } })
await ctx.info('Account Dropped', { email, account })
}
/**
* @public
*/
export async function leaveWorkspace (db: Db, productId: string, token: string, email: string): Promise<void> {
export async function leaveWorkspace (
ctx: MeasureContext,
db: Db,
productId: string,
token: string,
email: string
): Promise<void> {
const tokenData = decodeToken(token)
const currentAccount = await getAccount(db, tokenData.email)
@ -1453,7 +1586,7 @@ export async function leaveWorkspace (db: Db, productId: string, token: string,
)
}
await deactivatePersonAccount(email, workspace.workspace, workspace.productId)
await deactivatePersonAccount(ctx, email, workspace.workspace, workspace.productId)
const account = tokenData.email !== email ? await getAccount(db, email) : currentAccount
if (account !== null) {
@ -1464,12 +1597,19 @@ export async function leaveWorkspace (db: Db, productId: string, token: string,
.collection<Account>(ACCOUNT_COLLECTION)
.updateOne({ _id: account._id }, { $pull: { workspaces: workspace._id } })
}
await ctx.info('Account removed from workspace', { email, workspace })
}
/**
* @public
*/
export async function sendInvite (db: Db, productId: string, token: string, email: string): Promise<void> {
export async function sendInvite (
ctx: MeasureContext,
db: Db,
productId: string,
token: string,
email: string
): Promise<void> {
const tokenData = decodeToken(token)
const currentAccount = await getAccount(db, tokenData.email)
if (currentAccount === null) {
@ -1483,8 +1623,9 @@ export async function sendInvite (db: Db, productId: string, token: string, emai
)
}
const account = await getAccount(db, email)
if (account !== null) return
// TODO: Why we not send invite if user has account???
// const account = await getAccount(db, email)
// if (account !== null) return
const sesURL = getMetadata(accountPlugin.metadata.SES_URL)
if (sesURL === undefined || sesURL === '') {
@ -1498,7 +1639,7 @@ export async function sendInvite (db: Db, productId: string, token: string, emai
const expHours = 48
const exp = expHours * 60 * 60 * 1000
const inviteId = await getInviteLink(db, productId, token, exp, email, 1)
const inviteId = await getInviteLink(ctx, db, productId, token, exp, email, 1)
const link = concatLink(front, `/login/join?inviteId=${inviteId.toString()}`)
const ws = workspace.workspace
@ -1519,9 +1660,15 @@ export async function sendInvite (db: Db, productId: string, token: string, emai
to
})
})
await ctx.info('Invite sent', { email, workspace, link })
}
async function deactivatePersonAccount (email: string, workspace: string, productId: string): Promise<void> {
async function deactivatePersonAccount (
ctx: MeasureContext,
email: string,
workspace: string,
productId: string
): Promise<void> {
const connection = await connect(getTransactor(), getWorkspaceId(workspace, productId))
try {
const ops = new TxOperations(connection, core.account.System)
@ -1535,6 +1682,7 @@ async function deactivatePersonAccount (email: string, workspace: string, produc
active: false
})
}
await ctx.info('account deactivated', { email, workspace })
}
} finally {
await connection.close()
@ -1544,12 +1692,20 @@ async function deactivatePersonAccount (email: string, workspace: string, produc
/**
* @public
*/
export type AccountMethod = (db: Db, productId: string, request: any, token?: string) => Promise<any>
export type AccountMethod = (
ctx: MeasureContext,
db: Db,
productId: string,
request: any,
token?: string
) => Promise<any>
function wrap (f: (db: Db, productId: string, ...args: any[]) => Promise<any>): AccountMethod {
return async function (db: Db, productId: string, request: any, token?: string): Promise<any> {
function wrap (
accountMethod: (ctx: MeasureContext, db: Db, productId: string, ...args: any[]) => Promise<any>
): AccountMethod {
return async function (ctx: MeasureContext, db: Db, productId: string, request: any, token?: string): Promise<any> {
if (token !== undefined) request.params.unshift(token)
return await f(db, productId, ...request.params)
return await accountMethod(ctx, db, productId, ...request.params)
.then((result) => ({ id: request.id, result }))
.catch((err) => {
const status =
@ -1557,9 +1713,9 @@ function wrap (f: (db: Db, productId: string, ...args: any[]) => Promise<any>):
? err.status
: new Status(Severity.ERROR, platform.status.InternalServerError, {})
if (status.code === platform.status.InternalServerError) {
console.error(status, err)
void ctx.error('error', { status, err })
} else {
console.error(status)
void ctx.error('error', { status })
}
return {
error: status
@ -1569,6 +1725,7 @@ function wrap (f: (db: Db, productId: string, ...args: any[]) => Promise<any>):
}
export async function joinWithProvider (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
@ -1602,17 +1759,17 @@ export async function joinWithProvider (
return result
}
const wsRes = await assignWorkspace(db, productId, email, workspace.name, false)
const result = await selectWorkspace(db, productId, token, wsRes.workspaceUrl ?? wsRes.workspace, false)
const wsRes = await assignWorkspace(ctx, db, productId, email, workspace.name, false)
const result = await selectWorkspace(ctx, db, productId, token, wsRes.workspaceUrl ?? wsRes.workspace, false)
await useInvite(db, inviteId)
return result
}
const newAccount = await createAcc(db, productId, email, null, first, last, true, extra)
const newAccount = await createAcc(ctx, db, productId, email, null, first, last, true, extra)
const token = generateToken(email, getWorkspaceId('', productId), getExtra(newAccount))
const ws = await assignWorkspace(db, productId, email, workspace.name, false)
const result = await selectWorkspace(db, productId, token, ws.workspaceUrl ?? ws.workspace, false)
const ws = await assignWorkspace(ctx, db, productId, email, workspace.name, false)
const result = await selectWorkspace(ctx, db, productId, token, ws.workspaceUrl ?? ws.workspace, false)
await useInvite(db, inviteId)
@ -1620,6 +1777,7 @@ export async function joinWithProvider (
}
export async function loginWithProvider (
ctx: MeasureContext,
db: Db,
productId: string,
_email: string,
@ -1645,7 +1803,7 @@ export async function loginWithProvider (
return result
}
const newAccount = await createAcc(db, productId, email, null, first, last, true, extra)
const newAccount = await createAcc(ctx, db, productId, email, null, first, last, true, extra)
const result = {
endpoint: getEndpoint(),

View File

@ -56,7 +56,7 @@ export class MinioService implements StorageAdapter {
try {
const items = new Map<string, WorkspaceItem>()
const list = this.client.listObjects(getBucketId(workspaceId), prefix, true)
await new Promise((resolve) => {
await new Promise((resolve, reject) => {
list.on('data', (data) => {
if (data.name !== undefined) {
items.set(data.name, { metaData: {}, ...data } as any)
@ -66,10 +66,14 @@ export class MinioService implements StorageAdapter {
list.destroy()
resolve(null)
})
list.on('error', (err) => {
reject(err)
})
})
return Array.from(items.values())
} catch (err: any) {
if (((err?.message as string) ?? '').includes('Invalid bucket name')) {
const msg = (err?.message as string) ?? ''
if (msg.includes('Invalid bucket name') || msg.includes('The specified bucket does not exist')) {
return []
}
throw err
@ -98,7 +102,7 @@ export class MinioService implements StorageAdapter {
const data = await this.client.getObject(getBucketId(workspaceId), name)
const chunks: Buffer[] = []
await new Promise((resolve) => {
await new Promise((resolve, reject) => {
data.on('readable', () => {
let chunk
while ((chunk = data.read()) !== null) {
@ -111,6 +115,9 @@ export class MinioService implements StorageAdapter {
data.destroy()
resolve(null)
})
data.on('error', (err) => {
reject(err)
})
})
return chunks
}

View File

@ -152,5 +152,11 @@ export async function createStorageDataAdapter (
if (storage === undefined) {
throw new Error('minio storage adapter require minio')
}
// We need to create bucket if it doesn't exist
if (storage !== undefined) {
if (!(await storage.exists(workspaceId))) {
await storage.make(workspaceId)
}
}
return new StorageBlobAdapter(workspaceId, storage)
}

View File

@ -53,8 +53,12 @@ export class FileModelLogger implements ModelLogger {
this.handle = fs.createWriteStream(this.file, { flags: 'a' })
}
log (...data: any[]): void {
this.handle.write(data.map((it: any) => JSON.stringify(it)).join(' ') + '\n')
log (msg: string, data: any): void {
this.handle.write(msg + ' : ' + JSON.stringify(data) + '\n')
}
error (msg: string, data: any): void {
this.handle.write(msg + ': ' + JSON.stringify(data) + '\n')
}
close (): void {
@ -129,34 +133,32 @@ export async function initModel (
await client.connect()
const db = getWorkspaceDB(client, workspaceId)
logger.log('dropping database...', workspaceId)
await db.dropDatabase()
logger.log('creating model...', workspaceId)
const model = txes
const result = await db.collection(DOMAIN_TX).insertMany(model as Document[])
logger.log(`${result.insertedCount} model transactions inserted.`)
logger.log('model transactions inserted.', { count: result.insertedCount })
logger.log('creating data...', transactorUrl)
logger.log('creating data...', { transactorUrl })
connection = (await connect(transactorUrl, workspaceId, undefined, {
model: 'upgrade'
model: 'upgrade',
admin: 'true'
})) as unknown as CoreClient & BackupClient
try {
for (const op of migrateOperations) {
logger.log('Migrate', op[0])
logger.log('Migrate', { name: op[0] })
await op[1].upgrade(connection, logger)
}
// Create update indexes
await createUpdateIndexes(connection, db, logger)
logger.log('create minio bucket')
logger.log('create minio bucket', { workspaceId })
if (!(await minio.exists(workspaceId))) {
await minio.make(workspaceId)
}
} catch (e) {
logger.log(e)
} catch (e: any) {
logger.error('error', { error: e })
}
} finally {
await client.close()
@ -185,19 +187,19 @@ export async function upgradeModel (
await client.connect()
const db = getWorkspaceDB(client, workspaceId)
logger.log(`${workspaceId.name}: removing model...`)
logger.log('removing model...', { workspaceId: workspaceId.name })
// we're preserving accounts (created by core.account.System).
const result = await db.collection(DOMAIN_TX).deleteMany({
objectSpace: core.space.Model,
modifiedBy: core.account.System,
objectClass: { $nin: [contact.class.PersonAccount, 'contact:class:EmployeeAccount'] }
})
logger.log(`${workspaceId.name}: ${result.deletedCount} transactions deleted.`)
logger.log('transactions deleted.', { workspaceId: workspaceId.name, count: result.deletedCount })
logger.log(`${workspaceId.name}: creating model...`)
logger.log('creating model...', { workspaceId: workspaceId.name })
const model = txes
const insert = await db.collection(DOMAIN_TX).insertMany(model as Document[])
logger.log(`${workspaceId.name}: ${insert.insertedCount} model transactions inserted.`)
logger.log('model transactions inserted.', { workspaceId: workspaceId.name, count: insert.insertedCount })
const hierarchy = new Hierarchy()
const modelDb = new ModelDb(hierarchy)
@ -216,10 +218,10 @@ export async function upgradeModel (
for (const op of migrateOperations) {
const t = Date.now()
await op[1].migrate(migrateClient, logger)
logger.log(`${workspaceId.name}: migrate:`, op[0], Date.now() - t)
logger.log('migrate:', { workspaceId: workspaceId.name, operation: op[0], time: Date.now() - t })
}
logger.log(`${workspaceId.name}: Apply upgrade operations`)
logger.log('Apply upgrade operations', { workspaceId: workspaceId.name })
const connection = await connect(transactorUrl, workspaceId, undefined, {
mode: 'backup',
@ -233,7 +235,7 @@ export async function upgradeModel (
for (const op of migrateOperations) {
const t = Date.now()
await op[1].upgrade(connection, logger)
logger.log(`${workspaceId.name}: upgrade:`, op[0], Date.now() - t)
logger.log('upgrade:', { operation: op[0], time: Date.now() - t, workspaceId: workspaceId.name })
}
return connection
@ -296,12 +298,12 @@ async function createUpdateIndexes (connection: CoreClient, db: Db, logger: Mode
await collection.createIndex(vv)
}
} catch (err: any) {
logger.log('error: failed to create index', d, vv, JSON.stringify(err))
logger.error('error: failed to create index', { d, vv, err })
}
bb.push(vv)
}
if (bb.length > 0) {
logger.log('created indexes', d, JSON.stringify(bb))
logger.log('created indexes', { d, bb })
}
}
}

View File

@ -134,7 +134,7 @@ export class MigrateClientImpl implements MigrationClient {
}
} finally {
if (Date.now() - t > 1000) {
this.logger.log(`update${Date.now() - t > 5000 ? 'slow' : ''}`, domain, query, Date.now() - t)
this.logger.log(`update${Date.now() - t > 5000 ? 'slow' : ''}`, { domain, query, time: Date.now() - t })
}
}
}
@ -160,7 +160,7 @@ export class MigrateClientImpl implements MigrationClient {
query: DocumentQuery<T>,
targetDomain: Domain
): Promise<MigrationResult> {
this.logger.log('move', sourceDomain, query)
this.logger.log('move', { sourceDomain, query })
const q = this.translateQuery(query)
const cursor = this.db.collection(sourceDomain).find<T>(q)
const target = this.db.collection(targetDomain)