Files
overleaf-cep/services/history-v1/backup-deletion-app.mjs
T
Jakob Ackermann 49969f6e0d [history-v1] adopt strict zod types and extend test coverage (#36089)
GitOrigin-RevId: 7bdffe2c05c7c17be6eb3e4296d8df05d650c53e
2026-09-17 11:49:08 +00:00

103 lines
2.7 KiB
JavaScript

// @ts-check
// Metrics must be initialized before importing anything else
import '@overleaf/metrics/initialize.js'
import http from 'node:http'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import express from 'express'
import logger from '@overleaf/logger'
import Metrics from '@overleaf/metrics'
import { hasValidBasicAuthCredentials } from './api/middleware/security.js'
import {
handleValidationError,
parseReq,
z,
zz,
} from '@overleaf/validation-tools'
import {
deleteProjectBackupCb,
healthCheck,
healthCheckCb,
NotReadyToDelete,
} from './storage/lib/backupDeletion.mjs'
import { mongodb } from './storage/index.js'
const app = express()
logger.initialize('history-v1-backup-deletion')
Metrics.open_sockets.monitor()
Metrics.injectMetricsRoute(app)
app.use(Metrics.http.monitor(logger))
Metrics.leaked_sockets.monitor(logger)
Metrics.event_loop.monitor(logger)
Metrics.memory.monitor(logger)
function basicAuth(req, res, next) {
if (hasValidBasicAuthCredentials(req)) return next()
res.setHeader('WWW-Authenticate', 'Basic realm="Application"')
res.sendStatus(401)
}
const deleteProjectBackupSchema = z.object({
params: z.strictObject({
projectId: zz.objectId(),
}),
})
app.delete('/project/:projectId/backup', basicAuth, (req, res, next) => {
// Rollout-temporary: main read req.params directly here; log-only while
// REQ_VALIDATION_MODE=log rolls this new call site out. No transforms in
// deleteProjectBackupSchema, so a raw passthrough on failure needs no
// fallback.
const { params } = parseReq(req, deleteProjectBackupSchema, {
logOnly: true,
})
deleteProjectBackupCb(params.projectId, err => {
if (err) {
return next(err)
}
res.sendStatus(204)
})
})
app.get('/status', (req, res) => {
res.send('history-v1-backup-deletion is up')
})
app.get('/health_check', (req, res, next) => {
healthCheckCb(err => {
if (err) return next(err)
res.sendStatus(200)
})
})
app.use(handleValidationError)
app.use((err, req, res, next) => {
req.logger.addFields({ err })
if (err instanceof NotReadyToDelete) {
req.logger.setLevel('warn')
return res.status(422).send(err.message)
}
req.logger.setLevel('error')
next(err)
})
/**
* @param {number} port
* @return {Promise<http.Server>}
*/
export async function startApp(port) {
await mongodb.client.connect()
await healthCheck()
const server = http.createServer(app)
await promisify(server.listen.bind(server, port))()
return server
}
// Run this if we're called directly
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const PORT = parseInt(process.env.PORT || '3101', 10)
await startApp(PORT)
}