code:
#!/bin/bash
#sets up nginx config

#are you root?
[[ "$EUID" -ne 0 ]] && echo "Please run as root" && exit;

echo "[jschan nginx configuration helper]"
read -p "Enter the directory you cloned jschan, no trailing slash. (blank=$(pwd)): " JSCHAN_DIRECTORY
JSCHAN_DIRECTORY=${JSCHAN_DIRECTORY:-$(pwd)}
read -p "Enter your clearnet domain name e.g. example.com (blank=no clearnet domain): " CLEARNET_DOMAIN
SITES_AVAILABLE_NAME=${CLEARNET_DOMAIN:-jschan} #not sure on a good default, used for sites-available config name
read -p "Enter tor .onion address (blank=no .onion address): " ONION_DOMAIN
read -p "Enter lokinet .loki address (blank=no .loki address): " LOKI_DOMAIN
read -p "Would you like to add a www. subdomain? (y/n): " ADD_WWW_SUBDOMAIN
if [ "$CLEARNET_DOMAIN" != "" ]; then
	read -p "Run certbot and automatically configure a certificate for https on clearnet? (y/n): " CERTBOT
	if [ "$CERTBOT" == "n" ]; then
		read -p "Generate a self-signed certificate instead? (y/n): " SELFSIGNED
	fi
	if [ "$SELFSIGNED" == "n" ]; then
		read -p "Warning: no https certificate chosen for clearnet. Continue without https? (y/n): " NOHTTPS
		[[ "$NOHTTPS" == "n" ]] && echo "Exiting..." && exit;
	fi
fi
read -p "Should robots.txt disallow compliant crawlers? (y/n): " ROBOTS_TXT_DISALLOW
read -p "Allow google captcha in content-security policy? (y/n): " GOOGLE_CAPTCHA
read -p "Allow Hcaptcha in content-security policy? (y/n): " H_CAPTCHA
read -p "Allow Yandex SmartCaptcha in content-security policy? (y/n): " Y_CAPTCHA
read -p "Download and setup geoip for post flags? (y/n): " GEOIP

#looks good?
read -p "Is this correct?
jschan directory: $JSCHAN_DIRECTORY
clearnet domain: $CLEARNET_DOMAIN
.onion address: $ONION_DOMAIN
.loki address: $LOKI_DOMAIN
www subdomains: $ADD_WWW_SUBDOMAIN
certbot https cert: $CERTBOT
self-signed https cert: $SELFSIGNED
no https cert: $NOHTTPS
robots.txt disallow all: $ROBOTS_TXT_DISALLOW
google captcha: $GOOGLE_CAPTCHA
hcaptcha: $H_CAPTCHA
yandex captcha: $Y_CAPTCHA
geoip: $GEOIP
(y/n): " CORRECT
#not saying no = yes, just like real life
[[ "$CORRECT" == "n" ]] && echo "Exiting..." && exit;

#ask to overwrite if already exists
if [[ -f /etc/nginx/sites-available/$SITES_AVAILABLE_NAME ]]; then
	read -p "/etc/nginx/sites-available/$SITES_AVAILABLE_NAME already exists. Continue and overwrite existing configuration? (y/n)" OVERWRITE
	[[ "$OVERWRITE" == "n" ]] && echo "Exiting..." && exit;
	rm /etc/nginx/sites-available/$SITES_AVAILABLE_NAME
	rm /etc/nginx/sites-enabled/$SITES_AVAILABLE_NAME
fi

echo "Stopping nginx..."
sudo systemctl stop nginx

if [ "$CERTBOT" == "y" ]; then
	#run certbot for certificate
	if [ "$ADD_WWW_SUBDOMAIN" == "y" ]; then
		echo "Running certbot to setup SSL cert for $CLEARNET_DOMAIN and www.$CLEARNET_COMAIN..."
		sudo certbot certonly --nginx -d $CLEARNET_DOMAIN -d www.$CLEARNET_DOMAIN
	else
		echo "Running certbot to setup SSL cert for $CLEARNET_DOMAIN..."
		sudo certbot certonly --nginx -d $CLEARNET_DOMAIN
	fi
fi

