initial webserver setup

This commit is contained in:
Evert Prants 2017-08-03 00:24:01 +03:00
parent b8c1f26098
commit 5536ecbf58
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
26 changed files with 6767 additions and 3 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.js]
charset = utf-8
indent_style = space
indent_size = 2
[*.css,*.styl]
indent_size = space
indent_size = 4

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/node_modules/
/build
/config.toml

16
icynet.eu.js Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env node
const path = require('path')
if (process.argv.indexOf('-d') === -1 && process.argv.indexOf('--development') === -1) {
process.env.NODE_ENV = 'production'
}
require('babel-core/register')({
plugins: [
'transform-es2015-modules-commonjs',
'syntax-async-functions',
'transform-async-to-generator'
]
})
require(path.join(__dirname, 'server'))

3
knexfile.js Normal file
View File

@ -0,0 +1,3 @@
const config = require('./scripts/load-config.js')
module.exports = config.database

View File

@ -0,0 +1,180 @@
exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.createTable('users', (table) => {
table.increments('id').primary()
table.string('username', 26).unique().notNullable()
table.string('display_name', 32).notNullable()
table.string('email').notNullable()
table.string('avatar_file')
table.text('password').notNullable()
table.boolean('activated').defaultTo(false)
table.boolean('locked').defaultTo(false)
table.integer('nw_privilege').defaultTo(0)
table.string('ip_address').notNullable()
table.dateTime('activity_at')
table.timestamps()
}),
knex.schema.createTable('external', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.string('service')
table.text('identifier')
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
table.timestamps()
}),
knex.schema.createTable('simple_token', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.integer('type').notNullable()
table.text('token')
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
table.dateTime('expires_at')
}),
knex.schema.createTable('oauth2_client', (table) => {
table.increments('id').primary()
table.string('title')
table.text('description')
table.text('url')
table.text('redirect_url')
table.text('icon')
table.text('secret')
table.text('scopes')
table.text('grants')
table.integer('user_id').unsigned().notNullable()
table.boolean('verified')
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
table.timestamps()
}),
knex.schema.createTable('oauth2_client_authorization', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.integer('client_id').unsigned().notNullable()
table.dateTime('expires_at')
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
table.timestamps()
}),
knex.schema.createTable('oauth2_code', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.integer('client_id').unsigned().notNullable()
table.text('code')
table.text('scopes')
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
table.dateTime('expires_at')
table.timestamps()
}),
knex.schema.createTable('oauth2_access_token', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.integer('client_id').unsigned().notNullable()
table.text('token')
table.text('scopes')
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
table.dateTime('expires_at')
table.timestamps()
}),
knex.schema.createTable('oauth2_refresh_token', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.integer('client_id').unsigned().notNullable()
table.text('token')
table.text('scopes')
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
table.timestamps()
}),
knex.schema.createTable('totp_token', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.string('token')
table.string('recovery_code')
table.boolean('activated')
table.foreign('user_id').references('users.id').onDelete('CASCADE').onUpdate('CASCADE')
table.timestamps()
}),
knex.schema.createTable('network_ban', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.integer('admin_id').unsigned().notNullable()
table.string('associated_ip')
table.string('reason')
table.dateTime('expires_at')
table.timestamps()
}),
knex.schema.createTable('news', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().notNullable()
table.string('title')
table.string('slug')
table.text('content')
table.text('tags')
table.timestamps()
}),
knex.schema.createTable('donation', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned()
table.string('amount')
table.string('source')
table.text('note')
table.boolean('read')
table.timestamps()
}),
knex.schema.createTable('subscription', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned()
table.timestamps()
})
])
}
exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('sessions'),
knex.schema.dropTable('users'),
knex.schema.dropTable('external'),
knex.schema.dropTable('simple_token'),
knex.schema.dropTable('oauth2_client'),
knex.schema.dropTable('oauth2_code'),
knex.schema.dropTable('oauth2_access_token'),
knex.schema.dropTable('oauth2_client_authorization'),
knex.schema.dropTable('totp_token'),
knex.schema.dropTable('network_ban'),
knex.schema.dropTable('news'),
knex.schema.dropTable('donation'),
knex.schema.dropTable('subscription')
])
}

