platform/server/account-service/src/index.ts
Alexey Zinoviev 171e1e88b7
uberf-8480: configurable liveness condition for upgrade (#6943)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
2024-10-15 17:09:00 +04:00

264 lines
7.5 KiB
TypeScript

//
// Copyright © 2023 Hardcore Engineering Inc.
//
import account, {
EndpointKind,
accountId,
cleanExpiredOtp,
getAccountDB,
getAllTransactors,
getMethods
} from '@hcengineering/account'
import accountEn from '@hcengineering/account/lang/en.json'
import accountRu from '@hcengineering/account/lang/ru.json'
import { Analytics } from '@hcengineering/analytics'
import { registerProviders } from '@hcengineering/auth-providers'
import { metricsAggregate, type BrandingMap, type MeasureContext } from '@hcengineering/core'
import platform, { Severity, Status, addStringsLoader, setMetadata } from '@hcengineering/platform'
import serverToken, { decodeToken } from '@hcengineering/server-token'
import toolPlugin from '@hcengineering/server-tool'
import cors from '@koa/cors'
import { type IncomingHttpHeaders } from 'http'
import Koa from 'koa'
import bodyParser from 'koa-bodyparser'
import Router from 'koa-router'
import os from 'os'
/**
* @public
*/
export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap, onClose?: () => void): void {
console.log('Starting account service with brandings: ', brandings)
const ACCOUNT_PORT = parseInt(process.env.ACCOUNT_PORT ?? '3000')
const dbUrl = process.env.DB_URL
if (dbUrl === undefined) {
console.log('Please provide DB_URL')
process.exit(1)
}
const transactorUri = process.env.TRANSACTOR_URL
if (transactorUri === undefined) {
console.log('Please provide transactor url')
process.exit(1)
}
const serverSecret = process.env.SERVER_SECRET
if (serverSecret === undefined) {
console.log('Please provide server secret')
process.exit(1)
}
addStringsLoader(accountId, async (lang: string) => {
switch (lang) {
case 'en':
return accountEn
case 'ru':
return accountRu
default:
return accountEn
}
})
const ses = process.env.SES_URL
const frontURL = process.env.FRONT_URL
const productName = process.env.PRODUCT_NAME
const lang = process.env.LANGUAGE ?? 'en'
const wsLivenessDaysRaw = process.env.WS_LIVENESS_DAYS
let wsLivenessDays: number | undefined
if (wsLivenessDaysRaw !== undefined) {
try {
wsLivenessDays = parseInt(wsLivenessDaysRaw)
} catch (err: any) {
// DO NOTHING
}
}
setMetadata(account.metadata.Transactors, transactorUri)
setMetadata(platform.metadata.locale, lang)
setMetadata(account.metadata.ProductName, productName)
setMetadata(account.metadata.OtpTimeToLiveSec, parseInt(process.env.OTP_TIME_TO_LIVE ?? '60'))
setMetadata(account.metadata.OtpRetryDelaySec, parseInt(process.env.OTP_RETRY_DELAY ?? '60'))
setMetadata(account.metadata.SES_URL, ses)
setMetadata(account.metadata.FrontURL, frontURL)
setMetadata(account.metadata.WsLivenessDays, wsLivenessDays)
setMetadata(serverToken.metadata.Secret, serverSecret)
const initScriptUrl = process.env.INIT_SCRIPT_URL
if (initScriptUrl !== undefined) {
setMetadata(toolPlugin.metadata.InitScriptURL, initScriptUrl)
}
const hasSignUp = process.env.DISABLE_SIGNUP !== 'true'
const methods = getMethods(hasSignUp)
const accountsDb = getAccountDB(dbUrl)
const app = new Koa()
const router = new Router()
app.use(
cors({
credentials: true
})
)
app.use(bodyParser())
registerProviders(
measureCtx,
app,
router,
new Promise((resolve) => {
void accountsDb.then((res) => {
const [db] = res
resolve(db)
})
}),
serverSecret,
frontURL,
brandings,
!hasSignUp
)
void accountsDb.then((res) => {
const [db] = res
setInterval(
() => {
void cleanExpiredOtp(db)
},
3 * 60 * 1000
)
})
const extractToken = (header: IncomingHttpHeaders): string | undefined => {
try {
return header.authorization?.slice(7) ?? undefined
} catch {
return undefined
}
}
router.get('/api/v1/statistics', (req, res) => {
try {
const token = req.query.token as string
const payload = decodeToken(token)
const admin = payload.extra?.admin === 'true'
const data: Record<string, any> = {
metrics: admin ? metricsAggregate((measureCtx as any).metrics) : {},
statistics: {}
}
data.statistics.totalClients = 0
data.statistics.memoryUsed = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100
data.statistics.memoryTotal = Math.round((process.memoryUsage().heapTotal / 1024 / 1024) * 100) / 100
data.statistics.cpuUsage = Math.round(os.loadavg()[0] * 100) / 100
data.statistics.freeMem = Math.round((os.freemem() / 1024 / 1024) * 100) / 100
data.statistics.totalMem = Math.round((os.totalmem() / 1024 / 1024) * 100) / 100
const json = JSON.stringify(data)
req.res.writeHead(200, { 'Content-Type': 'application/json' })
req.res.end(json)
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
req.res.writeHead(404, {})
req.res.end()
}
})
router.put('/api/v1/manage', async (req, res) => {
try {
const token = req.query.token as string
const payload = decodeToken(token)
if (payload.extra?.admin !== 'true') {
req.res.writeHead(404, {})
req.res.end()
return
}
const operation = req.query.operation
switch (operation) {
case 'maintenance': {
const timeMinutes = parseInt((req.query.timeout as string) ?? '5')
const transactors = getAllTransactors(EndpointKind.Internal)
for (const tr of transactors) {
const serverEndpoint = tr.replaceAll('wss://', 'https://').replace('ws://', 'http://')
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=maintenance&timeout=${timeMinutes}`, {
method: 'PUT'
})
}
req.res.writeHead(200)
req.res.end()
return
}
}
req.res.writeHead(404, {})
req.res.end()
} catch (err: any) {
Analytics.handleError(err)
req.res.writeHead(404, {})
req.res.end()
}
})
router.post('rpc', '/', async (ctx) => {
const token = extractToken(ctx.request.headers)
const request = ctx.request.body as any
const method = methods[request.method]
if (method === undefined) {
const response = {
id: request.id,
error: new Status(Severity.ERROR, platform.status.UnknownMethod, { method: request.method })
}
ctx.body = JSON.stringify(response)
}
const [db] = await accountsDb
let host: string | undefined
const origin = ctx.request.headers.origin ?? ctx.request.headers.referer
if (origin !== undefined) {
host = new URL(origin).host
}
const branding = host !== undefined ? brandings[host] : null
const result = await measureCtx.with(
request.method,
{},
async (ctx) => await method(ctx, db, branding, request, token)
)
ctx.body = result
})
app.use(router.routes()).use(router.allowedMethods())
const server = app.listen(ACCOUNT_PORT, () => {
console.log(`server started on port ${ACCOUNT_PORT}`)
})
const close = (): void => {
onClose?.()
void accountsDb.then(([, closeAccountsDb]) => {
closeAccountsDb()
})
server.close()
}
process.on('uncaughtException', (e) => {
measureCtx.error('uncaughtException', { error: e })
})
process.on('unhandledRejection', (reason, promise) => {
measureCtx.error('Unhandled Rejection at:', { reason, promise })
})
process.on('SIGINT', close)
process.on('SIGTERM', close)
process.on('exit', close)
}