chore: initial

This commit is contained in:
cha0s 2020-07-12 03:45:53 -05:00
commit 7469ceecb1
45 changed files with 11524 additions and 0 deletions

33
.eslint.defaults.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
TODO.md Normal file
View File

0
attribution.txt Normal file
View File

33
docker-compose.yml Normal file
View 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
View File

@ -0,0 +1,5 @@
const neutrino = require('neutrino');
const side = require('./side');
neutrino(require(`./.neutrinorc.${side}`)).inspect();

134
middleware.js Normal file
View 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
View 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
View File

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

8
side.js Normal file
View 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
View 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
View 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%;
}
}

View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

2
src/common/store/effects.js vendored Normal file
View File

@ -0,0 +1,2 @@
export default {
};

18
src/common/store/index.js Normal file
View 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,
),
);
}

View 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
View 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
View 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
View 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
View 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
View 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;

View 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;

View File

@ -0,0 +1,3 @@
module.exports = (scwp) => {
scwp.autoreg('model');
};

View 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;

View 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);
}

View 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
View 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();
}

View File

@ -0,0 +1,4 @@
import passport from 'passport';
export default function userRoutes(app) {
}

16
src/server/session.js Normal file
View 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
View 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
View 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();

10054
yarn.lock Normal file

File diff suppressed because it is too large Load Diff