Files
overleaf-cep/services/web/modules/admin-tools/app/src/ProjectListController.mjs
2026-05-19 15:51:34 +02:00

240 lines
7.2 KiB
JavaScript

import _ from 'lodash'
import Path from 'node:path'
import { fileURLToPath } from 'node:url'
import { expressify } from '@overleaf/promise-utils'
import logger from '@overleaf/logger'
import Metrics from '@overleaf/metrics'
import ProjectHelper from '../../../../app/src/Features/Project/ProjectHelper.mjs'
import ProjectGetter from '../../../../app/src/Features/Project/ProjectGetter.mjs'
import PrivilegeLevels from '../../../../app/src/Features/Authorization/PrivilegeLevels.mjs'
import SessionManager from '../../../../app/src/Features/Authentication/SessionManager.mjs'
import UserGetter from '../../../../app/src/Features/User/UserGetter.mjs'
import { OError } from '../../../../app/src/Features/Errors/Errors.js'
import { User } from '../../../../app/src/models/User.mjs'
import { Project } from '../../../../app/src/models/Project.mjs'
import { DeletedProject } from '../../../../app/src/models/DeletedProject.mjs'
import ProjectDeleter from '../../../../app/src/Features/Project/ProjectDeleter.mjs'
import HttpErrorHandler from '../../../../app/src/Features/Errors/HttpErrorHandler.mjs'
const __dirname = Path.dirname(fileURLToPath(import.meta.url))
async function manageProjectsPage(req, res, next) {
const projectsBlobPending = _getProjects().catch(err => {
logger.err({ err }, 'projects listing in background failed')
return undefined
})
const prefetchedProjectsBlob = await projectsBlobPending
Metrics.inc('project-list-prefetch-projects', 1, {
status: prefetchedProjectsBlob ? 'success' : 'error',
})
res.render(Path.resolve(__dirname, '../views/manage-projects-react'), {
title: 'Manage Projects',
prefetchedProjectsBlob,
})
}
async function getProjectsJson(req, res) {
const { filters, page, sort } = req.body
const { userId } = req.params
const projectsPage = await _getProjects(userId, filters, sort, page)
res.json(projectsPage)
}
async function _getProjects(
userId = null,
filters = {},
sort = { by: 'lastUpdated', order: 'desc' },
page = { size: 20 }
) {
const projection = {
_id: 1,
name: 1,
lastUpdated: 1,
lastUpdatedBy: 1,
lastOpened: 1,
trashed: 1,
owner_ref: 1,
}
const actualProjects = await Project.find(
userId == null ? {} : { owner_ref: userId },
projection,
).lean().exec()
const delProjection = Object.fromEntries(
Object.keys(projection).map(k => [`project.${k}`, 1])
)
delProjection['deleterData.deletedAt'] = 1
delProjection['deleterData.deleterId'] = 1
const deletedProjects = await DeletedProject.find(
userId == null ? { project: { $type: 'object' } } : { 'project.owner_ref': userId },
delProjection
).lean().exec()
const formattedActualProjects = _formatProjects(actualProjects, _formatProjectInfo)
const formattedDeletedProjects = _formatProjects(deletedProjects, _formatDeletedProjectInfo)
const formattedProjects = [...formattedActualProjects, ...formattedDeletedProjects]
const filteredProjects = _applyFilters(formattedProjects, filters)
const projects = _sortAndPaginate(filteredProjects, sort, page)
return {
totalSize: filteredProjects.length,
projects,
}
}
function _formatProjects(projects, formatProjectInfo) {
const yearAgo = new Date()
yearAgo.setFullYear(yearAgo.getFullYear() - 1)
const formattedProjects = []
for (const project of projects) {
formattedProjects.push(
formatProjectInfo(project, yearAgo)
)
}
return formattedProjects
}
function _applyFilters(projects, filters) {
if (!_hasActiveFilter(filters)) {
return projects
}
return projects.filter(project => _matchesFilters(project, filters))
}
function _sortAndPaginate(projects, sort, page) {
if (
(sort.by && !['lastUpdated', 'title', 'deletedAt', 'owner'].includes(sort.by)) ||
(sort.order && !['asc', 'desc'].includes(sort.order))
) {
throw new OError('Invalid sorting criteria', { sort })
}
// sorting by owner is not implemented, it is mot needed
const sortedProjects =
sort.by === 'title'
? [...projects].sort((a, b) =>
(a.name ?? '\uffff').localeCompare(b.name ?? '\uffff')
)
: _.orderBy(
projects,
[sort.by || 'lastUpdated'],
[sort.order || 'desc']
)
return sortedProjects
}
function _formatProjectInfo(project, maxDate) {
const owner_ref = project.owner_ref
const trashed = owner_ref ? ProjectHelper.isTrashed(project, owner_ref) : false
return {
id: project._id.toString(),
name: project.name,
owner: project.owner_ref,
lastUpdated: project.lastUpdated?.toISOString(),
lastUpdatedBy: project.lastUpdatedBy,
inactive: project.lastOpened < maxDate,
trashed,
deleted: false,
}
}
function _formatDeletedProjectInfo(deletedProject, maxDate) {
const project = deletedProject.project
const owner_ref = project.owner_ref
const trashed = owner_ref ? ProjectHelper.isTrashed(project, owner_ref) : false
return {
id: project._id.toString(),
name: project.name,
owner: owner_ref,
lastUpdated: project.lastUpdated?.toISOString(),
lastUpdatedBy: project.lastUpdatedBy,
inactive: project.lastOpened < maxDate,
trashed,
deleted: true,
deletedAt: deletedProject.deleterData?.deletedAt?.toISOString(),
deletedBy: deletedProject.deleterData?.deleterId,
}
}
function _matchesFilters(project, filters) {
if (filters.owned && (project.trashed || project.deleted)) {
return false
}
if (filters.trashed && (!project.trashed || project.deleted)) {
return false
}
if (filters.deleted && !project.deleted) {
return false
}
if (filters.inactive && (project.trashed || project.deleted || !project.inactive)) {
return false
}
if (
filters.search?.length &&
project.name.toLowerCase().indexOf(filters.search.toLowerCase()) === -1
) {
return false
}
return true
}
function _hasActiveFilter(filters) {
return Boolean(
filters.owned ||
filters.inactive ||
filters.trashed ||
filters.deleted ||
filters.search?.length
)
}
async function trashProjectForUser(req, res) {
const projectId = req.params.project_id
const { userId } = req.body
await ProjectDeleter.promises.trashProject(projectId, userId)
res.sendStatus(200)
}
async function untrashProjectForUser(req, res) {
const projectId = req.params.project_id
const { userId } = req.body
await ProjectDeleter.promises.untrashProject(projectId, userId)
res.sendStatus(200)
}
async function undeleteProject(req, res) {
const projectId = req.params.project_id
const { userId } = req.body
const undelededProject = await ProjectDeleter.promises.undeleteProject(projectId, { userId })
await ProjectDeleter.promises.untrashProject(projectId, userId)
return res.json({
name: undelededProject.name,
})
}
async function purgeDeletedProject(req, res) {
const projectId = req.params.project_id
await ProjectDeleter.promises.expireDeletedProject(projectId)
res.sendStatus(200)
}
export default {
manageProjectsPage: expressify(manageProjectsPage),
getProjectsJson: expressify(getProjectsJson),
undeleteProject: expressify(undeleteProject),
purgeDeletedProject: expressify(purgeDeletedProject),
trashProjectForUser: expressify(trashProjectForUser),
untrashProjectForUser: expressify(untrashProjectForUser),
}