import { HttpStatusCode } from '@peertube/peertube-models' import { isIdValid } from '@server/helpers/custom-validators/misc.js' import { EventModel } from '@server/models/video/event.js' import { VideoChannelModel } from '@server/models/video/video-channel.js' import express from 'express' import { body, param } from 'express-validator' import { areValidationErrors } from './shared/utils.js' export const createEventValidator = [ body('title') .trim() .isLength({ min: 1, max: 255 }) .withMessage('Event title must be between 1 and 255 characters'), body('description') .optional() .trim() .isLength({ max: 5000 }) .withMessage('Event description must not exceed 5000 characters'), body('startTime') .isISO8601() .withMessage('Start time must be a valid ISO 8601 date'), body('endTime') .isISO8601() .withMessage('End time must be a valid ISO 8601 date'), body('state') .optional() .isIn([ 'active', 'disabled' ]) .withMessage('Invalid event state'), body('liveUrl') .optional({ checkFalsy: true}) .isLength({ max: 2048 }) .withMessage('Live URL must not exceed 2048 characters') .bail() .isURL() .withMessage('Live URL must be a valid URL'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return // Validate end time is after start time const startTime = new Date(req.body.startTime) const endTime = new Date(req.body.endTime) if (endTime <= startTime) { return res.fail({ status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, message: 'End time must be after start time' }) } // Validate channel exists 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' }) } const resLocals = res.locals as any resLocals.channel = channel return next() } ] export const updateEventValidator = [ param('eventId') .custom(isIdValid) .withMessage('Invalid event id'), body('title') .optional() .trim() .isLength({ min: 1, max: 255 }) .withMessage('Event title must be between 1 and 255 characters'), body('description') .optional() .trim() .isLength({ max: 5000 }) .withMessage('Event description must not exceed 5000 characters'), body('startTime') .optional() .isISO8601() .withMessage('Start time must be a valid ISO 8601 date'), body('endTime') .optional() .isISO8601() .withMessage('End time must be a valid ISO 8601 date'), body('state') .optional() .isIn([ 'active', 'disabled' ]) .withMessage('Invalid event state'), body('liveUrl') .optional({ checkFalsy: true}) .isLength({ max: 2048 }) .withMessage('Live URL must not exceed 2048 characters') .bail() .isURL() .withMessage('Live URL must be a valid URL'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return // Validate time range if both provided if (req.body.startTime && req.body.endTime) { const startTime = new Date(req.body.startTime) const endTime = new Date(req.body.endTime) if (endTime <= startTime) { return res.fail({ status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, message: 'End time must be after start time' }) } } // Validate channel exists const { channelId, eventId } = 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' }) } // Validate event exists and belongs to channel const event = await EventModel.findByIdAndChannelId(parseInt(eventId), parseInt(channelId)) if (!event) { return res.fail({ status: HttpStatusCode.NOT_FOUND_404, message: 'Event not found' }) } const resLocals = res.locals as any resLocals.channel = channel resLocals.event = event return next() } ] export const deleteEventValidator = [ param('eventId') .custom(isIdValid) .withMessage('Invalid event id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return const { channelId, eventId } = 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' }) } const event = await EventModel.findByIdAndChannelId(parseInt(eventId), parseInt(channelId)) if (!event) { return res.fail({ status: HttpStatusCode.NOT_FOUND_404, message: 'Event not found' }) } const resLocals = res.locals as any resLocals.channel = channel resLocals.event = event return next() } ] export const updateEventStateValidator = [ param('eventId') .custom(isIdValid) .withMessage('Invalid event id'), body('state') .isIn([ 'active', 'disabled' ]) .withMessage('Invalid event state'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return const { channelId, eventId } = 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' }) } const event = await EventModel.findByIdAndChannelId(parseInt(eventId), parseInt(channelId)) if (!event) { return res.fail({ status: HttpStatusCode.NOT_FOUND_404, message: 'Event not found' }) } const resLocals = res.locals as any resLocals.channel = channel resLocals.event = event return next() } ] export const getEventValidator = [ param('id') .custom(isIdValid) .withMessage('Invalid event id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return const event = await EventModel.findByPk(req.params.id) if (!event) { return res.fail({ status: HttpStatusCode.NOT_FOUND_404, message: 'Event not found' }) } (res.locals as any).event = event return next() } ]