diff options
Diffstat (limited to 'bucky')
| -rw-r--r-- | bucky/app/bucky.js | 211 | ||||
| -rw-r--r-- | bucky/app/index.js | 63 | ||||
| -rw-r--r-- | bucky/app/router.js | 156 | ||||
| -rw-r--r-- | bucky/db/bookshelf.js | 24 | ||||
| -rw-r--r-- | bucky/db/fortune.js | 28 | ||||
| -rw-r--r-- | bucky/db/index.js | 187 | ||||
| -rw-r--r-- | bucky/util/auth.js | 78 | ||||
| -rw-r--r-- | bucky/util/middleware.js | 23 | ||||
| -rw-r--r-- | bucky/util/util.js | 6 |
9 files changed, 776 insertions, 0 deletions
diff --git a/bucky/app/bucky.js b/bucky/app/bucky.js new file mode 100644 index 0000000..757592a --- /dev/null +++ b/bucky/app/bucky.js @@ -0,0 +1,211 @@ +var db = require('../db') +var util = require('../util/util') +var _ = require('lodash') + +var bucky = module.exports = { + + /* INDEX */ + + ensureLatestThreads: function (req, res, next){ + db.getLatestThreads().then(function(threads){ + res.threads = threads + res.threads_ids = res.threads.pluck("id").sort() + res.keywords = _.uniq(res.threads.pluck("keyword")) + next() + }) + }, + ensureCommentCountsForThreads: function (req, res, next){ + db.getCommentCounts(res.threads_ids).then(function(counts){ + var lookup = {} + counts.forEach(function(c){ + lookup[c.thread] = c + }) + res.threads.forEach(function(thread){ + if (lookup[thread.id]) { + thread.set("comment_count", lookup[thread.id].count) + } + }) + next() + }) + }, + ensureFileCountsForThreads: function (req, res, next){ + db.getFileCounts(res.threads_ids).then(function(counts){ + var lookup = {} + counts.forEach(function(c){ + lookup[c.thread] = c + }) + res.threads.forEach(function(t){ + var c = lookup[t.id] + t.set("file_count", c ? c.count : 0) + }) + next() + }) + }, + ensureKeywordsForThreads: function (req, res, next){ + db.getKeywords(res.keywords).then(function(keywords){ + var lookup = {} + keywords.forEach(function(k){ + lookup[k.get('keyword')] = k + }) + res.threads.forEach(function(t){ + var kw = t.get('keyword') + if (! kw) return + var k = lookup[kw] + if (! k) return + if (! t.get("color")) { + t.set("color", k.get("color")) + } + }) + next() + }) + }, + ensureHootbox: function (req, res, next){ + db.getCommentsForThread(1, 15, 0, "desc").then(function(hootbox){ + res.hootbox = hootbox + next() + }) + }, + ensureLastlog: function (req, res, next){ + db.getLastlog(6).then(function(lastlog){ + res.lastlog = lastlog + next() + }) + }, + + /* DETAILS */ + + ensureThread: function (req, res, next){ + var id = req.params.id.replace(/\D/g, "") + if (! id) { + return res.sendStatus(404) + } + db.getThread(id).then(function(thread){ + if (thread) { + res.thread = thread + next() + } + else { + res.sendStatus(404) + } + }) + }, + ensureKeywordForThread: function (req, res, next){ + var keyword = res.thread.get('keyword') + if (! keyword) return next() + db.getKeyword(keyword).then(function(keyword){ + res.keyword = keyword + next() + }) + }, + ensureCommentsForThread: function (req, res, next){ + db.getCommentsForThread(res.thread.get('id')).then(function(comments){ + res.comments = comments + next() + }) + }, + ensureFilesForThread: function (req, res, next){ + db.getFilesForThread(res.thread.get('id')).then(function(files){ + res.files = files + next() + }) + }, + + /* KEYWORDS */ + + ensureKeyword: function (req, res, next){ + var keyword = req.params.keyword + if (! keyword) { + return res.sendStatus(404) + } + db.getKeyword(keyword).then(function(k){ + if (! k) { + return res.sendStatus(404) + } + res.keyword = k + next() + }) + }, + ensureThreadsForKeyword: function (req, res, next){ + var keyword = req.params.keyword + if (! keyword) { + res.sendStatus(404) + } + db.getThreadsForKeyword(keyword).then(function(threads){ + res.threads = threads + res.threads_ids = res.threads.pluck("id").sort() + res.keywords = _.uniq(res.threads.pluck("keyword")) + next() + }) + }, + + /* COMMENTS */ + + createComment: function (req, res, next){ + if (! req.body.comment || ! req.body.comment.length) { + res.json({ error: "no comment" }) + return + } + var data = { + thread: res.thread.get('id'), + parent_id: req.body.parent_id || -1, + username: req.user.get('username'), + date: util.now(), + comment: req.body.comment, + } + db.createComment(data).then(function(comment){ + res.comment = comment + next() + }) + }, + + /* MAIL */ + + ensureMailboxes: function (req, res, next){ + var username = req.user.get('username') + var box = req.params.box + var mbox = username + "." + box + if (! box) { + res.sendStatus(404) + } + db.getMailboxes(username).then(function(boxes){ + if (! boxes) { + return res.sendStatus(404) + } + if (! boxes.models.some(function(box){ return box.get('mbox') == mbox })) { + return res.sendStatus(404) + } + res.boxes = boxes + next() + }) + }, + ensureMailboxCounts: function (req, res, next){ + db.getMailboxCounts(res.boxes.pluck("mbox")).then(function(counts){ + var lookup = {} + counts.forEach(function(c){ + lookup[c.mbox] = c + }) + res.boxes.forEach(function(box){ + var count = lookup[box.get('mbox')] ? lookup[box.get('mbox')].count : 0 + box.set("count", count) + }) + next() + }) + }, + ensureMessages: function (req, res, next){ + db.getMessages(req.user.get('username'), req.params.box, 50, 0).then(function(messages){ + res.messages = messages + next() + }) + }, + ensureMessage: function(req, res, next){ + db.getMessage(req.params.id).then(function(message){ + var username = req.user.get('username') + if (username !== message.get('recipient') && username !== message.get('sender')) { + res.sendStatus(404) + return + } + res.message = message + next() + }) + } +}
\ No newline at end of file diff --git a/bucky/app/index.js b/bucky/app/index.js new file mode 100644 index 0000000..03c5593 --- /dev/null +++ b/bucky/app/index.js @@ -0,0 +1,63 @@ +require('dotenv').load(); +var fs = require('fs') +var app, express = require('express'); +var http = require('http'); +var https = require('https'); +var bodyParser = require('body-parser') +var cookieParser = require('cookie-parser') +var csurf = require('csurf') +var path = require('path') +var multiparty = require('multiparty') +var ejs = require('ejs') +var favicon = require('serve-favicon') +var passport = require('passport') +var sessionstore = require('sessionstore') +var session = require('express-session') +var multer = require('multer') + +var app, server + +var mongodb = require('mongodb') + +var site = module.exports = {} +site.init = function(){ + app = express() + app.set('port', 5000) + app.set('view engine', 'ejs') + app.set('views', path.join(__dirname, '../../views')) + app.use(express.static(path.join(__dirname, '../../public'))) + + app.use(favicon(__dirname + '../../public/favicon.ico')) + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: false })) + app.use( multer({ dest:'./uploads/' }).single("file") ) + + app.use(session({ + key: 'bucky.sid', + secret: 'argonauts', + cookie: { domain: '.' + process.env.HOST_NAME, maxAge: 43200000000 }, + store: sessionstore.createSessionStore({ + type: 'mongodb', + host: 'localhost', + port: 27017, + dbName: 'buckySessionDb', + collectionName: 'sessions', + timeout: 10000, + }), + resave: true, + saveUninitialized: false, + })) + app.use(csurf({ cookie: false })) + + app.use(express.query()) + app.use(passport.initialize()) + app.use(passport.session()) + + server = http.createServer(app).listen(process.env.PORT || 5000, function () { + console.log('Bucky listening at http://5.k:%s', server.address().port) + }) + + site.route(app) +} + +site.route = require('./router') diff --git a/bucky/app/router.js b/bucky/app/router.js new file mode 100644 index 0000000..c3af565 --- /dev/null +++ b/bucky/app/router.js @@ -0,0 +1,156 @@ +var auth = require('./auth') +var middleware = require('./middleware') +var fortune = require('./fortune') +var bucky = require('./bucky') +var db = require('./db') +var util = require('./util') + +module.exports = function(app){ + app.all('*', middleware.ensureLocals) + + auth.init() + + app.get("/", middleware.ensureAuthenticated, function(req, res){ + res.redirect('/index') + }) + app.get("/login", function(req, res){ + res.render("pages/login", { + title: "login" + }) + }) + app.get("/index", middleware.ensureAuthenticated, function(req, res){ + res.render("pages/index", { + title: fortune("titles"), + hoot_text: fortune("hoots"), + }) + }) + app.get("/details/:id", middleware.ensureAuthenticated, function(req, res){ + res.render("pages/details", {}) + }) + + app.post("/api/login", auth.loggedInLocal) + app.get("/api/index", + bucky.ensureLastlog, + middleware.ensureAuthenticated, + bucky.ensureLatestThreads, + bucky.ensureCommentCountsForThreads, + bucky.ensureFileCountsForThreads, + bucky.ensureKeywordsForThreads, + bucky.ensureHootbox, + function(req, res){ + res.json({ + threads: res.threads, + hootbox: res.hootbox, + lastlog: res.lastlog, + }) + } + ) + app.get("/api/thread/:id", + middleware.ensureAuthenticated, + bucky.ensureThread, + bucky.ensureKeywordForThread, + bucky.ensureCommentsForThread, + bucky.ensureFilesForThread, + function(req, res){ + res.json({ + thread: res.thread, + comments: res.comments, + files: res.files, + keyword: res.keyword, + }) + } + ) + app.post("/api/thread", + middleware.ensureAuthenticated, + function(req, res){ + // make a new thread + }) + app.post("/api/thread/:id/comment", + middleware.ensureAuthenticated, + bucky.ensureThread, + // ensure thread privacy + bucky.createComment, + function(req, res){ + res.json({ + comment: res.comment + }) + }) + app.delete("/api/thread/:id", + middleware.ensureAuthenticated, + function(req, res){ + // delete a thread + }) + app.put("/api/comment/:id", + middleware.ensureAuthenticated, + function(req, res){ + // edit a comment + }) + app.delete("/api/comment/:id", + middleware.ensureAuthenticated, + function(req, res){ + // delete a comment + }) + + + app.get("/api/keyword/:keyword", + middleware.ensureAuthenticated, + bucky.ensureKeyword, + bucky.ensureThreadsForKeyword, + bucky.ensureCommentCountsForThreads, + bucky.ensureFileCountsForThreads, + bucky.ensureKeywordsForThreads, + function(req, res){ + res.json({ + keyword: res.keyword, + threads: res.threads, + }) + } + ) + + app.get("/mail/", + middleware.ensureAuthenticated, + function(req, res){ + res.render("pages/mailbox", {title: "inbox" }) + } + ) + app.get("/mail/:box", + middleware.ensureAuthenticated, + function(req, res){ + res.render("pages/mailbox", { title: util.sanitize(req.params.box) }) + } + ) + app.get("/message/:id", + middleware.ensureAuthenticated, + function(req, res){ + res.render("pages/message", { title: util.sanitize(req.params.box) }) + } + ) + app.get("/api/mailbox/:box", + middleware.ensureAuthenticated, + bucky.ensureMailboxes, + bucky.ensureMailboxCounts, + bucky.ensureMessages, + function(req, res){ + res.json({ + user: { id: req.user.get("id"), username: req.user.get("username") }, + messages: res.messages, + boxes: res.boxes, + }) + } + ) + app.get("/api/message/:id", + middleware.ensureAuthenticated, + bucky.ensureMessage, + function(req, res){ + res.json({ + message: res.message, + }) + }) + app.post("/mail/", + middleware.ensureAuthenticated, + function(req, res){ + // send new mail + } + ) + +} diff --git a/bucky/db/bookshelf.js b/bucky/db/bookshelf.js new file mode 100644 index 0000000..69157cc --- /dev/null +++ b/bucky/db/bookshelf.js @@ -0,0 +1,24 @@ +var knex = require('knex')({ + client: 'mysql2', + connection: { + host : process.env.DB_HOST, + user : process.env.DB_USER, + password : process.env.DB_PASS, + database : process.env.DB_NAME, + charset : 'utf8', + typecast : function (field, next) { + console.log(field.type) + if (field.type == 'BLOB') { + return field.string() + } + return next() + } + } +}) + +var bookshelf = require('bookshelf')(knex) + +module.exports = { + bookshelf: bookshelf, + knex: knex, +} diff --git a/bucky/db/fortune.js b/bucky/db/fortune.js new file mode 100644 index 0000000..7adba5a --- /dev/null +++ b/bucky/db/fortune.js @@ -0,0 +1,28 @@ +function choice (a){ return a[ Math.floor(Math.random()*a.length) ] } + +var fs = require("fs"), path = require("path") +var fortunes = {} +var dir = "fortune" + +fs.readdirSync(path.resolve(dir)).forEach(function(fn){ + + var file = dir + '/' + fn + var stat = fs.statSync(file) + + if (stat && ! stat.isDirectory()) { + fortunes[fn] = fs.readFileSync(file) + .toString() + .split("\n") + .filter(function(s){ return !! s }) + } + +}) + +module.exports = function(tag){ + if (tag in fortunes) { + return choice(fortunes[tag]) + } + else { + return "bucky" + } +}
\ No newline at end of file diff --git a/bucky/db/index.js b/bucky/db/index.js new file mode 100644 index 0000000..f376308 --- /dev/null +++ b/bucky/db/index.js @@ -0,0 +1,187 @@ +var db = module.exports + +var connection = require("./bookshelf") +var bookshelf = connection.bookshelf +var knex = connection.knex + + +/* MODELS */ + +var User = db.User = bookshelf.Model.extend({ + tableName: 'users', + hasTimestamps: false, +}) +var Thread = db.Thread = bookshelf.Model.extend({ + tableName: 'threads', + hasTimestamps: false, +}) +var Comment = db.Comment = bookshelf.Model.extend({ + tableName: 'comments', + hasTimestamps: false, +}) +var File = db.File = bookshelf.Model.extend({ + tableName: 'files', + hasTimestamps: false, +}) +var Keyword = db.Keyword = bookshelf.Model.extend({ + tableName: 'keywords', + hasTimestamps: false, +}) +var Mailbox = db.Mailbox = bookshelf.Model.extend({ + tableName: 'boxes', + hasTimestamps: false, +}) +var Message = db.Message = bookshelf.Model.extend({ + tableName: 'messages', + hasTimestamps: false, +}) + +/* USERS */ + +db.createUser = function(data){ + return new db.User(data).save() +} +db.getUsers = function () { + return User.query(function(qb){ + qb.orderBy("id", "desc") + }).fetchAll() +} +db.getUser = function(id) { + var model = new User({'id': id}) + return model.fetch() +} +db.getUserByUsername = function(username) { + var model = new User({'username': username}) + return model.fetch() +} +db.getLastlog = function(limit){ + return knex.column('username').column('lastseen').select().from('users').orderBy('lastseen', 'desc').limit(limit || 10) +} + +/* THREADS */ + +db.getLatestThreads = function () { + return Thread.query(function(qb){ + qb.orderBy("lastmodified", "desc").limit(50) + }).fetchAll() +} +db.getThreadsForKeyword = function (keyword) { + return Thread.query(function(qb){ + qb.where("keyword", "=", keyword).orderBy("id", "desc") + }).fetchAll() +} +db.getThread = function (id) { + return Thread.query("where", "id", "=", id).fetch() +} +db.createThread = function(data){ + return new db.Thread(data).save() +} +db.updateThread = function(data){ +} +db.removeThread = function(id){ +} + +/* FILES */ + +db.getFilesForThread = function (id){ + return File.query("where", "thread", "=", id).fetchAll() +} +db.getFileCounts = function(ids){ + return knex.column('thread').count('* as count').select().from('files').where('thread', 'in', ids).groupBy('thread') +} +db.getFileSizes = function(ids){ + return knex.column('thread').sum('size as size').select().from('files').where('thread', 'in', ids).groupBy('thread') +} +db.createFile = function(data){ + return new db.File(data).save() +} +db.removeFile = function(id){ +} + +/* COMMENTS */ + +db.getCommentsForThread = function (id, limit, offset, order){ + order = order || "asc" + return Comment.query(function(qb){ + qb.where("thread", "=", id).orderBy("id", order) + if (limit) { + qb.limit(limit) + } + if (offset) { + qb.offset(offset) + } + }).fetchAll().then(function(comments){ + comments.forEach(function(comment){ + comment.set("comment", comment.get("comment").toString() ) + }) + return comments + }) +} +db.getCommentCounts = function(ids){ + return knex.column('thread').count('* as count').select().from('comments').where('thread', 'in', ids).groupBy('thread') +} +db.createComment = function(data){ + return new db.Comment(data).save() +} +db.updateComment = function(data){ +} +db.removeComment = function(id){ +} + + +/* KEYWORDS */ + +db.getKeywords = function (keywords){ + return Keyword.query("where", "keyword", "in", keywords).fetchAll() +} +db.getKeyword = function (keyword) { + return Keyword.query("where", "keyword", "=", keyword).fetch() +} +db.createKeyword = function(data){ + return new db.Keyword(data).save() +} +db.updateKeyword = function(data){ +} +db.removeKeyword = function(id){ +} + + +/* MAILBOXES */ + +db.getMailboxes = function(username){ + return Mailbox.query("where", "owner", "=", username).fetchAll() +} +db.getMailboxCounts = function(boxes){ + return knex.column('mbox').count('* as count').select().from('messages').where('mbox', 'in', boxes).groupBy('mbox') +} +db.createMailbox = function(data){ +} + +/* MESSAGES */ + +db.getMessages = function(username, box, limit, offset){ + var mbox = username + "." + box + return Message.query(function(qb){ + qb.column('id', 'mbox', 'unread', 'sender', 'recipient', 'date', 'subject', knex.raw("CHAR_LENGTH(body) as size")).where("mbox", "=", mbox).orderBy("id", "desc") + if (limit) { + qb.limit(limit) + } + if (offset) { + qb.offset(offset) + } + }).fetchAll() +} +db.getMessage = function (id){ + var model = new Message({'id': id}) + return model.fetch().then(function(message){ + message.set("body", message.get("body").toString() ) + return message + }) +} +db.createMessage = function(data){ + return new db.Message(data).save() +} +db.updateMessage = function(data){ +} +db.removeMessage = function(id){ +} diff --git a/bucky/util/auth.js b/bucky/util/auth.js new file mode 100644 index 0000000..436d5e6 --- /dev/null +++ b/bucky/util/auth.js @@ -0,0 +1,78 @@ + +var passport = require('passport'), + LocalStrategy = require('passport-local').Strategy, + crypto = require('crypto'), + db = require('../db'); + + +var auth = module.exports = { + + init: function(){ + passport.serializeUser(auth.serializeUser) + passport.deserializeUser(auth.deserializeUser) + + passport.use(new LocalStrategy(auth.verifyLocalUser)) + }, + + serializeUser: function (user, done) { + done(null, user.id); + }, + + deserializeUser: function (id, done) { + db.getUser(id).then(function(user){ + done(! user, user) + }) + }, + + validPassword: function(user, pw){ + var shasum = crypto.createHash('sha1') + shasum.update(pw) + return user.get('password') === shasum.digest('hex'); + }, + + verifyLocalUser: function (username, password, done) { + // handle passwords!! + db.getUserByUsername(username).then(function(user){ + + // if (err) { return done(err); } + if (! user) { return done("no user") } + + return done(null, user) + + if (! user) { + return done(null, false, { error: { errors: { username: { message: 'No such username.' } }}}) + } + if (! auth.validPassword(user, password)) { + return done(null, false, { error: { errors: { password: { message: 'Incorrect password.' } }}}) + } + return done(null, user); + }) + }, + + loggedInLocal: function (req, res, next) { + passport.authenticate("local", function(err, user, info){ + if (err) { + return res.json({ error: err }); + } + if (! user) { + return info ? res.json(info) : res.redirect("/login"); + } + + // user.last_seen = new Date () + // user.save(function(err, data){ if (err) console.err('error setting ip for user') }) + + req.logIn(user, function(err) { + if (err) { return next(err); } + var returnTo = req.session.returnTo + delete req.session.returnTo + return res.json({ status: "OK", returnTo: returnTo || "/index" }) + }); + })(req, res, next) + }, + + logout: function (req, res) { + req.logout(); + res.redirect('/'); + }, + +}
\ No newline at end of file diff --git a/bucky/util/middleware.js b/bucky/util/middleware.js new file mode 100644 index 0000000..a744c89 --- /dev/null +++ b/bucky/util/middleware.js @@ -0,0 +1,23 @@ +var middleware = module.exports = { + + ensureAuthenticated: function (req, res, next) { + if (! req.isAuthenticated()) { + req.session.returnTo = req.path + return res.redirect('/login') + } + next() + }, + + ensureLocals: function (req, res, next) { + res.locals.csrfToken = req.csrfToken() + res.locals.title = "bucky" + if (req.isAuthenticated()) { + res.locals.show_header = true + } + else { + res.locals.show_header = false + } + next() + }, + +}
\ No newline at end of file diff --git a/bucky/util/util.js b/bucky/util/util.js new file mode 100644 index 0000000..d4b6b8a --- /dev/null +++ b/bucky/util/util.js @@ -0,0 +1,6 @@ +var util = module.exports = {} + +util.sanitizeName = function (s){ return (s || "").replace(new RegExp("[^-_a-zA-Z0-9]", 'g'), "") } +util.sanitize = function (s){ return (s || "").replace(/<>&/g, "") } + +util.now = function(){ return Math.floor( (+ new Date()) / 1000 ) } |