4687
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,17 @@
"name": "icynet.eu",
"version": "0.0.1-alpha1",
"description": "Icy Network web server",
"main": "icynet.js",
"main": "icynet.eu.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"start": "node icynet.eu.js",
"test": "echo \"Error: no test specified\" && exit 1",
"css": "mkdir -p build/style && stylus -o build/style src/style/*.styl",
"css:watch": "mkdir -p build/style && stylus -w -o build/style src/style/*.styl",
"js": "mkdir -p build/script && browserify src/script/main.js -o build/script/main.js && uglifyjs --overwrite build/script/main.js",
"js:watch": "mkdir -p build/script && watchify src/script/main.js -o build/script/main.js",
"watch": "concurrently --kill-others \"npm run css:watch\" \"npm run js:watch\"",
"clean": "rm -rf build/",
"build": "npm run clean && npm run css && npm run js"
},
"repository": {
"type": "git",
@ -20,5 +28,41 @@
"bugs": {
"url": "https://github.com/IcyNet/IcyNet.eu/issues"
},
"homepage": "https://github.com/IcyNet/IcyNet.eu#readme"
"homepage": "https://github.com/IcyNet/IcyNet.eu#readme",
"dependencies": {
"babel-core": "^6.25.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.17.2",
"connect-redis": "^3.3.0",
"connect-session-knex": "^1.3.4",
"express": "^4.15.3",
"express-session": "^1.15.3",
"knex": "^0.13.0",
"mysql": "^2.13.0",
"nodemailer": "^4.0.1",
"notp": "^2.0.3",
"objection": "^0.8.4",
"pug": "^2.0.0-rc.3",
"stylus": "^0.54.5",
"thirty-two": "^1.0.2",
"toml": "^2.3.2",
"uuid": "^3.1.0"
},
"devDependencies": {
"browserify": "^14.4.0",
"concurrently": "^3.5.0",
"jquery": "^3.2.1",
"standard": "^10.0.2",
"uglify-js": "^1.3.5",
"watchify": "^3.9.0"
},
"standard": {
"env": {
"browser": true,
"node": true
}
}
}

1
scripts/asyncRoute.js Normal file
View File

@ -0,0 +1 @@
module.exports = fn => (...args) => fn(...args).catch(args[2])

38
scripts/bcrypt.js Normal file
View File

@ -0,0 +1,38 @@
const bcrypt = require('bcryptjs')
function hashPassword (password, rounds) {
const salt = bcrypt.genSaltSync(parseInt(rounds, 10))
const hash = bcrypt.hashSync(password, salt)
return hash
}
function comparePassword (password, hash) {
const evalu = bcrypt.compareSync(password, hash)
return evalu
}
function done (data) {
process.send(data)
setTimeout(() => {
process.exit(0)
}, 500)
}
process.once('message', (msg) => {
msg = msg.toString()
let param
let res
if (msg.indexOf('hash') === 0) {
param = JSON.parse(msg.substring(5))
res = hashPassword(param.password, 12)
done(res)
} else if (msg.indexOf('compare') === 0) {
param = JSON.parse(msg.substring(8))
res = comparePassword(param.password, param.hash)
done(res)
} else {
done(null)
}
})

43
scripts/flash.js Normal file
View File

@ -0,0 +1,43 @@
const util = require('util')
const format = util.format
/*
* Clean version of https://github.com/jaredhanson/connect-flash
* Included here to avoid includng ridiculously small modules
*/
module.exports = function (options) {
options = options || {}
let safe = (options.unsafe === undefined) ? true : !options.unsafe
return function (req, res, next) {
if (req.flash && safe) { return next() }
req.flash = _flash
next()
}
}
function _flash (type, msg) {
if (this.session === undefined) throw Error('req.flash() requires sessions')
let msgs = this.session.flash = this.session.flash || {}
if (type && msg) {
if (arguments.length > 2 && format) {
let args = Array.prototype.slice.call(arguments, 1)
msg = format.apply(undefined, args)
} else if (Array.isArray(msg)) {
msg.forEach((val) => {
(msgs[type] = msgs[type] || []).push(val)
})
return msgs[type].length
}
return (msgs[type] = msgs[type] || []).push(msg)
} else if (type) {
let arr = msgs[type]
delete msgs[type]
return arr || []
} else {
this.session.flash = {}
return msgs
}
}

