mirror of https://github.com/Chocobozzz/PeerTube
Implement contact form on server side
parent
8d00889b60
commit
a4101923e6
|
@ -102,7 +102,12 @@ cache:
|
|||
size: 500 # Max number of video captions/subtitles you want to cache
|
||||
|
||||
admin:
|
||||
email: 'admin@example.com' # Your personal email as administrator
|
||||
# Used to generate the root user at first startup
|
||||
# And to receive emails from the contact form
|
||||
email: 'admin@example.com'
|
||||
|
||||
contact_form:
|
||||
enabled: true
|
||||
|
||||
signup:
|
||||
enabled: false
|
||||
|
|
|
@ -115,8 +115,13 @@ cache:
|
|||
size: 500 # Max number of video captions/subtitles you want to cache
|
||||
|
||||
admin:
|
||||
# Used to generate the root user at first startup
|
||||
# And to receive emails from the contact form
|
||||
email: 'admin@example.com'
|
||||
|
||||
contact_form:
|
||||
enabled: true
|
||||
|
||||
signup:
|
||||
enabled: false
|
||||
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
||||
|
|
|
@ -21,6 +21,9 @@ smtp:
|
|||
log:
|
||||
level: 'debug'
|
||||
|
||||
contact_form:
|
||||
enabled: true
|
||||
|
||||
redundancy:
|
||||
videos:
|
||||
check_interval: '10 minutes'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as express from 'express'
|
||||
import { omit } from 'lodash'
|
||||
import { omit, snakeCase } from 'lodash'
|
||||
import { ServerConfig, UserRight } from '../../../shared'
|
||||
import { About } from '../../../shared/models/server/about.model'
|
||||
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
|
||||
|
@ -12,6 +12,8 @@ import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '..
|
|||
import { remove, writeJSON } from 'fs-extra'
|
||||
import { getServerCommit } from '../../helpers/utils'
|
||||
import { Emailer } from '../../lib/emailer'
|
||||
import { isNumeric } from 'validator'
|
||||
import { objectConverter } from '../../helpers/core-utils'
|
||||
|
||||
const packageJSON = require('../../../../package.json')
|
||||
const configRouter = express.Router()
|
||||
|
@ -65,6 +67,9 @@ async function getConfig (req: express.Request, res: express.Response) {
|
|||
email: {
|
||||
enabled: Emailer.Instance.isEnabled()
|
||||
},
|
||||
contactForm: {
|
||||
enabled: CONFIG.CONTACT_FORM.ENABLED
|
||||
},
|
||||
serverVersion: packageJSON.version,
|
||||
serverCommit,
|
||||
signup: {
|
||||
|
@ -154,34 +159,10 @@ async function deleteCustomConfig (req: express.Request, res: express.Response,
|
|||
}
|
||||
|
||||
async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const toUpdate: CustomConfig = req.body
|
||||
const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
|
||||
|
||||
// Force number conversion
|
||||
toUpdate.cache.previews.size = parseInt('' + toUpdate.cache.previews.size, 10)
|
||||
toUpdate.cache.captions.size = parseInt('' + toUpdate.cache.captions.size, 10)
|
||||
toUpdate.signup.limit = parseInt('' + toUpdate.signup.limit, 10)
|
||||
toUpdate.user.videoQuota = parseInt('' + toUpdate.user.videoQuota, 10)
|
||||
toUpdate.user.videoQuotaDaily = parseInt('' + toUpdate.user.videoQuotaDaily, 10)
|
||||
toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10)
|
||||
|
||||
// camelCase to snake_case key
|
||||
const toUpdateJSON = omit(
|
||||
toUpdate,
|
||||
'user.videoQuota',
|
||||
'instance.defaultClientRoute',
|
||||
'instance.shortDescription',
|
||||
'cache.videoCaptions',
|
||||
'signup.requiresEmailVerification',
|
||||
'transcoding.allowAdditionalExtensions'
|
||||
)
|
||||
toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
|
||||
toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily
|
||||
toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
|
||||
toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
|
||||
toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
|
||||
toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification
|
||||
toUpdateJSON.transcoding['allow_additional_extensions'] = toUpdate.transcoding.allowAdditionalExtensions
|
||||
// camelCase to snake_case key + Force number conversion
|
||||
const toUpdateJSON = convertCustomConfigBody(req.body)
|
||||
|
||||
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
|
||||
|
||||
|
@ -243,6 +224,9 @@ function customConfig (): CustomConfig {
|
|||
admin: {
|
||||
email: CONFIG.ADMIN.EMAIL
|
||||
},
|
||||
contactForm: {
|
||||
enabled: CONFIG.CONTACT_FORM.ENABLED
|
||||
},
|
||||
user: {
|
||||
videoQuota: CONFIG.USER.VIDEO_QUOTA,
|
||||
videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
|
||||
|
@ -271,3 +255,20 @@ function customConfig (): CustomConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertCustomConfigBody (body: CustomConfig) {
|
||||
function keyConverter (k: string) {
|
||||
// Transcoding resolutions exception
|
||||
if (/^\d{3,4}p$/.exec(k)) return k
|
||||
|
||||
return snakeCase(k)
|
||||
}
|
||||
|
||||
function valueConverter (v: any) {
|
||||
if (isNumeric(v + '')) return parseInt('' + v, 10)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
return objectConverter(body, keyConverter, valueConverter)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import * as express from 'express'
|
||||
import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares'
|
||||
import { Redis } from '../../../lib/redis'
|
||||
import { Emailer } from '../../../lib/emailer'
|
||||
import { ContactForm } from '../../../../shared/models/server'
|
||||
|
||||
const contactRouter = express.Router()
|
||||
|
||||
contactRouter.post('/contact',
|
||||
asyncMiddleware(contactAdministratorValidator),
|
||||
asyncMiddleware(contactAdministrator)
|
||||
)
|
||||
|
||||
async function contactAdministrator (req: express.Request, res: express.Response) {
|
||||
const data = req.body as ContactForm
|
||||
|
||||
await Emailer.Instance.addContactFormJob(data.fromEmail, data.fromName, data.body)
|
||||
|
||||
await Redis.Instance.setContactFormIp(req.ip)
|
||||
|
||||
return res.status(204).end()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
contactRouter
|
||||
}
|
|
@ -3,6 +3,7 @@ import { serverFollowsRouter } from './follows'
|
|||
import { statsRouter } from './stats'
|
||||
import { serverRedundancyRouter } from './redundancy'
|
||||
import { serverBlocklistRouter } from './server-blocklist'
|
||||
import { contactRouter } from './contact'
|
||||
|
||||
const serverRouter = express.Router()
|
||||
|
||||
|
@ -10,6 +11,7 @@ serverRouter.use('/', serverFollowsRouter)
|
|||
serverRouter.use('/', serverRedundancyRouter)
|
||||
serverRouter.use('/', statsRouter)
|
||||
serverRouter.use('/', serverBlocklistRouter)
|
||||
serverRouter.use('/', contactRouter)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -11,6 +11,25 @@ import * as pem from 'pem'
|
|||
import { URL } from 'url'
|
||||
import { truncate } from 'lodash'
|
||||
import { exec } from 'child_process'
|
||||
import { isArray } from './custom-validators/misc'
|
||||
|
||||
const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
|
||||
if (!oldObject || typeof oldObject !== 'object') {
|
||||
return valueConverter(oldObject)
|
||||
}
|
||||
|
||||
if (isArray(oldObject)) {
|
||||
return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
|
||||
}
|
||||
|
||||
const newObject = {}
|
||||
Object.keys(oldObject).forEach(oldKey => {
|
||||
const newKey = keyConverter(oldKey)
|
||||
newObject[ newKey ] = objectConverter(oldObject[ oldKey ], keyConverter, valueConverter)
|
||||
})
|
||||
|
||||
return newObject
|
||||
}
|
||||
|
||||
const timeTable = {
|
||||
ms: 1,
|
||||
|
@ -235,6 +254,7 @@ export {
|
|||
isTestInstance,
|
||||
isProdInstance,
|
||||
|
||||
objectConverter,
|
||||
root,
|
||||
escapeHTML,
|
||||
pageToStartAndCount,
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'express-validator'
|
|||
|
||||
import { isArray, exists } from './misc'
|
||||
import { isTestInstance } from '../core-utils'
|
||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||
|
||||
function isHostValid (host: string) {
|
||||
const isURLOptions = {
|
||||
|
@ -26,9 +27,19 @@ function isEachUniqueHostValid (hosts: string[]) {
|
|||
})
|
||||
}
|
||||
|
||||
function isValidContactBody (value: any) {
|
||||
return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.BODY)
|
||||
}
|
||||
|
||||
function isValidContactFromName (value: any) {
|
||||
return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.FROM_NAME)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isValidContactBody,
|
||||
isValidContactFromName,
|
||||
isEachUniqueHostValid,
|
||||
isHostValid
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { join } from 'path'
|
|||
import { Instance as ParseTorrent } from 'parse-torrent'
|
||||
import { remove } from 'fs-extra'
|
||||
import * as memoizee from 'memoizee'
|
||||
import { isArray } from './custom-validators/misc'
|
||||
|
||||
function deleteFileAsync (path: string) {
|
||||
remove(path)
|
||||
|
@ -19,10 +20,7 @@ async function generateRandomString (size: number) {
|
|||
return raw.toString('hex')
|
||||
}
|
||||
|
||||
interface FormattableToJSON {
|
||||
toFormattedJSON (args?: any)
|
||||
}
|
||||
|
||||
interface FormattableToJSON { toFormattedJSON (args?: any) }
|
||||
function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
|
||||
const formattedObjects: U[] = []
|
||||
|
||||
|
|
|
@ -231,6 +231,9 @@ const CONFIG = {
|
|||
ADMIN: {
|
||||
get EMAIL () { return config.get<string>('admin.email') }
|
||||
},
|
||||
CONTACT_FORM: {
|
||||
get ENABLED () { return config.get<boolean>('contact_form.enabled') }
|
||||
},
|
||||
SIGNUP: {
|
||||
get ENABLED () { return config.get<boolean>('signup.enabled') },
|
||||
get LIMIT () { return config.get<number>('signup.limit') },
|
||||
|
@ -394,6 +397,10 @@ let CONSTRAINTS_FIELDS = {
|
|||
},
|
||||
VIDEO_SHARE: {
|
||||
URL: { min: 3, max: 2000 } // Length
|
||||
},
|
||||
CONTACT_FORM: {
|
||||
FROM_NAME: { min: 1, max: 120 }, // Length
|
||||
BODY: { min: 3, max: 5000 } // Length
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -409,6 +416,8 @@ const RATES_LIMIT = {
|
|||
}
|
||||
|
||||
let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
|
||||
let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
|
||||
|
||||
const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
|
||||
MIN: 10,
|
||||
AVERAGE: 30,
|
||||
|
@ -685,6 +694,7 @@ if (isTestInstance() === true) {
|
|||
REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
|
||||
|
||||
VIDEO_VIEW_LIFETIME = 1000 // 1 second
|
||||
CONTACT_FORM_LIFETIME = 1000 // 1 second
|
||||
|
||||
JOB_ATTEMPTS['email'] = 1
|
||||
|
||||
|
@ -756,6 +766,7 @@ export {
|
|||
HTTP_SIGNATURE,
|
||||
VIDEO_IMPORT_STATES,
|
||||
VIDEO_VIEW_LIFETIME,
|
||||
CONTACT_FORM_LIFETIME,
|
||||
buildLanguages
|
||||
}
|
||||
|
||||
|
|
|
@ -354,13 +354,32 @@ class Emailer {
|
|||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
sendMail (to: string[], subject: string, text: string) {
|
||||
addContactFormJob (fromEmail: string, fromName: string, body: string) {
|
||||
const text = 'Hello dear admin,\n\n' +
|
||||
fromName + ' sent you a message' +
|
||||
'\n\n---------------------------------------\n\n' +
|
||||
body +
|
||||
'\n\n---------------------------------------\n\n' +
|
||||
'Cheers,\n' +
|
||||
'PeerTube.'
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
from: fromEmail,
|
||||
to: [ CONFIG.ADMIN.EMAIL ],
|
||||
subject: '[PeerTube] Contact form submitted',
|
||||
text
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
sendMail (to: string[], subject: string, text: string, from?: string) {
|
||||
if (!this.enabled) {
|
||||
throw new Error('Cannot send mail because SMTP is not configured.')
|
||||
}
|
||||
|
||||
return this.transporter.sendMail({
|
||||
from: CONFIG.SMTP.FROM_ADDRESS,
|
||||
from: from || CONFIG.SMTP.FROM_ADDRESS,
|
||||
to: to.join(','),
|
||||
subject,
|
||||
text
|
||||
|
|
|
@ -6,13 +6,14 @@ export type EmailPayload = {
|
|||
to: string[]
|
||||
subject: string
|
||||
text: string
|
||||
from?: string
|
||||
}
|
||||
|
||||
async function processEmail (job: Bull.Job) {
|
||||
const payload = job.data as EmailPayload
|
||||
logger.info('Processing email in job %d.', job.id)
|
||||
|
||||
return Emailer.Instance.sendMail(payload.to, payload.subject, payload.text)
|
||||
return Emailer.Instance.sendMail(payload.to, payload.subject, payload.text, payload.from)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -2,7 +2,13 @@ import * as express from 'express'
|
|||
import { createClient, RedisClient } from 'redis'
|
||||
import { logger } from '../helpers/logger'
|
||||
import { generateRandomString } from '../helpers/utils'
|
||||
import { CONFIG, USER_PASSWORD_RESET_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers'
|
||||
import {
|
||||
CONFIG,
|
||||
CONTACT_FORM_LIFETIME,
|
||||
USER_EMAIL_VERIFY_LIFETIME,
|
||||
USER_PASSWORD_RESET_LIFETIME,
|
||||
VIDEO_VIEW_LIFETIME
|
||||
} from '../initializers'
|
||||
|
||||
type CachedRoute = {
|
||||
body: string,
|
||||
|
@ -76,6 +82,16 @@ class Redis {
|
|||
return this.getValue(this.generateVerifyEmailKey(userId))
|
||||
}
|
||||
|
||||
/************* Contact form per IP *************/
|
||||
|
||||
async setContactFormIp (ip: string) {
|
||||
return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
|
||||
}
|
||||
|
||||
async isContactFormIpExists (ip: string) {
|
||||
return this.exists(this.generateContactFormKey(ip))
|
||||
}
|
||||
|
||||
/************* Views per IP *************/
|
||||
|
||||
setIPVideoView (ip: string, videoUUID: string) {
|
||||
|
@ -175,7 +191,11 @@ class Redis {
|
|||
}
|
||||
|
||||
private generateViewKey (ip: string, videoUUID: string) {
|
||||
return videoUUID + '-' + ip
|
||||
return `views-${videoUUID}-${ip}`
|
||||
}
|
||||
|
||||
private generateContactFormKey (ip: string) {
|
||||
return 'contact-form-' + ip
|
||||
}
|
||||
|
||||
/************* Redis helpers *************/
|
||||
|
|
|
@ -1,29 +1,44 @@
|
|||
import * as express from 'express'
|
||||
import { body } from 'express-validator/check'
|
||||
import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
|
||||
import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } from '../../helpers/custom-validators/users'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { areValidationErrors } from './utils'
|
||||
|
||||
const customConfigUpdateValidator = [
|
||||
body('instance.name').exists().withMessage('Should have a valid instance name'),
|
||||
body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'),
|
||||
body('instance.description').exists().withMessage('Should have a valid instance description'),
|
||||
body('instance.terms').exists().withMessage('Should have a valid instance terms'),
|
||||
body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
|
||||
body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
|
||||
body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
|
||||
body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
|
||||
body('cache.previews.size').isInt().withMessage('Should have a valid previews size'),
|
||||
|
||||
body('services.twitter.username').exists().withMessage('Should have a valid twitter username'),
|
||||
body('services.twitter.whitelisted').isBoolean().withMessage('Should have a valid twitter whitelisted boolean'),
|
||||
|
||||
body('cache.previews.size').isInt().withMessage('Should have a valid previews cache size'),
|
||||
body('cache.captions.size').isInt().withMessage('Should have a valid captions cache size'),
|
||||
|
||||
body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'),
|
||||
body('signup.limit').isInt().withMessage('Should have a valid signup limit'),
|
||||
body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'),
|
||||
|
||||
body('admin.email').isEmail().withMessage('Should have a valid administrator email'),
|
||||
body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'),
|
||||
|
||||
body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'),
|
||||
body('user.videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily video quota'),
|
||||
|
||||
body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'),
|
||||
body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'),
|
||||
body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'),
|
||||
body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'),
|
||||
body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'),
|
||||
body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'),
|
||||
body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'),
|
||||
body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'),
|
||||
|
||||
body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'),
|
||||
body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'),
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import * as express from 'express'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { areValidationErrors } from './utils'
|
||||
import { isHostValid } from '../../helpers/custom-validators/servers'
|
||||
import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers'
|
||||
import { ServerModel } from '../../models/server/server'
|
||||
import { body } from 'express-validator/check'
|
||||
import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
|
||||
import { Emailer } from '../../lib/emailer'
|
||||
import { Redis } from '../../lib/redis'
|
||||
import { CONFIG } from '../../initializers/constants'
|
||||
|
||||
const serverGetValidator = [
|
||||
body('host').custom(isHostValid).withMessage('Should have a valid host'),
|
||||
|
@ -26,8 +30,49 @@ const serverGetValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
const contactAdministratorValidator = [
|
||||
body('fromName')
|
||||
.custom(isUserDisplayNameValid).withMessage('Should have a valid name'),
|
||||
body('fromEmail')
|
||||
.isEmail().withMessage('Should have a valid email'),
|
||||
body('body')
|
||||
.custom(isValidContactBody).withMessage('Should have a valid body'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking contactAdministratorValidator parameters', { parameters: req.body })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.CONTACT_FORM.ENABLED === false) {
|
||||
return res
|
||||
.status(409)
|
||||
.send({ error: 'Contact form is not enabled on this instance.' })
|
||||
.end()
|
||||
}
|
||||
|
||||
if (Emailer.Instance.isEnabled() === false) {
|
||||
return res
|
||||
.status(409)
|
||||
.send({ error: 'Emailer is not enabled on this instance.' })
|
||||
.end()
|
||||
}
|
||||
|
||||
if (await Redis.Instance.isContactFormIpExists(req.ip)) {
|
||||
logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
|
||||
|
||||
return res
|
||||
.status(403)
|
||||
.send({ error: 'You already sent a contact form recently.' })
|
||||
.end()
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
serverGetValidator
|
||||
serverGetValidator,
|
||||
contactAdministratorValidator
|
||||
}
|
||||
|
|
|
@ -48,6 +48,9 @@ describe('Test config API validators', function () {
|
|||
admin: {
|
||||
email: 'superadmin1@example.com'
|
||||
},
|
||||
contactForm: {
|
||||
enabled: false
|
||||
},
|
||||
user: {
|
||||
videoQuota: 5242881,
|
||||
videoQuotaDaily: 318742
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/* tslint:disable:no-unused-expression */
|
||||
|
||||
import 'mocha'
|
||||
|
||||
import {
|
||||
flushTests,
|
||||
immutableAssign,
|
||||
killallServers,
|
||||
reRunServer,
|
||||
runServer,
|
||||
ServerInfo,
|
||||
setAccessTokensToServers
|
||||
} from '../../../../shared/utils'
|
||||
import {
|
||||
checkBadCountPagination,
|
||||
checkBadSortPagination,
|
||||
checkBadStartPagination
|
||||
} from '../../../../shared/utils/requests/check-api-params'
|
||||
import { getAccount } from '../../../../shared/utils/users/accounts'
|
||||
import { sendContactForm } from '../../../../shared/utils/server/contact-form'
|
||||
import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
|
||||
|
||||
describe('Test contact form API validators', function () {
|
||||
let server: ServerInfo
|
||||
const emails: object[] = []
|
||||
const defaultBody = {
|
||||
fromName: 'super name',
|
||||
fromEmail: 'toto@example.com',
|
||||
body: 'Hello, how are you?'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await flushTests()
|
||||
await MockSmtpServer.Instance.collectEmails(emails)
|
||||
|
||||
// Email is disabled
|
||||
server = await runServer(1)
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if emails are disabled', async function () {
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 409 }))
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if it is disabled in the configuration', async function () {
|
||||
killallServers([ server ])
|
||||
|
||||
// Contact form is disabled
|
||||
await reRunServer(server, { smtp: { hostname: 'localhost' }, contact_form: { enabled: false } })
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 409 }))
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if from email is invalid', async function () {
|
||||
killallServers([ server ])
|
||||
|
||||
// Email & contact form enabled
|
||||
await reRunServer(server, { smtp: { hostname: 'localhost' } })
|
||||
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail' }))
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail@' }))
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: undefined }))
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if from name is invalid', async function () {
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: 'name'.repeat(100) }))
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: '' }))
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: undefined }))
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if body is invalid', async function () {
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: 'body'.repeat(5000) }))
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: 'a' }))
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: undefined }))
|
||||
})
|
||||
|
||||
it('Should accept a contact form with the correct parameters', async function () {
|
||||
await sendContactForm(immutableAssign(defaultBody, { url: server.url }))
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
killallServers([ server ])
|
||||
|
||||
// Keep the logs if the test failed
|
||||
if (this['ok']) {
|
||||
await flushTests()
|
||||
}
|
||||
})
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
// Order of the tests we want to execute
|
||||
import './accounts'
|
||||
import './blocklist'
|
||||
import './config'
|
||||
import './contact-form'
|
||||
import './follows'
|
||||
import './jobs'
|
||||
import './redundancy'
|
||||
|
|
|
@ -33,14 +33,20 @@ function checkInitialConfig (data: CustomConfig) {
|
|||
expect(data.instance.defaultNSFWPolicy).to.equal('display')
|
||||
expect(data.instance.customizations.css).to.be.empty
|
||||
expect(data.instance.customizations.javascript).to.be.empty
|
||||
|
||||
expect(data.services.twitter.username).to.equal('@Chocobozzz')
|
||||
expect(data.services.twitter.whitelisted).to.be.false
|
||||
|
||||
expect(data.cache.previews.size).to.equal(1)
|
||||
expect(data.cache.captions.size).to.equal(1)
|
||||
|
||||
expect(data.signup.enabled).to.be.true
|
||||
expect(data.signup.limit).to.equal(4)
|
||||
expect(data.signup.requiresEmailVerification).to.be.false
|
||||
|
||||
expect(data.admin.email).to.equal('admin1@example.com')
|
||||
expect(data.contactForm.enabled).to.be.true
|
||||
|
||||
expect(data.user.videoQuota).to.equal(5242880)
|
||||
expect(data.user.videoQuotaDaily).to.equal(-1)
|
||||
expect(data.transcoding.enabled).to.be.false
|
||||
|
@ -64,16 +70,23 @@ function checkUpdatedConfig (data: CustomConfig) {
|
|||
expect(data.instance.defaultNSFWPolicy).to.equal('blur')
|
||||
expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
|
||||
expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
|
||||
|
||||
expect(data.services.twitter.username).to.equal('@Kuja')
|
||||
expect(data.services.twitter.whitelisted).to.be.true
|
||||
|
||||
expect(data.cache.previews.size).to.equal(2)
|
||||
expect(data.cache.captions.size).to.equal(3)
|
||||
|
||||
expect(data.signup.enabled).to.be.false
|
||||
expect(data.signup.limit).to.equal(5)
|
||||
expect(data.signup.requiresEmailVerification).to.be.true
|
||||
|
||||
expect(data.admin.email).to.equal('superadmin1@example.com')
|
||||
expect(data.contactForm.enabled).to.be.false
|
||||
|
||||
expect(data.user.videoQuota).to.equal(5242881)
|
||||
expect(data.user.videoQuotaDaily).to.equal(318742)
|
||||
|
||||
expect(data.transcoding.enabled).to.be.true
|
||||
expect(data.transcoding.threads).to.equal(1)
|
||||
expect(data.transcoding.allowAdditionalExtensions).to.be.true
|
||||
|
@ -82,6 +95,7 @@ function checkUpdatedConfig (data: CustomConfig) {
|
|||
expect(data.transcoding.resolutions['480p']).to.be.true
|
||||
expect(data.transcoding.resolutions['720p']).to.be.false
|
||||
expect(data.transcoding.resolutions['1080p']).to.be.false
|
||||
|
||||
expect(data.import.videos.http.enabled).to.be.false
|
||||
expect(data.import.videos.torrent.enabled).to.be.false
|
||||
}
|
||||
|
@ -127,6 +141,8 @@ describe('Test config', function () {
|
|||
expect(data.video.file.extensions).to.contain('.mp4')
|
||||
expect(data.video.file.extensions).to.contain('.webm')
|
||||
expect(data.video.file.extensions).to.contain('.ogv')
|
||||
|
||||
expect(data.contactForm.enabled).to.be.true
|
||||
})
|
||||
|
||||
it('Should get the customized configuration', async function () {
|
||||
|
@ -172,6 +188,9 @@ describe('Test config', function () {
|
|||
admin: {
|
||||
email: 'superadmin1@example.com'
|
||||
},
|
||||
contactForm: {
|
||||
enabled: false
|
||||
},
|
||||
user: {
|
||||
videoQuota: 5242881,
|
||||
videoQuotaDaily: 318742
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/* tslint:disable:no-unused-expression */
|
||||
|
||||
import * as chai from 'chai'
|
||||
import 'mocha'
|
||||
import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, wait } from '../../../../shared/utils'
|
||||
import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
|
||||
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
||||
import { sendContactForm } from '../../../../shared/utils/server/contact-form'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
describe('Test contact form', function () {
|
||||
let server: ServerInfo
|
||||
const emails: object[] = []
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await MockSmtpServer.Instance.collectEmails(emails)
|
||||
|
||||
await flushTests()
|
||||
|
||||
const overrideConfig = {
|
||||
smtp: {
|
||||
hostname: 'localhost'
|
||||
}
|
||||
}
|
||||
server = await runServer(1, overrideConfig)
|
||||
await setAccessTokensToServers([ server ])
|
||||
})
|
||||
|
||||
it('Should send a contact form', async function () {
|
||||
await sendContactForm({
|
||||
url: server.url,
|
||||
fromEmail: 'toto@example.com',
|
||||
body: 'my super message',
|
||||
fromName: 'Super toto'
|
||||
})
|
||||
|
||||
await waitJobs(server)
|
||||
|
||||
expect(emails).to.have.lengthOf(1)
|
||||
|
||||
const email = emails[0]
|
||||
|
||||
expect(email['from'][0]['address']).equal('toto@example.com')
|
||||
expect(email['to'][0]['address']).equal('admin1@example.com')
|
||||
expect(email['subject']).contains('Contact form')
|
||||
expect(email['text']).contains('my super message')
|
||||
})
|
||||
|
||||
it('Should not be able to send another contact form because of the anti spam checker', async function () {
|
||||
await sendContactForm({
|
||||
url: server.url,
|
||||
fromEmail: 'toto@example.com',
|
||||
body: 'my super message',
|
||||
fromName: 'Super toto'
|
||||
})
|
||||
|
||||
await sendContactForm({
|
||||
url: server.url,
|
||||
fromEmail: 'toto@example.com',
|
||||
body: 'my super message',
|
||||
fromName: 'Super toto',
|
||||
expectedStatus: 403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should be able to send another contact form after a while', async function () {
|
||||
await wait(1000)
|
||||
|
||||
await sendContactForm({
|
||||
url: server.url,
|
||||
fromEmail: 'toto@example.com',
|
||||
body: 'my super message',
|
||||
fromName: 'Super toto'
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
killallServers([ server ])
|
||||
})
|
||||
})
|
|
@ -8,18 +8,17 @@ import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-c
|
|||
|
||||
import {
|
||||
completeVideoCheck,
|
||||
getVideo,
|
||||
immutableAssign,
|
||||
reRunServer,
|
||||
unfollow,
|
||||
viewVideo,
|
||||
flushAndRunMultipleServers,
|
||||
getVideo,
|
||||
getVideosList,
|
||||
immutableAssign,
|
||||
killallServers,
|
||||
reRunServer,
|
||||
ServerInfo,
|
||||
setAccessTokensToServers,
|
||||
uploadVideo,
|
||||
unfollow,
|
||||
updateVideo,
|
||||
uploadVideo,
|
||||
wait
|
||||
} from '../../../../shared/utils'
|
||||
import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import './config'
|
||||
import './contact-form'
|
||||
import './email'
|
||||
import './follow-constraints'
|
||||
import './follows'
|
||||
|
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
import * as chai from 'chai'
|
||||
import 'mocha'
|
||||
import { snakeCase, isNumber } from 'lodash'
|
||||
import {
|
||||
parseBytes
|
||||
parseBytes, objectConverter
|
||||
} from '../../helpers/core-utils'
|
||||
import { isNumeric } from 'validator'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
describe('Parse Bytes', function () {
|
||||
|
||||
it('Should pass when given valid value', async function () {
|
||||
// just return it
|
||||
expect(parseBytes(1024)).to.be.eq(1024)
|
||||
|
@ -45,4 +48,51 @@ describe('Parse Bytes', function () {
|
|||
it('Should be invalid when given invalid value', async function () {
|
||||
expect(parseBytes('6GB 1GB')).to.be.eq(6)
|
||||
})
|
||||
|
||||
it('Should convert an object', async function () {
|
||||
function keyConverter (k: string) {
|
||||
return snakeCase(k)
|
||||
}
|
||||
|
||||
function valueConverter (v: any) {
|
||||
if (isNumeric(v + '')) return parseInt('' + v, 10)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
const obj = {
|
||||
mySuperKey: 'hello',
|
||||
mySuper2Key: '45',
|
||||
mySuper3Key: {
|
||||
mySuperSubKey: '15',
|
||||
mySuperSub2Key: 'hello',
|
||||
mySuperSub3Key: [ '1', 'hello', 2 ],
|
||||
mySuperSub4Key: 4
|
||||
},
|
||||
mySuper4Key: 45,
|
||||
toto: {
|
||||
super_key: '15',
|
||||
superKey2: 'hello'
|
||||
},
|
||||
super_key: {
|
||||
superKey4: 15
|
||||
}
|
||||
}
|
||||
|
||||
const res = objectConverter(obj, keyConverter, valueConverter)
|
||||
|
||||
expect(res.my_super_key).to.equal('hello')
|
||||
expect(res.my_super_2_key).to.equal(45)
|
||||
expect(res.my_super_3_key.my_super_sub_key).to.equal(15)
|
||||
expect(res.my_super_3_key.my_super_sub_2_key).to.equal('hello')
|
||||
expect(res.my_super_3_key.my_super_sub_3_key).to.deep.equal([ 1, 'hello', 2 ])
|
||||
expect(res.my_super_3_key.my_super_sub_4_key).to.equal(4)
|
||||
expect(res.toto.super_key).to.equal(15)
|
||||
expect(res.toto.super_key_2).to.equal('hello')
|
||||
expect(res.super_key.super_key_4).to.equal(15)
|
||||
|
||||
// Immutable
|
||||
expect(res.mySuperKey).to.be.undefined
|
||||
expect(obj['my_super_key']).to.be.undefined
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface ContactForm {
|
||||
fromEmail: string
|
||||
fromName: string
|
||||
body: string
|
||||
}
|
|
@ -41,6 +41,10 @@ export interface CustomConfig {
|
|||
email: string
|
||||
}
|
||||
|
||||
contactForm: {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
user: {
|
||||
videoQuota: number
|
||||
videoQuotaDaily: number
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export * from './about.model'
|
||||
export * from './contact-form.model'
|
||||
export * from './custom-config.model'
|
||||
export * from './job.model'
|
||||
export * from './server-config.model'
|
||||
export * from './server-stats.model'
|
|
@ -19,6 +19,10 @@ export interface ServerConfig {
|
|||
enabled: boolean
|
||||
}
|
||||
|
||||
contactForm: {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
signup: {
|
||||
allowed: boolean,
|
||||
allowedForCurrentIP: boolean,
|
||||
|
|
|
@ -15,6 +15,8 @@ class MockSmtpServer {
|
|||
return this.emails.push(msg.email)
|
||||
}
|
||||
})
|
||||
|
||||
process.on('exit', () => this.kill())
|
||||
}
|
||||
|
||||
collectEmails (emailsCollection: object[]) {
|
||||
|
@ -42,6 +44,8 @@ class MockSmtpServer {
|
|||
}
|
||||
|
||||
kill () {
|
||||
if (!this.emailChildProcess) return
|
||||
|
||||
process.kill(this.emailChildProcess.pid)
|
||||
|
||||
this.emailChildProcess = null
|
||||
|
|
|
@ -80,6 +80,9 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
|
|||
admin: {
|
||||
email: 'superadmin1@example.com'
|
||||
},
|
||||
contactForm: {
|
||||
enabled: true
|
||||
},
|
||||
user: {
|
||||
videoQuota: 5242881,
|
||||
videoQuotaDaily: 318742
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import * as request from 'supertest'
|
||||
import { ContactForm } from '../../models/server'
|
||||
|
||||
function sendContactForm (options: {
|
||||
url: string,
|
||||
fromEmail: string,
|
||||
fromName: string,
|
||||
body: string,
|
||||
expectedStatus?: number
|
||||
}) {
|
||||
const path = '/api/v1/server/contact'
|
||||
|
||||
const body: ContactForm = {
|
||||
fromEmail: options.fromEmail,
|
||||
fromName: options.fromName,
|
||||
body: options.body
|
||||
}
|
||||
return request(options.url)
|
||||
.post(path)
|
||||
.send(body)
|
||||
.expect(options.expectedStatus || 204)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
sendContactForm
|
||||
}
|
|
@ -18,3 +18,4 @@ PEERTUBE_ADMIN_EMAIL=admin@domain.tld
|
|||
# /!\ Prefer to use the PeerTube admin interface to set the following configurations /!\
|
||||
#PEERTUBE_SIGNUP_ENABLED=true
|
||||
#PEERTUBE_TRANSCODING_ENABLED=true
|
||||
#PEERTUBE_CONTACT_FORM_ENABLED=true
|
||||
|
|
|
@ -50,6 +50,11 @@ user:
|
|||
admin:
|
||||
email: "PEERTUBE_ADMIN_EMAIL"
|
||||
|
||||
contact_form:
|
||||
enabled:
|
||||
__name: "PEERTUBE_CONTACT_FORM_ENABLED"
|
||||
__format: "json"
|
||||
|
||||
signup:
|
||||
enabled:
|
||||
__name: "PEERTUBE_SIGNUP_ENABLED"
|
||||
|
|
Loading…
Reference in New Issue