Repository: shoumma/ReForum Branch: master Commit: f4b209b579f1 Files: 126 Total size: 634.6 KB Directory structure: gitextract_bfqsak_0/ ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── backend/ │ ├── dev.js │ ├── entities/ │ │ ├── admin/ │ │ │ ├── api.js │ │ │ └── controller.js │ │ ├── discussion/ │ │ │ ├── api.js │ │ │ ├── controller.js │ │ │ └── model.js │ │ ├── forum/ │ │ │ ├── api.js │ │ │ ├── controller.js │ │ │ └── model.js │ │ ├── opinion/ │ │ │ ├── api.js │ │ │ ├── controller.js │ │ │ └── model.js │ │ └── user/ │ │ ├── api.js │ │ ├── controller.js │ │ └── model.js │ ├── express.js │ ├── mockData/ │ │ ├── discussions.js │ │ ├── forum.js │ │ ├── opinions.js │ │ └── users.js │ ├── passport.js │ ├── routes.js │ └── utilities/ │ └── tools.js ├── config/ │ ├── credentials.js │ ├── serverConfig.js │ ├── webpack.dev.config.js │ └── webpack.prod.config.js ├── docs/ │ ├── api.md │ └── system_overview.md ├── frontend/ │ ├── App/ │ │ ├── Admin.js │ │ ├── App.js │ │ ├── actions.js │ │ ├── api.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── store.js │ │ └── styles.css │ ├── Components/ │ │ ├── Button/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Dashboard/ │ │ │ ├── Counts/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ └── ForumBox/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── FeedBox/ │ │ │ ├── DiscussionBox/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Footer/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Header/ │ │ │ ├── Logo/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── NavigationBar/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ └── UserMenu/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── NewDiscussion/ │ │ │ ├── PinButton/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ └── TagsInput/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── RichEditor/ │ │ │ ├── BlockStyleControls.js │ │ │ ├── InlineStyleControls.js │ │ │ ├── StyleButton.js │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── SideBar/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── SingleDiscussion/ │ │ │ ├── Discussion/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── Opinion/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ └── ReplyBox/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Tag/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── UserProfile/ │ │ └── Profile/ │ │ ├── index.js │ │ └── styles.css │ ├── Containers/ │ │ ├── AdminHeader/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── Header/ │ │ ├── index.js │ │ └── styles.css │ ├── SharedStyles/ │ │ ├── appLayout.css │ │ └── globalStyles.css │ └── Views/ │ ├── AdminDashboard/ │ │ ├── actions.js │ │ ├── api.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── styles.css │ ├── ForumFeed/ │ │ ├── actions.js │ │ ├── api.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── styles.css │ │ └── tests/ │ │ └── actions.test.js │ ├── NewDiscussion/ │ │ ├── actions.js │ │ ├── api.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── styles.css │ ├── NotFound/ │ │ └── index.js │ ├── SingleDiscussion/ │ │ ├── actions.js │ │ ├── api.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── styles.css │ └── UserProfile/ │ ├── actions.js │ ├── api.js │ ├── constants.js │ ├── index.js │ ├── reducers.js │ └── styles.css ├── package.json ├── public/ │ ├── build/ │ │ ├── bundle.js │ │ └── style.css │ └── index.html └── server.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "es2015", "stage-2", "react" ] } ================================================ FILE: .editorconfig ================================================ [*] indent_style = space end_of_line = lf indent_size = 2 charset = utf-8 trim_trailing_whitespace = true [*.md] max_line_length = 0 trim_trailing_whitespace = false ================================================ FILE: .eslintrc ================================================ { "ecmaFeatures": { "jsx": true, "modules": true }, "env": { "browser": true, "node": true }, "parser": "babel-eslint", "rules": { "comma-dangle": ["error", "always-multiline"], "semi": ["error", "always"], "quotes": [2, "single"], "strict": [2, "never"], "react/jsx-uses-react": 2, "react/jsx-uses-vars": 2, "react/react-in-jsx-scope": 2 }, "plugins": [ "react" ] } ================================================ FILE: .gitignore ================================================ # dependency node_modules # yarn yarn.lock # npm cache .npm # Logs logs *.log npm-debug.log* # coverages coverage # OSX stuffs .DS_Store ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Provash Shoumma Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================  # ReForum A minimal forum application built with the following technologies: * [React](https://facebook.github.io/react/) * [Redux](http://redux.js.org/) * [Webpack](https://webpack.js.org/) * [ExpressJS](https://expressjs.com/) * [PassportJS](http://passportjs.org/) * [MongoDB](https://www.mongodb.com/) ### Application Features * Users can post a discussion * Users can reply their opinions regarding discussion * Users can favorite discussions * Users have their own profile page * Admin can create new forum categories * Admin have a lot of power over every users discussions and opinions :-p ### Documentations * [API Docs](https://github.com/shoumma/ReForum/blob/master/docs/api.md) * [System Overview](https://github.com/shoumma/ReForum/blob/master/docs/system_overview.md) ### Home View  ### Admin View  ## Deploy on you own server Please make sure you have following software installed in your system: * Node.js > 6.0 * NPM / Yarn * Git * MongoDB First we need to clone the repository: ``` $ git clone https://github.com/shoumma/ReForum ``` Then we have to install the necessary dependencies using either NPM or Yarn: ``` $ npm i ``` ``` $ yarn ``` Since the app currently uses GitHub authentication, we need to configure a GitHub OAuth application. You can register a new application from this link https://github.com/settings/developers We need to grab the following information from the OAuth application. * Client ID * Client Secret * Callback URL The `Callback URL` is the domain where GitHub will redirect the user after a successful login. You can use a domain name or local host. But we need to append the URL with the path `/api/user/authViaGitHub/callback`. So, the complete url will look like: `https://localhost:8080/api/user/authViaGitHub/callback` Now, we need to configure the credentials inside of the codebase. Open the file `config/credentials.js` add the necessary information. The file looks like this: ```js module.exports = { GITHUB_CLIENT_ID: '', GITHUB_CLIENT_SECRET: '', GITHUB_CALLBACK_URL: '', DBURL: '', }; ``` We need to provide all the information here. You can notice that we need the database url here too. My `local` MongoDB url looks like: ``` mongodb://localhost:27017/reforum ``` Now we are ready to run the application. You can run either run the development environment of the application which will include Hot-Reload for JS codes using Webpack and the Redux dev tool extension, or you can run the production edition. The default port for developer edition is `8080`, and for production is `process.env.PORT`. To run the app in development environment: ``` $ npm run start:dev ``` To run the app in production environment: ``` $ npm run start ``` Now, if you visit [http://localhost:8080](http://localhost:8080) (if you ran the dev), or the production URL, you will see that the application is up and running. Congratulation! But, wait a minute, it's showing you `Sorry, couldn't find the forum`. That is because, we didn't create any forum yet. You can now sign up via github and then visit the admin panel with the url [http://localhost:8080/admin](http://localhost:8080/admin). The application is currently configured in a way that, the first user will become the admin for the system. Here we can create new forums and that forum will be displayed in the application. The first forum will be used as default forum. Congratulation! You now have a clone of this application in your server. :-) ## Path for Future Work * Add search functionality * Add unit tests for both backend and frontend * Ability to change the name and logo of the site from admin panel. * Make the installation process more interactive * Add multiple theme support. ## License [MIT License](https://github.com/shoumma/Mister-Poster/blob/master/LICENSE). Do whatever you want to do. :-) ## Conclusion The application is created with lots of ♥. Any pull request, issues and contribution is very appreciated. It would be really great if we can take this application to the next level, where it can be used as a platform for forums. [Provash Shoumma](https://twitter.com/proshoumma) ================================================ FILE: backend/dev.js ================================================ /** * module dependencies for development */ const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); /** * development configuration */ const devConfigs = (app) => { // webpack development configuration const webpackConfig = require('../config/webpack.dev.config'); const webpackCompiler = webpack(webpackConfig); // apply dev middleware app.use(webpackDevMiddleware(webpackCompiler, { publicPath: webpackConfig.output.publicPath, hot: true, stats: true, })); // apply hot middleware app.use(webpackHotMiddleware(webpackCompiler)); }; module.exports = devConfigs; ================================================ FILE: backend/entities/admin/api.js ================================================ // controllers const getAdminDashInfo = require('./controller').getAdminDashInfo; const createForum = require('./controller').createForum; const deleteForum = require('./controller').deleteForum; const deleteUser = require('./controller').deleteUser; const deleteDiscussion = require('./controller').deleteDiscussion; /** * admin apis * @param {Object} app */ const adminAPI = (app) => { // get all info for admin dashboard app.get('/api/admin/admin_dashboard_info', (req, res) => { if (req.user && req.user.role === 'admin') { getAdminDashInfo().then( (data) => { res.send(data); }, (error) => { res.send(error); } ); } else res.send({ error: 'You are not admin buddy 😛' }); }); // create a forum app.post('/api/admin/create_forum', (req, res) => { if (req.user && req.user.role === 'admin') { const { title, slug, } = req.body; createForum({ forum_name: title, forum_slug: slug }).then( (data) => { res.send(data); }, (error) => { res.send(error); } ); } else res.send({ error: 'You are not admin buddy 😛' }); }); // delete a forum app.post('/api/admin/delete_forum', (req, res) => { if (req.user && req.user.role === 'admin') { deleteForum(req.body).then( (data) => { res.send(data); }, (error) => { res.send(error); } ); } else res.send({ error: 'You are not admin buddy 😛' }); }); // delete an user app.post('/api/admin/delete_user', (req, res) => { if (req.user && req.user.role === 'admin') { deleteUser(req.body).then( (data) => { res.send(data); }, (error) => { res.send(error); } ); } else res.send({ error: 'You are not admin buddy 😛' }); }); // delete a discussion app.post('/api/admin/delete_discussion', (req, res) => { if (req.user && req.user.role === 'admin') { deleteDiscussion(req.body).then( (data) => { res.send(data); }, (error) => { res.send(error); } ); } else res.send({ error: 'You are not admin buddy 😛' }); }); }; module.exports = adminAPI; ================================================ FILE: backend/entities/admin/controller.js ================================================ const waterfall = require('async/waterfall'); // models const Discussion = require('../discussion/model'); const Opinion = require('../opinion/model'); const Forum = require('../forum/model'); const User = require('../user/model'); /** * get the information for admin dashboard * @return {Promise} */ const getAdminDashInfo = () => { return new Promise((resolve, reject) => { waterfall([ (callback) => { Discussion.count().exec((error, count) => { callback(null, { discussionCount: count }); }); }, (lastResult, callback) => { Opinion.count().exec((error, count) => { callback(null, Object.assign(lastResult, { opinionCount: count })); }); }, (lastResult, callback) => { Forum.count().exec((error, count) => { callback(null, Object.assign(lastResult, { forumCount: count })); }); }, (lastResult, callback) => { User.count().exec((error, count) => { callback(null, Object.assign(lastResult, { userCount: count })); }); }, (lastResult, callback) => { Forum .find({}) .sort({ date: -1 }) .lean() .exec((error, forums) => { callback(null, Object.assign(lastResult, { forums })); }); }, ], (error, result) => { if (error) { console.log(error); reject(error); } else resolve(result); }); }); }; /** * create a new forum * @param {String} forum_name * @param {String} forum_slug * @return {Promise} */ const createForum = ({ forum_name, forum_slug }) => { return new Promise((resolve, reject) => { // check if the forum exists Forum .findOne({ forum_slug }) .exec((error, forum) => { if (error) { console.log(error); reject({ serverError: true }); } else if (forum) { reject({ alreadyExists: true }); } else { // forum does not exists, so create a new one const newForum = new Forum({ forum_slug, forum_name, }); newForum.save((error) => { if (error) { console.log(error); reject({ created: false }); } else { resolve(Object.assign({}, newForum, { created: true })); } }); } }); }); }; /** * delete an entire forum * @param {String} forum_id * @return {Promise} */ const deleteForum = ({ forum_id }) => { return new Promise((resolve, reject) => { // first remove any discussion regarding the forum Discussion.remove({ forum_id }).exec((error) => { if (error) { console.log(error); reject({ deleted: false }); } else { // remove any opinion regarding the forum Opinion.remove({ forum_id }).exec((error) => { if (error) { console.log(error); reject({ deleted: false }); } else { // now we can remove the forum Forum.remove({ _id: forum_id }).exec((error) => { if (error) { console.log(error); reject({ deleted: false }); } else { resolve({ deleted: true }); } }); } }); } }); }); }; /** * delete an user * @param {String} user_id * @return {Promise} */ const deleteUser = ({ user_id }) => { return new Promise((resolve, reject) => { // first we need to remvoe any discussion the user created Discussion.remove({ user_id }).exec((error) => { if (error) { console.log(error); reject({ deleted: false }); } else { // now we need to remove any opinions that are created by the user Opinion.remove({ user_id }).exec((error) => { if (error) { console.log(error); reject({ deleted: false }); } else { // finally we can remove the user User.remove({ _id: user_id }).exec((error) => { if (error) { console.log(error); reject({ deleted: false }); } else { resolve({ deleted: true }); } }); } }); } }); }); }; /** * delete a single discussion * @param {String} discussion_id * @return {Promise} */ const deleteDiscussion = ({ discussion_id }) => { return new Promise((resolve, reject) => { // first we need to remove any opinion regarding the discussion Opinion.remove({ discussion_id }).exec((error) => { if (error) { console.log(error); reject({ deleted: false }); } else { // now we need to remove the discussion Discussion.remove({ _id: discussion_id }).exec((error) => { if (error) { console.log(error); reject({ deleted: false }); } else { resolve({ deleted: true }); } }); } }); }); }; module.exports = { getAdminDashInfo, createForum, deleteForum, deleteUser, deleteDiscussion, }; ================================================ FILE: backend/entities/discussion/api.js ================================================ // discussion controllers const getDiscussion = require('./controller').getDiscussion; const createDiscussion = require('./controller').createDiscussion; const toggleFavorite = require('./controller').toggleFavorite; const deleteDiscussion = require('./controller').deleteDiscussion; /** * discussion apis */ const discussionAPI = (app) => { // get signle discussion app.get('/api/discussion/:discussion_slug', (req, res) => { const { discussion_slug } = req.params; getDiscussion(discussion_slug).then( (result) => { res.send(result); }, (error) => { res.send(error); } ); }); // toggle favorite to the discussion app.put('/api/discussion/toggleFavorite/:discussion_id', (req, res) => { const { discussion_id } = req.params; if (req.user) { // TODO: describe the toggle process with comments toggleFavorite(discussion_id, req.user._id).then( (result) => { getDiscussion(result.discussion_slug).then( (result) => { res.send(result); }, (error) => { res.send({ discussionUpdated: false }); } ); }, (error) => { res.send({ discussionUpdated: false }); } ); } else { res.send({ discussionUpdated: false }); } }); // create a new discussion app.post('/api/discussion/newDiscussion', (req, res) => { if (req.user) { createDiscussion(req.body).then( (result) => { res.send(Object.assign({}, result._doc, { postCreated: true })); }, (error) => { res.send({ postCreated: false }); } ); } else { res.send({ postCreated: false }); } }); // delete a discussion app.delete('/api/discussion/deleteDiscussion/:discussion_slug', (req, res) => { if (req.user) { deleteDiscussion(req.params.discussion_slug).then( (result) => { res.send({ deleted: true }); }, (error) => { res.send({ deleted: false }); } ); } else { res.send({ deleted: false }); } }); }; module.exports = discussionAPI; ================================================ FILE: backend/entities/discussion/controller.js ================================================ const generateDiscussionSlug = require('../../utilities/tools').generateDiscussionSlug; const getAllOpinions = require('../opinion/controller').getAllOpinions; const getUser = require('../user/controller').getUser; const Discussion = require('./model'); const Opinion = require('../opinion/model'); /** * get a single discussion * @param {String} discussion_slug * @param {String} discussion_id * @return {Promise} */ const getDiscussion = (discussion_slug, discussion_id) => { return new Promise((resolve, reject) => { let findObject = {}; if (discussion_slug) findObject.discussion_slug = discussion_slug; if (discussion_id) findObject._id = discussion_id; Discussion .findOne(findObject) .populate('forum') .populate('user') .lean() .exec((error, result) => { if (error) { console.log(error); reject(error); } else if (!result) reject(null); else { // add opinions to the discussion object getAllOpinions(result._id).then( (opinions) => { result.opinions = opinions; resolve(result); }, (error) => { { console.log(error); reject(error); } } ); } }); }); }; /** * Create a new discussion * @param {Object} discussion * @return {Promise} */ const createDiscussion = (discussion) => { return new Promise((resolve, reject) => { const newDiscussion = new Discussion({ forum_id: discussion.forumId, forum: discussion.forumId, user_id: discussion.userId, user: discussion.userId, discussion_slug: generateDiscussionSlug(discussion.title), date: new Date(), title: discussion.title, content: discussion.content, favorites: [], tags: discussion.tags, pinned: discussion.pinned, }); newDiscussion.save((error) => { if (error) { console.log(error); reject(error); } resolve(newDiscussion); }); }); }; /** * toggle favorite status of discussion * @param {ObjectId} discussion_id * @param {ObjectId} user_id * @return {Promise} */ const toggleFavorite = (discussion_id, user_id) => { return new Promise((resolve, reject) => { Discussion.findById(discussion_id, (error, discussion) => { if (error) { console.log(error); reject(error); } else if (!discussion) reject(null); else { // add or remove favorite let matched = null; for (let i = 0; i < discussion.favorites.length; i++) { if (String(discussion.favorites[i]) === String(user_id)) { matched = i; } } if (matched === null) { discussion.favorites.push(user_id); } else { discussion.favorites = [ ...discussion.favorites.slice(0, matched), ...discussion.favorites.slice(matched + 1, discussion.favorites.length), ]; } discussion.save((error, updatedDiscussion) => { if (error) { console.log(error); reject(error); } resolve(updatedDiscussion); }); } }); }); }; const updateDiscussion = (forum_id, discussion_slug) => { // TODO: implement update feature }; const deleteDiscussion = (discussion_slug) => { return new Promise((resolve, reject) => { // find the discussion id first Discussion .findOne({ discussion_slug }) .exec((error, discussion) => { if (error) { console.log(error); reject(error); } // get the discussion id const discussion_id = discussion._id; // remove any opinion regarding the discussion Opinion .remove({ discussion_id }) .exec((error) => { if (error) { console.log(error); reject(error); } // finally remove the discussion else { Discussion .remove({ discussion_slug }) .exec((error) => { if (error) { console.log(error); reject(error); } else { resolve({ deleted: true }); } }); } }); }); }); }; module.exports = { getDiscussion, createDiscussion, updateDiscussion, deleteDiscussion, toggleFavorite, }; ================================================ FILE: backend/entities/discussion/model.js ================================================ /** * discussion model */ const mongoose = require('mongoose'); const discussionSchema = mongoose.Schema({ forum_id: mongoose.Schema.ObjectId, forum: { type: mongoose.Schema.ObjectId, ref: 'forum' }, discussion_slug: String, user_id: mongoose.Schema.ObjectId, user: { type: mongoose.Schema.ObjectId, ref: 'user' }, date: Date, title: String, content: Object, favorites: Array, tags: Array, pinned: Boolean, }); module.exports = mongoose.model('discussion', discussionSchema); ================================================ FILE: backend/entities/forum/api.js ================================================ // forum controllers const getAllForums = require('./controller').getAllForums; const getDiscussions = require('./controller').getDiscussions; /** * forum apis */ const forumAPI = (app) => { // get all forums app.get('/api/forum', (req, res) => { getAllForums().then( (result) => { res.send(result); }, (error) => { res.send(error); } ); }); // get discussions of a forum app.get('/api/forum/:forum_id/discussions', (req, res) => { getDiscussions(req.params.forum_id, false, req.query.sorting_method).then( (result) => { res.send(result); }, (error) => { res.send([]); } ); }); // get pinned discussions of a forum app.get('/api/forum/:forum_id/pinned_discussions', (req, res) => { getDiscussions(req.params.forum_id, true).then( (result) => { res.send(result); }, (error) => { res.send([]); } ); }); }; module.exports = forumAPI; ================================================ FILE: backend/entities/forum/controller.js ================================================ const asyncEach = require('async/each'); // models const Forum = require('./model'); const Discussion = require('../discussion/model'); // controllers const getAllOpinions = require('../opinion/controller').getAllOpinions; const getUser = require('../user/controller').getUser; /** * get all forums list * @type {Promise} */ const getAllForums = () => { return new Promise((resolve, reject) => { Forum .find({}) .exec((error, results) => { if (error) { console.log(error); reject(error); } else if (!results) reject(null); else resolve(results); }); }); }; /** * get discussions of a forum * @param {ObjectId} forum_id * @param {Boolean} pinned * @return {Promise} */ const getDiscussions = (forum_id, pinned, sorting_method='date') => { return new Promise((resolve, reject) => { // define sorthing method const sortWith = { }; if (sorting_method === 'date') sortWith.date = -1; if (sorting_method === 'popularity') sortWith.favorites = -1; // match discussion id and pinned status Discussion .find({ forum_id: forum_id, pinned: pinned }) .sort(sortWith) .populate('forum') .populate('user') .lean() .exec((error, discussions) => { if (error) { console.error(error); reject(error); } else if (!discussions) reject(null); else { // attach opinion count to each discussion asyncEach(discussions, (eachDiscussion, callback) => { // add opinion count getAllOpinions(eachDiscussion._id).then( (opinions) => { // add opinion count to discussion doc eachDiscussion.opinion_count = opinions ? opinions.length : 0; callback(); }, (error) => { console.error(error); callback(error); } ); }, (error) => { if (error) { console.error(error); reject(error); } else resolve(discussions); }); } }); }); }; module.exports = { getAllForums, getDiscussions, }; ================================================ FILE: backend/entities/forum/model.js ================================================ /** * forum model */ const mongoose = require('mongoose'); const forumSchema = mongoose.Schema({ forum_slug: String, forum_name: String, }); module.exports = mongoose.model('forum', forumSchema); ================================================ FILE: backend/entities/opinion/api.js ================================================ // controllers const getAllOpinions = require('./controller').getAllOpinions; const createOpinion = require('./controller').createOpinion; const deleteOpinion = require('./controller').deleteOpinion; /** * opinion apis */ const opinionAPI = (app) => { // create an opinion app.post('/api/opinion/newOpinion', (req, res) => { if(req.user) { createOpinion(req.body).then( (result) => { res.send(result); }, (error) => { res.send(error); } ); } else { res.send({ authenticated: false }); } }); // remove an opinion app.delete('/api/opinion/deleteOpinion/:opinion_id', (req, res) => { if(req.user) { deleteOpinion(req.params.opinion_id).then( (result) => { res.send({ deleted: true }); }, (error) => { res.send({ deleted: false }); } ); } }); }; module.exports = opinionAPI; ================================================ FILE: backend/entities/opinion/controller.js ================================================ // models const Opinion = require('./model'); /** * get all opinion regarding a single discussion * @param {ObjectId} discussion_id * @return {Promise} */ const getAllOpinions = (discussion_id) => { return new Promise((resolve, reject) => { Opinion .find({ discussion_id }) .populate('user') .sort({ date: -1 }) .exec((error, opinions) => { if (error) { console.log(error); reject(error); } else if (!opinions) reject(null); else resolve(opinions); }); }); }; /** * create an opinion regarding a discussion * @param {ObjectId} forum_id * @param {ObjectId} discussion_id * @param {ObjectId} user_id * @param {Object} content * @return {Promise} */ const createOpinion = ({ forum_id, discussion_id, user_id, content }) => { return new Promise((resolve, reject) => { const newOpinion = new Opinion({ forum_id, discussion_id, discussion: discussion_id, user_id, user: user_id, content, date: new Date(), }); newOpinion.save((error) => { if (error) { console.log(error); reject(error); } else { resolve(newOpinion); } }); }); }; const updateOpinion = (opinion_id) => { // TODO: implement update for opinion }; /** * delete a single opinion * @param {ObjectId} opinion_id * @return {Promise} */ const deleteOpinion = (opinion_id) => { return new Promise((resolve, reject) => { Opinion .remove({ _id: opinion_id }) .exec((error) => { if (error) { console.log(error); reject(error); } else resolve('deleted'); }); }); }; module.exports = { getAllOpinions, createOpinion, updateOpinion, deleteOpinion, }; ================================================ FILE: backend/entities/opinion/model.js ================================================ /** * opinion model */ const mongoose = require('mongoose'); const opinionSchema = mongoose.Schema({ forum_id: mongoose.Schema.ObjectId, forum: { type: mongoose.Schema.ObjectId, ref: 'forum' }, discussion_id: mongoose.Schema.ObjectId, discussion: { type: mongoose.Schema.ObjectId, ref: 'discussion' }, user_id: mongoose.Schema.ObjectId, user: { type: mongoose.Schema.ObjectId, ref: 'user' }, date: Date, content: Object, }); module.exports = mongoose.model('opinion', opinionSchema); ================================================ FILE: backend/entities/user/api.js ================================================ const passport = require('passport'); const signIn = require('./controller').signIn; const getFullProfile = require('./controller').getFullProfile; /** * user apis */ const userAPI = (app) => { // get authenticated user app.get('/api/user/getUser', (req, res) => { if (req.user) res.send(req.user); else res.send(null); }); // github authentication route app.get( '/api/user/authViaGitHub', passport.authenticate('github') ); // callback route from github app.get( // this should match callback url of github app '/api/user/authViaGitHub/callback', passport.authenticate('github', { failureRedirect: '/signIn/failed' }), (req, res) => { res.redirect('/'); } ); // signout the user app.get('/api/user/signout', (req, res) => { req.logout(); res.redirect('/'); }); // get user full profile app.get('/api/user/profile/:username', (req, res) => { getFullProfile(req.params.username).then( result => { res.send(result); }, error => { res.send({ error }); } ); }); }; module.exports = userAPI; ================================================ FILE: backend/entities/user/controller.js ================================================ const _ = require('lodash'); const asyncEach = require('async/each'); // controllers const getAllOpinions = require('../opinion/controller').getAllOpinions; // models const User = require('./model'); const Discussion = require('../discussion/model'); const Opinion = require('../opinion/model'); /** * get user doc by user id * @param {ObjectId} user_id * @return {promise} */ const getUser = (user_id) => { return new Promise((resolve, reject) => { User.findOne({ _id: user_id }, (error, user) => { if (error) { console.log(error); reject(error); } else if (!user) reject(null); else resolve(user); }); }); }; /** * sign in/up user via github provided info * this will signin the user if user existed * or will create a new user using git infos * @param {Object} gitProfile profile information provided by github * @return {promise} user doc */ const signInViaGithub = (gitProfile) => { return new Promise((resolve, reject) => { // find if user exist on db User.findOne({ username: gitProfile.username }, (error, user) => { if (error) { console.log(error); reject(error); } else { // get the email from emails array of gitProfile const email = _.find(gitProfile.emails, { verified: true }).value; // user existed on db if (user) { // update the user with latest git profile info user.name = gitProfile.displayName; user.username = gitProfile.username; user.avatarUrl = gitProfile._json.avatar_url; user.email = email; user.github.id = gitProfile._json.id, user.github.url = gitProfile._json.html_url, user.github.company = gitProfile._json.company, user.github.location = gitProfile._json.location, user.github.hireable = gitProfile._json.hireable, user.github.bio = gitProfile._json.bio, user.github.followers = gitProfile._json.followers, user.github.following = gitProfile._json.following, // save the info and resolve the user doc user.save((error) => { if (error) { console.log(error); reject(error); } else { resolve(user); } }); } // user doesn't exists on db else { // check if it is the first user (adam/eve) :-p // assign him/her as the admin User.count({}, (err, count) => { console.log('usercount: ' + count); let assignAdmin = false; if (count === 0) assignAdmin = true; // create a new user const newUser = new User({ name: gitProfile.displayName, username: gitProfile.username, avatarUrl: gitProfile._json.avatar_url, email: email, role: assignAdmin ? 'admin' : 'user', github: { id: gitProfile._json.id, url: gitProfile._json.html_url, company: gitProfile._json.company, location: gitProfile._json.location, hireable: gitProfile._json.hireable, bio: gitProfile._json.bio, followers: gitProfile._json.followers, following: gitProfile._json.following, }, }); // save the user and resolve the user doc newUser.save((error) => { if (error) { console.log(error); reject(error); } else { resolve(newUser); } }); }); } } }); }); }; /** * get the full profile of a user * @param {String} username * @return {Promise} */ const getFullProfile = (username) => { return new Promise((resolve, reject) => { User .findOne({ username }) .lean() .exec((error, result) => { if (error) { console.log(error); reject(error); } else if (!result) reject('not_found'); else { // we got the user, now we need all discussions by the user Discussion .find({ user_id: result._id }) .populate('forum') .lean() .exec((error, discussions) => { if (error) { console.log(error); reject(error); } else { // we got the discussions by the user // we need to add opinion count to each discussion asyncEach(discussions, (eachDiscussion, callback) => { getAllOpinions(eachDiscussion._id).then( (opinions) => { // add opinion count to discussion doc eachDiscussion.opinion_count = opinions ? opinions.length : 0; callback(); }, (error) => { console.error(error); callback(error); } ); }, (error) => { if (error) { console.log(error); reject(error); } else { result.discussions = discussions; resolve(result); } }); } }); } }); }); }; module.exports = { signInViaGithub, getUser, getFullProfile, }; ================================================ FILE: backend/entities/user/model.js ================================================ /** * user model */ const mongoose = require('mongoose'); const userSchema = mongoose.Schema({ name: String, username: String, avatarUrl: String, email: String, role: { type: String, default: 'user' }, // ['admin', 'moderator', 'user'] github: { id: Number, url: String, company: String, location: String, bio: String, hireable: Boolean, followers: Number, following: Number, }, }); module.exports = mongoose.model('user', userSchema); ================================================ FILE: backend/express.js ================================================ /** * module dependencies for express configuration */ const passport = require('passport'); const morgan = require('morgan'); const compress = require('compression'); const cookieParser = require('cookie-parser'); const bodyParser = require('body-parser'); const session = require('express-session'); const mongoStore = require('connect-mongo')(session); const flash = require('connect-flash'); /** * express configuration */ const expressConfig = (app, serverConfigs) => { // apply gzip compression (should be placed before express.static) app.use(compress()); // log server requests to console !serverConfigs.PRODUCTION && app.use(morgan('dev')); // get data from html froms app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // read cookies (should be above session) app.use(cookieParser()); // use session with mongo app.use(session({ resave: false, saveUninitialized: true, secret: 'secret', store: new mongoStore({ url: serverConfigs.DBURL, collection : 'sessions', }), })); // use passport session app.use(passport.initialize()); app.use(passport.session()); // apply passport configs require('./passport')(app); // connect flash for flash messages (should be declared after sessions) app.use(flash()); // apply development environment additionals if (!serverConfigs.PRODUCTION) { require('./dev')(app); } // apply route configs require('./routes')(app); }; module.exports = expressConfig; ================================================ FILE: backend/mockData/discussions.js ================================================ const discussions = [ { 'forum_id': '58c23d2efce8810b6f20b0b3', 'discussion_slug': 'a_pinned_discussion_' + ObjectId, 'user_id': '58c242a96ba2030d170f86f9', 'date': 1486450269704, 'title': 'A pinned discussion', 'content': 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', 'favorites': 2, 'tags': ['react', 'redux', 'mongodb'], 'pinned': true, }, { 'forum_id': '58c23d2efce8810b6f20b0b3', 'discussion_slug': 'another_pinned_discussion_' + ObjectId, 'user_id': '58c242a96ba2030d170f86f9', 'date': 1486450269704, 'title': 'Another pinned discussion', 'content': 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', 'favorites': 3, 'tags': ['react', 'redux'], 'pinned': true, }, { 'forum_id': '58c23d2efce8810b6f20b0b3', 'discussion_slug': 'one_another_pinned_discussion_' + ObjectId, 'user_id': '58c242e2fb2e150d2570e02b', 'date': 1486450269704, 'title': 'One another pinned discussion', 'content': 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', 'favorites': 5, 'tags': ['express', 'mongodb'], 'pinned': true, }, { 'forum_id': '58c23d2efce8810b6f20b0b3', 'discussion_slug': 'a_discussion_from_general_forum_' + ObjectId, 'user_id': '58c242e2fb2e150d2570e02b', 'date': 1486450269704, 'title': 'A discussion from general forum', 'content': 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', 'favorites': 2, 'tags': ['react', 'redux', 'mongodb'], 'pinned': false, }, ]; module.exports = discussions; ================================================ FILE: backend/mockData/forum.js ================================================ const forums = [ { 'forum_id': 0, 'forum_slug': 'general', 'forum_name': 'General', }, { 'forum_id': 1, 'forum_slug': 'react', 'forum_name': 'React', }, { 'forum_id': 2, 'forum_slug': 'redux', 'forum_name': 'Redux', }, ]; module.exports = forums; ================================================ FILE: backend/mockData/opinions.js ================================================ const opinions = [ { 'discussion_id': '58c641904e457708a7147417', 'user_id': '58c242e2fb2e150d2570e02b', 'date': 1486450269704, 'content': 'Awesome stuffs!', }, { 'discussion_id': '58c641904e457708a7147417', 'user_id': '58c242e2fb2e150d2570e02b', 'date': 1486450269704, 'content': 'Awesome stuffs really!', }, { 'discussion_id': '58c641cf88336b08c76f3b50', 'user_id': '58c242a96ba2030d170f86f9', 'date': 1486450269704, 'content': 'Great job dude!', }, { 'discussion_id': '58c641cf88336b08c76f3b50', 'user_id': '58c242a96ba2030d170f86f9', 'date': 1486450269704, 'content': 'These discussions!!!', }, ]; module.exports = opinions; ================================================ FILE: backend/mockData/users.js ================================================ const users = [ { 'user_id': 0, 'username': 'testuser1', 'email': 'testuser1@reforum.abc', 'avatarUrl': 'https://robohash.org/quisapientelibero.png?size=50x50&set=set1', 'name': 'Test User 1', }, { 'user_id': 1, 'username': 'testuser2', 'email': 'testuser2@reforum.abc', 'avatarUrl': 'https://robohash.org/magnidictadeserunt.png?size=50x50&set=set1', 'name': 'Test User 2', }, { 'user_id': 2, 'username': 'testuser3', 'email': 'testuser3@reforum.abc', 'avatarUrl': 'https://robohash.org/ducimusnostrumillo.jpg?size=50x50&set=set1', 'name': 'Test User 3', }, { 'user_id': 3, 'username': 'testuser4', 'email': 'testuser4@reforum.abc', 'avatarUrl': 'https://robohash.org/autemharumvitae.bmp?size=50x50&set=set1', 'name': 'Test User 4', }, { 'user_id': 4, 'username': 'testuser5', 'email': 'testuser5@reforum.abc', 'avatarUrl': 'https://robohash.org/similiquealiquidmaiores.jpg?size=50x50&set=set1', 'name': 'Test User 5', }, ]; module.exports = users; ================================================ FILE: backend/passport.js ================================================ /** * module dependencies for passport configuration */ const passport = require('passport'); const GitHubStrategy = require('passport-github').Strategy; const GITHUB_CLIENT_ID = require('../config/credentials').GITHUB_CLIENT_ID; const GITHUB_CLIENT_SECRET = require('../config/credentials').GITHUB_CLIENT_SECRET; const GITHUB_CALLBACK_URL = require('../config/credentials').GITHUB_CALLBACK_URL; // controllers const getUser = require('./entities/user/controller').getUser; const signInViaGithub = require('./entities/user/controller').signInViaGithub; /** * passport configuration */ const passportConfig = (app) => { passport.serializeUser((user, done) => { done(null, user._id); }); passport.deserializeUser((id, done) => { getUser(id).then( (user) => { done(null, user); }, (error) => { done(error); } ); }); // github strategy for passport using OAuth passport.use(new GitHubStrategy( { clientID: GITHUB_CLIENT_ID, clientSecret: GITHUB_CLIENT_SECRET, callbackURL: GITHUB_CALLBACK_URL, scope: 'user:email', }, (accessToken, refreshToken, gitProfile, done) => { signInViaGithub(gitProfile).then( (user) => { console.log('got the user'); done(null, user); }, (error) => { console.log('something error occurs'); done(error); } ); } )); }; module.exports = passportConfig; ================================================ FILE: backend/routes.js ================================================ /** * module dependencies for routes configuration */ const path = require('path'); const express = require('express'); const userAPI = require('./entities/user/api'); const forumAPI = require('./entities/forum/api'); const discussionAPI = require('./entities/discussion/api'); const opinionAPI = require('./entities/opinion/api'); const adminAPI = require('./entities/admin/api'); /** * routes configurations */ const routesConfig = (app) => { // serves static files from public directory const publicPath = path.resolve(__dirname, '../public'); app.use(express.static(publicPath)); // serve api endpoint app.get('/api', (req, res) => { res.send('Hello from API endpoint'); }); // apply user apis userAPI(app); // apply forum apis forumAPI(app); // apply discussion apis discussionAPI(app); // apply opinion apis opinionAPI(app); // apply admin apis adminAPI(app); // all get request will send index.html for react-router // to handle the route request app.get('*', (req, res) => { res.sendFile(path.resolve(__dirname, '../public', 'index.html')); }); }; module.exports = routesConfig; ================================================ FILE: backend/utilities/tools.js ================================================ /** * Search object properties recursively and * perform callback action on each * @param {Object} obj [object to search props] * @param {Function} callback [action to perform on each props, two parameters (property, object)] * @return {Object} [new modified object] */ const deepPropSearch = (obj, callback) => { // new object for immutability const newObj = Object.assign({}, obj); // recursive search function const deepSearch = (obj) => { for (const prop in obj) { // perform callback for each property callback && callback(prop, obj); // recursive search inside objects/arrays if (typeof obj[prop] === 'object') { if (obj[prop].length && obj[prop].length > 0) { obj[prop].forEach((deepObj) => { deepSearch(deepObj); }); } else { deepSearch(obj[prop]); } } } }; // start deep searching for properties deepSearch(newObj, callback); // return new object, maintain immutability return newObj; }; const generateDiscussionSlug = (discussionTitle) => { const ObjectId = require('mongoose').Types.ObjectId(); return discussionTitle.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '_' + ObjectId; }; module.exports = { deepPropSearch, generateDiscussionSlug, }; ================================================ FILE: config/credentials.js ================================================ module.exports = { GITHUB_CLIENT_ID: '', GITHUB_CLIENT_SECRET: '', GITHUB_CALLBACK_URL: '', DBURL: '', }; ================================================ FILE: config/serverConfig.js ================================================ /** * module dependencies for server configuration */ const path = require('path'); const databaseUrl = require('./credentials').DBURL; /** * server configurations */ const serverConfigs = { PRODUCTION: process.env.NODE_ENV === 'production', PORT: process.env.PORT || 8080, ROOT: path.resolve(__dirname, '..'), DBURL: databaseUrl, }; module.exports = serverConfigs; ================================================ FILE: config/webpack.dev.config.js ================================================ /** * module dependencies for webpack dev configuration */ const path = require('path'); const webpack = require('webpack'); // define paths const nodeModulesPath = path.resolve(__dirname, '../node_modules'); const buildPath = path.resolve(__dirname, '../public', 'build'); const mainAppPath = path.resolve(__dirname, '../frontend', 'App', 'index.js'); const sharedStylesPath = path.resolve(__dirname, '../frontend', 'SharedStyles'); const componentsPath = path.resolve(__dirname, '../frontend', 'Components'); const containersPath = path.resolve(__dirname, '../frontend', 'Containers'); const viewsPath = path.resolve(__dirname, '../frontend', 'Views'); /** * webpack development configuration */ module.exports = { target : 'web', devtool: 'inline-source-map', entry: [ 'webpack-hot-middleware/client', mainAppPath, ], output: { filename: 'bundle.js', path: buildPath, publicPath: '/build/', }, module: { loaders: [ { test: /\.js$/, loaders: [ 'react-hot', 'babel-loader' ], exclude: [nodeModulesPath], }, { test: /\.css$/, loaders: [ 'style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'postcss-loader?sourceMap=inline', ], }, { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' }, { test: /\.svg$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml' }, ], }, postcss: [ require('autoprefixer'), require('postcss-nesting') ], plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin(), ], resolve : { extensions: ['', '.js', '.css'], alias: { SharedStyles: sharedStylesPath, Components: componentsPath, Containers: containersPath, Views: viewsPath, }, }, }; ================================================ FILE: config/webpack.prod.config.js ================================================ /** * module dependencies for webpack production configuration */ const path = require('path'); const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); // define paths const nodeModulesPath = path.resolve(__dirname, '../node_modules'); const buildPath = path.resolve(__dirname, '../public', 'build'); const mainAppPath = path.resolve(__dirname, '../frontend', 'App', 'index.js'); const sharedStylesPath = path.resolve(__dirname, '../frontend', 'SharedStyles'); const componentsPath = path.resolve(__dirname, '../frontend', 'Components'); const containersPath = path.resolve(__dirname, '../frontend', 'Containers'); const viewsPath = path.resolve(__dirname, '../frontend', 'Views'); /** * webpack production configuration */ module.exports = { target : 'web', entry: [ mainAppPath, ], output: { filename: 'bundle.js', path: buildPath, }, module: { loaders: [ { test: /\.js$/, loaders: [ 'react-hot', 'babel-loader' ], exclude: [nodeModulesPath], }, { test: /\.css$/, loader: ExtractTextPlugin.extract( 'style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader?sourceMap=inline' ), }, { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' }, { test: /\.svg$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml' }, ], }, postcss: [ require('autoprefixer'), require('postcss-nesting') ], plugins: [ new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin(), new ExtractTextPlugin('style.css', { allChunks: true }), ], resolve: { extensions: ['', '.js', '.css'], alias: { SharedStyles: sharedStylesPath, Components: componentsPath, Containers: containersPath, Views: viewsPath, }, }, externals: { 'react': 'React', 'react-dom': 'ReactDOM', 'redux': 'Redux', 'react-router': 'ReactRouter', 'moment': 'moment', }, }; ================================================ FILE: docs/api.md ================================================ # API Docs Work in progress :-) ================================================ FILE: docs/system_overview.md ================================================ # System Overview Work in progress :-) ================================================ FILE: frontend/App/Admin.js ================================================ import React, { Component } from 'react'; import { Link, browserHistory } from 'react-router'; import { connect } from 'react-redux'; import { Helmet } from 'react-helmet'; import { getUser } from './actions'; import AdminHeader from 'Containers/AdminHeader'; import appLayout from 'SharedStyles/appLayout.css'; import styles from './styles.css'; class AdminContainer extends Component { componentDidMount() { // fetch the user this.props.getUser(); } render() { const { user } = this.props; if (user.fetchingUser) { return (