15
scripts/load-config.js Normal file
View File

@ -0,0 +1,15 @@
const fs = require('fs')
const path = require('path')
const toml = require('toml')
const filename = path.join(__dirname, '..', 'config.toml')
let config
try {
config = toml.parse(fs.readFileSync(filename))
} catch (e) {
console.error(e)
process.exit(1)
}
module.exports = config

15
scripts/load-database.js Normal file
View File

@ -0,0 +1,15 @@
const path = require('path')
const knex = require('knex')
const objection = require('objection')
const knexfile = require(path.join(__dirname, '../knexfile'))
let knexDB = knex(knexfile)
let objectionModel = objection.Model
objectionModel.knex(knexDB)
module.exports = {
knex: knexDB,
Model: objectionModel,
destroy: knex.destroy
}

43
scripts/logger.js Normal file
View File

@ -0,0 +1,43 @@
function pz (z) {
if (z < 10) {
return '0' + z
}
return z
}
function dateFormat (date) {
return date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear() + ' ' +
pz(date.getHours()) + ':' + pz(date.getMinutes()) + ':' + pz(date.getSeconds())
}
const realConsoleLog = console.log
console.log = function () {
process.stdout.write('\x1b[2K\r')
process.stdout.write('[info] [' + dateFormat(new Date()) + '] ')
realConsoleLog.apply(this, arguments)
}
const realConsoleWarn = console.warn
console.warn = function () {
process.stdout.write('\x1b[2K\r')
process.stdout.write('[warn] [' + dateFormat(new Date()) + '] ')
realConsoleWarn.apply(this, arguments)
}
const realConsoleError = console.error
console.error = function () {
process.stdout.write('\x1b[2K\r')
process.stdout.write('[ err] [' + dateFormat(new Date()) + '] ')
realConsoleError.apply(this, arguments)
}
module.exports = function () {
this.logProcess = (pid, msg) => {
if (msg.indexOf('warn') === 0) {
msg = msg.substring(5)
} else if (msg.indexOf('error') === 0) {
msg = msg.substring(6)
}
console.log('[%s] %s', pid, msg)
}
}

131
server/api/index.js Normal file
View File

