initial webserver setup
This commit is contained in:
parent
b8c1f26098
commit
5536ecbf58
18
.editorconfig
Normal file
18
.editorconfig
Normal 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
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/node_modules/
|
||||
/build
|
||||
/config.toml
|
16
icynet.eu.js
Executable file
16
icynet.eu.js
Executable 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
3
knexfile.js
Normal file
@ -0,0 +1,3 @@
|
||||
const config = require('./scripts/load-config.js')
|
||||
|
||||
module.exports = config.database
|
180
migrations/20170801231334_initial.js
Normal file
180
migrations/20170801231334_initial.js
Normal 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
4687
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@ -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
1
scripts/asyncRoute.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = fn => (...args) => fn(...args).catch(args[2])
|
38
scripts/bcrypt.js
Normal file
38
scripts/bcrypt.js
Normal 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
43
scripts/flash.js
Normal 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
15
scripts/load-config.js
Normal 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
15
scripts/load-database.js
Normal 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
43
scripts/logger.js
Normal 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
131
server/api/index.js
Normal 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
95
server/api/models.js
Normal 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
86
server/index.js
Normal 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
206
server/routes/index.js
Normal 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
72
server/server.js
Normal 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
52
server/worker.js
Normal 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
37
src/script/main.js
Normal 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
157
src/style/main.styl
Normal 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
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
11
views/index.pug
Normal 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
49
views/layout.pug
Normal 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 © 2017 - Icy Network - Some Rights Reserved
|
||||
span.copyright Designed by
|
||||
a(href="https://lunasqu.ee") LunaSquee
|
26
views/login.pug
Normal file
26
views/login.pug
Normal 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
33
views/register.pug
Normal 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
|
Reference in New Issue
Block a user