commit
4047a7ec23
378 changed files with 29334 additions and 0 deletions
49
scripts/build-geo.js
Normal file
49
scripts/build-geo.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const zlib = require('zlib');
|
||||
const tar = require('tar');
|
||||
|
||||
let url =
|
||||
'https://raw.githubusercontent.com/GitSquared/node-geolite2-redist/master/redist/GeoLite2-Country.tar.gz';
|
||||
|
||||
if (process.env.MAXMIND_LICENSE_KEY) {
|
||||
url =
|
||||
`https://download.maxmind.com/app/geoip_download` +
|
||||
`?edition_id=GeoLite2-Country&license_key=${process.env.MAXMIND_LICENSE_KEY}&suffix=tar.gz`;
|
||||
}
|
||||
|
||||
const dest = path.resolve(__dirname, '../public/geo');
|
||||
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest);
|
||||
}
|
||||
|
||||
const download = url =>
|
||||
new Promise(resolve => {
|
||||
https.get(url, res => {
|
||||
resolve(res.pipe(zlib.createGunzip({})).pipe(tar.t()));
|
||||
});
|
||||
});
|
||||
|
||||
download(url).then(
|
||||
res =>
|
||||
new Promise((resolve, reject) => {
|
||||
res.on('entry', entry => {
|
||||
if (entry.path.endsWith('.mmdb')) {
|
||||
const filename = path.join(dest, path.basename(entry.path));
|
||||
entry.pipe(fs.createWriteStream(filename));
|
||||
|
||||
console.log('Saved geo database:', filename);
|
||||
}
|
||||
});
|
||||
|
||||
res.on('error', e => {
|
||||
reject(e);
|
||||
});
|
||||
res.on('finish', () => {
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
);
|
||||
98
scripts/change-password.js
Normal file
98
scripts/change-password.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
require('dotenv').config();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const chalk = require('chalk');
|
||||
const prompts = require('prompts');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
const runQuery = async query => {
|
||||
return query.catch(e => {
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
const updateAccountByUsername = (username, data) => {
|
||||
return runQuery(
|
||||
prisma.account.update({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
data,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const hashPassword = password => {
|
||||
return bcrypt.hashSync(password, SALT_ROUNDS);
|
||||
};
|
||||
|
||||
const changePassword = async (username, newPassword) => {
|
||||
const password = hashPassword(newPassword);
|
||||
return updateAccountByUsername(username, { password });
|
||||
};
|
||||
|
||||
const getUsernameAndPassword = async () => {
|
||||
let [username, password] = process.argv.slice(2);
|
||||
if (username && password) {
|
||||
return { username, password };
|
||||
}
|
||||
|
||||
const questions = [];
|
||||
if (!username) {
|
||||
questions.push({
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
message: 'Enter account to change password',
|
||||
});
|
||||
}
|
||||
if (!password) {
|
||||
questions.push(
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: 'Enter new password',
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'confirmation',
|
||||
message: 'Confirm new password',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const answers = await prompts(questions);
|
||||
if (answers.password !== answers.confirmation) {
|
||||
throw new Error(`Passwords don't match`);
|
||||
}
|
||||
|
||||
return {
|
||||
username: username || answers.username,
|
||||
password: answers.password,
|
||||
};
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let username, password;
|
||||
|
||||
try {
|
||||
({ username, password } = await getUsernameAndPassword());
|
||||
} catch (error) {
|
||||
console.log(chalk.redBright(error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await changePassword(username, password);
|
||||
console.log('Password changed for user', chalk.greenBright(username));
|
||||
} catch (error) {
|
||||
if (error.message.includes('RecordNotFound')) {
|
||||
console.log('Account not found:', chalk.redBright(username));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
prisma.$disconnect();
|
||||
})();
|
||||
38
scripts/check-lang.js
Normal file
38
scripts/check-lang.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const messages = require('../lang/en-US.json');
|
||||
const ignore = require('../lang-ignore.json');
|
||||
|
||||
const dir = path.resolve(__dirname, '../lang');
|
||||
const files = fs.readdirSync(dir);
|
||||
const keys = Object.keys(messages).sort();
|
||||
const filter = process.argv?.[2];
|
||||
|
||||
files.forEach(file => {
|
||||
if (file !== 'en-US.json') {
|
||||
const lang = require(`../lang/${file}`);
|
||||
const id = file.replace('.json', '');
|
||||
|
||||
if (filter && filter !== id) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.yellowBright(`\n## ${file.replace('.json', '')}`));
|
||||
let count = 0;
|
||||
keys.forEach(key => {
|
||||
const orig = messages[key];
|
||||
const check = lang[key];
|
||||
const ignored = ignore[id]?.includes(key);
|
||||
|
||||
if (!ignored && (!check || check === orig)) {
|
||||
console.log(chalk.redBright('*'), chalk.greenBright(`${key}:`), orig);
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
if (count === 0) {
|
||||
console.log('**👍 Complete!**');
|
||||
}
|
||||
}
|
||||
});
|
||||
30
scripts/copy-db-schema.js
Normal file
30
scripts/copy-db-schema.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function getDatabase() {
|
||||
const type =
|
||||
process.env.DATABASE_TYPE ||
|
||||
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
|
||||
|
||||
if (type === 'postgres') {
|
||||
return 'postgresql';
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
const databaseType = getDatabase();
|
||||
|
||||
if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
|
||||
throw new Error('Missing or invalid database');
|
||||
}
|
||||
|
||||
console.log(`Database type detected: ${databaseType}`);
|
||||
|
||||
const src = path.resolve(__dirname, `../prisma/schema.${databaseType}.prisma`);
|
||||
const dest = path.resolve(__dirname, '../prisma/schema.prisma');
|
||||
|
||||
fs.copyFileSync(src, dest);
|
||||
|
||||
console.log(`Copied ${src} to ${dest}`);
|
||||
39
scripts/download-country-names.js
Normal file
39
scripts/download-country-names.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const chalk = require('chalk');
|
||||
|
||||
const src = path.resolve(__dirname, '../lang');
|
||||
const dest = path.resolve(__dirname, '../public/country');
|
||||
const files = fs.readdirSync(src);
|
||||
|
||||
const getUrl = locale =>
|
||||
`https://raw.githubusercontent.com/umpirsky/country-list/master/data/${locale}/country.json`;
|
||||
|
||||
const asyncForEach = async (array, callback) => {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
};
|
||||
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest);
|
||||
}
|
||||
|
||||
const download = async files => {
|
||||
await asyncForEach(files, async file => {
|
||||
const locale = file.replace('-', '_').replace('.json', '');
|
||||
|
||||
const filename = path.join(dest, file);
|
||||
if (!fs.existsSync(filename)) {
|
||||
await new Promise(resolve => {
|
||||
https.get(getUrl(locale), res => {
|
||||
console.log('Downloaded', chalk.greenBright('->'), filename);
|
||||
resolve(res.pipe(fs.createWriteStream(filename)));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
download(files);
|
||||
35
scripts/format-lang.js
Normal file
35
scripts/format-lang.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const del = require('del');
|
||||
const prettier = require('prettier');
|
||||
const chalk = require('chalk');
|
||||
|
||||
const src = path.resolve(__dirname, '../lang');
|
||||
const dest = path.resolve(__dirname, '../build');
|
||||
const files = fs.readdirSync(src);
|
||||
|
||||
const removed = del.sync([path.join(dest, '*.json')]);
|
||||
|
||||
if (removed.length) {
|
||||
console.log(removed.map(n => `${n} ${chalk.redBright('✗')}`).join('\n'));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest);
|
||||
}
|
||||
|
||||
files.forEach(file => {
|
||||
const lang = require(`../lang/${file}`);
|
||||
const keys = Object.keys(lang).sort();
|
||||
|
||||
const formatted = keys.reduce((obj, key) => {
|
||||
obj[key] = { defaultMessage: lang[key] };
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
const json = prettier.format(JSON.stringify(formatted), { parser: 'json' });
|
||||
|
||||
fs.writeFileSync(path.resolve(dest, file), json);
|
||||
|
||||
console.log(path.resolve(src, file), chalk.greenBright('->'), path.resolve(dest, file));
|
||||
});
|
||||
142
scripts/loadtest.js
Normal file
142
scripts/loadtest.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
const loadtest = require('loadtest');
|
||||
const chalk = require('chalk');
|
||||
const trunc = num => +num.toFixed(1);
|
||||
|
||||
/**
|
||||
* Example invocations:
|
||||
*
|
||||
* npm run loadtest -- --weight=heavy
|
||||
* npm run loadtest -- --weight=heavy --verbose
|
||||
* npm run loadtest -- --weight=single --verbose
|
||||
* npm run loadtest -- --weight=medium
|
||||
*/
|
||||
|
||||
/**
|
||||
* Command line arguments like --weight=heavy and --verbose use this object
|
||||
* If you are providing _alternative_ configs, use --weight
|
||||
* e.g. add --weight=ultra then add commandlineOptions.ultra={}
|
||||
* --verbose can be combied with any weight.
|
||||
*/
|
||||
const commandlineOptions = {
|
||||
single: {
|
||||
concurrency: 1,
|
||||
requestsPerSecond: 1,
|
||||
maxSeconds: 5,
|
||||
maxRequests: 1,
|
||||
},
|
||||
// Heavy can saturate CPU which leads to requests stalling depending on machine
|
||||
// Keep an eye if --verbose logs pause, or if node CPU in top is > 100.
|
||||
// https://github.com/alexfernandez/loadtest#usage-donts
|
||||
heavy: {
|
||||
concurrency: 10,
|
||||
requestsPerSecond: 200,
|
||||
maxSeconds: 60,
|
||||
},
|
||||
// Throttled requests should not max out CPU,
|
||||
medium: {
|
||||
concurrency: 3,
|
||||
requestsPerSecond: 5,
|
||||
maxSeconds: 60,
|
||||
},
|
||||
verbose: { statusCallback },
|
||||
};
|
||||
|
||||
const options = {
|
||||
url: 'http://localhost:3000',
|
||||
method: 'POST',
|
||||
concurrency: 5,
|
||||
requestsPerSecond: 5,
|
||||
maxSeconds: 5,
|
||||
requestGenerator: (params, options, client, callback) => {
|
||||
const message = JSON.stringify(mockPageView());
|
||||
options.headers['Content-Length'] = message.length;
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
options.body = message;
|
||||
options.path = '/api/collect';
|
||||
const request = client(options, callback);
|
||||
request.write(message);
|
||||
return request;
|
||||
},
|
||||
};
|
||||
|
||||
function getArgument() {
|
||||
const weight = process.argv[2] && process.argv[2].replace('--weight=', '');
|
||||
const verbose = process.argv.includes('--verbose') && 'verbose';
|
||||
return [weight, verbose];
|
||||
}
|
||||
|
||||
// Patch in all command line arguments over options object
|
||||
// Must do this prior to calling `loadTest()`
|
||||
getArgument().map(arg => Object.assign(options, commandlineOptions[arg]));
|
||||
|
||||
loadtest.loadTest(options, (error, results) => {
|
||||
if (error) {
|
||||
return console.error(chalk.redBright('Got an error: %s', error));
|
||||
}
|
||||
console.log(chalk.bold(chalk.yellow('\n--------\n')));
|
||||
console.log(chalk.yellowBright('Loadtests complete:'), chalk.greenBright('success'), '\n');
|
||||
prettyLogItem('Total Requests:', results.totalRequests);
|
||||
prettyLogItem('Total Errors:', results.totalErrors);
|
||||
|
||||
prettyLogItem(
|
||||
'Latency(mean/min/max)',
|
||||
trunc(results.meanLatencyMs),
|
||||
'/',
|
||||
trunc(results.maxLatencyMs),
|
||||
'/',
|
||||
trunc(results.minLatencyMs),
|
||||
);
|
||||
|
||||
if (results.totalErrors) {
|
||||
console.log(chalk.redBright('*'), chalk.red('Total Errors:'), results.totalErrors);
|
||||
}
|
||||
|
||||
if (results.errorCodes && Object.keys(results.errorCodes).length) {
|
||||
console.log(chalk.redBright('*'), chalk.red('Error Codes:'), results.errorCodes);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new object for each request. Note, we could randomize values here if desired.
|
||||
*
|
||||
* TODO: Need a better way of passing in websiteId, hostname, URL.
|
||||
*
|
||||
* @param {object} payload pageview payload same as sent via tracker
|
||||
*/
|
||||
function mockPageView(
|
||||
payload = {
|
||||
website: 'fcd4c7e3-ed76-439c-9121-3a0f102df126',
|
||||
hostname: 'localhost',
|
||||
screen: '1680x1050',
|
||||
url: '/LOADTESTING',
|
||||
referrer: '/REFERRER',
|
||||
},
|
||||
) {
|
||||
return {
|
||||
type: 'pageview',
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
// If you pass in --verbose, this function is called
|
||||
function statusCallback(error, result, latency) {
|
||||
if (error) {
|
||||
return console.error(chalk.redBright(error));
|
||||
}
|
||||
console.log(
|
||||
chalk.yellowBright(`\n## req #${result.requestIndex + 1} of ${latency.totalRequests}`),
|
||||
);
|
||||
prettyLogItem('Request elapsed milliseconds:', trunc(result.requestElapsed));
|
||||
prettyLogItem(
|
||||
'Latency(mean/max/min):',
|
||||
trunc(latency.meanLatencyMs),
|
||||
'/',
|
||||
trunc(latency.maxLatencyMs),
|
||||
'/',
|
||||
trunc(latency.minLatencyMs),
|
||||
);
|
||||
}
|
||||
|
||||
function prettyLogItem(label, ...args) {
|
||||
console.log(chalk.redBright('*'), chalk.green(label), ...args);
|
||||
}
|
||||
30
scripts/merge-lang.js
Normal file
30
scripts/merge-lang.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const prettier = require('prettier');
|
||||
const messages = require('../build/messages.json');
|
||||
|
||||
const dest = path.resolve(__dirname, '../lang');
|
||||
const files = fs.readdirSync(dest);
|
||||
const keys = Object.keys(messages).sort();
|
||||
|
||||
files.forEach(file => {
|
||||
const lang = require(`../lang/${file}`);
|
||||
|
||||
console.log(`Merging ${file}`);
|
||||
|
||||
const merged = keys.reduce((obj, key) => {
|
||||
const message = lang[key];
|
||||
|
||||
obj[key] = message || messages[key].defaultMessage;
|
||||
|
||||
if (!message) {
|
||||
console.log(`* Added key ${key}`);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
const json = prettier.format(JSON.stringify(merged), { parser: 'json' });
|
||||
|
||||
fs.writeFileSync(path.resolve(dest, file), json);
|
||||
});
|
||||
3
scripts/start-env.js
Normal file
3
scripts/start-env.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
const cli = require('next/dist/cli/next-start');
|
||||
|
||||
cli.nextStart(['-p', process.env.PORT || 3000, '-H', process.env.HOSTNAME || '0.0.0.0']);
|
||||
Loading…
Add table
Add a link
Reference in a new issue