echo "Copying snippets to nginx folder & replacing paths..."
#copy the snippets and replace install path, they aren't templated
sudo cp $JSCHAN_DIRECTORY/configs/nginx/snippets/* /etc/nginx/snippets
sudo sed -i "s|/path/to/jschan|$JSCHAN_DIRECTORY|g" /etc/nginx/snippets/*

#declare teplate start
JSCHAN_CONFIG="upstream chan {
	server 127.0.0.1:7000;
}"

#Use some variabels to make the templating easier later, depending on if they want www. or not
CLEARNET_SERVER_NAME="$CLEARNET_DOMAIN"
LOKI_SERVER_NAME="$LOKI_DOMAIN"
ONION_SERVER_NAME="$ONION_DOMAIN"
if [ "$ADD_WWW_SUBDOMAIN" == "y" ]; then
	CLEARNET_SERVER_NAME="$CLEARNET_DOMAIN www.$CLEARNET_DOMAIN"
	LOKI_SERVER_NAME="$LOKI_DOMAIN www.$LOKI_DOMAIN"
	ONION_SERVER_NAME="$ONION_DOMAIN www.$ONION_DOMAIN"
fi

if [ "$CLEARNET_DOMAIN" != "" ]; then

	HTTPS_MIDSECTION=""
	HTTPS_CERT_SECTION=""
	if [ "$CERTBOT" == "y" ]; then
		HTTPS_CERT_SECTION="
	ssl_certificate /etc/letsencrypt/live/$CLEARNET_DOMAIN/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/$CLEARNET_DOMAIN/privkey.pem;
	include /etc/letsencrypt/options-ssl-nginx.conf;
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
	"
	elif [ "$SELFSIGNED" == "y" ]; then
		echo "Generating self-signed SSL cert..."
		mkdir /etc/ssl/private/
		mkdir /etc/ssl/certs/
		sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt
		echo "Generating dh group. This may take a while..."
		sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096
		cat > /etc/nginx/snippets/ssl-params.conf <<EOF
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA";
EOF

		HTTPS_CERT_SECTION="
	ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
	ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
	ssl_dhparam /etc/nginx/dhparam.pem;
	include /etc/nginx/snippets/ssl-params.conf;
	"
	fi

	if [ "$NOHTTPS" != "y" ]; then
			HTTPS_MIDSECTION="
	listen [::]:443 ssl ipv6only=on;
	listen 443 ssl;
	$HTTPS_CERT_SECTION

}

server {
	if (\$host = www.$CLEARNET_DOMAIN) {
		return 301 https://\$host\$request_uri;
	}

	if (\$host = $CLEARNET_DOMAIN) {
		return 301 https://\$host\$request_uri;
	}

	server_name $CLEARNET_SERVER_NAME;
	return 444;
"
	fi

	#onion_location redirect header
	ONION_LOCATION=""
	if [ "$ONION_DOMAIN" != "" ]; then
		ONION_LOCATION="add_header onion-location 'http://$ONION_DOMAIN\$request_uri';"
	fi

	#concat clearnet server{} block
	JSCHAN_CONFIG="${JSCHAN_CONFIG}

server {
	server_name $CLEARNET_SERVER_NAME;
	client_max_body_size 0;

	$ONION_LOCATION

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_clearnet_routes.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;

$HTTPS_MIDSECTION

	listen 80;
	listen [::]:80;
}"

	#replace clearnet domain in snippets
	echo "Replacing clearnet domain in snippets..."
	sudo sed -i "s/example.com/$CLEARNET_DOMAIN/g" /etc/nginx/snippets/*

fi

if [ "$ONION_DOMAIN" != "" ]; then

	#concat onion server{} block
	JSCHAN_CONFIG="${JSCHAN_CONFIG}

server {
	server_name www.$ONION_DOMAIN $ONION_DOMAIN;
	client_max_body_size 0;

	listen unix:/var/run/nginx-tor.sock;
	allow 'unix:';
	deny all;

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
	include /etc/nginx/snippets/jschan_tor_routes.conf;
}"

	#replace onion domain in snippets
	echo "Replacing onion domain in snippets..."
	sudo sed -i "s/example.onion/$ONION_DOMAIN/g" /etc/nginx/snippets/*

else
	#no onion, remove it from CSP
	echo "No onion, removing example.onion from CSP..."
	sudo sed -i 's/ wss:\/\/www.example.onion\/ wss:\/\/example.onion\///g' /etc/nginx/snippets/security_headers*
fi

if [ "$LOKI_DOMAIN" != "" ]; then

	#concat lokinet server{} block
	JSCHAN_CONFIG="${JSCHAN_CONFIG}

server {
	server_name www.$LOKI_DOMAIN $LOKI_DOMAIN;
	client_max_body_size 0;

	#address may vary if this address is already used by something other than lokinet
	listen 172.16.0.1:80;

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
	include /etc/nginx/snippets/jschan_loki_routes.conf;
}"

	#replace lokinet domain in snippets
	echo "Replacing loki domain in snippets..."
	sudo sed -i "s/example.loki/$LOKI_DOMAIN/g" /etc/nginx/snippets/*

else
	#no lokinet, remove it from csp
	echo "No lokinet, removing example.loki from CSP..."
	sudo sed -i 's/ wss:\/\/www.example.loki\/ wss:\/\/example.loki\///g' /etc/nginx/snippets/security_headers*
fi

#write the config to file and syymlink to sites-available
echo "Writing main jschan vhost config..."
printf "$JSCHAN_CONFIG" > /etc/nginx/sites-available/$SITES_AVAILABLE_NAME
sudo ln -s -f /etc/nginx/sites-available/$SITES_AVAILABLE_NAME /etc/nginx/sites-enabled/$SITES_AVAILABLE_NAME

if [ "$NOHTTPS" == "y" ]; then
	echo "Adjusting config snippets to support NOHTTPS mode..."
	sudo sed -i "s/Forwarded-Proto https/Forwarded-Proto http/g" /etc/nginx/snippets/jschan_clearnet_routes.conf
fi

if [ "$GOOGLE_CAPTCHA" == "y" ]; then
	echo "Allowing recaptcha in CSP..."
	#add google captcha CSP exceptions
	sudo sed -i "s|script-src|script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ |g" /etc/nginx/snippets/*
	sudo sed -i "s|frame-src|frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/ |g" /etc/nginx/snippets/*
fi

if [ "$H_CAPTCHA" == "y" ]; then
	echo "Allowing hcaptcha in CSP..."
	#add hcaptcha CSP exceptions
	sudo sed -i "s|script-src|script-src https://hcaptcha.com https://*.hcaptcha.com |g" /etc/nginx/snippets/*
	sudo sed -i "s|frame-src|frame-src https://hcaptcha.com https://*.hcaptcha.com |g" /etc/nginx/snippets/*
	sudo sed -i "s|style-src|style-src https://hcaptcha.com https://*.hcaptcha.com |g" /etc/nginx/snippets/*
	sudo sed -i "s|connect-src|connect-src https://hcaptcha.com https://*.hcaptcha.com |g" /etc/nginx/snippets/*
fi

if [ "$Y_CAPTCHA" == "y" ]; then
	echo "Allowing Yandex SmartCaptcha in CSP..."
	#add yandex captcha CSP exceptions
	sudo sed -i "s|script-src|script-src https://smartcaptcha.yandexcloud.net/captcha.js |g" /etc/nginx/snippets/*
	sudo sed -i "s|frame-src|frame-src https://smartcaptcha.yandexcloud.net/ |g" /etc/nginx/snippets/*
	sudo sed -i "s|font-src|font-src yastatic.net |g" /etc/nginx/snippets/*
fi

if [ "$ROBOTS_TXT_DISALLOW" == "y" ]; then
	echo "Setting robots.txt to disallow all..."
	#add path / (all) to disallow to make robots.txt block all robots instead of allowing
	sudo sed -i "s|Disallow:|Disallow: /|g" /etc/nginx/snippets/jschan_common_routes.conf
fi

if [ "$GEOIP" == "y" ]; then
	echo "Downloading and installing geoip database for nginx..."
	#download geoip data
	cd /usr/share/GeoIP
	wget --retry-connrefused -qO- "https://download.db-ip.com/free/dbip-country-lite-`date +%Y-%m`.mmdb.gz"  | tee "/usr/share/GeoIP/dbip.mmdb.gz" >/dev/null
	gunzip "/usr/share/GeoIP/dbip.mmdb.gz"
	#add goeip_country to /etc/nginx/nginx.conf, only if not already exists
	grep -qF "geoip_country" /etc/nginx/nginx.conf
	if [ $? -eq 1 ]; then
		sudo sed -i '/http {/a \
geoip_country /usr/share/GeoIP/geoip.mmdb;' /etc/nginx/nginx.conf
	fi
else
	echo "Geoip not installed, removing directives..."
	sudo sed '/geoip_country/d' /etc/nginx/nginx.conf
	sudo sed '/geoip_country_code/d' /etc/nginx/snippets/jschan_clearnet_routes.conf
fi

echo "Restarting nginx..."
#and restart nginx
sudo systemctl restart nginx


code:
upstream chan {
	server 127.0.0.1:7000;
}

server {
	server_name www.example.loki example.loki;
	client_max_body_size 0;

	listen 172.16.0.1:80;

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
	include /etc/nginx/snippets/jschan_loki_routes.conf;
}

server {
	server_name www.example.onion example.onion;
	client_max_body_size 0;

	listen unix:/var/run/nginx-tor.sock;
	allow "unix:";
	deny all;

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
	include /etc/nginx/snippets/jschan_tor_routes.conf;
}

server {
	server_name www.example.com example.com;
	client_max_body_size 0;

	add_header onion-location 'http://example.onion$request_uri';

	listen [::]:443 ssl ipv6only=on; # managed by Certbot
	listen 443 ssl; # managed by Certbot
	ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
	ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
	include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_clearnet_routes.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
}

server {
	if ($host = www.example.com) {
		return 301 https://$host$request_uri;
	} # managed by Certbot

	if ($host = example.com) {
		return 301 https://$host$request_uri;
	} # managed by Certbot

	server_name www.example.com example.com;

	listen 80;
	listen [::]:80;
	return 444; # managed by Certbot
}

repo_name: diegolegion/algochan

license: mit


code:
error_page 404 /404.html;
error_page 500 /500.html;
error_page 502 /502.html;
error_page 503 /503.html;
error_page 504 /504.html;
location = /404.html {
	root /path/to/jschan/static/html;
	internal;
}
location = /500.html {
	root /path/to/jschan/static/html;
	internal;
}
location = /502.html {
	root /path/to/jschan/static/html;
	internal;
}
location = /503.html {
	root /path/to/jschan/static/html;
	internal;
}
location = /504.html {
	root /path/to/jschan/static/html;
	internal;
}


code:
location / {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;

	proxy_set_header Host $host;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection 'upgrade';
	proxy_cache_bypass $http_upgrade;

	proxy_set_header X-Forwarded-Proto https;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code $geoip_country_code;
}

location @backend {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto https;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code $geoip_country_code;
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}

location @backend-private {
	include /etc/nginx/snippets/security_headers_nocache.conf;
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto https;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code $geoip_country_code;
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}


code:
if ($request_uri ~ ^/(?!captcha|randombanner|forms|logout|socket\.io)) {
	rewrite ^([^.\?]*[^/])$ $1/ redirect;
	rewrite ^(.+)/$ $1/index.html redirect;
}

location = /robots.txt {
	access_log off;
	add_header Content-Type text/plain;
	return 200 "User-agent: *\nDisallow:\n";
}

location = /favicon.ico {
	access_log off;
	expires max;
	root /path/to/jschan/static/file;
	try_files $uri =404;
}

location = / {
	return 302 $scheme://$host/index.html;
}

location /captcha {
	root /path/to/jschan/static/captcha;
	if ($cookie_captchaid) {
		return 302 $scheme://$host/captcha/$cookie_captchaid.jpg;
	}
	try_files /$cookie_captchaid.jpg @backend;
}

# authed, no cache pages
location ~* ^/((\w+/manage/.*|globalmanage/(reports|bans|recent|boards|filters|globallogs|news|editnews/.*|accounts|editaccount/.*|roles|editrole/.*|settings))|sessions|mypermissions|account|create|twofactor|csrf)\.(html|json)$ {
	expires 0;
	try_files /dev/null @backend-private;
}

# public html
location ~* \.html$ {
	expires 0;
	root /path/to/jschan/static/html;
	try_files $uri @backend;
}

# public json
location ~* \.json$ {
	expires 0;
	root /path/to/jschan/static/json;
	try_files $uri @backend;
}

# CSS
location ~* \.css$ {
	access_log off;
	expires 1w;
	root /path/to/jschan/static;
	try_files $uri =404;
}

# Scripts
location ~* \.js$ {
	expires 1w;
	access_log off;
	root /path/to/jschan/static;
	try_files $uri =404;
}

# Files (image, video, audio, other)
location ~* \.(png|jpg|jpeg|webmanifest|xml|ico|apng|bmp|webp|pjpeg|jfif|gif|mp4|webm|mov|mkv|svg|flac|mp3|m4a|ogg|wav|opus|ttf|woff2)$ {
	access_log off;
	expires max;
	root /path/to/jschan/static;
	try_files $uri =404;
}

# inline in browser so even HTML filetypes can be offered and will present a "save" dialog box
location ~* \.(txt|pdf|tgkr)$ {
	access_log off;
	expires max;
	add_header Cache-Control "public";
	add_header X-Content-Type-Options "nosniff" always;
	add_header Content-Disposition "attachment";
	root /path/to/jschan/static;
	try_files $uri =404;
}


code:
location / {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;

	proxy_set_header Host $host;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection 'upgrade';
	proxy_cache_bypass $http_upgrade;

	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'LOKI';
}

location @backend {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'LOKI';
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}

location @backend-private {
	include /etc/nginx/snippets/security_headers_nocache.conf;
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'LOKI';
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}


code:
location / {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header Host $host;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection 'upgrade';
	proxy_cache_bypass $http_upgrade;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'TOR';
}

location @backend {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'TOR';
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}

location @backend-private {
	include /etc/nginx/snippets/security_headers_nocache.conf;
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'TOR';
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}


code:
include /etc/nginx/snippets/security_headers_nocache.conf;
add_header Cache-Control "public";


code:
add_header Content-Security-Policy "default-src 'self'; media-src 'self' blob:; img-src 'self' blob:; object-src 'self' blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' https://www.youtube-nocookie.com/embed/ https://www.bitchute.com/embed/ https://odysee.com/%24/embed/; connect-src 'self' wss://example.com/ wss://www.example.com/ wss://www.example.onion/ wss://example.onion/ wss://www.example.loki/ wss://example.loki/; font-src 'self'" always;
add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin" always;
add_header X-Frame-Options "sameorigin" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;


code:
module.exports = {

	//mongodb connection string
	dbURL: 'mongodb://jschan:CHANGE-ME-YOUR-SECURE-MONGODB-PASSWORD@127.0.0.1:27017/jschan',

	//database name
	dbName: 'jschan',

	//redis connection info
	redis: {
		host: '127.0.0.1',
		port: '6379',
		password: 'CHANGE-ME-YOUR-SECURE-REDIS-PASSWORD'
	},

	//backend webserver port
	port: 7000,

	//secrets/salts for various things
	cookieSecret: 'changeme',
	tripcodeSecret: 'changeme',
	ipHashSecret: 'changeme',
	postPasswordSecret: 'changeme',

	//keys for google recaptcha
	google: {
		siteKey: 'changeme',
		secretKey: 'changeme'
	},

	//keys for hcaptcha
	hcaptcha: {
		siteKey: '10000000-ffff-ffff-ffff-000000000001',
		secretKey: '0x0000000000000000000000000000000000000000'
	},

	//keys for yandex smartcaptcha
	yandex: {
		siteKey: 'changeme',
		secretKey: 'changeme'
	},

	//enable debug logging
	debugLogs: true,

};


code:
module.exports = {

	globalAnnouncement: {
		markdown: '',
		raw: '',
	},

	secureCookies: true,

	//check referrer to prevent some CSRF attack
	refererCheck: false,

	//list of allowed host for checking referrer
	allowedHosts: [],

	//header for country codes
	countryCodeHeader: 'x-country-code',
	//header for visitor IP
	ipHeader: 'x-real-ip',

	//data used in opengraph meta tags. used to generate link previews in e.g. discord, twitter, etc
	meta: {
		siteName: '',
		url: ''
	},

	language: 'en-GB',

	//settings for captchas
	captchaOptions: {
		type: 'text',
		generateLimit: 250,
		font: 'default',
		text: {
			line: true,
			wave: 0,
			paint: 2,
			noise: 0,
		},
		grid: {
			falses: ['○','□','♘','♢','▽','△','♖','✧','♔','♘','♕','♗','♙','♧'],
			trues: ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣'],
			question: 'Select the solid/filled icons',
			size: 4,
			imageSize: 120,
			iconYOffset: 15,
			edge: 25,
			noise: 0,
		},
		numDistorts: {
			min: 2,
			max: 3
		},
		distortion: 7,
	},

	/* dnsbl, will add a small delay for uncached requests. You could also install some
		kind of dns cache e.g. unbound to improve performance. DNSBL only checked for posting */
	dnsbl: {
		enabled: false,
		blacklists: ['tor.dan.me.uk'],
		cacheTime: 3600 //in seconds, idk whats a good value
	},

	//disable file posting over anonymizers globally, overrides any board setting.
	disableAnonymizerFilePosting: false,

	//count anon posters (e.g. loki, tor .onion) as "users" in stats. if set to false, all .onion is counted as a single user. doesnt affect pph stat.
	statsCountAnonymizers: true,

	floodTimers: { //basic delays to stop flooding, in ms. 0 to disable
		sameContentSameIp: 120000, //same message or any file from same ip
		sameContentAnyIp: 30000, //same message or file from any ip
		anyContentSameIp: 5000, //any post from the same ip
	},

	//block bypasses
	blockBypass: {
		enabled: false,
		forceAnonymizers: true,
		expireAfterUses: 50, //however many (attempted) posts per block bypass captcha
		expireAfterTime: 86400000, //expiry in ms regardless if the limit was reached, default 1 day
		bypassDnsbl: false,
	},

	/* delete files immediately rather than pruning later. usually disabled to prevent re-thumbnailing and processing commonly
		uploaded files, but deleting immediately is better if you are concerned about "deleted" content not being immediately removed */
	pruneImmediately: true,

	//do not use
	hashImages: false,

	rateLimitCost: { //Cost out of 100 per minute e.g. cost of 25 means 4 per minute. Cost is separate for each.
		captcha: 10,
		boardSettings: 30,
		editPost: 30,
	},

	//how many threads to show on overboard index view
	overboardLimit: 20,
	//how many threads to show on overboard catalog view
	overboardCatalogLimit: 100,
	//allow custom overboard (user pick what boards to show)
	allowCustomOverboard: true,
	//whether to show reverse links on overboard
	overboardReverseLinks: true,

	//how many hot threads show on homepage. 0 disabled
	hotThreadsLimit: 5,
	//how many replies the thread needs to be "hot"
	hotThreadsThreshold: 10,
	//max age of a hot thread, with a reducing score multiplier from 0-1 bias towards this date
	hotThreadsMaxAge: 2629800000,

	//default url format/links for archive, reverse image search, and ethereum links, %s will be replaced by the url
	archiveLinksURL: 'https://archive.today/?run=1&url=%s',
	reverseImageLinksURL: 'https://tineye.com/search?url=%s',
	ethereumLinksURL: 'https://etherscan.io/address/%s',

	//cache templates in memory. disable only if editing templates and doing dev work
	cacheTemplates: true,

	//max wait time in ms for obtaining locks for saving files
	lockWait: 3000,

	//optionally prune modlog entries older than x days
	pruneModlogs: 30,

	//option to prune ips on posts older than x days
	pruneIps: 0,

	//dont store raw ips at all (in jschan), not counting webservers or whatever else.
	dontStoreRawIps: false,

	//enable the webring (also copy configs/webring.json.example -> configs/webring.json and edit)
	enableWebring: false,

	//web3
	enableWeb3: false,
	ethereumNode: '',

	//extension for thumbnails
	thumbExtension: '.webp',
	//whether to animate gif thumbnails
	animatedGifThumbnails: false,
	//generate waveform thumbnails for audio
	audioThumbnails: true,
	/* if animatedGifThumbnails is true, use ffmpeg for better animated gif thumbnailing. Recommended.
		requires ffmpeg >=4.3.x. You can install from debian testing repo, or compile from source. */
	ffmpegGifThumbnails: true,

	//max thumb dimensions (square) in px
	thumbSize: 250,

	//0-100 percent through a video to try taking video thumbnails from, to remedy black first frames or fade-ins
	videoThumbPercentage: 5,

	/* extra mime types for files to be uploaded as attachments (no thumbnails) e.g. text files/archives
		NOTE: appropriate extensions will need to be added to nginx configuration, and uncommend the provided
		"other files" section which includes an example configuration for .txt files to match this default config.
		mime types and file extention does not always correspond exactly this cant be done automatically. */
	otherMimeTypes: [
		'text/plain',
		'application/pdf',
		'tegaki/replay',
	],

	//check the real mime type of uploaded files
	checkRealMimeTypes: false,
	//if checking real mime types, and the real type is unknown, allow it anyway
	allowMimeNoMatch: false,

	//default ban duration in ms if ban duration field is left blank (default value is 1 year)
	defaultBanDuration: 31536000000,

	//max number of quotes that will be linked in a post. 0 for unlimited (not recommended)
	quoteLimit: 25,

	//how many replies to show on index pages under each OP
	previewReplies: 5,
	stickyPreviewReplies: 5, //choose a different amount for sticky posts if desired

	/* the fraction of threadLimit beyond which early404 prunes posts
		e.g. 3 means after the first third, so if you had 6 pages,
		anything after page 2 with less than early404Replies gets pruned */
	early404Fraction: 3,
	//how many replies a thread needs to not get removed by early404
	early404Replies: 5,

	//how many of the most recent newsposts to show on the homepage
	maxRecentNews: 5,

	//time for an account to be considered inactive
	inactiveAccountTime: 7889400000,
	//action for inactive accounts: nothing, forfeit staff positions, delete account
	inactiveAccountAction: 0,

	//action for handling abandoned boards (no board owner): nothing, lock, lock + unlist, delete board
	abandonedBoardAction: 0,

	/* filter filenames on posts and banners
		false=no filtering
		true=allow only A-Za-z0-9_-
		regex=custom regex of what to replace e.g. /[^\w\s-]+/g */
	filterFileNames: false,
	/* replace spaces and multiple spaces with some character default _ for better filenames
		(spaces dont belong in filenames) */
	spaceFileNameReplacement: '_',

	uriDecodeFileNames: false,

	//options for code block highlighting in posts
	highlightOptions: {

		//subset of languages to allow.
		languageSubset: [
			'javascript',
			'typescript',
			'perl',
			'js',
			'c++',
			'c',
			'java',
			'kotlin',
			'php',
			'h',
			'csharp',
			'bash',
			'sh',
			'zsh',
			'python',
			'ruby',
			'css',
			'html',
			'json',
			'golang',
			'rust',
			'aa',
		],

		//threshold below which auto language is ignored
		threshold: 5

	},

	//uses names of css in gulp/res/css. if blank, all themes are enabled.
	themes: [],
	codeThemes: [],

	//global limits for board settings
	globalLimits:  {
		threadLimit: { //number of threads, 10 per page
			min: 10,
			max: 200
		},
		replyLimit: { //number of replies to a thread, thread is locked after this limit is reached
			min: 10,
			max: 1000
		},
		bumpLimit: { //number of replies to a thread before it wont get bumped anymore
			min: 10,
			max: 1000
		},
		postFiles: { //number of files in a post
			max: 5,
		},
		postFilesSize: {
			max: 10485760, //in bytes, 10MB default
			//width*height max number, 10000*10000 default, since square vs very wids vs very tall doesnt matter,
			//just the raw number of pixels e.g. 4K 3840x2160=77414400, so we give PLENTY of headroom
			imageResolution: 100000000,
			videoResolution: 77414400,
		},
		bannerFiles: {
			width: 300, //banner image max width in px
			height: 100, //banner image max height in px
			forceAspectRatio: true, //enforce 3:1 aspect ratio
			max: 10, //number of banners uploadable in one request
			total: 100, //max number of banners for a board
		},
		flagFiles: {
			max: 10, //number of banners uploadable in one request
			total: 100, //max number of flags for a board
		},
		assetFiles: {
			max: 10, //number of assets uploadable in one request
			total: 50, //max number of assets for a board
		},
		bannerFilesSize: { //in bytes, 10MB default
			max: 10485760
		},
		assetFilesSize: { //in bytes, 10MB default
			max: 10485760
		},
		flagFilesSize: { //in bytes, 1MB default
			max: 1048576
		},
		/* NOTE: xFilesSize in bytes the amount of total data in form submission including
			other fields like message, name, etc. Therefore a very long message would reduce the space left for files very slightly.
			To counteract this, consider increasing them beyond your desired max filesize by a small margin */
		fieldLength: { //max length of fields in some forms
			//post form
			name: 100,
			email: 100,
			subject: 100,
			postpassword: 100,
			message: 20000,
			//reports/post actions
			report_reason: 100,
			ban_reason: 100,
			log_message: 100,
			//board creation
			uri: 50,
			boardname: 50,
			description: 100,
		},
		multiInputs: {
			//limits on certain requests that have multi-select. set any to 0 for unlimited
			posts: { //post actions (report, delete, spoiler, ban, etc).
				anon: 20, //separate for non-staff to prevent performance issues or abuse e.g. user mass self-deleting or reporting posts with single captcha
				staff: 100, //but to still allow mass moderation for staff
			},
			//note: room for more limits here if they are needed for other checkbox forms in future
		},
		customCss: {
			enabled: true, //allow custom css by board owners
			max: 10000, //max number of characters to allow
			strict: true, //enables filters to block certain strings in custom css
			filters: [
				'@',
				'url(',
			]
		},
		customPages: {
			max: 10, //max number of custom pages per board
			maxLength: 10000, //max number of characters to allow per page
		},
		filters: {
			max: 50,
		}
	},

	//default frontend script settings
	frontendScriptDefault: {
		embedsEnabled: true, //show [embed] toggle next to embeddable video links
		heightUnlimit: false, //allow images taller than window height
		hideRecursive: true, //hide replies to hidden OPs
		crispImages: false, //use crisp/pixelated image rendering
		hideThumbnails: false, //hide thumbnails
		nonColorIds: false, //dont color ids
		alwaysShowSpoilers: false , //always reveal spoiler text
		hidePostStubs: false, //hide hidden posts completely, dont even show it minimized
		smoothScrolling: true, //smooth scrolling. annoying for chrome users on long threads
		threadWatcher: true, //make threadwatcher visible
		defaultVolume: 100, //default video/audio volume
		loop: true, //looping video/audio
		imageLoadingBars: true, //show loading bar for images, changes behaviour of right click save/open
		live: true, //update threads automatically
		scrollToPosts: false, //scroll to new posts
		localTime: true, //show local time for posts
		hour24Time: false, //show 24 hour time for posts
		relativeTime: true, //show relative time i.e. "5 minutes ago" for posts
		//note: relative time overrides other time options, they show on hover in the tooltip/"title" property
		notificationsEnabled: false, //show notifications for new posts in a thread
		notificationsYousOnly: true, //only show notifications for posts that quote you
		showYous: true, //show (You) next to name on your posts, and quotes linking to your post
		hideDeletedPostContent: false, //hide the message and files from marked deleted posts from websocket
		tegakiWidth: 500, //default canvas size for tegaki drawings
		tegakiHeight: 500,
	},

	//default board settings when a board is created
	boardDefaults: {
		language: 'en-GB',
		theme: 'yotsuba-b',
		codeTheme: 'ir-black',
		sfw: false, //safe for work board
		lockMode: 0, //board lock mode
		fileR9KMode: 0, //enfore unique files, 0=off, 1=per thread, 2=whole board
		messageR9KMode: 0, //enforce unique message, 0=off, 1=per thread, 2=whole board
		unlistedLocal: false, //board hidden from on-site board list and frontpage
		unlistedWebring: false, //board hidden from webring
		captchaMode: 0, //0=disabled, 1=for threads, 2=for all posts
		tphTrigger: 10, //numebr of threads in an hour before trigger action is activated
		pphTrigger: 50, //number of posts in an hour before ^
		//0=none, 1=captcha enable for threads, 2=captcha enable for all posts, 3=lock thread creation, 4=lock board
		tphTriggerAction: 1,
		pphTriggerAction: 2,
		//0=dont change, 1=unlock board/disable captcha, 2=lock thread creation/enable captcha for thread creation
		captchaReset: 0,
		lockReset: 0,
		forceAnon: false, //disable name and subject, only allow sage email
		sageOnlyEmail: false, //only allow sage email
		early404: true, //delete threads beyond the first 1/3 of pages with less than 5 replies
		ids: false, //show per thread poster ID based on ip
		geoFlags: false, //show geo flags, requires nginx setup
		customFlags: false, //show custom flags
		enableTegaki: true,
		enableWeb3: false,
		userPostDelete: true, //allow users to delete their posts
		userPostSpoiler: true, //allow user to spoiler their post files
		userPostUnlink: true, //alow user to unlink files fomr their post
		reverseImageSearchLinks: false, //show imgops or similar
		archiveLinks: false, //show external archive link
		threadLimit: 200,
		replyLimit: 1000,
		bumpLimit: 500,
		maxFiles: 5,
		forceReplyMessage: false,
		forceReplyFile: false,
		forceThreadMessage: false,
		forceThreadFile: false,
		forceThreadSubject: false,
		disableReplySubject: false,
		hideBanners: false,
		minThreadMessageLength: 0,
		minReplyMessageLength: 0,
		maxThreadMessageLength: 20000,
		maxReplyMessageLength: 20000,
		defaultName: 'Anon',
		customCSS: null,
		blockedCountries: [], //2 char ISO country codes to block
		disableAnonymizerFilePosting: false,
		deleteProtectionAge: 0, //prevent non-staff OP from deleting their thread if it older than this age in ms
		deleteProtectionCount: 0, //prevent non-staff op deleting their thread if it has more than this many replies
		announcement: {
			raw: null,
			markdown: null
		},
		allowedFileTypes: { //enable different types of files to be posted
			animatedImage: true,
			image: true,
			video: true,
			audio: true,
			other: false
		}
	},

	following: [],
	blacklist: [],
	logo: [],
	proxy: {
		enabled: false,
		address: ''
	}

};


