diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs
index f29e13b58e..9057a6a538 100644
--- a/services/web/app/src/Features/Project/ProjectController.mjs
+++ b/services/web/app/src/Features/Project/ProjectController.mjs
@@ -753,7 +753,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 d61f45f5f0..f2907ed4e5 100644
--- a/services/web/app/src/infrastructure/mongodb.mjs
+++ b/services/web/app/src/infrastructure/mongodb.mjs
@@ -83,6 +83,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 {
const updatedFields: Partial = {
name: editedTemplate.name.trim(),
- license: editedTemplate.license.trim(),
+ license: editedTemplate.license,
category: editedTemplate.category,
language: editedTemplate.language,
authorMD: editedTemplate.authorMD.trim(),
@@ -36,12 +35,10 @@ export function updateTemplate({
}, {} as Partial)
if (Object.keys(changedFields).length === 0) {
- return null
+ return Promise.resolve(null)
}
- const updated = postJSON(`/template/${editedTemplate.id}/edit`, {
+ return postJSON(`/template/${editedTemplate.id}/edit`, {
body: changedFields
})
-
- return updated
}
diff --git a/services/web/modules/template-gallery/index.mjs b/services/web/modules/template-gallery/index.mjs
index 16e39f46e7..563113a5c3 100644
--- a/services/web/modules/template-gallery/index.mjs
+++ b/services/web/modules/template-gallery/index.mjs
@@ -19,7 +19,8 @@ if (process.env.OVERLEAF_TEMPLATE_GALLERY === 'true') {
}
Settings.templates = {
- nonAdminCanManage: boolFromEnv(process.env.OVERLEAF_NON_ADMIN_CAN_PUBLISH_TEMPLATES)
+ nonAdminCanManage: boolFromEnv(process.env.OVERLEAF_NON_ADMIN_CAN_PUBLISH_TEMPLATES),
+ user_id: process.env.OVERLEAF_TEMPLATES_USER_ID
}
const templateKeys = process.env.OVERLEAF_TEMPLATE_CATEGORIES