diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index a07c5c1dcc..e236ff44f8 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -731,7 +731,7 @@ const _ProjectController = { } const isAdminOrTemplateOwner = - hasAdminAccess(user) || Settings.templates?.nonAdminCanManage + hasAdminAccess(user) || Settings.templates?.nonAdminCanManage || Settings.templates?.user_id === userId const showTemplatesServerPro = Features.hasFeature('templates-server-pro') && isAdminOrTemplateOwner diff --git a/services/web/app/src/infrastructure/mongodb.mjs b/services/web/app/src/infrastructure/mongodb.mjs index 6ffc38639f..4b42ea54c6 100644 --- a/services/web/app/src/infrastructure/mongodb.mjs +++ b/services/web/app/src/infrastructure/mongodb.mjs @@ -79,6 +79,7 @@ export const db = { systemmessages: internalDb.collection('systemmessages'), tags: internalDb.collection('tags'), teamInvites: internalDb.collection('teamInvites'), + templates: internalDb.collection('templates'), tokens: internalDb.collection('tokens'), userAuditLogEntries: internalDb.collection('userAuditLogEntries'), users: internalDb.collection('users'), diff --git a/services/web/modules/template-gallery/app/src/TemplateAuthorizationMiddleware.mjs b/services/web/modules/template-gallery/app/src/TemplateAuthorizationMiddleware.mjs new file mode 100644 index 0000000000..b949e4198b --- /dev/null +++ b/services/web/modules/template-gallery/app/src/TemplateAuthorizationMiddleware.mjs @@ -0,0 +1,38 @@ +import Settings from '@overleaf/settings' +import AdminAuthorizationHelper from '../../../../app/src/Features/Helpers/AdminAuthorizationHelper.mjs' +const { hasAdminAccess } = AdminAuthorizationHelper +import HttpErrorHandler from '../../../../app/src/Features/Errors/HttpErrorHandler.mjs' +import SessionManager from '../../../../app/src/Features/Authentication/SessionManager.mjs' +import { Template } from './models/Template.mjs' + +async function ensureTemplateManagementAccess(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + const userId = SessionManager.getLoggedInUserId(req.session) + + const isPrivileged = + hasAdminAccess(user) || + Settings.templates?.user_id === userId + + if (isPrivileged) return next() + + const templateId = req.params?.template_id + + if (!templateId) { + if (Settings.templates?.nonAdminCanManage) return next() + return HttpErrorHandler.forbidden(req, res) + } + + // unprivileged owner is allowed to edit/delete own template + // even non-admin is not allowed to manage templates + const template = await Template.findById(templateId) + .select('owner') + .lean() + + if (template?.owner?.toString() === userId) return next() + + return HttpErrorHandler.forbidden(req, res) +} + +export default { + ensureTemplateManagementAccess, +} diff --git a/services/web/modules/template-gallery/app/src/TemplateGalleryController.mjs b/services/web/modules/template-gallery/app/src/TemplateGalleryController.mjs index a5e01b9c58..b8b294f063 100644 --- a/services/web/modules/template-gallery/app/src/TemplateGalleryController.mjs +++ b/services/web/modules/template-gallery/app/src/TemplateGalleryController.mjs @@ -123,12 +123,14 @@ async function templatesCategoryPage(req, res, next) { async function templateDetailsPage(req, res, next) { const t = req.i18n.translate + const userId = SessionManager.getLoggedInUserId(req.session) try { const template = await TemplateGalleryManager.getTemplate('_id', req.params.template_id) res.render(Path.resolve(__dirname, '../views/template_gallery/template'), { title: `${t('template')}: ${template.name}`, template: JSON.stringify(template), languages: Settings.languages, + userIsTemplatesManager: Boolean(Settings.templates?.user_id && Settings.templates.user_id === userId) }) } catch (error) { return ErrorController.notFound(req, res, next) diff --git a/services/web/modules/template-gallery/app/src/TemplateGalleryHelper.mjs b/services/web/modules/template-gallery/app/src/TemplateGalleryHelper.mjs index ad3391cb2e..884a844ec8 100644 --- a/services/web/modules/template-gallery/app/src/TemplateGalleryHelper.mjs +++ b/services/web/modules/template-gallery/app/src/TemplateGalleryHelper.mjs @@ -71,13 +71,13 @@ export async function canUserOverrideTemplate(template, userId) { userIsOwner = false try { userIsAdmin = (await UserGetter.promises.getUser(userId, { isAdmin: 1 })).isAdmin - } catch { + } catch (error) { logger.error({ error, userId }, 'Logged in user does not exist, strange...') userIsAdmin = false } templateOwnerName = await getUserName(templateOwnerId) || 'unknown' } - const canOverride = userIsOwner || userIsAdmin + const canOverride = userIsOwner || userIsAdmin || (settings.templates?.user_id === userId) return { canOverride, templateOwnerName } } @@ -193,7 +193,7 @@ export async function deleteTemplateAssets(templateId, version, deleteFromDb) { try { await Template.deleteOne({ _id: templateId }).exec() } catch (error) { - logger.error({ err, templateId }, 'Failed to delete template from MongoDB') + logger.error({ error, templateId }, 'Failed to delete template from MongoDB') throw error } } diff --git a/services/web/modules/template-gallery/app/src/TemplateGalleryRouter.mjs b/services/web/modules/template-gallery/app/src/TemplateGalleryRouter.mjs index de52e82ee1..9102067261 100644 --- a/services/web/modules/template-gallery/app/src/TemplateGalleryRouter.mjs +++ b/services/web/modules/template-gallery/app/src/TemplateGalleryRouter.mjs @@ -4,6 +4,7 @@ import AuthenticationController from '../../../../app/src/Features/Authenticatio import RateLimiterMiddleware from '../../../../app/src/Features/Security/RateLimiterMiddleware.mjs' import { RateLimiter } from '../../../../app/src/infrastructure/RateLimiter.mjs' import TemplateGalleryController from './TemplateGalleryController.mjs' +import TemplateAuthorizationMiddleware from './TemplateAuthorizationMiddleware.mjs' const rateLimiterNewTemplate = new RateLimiter('create-template-from-project', { points: 20, @@ -18,7 +19,6 @@ const rateLimiterThumbnails = new RateLimiter('template-gallery-thumbnails', { duration: 60, }) - export default { rateLimiter, apply(webRouter) { @@ -28,6 +28,7 @@ export default { '/template/new/:Project_id', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit(rateLimiterNewTemplate), + TemplateAuthorizationMiddleware.ensureTemplateManagementAccess, TemplateGalleryController.createTemplateFromProject ) @@ -41,6 +42,7 @@ export default { '/template/:template_id/edit', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit(rateLimiter), + TemplateAuthorizationMiddleware.ensureTemplateManagementAccess, TemplateGalleryController.editTemplate ) @@ -48,6 +50,7 @@ export default { '/template/:template_id/delete', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit(rateLimiter), + TemplateAuthorizationMiddleware.ensureTemplateManagementAccess, TemplateGalleryController.deleteTemplate ) diff --git a/services/web/modules/template-gallery/app/views/template_gallery/template.pug b/services/web/modules/template-gallery/app/views/template_gallery/template.pug index 81f3538492..5bf1441b26 100644 --- a/services/web/modules/template-gallery/app/views/template_gallery/template.pug +++ b/services/web/modules/template-gallery/app/views/template_gallery/template.pug @@ -12,6 +12,7 @@ block append meta meta(name="ol-template" data-type="json" content=template) meta(name="ol-languages" data-type="json" content=languages) meta(name="ol-userIsAdmin" data-type="boolean" content=hasAdminAccess()) + meta(name="ol-userIsTemplatesManager" data-type="boolean" content=userIsTemplatesManager) block content #template-root diff --git a/services/web/modules/template-gallery/frontend/js/features/template-gallery/components/pagination.tsx b/services/web/modules/template-gallery/frontend/js/features/template-gallery/components/pagination.tsx deleted file mode 100644 index a55f96a6a4..0000000000 --- a/services/web/modules/template-gallery/frontend/js/features/template-gallery/components/pagination.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useTranslation } from 'react-i18next' - -export default function Pagination({ currentPage, totalPages, onPageChange }) { - const { t } = useTranslation() - if (totalPages <= 1) return null - - const pageNumbers = [] - let startPage = Math.max(1, currentPage - 4) - let endPage = Math.min(totalPages, currentPage + 4) - - if (startPage > 1) { - pageNumbers.push(1) - if (startPage > 2) { - pageNumbers.push("...") - } - } - - for (let i = startPage; i <= endPage; i++) { - pageNumbers.push(i) - } - - if (endPage < totalPages) { - if (endPage < totalPages - 1) { - pageNumbers.push("...") - } - pageNumbers.push(totalPages) - } - - return ( - - ) -} diff --git a/services/web/modules/template-gallery/frontend/js/features/template-gallery/components/template-gallery.tsx b/services/web/modules/template-gallery/frontend/js/features/template-gallery/components/template-gallery.tsx index 789227b02d..faad456e64 100644 --- a/services/web/modules/template-gallery/frontend/js/features/template-gallery/components/template-gallery.tsx +++ b/services/web/modules/template-gallery/frontend/js/features/template-gallery/components/template-gallery.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import OLRow from '@/shared/components/ol/ol-row' import { useTemplateGalleryContext } from '../context/template-gallery-context' import TemplateGalleryEntry from './template-gallery-entry' -import Pagination from './pagination' +import Pagination from '@/shared/components/pagination-cep' export default function TemplateGallery() { const { t } = useTranslation() diff --git a/services/web/modules/template-gallery/frontend/js/features/template/components/template-details.tsx b/services/web/modules/template-gallery/frontend/js/features/template/components/template-details.tsx index ff2602137e..0e19a64e73 100644 --- a/services/web/modules/template-gallery/frontend/js/features/template/components/template-details.tsx +++ b/services/web/modules/template-gallery/frontend/js/features/template/components/template-details.tsx @@ -16,7 +16,8 @@ function TemplateDetails() { const lastUpdatedDate = fromNowDate(template.lastUpdated) const tooltipText = formatDate(template.lastUpdated) const loggedInUserId = getMeta('ol-user_id') - const loggedInUserIsAdmin = getMeta('ol-userIsAdmin') + const loggedInUserCanManageTemplates = getMeta('ol-userIsAdmin') + || getMeta('ol-userIsTemplatesManager') const openAsTemplateParams = new URLSearchParams({ version: template.version, @@ -90,7 +91,7 @@ function TemplateDetails() { )} - {loggedInUserId && (loggedInUserId === template.owner || loggedInUserIsAdmin) && ( + {loggedInUserId && (loggedInUserId === template.owner || loggedInUserCanManageTemplates) && ( diff --git a/services/web/modules/template-gallery/frontend/js/features/template/util/api.ts b/services/web/modules/template-gallery/frontend/js/features/template/util/api.ts index bdf4f57174..46e6abb2f4 100644 --- a/services/web/modules/template-gallery/frontend/js/features/template/util/api.ts +++ b/services/web/modules/template-gallery/frontend/js/features/template/util/api.ts @@ -11,17 +11,16 @@ export function deleteTemplate(template: Template) { type UpdateTemplateOptions = { template: Template - initialTemplate: Template - descriptionEdited: boolean + editedTemplate: Template } export function updateTemplate({ - editedTemplate, - template + template, + editedTemplate }: UpdateTemplateOptions): Promise