code:
'use strict';

const express  = require('express')
	, router = express.Router({ caseSensitive: true })
	, Boards = require(__dirname+'/../db/boards.js')
//middlewares
	, geoIp = require(__dirname+'/../lib/middleware/ip/geoip.js')
	, processIp = require(__dirname+'/../lib/middleware/ip/processip.js')
	, calcPerms = require(__dirname+'/../lib/middleware/permission/calcpermsmiddleware.js')
	, { Permissions } = require(__dirname+'/../lib/permission/permissions.js')
	, hasPerms = require(__dirname+'/../lib/middleware/permission/haspermsmiddleware.js')
	, numFiles = require(__dirname+'/../lib/middleware/file/numfiles.js')
	, imageHashes = require(__dirname+'/../lib/middleware/file/imagehash.js')
	, banCheck = require(__dirname+'/../lib/middleware/permission/bancheck.js')
	, isLoggedIn = require(__dirname+'/../lib/middleware/permission/isloggedin.js')
	, verifyCaptcha = require(__dirname+'/../lib/middleware/captcha/verify.js')
	, csrf = require(__dirname+'/../lib/middleware/misc/csrfmiddleware.js')
	, useSession = require(__dirname+'/../lib/middleware/permission/usesession.js')
	, sessionRefresh = require(__dirname+'/../lib/middleware/permission/sessionrefresh.js')
	, dnsblCheck = require(__dirname+'/../lib/middleware/ip/dnsbl.js')
	, blockBypass = require(__dirname+'/../lib/middleware/captcha/blockbypass.js')
	, fileMiddlewares = require(__dirname+'/../lib/middleware/file/filemiddlewares.js')
	, { setBoardLanguage, setQueryLanguage } = require(__dirname+'/../lib/middleware/locale/locale.js')
