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
|
size: 500 # Max number of video captions/subtitles you want to cache
|
||||||
|
|
||||||
admin:
|
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:
|
signup:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
|
@ -115,8 +115,13 @@ cache:
|
||||||
size: 500 # Max number of video captions/subtitles you want to cache
|
size: 500 # Max number of video captions/subtitles you want to cache
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
|
# Used to generate the root user at first startup
|
||||||
|
# And to receive emails from the contact form
|
||||||
email: 'admin@example.com'
|
email: 'admin@example.com'
|
||||||
|
|
||||||
|
contact_form:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
signup:
|
signup:
|
||||||
enabled: false
|
enabled: false
|
||||||
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
||||||
|
|
|
@ -21,6 +21,9 @@ smtp:
|
||||||
log:
|
log:
|
||||||
level: 'debug'
|
level: 'debug'
|
||||||
|
|
||||||
|
contact_form:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
redundancy:
|
redundancy:
|
||||||
videos:
|
videos:
|
||||||
check_interval: '10 minutes'
|
check_interval: '10 minutes'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { omit } from 'lodash'
|
import { omit, snakeCase } from 'lodash'
|
||||||
import { ServerConfig, UserRight } from '../../../shared'
|
import { ServerConfig, UserRight } from '../../../shared'
|
||||||
import { About } from '../../../shared/models/server/about.model'
|
import { About } from '../../../shared/models/server/about.model'
|
||||||
import { CustomConfig } from '../../../shared/models/server/custom-config.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 { remove, writeJSON } from 'fs-extra'
|
||||||
import { getServerCommit } from '../../helpers/utils'
|
import { getServerCommit } from '../../helpers/utils'
|
||||||
import { Emailer } from '../../lib/emailer'
|
import { Emailer } from '../../lib/emailer'
|
||||||
|
import { isNumeric } from 'validator'
|
||||||
|
import { objectConverter } from '../../helpers/core-utils'
|
||||||
|
|
||||||
const packageJSON = require('../../../../package.json')
|
const packageJSON = require('../../../../package.json')
|
||||||
const configRouter = express.Router()
|
const configRouter = express.Router()
|
||||||
|
@ -65,6 +67,9 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||||
email: {
|
email: {
|
||||||
enabled: Emailer.Instance.isEnabled()
|
enabled: Emailer.Instance.isEnabled()
|
||||||
},
|
},
|
||||||
|
contactForm: {
|
||||||
|
enabled: CONFIG.CONTACT_FORM.ENABLED
|
||||||
|
},
|
||||||
serverVersion: packageJSON.version,
|
serverVersion: packageJSON.version,
|
||||||
serverCommit,
|
serverCommit,
|
||||||
signup: {
|
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) {
|
async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const toUpdate: CustomConfig = req.body
|
|
||||||
const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
|
const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
|
||||||
|
|
||||||
// Force number conversion
|
// camelCase to snake_case key + Force number conversion
|
||||||
toUpdate.cache.previews.size = parseInt('' + toUpdate.cache.previews.size, 10)
|
const toUpdateJSON = convertCustomConfigBody(req.body)
|
||||||
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
|
|
||||||
|
|
||||||
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
|
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
|
||||||
|
|
||||||
|
@ -243,6 +224,9 @@ function customConfig (): CustomConfig {
|
||||||
admin: {
|
admin: {
|
||||||
email: CONFIG.ADMIN.EMAIL
|
email: CONFIG.ADMIN.EMAIL
|
||||||
},
|
},
|
||||||
|
contactForm: {
|
||||||
|
enabled: CONFIG.CONTACT_FORM.ENABLED
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
videoQuota: CONFIG.USER.VIDEO_QUOTA,
|
videoQuota: CONFIG.USER.VIDEO_QUOTA,
|
||||||
videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
|
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 { statsRouter } from './stats'
|
||||||
import { serverRedundancyRouter } from './redundancy'
|
import { serverRedundancyRouter } from './redundancy'
|
||||||
import { serverBlocklistRouter } from './server-blocklist'
|
import { serverBlocklistRouter } from './server-blocklist'
|
||||||
|
import { contactRouter } from './contact'
|
||||||
|
|
||||||
const serverRouter = express.Router()
|
const serverRouter = express.Router()
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ serverRouter.use('/', serverFollowsRouter)
|
||||||
serverRouter.use('/', serverRedundancyRouter)
|
serverRouter.use('/', serverRedundancyRouter)
|
||||||
serverRouter.use('/', statsRouter)
|
serverRouter.use('/', statsRouter)
|
||||||
serverRouter.use('/', serverBlocklistRouter)
|
serverRouter.use('/', serverBlocklistRouter)
|
||||||
|
serverRouter.use('/', contactRouter)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,25 @@ import * as pem from 'pem'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import { exec } from 'child_process'
|
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 = {
|
const timeTable = {
|
||||||
ms: 1,
|
ms: 1,
|
||||||
|
@ -235,6 +254,7 @@ export {
|
||||||
isTestInstance,
|
isTestInstance,
|
||||||
isProdInstance,
|
isProdInstance,
|
||||||
|
|
||||||
|
objectConverter,
|
||||||
root,
|
root,
|
||||||
escapeHTML,
|
escapeHTML,
|
||||||
pageToStartAndCount,
|
pageToStartAndCount,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'express-validator'
|
||||||
|
|
||||||
import { isArray, exists } from './misc'
|
import { isArray, exists } from './misc'
|
||||||
import { isTestInstance } from '../core-utils'
|
import { isTestInstance } from '../core-utils'
|
||||||
|
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||||
|
|
||||||
function isHostValid (host: string) {
|
function isHostValid (host: string) {
|
||||||
const isURLOptions = {
|
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 {
|
export {
|
||||||
|
isValidContactBody,
|
||||||
|
isValidContactFromName,
|
||||||
isEachUniqueHostValid,
|
isEachUniqueHostValid,
|
||||||
isHostValid
|
isHostValid
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { join } from 'path'
|
||||||
import { Instance as ParseTorrent } from 'parse-torrent'
|
import { Instance as ParseTorrent } from 'parse-torrent'
|
||||||
import { remove } from 'fs-extra'
|
import { remove } from 'fs-extra'
|
||||||
import * as memoizee from 'memoizee'
|
import * as memoizee from 'memoizee'
|
||||||
|
import { isArray } from './custom-validators/misc'
|
||||||
|
|
||||||
function deleteFileAsync (path: string) {
|
function deleteFileAsync (path: string) {
|
||||||
remove(path)
|
remove(path)
|
||||||
|
@ -19,10 +20,7 @@ async function generateRandomString (size: number) {
|
||||||
return raw.toString('hex')
|
return raw.toString('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormattableToJSON {
|
interface FormattableToJSON { toFormattedJSON (args?: any) }
|
||||||
toFormattedJSON (args?: any)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
|
function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
|
||||||
const formattedObjects: U[] = []
|
const formattedObjects: U[] = []
|
||||||
|
|
||||||
|
|
|
@ -231,6 +231,9 @@ const CONFIG = {
|
||||||
ADMIN: {
|
ADMIN: {
|
||||||
get EMAIL () { return config.get<string>('admin.email') }
|
get EMAIL () { return config.get<string>('admin.email') }
|
||||||
},
|
},
|
||||||
|
CONTACT_FORM: {
|
||||||
|
get ENABLED () { return config.get<boolean>('contact_form.enabled') }
|
||||||
|
},
|
||||||
SIGNUP: {
|
SIGNUP: {
|
||||||
get ENABLED () { return config.get<boolean>('signup.enabled') },
|
get ENABLED () { return config.get<boolean>('signup.enabled') },
|
||||||
get LIMIT () { return config.get<number>('signup.limit') },
|
get LIMIT () { return config.get<number>('signup.limit') },
|
||||||
|
@ -394,6 +397,10 @@ let CONSTRAINTS_FIELDS = {
|
||||||
},
|
},
|
||||||
VIDEO_SHARE: {
|
VIDEO_SHARE: {
|
||||||
URL: { min: 3, max: 2000 } // Length
|
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 VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
|
||||||
|
let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
|
||||||
|
|
||||||
const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
|
const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
|
||||||
MIN: 10,
|
MIN: 10,
|
||||||
AVERAGE: 30,
|
AVERAGE: 30,
|
||||||
|
@ -685,6 +694,7 @@ if (isTestInstance() === true) {
|
||||||
REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
|
REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
|
||||||
|
|
||||||
VIDEO_VIEW_LIFETIME = 1000 // 1 second
|
VIDEO_VIEW_LIFETIME = 1000 // 1 second
|
||||||
|
CONTACT_FORM_LIFETIME = 1000 // 1 second
|
||||||
|
|
||||||
JOB_ATTEMPTS['email'] = 1
|
JOB_ATTEMPTS['email'] = 1
|
||||||
|
|
||||||
|
@ -756,6 +766,7 @@ export {
|
||||||
HTTP_SIGNATURE,
|
HTTP_SIGNATURE,
|
||||||
VIDEO_IMPORT_STATES,
|
VIDEO_IMPORT_STATES,
|
||||||
VIDEO_VIEW_LIFETIME,
|
VIDEO_VIEW_LIFETIME,
|
||||||
|
CONTACT_FORM_LIFETIME,
|
||||||
buildLanguages
|
buildLanguages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -354,13 +354,32 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
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) {
|
if (!this.enabled) {
|
||||||
throw new Error('Cannot send mail because SMTP is not configured.')
|
throw new Error('Cannot send mail because SMTP is not configured.')
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.transporter.sendMail({
|
return this.transporter.sendMail({
|
||||||
from: CONFIG.SMTP.FROM_ADDRESS,
|
from: from || CONFIG.SMTP.FROM_ADDRESS,
|
||||||
to: to.join(','),
|
to: to.join(','),
|
||||||
subject,
|
subject,
|
||||||
text
|
text
|
||||||
|
|
|
@ -6,13 +6,14 @@ export type EmailPayload = {
|
||||||
to: string[]
|
to: string[]
|
||||||
subject: string
|
subject: string
|
||||||
text: string
|
text: string
|
||||||
|
from?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processEmail (job: Bull.Job) {
|
async function processEmail (job: Bull.Job) {
|
||||||
const payload = job.data as EmailPayload
|
const payload = job.data as EmailPayload
|
||||||
logger.info('Processing email in job %d.', job.id)
|
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 { createClient, RedisClient } from 'redis'
|
||||||
import { logger } from '../helpers/logger'
|
import { logger } from '../helpers/logger'
|
||||||
import { generateRandomString } from '../helpers/utils'
|
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 = {
|
type CachedRoute = {
|
||||||
body: string,
|
body: string,
|
||||||
|
@ -76,6 +82,16 @@ class Redis {
|
||||||
return this.getValue(this.generateVerifyEmailKey(userId))
|
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 *************/
|
/************* Views per IP *************/
|
||||||
|
|
||||||
setIPVideoView (ip: string, videoUUID: string) {
|
setIPVideoView (ip: string, videoUUID: string) {
|
||||||
|
@ -175,7 +191,11 @@ class Redis {
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateViewKey (ip: string, videoUUID: string) {
|
private generateViewKey (ip: string, videoUUID: string) {
|
||||||
return videoUUID + '-' + ip
|
return `views-${videoUUID}-${ip}`
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateContactFormKey (ip: string) {
|
||||||
|
return 'contact-form-' + ip
|
||||||
}
|
}
|
||||||
|
|
||||||
/************* Redis helpers *************/
|
/************* Redis helpers *************/
|
||||||
|
|
|
@ -1,29 +1,44 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { body } from 'express-validator/check'
|
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 { logger } from '../../helpers/logger'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from './utils'
|
||||||
|
|
||||||
const customConfigUpdateValidator = [
|
const customConfigUpdateValidator = [
|
||||||
body('instance.name').exists().withMessage('Should have a valid instance name'),
|
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.description').exists().withMessage('Should have a valid instance description'),
|
||||||
body('instance.terms').exists().withMessage('Should have a valid instance terms'),
|
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.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.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.css').exists().withMessage('Should have a valid instance CSS customization'),
|
||||||
body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript 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.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'),
|
||||||
body('signup.limit').isInt().withMessage('Should have a valid signup limit'),
|
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('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.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.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.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.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.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.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.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('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.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'),
|
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 * as express from 'express'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { areValidationErrors } from './utils'
|
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 { ServerModel } from '../../models/server/server'
|
||||||
import { body } from 'express-validator/check'
|
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 = [
|
const serverGetValidator = [
|
||||||
body('host').custom(isHostValid).withMessage('Should have a valid host'),
|
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 {
|
export {
|
||||||
serverGetValidator
|
serverGetValidator,
|
||||||
|
contactAdministratorValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,9 @@ describe('Test config API validators', function () {
|
||||||
admin: {
|
admin: {
|
||||||
email: 'superadmin1@example.com'
|
email: 'superadmin1@example.com'
|
||||||
},
|
},
|
||||||
|
contactForm: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
videoQuota: 5242881,
|
videoQuota: 5242881,
|
||||||
videoQuotaDaily: 318742
|
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 './accounts'
|
||||||
import './blocklist'
|
import './blocklist'
|
||||||
import './config'
|
import './config'
|
||||||
|
import './contact-form'
|
||||||
import './follows'
|
import './follows'
|
||||||
import './jobs'
|
import './jobs'
|
||||||
import './redundancy'
|
import './redundancy'
|
||||||
|
|
|
@ -33,14 +33,20 @@ function checkInitialConfig (data: CustomConfig) {
|
||||||
expect(data.instance.defaultNSFWPolicy).to.equal('display')
|
expect(data.instance.defaultNSFWPolicy).to.equal('display')
|
||||||
expect(data.instance.customizations.css).to.be.empty
|
expect(data.instance.customizations.css).to.be.empty
|
||||||
expect(data.instance.customizations.javascript).to.be.empty
|
expect(data.instance.customizations.javascript).to.be.empty
|
||||||
|
|
||||||
expect(data.services.twitter.username).to.equal('@Chocobozzz')
|
expect(data.services.twitter.username).to.equal('@Chocobozzz')
|
||||||
expect(data.services.twitter.whitelisted).to.be.false
|
expect(data.services.twitter.whitelisted).to.be.false
|
||||||
|
|
||||||
expect(data.cache.previews.size).to.equal(1)
|
expect(data.cache.previews.size).to.equal(1)
|
||||||
expect(data.cache.captions.size).to.equal(1)
|
expect(data.cache.captions.size).to.equal(1)
|
||||||
|
|
||||||
expect(data.signup.enabled).to.be.true
|
expect(data.signup.enabled).to.be.true
|
||||||
expect(data.signup.limit).to.equal(4)
|
expect(data.signup.limit).to.equal(4)
|
||||||
expect(data.signup.requiresEmailVerification).to.be.false
|
expect(data.signup.requiresEmailVerification).to.be.false
|
||||||
|
|
||||||
expect(data.admin.email).to.equal('admin1@example.com')
|
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.videoQuota).to.equal(5242880)
|
||||||
expect(data.user.videoQuotaDaily).to.equal(-1)
|
expect(data.user.videoQuotaDaily).to.equal(-1)
|
||||||
expect(data.transcoding.enabled).to.be.false
|
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.defaultNSFWPolicy).to.equal('blur')
|
||||||
expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
|
expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
|
||||||
expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
|
expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
|
||||||
|
|
||||||
expect(data.services.twitter.username).to.equal('@Kuja')
|
expect(data.services.twitter.username).to.equal('@Kuja')
|
||||||
expect(data.services.twitter.whitelisted).to.be.true
|
expect(data.services.twitter.whitelisted).to.be.true
|
||||||
|
|
||||||
expect(data.cache.previews.size).to.equal(2)
|
expect(data.cache.previews.size).to.equal(2)
|
||||||
expect(data.cache.captions.size).to.equal(3)
|
expect(data.cache.captions.size).to.equal(3)
|
||||||
|
|
||||||
expect(data.signup.enabled).to.be.false
|
expect(data.signup.enabled).to.be.false
|
||||||
expect(data.signup.limit).to.equal(5)
|
expect(data.signup.limit).to.equal(5)
|
||||||
expect(data.signup.requiresEmailVerification).to.be.true
|
expect(data.signup.requiresEmailVerification).to.be.true
|
||||||
|
|
||||||
expect(data.admin.email).to.equal('superadmin1@example.com')
|
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.videoQuota).to.equal(5242881)
|
||||||
expect(data.user.videoQuotaDaily).to.equal(318742)
|
expect(data.user.videoQuotaDaily).to.equal(318742)
|
||||||
|
|
||||||
expect(data.transcoding.enabled).to.be.true
|
expect(data.transcoding.enabled).to.be.true
|
||||||
expect(data.transcoding.threads).to.equal(1)
|
expect(data.transcoding.threads).to.equal(1)
|
||||||
expect(data.transcoding.allowAdditionalExtensions).to.be.true
|
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['480p']).to.be.true
|
||||||
expect(data.transcoding.resolutions['720p']).to.be.false
|
expect(data.transcoding.resolutions['720p']).to.be.false
|
||||||
expect(data.transcoding.resolutions['1080p']).to.be.false
|
expect(data.transcoding.resolutions['1080p']).to.be.false
|
||||||
|
|
||||||
expect(data.import.videos.http.enabled).to.be.false
|
expect(data.import.videos.http.enabled).to.be.false
|
||||||
expect(data.import.videos.torrent.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('.mp4')
|
||||||
expect(data.video.file.extensions).to.contain('.webm')
|
expect(data.video.file.extensions).to.contain('.webm')
|
||||||
expect(data.video.file.extensions).to.contain('.ogv')
|
expect(data.video.file.extensions).to.contain('.ogv')
|
||||||
|
|
||||||
|
expect(data.contactForm.enabled).to.be.true
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should get the customized configuration', async function () {
|
it('Should get the customized configuration', async function () {
|
||||||
|
@ -172,6 +188,9 @@ describe('Test config', function () {
|
||||||
admin: {
|
admin: {
|
||||||
email: 'superadmin1@example.com'
|
email: 'superadmin1@example.com'
|
||||||
},
|
},
|
||||||
|
contactForm: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
videoQuota: 5242881,
|
videoQuota: 5242881,
|
||||||
videoQuotaDaily: 318742
|
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 {
|
import {
|
||||||
completeVideoCheck,
|
completeVideoCheck,
|
||||||
getVideo,
|
|
||||||
immutableAssign,
|
|
||||||
reRunServer,
|
|
||||||
unfollow,
|
|
||||||
viewVideo,
|
|
||||||
flushAndRunMultipleServers,
|
flushAndRunMultipleServers,
|
||||||
|
getVideo,
|
||||||
getVideosList,
|
getVideosList,
|
||||||
|
immutableAssign,
|
||||||
killallServers,
|
killallServers,
|
||||||
|
reRunServer,
|
||||||
ServerInfo,
|
ServerInfo,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
uploadVideo,
|
unfollow,
|
||||||
updateVideo,
|
updateVideo,
|
||||||
|
uploadVideo,
|
||||||
wait
|
wait
|
||||||
} from '../../../../shared/utils'
|
} from '../../../../shared/utils'
|
||||||
import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows'
|
import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows'
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import './config'
|
import './config'
|
||||||
|
import './contact-form'
|
||||||
import './email'
|
import './email'
|
||||||
import './follow-constraints'
|
import './follow-constraints'
|
||||||
import './follows'
|
import './follows'
|
||||||
|
|
|
@ -2,13 +2,16 @@
|
||||||
|
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
|
import { snakeCase, isNumber } from 'lodash'
|
||||||
import {
|
import {
|
||||||
parseBytes
|
parseBytes, objectConverter
|
||||||
} from '../../helpers/core-utils'
|
} from '../../helpers/core-utils'
|
||||||
|
import { isNumeric } from 'validator'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('Parse Bytes', function () {
|
describe('Parse Bytes', function () {
|
||||||
|
|
||||||
it('Should pass when given valid value', async function () {
|
it('Should pass when given valid value', async function () {
|
||||||
// just return it
|
// just return it
|
||||||
expect(parseBytes(1024)).to.be.eq(1024)
|
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 () {
|
it('Should be invalid when given invalid value', async function () {
|
||||||
expect(parseBytes('6GB 1GB')).to.be.eq(6)
|
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
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contactForm: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
user: {
|
user: {
|
||||||
videoQuota: number
|
videoQuota: number
|
||||||
videoQuotaDaily: 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
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contactForm: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
signup: {
|
signup: {
|
||||||
allowed: boolean,
|
allowed: boolean,
|
||||||
allowedForCurrentIP: boolean,
|
allowedForCurrentIP: boolean,
|
||||||
|
|
|
@ -15,6 +15,8 @@ class MockSmtpServer {
|
||||||
return this.emails.push(msg.email)
|
return this.emails.push(msg.email)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
process.on('exit', () => this.kill())
|
||||||
}
|
}
|
||||||
|
|
||||||
collectEmails (emailsCollection: object[]) {
|
collectEmails (emailsCollection: object[]) {
|
||||||
|
@ -42,6 +44,8 @@ class MockSmtpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
kill () {
|
kill () {
|
||||||
|
if (!this.emailChildProcess) return
|
||||||
|
|
||||||
process.kill(this.emailChildProcess.pid)
|
process.kill(this.emailChildProcess.pid)
|
||||||
|
|
||||||
this.emailChildProcess = null
|
this.emailChildProcess = null
|
||||||
|
|
|
@ -80,6 +80,9 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
|
||||||
admin: {
|
admin: {
|
||||||
email: 'superadmin1@example.com'
|
email: 'superadmin1@example.com'
|
||||||
},
|
},
|
||||||
|
contactForm: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
videoQuota: 5242881,
|
videoQuota: 5242881,
|
||||||
videoQuotaDaily: 318742
|
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 /!\
|
# /!\ Prefer to use the PeerTube admin interface to set the following configurations /!\
|
||||||
#PEERTUBE_SIGNUP_ENABLED=true
|
#PEERTUBE_SIGNUP_ENABLED=true
|
||||||
#PEERTUBE_TRANSCODING_ENABLED=true
|
#PEERTUBE_TRANSCODING_ENABLED=true
|
||||||
|
#PEERTUBE_CONTACT_FORM_ENABLED=true
|
||||||
|
|
|
@ -50,6 +50,11 @@ user:
|
||||||
admin:
|
admin:
|
||||||
email: "PEERTUBE_ADMIN_EMAIL"
|
email: "PEERTUBE_ADMIN_EMAIL"
|
||||||
|
|
||||||
|
contact_form:
|
||||||
|
enabled:
|
||||||
|
__name: "PEERTUBE_CONTACT_FORM_ENABLED"
|
||||||
|
__format: "json"
|
||||||
|
|
||||||
signup:
|
signup:
|
||||||
enabled:
|
enabled:
|
||||||
__name: "PEERTUBE_SIGNUP_ENABLED"
|
__name: "PEERTUBE_SIGNUP_ENABLED"
|
||||||
|
|
Loading…
Reference in New Issue