@ -0,0 +1,131 @@
import path from 'path'
import cprog from 'child_process'
import config from '../../scripts/load-config'
import database from '../../scripts/load-database'
import models from './models'
import crypto from 'crypto'
const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
function bcryptTask (data) {
return new Promise((resolve, reject) => {
let proc = cprog.fork(path.join(__dirname, '../../scripts', 'bcrypt.js'))
let done = false
proc.send(data.task + ' ' + JSON.stringify(data))
proc.on('message', (chunk) => {
if (chunk == null) return reject(new Error('No response'))
let line = chunk.toString().trim()
done = true
if (line === 'error') {
return reject(new Error(line))
}
if (line === 'true' || line === 'false') {
return resolve(line === 'true')
}
resolve(line)
})
proc.on('exit', () => {
if (!done) {
reject(new Error('No response'))
}
})
})
}
const API = {
User: {
get: async function (identifier) {
let scope = 'id'
if (typeof identifier === 'string') {
scope = 'username'
if (identifier.indexOf('@') !== -1) {
scope = 'email'
}
} else if (typeof identifier === 'object') {
if (identifier.id != null) {
identifier = identifier.id
} else if (identifier.username) {
scope = 'username'
identifier = identifier.username
} else {
return null
}
}
let user = await models.User.query().where(scope, identifier)
if (!user.length) return null
return user[0]
},
ensureObject: async function (user) {
if (!typeof user === 'object') {
return await API.User.get(user)
}
if (user.id) {
return user
}
return null
},
Login: {
password: async function (user, password) {
user = await API.User.ensureObject(user)
if (!user.password) return false
return bcryptTask({task: 'compare', password: password, hash: user.password})
},
activationToken: async function (token) {
let getToken = await models.Token.query().where('token', token)
if (!getToken || !getToken.length) return false
let user = await API.User.get(getToken[0].user_id)
if (!user) return false
await models.User.query().patchAndFetchById(user.id, {activated: 1})
await models.Token.query().delete().where('id', getToken[0].id)
return true
}
},
Register: {
hashPassword: async function (password) {
return bcryptTask({task: 'hash', password: password})
},
validateEmail: (email) => {
return emailRe.test(email)
},
newAccount: async function (regdata) {
let data = Object.assign(regdata, {
created_at: new Date()
})
let userTest = await API.User.get(regdata.username)
if (userTest) {
return {error: 'This username is already taken!'}
}
let emailTest = await API.User.get(regdata.email)
if (emailTest) {
return {error: 'This email address is already registered!'}
}
// Create user
let user = await models.User.query().insert(data)
// Activation token
let activationToken = crypto.randomBytes(16).toString('hex')
await models.Token.query().insert({
expires_at: new Date(Date.now() + 86400000), // 1 day
token: activationToken,
user_id: user.id,
type: 1
})
// TODO: Send email
console.log(activationToken)
return {error: null, user: user}
}
}
}
}
module.exports = API

95
server/api/models.js Normal file
View File

@ -0,0 +1,95 @@
import {Model} from '../../scripts/load-database'
class User extends Model {
static get tableName () {
return 'users'
}
}
class External extends Model {
static get tableName () {
return 'external'
}
}
class Token extends Model {
static get tableName () {
return 'simple_token'
}
}
class OAuth2Client extends Model {
static get tableName () {
return 'oauth2_client'
}
}
class OAuth2AuthorizedClient extends Model {
static get tableName () {
return 'oauth2_client_authorization'
}
}
class OAuth2Code extends Model {
static get tableName () {
return 'oauth2_client_authorization'
}
}
class OAuth2AccessToken extends Model {
static get tableName () {
return 'oauth2_access_token'
}
}
class OAuth2RefreshToken extends Model {
static get tableName () {
return 'oauth2_refresh_token'
}
}
class TotpToken extends Model {
static get tableName () {
return 'totp_token'
}
}
class Ban extends Model {
static get tableName () {
return 'network_ban'
}
}
class News extends Model {
static get tableName () {
return 'news'
}
}
class Donation extends Model {
static get tableName () {
return 'donation'
}
}
class Subscription extends Model {
static get tableName () {
return 'subscription'
}
}
module.exports = {
User: User,
External: External,
Token: Token,
OAuth2Client: OAuth2Client,
OAuth2AuthorizedClient: OAuth2AuthorizedClient,
OAuth2Code: OAuth2Code,
OAuth2AccessToken: OAuth2AccessToken,
OAuth2RefreshToken: OAuth2RefreshToken,
TotpToken: TotpToken,
Ban: Ban,
News: News,
Donation: Donation,
Subscription: Subscription
}

86
server/index.js Normal file
View File