//controllers
	, { deleteBoardController, editBansController, appealController, globalActionController, twofactorController,
		actionController, addCustomPageController, deleteCustomPageController, addNewsController,
		editNewsController, deleteNewsController, uploadBannersController, deleteBannersController, addFlagsController,
		deleteFlagsController, boardSettingsController, transferController, addAssetsController, deleteAssetsController,
		resignController, deleteAccountController, loginController, registerController, changePasswordController,
		deleteAccountsController, editAccountController, addFilterController, editFilterController, deleteFilterController,
		globalSettingsController, createBoardController, makePostController, addStaffController, deleteStaffController,
		editStaffController, editCustomPageController, editPostController, editRoleController, newCaptchaForm,
		blockBypassForm, logoutForm, deleteSessionsController, globalClearController } = require(__dirname+'/forms/index.js');

//make new post
router.post('/board/:board/post', geoIp, processIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, calcPerms, banCheck, fileMiddlewares.posts,
	makePostController.paramConverter, verifyCaptcha, numFiles, blockBypass.middleware, dnsblCheck, imageHashes, makePostController.controller);
router.post('/board/:board/modpost', geoIp, processIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, calcPerms, banCheck, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), fileMiddlewares.posts, makePostController.paramConverter, csrf, numFiles, blockBypass.middleware, dnsblCheck, imageHashes, makePostController.controller); //mod post has token instead of captcha

//post actions
router.post('/board/:board/actions', geoIp, processIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, calcPerms, banCheck, actionController.paramConverter, verifyCaptcha, actionController.controller); //public, with captcha
router.post('/board/:board/modactions', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, banCheck, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), actionController.paramConverter, actionController.controller); //board manage page

