Skip to content

Instantly share code, notes, and snippets.

@jda0
Last active April 19, 2018 19:24
Show Gist options
  • Save jda0/5c0fc6aacf1891ced869 to your computer and use it in GitHub Desktop.
Save jda0/5c0fc6aacf1891ced869 to your computer and use it in GitHub Desktop.
flyt.server
###
REDIS SCHEMA
------------
SET @domain.al@email key
STRING @domain.ae@email token
HASH @domain.as@key A acl, S secret
HASH @domain.u@email N name, A acl, [T trustee], [X delete]
ZSET @domain.uf@email reminder_timestamp rid
ZSET @domain.su timestamp email
HASH @domain.rb@rid T timestamp, U update_timestamp, C type, B body, A authors (CSV)
HASH @domain.rc@cid I rid, T timestamp, B body, A authors
ZSET @domain.rs@rid score pid
HASH @domain.p@pid N name, G gid, S score, S@tp score@tp
ZSET @domain.pf@gid update_timestamp rid
SET @domain.pl@gid pid
HASH @domain.g@gid N name, C colour, S score, S@tp score@tp
ZSET @domain.gf@gid.@tp update_timestamp rid
SET @domain.gl gid
STRING @domain.cr next_rid
STRING @domain.cc next_cid
STRING @domain.cp next_pid
STRING @domain.cg next_gid
LIST @domain.tl timestamp+time_interval
STRING @domain.ti time_interval (days)
SET d domain
###
config = require './config'
bodyParser = require 'body-parser'
crypto = require 'crypto'
express = require 'express'
mandrill = require 'mandrill-api/mandrill'
moment = require 'moment'
redis = require 'ioredis'
validator = require 'validator'
app = express()
db = new Redis
port: config.REDIS_PORT or 6379
host: config.REDIS_HOST or '127.0.0.1'
password: config.REDIS_PASS
db: config.REDIS_DB or 0
email = new mandrill.Mandrill config.MANDRILL_KEY
json = bodyParser.json()
auth = (acl) ->
return ((req, res, next) ->
if req.params.domain and validator.isAlphanumeric req.params.domain
if req.body.email and validator.isEmail req.body.email
if req.body.api_key?.length is config.API_SECRET_LENGTH * 2 and
validator.isHexadecimal req.body.api_key and
req.body.api_secret?.length is config.API_SECRET_LENGTH * 2 and
validator.isHexadecimal req.body.api_secret
db.multi()
.sismember "#{req.params.domain}.al#{req.body.email}",
req.body.api_key
.hmget "#{req.params.domain}.as#{req.body.api_key}", 'S', 'A'
.exec (e, r) ->
if e
return res.status(500).json error: 'db_error'
if r[0][1] is 1 and r[1][1] is req.body.api_secret
if not acl or acl is r[1][2]
db.expire "#{req.params.domain}.as#{req.body.api_key}",
config.API_KEY_EXPIRE, (e, r) ->
if e or r isnt 1
return res.status(500).json error: 'db_error'
next()
else
return res.status(401).json error: 'insufficient_permissions'
else
return res.status(401).json error: 'bad_credentials'
else if req.body.token?.length is config.ETOKEN_LENGTH and
validator.isHexadecimal req.body.token
db.get "#{req.params.domain}.ae#{req.body.email}", (e, r) ->
if e
return res.status(500).json error: 'db_error'
if r and r is req.body
user = {}
db.hmget "#{req.params.domain}.u#{req.body.email}",
'N', 'A', 'T', 'X' (e, r) ->
if e
return res.status(500).json 'db_error'
if r[3]
db.multi()
.sismember 'd', req.params.domain
.expire "#{req.params.domain}.u#{req.body.email}",
config.NEW_USER_EXPIRE
.hdel "#{req.params.domain}.u#{req.body.email}", 'X'
.zadd "#{req.params.domain}.su",
moment().utc().valueOf(), req.body.email
.exec (e, r) ->
if e
return res.status(500).json error: 'db_error'
if not r[0][1]
db.multi()
.persist "#{req.params.domain}.ti"
.set "#{req.params.cr}", 1
.set "#{req.params.cc}", 1
.set "#{req.params.cp}", 1
.set "#{req.params.cg}", 1
.exec (e, r) ->
if e
return res.status(500).json error: 'db_error'
if r[2]
user.name: r[0]
user.acl: r[1]
crypto.randomBytes config.API_SECRET_LENGTH, (e, b) ->
if e
user.api_key = crypto.pseudoRandomBytes(config.API_KEY_LENGTH)
.toString 'hex'
else
user.api_key = b.toString 'hex'
crypto.randomBytes config.API_SECRET_LENGTH, (e, b) ->
if e
user.api_secret = crypto.pseudoRandomBytes(config.API_SECRET_LENGTH)
.toString 'hex'
else
user.api_secret = b.toString 'hex'
db.multi()
.del "#{req.params.domain}.ae#{req.body.email}"
.hmset "#{req.params.domain}.as#{user.api_key}",
'A', user.acl,
'S', user.api_secret
.expire "#{req.params.domain}.as#{user.api_key}",
config.API_KEY_EXPIRE
.sadd "#{req.params.domain}.al#{req.body.email}",
user.api_key
.exec (e, r) ->
if e
return res.status(500).json error: 'db_error'
return res.json result: user
else
return res.status(401).json 'untrusted'
else
return res.status(401).json error: 'bad_token'
else
db.hget "#{req.params.domain}.u#{req.body.email}", 'N', (e, r) ->
if e
return res.status(500).json error: 'db_error'
if r
return sendToken req.params.domain, req.body.email, r, res
else
return res.status(401).json error: 'not_found'
else
return res.status(401).json error: 'bad_credentials'
else
return res.status(400).json error: 'bad_domain'
)()
sendToken = (domain, email, name, res) ->
crypto.randomBytes config.API_SECRET_LENGTH, (e, b) ->
if e
t = crypto.pseudoRandomBytes(config.API_KEY_LENGTH)
.toString 'hex'
else
t = b.toString 'hex'
db.setex "#{domain}.ae#{email}",
config.ETOKEN_EXPIRE, t, (e, r) ->
if e
return res.status(500).json error: 'db_error'
app.render 'email.jade',
logo: "#{req.protocol}://#{req.get 'host'}/static/asset/elogo.png"
name: name.replace(/\ /g, '\u00a0')
key: key
expiry: expiry.format('ddd Do MMMM, ha [UTC]')
link: "#{req.protocol}://#{req.get 'host'}/?i=
#{encodeURIComponent email}&key=#{key}"
, (er, render) ->
edata =
text: "Welcome back, #{name}.\n\n
Your key is #{key}, and is valid until
#{moment().utc().add(1, 'days').format 'ddd Do MMMM, ha'}.\n
You can enter the system directly here:
#{req.protocol}://#{req.get 'host'}/auth?i=#{encodeURIComponent email}&key=#{key}\n\n
Kind regards,\n
James Daly, Developer, Flyt"
from_name: 'Flyt'
from_email: config.MANDRILL_EMAIL
to: [
name: r
email: req.body.email
]
subject: 'Login to Flyt'
important: false
tags: ['flyt.token', "flyt:#{domain}"]
if not er
edata.html = render
email.messages.send message: edata, (r) ->
if r[0].status is 'sent'
return res.json result: 'sent'
else
return res.status(500).json
error: 'send_error'
result: r[0].status + ' ' + r[0].reject_reason
app.post 'api/:domain/auth', json, auth
app.post 'api/:domain/register', json, (req, res) ->
if req.params.domain and validator.isAlphanumeric req.params.domain
if req.body.email and validator.isEmail req.body.email and
req.body.name?.length > 3 and req.body.acl and
(req.body.name.split(' ').every (v) -> validator.isAlpha v) and
validator.isInt req.body.acl
db.exists "#{req.params.domain}.u#{req.body.email}", (e, r) ->
if e
return res.status(500).json error: 'db_error'
if r
return res.status(401).json error: 'user_exists'
else
db.multi()
.hmset "#{req.params.domain}.u#{req.body.email}",
'N', req.body.name,
'A', req.body.acl,
'X', 1
.expire "#{req.params.domain}.u#{req.body.email}",
config.ETOKEN_EXPIRE + 3600
.exec (e, r) ->
if e
return res.status(500).json error: 'db_error'
else
return sendToken req.params.domain,
req.body.email, req.body.name, res
else
return res.status(401).json error: 'bad_credentials'
else
return res.status(400).json error: 'bad_domain'
app.post 'api/:domain/trust', json, auth(config.ACL_FLAGS.TRUST_USERS), (req, res) ->
if req.body.user and validator.isEmail req.body.user
db.multi()
.hsetnx "#{req.params.domain}.u#{req.body.user}", 'T', req.body.email
.persist "#{req.params.domain}.u#{req.body.user}"
.exec (e, r)->
if e
return res.status(500).json error: 'db_error'
else
return res.json result: 'OK'
app.post 'api/:domain/setup', json, (req, res) ->
if req.params.domain and validator.isAlphanumeric req.params.domain
db.sismember 'd', req.params.domain, (e, r) ->
if e
return res.status(500).json error: 'db_error'
if r
return res.status(400).json error: 'domain_exists'
else
if req.body.time_interval and validator.isInt req.params.time_interval
if req.body.email and validator.isEmail req.body.email and
req.body.name?.length > 3 and
(req.body.name.split(' ').every (v) -> validator.isAlpha v)
db.multi()
.setex "#{req.params.domain}.ti",
config.ETOKEN_EXPIRE + 3600, req.body.time_interval
.hmset "#{req.params.domain}.u#{req.body.email}",
'N', req.body.name,
'A', config.roles['Admin'],
'T', req.body.email
'X', 1
.expire "#{req.params.domain}.u#{req.body.email}",
config.ETOKEN_EXPIRE + 3600
.exec (e, r) ->
if e
return res.status(500).json error: 'db_error'
else
return sendToken req.params.domain,
req.body.email, req.body.name, res
else
return res.status(401).json error: 'bad_credentials'
else
return res.status(400).json error: 'bad_info'
else
return res.status(400).json error: 'bad_domain'
app.use '/static', express.static '/public'
app.get '/:domain', (req, res) ->
db.sismember 'd', req.params.domain, (e, r) ->
if e
return res.status(500).sendfile '/public/500.html'
if r
res.sendfile '/public/app.html'
else
res.sendfile '/public/bad_domain.html'
app.get '/', (req, res) ->
res.sendfile '/public/index.html'
app.listen process.env.PORT or config.DEFAULT_PORT or 80
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment