Init commit
This commit is contained in:
537
server/core/controllers/api/events.ts
Normal file
537
server/core/controllers/api/events.ts
Normal file
@@ -0,0 +1,537 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user