router.post('/global/actions', geoIp, processIp, useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_GENERAL), globalActionController.paramConverter, globalActionController.controller); //global manage page

//appeal ban
router.post('/appeal', geoIp, processIp, useSession, sessionRefresh, appealController.paramConverter, verifyCaptcha, appealController.controller);
//edit post
router.post('/editpost', geoIp, processIp, useSession, sessionRefresh, csrf, editPostController.paramConverter, Boards.bodyExists, setBoardLanguage, calcPerms,
	hasPerms.any(Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_BOARD_GENERAL), editPostController.controller);

//board management forms
router.post('/board/:board/transfer', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.any(Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_GLOBAL_BOARDS), transferController.paramConverter, transferController.controller);
router.post('/board/:board/settings', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), boardSettingsController.paramConverter, boardSettingsController.controller);
router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_BANS), editBansController.paramConverter, editBansController.controller); //edit bans
router.post('/board/:board/addfilter', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), addFilterController.paramConverter, addFilterController.controller); //add new filter
router.post('/board/:board/editfilter', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), editFilterController.paramConverter, editFilterController.controller); //edit filter
router.post('/board/:board/deletefilter', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), deleteFilterController.paramConverter, deleteFilterController.controller); //delete filter
router.post('/board/:board/deleteboard', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.any(Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_GLOBAL_BOARDS), deleteBoardController.controller); //delete board

//board crud banners, flags, assets, custompages
router.post('/board/:board/addbanners', geoIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, fileMiddlewares.banner, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), numFiles, uploadBannersController.controller); //add banners
router.post('/board/:board/deletebanners', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), deleteBannersController.paramConverter, deleteBannersController.controller); //delete banners
router.post('/board/:board/addassets', geoIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, fileMiddlewares.asset, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), numFiles, addAssetsController.controller); //add assets
router.post('/board/:board/deleteassets', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), deleteAssetsController.paramConverter, deleteAssetsController.controller); //delete assets
router.post('/board/:board/addflags', geoIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, fileMiddlewares.flag, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), numFiles, addFlagsController.controller); //add flags
router.post('/board/:board/deleteflags', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), deleteFlagsController.paramConverter, deleteFlagsController.controller); //delete flags
router.post('/board/:board/addcustompages', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), addCustomPageController.paramConverter, addCustomPageController.controller); //add custom pages
router.post('/board/:board/deletecustompages', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), deleteCustomPageController.paramConverter, deleteCustomPageController.controller); //delete custom pages
router.post('/board/:board/editcustompage', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), editCustomPageController.paramConverter, editCustomPageController.controller); //edit custom page
router.post('/board/:board/addstaff', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_STAFF), addStaffController.paramConverter, addStaffController.controller); //add board staff
router.post('/board/:board/editstaff', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_STAFF), editStaffController.paramConverter, editStaffController.controller); //edit staff permission
router.post('/board/:board/deletestaff', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_BOARD_STAFF), deleteStaffController.paramConverter, deleteStaffController.controller); //delete board staff

//global management forms
router.post('/global/editbans', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_BANS), editBansController.paramConverter, editBansController.controller); //remove bans
router.post('/global/deleteboard', useSession, sessionRefresh, csrf, deleteBoardController.paramConverter, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_BOARDS), deleteBoardController.controller); //delete board from global management panel
router.post('/global/addnews', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_NEWS), addNewsController.paramConverter, addNewsController.controller); //add new newspost
router.post('/global/editnews', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_NEWS), editNewsController.paramConverter, editNewsController.controller); //add new newspost
router.post('/global/deletenews', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_NEWS), deleteNewsController.paramConverter, deleteNewsController.controller); //delete news
router.post('/global/deleteaccounts', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_ACCOUNTS), deleteAccountsController.paramConverter, deleteAccountsController.controller); //account deleting
router.post('/global/editaccount', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_ACCOUNTS), editAccountController.paramConverter, editAccountController.controller); //account editing
router.post('/global/editrole', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_ROLES), editRoleController.paramConverter, editRoleController.controller); //role editing
router.post('/global/addfilter', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_SETTINGS), addFilterController.paramConverter, addFilterController.controller); //add new filter
router.post('/global/editfilter', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_SETTINGS), editFilterController.paramConverter, editFilterController.controller); //edit filter
router.post('/global/deletefilter', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_SETTINGS), deleteFilterController.paramConverter, deleteFilterController.controller); //delete filter
router.post('/global/settings', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_SETTINGS), globalSettingsController.paramConverter, globalSettingsController.controller); //global settings
router.post('/global/clear', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
	hasPerms.one(Permissions.MANAGE_GLOBAL_SETTINGS), globalClearController.paramConverter, globalClearController.controller); //global clear

//create board
router.post('/create', geoIp, processIp, useSession, sessionRefresh, isLoggedIn, calcPerms, verifyCaptcha, createBoardController.paramConverter, createBoardController.controller);

//accounts
router.post('/login', useSession, loginController.paramConverter, loginController.controller);
router.post('/twofactor', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, twofactorController.paramConverter, twofactorController.controller);
router.post('/logout', useSession, logoutForm);
router.post('/register', geoIp, processIp, useSession, sessionRefresh, calcPerms, verifyCaptcha, registerController.paramConverter, registerController.controller);
router.post('/changepassword', geoIp, processIp, useSession, sessionRefresh, verifyCaptcha, changePasswordController.paramConverter, changePasswordController.controller);
router.post('/resign', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, resignController.paramConverter, resignController.controller);
router.post('/deleteaccount', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, deleteAccountController.controller);
router.post('/deletesessions', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, deleteSessionsController.paramConverter, deleteSessionsController.controller);

//removes captcha cookie, for refreshing for noscript users
router.post('/newcaptcha', newCaptchaForm);
//solve captcha for block bypass
router.post('/blockbypass', geoIp, processIp, useSession, sessionRefresh, calcPerms, setQueryLanguage, verifyCaptcha, blockBypassForm);

module.exports = router;


code:
'use strict';

