Files
ShreejitPanchal 6601501eff Init commit
2026-05-19 19:56:02 +08:00

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 }