chore: initial
This commit is contained in:
commit
7469ceecb1
33
.eslint.defaults.js
Normal file
33
.eslint.defaults.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const side = require('./side');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
globals: {
|
||||||
|
process: true,
|
||||||
|
AVOCADO_CLIENT: true,
|
||||||
|
AVOCADO_SERVER: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'babel/object-curly-spacing': 'off',
|
||||||
|
'brace-style': ['error', 'stroustrup'],
|
||||||
|
'no-bitwise': ['error', {int32Hint: true}],
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'padded-blocks': ['error', {classes: 'always'}],
|
||||||
|
yoda: 'off',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
webpack: {
|
||||||
|
config: `${__dirname}/webpack.config.js`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('client' === side) {
|
||||||
|
config.rules['jsx-a11y/label-has-associated-control'] = [2, {
|
||||||
|
'assert': 'either',
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
8
.eslintrc.js
Normal file
8
.eslintrc.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const neutrino = require('neutrino');
|
||||||
|
|
||||||
|
// Have to default side for IDE eslint.
|
||||||
|
process.env.SIDE = process.env.SIDE || 'client';
|
||||||
|
|
||||||
|
const side = require('./side');
|
||||||
|
|
||||||
|
module.exports = neutrino(require(`./.neutrinorc.${side}`)).eslintrc();
|
121
.gitignore
vendored
Normal file
121
.gitignore
vendored
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# Neutrino build directory
|
||||||
|
build
|
||||||
|
|
||||||
|
/var/lib
|
5
.mocharc.js
Normal file
5
.mocharc.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const neutrino = require('neutrino');
|
||||||
|
|
||||||
|
const side = require('./side');
|
||||||
|
|
||||||
|
module.exports = neutrino(require(`./.neutrinorc.${side}`)).mocha();
|
92
.neutrinorc.client.js
Normal file
92
.neutrinorc.client.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const copy = require('@neutrinojs/copy');
|
||||||
|
const react = require('@neutrinojs/react');
|
||||||
|
const styles = require('@neutrinojs/style-loader');
|
||||||
|
const globImporter = require('node-sass-glob-importer');
|
||||||
|
const scwp = require('scwp/neutrino');
|
||||||
|
const {DefinePlugin} = require('webpack');
|
||||||
|
|
||||||
|
const {afterPlatform, initial} = require('./middleware');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
options: {
|
||||||
|
root: __dirname,
|
||||||
|
},
|
||||||
|
use: [
|
||||||
|
initial({
|
||||||
|
environmentDefines: ['FRONTEND_ORIGIN'],
|
||||||
|
scwpPaths: [
|
||||||
|
/^@avocado/,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
react({
|
||||||
|
style: {
|
||||||
|
test: /\.(css|sass|scss)$/,
|
||||||
|
modulesTest: /\.module\.(css|sass|scss)$/,
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
useId: 'postcss',
|
||||||
|
options: {
|
||||||
|
config: {
|
||||||
|
path: __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
useId: 'sass',
|
||||||
|
options: {
|
||||||
|
importer: globImporter(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
template: `${__dirname}/src/client/index.ejs`,
|
||||||
|
title: 'reddichat',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
afterPlatform({
|
||||||
|
babelPaths: [
|
||||||
|
/^@avocado/,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
(neutrino) => {
|
||||||
|
neutrino.config.module.rule('style')
|
||||||
|
.oneOf('raw')
|
||||||
|
.before('normal')
|
||||||
|
.test(/\.raw\.(css|sass|scss)$/)
|
||||||
|
.use('raw')
|
||||||
|
.loader('raw-loader')
|
||||||
|
.end()
|
||||||
|
.use('postcss')
|
||||||
|
.loader('postcss-loader')
|
||||||
|
.options({config: {path: __dirname}})
|
||||||
|
.end()
|
||||||
|
.use('sass')
|
||||||
|
.loader('sass-loader')
|
||||||
|
.options({importer: globImporter()})
|
||||||
|
.end()
|
||||||
|
neutrino.config.resolve.modules
|
||||||
|
.add(`${neutrino.options.source}/client/scss`);
|
||||||
|
neutrino.config
|
||||||
|
.plugin('avocado-define')
|
||||||
|
.use(DefinePlugin, [
|
||||||
|
{
|
||||||
|
AVOCADO_CLIENT: true,
|
||||||
|
AVOCADO_SERVER: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
copy({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: 'src/common/favicon.ico',
|
||||||
|
to: 'favicon.ico',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
39
.neutrinorc.server.js
Normal file
39
.neutrinorc.server.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const {join} = require('path');
|
||||||
|
const {spawn} = require('child_process');
|
||||||
|
|
||||||
|
const copy = require('@neutrinojs/copy');
|
||||||
|
const node = require('@neutrinojs/node');
|
||||||
|
const {DefinePlugin} = require('webpack');
|
||||||
|
|
||||||
|
const {afterPlatform, initial} = require('./middleware');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
options: {
|
||||||
|
root: __dirname,
|
||||||
|
},
|
||||||
|
use: [
|
||||||
|
initial({
|
||||||
|
environmentDefines: ['MYSQL_ORIGIN', 'REDIS_ORIGIN'],
|
||||||
|
scwpPaths: [
|
||||||
|
/^@avocado/,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
node(),
|
||||||
|
afterPlatform({
|
||||||
|
babelPaths: [
|
||||||
|
/^@avocado/,
|
||||||
|
],
|
||||||
|
externalMatcher: /(?:@avocado|@pixi|scwp|webpack)/,
|
||||||
|
}),
|
||||||
|
(neutrino) => {
|
||||||
|
neutrino.config
|
||||||
|
.plugin('avocado-define')
|
||||||
|
.use(DefinePlugin, [
|
||||||
|
{
|
||||||
|
AVOCADO_CLIENT: false,
|
||||||
|
AVOCADO_SERVER: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
// "preLaunchTask": "Both",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Terrible",
|
||||||
|
"address": "127.0.0.1",
|
||||||
|
"port": 43000,
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
79
.vscode/tasks.json
vendored
Normal file
79
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Client",
|
||||||
|
"type": "shell",
|
||||||
|
"isBackground": true,
|
||||||
|
"command": "yarn",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"client"
|
||||||
|
],
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "custom",
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "__________"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": "(Compiling|Project is running)",
|
||||||
|
"endsPattern": "(?:Compiled successfully|Failed to compile)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Server",
|
||||||
|
"type": "shell",
|
||||||
|
"isBackground": true,
|
||||||
|
"command": "yarn",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"server",
|
||||||
|
"--inspect=127.0.0.1:43000"
|
||||||
|
],
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "custom",
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "You need to restart the application"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "custom",
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "__________"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": "(Hash:|Updated modules)",
|
||||||
|
"endsPattern": "(Built at:|Update applied)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Both",
|
||||||
|
"isBackground": true,
|
||||||
|
"dependsOn": [
|
||||||
|
"Client",
|
||||||
|
"Server"
|
||||||
|
],
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"command": "yarn",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"lint"
|
||||||
|
],
|
||||||
|
"problemMatcher": [
|
||||||
|
"$eslint-stylish"
|
||||||
|
],
|
||||||
|
"label": "yarn run lint"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
Dockerfile
Normal file
5
Dockerfile
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FROM node:14
|
||||||
|
EXPOSE 31344
|
||||||
|
COPY ./build /var/www
|
||||||
|
COPY ./node_modules /var/www/server/node_modules
|
||||||
|
CMD ["node", "/var/www/server/index.js"]
|
0
attribution.txt
Normal file
0
attribution.txt
Normal file
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
|
||||||
|
reddichat_redis:
|
||||||
|
image: redis:6
|
||||||
|
ports:
|
||||||
|
- 31346:6379
|
||||||
|
|
||||||
|
reddichat_mysql:
|
||||||
|
image: mysql:8
|
||||||
|
ports:
|
||||||
|
- 31347:3306
|
||||||
|
environment:
|
||||||
|
- MYSQL_DATABASE=reddichat
|
||||||
|
- MYSQL_ROOT_PASSWORD=UNSAFE_DEV_PASSWORD
|
||||||
|
command:
|
||||||
|
- '--default-authentication-plugin=mysql_native_password'
|
||||||
|
|
||||||
|
reddichat_adminer:
|
||||||
|
image: adminer
|
||||||
|
|
||||||
|
environment:
|
||||||
|
ADMINER_DEFAULT_SERVER: reddichat_mysql
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- 'traefik.frontend.rule=Host:adminer.reddichat.localhost'
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external:
|
||||||
|
name: docker_system_default
|
5
inspect.js
Normal file
5
inspect.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const neutrino = require('neutrino');
|
||||||
|
|
||||||
|
const side = require('./side');
|
||||||
|
|
||||||
|
neutrino(require(`./.neutrinorc.${side}`)).inspect();
|
134
middleware.js
Normal file
134
middleware.js
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
const path = require('path');
|
||||||
|
const airbnb = require('@neutrinojs/airbnb');
|
||||||
|
const airbnbBase = require('@neutrinojs/airbnb-base');
|
||||||
|
const mocha = require('@neutrinojs/mocha');
|
||||||
|
const scwp = require('scwp/neutrino');
|
||||||
|
const {DefinePlugin} = require('webpack');
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
|
||||||
|
const side = require('./side');
|
||||||
|
|
||||||
|
const pkg = require(`${__dirname}/package.json`);
|
||||||
|
|
||||||
|
const gatherPackagePaths = (root, packageMatchers) => {
|
||||||
|
return packageMatchers.reduce((r, packageMatcher) => (
|
||||||
|
r.concat([
|
||||||
|
(pkg.dependencies || {}),
|
||||||
|
(pkg.devDependencies || {}),
|
||||||
|
].reduce((r, deps) => {
|
||||||
|
const packageNames = Object.keys(deps);
|
||||||
|
const packages = [];
|
||||||
|
for (let i = 0; i < packageNames.length; i++) {
|
||||||
|
const packageName = packageNames[i];
|
||||||
|
if (packageName.match(packageMatcher)) {
|
||||||
|
packages.push(path.relative(root, path.dirname(require.resolve(packageName))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.concat(packages);
|
||||||
|
}, []))
|
||||||
|
), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.initial = (options) => (neutrino) => {
|
||||||
|
const {
|
||||||
|
environmentDefines = [],
|
||||||
|
rawScwpPaths = [],
|
||||||
|
scwpPaths = [],
|
||||||
|
} = options;
|
||||||
|
neutrino.options.mains.index = `${side}/index`;
|
||||||
|
neutrino.options.output = `build/${side}`;
|
||||||
|
neutrino.config.resolve.modules
|
||||||
|
.add(`${neutrino.options.root}/node_modules`);
|
||||||
|
neutrino.config.resolveLoader.modules
|
||||||
|
.add(`${neutrino.options.root}/node_modules`);
|
||||||
|
neutrino.config.resolve.alias
|
||||||
|
.set('~', neutrino.options.source);
|
||||||
|
('server' === side ? airbnbBase : airbnb)({
|
||||||
|
eslint: {
|
||||||
|
cache: false,
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
baseConfig: require('./.eslint.defaults'),
|
||||||
|
},
|
||||||
|
})(neutrino);
|
||||||
|
mocha({
|
||||||
|
spec: `src/+(${side}|common)/**/*.spec.js`,
|
||||||
|
})(neutrino);
|
||||||
|
scwp({
|
||||||
|
paths: [
|
||||||
|
'./src/common',
|
||||||
|
`./src/${side}`,
|
||||||
|
].concat(
|
||||||
|
gatherPackagePaths(neutrino.options.root, scwpPaths.concat(/^scwp/)),
|
||||||
|
).concat(
|
||||||
|
rawScwpPaths,
|
||||||
|
),
|
||||||
|
})(neutrino);
|
||||||
|
neutrino.config
|
||||||
|
.plugin('environment-define')
|
||||||
|
.use(DefinePlugin, [
|
||||||
|
environmentDefines.reduce((r, k) => ({...r, [k]: JSON.stringify(process.env[k])}), {})
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.afterPlatform = (options) => (neutrino) => {
|
||||||
|
const {
|
||||||
|
babelPaths = [],
|
||||||
|
externalMatcher = [],
|
||||||
|
} = options;
|
||||||
|
const allBabelPaths = gatherPackagePaths(neutrino.options.root, babelPaths);
|
||||||
|
neutrino.config.module
|
||||||
|
.rule('compile')
|
||||||
|
.use('babel')
|
||||||
|
.tap((options) => {
|
||||||
|
options.only = [
|
||||||
|
neutrino.options.source,
|
||||||
|
].concat(allBabelPaths);
|
||||||
|
options.ignore = [];
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
allBabelPaths.forEach((babelPath) => {
|
||||||
|
neutrino.config.module
|
||||||
|
.rule('compile')
|
||||||
|
.include
|
||||||
|
.add(path.resolve(neutrino.options.root, babelPath));
|
||||||
|
});
|
||||||
|
neutrino.config.module
|
||||||
|
.rule('compile')
|
||||||
|
.use('babel')
|
||||||
|
.get('options').plugins.push(
|
||||||
|
[
|
||||||
|
'babel-plugin-webpack-alias',
|
||||||
|
{
|
||||||
|
config: `${__dirname}/webpack.config.js`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if ('client' === side) {
|
||||||
|
neutrino.config.node.delete('Buffer');
|
||||||
|
}
|
||||||
|
else /* if ('server' === side) */ {
|
||||||
|
neutrino.config.stats('normal');
|
||||||
|
if ('production' !== process.env.NODE_ENV) {
|
||||||
|
neutrino.config
|
||||||
|
.plugin('start-server')
|
||||||
|
.tap((args) => {
|
||||||
|
const options = args[0];
|
||||||
|
const inspectArg = process.argv.find((arg) => -1 !== arg.indexOf('--inspect'));
|
||||||
|
if (inspectArg) {
|
||||||
|
options.nodeArgs.push(inspectArg);
|
||||||
|
}
|
||||||
|
const profArg = process.argv.find((arg) => -1 !== arg.indexOf('--prof'));
|
||||||
|
if (profArg) {
|
||||||
|
options.nodeArgs.push(profArg);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
neutrino.config
|
||||||
|
.externals(nodeExternals({
|
||||||
|
whitelist: externalMatcher,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
80
package.json
Normal file
80
package.json
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
{
|
||||||
|
"name": "reddichat",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build:client": "SIDE=CLIENT webpack --mode production --config webpack.config.js",
|
||||||
|
"build:docker": "yarn run build:client && yarn run build:server && docker build",
|
||||||
|
"build:server": "SIDE=SERVER webpack --mode production --config webpack.config.js",
|
||||||
|
"client": "SIDE=CLIENT webpack-dev-server --verbose --disable-host-check --host localhost --port 31345 --mode development --config webpack.config.js",
|
||||||
|
"docker": "docker-compose -p reddichat up -d",
|
||||||
|
"inspect": "node ./inspect.js",
|
||||||
|
"lint": "eslint --cache --format codeframe --ext mjs,jsx,js src",
|
||||||
|
"repl": "rlwrap -C qmp socat STDIO UNIX:$(ls /tmp/reddichat-*.sock | tail -n 1)",
|
||||||
|
"server": "SIDE=SERVER webpack --watch --mode development --config webpack.config.js",
|
||||||
|
"test:client": "NODE_PRESERVE_SYMLINKS=1 SIDE=client mocha --watch",
|
||||||
|
"test:server": "NODE_PRESERVE_SYMLINKS=1 SIDE=server mocha --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@avocado/core": "1.x",
|
||||||
|
"@avocado/net": "1.x",
|
||||||
|
"@reduxjs/toolkit": "1.4.0",
|
||||||
|
"ansi-html": "0.0.7",
|
||||||
|
"bcrypt": "^5.0.0",
|
||||||
|
"classnames": "2.2.6",
|
||||||
|
"connect-redis": "^5.0.0",
|
||||||
|
"contempo": "1.x",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"express-session": "^1.17.1",
|
||||||
|
"express-socket.io-session": "^1.3.5",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"html-entities": "1.3.1",
|
||||||
|
"immer": "^7.0.1",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.memoize": "^4.1.2",
|
||||||
|
"mysql2": "^2.1.0",
|
||||||
|
"normalizr": "^3.6.0",
|
||||||
|
"passport": "^0.4.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
|
"prop-types": "^15",
|
||||||
|
"react": "16.8.6",
|
||||||
|
"react-dom": "16.8.6",
|
||||||
|
"react-hot-loader": "^4.12.21",
|
||||||
|
"react-markdown": "^4.3.1",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
|
"react-sortable-tree": "^2.7.1",
|
||||||
|
"react-tabs": "^3.1.1",
|
||||||
|
"redis": "^3.0.2",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"scwp": "1.x",
|
||||||
|
"sequelize": "^6.2.4",
|
||||||
|
"socket.io-redis": "^5.3.0",
|
||||||
|
"source-map-support": "^0.5.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@neutrinojs/airbnb": "^9.1.0",
|
||||||
|
"@neutrinojs/airbnb-base": "^9.1.0",
|
||||||
|
"@neutrinojs/copy": "^9.2.0",
|
||||||
|
"@neutrinojs/mocha": "^9.1.0",
|
||||||
|
"@neutrinojs/node": "^9.1.0",
|
||||||
|
"@neutrinojs/react": "^9.1.0",
|
||||||
|
"autoprefixer": "9.8.0",
|
||||||
|
"babel-plugin-webpack-alias": "^2.1.2",
|
||||||
|
"eslint": "^6",
|
||||||
|
"eslint-import-resolver-webpack": "^0.12.1",
|
||||||
|
"mocha": "^7",
|
||||||
|
"neutrino": "^9.1.0",
|
||||||
|
"node-sass": "4.12.0",
|
||||||
|
"node-sass-glob-importer": "5.3.2",
|
||||||
|
"postcss-loader": "3.0.0",
|
||||||
|
"raw-loader": "1.x",
|
||||||
|
"sass-loader": "7.1.0",
|
||||||
|
"sequelize-cli": "^6.1.0",
|
||||||
|
"v8-natives": "^1.1.0",
|
||||||
|
"webpack": "^4",
|
||||||
|
"webpack-cli": "^3",
|
||||||
|
"webpack-dev-server": "^3"
|
||||||
|
}
|
||||||
|
}
|
5
postcss.config.js
Normal file
5
postcss.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
8
side.js
Normal file
8
side.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const side = (process.env.SIDE || '').toLowerCase();
|
||||||
|
if (-1 === ['client', 'server'].indexOf(side)) {
|
||||||
|
throw new Error(
|
||||||
|
"You must define the SIDE environment variable as 'server' or 'client' when building!",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = side;
|
9
src/client/app.jsx
Normal file
9
src/client/app.jsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import './app.scss';
|
||||||
|
|
||||||
|
import {hot} from 'react-hot-loader';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const App = () => <div className="app" />;
|
||||||
|
|
||||||
|
export default hot(module)(App);
|
17
src/client/app.scss
Normal file
17
src/client/app.scss
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panes {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
&.horizontal > .pane {
|
||||||
|
float: left;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
&.vertical > .pane {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
12
src/client/hooks/useSocket.js
Normal file
12
src/client/hooks/useSocket.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import {useEffect} from 'react';
|
||||||
|
|
||||||
|
import {SocketClient} from '@avocado/net/client/socket';
|
||||||
|
|
||||||
|
const frontendOrigin = window.location.href;
|
||||||
|
const isSecure = 'https' === frontendOrigin.substr(0, 5);
|
||||||
|
export const socket = new SocketClient(frontendOrigin, {secure: isSecure});
|
||||||
|
|
||||||
|
export default function useSocket(fn) {
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
useEffect(() => fn(socket), []);
|
||||||
|
}
|
14
src/client/index.ejs
Normal file
14
src/client/index.ejs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= htmlWebpackPlugin.options.lang %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="icon" href="/favicon.ico"/>
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="<%= htmlWebpackPlugin.options.appMountId %>">
|
||||||
|
<div class="debug-container"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
22
src/client/index.jsx
Normal file
22
src/client/index.jsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
import 'react-hot-loader';
|
||||||
|
|
||||||
|
import {enableMapSet} from 'immer';
|
||||||
|
import React from 'react';
|
||||||
|
import {render} from 'react-dom';
|
||||||
|
import {Provider} from 'react-redux';
|
||||||
|
|
||||||
|
import App from './app';
|
||||||
|
import createStore from './store';
|
||||||
|
|
||||||
|
enableMapSet();
|
||||||
|
|
||||||
|
render(
|
||||||
|
(
|
||||||
|
<Provider store={createStore()}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
document.getElementById('root'),
|
||||||
|
);
|
232
src/client/index.scss
Normal file
232
src/client/index.scss
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
html, body, div, span, applet, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, embed,
|
||||||
|
figure, figcaption, footer, header, hgroup,
|
||||||
|
menu, nav, output, ruby, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol, ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
background-color: #212121;
|
||||||
|
color: #FFFFFF;
|
||||||
|
--active-color: rgb(0, 180, 204);
|
||||||
|
--title-font-family: Ubuntu, "Droid Sans", sans-serif;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #777 #333;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #777;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 3px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
align-items: left;
|
||||||
|
background-color: rgba(255, 255, 255, 0.025);
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-family: "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
min-height: 3em;
|
||||||
|
padding: 0.5em 0.5em 0.5em 1em;
|
||||||
|
user-select: none;
|
||||||
|
@media(min-width: 20em) {
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label:nth-of-type(2n+1) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable] {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.75em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
background-color: #151515;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
position: relative;
|
||||||
|
top: -0.5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #222222;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, input[type="checkbox"], input[type="checkbox"] + label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: #222222;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.75em;
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
box-shadow: 0 0 2px 0 var(--active-color);
|
||||||
|
outline: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab-list {
|
||||||
|
background-color: #272727;
|
||||||
|
font-family: var(--title-font-family);
|
||||||
|
font-size: 0.9em;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #2e1d1d;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #777;
|
||||||
|
border-radius: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #aaaaaa;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
height: 3em;
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
border-right: 1px solid #282828;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
align-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
.text {
|
||||||
|
height: 1.25em;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
padding: 0 0.25em 0 0.5em;
|
||||||
|
}
|
||||||
|
.close {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #999999;
|
||||||
|
padding: 0.25em;
|
||||||
|
visibility: hidden;
|
||||||
|
&:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover .close {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab--selected[class] {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab-panel {
|
||||||
|
display: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100% - 2.7em);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab-panel--selected {
|
||||||
|
display: block;
|
||||||
|
}
|
15
src/client/store.js
Normal file
15
src/client/store.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import merge from 'deepmerge';
|
||||||
|
|
||||||
|
import createCommonStore from '~/common/store';
|
||||||
|
|
||||||
|
export default function createStore(options = {}) {
|
||||||
|
return createCommonStore(
|
||||||
|
merge(
|
||||||
|
options,
|
||||||
|
{
|
||||||
|
middleware: [],
|
||||||
|
reducer: () => {},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
13
src/common/environment.js
Normal file
13
src/common/environment.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
const withDefault = (variable, value) => ('undefined' !== typeof variable ? variable : value);
|
||||||
|
|
||||||
|
export const mysqlOrigin = withDefault(
|
||||||
|
MYSQL_ORIGIN,
|
||||||
|
'mysql://root:UNSAFE_DEV_PASSWORD@localhost:31347/reddichat',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const redisOrigin = withDefault(
|
||||||
|
REDIS_ORIGIN,
|
||||||
|
'localhost:31346',
|
||||||
|
);
|
BIN
src/common/favicon.ico
Normal file
BIN
src/common/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
2
src/common/store/effects.js
vendored
Normal file
2
src/common/store/effects.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export default {
|
||||||
|
};
|
18
src/common/store/index.js
Normal file
18
src/common/store/index.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import merge from 'deepmerge';
|
||||||
|
import {configureStore, getDefaultMiddleware} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import commonMiddleware from './middleware';
|
||||||
|
|
||||||
|
export default function createStore(options = {}) {
|
||||||
|
return configureStore(
|
||||||
|
merge(
|
||||||
|
{
|
||||||
|
middleware: [
|
||||||
|
...getDefaultMiddleware(),
|
||||||
|
commonMiddleware,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
16
src/common/store/middleware.js
Normal file
16
src/common/store/middleware.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import effects from './effects';
|
||||||
|
|
||||||
|
const debug = require('debug')('persea:common:store');
|
||||||
|
|
||||||
|
export default (store) => (next) => (action) => {
|
||||||
|
const {meta, payload} = action;
|
||||||
|
debug("action '%s' dispatched: %o", action.type, {
|
||||||
|
payload,
|
||||||
|
meta,
|
||||||
|
});
|
||||||
|
const result = next(action);
|
||||||
|
if (effects[action.type]) {
|
||||||
|
setTimeout(() => effects[action.type](store, action), 0);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
30
src/server/app.js
Normal file
30
src/server/app.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import express from 'express';
|
||||||
|
import httpSession from 'express-session';
|
||||||
|
import passport from 'passport';
|
||||||
|
|
||||||
|
import {allModels} from './models/registrar';
|
||||||
|
import userRoutes from './routes/user';
|
||||||
|
import session from './session';
|
||||||
|
|
||||||
|
let insideSession;
|
||||||
|
|
||||||
|
passport.serializeUser((user, fn) => fn(null, user.id));
|
||||||
|
passport.deserializeUser(async (id, fn) => {
|
||||||
|
const {User} = allModels();
|
||||||
|
try {
|
||||||
|
fn(undefined, await User.findByPk(id));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
fn(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function createApp() {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.urlencoded({extended: true}));
|
||||||
|
app.use(session());
|
||||||
|
app.use(passport.initialize());
|
||||||
|
app.use(passport.session());
|
||||||
|
userRoutes(app);
|
||||||
|
return app;
|
||||||
|
}
|
38
src/server/db.js
Normal file
38
src/server/db.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import {registerHooks} from 'scwp';
|
||||||
|
import Sequelize from 'sequelize';
|
||||||
|
|
||||||
|
import {mysqlOrigin} from '~/common/environment';
|
||||||
|
|
||||||
|
import {allModels} from './models/registrar';
|
||||||
|
|
||||||
|
let map;
|
||||||
|
|
||||||
|
export async function createDatabaseConnection() {
|
||||||
|
const Models = allModels();
|
||||||
|
const sequelize = new Sequelize(mysqlOrigin);
|
||||||
|
Models.filter((Model) => Model.attributes).forEach((Model) => {
|
||||||
|
Model.init(Model.attributes, {
|
||||||
|
sequelize,
|
||||||
|
underscored: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
map = Models.reduce((r, Model) => ({...r, [Model.name]: Model}), {});
|
||||||
|
Models.forEach((Model) => Model.associate(map));
|
||||||
|
Models.forEach((Model) => Model.sync());
|
||||||
|
await sequelize.authenticate();
|
||||||
|
await sequelize.sync();
|
||||||
|
return sequelize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroyDatabaseConnection(databaseConnection) {
|
||||||
|
if (!databaseConnection) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return databaseConnection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHooks({
|
||||||
|
replContext: () => ({
|
||||||
|
Models: map,
|
||||||
|
}),
|
||||||
|
}, module.id);
|
48
src/server/http.js
Normal file
48
src/server/http.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import express from 'express';
|
||||||
|
import http, {ServerResponse} from 'http';
|
||||||
|
import {join} from 'path';
|
||||||
|
|
||||||
|
import httpProxy from 'http-proxy';
|
||||||
|
|
||||||
|
import createApp from './app';
|
||||||
|
|
||||||
|
let app;
|
||||||
|
|
||||||
|
export function createHttpServer() {
|
||||||
|
app = createApp();
|
||||||
|
const httpServer = http.createServer(app);
|
||||||
|
httpServer.listen(31344, '0.0.0.0');
|
||||||
|
if ('production' !== process.env.NODE_ENV) {
|
||||||
|
const proxy = httpProxy.createProxyServer({
|
||||||
|
secure: false,
|
||||||
|
target: 'http://127.0.0.1:31345',
|
||||||
|
});
|
||||||
|
proxy.on('error', (err, req, res) => {
|
||||||
|
if (!(res instanceof ServerResponse)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(500, {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
});
|
||||||
|
res.end('Bad Proxy');
|
||||||
|
});
|
||||||
|
app.get('*', (req, res) => proxy.web(req, res));
|
||||||
|
httpServer.on('close', () => proxy.close());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
app.use(express.static(join(__dirname, '..', 'client')));
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(join(__dirname, '..', 'client', 'index.html'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroyHttpServer(httpServer) {
|
||||||
|
if (!httpServer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
httpServer.close();
|
||||||
|
}
|
65
src/server/index.js
Normal file
65
src/server/index.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import {registerHooks} from 'scwp';
|
||||||
|
|
||||||
|
import {createDatabaseConnection, destroyDatabaseConnection} from './db';
|
||||||
|
import {createHttpServer, destroyHttpServer} from './http';
|
||||||
|
import {createReplServer, destroyReplServer} from './repl';
|
||||||
|
import {createSocketServer, destroySocketServer} from './sockets';
|
||||||
|
|
||||||
|
let httpServer;
|
||||||
|
let replServer;
|
||||||
|
let socketServer;
|
||||||
|
let databaseConnection;
|
||||||
|
|
||||||
|
const connectionSet = new Set();
|
||||||
|
function trackConnections() {
|
||||||
|
const trackConnection = (connection) => {
|
||||||
|
connectionSet.add(connection);
|
||||||
|
connection.once('close', () => {
|
||||||
|
connectionSet.delete(connection);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
httpServer.on('connection', trackConnection);
|
||||||
|
replServer.on('connection', trackConnection);
|
||||||
|
}
|
||||||
|
function releaseConnections() {
|
||||||
|
const connections = Array.from(connectionSet.values());
|
||||||
|
for (let i = 0; i < connections.length; i++) {
|
||||||
|
connections[i].destroy();
|
||||||
|
}
|
||||||
|
connectionSet.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartListening() {
|
||||||
|
// Destroy...
|
||||||
|
await destroyDatabaseConnection(databaseConnection);
|
||||||
|
await destroyReplServer(replServer);
|
||||||
|
await destroySocketServer(socketServer);
|
||||||
|
await destroyHttpServer(httpServer);
|
||||||
|
await releaseConnections();
|
||||||
|
// Create...
|
||||||
|
databaseConnection = await createDatabaseConnection();
|
||||||
|
httpServer = await createHttpServer();
|
||||||
|
replServer = await createReplServer();
|
||||||
|
socketServer = await createSocketServer(httpServer);
|
||||||
|
// Accounting bullshit
|
||||||
|
trackConnections();
|
||||||
|
}
|
||||||
|
restartListening();
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
module.hot.accept([
|
||||||
|
'./db.js',
|
||||||
|
'./http.js',
|
||||||
|
'./repl.js',
|
||||||
|
'./sockets.js',
|
||||||
|
], restartListening);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHooks({
|
||||||
|
replContext: () => ({
|
||||||
|
databaseConnection,
|
||||||
|
httpServer,
|
||||||
|
replServer,
|
||||||
|
socketServer,
|
||||||
|
}),
|
||||||
|
}, module.id);
|
14
src/server/models/base.js
Normal file
14
src/server/models/base.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import {Model} from 'sequelize';
|
||||||
|
|
||||||
|
class BaseModel extends Model {
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
static associate(map) {}
|
||||||
|
|
||||||
|
static attributes() {
|
||||||
|
throw new ReferenceError('You must define a static attributes() method in your model.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseModel;
|
24
src/server/models/friendship.model.js
Normal file
24
src/server/models/friendship.model.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import BaseModel from './base';
|
||||||
|
|
||||||
|
class Friendship extends BaseModel {
|
||||||
|
|
||||||
|
static associate({User}) {
|
||||||
|
User.hasMany(this, {
|
||||||
|
as: 'adder',
|
||||||
|
foreignKey: 'adderId',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
});
|
||||||
|
User.hasMany(this, {
|
||||||
|
as: 'addee',
|
||||||
|
foreignKey: 'addeeId',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get name() {
|
||||||
|
return 'Friendship';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Friendship;
|
3
src/server/models/models.scwp.js
Normal file
3
src/server/models/models.scwp.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = (scwp) => {
|
||||||
|
scwp.autoreg('model');
|
||||||
|
};
|
24
src/server/models/permission.model.js
Normal file
24
src/server/models/permission.model.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {DataTypes as Types} from 'sequelize';
|
||||||
|
|
||||||
|
import BaseModel from './base';
|
||||||
|
|
||||||
|
class Permission extends BaseModel {
|
||||||
|
|
||||||
|
static get attributes() {
|
||||||
|
return {
|
||||||
|
name: Types.STRING,
|
||||||
|
label: Types.STRING,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static associate({User}) {
|
||||||
|
this.belongsToMany(User, {through: 'user_permissions'});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get name() {
|
||||||
|
return 'Permission';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Permission;
|
39
src/server/models/registrar.js
Normal file
39
src/server/models/registrar.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import {registerHooks} from 'scwp';
|
||||||
|
|
||||||
|
import {all} from './models.scwp';
|
||||||
|
|
||||||
|
const modelTo = new Map();
|
||||||
|
const nameTo = new Map();
|
||||||
|
|
||||||
|
export function allModels() {
|
||||||
|
return Object.entries(all()).map(([, M]) => M.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasMapped = false;
|
||||||
|
function ensureNameMap() {
|
||||||
|
if (!hasMapped) {
|
||||||
|
const entries = Object.entries(all());
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const [, M] = entries[i];
|
||||||
|
const {default: Model} = M;
|
||||||
|
nameTo.set(Model.name, Model);
|
||||||
|
modelTo.set(Model, M);
|
||||||
|
}
|
||||||
|
hasMapped = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerHooks({
|
||||||
|
autoreg$accept: (type, M) => {
|
||||||
|
if ('model' === type) {
|
||||||
|
const {default: Model} = M;
|
||||||
|
nameTo.set(Model.name, Model);
|
||||||
|
modelTo.set(Model, M);
|
||||||
|
hasMapped = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, module.id);
|
||||||
|
|
||||||
|
export function lookupModel(name) {
|
||||||
|
ensureNameMap();
|
||||||
|
return nameTo.get(name);
|
||||||
|
}
|
55
src/server/models/user.model.js
Normal file
55
src/server/models/user.model.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import {registerHooks} from 'scwp';
|
||||||
|
import {DataTypes as Types} from 'sequelize';
|
||||||
|
|
||||||
|
import BaseModel from './base';
|
||||||
|
|
||||||
|
class User extends BaseModel {
|
||||||
|
|
||||||
|
static get attributes() {
|
||||||
|
return {
|
||||||
|
email: Types.STRING,
|
||||||
|
isAdmin: {
|
||||||
|
type: Types.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
hash: Types.STRING,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static associate({Permission}) {
|
||||||
|
this.belongsToMany(Permission, {through: 'user_permissions'});
|
||||||
|
}
|
||||||
|
|
||||||
|
addHashedPassword(plaintext) {
|
||||||
|
return bcrypt.hash(plaintext, this.constructor.saltRounds).then((hash) => {
|
||||||
|
this.hash = hash;
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePassword(plaintext) {
|
||||||
|
return bcrypt.compare(plaintext, this.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get name() {
|
||||||
|
return 'User';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change this on a running production site and lose all authentications.
|
||||||
|
User.saltRounds = 10;
|
||||||
|
|
||||||
|
export default User;
|
||||||
|
|
||||||
|
registerHooks({
|
||||||
|
replCommands: () => ({
|
||||||
|
createUser: (spec) => {
|
||||||
|
const [email, maybePassword] = spec.split(' ', 2);
|
||||||
|
const password = maybePassword || crypto.randomBytes(8).toString('hex');
|
||||||
|
const user = User.build({email});
|
||||||
|
user.addHashedPassword(password).then(() => user.save());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}, module.id);
|
33
src/server/repl.js
Normal file
33
src/server/repl.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import net from 'net';
|
||||||
|
import repl from 'repl';
|
||||||
|
import {invokeHookFlat} from 'scwp';
|
||||||
|
|
||||||
|
export function createReplServer() {
|
||||||
|
const netServer = net.createServer((socket) => {
|
||||||
|
const replServer = repl.start({
|
||||||
|
prompt: 'reddichat> ',
|
||||||
|
input: socket,
|
||||||
|
output: socket,
|
||||||
|
});
|
||||||
|
replServer.on('exit', () => socket.end());
|
||||||
|
Object.entries(
|
||||||
|
invokeHookFlat('replContext').reduce((r, vars) => ({...r, ...vars}), {}),
|
||||||
|
).forEach(([key, value]) => {
|
||||||
|
replServer.context[key] = value;
|
||||||
|
});
|
||||||
|
Object.entries(
|
||||||
|
invokeHookFlat('replCommands').reduce((r, commands) => ({...r, ...commands}), {}),
|
||||||
|
).forEach(([key, value]) => {
|
||||||
|
replServer.defineCommand(key, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
netServer.listen(`/tmp/reddichat-${Date.now()}.sock`);
|
||||||
|
return netServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroyReplServer(replServer) {
|
||||||
|
if (!replServer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
replServer.close();
|
||||||
|
}
|
4
src/server/routes/user.js
Normal file
4
src/server/routes/user.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import passport from 'passport';
|
||||||
|
|
||||||
|
export default function userRoutes(app) {
|
||||||
|
}
|
16
src/server/session.js
Normal file
16
src/server/session.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import redis from 'redis';
|
||||||
|
import session from 'express-session';
|
||||||
|
|
||||||
|
const redisClient = redis.createClient();
|
||||||
|
// eslint-disable-next-line import/newline-after-import
|
||||||
|
const RedisStore = require('connect-redis')(session);
|
||||||
|
|
||||||
|
export default (options = {}) => (
|
||||||
|
session({
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
secret: process.env.COOKIE_SECRET || 'UNSAFE_DEV_COOKIE',
|
||||||
|
store: new RedisStore({client: redisClient}),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
);
|
32
src/server/sockets.js
Normal file
32
src/server/sockets.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import redisAdapter from 'socket.io-redis';
|
||||||
|
|
||||||
|
import {SocketServer} from '@avocado/net/server/socket';
|
||||||
|
import socketSession from 'express-socket.io-session';
|
||||||
|
|
||||||
|
import {redisOrigin} from '~/common/environment';
|
||||||
|
|
||||||
|
import session from './session';
|
||||||
|
|
||||||
|
export function createSocketServer(httpServer) {
|
||||||
|
const [redisHost, redisPort] = redisOrigin.split(':');
|
||||||
|
const socketServer = new SocketServer(httpServer, {
|
||||||
|
adapter: redisAdapter({
|
||||||
|
host: redisHost || 'localhost',
|
||||||
|
port: redisPort || 31346,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
socketServer.on('connect', (socket) => {
|
||||||
|
socket.on('packet', (packet, fn) => {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
socketServer.io.use(socketSession(session()));
|
||||||
|
return socketServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroySocketServer(socketServer) {
|
||||||
|
if (!socketServer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socketServer.close();
|
||||||
|
}
|
9
webpack.config.js
Normal file
9
webpack.config.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Whilst the configuration object can be modified here, the recommended way of making
|
||||||
|
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
|
||||||
|
// Neutrino's inspect feature can be used to view/export the generated configuration.
|
||||||
|
const neutrino = require('neutrino');
|
||||||
|
|
||||||
|
const side = require('./side');
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-dynamic-require
|
||||||
|
module.exports = neutrino(require(`./.neutrinorc.${side}`)).webpack();
|
Loading…
Reference in New Issue
Block a user