const { Posts, Boards } = require(__dirname+'/../../db/')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, actionHandler = require(__dirname+'/../../models/forms/actionhandler.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, actionChecker = require(__dirname+'/../../lib/input/actionchecker.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		timeFields: ['ban_duration'],
		trimFields: ['postpassword', 'report_reason', 'ban_reason', 'log_message', 'move_to_board'],
		allowedArrays: ['checkedreports', 'checkedposts'],
		numberFields: ['move_to_thread', 'sticky'],
		numberArrays: ['checkedposts'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { globalLimits } = config.get;

		res.locals.actions = actionChecker(req, res);

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedposts, 1), expected: false, blocking: true, error: __('Must select at least one post') },
			{ result: lengthBody(res.locals.actions.validActions, 1), expected: false, blocking: true, error: __('No actions selected') },
			{ result: lengthBody(req.body.checkedposts, 1, globalLimits.multiInputs.posts.anon), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: __('Must not select >%s posts per request', globalLimits.multiInputs.posts.anon) },
			{ result: lengthBody(req.body.checkedposts, 1, globalLimits.multiInputs.posts.staff), expected: false, error: __('Must not select >%s posts per request', globalLimits.multiInputs.posts.staff) },
			{ result: (existsBody(req.body.report_ban) && !req.body.checkedreports), expected: false, error: __('Must select post and reports to ban reporter') },
			{ result: (existsBody(req.body.checkedreports) && !req.body.report_ban), expected: false, error: __('Must select a report action if checked reports') },
			{ result: (existsBody(req.body.checkedreports) && !req.body.checkedposts), expected: false, error: __('Must check parent post if checking reports for report action') },
			{ result: (existsBody(req.body.checkedreports) && existsBody(req.body.checkedposts) && lengthBody(req.body.checkedreports, 1, req.body.checkedposts.length*5)), expected: false, error: __('Invalid number of reports checked') },
			{ result: res.locals.actions.hasPermission, expected: true, blocking: true, error: __('No permission') },
			{ result: (existsBody(req.body.delete) && !res.locals.board.settings.userPostDelete), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: __('User post deletion is disabled on this board') },
			{ result: (existsBody(req.body.spoiler) && !res.locals.board.settings.userPostSpoiler), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: __('User file spoiling is disabled on this board') },
			{ result: (existsBody(req.body.unlink_file) && !res.locals.board.settings.userPostUnlink), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: __('User file unlinking is disabled on this board') },
			{ result: (existsBody(req.body.edit) && lengthBody(req.body.checkedposts, 1, 1)), expected: false, error: __('Must select only 1 post for edit action') },
			{ result: lengthBody(req.body.postpassword, 0, globalLimits.fieldLength.postpassword), expected: false, error: __('Password must be %s characters or less', globalLimits.fieldLength.postpassword) },
			{ result: lengthBody(req.body.report_reason, 0, globalLimits.fieldLength.report_reason), expected: false, error: __('Report must be %s characters or less', globalLimits.fieldLength.report_reason) },
			{ result: lengthBody(req.body.ban_reason, 0, globalLimits.fieldLength.ban_reason), expected: false, error: __('Ban reason must be %s characters or less', globalLimits.fieldLength.ban_reason) },
			{ result: lengthBody(req.body.log_message, 0, globalLimits.fieldLength.log_message), expected: false, error: __('Modlog message must be %s characters or less', globalLimits.fieldLength.log_message) },
			{ result: (existsBody(req.body.report || req.body.global_report) && lengthBody(req.body.report_reason, 1)), expected: false, blocking: true, error: __('Reports must have a reason') },
			{ result: (existsBody(req.body.move) && (!req.body.move_to_thread && !req.body.move_to_board)), expected: false, error: __('Must input destinaton thread number or board to move posts') },
			{ result: async () => {
				if (req.body.move && req.body.move_to_thread) {
					const moveBoard = req.body.move_to_board || req.params.board;
					res.locals.destinationThread = await Posts.getPost(moveBoard, req.body.move_to_thread);
					return res.locals.destinationThread != null;
				}
				return true;
			}, expected: true, error: __('Destination for move does not exist') },
			{ result: async () => {
				if (req.body.move && req.body.move_to_board
					&& req.body.move_to_board !== req.params.board) {
					if (!res.locals.user || !res.locals.user.username) {
						return false;
					}

					const destinationBoard = await Boards.findOne(req.body.move_to_board);
					if (res.locals.permissions.get(Permissions.MANAGE_GLOBAL_GENERAL)
						|| (res.locals.permissions.get(Permissions.MANAGE_BOARD_GENERAL)
							&& destinationBoard && destinationBoard.staff[res.locals.user.username] != null)) {
						res.locals.destinationBoard = destinationBoard;
					}
					return res.locals.destinationBoard != null;
				}
				return true;
			}, expected: true, error: __('Destination for move does not exist, or you do not have permission') },
		], res.locals.permissions);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.board}/`
			});
		}

		try {
			res.locals.posts = await Posts.getPosts(req.params.board, req.body.checkedposts, true);
		} catch (err) {
			return next(err);
		}

		if (!res.locals.posts || res.locals.posts.length === 0) {
			return dynamicResponse(req, res, 404, 'message', {
				'title': __('Not found'),
				'error': __('Selected posts not found'),
				'redirect': `/${req.params.board}/`
			});
		}

		if (req.body.edit) {
			//edit post only allows single post
			return res.redirect(`/${res.locals.posts[0].board}/manage/editpost/${res.locals.posts[0].postId || res.locals.posts[0].thread}.html`);
		} else if (req.body.move) {
			if (!res.locals.destinationBoard && !res.locals.destinationThread) {
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Bad Request'),
					'error': __('Invalid post move destination'),
					'redirect': `/${req.params.board}/`
				});
			}
			if (req.body.move_to_thread
				&& (!req.body.move_to_board || req.body.move_to_board === req.params.board)) {
				//If moving to thread on the same board, filter posts that are already in the destination thread
				res.locals.posts = res.locals.posts.filter(p => {
					return p.postId !== req.body.move_to_thread && p.thread !== req.body.move_to_thread;
				});
				if (res.locals.posts.length === 0) {
					return dynamicResponse(req, res, 409, 'message', {
						'title': __('Conflict'),
						'error': __('Invalid selected posts or destination thread'),
						'redirect': `/${req.params.board}/`
					});
				}
			}
		}

		try {
			await actionHandler(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const addAssets = require(__dirname+'/../../models/forms/addassets.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, deleteTempFiles = require(__dirname+'/../../lib/file/deletetempfiles.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, { checkSchema, numberBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	//paramConverter: paramConverter({}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { globalLimits, disableAnonymizerFilePosting } = config.get;

		const errors = await checkSchema([
			{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: __('Must provide a file') },
			{ result: (res.locals.anonymizer && disableAnonymizerFilePosting && !res.locals.permissions.get(Permissions.BYPASS_ANONYMIZER_RESTRICTIONS)), expected: false, error: __('Posting files through anonymizers has been disabled globally') },
			{ result: numberBody(res.locals.numFiles, 0, globalLimits.assetFiles.max), expected: true, error: __('Exceeded max asset uploads in one request of %s', globalLimits.assetFiles.max) },
			{ result: numberBody(res.locals.board.assets.length+res.locals.numFiles, 0, globalLimits.assetFiles.total), expected: true, error: __('Total number of assets would exceed global limit of %s', globalLimits.assetFiles.total) },
		]);

		if (errors.length > 0) {
			await deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.board}/manage/assets.html`
			});
		}

		try {
			await addAssets(req, res, next);
		} catch (err) {
			await deleteTempFiles(req).catch(console.error);
			return next(err);
		}

	}

};


code:
'use strict';

const addCustomPage = require(__dirname+'/../../models/forms/addcustompage.js')
	, { CustomPages } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody, numberBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		trimFields: ['message', 'title', 'page'],
		processMessageLength: true,
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { globalLimits } = config.get;

		const errors = await checkSchema([
			{ result: existsBody(req.body.message), expected: true, error: __('Missing message') },
			{ result: existsBody(req.body.title), expected: true, error: __('Missing title' ) },
			{ result: existsBody(req.body.page), expected: true, error: __('Missing .html name') },
			{ result: () => {
				if (req.body.page) {
					return /^[a-z0-9_-]+$/i.test(req.body.page);
				}
				return false;
			} , expected: true, error: __('.html name must contain a-z 0-9 _ - only') },
			{ result: numberBody(res.locals.messageLength, 0, globalLimits.customPages.maxLength), expected: true, error: __('Message must be %s characters or less', globalLimits.customPages.maxLength) },
			{ result: lengthBody(req.body.title, 0, 50), expected: false, error: __('Title must be 50 characters or less') },
			{ result: lengthBody(req.body.page, 0, 50), expected: false, error: __('.html name must be 50 characters or less') },
			{ result: async () => {
				return (await CustomPages.boardCount(req.params.board)) > globalLimits.customPages.max;
			}, expected: false, error: __('Can only create %s pages per board', globalLimits.customPages.max) },
			{ result: async () => {
				return (await CustomPages.findOne(req.params.board, req.body.page)) == null;
			}, expected: true, error: __('.html name must be unique') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.booard}/manage/custompages.html`
			});
		}

		try {
			await addCustomPage(req, res, next);
		} catch (err) {
			return next(err);
		}

	}
};


code:
'use strict';

const addFilter = require(__dirname+'/../../models/forms/addfilter.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody, numberBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		timeFields: ['filter_ban_duration'],
		trimFields: ['filters', 'filter_message'],
		numberFields: ['filter_mode'],
		objectIdFields: ['filter_id'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.filters, 0, 50000), expected: false, error: __('Filter text cannot exceed 50000 characters') },
			{ result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: __('Filter mode must be a number from 0-2') },
			{ result: numberBody(req.body.filter_ban_duration), expected: true, error: __('Invalid filter auto ban duration') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': req.params.board ? `/${req.params.board}/manage/filters.html` : '/globalmanage/filters.html'
			});
		}

		try {
			await addFilter(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const addFlags = require(__dirname+'/../../models/forms/addflags.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, deleteTempFiles = require(__dirname+'/../../lib/file/deletetempfiles.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, { checkSchema, numberBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	//paramConverter: paramConverter({}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { globalLimits, disableAnonymizerFilePosting } = config.get;

		const errors = await checkSchema([
			{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: __('Must provide a file') },
			{ result: (res.locals.anonymizer && disableAnonymizerFilePosting && !res.locals.permissions.get(Permissions.BYPASS_ANONYMIZER_RESTRICTIONS)), expected: false, error: __('Posting files through anonymizers has been disabled globally') },
			{ result: numberBody(res.locals.numFiles, 0, globalLimits.flagFiles.max), expected: true, error: __('Exceeded max flag uploads in one request of %s', globalLimits.flagFiles.max) },
			{ result: numberBody(Object.keys(res.locals.board.flags).length+res.locals.numFiles, 0, globalLimits.flagFiles.total), expected: true, error: __('Total number of flags would exceed global limit of %s', globalLimits.flagFiles.total) },
		]);

		if (errors.length > 0) {
			await deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.board}/manage/assets.html`
			});
		}

		try {
			await addFlags(req, res, next);
		} catch (err) {
			await deleteTempFiles(req).catch(console.error);
			return next(err);
		}

	}

};


code:
'use strict';

const addNews = require(__dirname+'/../../models/forms/addnews.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody, numberBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		trimFields: ['message', 'title'],
		processMessageLength: true,
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { globalLimits } = config.get;

		const errors = await checkSchema([
			{ result: existsBody(req.body.message), expected: true, error: __('Missing message') },
			{ result: existsBody(req.body.title), expected: true, error: __('Missing title') },
			{ result: numberBody(res.locals.messageLength, 0, globalLimits.fieldLength.message), expected: true, error: __('Message must be %s characters or less', globalLimits.fieldLength.message) },
			{ result: lengthBody(req.body.title, 0, 50), expected: false, error: __('Title must be 50 characters or less') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': '/globalmanage/news.html'
			});
		}

		try {
			await addNews(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const addStaff = require(__dirname+'/../../models/forms/addstaff.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, { Accounts } = require(__dirname+'/../../db/')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		trimFields: ['username'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: existsBody(req.body.username), expected: true, error: __('Missing staff username') },
			{ result: lengthBody(req.body.username, 0, 50), expected: false, error: __('Username must be 50 characters or less') },
			{ result: (res.locals.board.owner === req.body.username), expected: false, blocking: true, error: __('User is already board owner') },
			{ result: (res.locals.board.staff[req.body.username] != null), expected: false, blocking: true, error: __('User is already staff') },
			{ result: async () => {
				const numAccounts = await Accounts.countUsers([req.body.username]);
				return numAccounts > 0;
			}, expected: true, error: __('User does not exist') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
			});
		}

		try {
			await addStaff(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const appealBans = require(__dirname+'/../../models/forms/appeal.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, numberBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		trimFields: ['message'],
		allowedArrays: ['checkedbans'],
		processMessageLength: true,
		objectIdArrays: ['checkedbans']
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { globalLimits } = config.get;

		const errors = await checkSchema([
			{ result: existsBody(req.body.message), expected: true, error: __('Appeals must include a message') },
			{ result: existsBody(req.body.checkedbans), expected: true, error: __('Must select at least one ban to appeal') },
			{ result: numberBody(res.locals.messageLength, 0, globalLimits.fieldLength.message), expected: true, error: __('Appeal message must be %s characters or less', globalLimits.fieldLength.message) },
		]); //should appeals really be based off message field length global limit? minor.

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': '/'
			});
		}

		let amount = 0;
		try {
			amount = await appealBans(req, res, next);
		} catch (err) {
			return next(err);
		}

		if (amount === 0) {
			/* this can occur if they selected invalid id, non-ip match, already appealed, or unappealable bans. prevented by databse filter, so we use
				use the updatedCount return value to check if any appeals were made successfully. if not, we end up here. */
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'error': __('Invalid bans selected'),
				'redirect': '/'
			});
		}

		return dynamicResponse(req, res, 200, 'message', {
			'title': __('Success'),
			'message': __('Appealed %s bans successfully', amount),
			'redirect': '/'
		});

	}

};


