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