@ -0,0 +1,86 @@
'use strict'
import config from '../scripts/load-config.js'
import Logger from '../scripts/logger.js'
import cluster from 'cluster'
import path from 'path'
const cpuCount = require('os').cpus().length
const workers = []
const logger = new Logger()
const args = {
dev: process.env.NODE_ENV !== 'production',
port: config.server.port
}
async function initialize () {
try {
const knex = require('knex')(require('../knexfile'))
console.log('Initializing database...')
await knex.migrate.latest()
console.log('Database initialized')
await knex.destroy()
} catch (err) {
console.error('Database error:', err)
}
let workerCount = config.server.workers === 0 ? cpuCount : config.server.workers
console.log('Spinning up ' + workerCount + ' worker process' + (workerCount !== 1 ? 'es' : ''))
for (let i = 0; i < workerCount; i++) {
spawnWorker()
}
}
function spawnWorker (oldWorker) {
const w = cluster.fork()
w.process.stdout.on('data', (data) => {
console.log(w.process.pid, data.toString().trim())
})
w.process.stderr.on('data', (data) => {
console.log(w.process.pid, data.toString().trim())
})
args.verbose && console.log('Starting worker process ' + w.process.pid + '...')
w.on('message', (message) => {
if (message === 'started') {
workers.push(w)
args.verbose && console.log('Started worker process ' + w.process.pid)
if (oldWorker) {
args.verbose && console.log('Stopping worker process ' + oldWorker.process.pid)
oldWorker.send('stop')
}
} else {
logger.logProcess(w.process.pid, message)
}
})
args.id = w.process.pid
w.send(args)
return w
}
console.log('Initializing')
cluster.setupMaster({
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
exec: path.join(__dirname, './worker.js')
})
cluster.on('exit', (worker, code, signal) => {
let extra = ((code || '') + ' ' + (signal || '')).trim()
console.error('Worker process ' + worker.process.pid + ' exited ' + (extra ? '(' + extra + ')' : ''))
let index = workers.indexOf(worker)
if (index !== -1) workers.splice(index, 1)
if (code === 0) return
setTimeout(() => {
spawnWorker()
}, 10 * 1000)
})
initialize()

206
server/routes/index.js Normal file
View File

