├── .gitignore
├── 01-webpack-starter.txt
├── 02-webpack-end.txt
├── backend-api
├── Procfile
├── app.js
├── controllers
│ ├── followController.js
│ ├── postController.js
│ └── userController.js
├── db.js
├── models
│ ├── Follow.js
│ ├── Post.js
│ └── User.js
├── package.json
└── router.js
├── generateHtml.js
├── html-templates
├── about.html
├── chat-is-visible.html
├── create-post.html
├── index-empty-feed.html
├── index-feed.html
├── index-guest.html
├── main.css
├── profile-followers.html
├── profile-posts.html
├── search-is-visible.html
├── single-post.html
└── terms.html
├── previewDist.js
└── vscode-react-component.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | *.diff
3 | *.err
4 | *.orig
5 | *.log
6 | *.rej
7 | *.swo
8 | *.swp
9 | *.vi
10 | *~
11 | *.sass-cache
12 | .stylelintcache
13 | node_modules/
14 | .tmp/
15 | yarn.lock
16 | .DS_Store
17 | Thumbs.db
18 | .cache
19 | .project
20 | .settings
21 | .tmproj
22 | *.esproj
23 | nbproject
24 | *.sublime-project
25 | *.sublime-workspace
26 | *.komodoproject
27 | .komodotools
28 | _notes
29 | .vscode
30 | dwsync.xml
--------------------------------------------------------------------------------
/01-webpack-starter.txt:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 |
3 | module.exports = {
4 | entry: "./app/Main.js",
5 | output: {
6 | publicPath: "/",
7 | path: path.resolve(__dirname, "app"),
8 | filename: "bundled.js"
9 | },
10 | mode: "development",
11 | devtool: "source-map",
12 | devServer: {
13 | port: 3000,
14 | static: {
15 | directory: path.join(__dirname, "app")
16 | },
17 | hot: true,
18 | liveReload: false,
19 | historyApiFallback: { index: "index.html" }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.js$/,
25 | exclude: /(node_modules)/,
26 | use: {
27 | loader: "babel-loader",
28 | options: {
29 | presets: ["@babel/preset-react", ["@babel/preset-env", { targets: { node: "12" } }]]
30 | }
31 | }
32 | }
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/02-webpack-end.txt:
--------------------------------------------------------------------------------
1 | const currentTask = process.env.npm_lifecycle_event
2 | const path = require("path")
3 | const Dotenv = require("dotenv-webpack")
4 | const { CleanWebpackPlugin } = require("clean-webpack-plugin")
5 | const HtmlWebpackHarddiskPlugin = require("html-webpack-harddisk-plugin")
6 | const HtmlWebpackPlugin = require("html-webpack-plugin")
7 | const fse = require("fs-extra")
8 |
9 | /*
10 | Because I didn't bother making CSS part of our
11 | webpack workflow for this project I'm just
12 | manually copying our CSS file to the DIST folder.
13 | */
14 | class RunAfterCompile {
15 | apply(compiler) {
16 | compiler.hooks.done.tap("Copy files", function () {
17 | fse.copySync("./app/main.css", "./dist/main.css")
18 |
19 | /*
20 | If you needed to copy another file or folder
21 | such as your "images" folder, you could just
22 | call fse.copySync() as many times as you need
23 | to here to cover all of your files/folders.
24 | */
25 | })
26 | }
27 | }
28 |
29 | config = {
30 | entry: "./app/Main.js",
31 | output: {
32 | publicPath: "/",
33 | path: path.resolve(__dirname, "app"),
34 | filename: "bundled.js",
35 | },
36 | plugins: [
37 | new Dotenv(),
38 | new HtmlWebpackPlugin({
39 | filename: "index.html",
40 | template: "app/index-template.html",
41 | alwaysWriteToDisk: true,
42 | }),
43 | new HtmlWebpackHarddiskPlugin(),
44 | ],
45 | mode: "development",
46 | module: {
47 | rules: [
48 | {
49 | test: /\.js$/,
50 | exclude: /(node_modules)/,
51 | use: {
52 | loader: "babel-loader",
53 | options: {
54 | presets: ["@babel/preset-react", ["@babel/preset-env", { targets: { node: "12" } }]],
55 | },
56 | },
57 | },
58 | ],
59 | },
60 | }
61 |
62 | if (currentTask == "webpackDev" || currentTask == "dev") {
63 | config.devtool = "source-map"
64 | config.devServer = {
65 | port: 3000,
66 | static: {
67 | directory: path.join(__dirname, "app")
68 | },
69 | hot: true,
70 | liveReload: false,
71 | historyApiFallback: { index: "index.html" },
72 | }
73 | }
74 |
75 | if (currentTask == "webpackBuild") {
76 | config.plugins.push(new CleanWebpackPlugin(), new RunAfterCompile())
77 | config.mode = "production"
78 | config.output = {
79 | publicPath: "/",
80 | path: path.resolve(__dirname, "dist"),
81 | filename: "[name].[chunkhash].js",
82 | chunkFilename: "[name].[chunkhash].js",
83 | }
84 | }
85 |
86 | module.exports = config
87 |
--------------------------------------------------------------------------------
/backend-api/Procfile:
--------------------------------------------------------------------------------
1 | web: node db.js
--------------------------------------------------------------------------------
/backend-api/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express")
2 | const app = express()
3 | const sanitizeHTML = require("sanitize-html")
4 | const jwt = require("jsonwebtoken")
5 |
6 | app.use(express.urlencoded({ extended: false }))
7 | app.use(express.json())
8 |
9 | app.use("/", require("./router"))
10 |
11 | const server = require("http").createServer(app)
12 | const io = require("socket.io")(server, {
13 | pingTimeout: 30000,
14 | cors: true
15 | })
16 |
17 | io.on("connection", function(socket) {
18 | socket.on("chatFromBrowser", function(data) {
19 | try {
20 | let user = jwt.verify(data.token, process.env.JWTSECRET)
21 | socket.broadcast.emit("chatFromServer", { message: sanitizeHTML(data.message, { allowedTags: [], allowedAttributes: {} }), username: user.username, avatar: user.avatar })
22 | } catch (e) {
23 | console.log("Not a valid token for chat.")
24 | }
25 | })
26 | })
27 |
28 | module.exports = server
29 |
--------------------------------------------------------------------------------
/backend-api/controllers/followController.js:
--------------------------------------------------------------------------------
1 | const Follow = require("../models/Follow")
2 |
3 | exports.apiAddFollow = function(req, res) {
4 | let follow = new Follow(req.params.username, req.apiUser._id)
5 | follow
6 | .create()
7 | .then(() => {
8 | res.json(true)
9 | })
10 | .catch(errors => {
11 | res.json(false)
12 | })
13 | }
14 |
15 | exports.apiRemoveFollow = function(req, res) {
16 | let follow = new Follow(req.params.username, req.apiUser._id)
17 | follow
18 | .delete()
19 | .then(() => {
20 | res.json(true)
21 | })
22 | .catch(errors => {
23 | res.json(false)
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/backend-api/controllers/postController.js:
--------------------------------------------------------------------------------
1 | const Post = require("../models/Post")
2 |
3 | exports.apiCreate = function(req, res) {
4 | let post = new Post(req.body, req.apiUser._id)
5 | post
6 | .create()
7 | .then(function(newId) {
8 | res.json(newId)
9 | })
10 | .catch(function(errors) {
11 | res.json(errors)
12 | })
13 | }
14 |
15 | exports.apiUpdate = function(req, res) {
16 | let post = new Post(req.body, req.apiUser._id, req.params.id)
17 | post
18 | .update()
19 | .then(status => {
20 | // the post was successfully updated in the database
21 | // or user did have permission, but there were validation errors
22 | if (status == "success") {
23 | res.json("success")
24 | } else {
25 | res.json("failure")
26 | }
27 | })
28 | .catch(e => {
29 | // a post with the requested id doesn't exist
30 | // or if the current visitor is not the owner of the requested post
31 | res.json("no permissions")
32 | })
33 | }
34 |
35 | exports.apiDelete = function(req, res) {
36 | Post.delete(req.params.id, req.apiUser._id)
37 | .then(() => {
38 | res.json("Success")
39 | })
40 | .catch(e => {
41 | res.json("You do not have permission to perform that action.")
42 | })
43 | }
44 |
45 | exports.search = function(req, res) {
46 | Post.search(req.body.searchTerm)
47 | .then(posts => {
48 | res.json(posts)
49 | })
50 | .catch(e => {
51 | res.json([])
52 | })
53 | }
54 |
55 | exports.reactApiViewSingle = async function(req, res) {
56 | try {
57 | let post = await Post.findSingleById(req.params.id, 0)
58 | res.json(post)
59 | } catch (e) {
60 | res.json(false)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/backend-api/controllers/userController.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/User")
2 | const Post = require("../models/Post")
3 | const Follow = require("../models/Follow")
4 | const jwt = require("jsonwebtoken")
5 |
6 | // how long a token lasts before expiring
7 | const tokenLasts = "365d"
8 |
9 | exports.apiGetPostsByUsername = async function (req, res) {
10 | try {
11 | let authorDoc = await User.findByUsername(req.params.username)
12 | let posts = await Post.findByAuthorId(authorDoc._id)
13 | //res.header("Cache-Control", "max-age=10").json(posts)
14 | res.json(posts)
15 | } catch (e) {
16 | res.status(500).send("Sorry, invalid user requested.")
17 | }
18 | }
19 |
20 | exports.checkToken = function (req, res) {
21 | try {
22 | req.apiUser = jwt.verify(req.body.token, process.env.JWTSECRET)
23 | res.json(true)
24 | } catch (e) {
25 | res.json(false)
26 | }
27 | }
28 |
29 | exports.apiMustBeLoggedIn = function (req, res, next) {
30 | try {
31 | req.apiUser = jwt.verify(req.body.token, process.env.JWTSECRET)
32 | next()
33 | } catch (e) {
34 | res.status(500).send("Sorry, you must provide a valid token.")
35 | }
36 | }
37 |
38 | exports.doesUsernameExist = function (req, res) {
39 | User.findByUsername(req.body.username.toLowerCase())
40 | .then(function () {
41 | res.json(true)
42 | })
43 | .catch(function (e) {
44 | res.json(false)
45 | })
46 | }
47 |
48 | exports.doesEmailExist = async function (req, res) {
49 | let emailBool = await User.doesEmailExist(req.body.email)
50 | res.json(emailBool)
51 | }
52 |
53 | exports.sharedProfileData = async function (req, res, next) {
54 | let viewerId
55 | try {
56 | viewer = jwt.verify(req.body.token, process.env.JWTSECRET)
57 | viewerId = viewer._id
58 | } catch (e) {
59 | viewerId = 0
60 | }
61 | req.isFollowing = await Follow.isVisitorFollowing(req.profileUser._id, viewerId)
62 |
63 | let postCountPromise = Post.countPostsByAuthor(req.profileUser._id)
64 | let followerCountPromise = Follow.countFollowersById(req.profileUser._id)
65 | let followingCountPromise = Follow.countFollowingById(req.profileUser._id)
66 | let [postCount, followerCount, followingCount] = await Promise.all([postCountPromise, followerCountPromise, followingCountPromise])
67 |
68 | req.postCount = postCount
69 | req.followerCount = followerCount
70 | req.followingCount = followingCount
71 |
72 | next()
73 | }
74 |
75 | exports.apiLogin = function (req, res) {
76 | let user = new User(req.body)
77 | user
78 | .login()
79 | .then(function (result) {
80 | res.json({
81 | token: jwt.sign({ _id: user.data._id, username: user.data.username, avatar: user.avatar }, process.env.JWTSECRET, { expiresIn: tokenLasts }),
82 | username: user.data.username,
83 | avatar: user.avatar
84 | })
85 | })
86 | .catch(function (e) {
87 | res.json(false)
88 | })
89 | }
90 |
91 | exports.apiRegister = function (req, res) {
92 | let user = new User(req.body)
93 | user
94 | .register()
95 | .then(() => {
96 | res.json({
97 | token: jwt.sign({ _id: user.data._id, username: user.data.username, avatar: user.avatar }, process.env.JWTSECRET, { expiresIn: tokenLasts }),
98 | username: user.data.username,
99 | avatar: user.avatar
100 | })
101 | })
102 | .catch(regErrors => {
103 | res.status(500).send(regErrors)
104 | })
105 | }
106 |
107 | exports.apiGetHomeFeed = async function (req, res) {
108 | try {
109 | let posts = await Post.getFeed(req.apiUser._id)
110 | res.json(posts)
111 | } catch (e) {
112 | res.status(500).send("Error")
113 | }
114 | }
115 |
116 | exports.ifUserExists = function (req, res, next) {
117 | User.findByUsername(req.params.username)
118 | .then(function (userDocument) {
119 | req.profileUser = userDocument
120 | next()
121 | })
122 | .catch(function (e) {
123 | res.json(false)
124 | })
125 | }
126 |
127 | exports.profileBasicData = function (req, res) {
128 | res.json({
129 | profileUsername: req.profileUser.username,
130 | profileAvatar: req.profileUser.avatar,
131 | isFollowing: req.isFollowing,
132 | counts: { postCount: req.postCount, followerCount: req.followerCount, followingCount: req.followingCount }
133 | })
134 | }
135 |
136 | exports.profileFollowers = async function (req, res) {
137 | try {
138 | let followers = await Follow.getFollowersById(req.profileUser._id)
139 | //res.header("Cache-Control", "max-age=10").json(followers)
140 | res.json(followers)
141 | } catch (e) {
142 | res.status(500).send("Error")
143 | }
144 | }
145 |
146 | exports.profileFollowing = async function (req, res) {
147 | try {
148 | let following = await Follow.getFollowingById(req.profileUser._id)
149 | //res.header("Cache-Control", "max-age=10").json(following)
150 | res.json(following)
151 | } catch (e) {
152 | res.status(500).send("Error")
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/backend-api/db.js:
--------------------------------------------------------------------------------
1 | const dotenv = require("dotenv")
2 | dotenv.config()
3 | const { MongoClient } = require("mongodb")
4 |
5 | const client = new MongoClient(process.env.CONNECTIONSTRING)
6 |
7 | async function start() {
8 | await client.connect()
9 | module.exports = client
10 | const app = require("./app")
11 | app.listen(process.env.PORT)
12 | }
13 |
14 | start()
15 |
--------------------------------------------------------------------------------
/backend-api/models/Follow.js:
--------------------------------------------------------------------------------
1 | const usersCollection = require("../db").db().collection("users")
2 | const followsCollection = require("../db").db().collection("follows")
3 | const ObjectID = require("mongodb").ObjectId
4 | const User = require("./User")
5 |
6 | let Follow = function (followedUsername, authorId) {
7 | this.followedUsername = followedUsername
8 | this.authorId = authorId
9 | this.errors = []
10 | }
11 |
12 | Follow.prototype.cleanUp = async function () {
13 | if (typeof this.followedUsername != "string") {
14 | this.followedUsername = ""
15 | }
16 | }
17 |
18 | Follow.prototype.validate = async function (action) {
19 | // followedUsername must exist in database
20 | let followedAccount = await usersCollection.findOne({ username: this.followedUsername })
21 | if (followedAccount) {
22 | this.followedId = followedAccount._id
23 | } else {
24 | this.errors.push("You cannot follow a user that does not exist.")
25 | }
26 |
27 | let doesFollowAlreadyExist = await followsCollection.findOne({ followedId: this.followedId, authorId: new ObjectID(this.authorId) })
28 | if (action == "create") {
29 | if (doesFollowAlreadyExist) {
30 | this.errors.push("You are already following this user.")
31 | }
32 | }
33 | if (action == "delete") {
34 | if (!doesFollowAlreadyExist) {
35 | this.errors.push("You cannot stop following someone you do not already follow.")
36 | }
37 | }
38 |
39 | // should not be able to follow yourself
40 | if (this.followedId.equals(this.authorId)) {
41 | this.errors.push("You cannot follow yourself.")
42 | }
43 | }
44 |
45 | Follow.prototype.create = function () {
46 | return new Promise(async (resolve, reject) => {
47 | this.cleanUp()
48 | await this.validate("create")
49 | if (!this.errors.length) {
50 | await followsCollection.insertOne({ followedId: this.followedId, authorId: new ObjectID(this.authorId) })
51 | resolve()
52 | } else {
53 | reject(this.errors)
54 | }
55 | })
56 | }
57 |
58 | Follow.prototype.delete = function () {
59 | return new Promise(async (resolve, reject) => {
60 | this.cleanUp()
61 | await this.validate("delete")
62 | if (!this.errors.length) {
63 | await followsCollection.deleteOne({ followedId: this.followedId, authorId: new ObjectID(this.authorId) })
64 | resolve()
65 | } else {
66 | reject(this.errors)
67 | }
68 | })
69 | }
70 |
71 | Follow.isVisitorFollowing = async function (followedId, visitorId) {
72 | let followDoc = await followsCollection.findOne({ followedId: followedId, authorId: new ObjectID(visitorId) })
73 | if (followDoc) {
74 | return true
75 | } else {
76 | return false
77 | }
78 | }
79 |
80 | Follow.getFollowersById = function (id) {
81 | return new Promise(async (resolve, reject) => {
82 | try {
83 | let followers = await followsCollection
84 | .aggregate([
85 | { $match: { followedId: id } },
86 | { $lookup: { from: "users", localField: "authorId", foreignField: "_id", as: "userDoc" } },
87 | {
88 | $project: {
89 | username: { $arrayElemAt: ["$userDoc.username", 0] },
90 | email: { $arrayElemAt: ["$userDoc.email", 0] }
91 | }
92 | }
93 | ])
94 | .toArray()
95 | followers = followers.map(function (follower) {
96 | let user = new User(follower, true)
97 | return { username: follower.username, avatar: user.avatar }
98 | })
99 | resolve(followers)
100 | } catch (e) {
101 | reject()
102 | }
103 | })
104 | }
105 |
106 | Follow.getFollowingById = function (id) {
107 | return new Promise(async (resolve, reject) => {
108 | try {
109 | let followers = await followsCollection
110 | .aggregate([
111 | { $match: { authorId: id } },
112 | { $lookup: { from: "users", localField: "followedId", foreignField: "_id", as: "userDoc" } },
113 | {
114 | $project: {
115 | username: { $arrayElemAt: ["$userDoc.username", 0] },
116 | email: { $arrayElemAt: ["$userDoc.email", 0] }
117 | }
118 | }
119 | ])
120 | .toArray()
121 | followers = followers.map(function (follower) {
122 | let user = new User(follower, true)
123 | return { username: follower.username, avatar: user.avatar }
124 | })
125 | resolve(followers)
126 | } catch (e) {
127 | reject()
128 | }
129 | })
130 | }
131 |
132 | Follow.countFollowersById = function (id) {
133 | return new Promise(async (resolve, reject) => {
134 | let followerCount = await followsCollection.countDocuments({ followedId: id })
135 | resolve(followerCount)
136 | })
137 | }
138 |
139 | Follow.countFollowingById = function (id) {
140 | return new Promise(async (resolve, reject) => {
141 | let count = await followsCollection.countDocuments({ authorId: id })
142 | resolve(count)
143 | })
144 | }
145 |
146 | module.exports = Follow
147 |
--------------------------------------------------------------------------------
/backend-api/models/Post.js:
--------------------------------------------------------------------------------
1 | const postsCollection = require("../db").db().collection("posts")
2 | const followsCollection = require("../db").db().collection("follows")
3 | const ObjectId = require("mongodb").ObjectId
4 | const User = require("./User")
5 | const sanitizeHTML = require("sanitize-html")
6 |
7 | postsCollection.createIndex({ title: "text", body: "text" })
8 |
9 | let Post = function (data, userid, requestedPostId) {
10 | this.data = data
11 | this.errors = []
12 | this.userid = userid
13 | this.requestedPostId = requestedPostId
14 | }
15 |
16 | Post.prototype.cleanUp = function () {
17 | if (typeof this.data.title != "string") {
18 | this.data.title = ""
19 | }
20 | if (typeof this.data.body != "string") {
21 | this.data.body = ""
22 | }
23 |
24 | // get rid of any bogus properties
25 | this.data = {
26 | title: sanitizeHTML(this.data.title.trim(), { allowedTags: [], allowedAttributes: {} }),
27 | body: sanitizeHTML(this.data.body.trim(), { allowedTags: [], allowedAttributes: {} }),
28 | createdDate: new Date(),
29 | author: new ObjectId(this.userid)
30 | }
31 | }
32 |
33 | Post.prototype.validate = function () {
34 | if (this.data.title == "") {
35 | this.errors.push("You must provide a title.")
36 | }
37 | if (this.data.body == "") {
38 | this.errors.push("You must provide post content.")
39 | }
40 | }
41 |
42 | Post.prototype.create = function () {
43 | return new Promise((resolve, reject) => {
44 | this.cleanUp()
45 | this.validate()
46 | if (!this.errors.length) {
47 | // save post into database
48 | postsCollection
49 | .insertOne(this.data)
50 | .then(info => {
51 | resolve(info.insertedId)
52 | })
53 | .catch(e => {
54 | this.errors.push("Please try again later.")
55 | reject(this.errors)
56 | })
57 | } else {
58 | reject(this.errors)
59 | }
60 | })
61 | }
62 |
63 | Post.prototype.update = function () {
64 | return new Promise(async (resolve, reject) => {
65 | try {
66 | let post = await Post.findSingleById(this.requestedPostId, this.userid)
67 | if (post.isVisitorOwner) {
68 | // actually update the db
69 | let status = await this.actuallyUpdate()
70 | resolve(status)
71 | } else {
72 | reject()
73 | }
74 | } catch (e) {
75 | reject()
76 | }
77 | })
78 | }
79 |
80 | Post.prototype.actuallyUpdate = function () {
81 | return new Promise(async (resolve, reject) => {
82 | this.cleanUp()
83 | this.validate()
84 | if (!this.errors.length) {
85 | await postsCollection.findOneAndUpdate({ _id: new ObjectId(this.requestedPostId) }, { $set: { title: this.data.title, body: this.data.body } })
86 | resolve("success")
87 | } else {
88 | resolve("failure")
89 | }
90 | })
91 | }
92 |
93 | Post.reusablePostQuery = function (uniqueOperations, visitorId, finalOperations = []) {
94 | return new Promise(async function (resolve, reject) {
95 | let aggOperations = uniqueOperations
96 | .concat([
97 | { $lookup: { from: "users", localField: "author", foreignField: "_id", as: "authorDocument" } },
98 | {
99 | $project: {
100 | title: 1,
101 | body: 1,
102 | createdDate: 1,
103 | authorId: "$author",
104 | author: { $arrayElemAt: ["$authorDocument", 0] }
105 | }
106 | }
107 | ])
108 | .concat(finalOperations)
109 |
110 | let posts = await postsCollection.aggregate(aggOperations).toArray()
111 |
112 | // clean up author property in each post object
113 | posts = posts.map(function (post) {
114 | post.isVisitorOwner = post.authorId.equals(visitorId)
115 | post.authorId = undefined
116 |
117 | post.author = {
118 | username: post.author.username,
119 | avatar: new User(post.author, true).avatar
120 | }
121 |
122 | return post
123 | })
124 |
125 | resolve(posts)
126 | })
127 | }
128 |
129 | Post.findSingleById = function (id, visitorId) {
130 | return new Promise(async function (resolve, reject) {
131 | if (typeof id != "string" || !ObjectId.isValid(id)) {
132 | reject()
133 | return
134 | }
135 |
136 | let posts = await Post.reusablePostQuery([{ $match: { _id: new ObjectId(id) } }], visitorId)
137 |
138 | if (posts.length) {
139 | resolve(posts[0])
140 | } else {
141 | reject()
142 | }
143 | })
144 | }
145 |
146 | Post.findByAuthorId = function (authorId) {
147 | return Post.reusablePostQuery([{ $match: { author: authorId } }, { $sort: { createdDate: -1 } }])
148 | }
149 |
150 | Post.delete = function (postIdToDelete, currentUserId) {
151 | return new Promise(async (resolve, reject) => {
152 | try {
153 | let post = await Post.findSingleById(postIdToDelete, currentUserId)
154 | if (post.isVisitorOwner) {
155 | await postsCollection.deleteOne({ _id: new ObjectId(postIdToDelete) })
156 | resolve()
157 | } else {
158 | reject()
159 | }
160 | } catch (e) {
161 | reject()
162 | }
163 | })
164 | }
165 |
166 | Post.search = function (searchTerm) {
167 | return new Promise(async (resolve, reject) => {
168 | if (typeof searchTerm == "string") {
169 | let posts = await Post.reusablePostQuery([{ $match: { $text: { $search: searchTerm } } }], undefined, [{ $sort: { score: { $meta: "textScore" } } }])
170 | resolve(posts)
171 | } else {
172 | reject()
173 | }
174 | })
175 | }
176 |
177 | Post.countPostsByAuthor = function (id) {
178 | return new Promise(async (resolve, reject) => {
179 | let postCount = await postsCollection.countDocuments({ author: id })
180 | resolve(postCount)
181 | })
182 | }
183 |
184 | Post.getFeed = async function (id) {
185 | // create an array of the user ids that the current user follows
186 | let followedUsers = await followsCollection.find({ authorId: new ObjectId(id) }).toArray()
187 | followedUsers = followedUsers.map(function (followDoc) {
188 | return followDoc.followedId
189 | })
190 |
191 | // look for posts where the author is in the above array of followed users
192 | return Post.reusablePostQuery([{ $match: { author: { $in: followedUsers } } }, { $sort: { createdDate: -1 } }])
193 | }
194 |
195 | module.exports = Post
196 |
--------------------------------------------------------------------------------
/backend-api/models/User.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcryptjs")
2 | const usersCollection = require('../db').db().collection("users")
3 | const validator = require("validator")
4 | const md5 = require('md5')
5 |
6 | let User = function(data, getAvatar) {
7 | this.data = data
8 | this.errors = []
9 | if (getAvatar == undefined) {getAvatar = false}
10 | if (getAvatar) {this.getAvatar()}
11 | }
12 |
13 | User.prototype.cleanUp = function() {
14 | if (typeof(this.data.username) != "string") {this.data.username = ""}
15 | if (typeof(this.data.email) != "string") {this.data.email = ""}
16 | if (typeof(this.data.password) != "string") {this.data.password = ""}
17 |
18 | // get rid of any bogus properties
19 | this.data = {
20 | username: this.data.username.trim().toLowerCase(),
21 | email: this.data.email.trim().toLowerCase(),
22 | password: this.data.password
23 | }
24 | }
25 |
26 | User.prototype.validate = function() {
27 | return new Promise(async (resolve, reject) => {
28 | if (this.data.username == "") {this.errors.push("You must provide a username.")}
29 | if (this.data.username != "" && !validator.isAlphanumeric(this.data.username)) {this.errors.push("Username can only contain letters and numbers.")}
30 | if (!validator.isEmail(this.data.email)) {this.errors.push("You must provide a valid email address.")}
31 | if (this.data.password == "") {this.errors.push("You must provide a password.")}
32 | if (this.data.password.length > 0 && this.data.password.length < 12) {this.errors.push("Password must be at least 12 characters.")}
33 | if (this.data.password.length > 50) {this.errors.push("Password cannot exceed 50 characters.")}
34 | if (this.data.username.length > 0 && this.data.username.length < 3) {this.errors.push("Username must be at least 3 characters.")}
35 | if (this.data.username.length > 30) {this.errors.push("Username cannot exceed 30 characters.")}
36 |
37 | // Only if username is valid then check to see if it's already taken
38 | if (this.data.username.length > 2 && this.data.username.length < 31 && validator.isAlphanumeric(this.data.username)) {
39 | let usernameExists = await usersCollection.findOne({username: this.data.username})
40 | if (usernameExists) {this.errors.push("That username is already taken.")}
41 | }
42 |
43 | // Only if email is valid then check to see if it's already taken
44 | if (validator.isEmail(this.data.email)) {
45 | let emailExists = await usersCollection.findOne({email: this.data.email})
46 | if (emailExists) {this.errors.push("That email is already being used.")}
47 | }
48 | resolve()
49 | })
50 | }
51 |
52 | User.prototype.login = function() {
53 | return new Promise((resolve, reject) => {
54 | this.cleanUp()
55 | usersCollection.findOne({username: this.data.username}).then((attemptedUser) => {
56 | if (attemptedUser && bcrypt.compareSync(this.data.password, attemptedUser.password)) {
57 | this.data = attemptedUser
58 | this.getAvatar()
59 | resolve("Congrats!")
60 | } else {
61 | reject("Invalid username / password.")
62 | }
63 | }).catch(function(e) {
64 | reject("Please try again later.")
65 | })
66 | })
67 | }
68 |
69 | User.prototype.register = function() {
70 | return new Promise(async (resolve, reject) => {
71 | // Step #1: Validate user data
72 | this.cleanUp()
73 | await this.validate()
74 |
75 | // Step #2: Only if there are no validation errors
76 | // then save the user data into a database
77 | if (!this.errors.length) {
78 | // hash user password
79 | let salt = bcrypt.genSaltSync(10)
80 | this.data.password = bcrypt.hashSync(this.data.password, salt)
81 | await usersCollection.insertOne(this.data)
82 | this.getAvatar()
83 | resolve()
84 | } else {
85 | reject(this.errors)
86 | }
87 | })
88 | }
89 |
90 | User.prototype.getAvatar = function() {
91 | this.avatar = `https://gravatar.com/avatar/${md5(this.data.email)}?s=128`
92 | }
93 |
94 | User.findByUsername = function(username) {
95 | return new Promise(function(resolve, reject) {
96 | if (typeof(username) != "string") {
97 | reject()
98 | return
99 | }
100 | usersCollection.findOne({username: username}).then(function(userDoc) {
101 | if (userDoc) {
102 | userDoc = new User(userDoc, true)
103 | userDoc = {
104 | _id: userDoc.data._id,
105 | username: userDoc.data.username,
106 | avatar: userDoc.avatar
107 | }
108 | resolve(userDoc)
109 | } else {
110 | reject()
111 | }
112 | }).catch(function(e) {
113 | reject()
114 | })
115 | })
116 | }
117 |
118 | User.doesEmailExist = function(email) {
119 | return new Promise(async function(resolve, reject) {
120 | if (typeof(email) != "string") {
121 | resolve(false)
122 | return
123 | }
124 |
125 | let user = await usersCollection.findOne({email: email})
126 | if (user) {
127 | resolve(true)
128 | } else {
129 | resolve(false)
130 | }
131 | })
132 | }
133 |
134 | module.exports = User
--------------------------------------------------------------------------------
/backend-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "complex-app",
3 | "version": "1.0.0",
4 | "description": "hey dude",
5 | "main": "db.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+ssh://git@github.com/mongodb/node-mongodb-native.git"
9 | },
10 | "scripts": {
11 | "watch": "nodemon db",
12 | "start": "node db",
13 | "test": "echo \"Error: no test specified\" && exit 1"
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "ISC",
18 | "overrides": {
19 | "semver": "7.5.3"
20 | },
21 | "dependencies": {
22 | "bcryptjs": "^2.4.3",
23 | "cors": "^2.8.5",
24 | "dotenv": "^16.3.1",
25 | "express": "^4.18.2",
26 | "jsonwebtoken": "^9.0.0",
27 | "md5": "^2.3.0",
28 | "mongodb": "^5.6.0",
29 | "nodemon": "^2.0.22",
30 | "sanitize-html": "^2.11.0",
31 | "socket.io": "^4.7.1",
32 | "validator": "^13.9.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend-api/router.js:
--------------------------------------------------------------------------------
1 | const apiRouter = require("express").Router()
2 | const userController = require("./controllers/userController")
3 | const postController = require("./controllers/postController")
4 | const followController = require("./controllers/followController")
5 | const cors = require("cors")
6 |
7 | apiRouter.use(cors())
8 |
9 | apiRouter.get("/", (req, res) => res.json("Hello, if you see this message that means your backend is up and running successfully. Congrats! Now let's continue learning React!"))
10 |
11 | // check token to log out front-end if expired
12 | apiRouter.post("/checkToken", userController.checkToken)
13 |
14 | apiRouter.post("/getHomeFeed", userController.apiMustBeLoggedIn, userController.apiGetHomeFeed)
15 | apiRouter.post("/register", userController.apiRegister)
16 | apiRouter.post("/login", userController.apiLogin)
17 | apiRouter.get("/post/:id", postController.reactApiViewSingle)
18 | apiRouter.post("/post/:id/edit", userController.apiMustBeLoggedIn, postController.apiUpdate)
19 | apiRouter.delete("/post/:id", userController.apiMustBeLoggedIn, postController.apiDelete)
20 | apiRouter.post("/create-post", userController.apiMustBeLoggedIn, postController.apiCreate)
21 | apiRouter.post("/search", postController.search)
22 |
23 | apiRouter.post("/doesUsernameExist", userController.doesUsernameExist)
24 | apiRouter.post("/doesEmailExist", userController.doesEmailExist)
25 |
26 | // profile related routes
27 | apiRouter.post("/profile/:username", userController.ifUserExists, userController.sharedProfileData, userController.profileBasicData)
28 | apiRouter.get("/profile/:username/posts", userController.ifUserExists, userController.apiGetPostsByUsername)
29 | apiRouter.get("/profile/:username/followers", userController.ifUserExists, userController.profileFollowers)
30 | apiRouter.get("/profile/:username/following", userController.ifUserExists, userController.profileFollowing)
31 |
32 | // follow routes
33 | apiRouter.post("/addFollow/:username", userController.apiMustBeLoggedIn, followController.apiAddFollow)
34 | apiRouter.post("/removeFollow/:username", userController.apiMustBeLoggedIn, followController.apiRemoveFollow)
35 |
36 | module.exports = apiRouter
37 |
--------------------------------------------------------------------------------
/generateHtml.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOMServer from "react-dom/server"
3 | import fs from "fs"
4 | import Footer from "./app/components/Footer"
5 | import Header from "./app/components/Header"
6 | import LoadingDotsIcon from "./app/components/LoadingDotsIcon"
7 | import { StaticRouter as Router } from "react-router"
8 | import StateContext from "./app/StateContext"
9 |
10 | function Shell() {
11 | return (
12 |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Corporis dolorum labore quisquam vel id dicta fuga! Ducimus, quo. Dolore commodi aliquid error veritatis consequuntur, excepturi cumque fuga eum incidunt doloremque?
39 |Lorem ipsum, dolor sit amet consectetur adipisicing elit. At qui enim rem totam voluptatum. Aut saepe temporibus, facilis ex a iste expedita minima dolorum dicta doloribus libero aliquid, quae maxime? Lorem ipsum dolor sit amet consectetur adipisicing elit. Fugiat suscipit beatae eum, est soluta ducimus ratione et impedit sapiente, nihil, atque dignissimos adipisci? Totam atque officia quis voluptates sed veniam?
40 |Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita voluptates quisquam possimus tenetur, dicta enim rerum quis, quaerat id nobis provident quo dolorum sapiente temporibus facere non repellendus consequatur cupiditate!
41 |Your feed displays the latest posts from the people you follow. If you don’t have any friends to follow that’s okay; you can use the “Search” feature in the top menu bar to find content written by people with similar interests and then follow them.
44 |Are you sick of short tweets and impersonal “shared” posts that are reminiscent of the late 90’s email forwards? We believe getting back to actually writing is the key to enjoying the internet again.
41 |
51 |
52 |
53 |
54 | Posted by brad on 2/10/2020
55 |
Lorem ipsum dolor sit example post adipisicing elit. Iure ea at esse, tempore qui possimus soluta impedit natus voluptate, sapiente saepe modi est pariatur. Aut voluptatibus aspernatur fugiat asperiores at.
59 |Lorem ipsum dolor sit amet consectetur adipisicing elit. Beatae quod asperiores corrupti omnis qui, placeat neque modi, dignissimos, ab exercitationem eligendi culpa explicabo nulla tempora rem? Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure ea at esse, tempore qui possimus soluta impedit natus voluptate, sapiente saepe modi est pariatur. Aut voluptatibus aspernatur fugiat asperiores at.
60 |Lorem ipsum dolor sit amet consectetur adipisicing elit. Corporis dolorum labore quisquam vel id dicta fuga! Ducimus, quo. Dolore commodi aliquid error veritatis consequuntur, excepturi cumque fuga eum incidunt doloremque?
39 |Lorem ipsum, dolor sit amet consectetur adipisicing elit. At qui enim rem totam voluptatum. Aut saepe temporibus, facilis ex a iste expedita minima dolorum dicta doloribus libero aliquid, quae maxime? Lorem ipsum dolor sit amet consectetur adipisicing elit. Fugiat suscipit beatae eum, est soluta ducimus ratione et impedit sapiente, nihil, atque dignissimos adipisci? Totam atque officia quis voluptates sed veniam?
40 |Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita voluptates quisquam possimus tenetur, dicta enim rerum quis, quaerat id nobis provident quo dolorum sapiente temporibus facere non repellendus consequatur cupiditate!
41 |Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae laboriosam distinctio atque sint earum? Temporibus, voluptas aspernatur aliquam nisi sed harum laborum, nemo odio animi officia quisquam. Veniam, natus reprehenderit.
43 |