538 lines
17 KiB
TypeScript
538 lines
17 KiB
TypeScript
import { HttpStatusCode } from '@peertube/peertube-models'
|
|
import express from 'express'
|
|
import { auditLoggerFactory, getAuditIdFromRes, EventAuditView } from '@server/helpers/audit-logger.js'
|
|
import { getFormattedObjects } from '@server/helpers/utils.js'
|
|
import { createHash } from 'crypto'
|
|
import {
|
|
apiRateLimiter,
|
|
asyncMiddleware,
|
|
authenticate,
|
|
paginationValidator,
|
|
setDefaultPagination
|
|
} from '@server/middlewares/index.js'
|
|
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
|
import {
|
|
createEventValidator,
|
|
updateEventValidator,
|
|
deleteEventValidator,
|
|
updateEventStateValidator,
|
|
getEventValidator
|
|
} from '@server/middlewares/validators/events.js'
|
|
import {
|
|
createEventRegistrationValidator,
|
|
deleteEventRegistrationValidator,
|
|
getEventRegistrationValidator,
|
|
listEventRegistrationsValidator,
|
|
updateEventRegistrationValidator
|
|
} from '@server/middlewares/validators/event-registrations.js'
|
|
import { EventRegistrationModel } from '@server/models/video/event-registration.js'
|
|
import { EventModel, EventState } from '@server/models/video/event.js'
|
|
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
|
import { sequelizeTypescript } from '@server/initializers/database.js'
|
|
import { createReqFiles } from '@server/helpers/express-utils.js'
|
|
import { MIMETYPES, THUMBNAILS_SIZE } from '@server/initializers/constants.js'
|
|
import { CONFIG } from '@server/initializers/config.js'
|
|
import { processImageFromWorker } from '@server/lib/worker/parent-process.js'
|
|
import { join } from 'path'
|
|
import { ensureDir, remove } from 'fs-extra'
|
|
import { readFile } from 'fs/promises'
|
|
import { generateImageFilename } from '@server/helpers/image-utils.js'
|
|
|
|
const auditLogger = auditLoggerFactory('events')
|
|
const eventsRouter = express.Router()
|
|
const SPECIAL_THUMBNAILS_TMP_DIR = join(CONFIG.STORAGE.TMP_DIR, 'special-thumbnails')
|
|
|
|
const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
|
|
|
eventsRouter.use(apiRateLimiter)
|
|
|
|
// List public events
|
|
eventsRouter.get(
|
|
'/',
|
|
openapiOperationDoc({ operationId: 'getEvents' }),
|
|
paginationValidator,
|
|
setDefaultPagination,
|
|
asyncMiddleware(listPublicEvents)
|
|
)
|
|
|
|
eventsRouter.get(
|
|
'/registrations',
|
|
openapiOperationDoc({ operationId: 'getAllEventRegistrations' }),
|
|
authenticate,
|
|
paginationValidator,
|
|
setDefaultPagination,
|
|
asyncMiddleware(listAllEventRegistrations)
|
|
)
|
|
|
|
// Get event by ID (public)
|
|
eventsRouter.get(
|
|
'/:id',
|
|
openapiOperationDoc({ operationId: 'getEvent' }),
|
|
getEventValidator,
|
|
asyncMiddleware(getEvent)
|
|
)
|
|
|
|
eventsRouter.get(
|
|
'/:eventId/registrations',
|
|
openapiOperationDoc({ operationId: 'getEventRegistrations' }),
|
|
authenticate,
|
|
paginationValidator,
|
|
setDefaultPagination,
|
|
listEventRegistrationsValidator,
|
|
asyncMiddleware(listEventRegistrations)
|
|
)
|
|
|
|
eventsRouter.get(
|
|
'/:eventId/registrations/:registrationId',
|
|
openapiOperationDoc({ operationId: 'getEventRegistration' }),
|
|
authenticate,
|
|
getEventRegistrationValidator,
|
|
asyncMiddleware(getEventRegistration)
|
|
)
|
|
|
|
eventsRouter.post(
|
|
'/:eventId/registrations',
|
|
openapiOperationDoc({ operationId: 'createEventRegistration' }),
|
|
createEventRegistrationValidator,
|
|
asyncMiddleware(createEventRegistration)
|
|
)
|
|
|
|
eventsRouter.put(
|
|
'/:eventId/registrations/:registrationId',
|
|
openapiOperationDoc({ operationId: 'updateEventRegistration' }),
|
|
authenticate,
|
|
updateEventRegistrationValidator,
|
|
asyncMiddleware(updateEventRegistration)
|
|
)
|
|
|
|
eventsRouter.delete(
|
|
'/:eventId/registrations/:registrationId',
|
|
openapiOperationDoc({ operationId: 'deleteEventRegistration' }),
|
|
authenticate,
|
|
deleteEventRegistrationValidator,
|
|
asyncMiddleware(deleteEventRegistration)
|
|
)
|
|
|
|
// Create event (admin/channel owner only)
|
|
eventsRouter.post(
|
|
'/:channelId/events',
|
|
openapiOperationDoc({ operationId: 'createEvent' }),
|
|
authenticate,
|
|
reqThumbnailFile,
|
|
createEventValidator,
|
|
asyncMiddleware(createEvent)
|
|
)
|
|
|
|
// Update event (admin/channel owner only)
|
|
eventsRouter.put(
|
|
'/:channelId/events/:eventId',
|
|
openapiOperationDoc({ operationId: 'updateEvent' }),
|
|
authenticate,
|
|
reqThumbnailFile,
|
|
updateEventValidator,
|
|
asyncMiddleware(updateEvent)
|
|
)
|
|
|
|
// Delete event (admin/channel owner only)
|
|
eventsRouter.delete(
|
|
'/:channelId/events/:eventId',
|
|
openapiOperationDoc({ operationId: 'deleteEvent' }),
|
|
authenticate,
|
|
deleteEventValidator,
|
|
asyncMiddleware(deleteEvent)
|
|
)
|
|
|
|
// Change event state (admin/channel owner only)
|
|
eventsRouter.patch(
|
|
'/:channelId/events/:eventId/state',
|
|
openapiOperationDoc({ operationId: 'updateEventState' }),
|
|
authenticate,
|
|
updateEventStateValidator,
|
|
asyncMiddleware(updateEventState)
|
|
)
|
|
|
|
// List channel events (admin/channel owner only)
|
|
eventsRouter.get(
|
|
'/:channelId/events',
|
|
openapiOperationDoc({ operationId: 'getChannelEvents' }),
|
|
authenticate,
|
|
asyncMiddleware(listChannelEvents)
|
|
)
|
|
|
|
async function listPublicEvents (req: express.Request, res: express.Response) {
|
|
const state = req.query.state as string | undefined
|
|
const states = state ? state.split(',').filter(s => s) : undefined
|
|
const events = await EventModel.listPublicEvents(undefined, states)
|
|
const count = events.length
|
|
|
|
return res.json(getFormattedObjects(events, count))
|
|
}
|
|
|
|
async function getEvent (req: express.Request, res: express.Response) {
|
|
const event = (res.locals as any).event
|
|
|
|
return res.json(event.toFormattedJSON())
|
|
}
|
|
|
|
async function listEventRegistrations (req: express.Request, res: express.Response) {
|
|
const event = (res.locals as any).event
|
|
|
|
if (!isEventOwnerOrAdmin(res, event)) {
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to manage this event'
|
|
})
|
|
}
|
|
|
|
const { count, rows } = await EventRegistrationModel.listForApi({
|
|
eventId: event.id,
|
|
start: req.query.start as number,
|
|
count: req.query.count as number
|
|
})
|
|
|
|
return res.json(getFormattedObjects(rows, count))
|
|
}
|
|
|
|
async function listAllEventRegistrations (req: express.Request, res: express.Response) {
|
|
const user = res.locals.oauth.token.User
|
|
|
|
// Only admin allowed (or extend if needed)
|
|
if (user.role !== 0) {
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'Only admin can view all event registrations'
|
|
})
|
|
}
|
|
|
|
const { count, rows } = await EventRegistrationModel.findAndCountAll({
|
|
include: [
|
|
{
|
|
model: EventModel,
|
|
attributes: ['id', 'title', 'startTime', 'endTime', 'thumbnailFilename']
|
|
}
|
|
],
|
|
offset: req.query.start as number,
|
|
limit: req.query.count as number,
|
|
order: [['createdAt', 'DESC']]
|
|
})
|
|
|
|
const data = rows.map((row: any) => {
|
|
const json = row.toJSON()
|
|
|
|
return {
|
|
...json,
|
|
event: json.Event
|
|
? {
|
|
id: json.Event.id,
|
|
title: json.Event.title,
|
|
startTime: json.Event.startTime,
|
|
endTime: json.Event.endTime,
|
|
thumbnailFilename: json.Event.thumbnailFilename
|
|
}
|
|
: null
|
|
}
|
|
})
|
|
|
|
return res.json({
|
|
total: count,
|
|
data
|
|
})
|
|
}
|
|
|
|
async function getEventRegistration (req: express.Request, res: express.Response) {
|
|
const event = (res.locals as any).event
|
|
const registration = (res.locals as any).eventRegistration
|
|
|
|
if (!isEventOwnerOrAdmin(res, event)) {
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to manage this event'
|
|
})
|
|
}
|
|
|
|
return res.json(registration.toFormattedJSON())
|
|
}
|
|
|
|
async function listChannelEvents (req: express.Request, res: express.Response) {
|
|
const { channelId } = req.params
|
|
const channel = await VideoChannelModel.scope([ 'WITH_ACCOUNT' ]).findByPk(channelId)
|
|
|
|
if (!channel) {
|
|
return res.fail({
|
|
status: HttpStatusCode.NOT_FOUND_404,
|
|
message: 'Channel not found'
|
|
})
|
|
}
|
|
|
|
// Check if user is channel owner or admin
|
|
const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
|
|
if (!isChannelOwner && res.locals.oauth.token.User.role !== 0) { // 0 = admin
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to manage this channel'
|
|
})
|
|
}
|
|
|
|
const events = await EventModel.listByChannelId(parseInt(channelId))
|
|
const count = events.length
|
|
|
|
return res.json(getFormattedObjects(events, count))
|
|
}
|
|
|
|
async function createEvent (req: express.Request, res: express.Response) {
|
|
const { channelId } = req.params
|
|
const { title, description, startTime, endTime, state, liveUrl } = req.body
|
|
const channel = (res.locals as any).channel
|
|
const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined
|
|
const thumbnailFile = files?.thumbnailfile?.[0]
|
|
|
|
// Check if user is channel owner or admin
|
|
const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
|
|
if (!isChannelOwner && (res.locals as any).oauth.token.User.role !== 0) { // 0 = admin
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to manage this channel'
|
|
})
|
|
}
|
|
|
|
const event = await sequelizeTypescript.transaction(async t => {
|
|
let thumbnail: { filename: string, data: Buffer, size: number, mimeType: string, etag: string } | null = null
|
|
|
|
if (thumbnailFile) {
|
|
thumbnail = await saveEventThumbnailToTemporaryFile(thumbnailFile.path)
|
|
}
|
|
|
|
const created = await EventModel.create(
|
|
{
|
|
title,
|
|
description: description || null,
|
|
startTime: new Date(startTime),
|
|
endTime: new Date(endTime),
|
|
state: state || EventState.ACTIVE,
|
|
liveUrl: liveUrl || null,
|
|
channelId: parseInt(channelId),
|
|
thumbnailFilename: thumbnail?.filename || null,
|
|
thumbnailData: thumbnail?.data || null,
|
|
thumbnailMimeType: thumbnail?.mimeType || null,
|
|
thumbnailSize: thumbnail?.size || null,
|
|
thumbnailEtag: thumbnail?.etag || null
|
|
},
|
|
{ transaction: t }
|
|
)
|
|
|
|
return created
|
|
})
|
|
|
|
auditLogger.create(getAuditIdFromRes(res), new EventAuditView(event))
|
|
|
|
return res.status(HttpStatusCode.CREATED_201).json(event.toFormattedJSON())
|
|
}
|
|
|
|
async function updateEvent (req: express.Request, res: express.Response) {
|
|
const { title, description, startTime, endTime, state, liveUrl } = req.body
|
|
const event = (res.locals as any).event
|
|
const channel = (res.locals as any).channel
|
|
const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined
|
|
const thumbnailFile = files?.thumbnailfile?.[0]
|
|
const oldEventAuditView = new EventAuditView(event.toFormattedJSON())
|
|
|
|
// Check if user is channel owner or admin
|
|
const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
|
|
if (!isChannelOwner && (res.locals as any).oauth.token.User.role !== 0) { // 0 = admin
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to manage this channel'
|
|
})
|
|
}
|
|
|
|
const updatedEvent = await sequelizeTypescript.transaction(async t => {
|
|
let thumbnailFilename = event.thumbnailFilename
|
|
|
|
if (thumbnailFile) {
|
|
const thumbnail = await saveEventThumbnailToTemporaryFile(thumbnailFile.path)
|
|
thumbnailFilename = thumbnail.filename
|
|
|
|
Object.assign(event, {
|
|
thumbnailData: thumbnail.data,
|
|
thumbnailMimeType: thumbnail.mimeType,
|
|
thumbnailSize: thumbnail.size,
|
|
thumbnailEtag: thumbnail.etag
|
|
})
|
|
}
|
|
|
|
await event.update(
|
|
{
|
|
title: title || event.title,
|
|
description: description !== undefined ? description : event.description,
|
|
startTime: startTime ? new Date(startTime) : event.startTime,
|
|
endTime: endTime ? new Date(endTime) : event.endTime,
|
|
state: state || event.state,
|
|
liveUrl: liveUrl !== undefined ? liveUrl : event.liveUrl,
|
|
thumbnailFilename,
|
|
thumbnailData: event.thumbnailData,
|
|
thumbnailMimeType: event.thumbnailMimeType,
|
|
thumbnailSize: event.thumbnailSize,
|
|
thumbnailEtag: event.thumbnailEtag
|
|
},
|
|
{ transaction: t }
|
|
)
|
|
|
|
return event
|
|
})
|
|
|
|
auditLogger.update(getAuditIdFromRes(res), new EventAuditView(updatedEvent.toFormattedJSON()), oldEventAuditView)
|
|
|
|
return res.json(updatedEvent.toFormattedJSON())
|
|
}
|
|
|
|
async function deleteEvent (req: express.Request, res: express.Response) {
|
|
const event = (res.locals as any).event
|
|
const channel = (res.locals as any).channel
|
|
|
|
// Check if user is channel owner or admin
|
|
const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
|
|
if (!isChannelOwner && (res.locals as any).oauth.token.User.role !== 0) { // 0 = admin
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to manage this channel'
|
|
})
|
|
}
|
|
|
|
await sequelizeTypescript.transaction(async t => {
|
|
await event.destroy({ transaction: t })
|
|
})
|
|
|
|
auditLogger.delete(getAuditIdFromRes(res), new EventAuditView(event.toFormattedJSON()))
|
|
|
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
|
}
|
|
|
|
async function saveEventThumbnailToTemporaryFile (sourcePath: string) {
|
|
const thumbnailFilename = generateImageFilename('.jpg')
|
|
const destination = join(SPECIAL_THUMBNAILS_TMP_DIR, thumbnailFilename)
|
|
|
|
await ensureDir(SPECIAL_THUMBNAILS_TMP_DIR)
|
|
|
|
await processImageFromWorker({
|
|
path: sourcePath,
|
|
destination,
|
|
newSize: THUMBNAILS_SIZE,
|
|
keepOriginal: true
|
|
})
|
|
|
|
const data = await readFile(destination)
|
|
await remove(destination).catch(() => undefined)
|
|
|
|
return {
|
|
filename: thumbnailFilename,
|
|
data,
|
|
size: data.byteLength,
|
|
mimeType: 'image/jpeg',
|
|
etag: createHash('sha256').update(data).digest('hex')
|
|
}
|
|
}
|
|
|
|
async function updateEventState (req: express.Request, res: express.Response) {
|
|
const { state } = req.body
|
|
const event = (res.locals as any).event
|
|
const channel = (res.locals as any).channel
|
|
const oldEventAuditView = new EventAuditView(event.toFormattedJSON())
|
|
|
|
// Check if user is channel owner or admin
|
|
const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
|
|
if (!isChannelOwner && (res.locals as any).oauth.token.User.role !== 0) { // 0 = admin
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to manage this channel'
|
|
})
|
|
}
|
|
|
|
await sequelizeTypescript.transaction(async t => {
|
|
await event.update({ state }, { transaction: t })
|
|
})
|
|
|
|
auditLogger.update(getAuditIdFromRes(res), new EventAuditView(event.toFormattedJSON()), oldEventAuditView)
|
|
|
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
|
}
|
|
|
|
async function createEventRegistration (req: express.Request, res: express.Response) {
|
|
const event = (res.locals as any).event
|
|
|
|
const registration = await sequelizeTypescript.transaction(async t => {
|
|
return EventRegistrationModel.create({
|
|
firstName: req.body.firstName,
|
|
lastName: req.body.lastName,
|
|
email: req.body.email,
|
|
phone: req.body.phone,
|
|
companyName: req.body.companyName,
|
|
jobTitle: req.body.jobTitle,
|
|
country: req.body.country,
|
|
eventId: event.id
|
|
}, {
|
|
transaction: t
|
|
})
|
|
})
|
|
|
|
return res.status(HttpStatusCode.CREATED_201).json(registration.toFormattedJSON())
|
|
}
|
|
|
|
async function updateEventRegistration (req: express.Request, res: express.Response) {
|
|
const event = (res.locals as any).event
|
|
const registration = (res.locals as any).eventRegistration as EventRegistrationModel
|
|
|
|
if (!canManageRegistration(res, event, registration)) {
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to update this registration'
|
|
})
|
|
}
|
|
|
|
await sequelizeTypescript.transaction(async t => {
|
|
await registration.update({
|
|
firstName: req.body.firstName ?? registration.firstName,
|
|
lastName: req.body.lastName ?? registration.lastName,
|
|
email: req.body.email ?? registration.email,
|
|
phone: req.body.phone ?? registration.phone,
|
|
companyName: req.body.companyName ?? registration.companyName,
|
|
jobTitle: req.body.jobTitle ?? registration.jobTitle,
|
|
country: req.body.country ?? registration.country
|
|
}, {
|
|
transaction: t
|
|
})
|
|
})
|
|
|
|
return res.json(registration.toFormattedJSON())
|
|
}
|
|
|
|
async function deleteEventRegistration (req: express.Request, res: express.Response) {
|
|
const event = (res.locals as any).event
|
|
const registration = (res.locals as any).eventRegistration as EventRegistrationModel
|
|
|
|
if (!canManageRegistration(res, event, registration)) {
|
|
return res.fail({
|
|
status: HttpStatusCode.FORBIDDEN_403,
|
|
message: 'User is not allowed to delete this registration'
|
|
})
|
|
}
|
|
|
|
await sequelizeTypescript.transaction(async t => {
|
|
await registration.destroy({ transaction: t })
|
|
})
|
|
|
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
|
}
|
|
|
|
function isEventOwnerOrAdmin (res: express.Response, event: EventModel) {
|
|
const user = res.locals.oauth.token.User
|
|
const isEventOwner = event.Channel?.Account?.userId === user.id
|
|
|
|
return isEventOwner || user.role === 0
|
|
}
|
|
|
|
function canManageRegistration (res: express.Response, event: EventModel, registration: EventRegistrationModel) {
|
|
return isEventOwnerOrAdmin(res, event)
|
|
}
|
|
|
|
export { eventsRouter }
|