@ -0,0 +1,206 @@
import express from 'express'
import parseurl from 'parseurl'
import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute'
import API from '../api'
let router = express.Router()
router.use(wrap(async (req, res, next) => {
let messages = req.flash('message')
if (!messages || !messages.length) {
messages = {}
} else {
messages = messages[0]
}
res.locals.message = messages
next()
}))
/*
================
RENDER VIEWS
================
*/
router.get('/', wrap(async (req, res) => {
res.render('index')
}))
router.get('/login', wrap(async (req, res) => {
if (req.session.user) {
let uri = '/'
if (req.session.redirectUri) {
uri = req.session.redirectUri
delete req.session.redirectUri
}
return res.redirect(uri)
}
res.render('login')
}))
router.get('/register', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/')
let dataSave = req.flash('formkeep')
if (dataSave.length) {
dataSave = dataSave[0]
} else {
dataSave = {}
}
res.locals.formkeep = dataSave
res.render('register')
}))
/*
=================
POST HANDLING
=================
*/
function formError (req, res, error, path) {
req.flash('formkeep', req.body || {})
req.flash('message', {error: true, text: error})
res.redirect(path || parseurl(req).path)
}
router.post('/login', wrap(async (req, res) => {
if (!req.body.username || !req.body.password) {
return res.redirect('/login')
}
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
let user = await API.User.get(req.body.username)
if (!user) return formError(req, res, 'Invalid username or password.')
let pwMatch = await API.User.Login.password(user, req.body.password)
if (!pwMatch) return formError(req, res, 'Invalid username or password.')
if (user.activated === 0) return formError(req, res, 'Please activate your account first.')
if (user.locked === 1) return formError(req, res, 'This account has been locked.')
// TODO: TOTP checks
// TODO: Ban checks
// Set session
req.session.user = {
id: user.id,
username: user.username,
display_name: user.display_name,
email: user.email,
avatar_file: user.avatar_file
}
let uri = '/'
if (req.session.redirectUri) {
uri = req.session.redirectUri
delete req.session.redirectUri
}
if (req.query.redirect) {
uri = req.query.redirect
}
res.redirect(uri)
}))
router.post('/register', wrap(async (req, res) => {
if (!req.body.username || !req.body.display_name || !req.body.password || !req.body.email) {
return formError(req, res, 'Please fill in all the fields.')
}
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
// 1st Check: Username Characters and length
let username = req.body.username
if (!username || !username.match(/^([\w-]{3,26})$/i)) {
return formError(req, res, 'Invalid username! Must be between 3-26 characters and composed of alphanumeric characters!')
}
// 2nd Check: Display Name
let display_name = req.body.display_name
if (!display_name || !display_name.match(/^([^\\`]{3,32})$/i)) {
return formError(req, res, 'Invalid display name!')
}
// 3rd Check: Email Address
let email = req.body.email
if (!email || !API.User.Register.validateEmail(email)) {
return formError(req, res, 'Invalid email address!')
}
// 4th Check: Password length
let password = req.body.password
if (!password || password.length < 8 || password.length > 32) {
return formError(req, res, 'Invalid password! Keep it between 8 and 32 characters!')
}
// 5th Check: Password match
let passwordAgain = req.body.password_repeat
if (!passwordAgain || password !== passwordAgain) {
return formError(req, res, 'Passwords do not match!')
}
// TODO: Add reCaptcha
// Hash the password
let hash = await API.User.Register.hashPassword(password)
// Attempt to create the user
let newUser = await API.User.Register.newAccount({
username: username,
display_name: display_name,
password: hash,
email: email,
ip_address: req.realIP
})
if (!newUser || newUser.error != null) {
return formError(req, res, newUser.error)
}
req.flash('message', {error: false, text: 'Account created successfully! Please check your email for an activation link.'})
res.redirect('/login')
}))
router.get('/activate/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login')
let token = req.params.token
let success = await API.User.Login.activationToken(token)
if (!success) return formError(req, res, 'Unknown or invalid activation token')
req.flash('message', {error: false, text: 'Your account has been activated! You may now log in.'})
res.redirect('/login')
}))
/*
=========
OTHER
=========
*/
router.get('/logout', wrap(async (req, res) => {
if (req.session.user) {
delete req.session.user
}
res.redirect('/')
}))
router.use((err, req, res, next) => {
console.error(err)
next()
})
module.exports = router

72
server/server.js Normal file
View File

@ -0,0 +1,72 @@
import express from 'express'
import session from 'express-session'
import bodyParser from 'body-parser'
import connectSession from 'connect-redis'
import path from 'path'
import crypto from 'crypto'
import fs from 'fs'
import routes from './routes'
import flash from '../scripts/flash'
import config from '../scripts/load-config'
import database from '../scripts/load-database'
let app = express()
let SessionStore = connectSession(session)
app.enable('trust proxy', 1)
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use(flash())
app.disable('x-powered-by')
app.use(session({
key: config.server.session_key,
secret: config.server.session_secret,
store: new SessionStore(config.redis),
resave: false,
saveUninitialized: true
}))
app.use((req, res, next) => {
let ipAddr = req.headers['x-forwarded-for'] || req.connection.remoteAddress
if (ipAddr.indexOf('::ffff:') !== -1) {
ipAddr = ipAddr.replace('::ffff:', '')
}
if (!req.session.csrf) {
req.session.csrf = crypto.randomBytes(12).toString('hex')
}
req.realIP = ipAddr
res.locals = Object.assign(res.locals, {
user: req.session.user || null,
csrf: req.session.csrf
})
next()
})
module.exports = (args) => {
app.set('view options', {layout: false})
app.set('view engine', 'pug')
app.set('views', path.join(__dirname, '../views'))
if (args.dev) console.log('Worker is in development mode')
let staticAge = args.dev ? 1000 : 7 * 24 * 60 * 60 * 1000
app.use('/style', express.static(path.join(__dirname, '../build/style'), { maxAge: staticAge }))
app.use('/script', express.static(path.join(__dirname, '../build/script'), { maxAge: staticAge }))
app.use('/static', express.static(path.join(__dirname, '../static'), { maxAge: staticAge }))
app.use(routes)
app.listen(args.port, () => {
console.log('Listening on 0.0.0.0:' + args.port)
})
}

52
server/worker.js Normal file
View File

@ -0,0 +1,52 @@
'use strict'
const path = require('path')
const util = require('util')
require('babel-core/register')({
plugins: [
'transform-es2015-modules-commonjs',
'syntax-async-functions',
'transform-async-to-generator'
]
})
process.once('message', (args) => {
if (args.dev) {
process.env.NODE_ENV = 'development'
} else {
process.env.NODE_ENV = 'production'
}
console.log = function () {
process.send(util.format.apply(this, arguments))
}
console.warn = function () {
process.send('warn ' + util.format.apply(this, arguments))
}
console.error = function () {
process.send('error ' + util.format.apply(this, arguments))
}
try {
require(path.join(__dirname, 'server'))(args)
console.log('Worker process starting')
} catch (e) {
console.error(e)
}
process.on('message', (message) => {
if (message === 'stop') {
console.log('Recieved stop signal')
const knex = require(path.join(__dirname, '../scripts/load-database')).knex
knex.destroy(() => {
process.exit(0)
})
}
})
process.send('started')
})

37
src/script/main.js Normal file
View File

@ -0,0 +1,37 @@
window.$ = require('jquery')
$(document).ready(function () {
$(window).on('scroll', function() {
if($(window).scrollTop() >= $('.banner').innerHeight()) {
$('.anchor').css('height', $('.navigator').innerHeight() + 'px')
$('#navlogo').removeClass('hidden')
$('.navigator').addClass('fix')
} else {
$('#navlogo').addClass('hidden')
$('.navigator').removeClass('fix')
$('.anchor').css('height', '0px')
}
})
if($(window).scrollTop() >= $('.banner').innerHeight()) {
$('#navlogo').removeClass('hidden')
$('.navigator').addClass('fix')
$('.anchor').css('height', $('.navigator').innerHeight() + 'px')
}
$('a[href*=\\#]').on('click', function (e) {
if (!$(this.hash).length) return
e.preventDefault()
let dest = 0
if ($(this.hash).offset().top > $(document).height() - $(window).height()) {
dest = $(document).height() - $(window).height()
} else {
dest = $(this.hash).offset().top
}
$('html,body').animate({
scrollTop: dest - $('.navigator').innerHeight()
}, 1000, 'swing')
})
})

157
src/style/main.styl Normal file
View File

@ -0,0 +1,157 @@
body
margin: 0
color: black
font-family: sans-serif
background-color: #82fff4;
background-image: linear-gradient(-45deg, #80d7ff 25%, transparent 25.5%, transparent 50%,
#80d7ff 50.5%, #80d7ff 75%, transparent 75.5%, transparent)
height: 100vh;
background-size: 50px 50px;
.logo
font-size: 8vw
font-family: "Open Sans"
font-weight: bold
text-transform: uppercase
text-align: center
text-shadow: 2px 2px 1px #007104
color: #00b300
letter-spacing: 5px
user-select: none
cursor: default
.part1, .part2
display: inline-block
.part1
color: #03A9F4
text-shadow: 2px 2px 1px #0059a0
margin-right: 5px
&.small
font-size: 30px
text-align: inherit
cursor: pointer
display: inline-block
.mobview
display: none !important
.navigator
padding: 0 50px
transition: background-color 0.1s linear
background-color: rgb(0, 219, 247)
z-index: 5
&.fix
top: 0
left: 0
right: 0
position: fixed
background-color: rgba(0, 219, 247, 0.67)
#navlogo
&.hidden
display: none
ul
padding: 0
margin: 0
display: inline-block
list-style-type: none
&.floating
float: right
li
font-size: 30px
display: inline-block
height: 68px
line-height: 2.2
a
text-decoration: none
padding: 20px
color: #007cde
font-weight: bold
text-transform: uppercase
font-size: 25px
transition: background-color 0.1s linear
&:hover
background-color: rgba(255, 255, 255, 0.25)
section
font-family: "Open Sans"
position: relative
height: 100vh
.content
position: absolute
left: 0
right: 0
top: 0
bottom: 40%
background-color: #ffffff
padding: 40px
footer
padding: 20px
background-color: #fff
.copyright
display: block
text-align: center
.wrapper
overflow: hidden
height: 100vh
label
display: block
margin-top: 20px
input:not([type="submit"])
padding: 5px
font-size: 120%
border-radius: 5px
border: 1px solid #c1c1c1
background-color: #f5f5f5
box-shadow: inset 2px 2px 5px #ddd
input[type="submit"]
display: block
padding: 5px 10px
background-color: #fbfbfb
border: 1px solid #c1c1c1
border-radius: 5px
font-size: 120%
margin: 10px 0
cursor: pointer
.boxcont
.box
.left, .right
display: inline-block
width: 50%
.right
float: right
width: 46%
&#login
max-width: 700px
padding: 20px
margin: auto
margin-top: 5%
background-color: #fff
box-shadow: 5px 5px 15px #868686
border: 1px solid #ddd
h1, h2, h3
margin-top: 0
@media all and (max-width: 800px)
.navigator
padding: 0 10px
#navlogo
display: inline-block !important
.banner
display: none
.fullview
display: none !important
.mobview
display: inline-block !important
.logo
font-size: 10vw
@media all and (max-width: 500px)
.logo.small
font-size: 5vw

703
static/image/squeebot.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 158 KiB

11
views/index.pug Normal file
View File

@ -0,0 +1,11 @@
extends layout.pug
block title
|Icy Network
block body
section#home
.content
h1 Welcome to Icy Network!
section#news
.content
h1 Icy Network News

49
views/layout.pug Normal file
View File

@ -0,0 +1,49 @@
doctype html
html
head
meta(charset="utf8")
meta(name="viewport", content="width=device-width, initial-scale=1")
link(rel="stylesheet", type="text/css", href="/style/main.css")
link(rel="stylesheet", type="text/css", href="https://fonts.googleapis.com/css?family=Open+Sans")
link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css")
script(src="/script/main.js")
title
block title
body
block banner
.banner
.logo
.part1 Icy
.part2 Network
block nav
.anchor
nav.navigator
ul
li.hidden#navlogo
a(href="/")
.logo.small
.part1 Icy
.part2 Network
li.fullview
a.scroll(href="/#home") Home
li.fullview
a.scroll(href="/#news") News
ul.fullview.floating
if user
li#user
a(href="/user/manage") #{user.display_name}
li
a(href="/logout") Log out
else
li
a(href="/login") Log in
ul.mobview.floating
li
a#mobile(href="#")
i.fa.fa-fw.fa-bars
block body
block footer
footer
span.copyright &copy; 2017 - Icy Network - Some Rights Reserved
span.copyright Designed by
a(href="https://lunasqu.ee") LunaSquee

26
views/login.pug Normal file
View File

@ -0,0 +1,26 @@
extends layout.pug
block title
|Icy Network - Log In
block body
.wrapper
.boxcont
.box#login
.left
h1 Log in
if message
if message.error
.message.error
else
.message
span #{message.text}
form#loginForm(method="POST", action="")
input(type="hidden", name="csrf", value=csrf)
label(for="username") Username or Email Address
input(type="text", name="username", id="username")
label(for="password") Password
input(type="password", name="password", id="password")
input(type="submit", value="Log in")
a#create(href="/register") Create an account
.right
h3 More options

33
views/register.pug Normal file
View File

@ -0,0 +1,33 @@
extends layout.pug
block title
|Icy Network - Register
block body
.wrapper
.boxcont
.box#login
.left
h1 Create a new account
if message
if message.error
.message.error
else
.message
span #{message.text}
form#loginForm(method="POST", action="")
input(type="hidden", name="csrf", value=csrf)
label(for="username") Username
input(type="text", name="username", id="username")
label(for="display_name") Display Name
input(type="text", name="display_name", id="display_name")
label(for="email") Email Address
input(type="email", name="email", id="email")
label(for="password") Password
input(type="password", name="password", id="password")
label(for="password_repeat") Repeat Password
input(type="password", name="password_repeat", id="password_repeat")
div#repeatcheck(style="display: none")
input(type="submit", value="Register")
a#create(href="/login") Log in with an existing account
.right
h3 More options