First commit

merge-requests/208/head
fatchan 5 years ago
commit 8ffdedee8a
  1. 2
      .gitignore
  2. 1
      README.md
  3. 5
      configs/main.json.example
  4. 159
      controllers/posts.js
  5. 17
      helpers/db.js
  6. 13
      models/index.js
  7. 109
      models/posts.js
  8. 1844
      package-lock.json
  9. 29
      package.json
  10. 92
      server.js
  11. 32
      static/css/style.css
  12. 8
      utils.js
  13. 2
      views/includes/footer.pug
  14. 3
      views/includes/head.pug
  15. 3
      views/includes/navbar.pug
  16. 14
      views/layout.pug
  17. 4
      views/pages/404.pug
  18. 20
      views/pages/board.pug
  19. 4
      views/pages/error.pug

2
.gitignore vendored

@ -0,0 +1,2 @@
node_modules/
configs/*.json

@ -0,0 +1 @@
# jschan

@ -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();
}
}()

1844
package-lock.json generated

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…
Cancel
Save