mirror of https://gitgud.io/fatchan/jschan.git
commit
8ffdedee8a
19 changed files with 2361 additions and 0 deletions
@ -0,0 +1,2 @@ |
||||
node_modules/ |
||||
configs/*.json |
@ -0,0 +1,5 @@ |
||||
{ |
||||
"dbURL": "mongodb://", |
||||
"port": 7777, |
||||
"sessionSecret": "xxxxx" |
||||
} |
@ -0,0 +1,159 @@ |
||||
'use strict'; |
||||
|
||||
const express = require('express') |
||||
, router = express.Router() |
||||
, { check, validationResult } = require('express-validator/check') |
||||
, utils = require('../utils.js') |
||||
, util = require('util') |
||||
, fs = require('fs') |
||||
, mkdir = util.promisify(fs.mkdir) |
||||
, Posts = require(__dirname+'/../models/posts.js'); |
||||
|
||||
/* |
||||
roughly: |
||||
- GET /api/board/:board/catalog -> all threads (catalog) |
||||
- GET /api/board/:board/recent/:page? -> recent posts per page (board homepage) |
||||
- GET /api/board/:board/thread/:thread -> get all posts in a thread |
||||
|
||||
- POST /api/board/:board -> make a new thread |
||||
- POST /api/board/:board/thread/:thread -> make a new post in a thread |
||||
|
||||
- DELETE /api/board/:board/post/:id -> delete a post |
||||
*/ |
||||
|
||||
// make new post
|
||||
router.post('/api/board/:board', async (req, res, next) => { |
||||
|
||||
}); |
||||
|
||||
// delete a post
|
||||
router.delete('/api/board/:board/post/:id', async (req, res, next) => { |
||||
|
||||
}); |
||||
|
||||
// get recent threads and preview posts
|
||||
router.get('/api/board/:board/recent/:page', async (req, res, next) => { |
||||
|
||||
//make sure the board exists
|
||||
const boards = await Posts.checkBoard(req.params.board) |
||||
if (boards.length <= 0) { |
||||
return next(); |
||||
} |
||||
|
||||
//get the recently bumped thread & preview posts
|
||||
let threads; |
||||
try { |
||||
threads = await Posts.getRecent(req.params.board, req.params.page || 1); |
||||
} catch (err) { |
||||
return next(err); |
||||
} |
||||
|
||||
if (!threads || threads.lenth === 0) { |
||||
return next(); |
||||
} |
||||
|
||||
return res.json(threads) |
||||
|
||||
}); |
||||
|
||||
// get a thread
|
||||
router.get('/api/board/:board/thread/:thread', async (req, res, next) => { |
||||
|
||||
//make sure the board exists
|
||||
const boards = await Posts.checkBoard(req.params.board) |
||||
if (boards.length <= 0) { |
||||
return next(); |
||||
} |
||||
|
||||
//get the recently bumped thread & preview posts
|
||||
let thread; |
||||
try { |
||||
thread = await Posts.getThread(req.params.board, req.params.thread); |
||||
} catch (err) { |
||||
return next(err); |
||||
} |
||||
|
||||
if (!thread) { |
||||
return next(); |
||||
} |
||||
|
||||
return res.json(thread) |
||||
|
||||
}); |
||||
|
||||
// get array of threads (catalog)
|
||||
router.get('/api/board/:board/catalog', async (req, res, next) => { |
||||
|
||||
//make sure the board exists
|
||||
const boards = await Posts.checkBoard(req.params.board) |
||||
if (boards.length <= 0) { |
||||
return next(); |
||||
} |
||||
|
||||
//get the recently bumped thread & preview posts
|
||||
let data; |
||||
try { |
||||
data = await Posts.getCatalog(req.params.board); |
||||
} catch (err) { |
||||
return next(err); |
||||
} |
||||
|
||||
if (!data) { |
||||
return next(); |
||||
} |
||||
|
||||
return res.json(data) |
||||
|
||||
}); |
||||
|
||||
// board page web frontend
|
||||
router.get('/:board/:page?', async (req, res, next) => { |
||||
|
||||
//make sure the board exists
|
||||
const boards = await Posts.checkBoard(req.params.board) |
||||
if (boards.length <= 0) { |
||||
return next(); |
||||
} |
||||
|
||||
//get the recently bumped thread & preview posts
|
||||
let threads; |
||||
try { |
||||
threads = await Posts.getRecent(req.params.board, req.params.page); |
||||
} catch (err) { |
||||
return next(err); |
||||
} |
||||
|
||||
//render the page
|
||||
res.render('board', { |
||||
csrf: req.csrfToken(), |
||||
board: req.params.board, |
||||
threads: threads || [] |
||||
}); |
||||
|
||||
}); |
||||
|
||||
/* |
||||
(async () => { |
||||
await Posts.deleteAll('b'); |
||||
for (let i = 0; i < 5; i++) { |
||||
const thread = await Posts.insertOne('b', { |
||||
'author': 'Anonymous', |
||||
'date': new Date(), |
||||
'content': Math.random().toString(36).replace(/[^a-z]+/g, ''), |
||||
'thread': null |
||||
}) |
||||
for (let j = 0; j < 5; j++) { |
||||
await new Promise(resolve => {setTimeout(resolve, 500)}) |
||||
const post = await Posts.insertOne('b', { |
||||
'author': 'Anonymous', |
||||
'date': new Date(), |
||||
'content': Math.random().toString(36).replace(/[^a-z]+/g, ''), |
||||
'thread': thread.insertedId |
||||
}) |
||||
} |
||||
} |
||||
})(); |
||||
*/ |
||||
|
||||
module.exports = router; |
||||
|
@ -0,0 +1,17 @@ |
||||
'use strict'; |
||||
|
||||
const { MongoClient, ObjectId } = require('mongodb') |
||||
, configs = require(__dirname+'/../configs/main.json'); |
||||
|
||||
module.exports = { |
||||
|
||||
connect: async () => { |
||||
if (module.exports.client) { |
||||
throw new Error('Mongo already connected'); |
||||
} |
||||
module.exports.client = await MongoClient.connect(configs.dbURL, { useNewUrlParser: true }); |
||||
}, |
||||
|
||||
ObjectId |
||||
|
||||
} |
@ -0,0 +1,13 @@ |
||||
'use strict'; |
||||
|
||||
const fs = require('fs') |
||||
, models = {}; |
||||
|
||||
fs.readdirSync(__dirname).forEach(file => { |
||||
if (file === 'index.js') { |
||||
return; |
||||
} |
||||
const name = file.substring(0,file.length-3); |
||||
const model = require(__dirname+'/'+file); |
||||
module.exports[name] = model; |
||||
}); |
@ -0,0 +1,109 @@ |
||||
'use strict'; |
||||
|
||||
const Mongo = require(__dirname+'/../helpers/db.js') |
||||
|
||||
module.exports = new class Posts { |
||||
|
||||
constructor() { |
||||
this._db = Mongo.client.db('chan-boards'); |
||||
} |
||||
|
||||
//TODO: IMPLEMENT PAGINATION
|
||||
async getRecent(board, page) { |
||||
|
||||
// get all thread posts (posts with null thread id)
|
||||
const threads = await this._db.collection(board).find({ |
||||
'thread': null |
||||
}).sort({ |
||||
'bumped': -1 |
||||
}).limit(10).toArray(); |
||||
|
||||
// add posts to all threads in parallel
|
||||
await Promise.all(threads.map(async thread => { |
||||
thread.replies = await this._db.collection(board).find({ |
||||
'thread': thread._id |
||||
}).sort({ |
||||
'_id': 1 |
||||
}).limit(3).toArray(); |
||||
})); |
||||
|
||||
return threads; |
||||
|
||||
} |
||||
|
||||
async getThread(board, id) { |
||||
|
||||
// get thread post and potential replies concurrently
|
||||
const data = await Promise.all([ |
||||
this._db.collection(board).findOne({ |
||||
'_id': Mongo.ObjectId(id) |
||||
}), |
||||
this._db.collection(board).find({ |
||||
'thread': Mongo.ObjectId(id) |
||||
}).sort({ |
||||
'_id': 1 |
||||
}).toArray() |
||||
]) |
||||
|
||||
// attach the replies to the thread post
|
||||
const thread = data[0]; |
||||
if (thread) { |
||||
thread.replies = data[1]; |
||||
} |
||||
|
||||
return thread; |
||||
|
||||
} |
||||
|
||||
async getCatalog(board) { |
||||
|
||||
// get all threads for catalog
|
||||
return this._db.collection(board).find({ |
||||
'thread': null |
||||
}).toArray(); |
||||
|
||||
} |
||||
|
||||
async getPost(board, id) { |
||||
|
||||
// get a post
|
||||
return this._db.collection(board).findOne({ |
||||
'_id': Mongo.ObjectId(id) |
||||
}); |
||||
|
||||
} |
||||
|
||||
async insertOne(board, data) { |
||||
|
||||
// bump thread if name not sage
|
||||
if (data.thread !== null && data.author !== 'sage') { |
||||
await this._db.collection(board).updateOne({ |
||||
'_id': data.thread |
||||
}, { |
||||
$set: { |
||||
'bumped': Date.now() |
||||
} |
||||
}) |
||||
} |
||||
|
||||
return this._db.collection(board).insertOne(data); |
||||
|
||||
} |
||||
|
||||
async deleteOne(board, options) { |
||||
return this._db.collection(board).deleteOne(options); |
||||
} |
||||
|
||||
async deleteMany(board, options) { |
||||
return this._db.collection(board).deleteMany(options); |
||||
} |
||||
|
||||
async deleteAll(board) { |
||||
return this._db.collection(board).deleteMany({}); |
||||
} |
||||
|
||||
async checkBoard(name) { |
||||
return this._db.listCollections({ 'name': name }, { 'nameOnly': true }).toArray(); |
||||
} |
||||
|
||||
}() |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@ |
||||
{ |
||||
"name": "elec3609", |
||||
"version": "1.0.0", |
||||
"description": "", |
||||
"main": "server.js", |
||||
"dependencies": { |
||||
"bcrypt": "^3.0.5", |
||||
"body-parser": "^1.18.3", |
||||
"connect-mongo": "^2.0.3", |
||||
"cookie-parser": "^1.4.3", |
||||
"csurf": "^1.9.0", |
||||
"express": "^4.16.3", |
||||
"express-fileupload": "^1.0.0-alpha.1", |
||||
"express-session": "^1.15.6", |
||||
"express-validator": "^5.3.0", |
||||
"fs": "0.0.1-security", |
||||
"helmet": "^3.13.0", |
||||
"mongodb": "^3.2.2", |
||||
"path": "^0.12.7", |
||||
"pug": "^2.0.3" |
||||
}, |
||||
"devDependencies": {}, |
||||
"scripts": { |
||||
"test": "echo \"Error: no test specified\" && exit 1", |
||||
"start": "node server.js" |
||||
}, |
||||
"author": "", |
||||
"license": "ISC" |
||||
} |
@ -0,0 +1,92 @@ |
||||
'use strict'; |
||||
|
||||
process.on('uncaughtException', console.error); |
||||
process.on('unhandledRejection', console.error); |
||||
|
||||
const express = require('express') |
||||
, session = require('express-session') |
||||
, MongoStore = require('connect-mongo')(session) |
||||
, path = require('path') |
||||
, app = express() |
||||
, helmet = require('helmet') |
||||
, csrf = require('csurf') |
||||
, bodyParser = require('body-parser') |
||||
, cookieParser = require('cookie-parser') |
||||
, upload = require('express-fileupload') |
||||
, configs = require(__dirname+'/configs/main.json') |
||||
, Mongo = require(__dirname+'/helpers/db.js'); |
||||
|
||||
(async () => { |
||||
|
||||
// let db connect
|
||||
await Mongo.connect(); |
||||
|
||||
// parse forms and allow file uploads
|
||||
app.use(bodyParser.urlencoded({extended: true})); |
||||
app.use(bodyParser.json()); |
||||
app.use(upload({ |
||||
createParentPath: true, |
||||
safeFileNames: true, |
||||
preserveExtension: true, |
||||
limits: { fileSize: 50 * 1024 * 1024 }, |
||||
abortOnLimit: true |
||||
})); |
||||
|
||||
// session store
|
||||
app.use(session({ |
||||
secret: configs.sessionSecret, |
||||
store: new MongoStore({ db: Mongo.client.db('sessions') }), |
||||
resave: false, |
||||
saveUninitialized: false |
||||
})); |
||||
app.use(cookieParser()); |
||||
|
||||
// csurf and helmet
|
||||
app.use(helmet()); |
||||
app.use(csrf()); |
||||
|
||||
// use pug view engine
|
||||
app.set('view engine', 'pug'); |
||||
app.set('views', path.join(__dirname, 'views/pages')); |
||||
app.enable('view cache'); |
||||
|
||||
// static files
|
||||
app.use('/css', express.static(__dirname + '/static/css')); |
||||
app.use('/js', express.static(__dirname + '/static/js')); |
||||
app.use('/img', express.static(__dirname + '/static/img')); |
||||
|
||||
// routes
|
||||
const posts = require(__dirname+'/controllers/posts.js'); |
||||
// const modRoutes = require(__dirname+'/controllers/mod.js')()
|
||||
app.use('/', posts) |
||||
// app.use('/', mod)
|
||||
|
||||
//generic error page
|
||||
app.get('/error', (req, res) => { |
||||
res.status(500).render('error', { |
||||
user: req.session.user |
||||
}) |
||||
}) |
||||
|
||||
//wildcard after all other routes -- how we handle 404s
|
||||
app.get('*', (req, res) => { |
||||
res.status(404).render('404', { |
||||
user: req.session.user |
||||
}) |
||||
}) |
||||
|
||||
//catch any unhandled errors
|
||||
app.use((err, req, res, next) => { |
||||
if (err.code === 'EBADCSRFTOKEN') { |
||||
return res.status(403).send('Invalid CSRF token') |
||||
} |
||||
console.error(err.stack) |
||||
return res.redirect('/error') |
||||
}) |
||||
|
||||
//listen
|
||||
app.listen(configs.port, () => { |
||||
console.log(`Listening on port ${configs.port}`); |
||||
}); |
||||
|
||||
})(); |
@ -0,0 +1,32 @@ |
||||
body { |
||||
flex: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
min-height: 100vh; |
||||
margin: 0; |
||||
} |
||||
|
||||
.container { |
||||
clear: both; |
||||
margin: 0 auto; |
||||
} |
||||
|
||||
.navbar { |
||||
|
||||
} |
||||
.nav-item { |
||||
float: left; |
||||
margin: 10px; |
||||
} |
||||
|
||||
.section {} |
||||
.card {} |
||||
.card-title {} |
||||
.card-content {} |
||||
|
||||
.footer { |
||||
padding: 10px; |
||||
text-align: center; |
||||
flex-shrink: 0; |
||||
margin-top: auto; |
||||
} |
@ -0,0 +1,8 @@ |
||||
'use strict'; |
||||
|
||||
module.exports = { |
||||
checkAuth: (req, res, next) => { |
||||
if (req.session.authenticated === true) return next() |
||||
res.redirect('/login') |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
.footer |
||||
p footer |
@ -0,0 +1,3 @@ |
||||
meta(charset='utf-8') |
||||
meta(name='viewport', content='width=device-width, initial-scale=1') |
||||
link(rel='stylesheet' href='/css/style.css') |
@ -0,0 +1,3 @@ |
||||
div |
||||
ul.navbar |
||||
li.nav-item: a(href='/') Home |
@ -0,0 +1,14 @@ |
||||
doctype html |
||||
html |
||||
head |
||||
include includes/head.pug |
||||
block head |
||||
body |
||||
nav |
||||
.container |
||||
include includes/navbar.pug |
||||
main |
||||
.container |
||||
block content |
||||
include includes/footer.pug |
||||
|
@ -0,0 +1,4 @@ |
||||
extends ../layout.pug |
||||
|
||||
block content |
||||
h1 404 not found |
@ -0,0 +1,20 @@ |
||||
extends ../layout.pug |
||||
|
||||
block head |
||||
title /#{board}/ |
||||
|
||||
block content |
||||
for thread in threads |
||||
h1 OP: |
||||
div #{thread._id} |
||||
div #{thread.author} |
||||
div #{thread.date} |
||||
div #{thread.content} |
||||
h1 Replies: |
||||
for post in thread.replies |
||||
div #{post._id} |
||||
div #{post.author} |
||||
div #{post.date} |
||||
div #{post.content} |
||||
br |
||||
hr |
@ -0,0 +1,4 @@ |
||||
extends ../layout.pug |
||||
|
||||
block content |
||||
h1 Internal server error |
Loading…
Reference in new issue