code:
'use strict';

const changeBoardSettings = require(__dirname+'/../../models/forms/changeboardsettings.js')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, { themes, codeThemes } = require(__dirname+'/../../lib/misc/themes.js')
	, { Ratelimits } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, i18n = require(__dirname+'/../../lib/locale/locale.js')
	, { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
		inArrayBody, arrayInBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		timeFields: ['delete_protection_age'],
		trimFields: ['tags', 'announcement', 'description', 'name', 'custom_css', 'language'],
		allowedArrays: ['countries'],
		numberFields: ['lock_reset', 'captcha_reset', 'lock_mode', 'message_r9k_mode', 'file_r9k_mode', 'captcha_mode', 'tph_trigger', 'pph_trigger', 'pph_trigger_action',
			'tph_trigger_action', 'bump_limit', 'reply_limit', 'max_files', 'thread_limit', 'max_thread_message_length', 'max_reply_message_length', 'min_thread_message_length',
			'min_reply_message_length', 'delete_protection_count'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { globalLimits, rateLimitCost } = config.get
			, maxThread = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength) || globalLimits.fieldLength.message)
			, maxReply = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) || globalLimits.fieldLength.message);

		const errors = await checkSchema([
			{ result: lengthBody(req.body.description, 0, globalLimits.fieldLength.description), expected: false, error: __('Board description must be %s characters or less', globalLimits.fieldLength.description) },
			{ result: lengthBody(req.body.announcements, 0, 5000), expected: false, error: __('Board announcements must be 5000 characters or less') },
			{ result: lengthBody(req.body.tags, 0, 2000), expected: false, error: __('Tags length must be 2000 characters or less') },
			{ result: lengthBody(req.body.custom_css, 0, globalLimits.customCss.max), expected: false, error: __('Custom CSS must be %s characters or less', globalLimits.customCss.max) },
			{ result: arrayInBody(globalLimits.customCss.filters, req.body.custom_css), permission: Permissions.ROOT, expected: false, error: __('Custom CSS strict mode is enabled and does not allow the following: "%s"', globalLimits.customCss.filters.join('", "')) },
			{ result: lengthBody(req.body.name, 1, globalLimits.fieldLength.boardname), expected: false, error: __('Board name must be 1-%s characters', globalLimits.fieldLength.boardname) },
			{ result: lengthBody(req.body.default_name, 0, 50), expected: false, error: __('Anon name must be 50 characters or less') },
			{ result: numberBody(req.body.reply_limit, globalLimits.replyLimit.min, globalLimits.replyLimit.max), expected: true, error: __('Reply Limit must be %s-%s', globalLimits.replyLimit.min, globalLimits.replyLimit.max) },
			{ result: numberBody(req.body.bump_limit, globalLimits.bumpLimit.min, globalLimits.bumpLimit.max), expected: true, error: __('Bump Limit must be %s-%s', globalLimits.bumpLimit.min, globalLimits.bumpLimit.max) },
			{ result: numberBody(req.body.thread_limit, globalLimits.threadLimit.min, globalLimits.threadLimit.max), expected: true, error: __('Threads Limit must be %s-%s', globalLimits.threadLimit.min, globalLimits.threadLimit.max) },
			{ result: numberBody(req.body.max_files, 0, globalLimits.postFiles.max), expected: true, error: __('Max files must be 0-%s', globalLimits.postFiles.max) },
			{ result: numberBody(req.body.min_thread_message_length, 0, globalLimits.fieldLength.message), expected: true, error: __('Min thread message length must be 0-%s', globalLimits.fieldLength.message) },
			{ result: numberBody(req.body.min_reply_message_length, 0, globalLimits.fieldLength.message), expected: true, error: __('Min reply message length must be 0-%s', globalLimits.fieldLength.message) },
			{ result: numberBody(req.body.max_thread_message_length, 0, globalLimits.fieldLength.message), expected: true, error: __('Max thread message length must be 0-%s', globalLimits.fieldLength.message) },
			{ result: numberBody(req.body.max_reply_message_length, 0, globalLimits.fieldLength.message), expected: true, error: __('Max reply message length must be 0-%s', globalLimits.fieldLength.message) },
			{ result: minmaxBody(req.body.min_thread_message_length, req.body.max_thread_message_length), expected: true, error: __('Min and max thread message lengths must not violate eachother') },
			{ result: minmaxBody(req.body.min_reply_message_length, req.body.max_reply_message_length), expected: true, error: __('Min and max reply message lengths must not violate eachother') },
			{ result: numberBodyVariable(req.body.min_thread_message_length, res.locals.board.settings.minThreadMessageLength,
				req.body.min_thread_message_length, maxThread, req.body.max_thread_message_length), expected: true,
			error: __('Min thread message length must be 0-%s and not more than "Max Thread Message Length" (currently %s)', globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength)  },
			{ result: numberBodyVariable(req.body.min_reply_message_length, res.locals.board.settings.minReplyMessageLength,
				req.body.min_reply_message_length, maxReply, req.body.max_reply_message_length), expected: true,
			error: __('Min reply message length must be 0-%s and not more than "Max Reply Message Length" (currently %s)', globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) },
			{ result: numberBodyVariable(req.body.max_thread_message_length, res.locals.board.settings.minThreadMessageLength,
				req.body.min_thread_message_length, globalLimits.fieldLength.message, globalLimits.fieldLength.message), expected: true,
			error: __('Max thread message length must be 0-%s and not less than "Min Thread Message Length" (currently %s)', globalLimits.fieldLength.message, res.locals.board.settings.minThreadMessageLength) },
			{ result: numberBodyVariable(req.body.max_reply_message_length, res.locals.board.settings.minReplyMessageLength,
				req.body.min_reply_message_length, globalLimits.fieldLength.message, globalLimits.fieldLength.message), expected: true,
			error: __('Max reply message length must be 0-%s and not less than "Min Reply Message Length" (currently %s)', globalLimits.fieldLength.message, res.locals.board.settings.minReplyMessageLength) },
			{ result: numberBody(req.body.lock_mode, 0, 2), expected: true, error: __('Invalid lock mode') },
			{ result: numberBody(req.body.captcha_mode, 0, 2), expected: true, error: __('Invalid captcha mode') },
			{ result: numberBody(req.body.tph_trigger, 0, 10000), expected: true, error: __('Invalid tph trigger threshold') },
			{ result: numberBody(req.body.tph_trigger_action, 0, 4), expected: true, error: __('Invalid tph trigger action') },
			{ result: numberBody(req.body.pph_trigger, 0, 10000), expected: true, error: __('Invalid pph trigger threshold') },
			{ result: numberBody(req.body.pph_trigger_action, 0, 4), expected: true, error: __('Invalid pph trigger action') },
			{ result: numberBody(req.body.lock_reset, 0, 2), expected: true, error: __('Invalid trigger reset lock') },
			{ result: numberBody(req.body.captcha_reset, 0, 2), expected: true, error: __('Invalid trigger reset captcha') },
			{ result: numberBody(req.body.delete_protection_age, 0), expected: true, error: __('Invalid OP thread age delete protection') },
			{ result: numberBody(req.body.delete_protection_count, 0), expected: true, error: __('Invalid OP thread reply count delete protection') },
			{ result: inArrayBody(req.body.language, i18n.getLocales()), expected: true, error: __('Invalid language') },
			{ result: inArrayBody(req.body.theme, themes), expected: true, error: __('Invalid theme') },
			{ result: inArrayBody(req.body.code_theme, codeThemes), expected: true, error: __('Invalid code theme') },
		], res.locals.permissions);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.board}/manage/settings.html`
			});
		}

		if (!res.locals.permissions.get(Permissions.BYPASS_RATELIMITS)) {
			const ratelimitBoard = await Ratelimits.incrmentQuota(req.params.board, 'settings', rateLimitCost.boardSettings); //2 changes a minute
			const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'settings', rateLimitCost.boardSettings));
			if (ratelimitBoard > 100 || ratelimitIp > 100) {
				return dynamicResponse(req, res, 429, 'message', {
					'title': __('Ratelimited'),
					'error': __('You are changing settings too quickly, please wait a minute and try again'),
					'redirect': `/${req.params.board}/manage/settings.html`
				});
			}
		}

		try {
			await changeBoardSettings(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const changePassword = require(__dirname+'/../../models/forms/changepassword.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		trimFields: ['username', 'password', 'twofactor', 'newpassword', 'newpasswordconfirm'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: existsBody(req.body.username), expected: true, error: __('Missing username') },
			{ result: lengthBody(req.body.username, 0, 50), expected: false, error: __('Username must be 50 characters or less') },
			{ result: existsBody(req.body.password), expected: true, error: __('Missing password') },
			{ result: lengthBody(req.body.password, 0, 50), expected: false, error: __('Password must be 50 characters or less') },
			{ result: existsBody(req.body.newpassword), expected: true, error: __('Missing new password') },
			{ result: lengthBody(req.body.newpassword, 0, 100), expected: false, error: __('New pasword must be 100 characters or less') },
			{ result: existsBody(req.body.newpasswordconfirm), expected: true, error: __('Missing new password confirmation') },
			{ result: lengthBody(req.body.newpasswordconfirm, 0, 100), expected: false, error: __('New password confirmation must be 100 characters or less') },
			{ result: (req.body.newpassword === req.body.newpasswordconfirm), expected: true, error: __('New password and password confirmation must match') },
			{ result: lengthBody(req.body.twofactor, 0, 6), expected: false, error: __('Invalid 2FA code') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': '/changepassword.html'
			});
		}

		try {
			await changePassword(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const createBoard = require(__dirname+'/../../models/forms/create.js')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { alphaNumericRegex, checkSchema, lengthBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		trimFields: ['name', 'uri', 'description'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { globalLimits } = config.get;

		const errors = await checkSchema([
			{ result: res.locals.permissions.get(Permissions.CREATE_BOARD), blocking: true, expected: true, error: __('No permission') },
			{ result: existsBody(req.body.uri), expected: true, error: __('Missing URI') },
			{ result: lengthBody(req.body.uri, 0, globalLimits.fieldLength.uri), expected: false, error: __('URI must be %s characters or less', globalLimits.fieldLength.uri) },
			{ result: existsBody(req.body.name), expected: true, error: __('Missing name') },
			{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.boardname), expected: false, error: __('Name must be %s characters or less', globalLimits.fieldLength.boardname) },
			{ result: alphaNumericRegex.test(req.body.uri), expected: true, error: __('URI must contain a-z 0-9 only') },
			{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.description), expected: false, error: __('Description must be %s characters or less', globalLimits.fieldLength.description) },
		], res.locals.permissions);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': '/create.html'
			});
		}

		try {
			await createBoard(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const deleteAccount = require(__dirname+'/../../models/forms/deleteaccount.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	//, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, numberBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	//paramConverter: paramConverter({}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const { staffBoards, ownedBoards } = res.locals.user;

		const errors = await checkSchema([
			{ result: existsBody(req.body.confirm), expected: true, error: __('Missing confirmation') },
			{ result: (numberBody(ownedBoards.length, 0, 0) && numberBody(staffBoards.length, 0, 0)), expected: true, error: __('You cannot delete your account while you hold staff position on any board') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': '/account.html',
			});
		}

		try {
			await deleteAccount(res.locals.user.username);
		} catch (err) {
			return next(err);
		}

		return dynamicResponse(req, res, 200, 'message', {
			'title': __('Success'),
			'message': __('Account deleted'),
			'redirect': '/',
		});

	}

};


code:
'use strict';

const deleteAccounts = require(__dirname+'/../../models/forms/deleteaccounts.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, { existsBody, checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkedaccounts', 'delete_owned_boards'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedaccounts, 1), expected: false, error: __('Must select at least one account') },
			{ result: !existsBody(req.body.delete_owned_boards) || res.locals.permissions.get(Permissions.GLOBAL_MANAGE_BOARDS), expected: true, error: __('No permission') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': '/globalmanage/accounts.html'
			});
		}

		try {
			await deleteAccounts(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const deleteAssets = require(__dirname+'/../../models/forms/deleteassets.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkedassets']
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedassets, 1), expected: false, error: __('Must select at least one asset to delete') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.board}/manage/assets.html`
			});
		}

		for (let i = 0; i < req.body.checkedassets.length; i++) {
			if (!res.locals.board.assets.includes(req.body.checkedassets[i])) {
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Bad request'),
					'message': __('Invalid assets selected'),
					'redirect': `/${req.params.board}/manage/assets.html`
				});
			}
		}

		try {
			await deleteAssets(req, res, next);
		} catch (err) {
			console.error(err);
			return next(err);
		}

	}

};


code:
'use strict';

const deleteBanners = require(__dirname+'/../../models/forms/deletebanners.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkedbanners']
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedbanners, 1), expected: false, error: __('Must select at least one banner to delete') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.board}/manage/assets.html`
			});
		}

		for (let i = 0; i < req.body.checkedbanners.length; i++) {
			if (!res.locals.board.banners.includes(req.body.checkedbanners[i])) {
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Bad request'),
					'message': __('Invalid banners selected'),
					'redirect': `/${req.params.board}/manage/assets.html`
				});
			}
		}

		try {
			await deleteBanners(req, res, next);
		} catch (err) {
			console.error(err);
			return next(err);
		}

	}

};


code:
'use strict';

const { Boards } = require(__dirname+'/../../db/')
	, deleteBoard = require(__dirname+'/../../models/forms/deleteboard.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { alphaNumericRegex, checkSchema, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		trimFields: ['uri'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		let board = null;
		const errors = await checkSchema([
			{ result: existsBody(req.body.confirm), expected: true, error: __('Missing confirmation') },
			{ result: existsBody(req.body.uri), expected: true, error: __('Missing URI') },
			{ result: alphaNumericRegex.test(req.body.uri), blocking: true, expected: true, error: __('URI must contain a-z 0-9 only') },
			{ result: req.params.board == null || (req.params.board === req.body.uri), expected: true, error: __('URI does not match current board') },
			{ result: async () => {
				board = await Boards.findOne(req.body.uri);
				return board != null;
			}, expected: true, error: __('Board /%s/ does not exist', req.body.uri) }
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': req.params.board ? `/${req.params.board}/manage/settings.html` : '/globalmanage/settings.html'
			});
		}

		try {
			await deleteBoard(board._id, board);
		} catch (err) {
			return next(err);
		}

		return dynamicResponse(req, res, 200, 'message', {
			'title': __('Success'),
			'message': __('Board deleted'),
			'redirect': req.params.board ? '/' : '/globalmanage/settings.html'
		});

	}

};


code:
'use strict';

const deleteCustomPage = require(__dirname+'/../../models/forms/deletecustompage.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkedcustompages'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedcustompages, 1), expected: false, error: __('Must select at least one custom page to delete') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.board}/manage/custompages.html`
			});
		}

		try {
			await deleteCustomPage(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const deleteFilter = require(__dirname+'/../../models/forms/deletefilter.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkedfilters'],
		objectIdArrays: ['checkedfilters']
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedfilters, 1), expected: false, error: __('Must select at least one filter to delete') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': req.params.board ? `/${req.params.board}/manage/filters.html` : '/globalmanage/filters.html'
			});
		}

		try {
			await deleteFilter(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const deleteFlags = require(__dirname+'/../../models/forms/deleteflags.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkedflags'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedflags, 1), expected: false, error: __('Must select at least one flag to delete') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': `/${req.params.board}/manage/assets.html`
			});
		}

		for (let i = 0; i < req.body.checkedflags.length; i++) {
			if (!res.locals.board.flags[req.body.checkedflags[i]]) {
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Bad request'),
					'message': __('Invalid flags selected'),
					'redirect': `/${req.params.board}/manage/assets.html`
				});
			}
		}

		try {
			await deleteFlags(req, res, next);
		} catch (err) {
			console.error(err);
			return next(err);
		}

	}

};


code:
'use strict';

const deleteNews = require(__dirname+'/../../models/forms/deletenews.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkednews'],
		objectIdArrays: ['checkednews']
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkednews, 1), expected: false, error: __('Must select at least one newspost to delete') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': '/globalmanage/news.html'
			});
		}

		try {
			await deleteNews(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};


code:
'use strict';

const deleteSessions = require(__dirname+'/../../models/forms/deletesessions.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkedsessionids'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const username = res.locals.user.username;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedsessionids, 1), expected: false, blocking: true, error: __('Must select at least one session to delete') },
			{ result: () => {
				//return if any input "session ids" dont start with sess: or dont end with :username
				return req.body.checkedsessionids.some(sid => !sid.startsWith('sess:') || !sid.endsWith(`:${username}`));
			}, expected: false, error: __('Invalid checked sessions') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': '/sessions.html',
			});
		}

		try {
			await deleteSessions(req.body.checkedsessionids);
		} catch (err) {
			return next(err);
		}

		return dynamicResponse(req, res, 200, 'message', {
			'title': __('Success'),
			'message': __('Sessions deleted'),
			'redirect': '/sessions.html', //if deleting all, will get redirected back to login anyway
		});

	}

};


code:
'use strict';

const deleteStaff = require(__dirname+'/../../models/forms/deletestaff.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, { checkSchema, lengthBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');

module.exports = {

	paramConverter: paramConverter({
		allowedArrays: ['checkedstaff'],
	}),

	controller: async (req, res, next) => {

		const { __ } = res.locals;

		const errors = await checkSchema([
			{ result: lengthBody(req.body.checkedstaff, 1), expected: false, error: __('Must select at least one staff to delete') },
			{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => !res.locals.board.staff[s]), expected: false, error: __('Invalid staff selection') },
			{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => s === res.locals.board.owner), expected: false, permission: Permissions.ROOT, error: __('You can\'t delete the board owner') },
			//not really necessary, but its a bit retarded to "delete yourself" as staff this way
			{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => s === res.locals.user.username), expected: false, error: __('Resign from the accounts page instead') },
		]);

		if (errors.length > 0) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'errors': errors,
				'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
			});
		}

		try {
			await deleteStaff(req, res, next);
		} catch (err) {
			return next(err);
		}

	}

};