├── Procfile ├── AUTHORS ├── .dockerignore ├── test ├── fixtures │ └── users.js ├── test_tweets.js └── test.js ├── Makefile ├── public ├── img │ ├── bird.png │ └── bird.svg ├── font │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── webfonts │ ├── fa-solid-900.eot │ ├── fa-solid-900.ttf │ ├── fa-brands-400.eot │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-regular-400.eot │ ├── fa-regular-400.ttf │ ├── fa-solid-900.woff │ ├── fa-solid-900.woff2 │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.woff │ └── fa-regular-400.woff2 ├── css │ ├── fa-brands.min.css │ ├── fa-solid.min.css │ ├── fa-regular.min.css │ ├── fa-brands.css │ ├── fa-solid.css │ ├── fa-regular.css │ ├── pygments-manni.css │ ├── bootstrap │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.css │ │ └── bootstrap-reboot.min.css.map │ ├── style.css.map │ └── style.css └── js │ └── app.js ├── app ├── views │ ├── components │ │ ├── tweets.pug │ │ ├── modals │ │ │ ├── tweet-modal.pug │ │ │ ├── delete-confirmation-modal.pug │ │ │ ├── new-tweet-modal.pug │ │ │ └── new-message-modal.pug │ │ ├── pagination.pug │ │ ├── followers.pug │ │ ├── recent-visits.pug │ │ ├── useful-links.pug │ │ ├── comments.pug │ │ ├── tweet.pug │ │ └── profile-card.pug │ ├── layouts │ │ ├── default.pug │ │ ├── footer.pug │ │ ├── head.pug │ │ └── header.pug │ ├── pages │ │ ├── 404.pug │ │ ├── 500.pug │ │ ├── followers.pug │ │ ├── profile.pug │ │ ├── activity.pug │ │ ├── login.pug │ │ ├── analytics.pug │ │ └── index.pug │ └── chat │ │ ├── chat.pug │ │ └── index.pug ├── styles │ ├── layout │ │ ├── _footer.scss │ │ └── _navigation.scss │ ├── abstracts │ │ ├── _placeholders.scss │ │ └── _variables.scss │ ├── components │ │ ├── _followers.scss │ │ ├── _pagination.scss │ │ ├── _profile-card.scss │ │ ├── _modals.scss │ │ └── _tweets.scss │ ├── pages │ │ ├── _login.scss │ │ ├── _messaging.scss │ │ ├── _analytics.scss │ │ └── _home.scss │ ├── main.scss │ └── base │ │ └── _reset.scss ├── models │ ├── notification.js │ ├── analytics.js │ ├── activity.js │ ├── chat.js │ ├── user.js │ └── tweets.js ├── controllers │ ├── activity.js │ ├── favorites.js │ ├── apiv1.js │ ├── follows.js │ ├── comments.js │ ├── chat.js │ ├── tweets.js │ ├── analytics.js │ └── users.js └── middlewares │ └── logger.js ├── .travis.yml ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── -meta-.md │ ├── meta.md │ ├── Feature_request.md │ └── Bug_report.md ├── workflows │ ├── greetings.yml │ └── codeql.yml ├── FUNDING.yml ├── pull_request_template.md └── issue_template.md ├── Dockerfile ├── .env.example ├── docker-compose.yml ├── .eslintrc.js ├── gruntfile.js ├── config ├── config.example.js ├── config.local.js ├── middlewares │ ├── authorization.js │ └── logger.js ├── config.js ├── passport.js ├── express.js └── routes.js ├── lib └── utils.js ├── server.js ├── package.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── readme.md └── LICENSE /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Vinit Kumar 2 | Robert Cooper 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules* 2 | npm-debug.log -------------------------------------------------------------------------------- /test/fixtures/users.js: -------------------------------------------------------------------------------- 1 | exports.userA = { 2 | }; 3 | 4 | exports.userB = { 5 | }; 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ./node_modules/.bin/mocha --reporter spec --exit 3 | .PHONY: test 4 | -------------------------------------------------------------------------------- /public/img/bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/img/bird.png -------------------------------------------------------------------------------- /app/views/components/tweets.pug: -------------------------------------------------------------------------------- 1 | each tweet in tweets 2 | include tweet 3 | include modals/tweet-modal 4 | -------------------------------------------------------------------------------- /public/font/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/font/FontAwesome.otf -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - mongodb 3 | language: node_js 4 | node_js: 5 | - "12.18.3" 6 | script: npm test 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | npm-debug.log 3 | .vscode/* 4 | .idea 5 | .DS_Store 6 | .sass-cache 7 | .env 8 | data/* 9 | -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /public/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinitkumar/node-twitter/HEAD/public/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /app/views/components/modals/tweet-modal.pug: -------------------------------------------------------------------------------- 1 | .modal.fade(id="tweet-modal-"+tweet._id) 2 | .modal-dialog(role='document') 3 | include ../tweet 4 | -------------------------------------------------------------------------------- /app/views/components/pagination.pug: -------------------------------------------------------------------------------- 1 | - if (pages > 1) 2 | nav(aria-label='Page navigation').pagination 3 | ul.pagination__list 4 | != pagination 5 | -------------------------------------------------------------------------------- /app/styles/layout/_footer.scss: -------------------------------------------------------------------------------- 1 | /* Footer */ 2 | 3 | footer { 4 | text-align: center; 5 | background-color: #243447; 6 | padding-top: 30px; 7 | height: 150px; 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-meta-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[META]" 3 | about: Discuss anything that's not a feature request or bug report 4 | title: '' 5 | labels: question, discussion 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Meta 3 | about: Discuss anything that's not a feature request or bug report 4 | title: "[META]" 5 | labels: discussion, question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/styles/abstracts/_placeholders.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | %block-content { 4 | background-color: $yankees-blue; 5 | padding: 15px; 6 | border-radius: 2px; 7 | border: 1px solid $black; 8 | margin-bottom: 20px; 9 | } 10 | -------------------------------------------------------------------------------- /app/views/layouts/default.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | include head 4 | if (req.isAuthenticated()) 5 | body.login-screen 6 | include header 7 | .container 8 | block main 9 | block content 10 | include footer 11 | -------------------------------------------------------------------------------- /app/views/pages/404.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1 Oops something went wrong 5 | br 6 | span 404 7 | 8 | block content 9 | #error-message-box 10 | #error-stack-trace 11 | pre 12 | code!= error 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | WORKDIR /usr/src/app 4 | # Install app dependencies 5 | COPY package*.json ./ 6 | RUN npm install 7 | # Copy app source code 8 | COPY . . 9 | #Expose port and start application 10 | EXPOSE 3000 11 | 12 | CMD [ "node", "server.js" ] -------------------------------------------------------------------------------- /app/views/pages/500.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1 Oops something went wrong 5 | br 6 | span 500 7 | 8 | block content 9 | #error-message-box 10 | #error-stack-trace 11 | pre.pre-scrollable 12 | code!=error 13 | -------------------------------------------------------------------------------- /app/models/notification.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const Notification = new Schema({ 5 | type: { type: Number }, 6 | activity: { type: Schema.ObjectId, ref: "Activity" } 7 | }); 8 | 9 | mongoose.model("Notification", Notification); 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Example of Environment Variables 2 | 3 | NODE_ENV=development 4 | GITHUB_CLIENT_SECRET="your_github_client_secret" 5 | SECRET="your_secret" 6 | GITHUB_CLIENT_ID="your_github_client_id" 7 | DB="mongodb://localhost/ntwitter" 8 | # Use this when run via Docker 9 | # DB="mongodb://mongodb/ntwitter" 10 | PORT=3000 11 | -------------------------------------------------------------------------------- /app/styles/components/_followers.scss: -------------------------------------------------------------------------------- 1 | @import '../abstracts/placeholders'; 2 | 3 | .follower { 4 | @extend %block-content; 5 | text-align: center; 6 | } 7 | 8 | .follower__user-info { 9 | margin-top: 10px; 10 | } 11 | 12 | .follower__image { 13 | border-radius: 50%; 14 | width: 100px; 15 | max-width: 100%; 16 | } 17 | -------------------------------------------------------------------------------- /app/views/components/followers.pug: -------------------------------------------------------------------------------- 1 | .col-lg-4 2 | .follower 3 | - if (user.github !== undefined) 4 | img(class="follower__image", src=user.github.avatar_url) 5 | div.follower__user-info 6 | span.follower__handle 7 | - if (user.github !== undefined) 8 | a(href="/users/" + user._id) @#{user.github.login} 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "3000:3000" 7 | depends_on: 8 | - mongodb 9 | restart: always 10 | mongodb: 11 | container_name: mongodb 12 | image: mongo 13 | volumes: 14 | - ./data:/data/db 15 | ports: 16 | - "27017:27017" 17 | -------------------------------------------------------------------------------- /app/views/components/recent-visits.pug: -------------------------------------------------------------------------------- 1 | h4.recent-visits__title Recent Visits 2 | ul.recent-visits__list 3 | each analytic in analytics 4 | if analytic.user 5 | -var name = analytic.user.name ? analytic.user.name : analytic.user.username 6 | li 7 | a(href="/users/"+analytic.user._id)= name 8 | span (#{moment(analytic.createdAt).format('lll')}) 9 | -------------------------------------------------------------------------------- /app/controllers/activity.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Activity = mongoose.model("Activity"); 3 | 4 | exports.index = (req, res) => { 5 | let activities; 6 | let options = {}; 7 | Activity.list(options).then(result => { 8 | activities = result; 9 | return res.render("pages/activity", { 10 | activities: activities 11 | }); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /app/views/pages/followers.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row#followers-page 5 | .col-lg-4 6 | include ../components/profile-card 7 | .col-lg-8 8 | if (followers.length !== 0) 9 | .row 10 | each user in followers 11 | include ../components/followers 12 | else 13 | h4 Sorry! It looks like no one is following you. 14 | -------------------------------------------------------------------------------- /app/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | 3 | const level = process.env.LOG_LEVEL || 'debug'; 4 | 5 | const logger = new winston.createLogger({ 6 | transports: [ 7 | new winston.transports.Console({ 8 | level: level, 9 | timestamp: function () { 10 | return (new Date()).toISOString(); 11 | } 12 | }) 13 | ] 14 | }); 15 | 16 | module.exports = logger; 17 | -------------------------------------------------------------------------------- /app/styles/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $dark-jungle-green: #141D26; 3 | $grey-blue: #8899A6; 4 | $dodger-blue: #1DA1F2; 5 | $jungle-green: #2AAF81; 6 | $yankees-blue: #1B2836; 7 | $white: #ffffff; 8 | $go-red: #E53939; 9 | $black: #000; 10 | 11 | // Breakpoints 12 | $small: '(min-width: 576px)'; 13 | $medium: '(min-width: 768px)'; 14 | $large: '(min-width: 992px)'; 15 | $xlarge: '(min-width: 1200px)'; 16 | -------------------------------------------------------------------------------- /app/styles/pages/_login.scss: -------------------------------------------------------------------------------- 1 | /* Login Page */ 2 | 3 | .login { 4 | padding-top: 36px; 5 | } 6 | 7 | .login-container { 8 | text-align: center; 9 | margin: auto; 10 | } 11 | 12 | .github-btn { 13 | margin-left: 20px; 14 | } 15 | 16 | .icon-container { 17 | margin-left: 6px; 18 | } 19 | 20 | .stats-parent-container { 21 | margin-top: 60px; 22 | } 23 | 24 | .login-using-github { 25 | font-size: 20px; 26 | } 27 | -------------------------------------------------------------------------------- /app/views/components/useful-links.pug: -------------------------------------------------------------------------------- 1 | h4.useful-links__title Useful links 2 | ul.useful-links__list 3 | li 4 | a(href="https://github.com/vinitkumar/node-twitter/graphs/contributors") Authors 5 | li 6 | a(href="https://github.com/vinitkumar/node-twitter/blob/master/License") License 7 | li 8 | a(href="http://github.com/vinitkumar/node-twitter") Source Code 9 | li 10 | a(href="https://github.com/vinitkumar/node-twitter/issues/new") Report a Bug 11 | -------------------------------------------------------------------------------- /app/views/components/modals/delete-confirmation-modal.pug: -------------------------------------------------------------------------------- 1 | .delete-confirmation.modal.fade 2 | .modal-dialog(role='document') 3 | .modal-content 4 | .modal-header 5 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 6 | h4.modal-title Are You Sure ? 7 | .modal-body 8 | form(method='post', action=`/users/${user._id}/delete`) 9 | button.btn.delete-confirmation__button(type='submit') Delete My Account 10 | -------------------------------------------------------------------------------- /app/views/components/modals/new-tweet-modal.pug: -------------------------------------------------------------------------------- 1 | .new-tweet.modal.fade 2 | .modal-dialog(role='document') 3 | .modal-content 4 | .modal-header 5 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 6 | h4.modal-title Compose new Tweet 7 | .modal-body 8 | form(method='post', action='/tweets') 9 | textarea(type='text', name='body', placeholder='Enter your tweet here', maxlength='280') 10 | button.btn.new-tweet__button(type='submit') Tweet 11 | -------------------------------------------------------------------------------- /app/views/pages/profile.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | - var currentUserId = req.user._id.toString() 5 | - var userId = user._id.toString() 6 | .row#profile-page 7 | .col-lg-4 8 | include ../components/profile-card 9 | - if (currentUserId === userId) 10 | a.btn.btn__delete(data-toggle='modal', href='.delete-confirmation') 11 | span Delete 12 | .col-lg-8 13 | include ../components/tweets 14 | include ../components/modals/delete-confirmation-modal 15 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'base/reset'; 2 | @import 'layout/navigation'; 3 | @import 'components/followers'; 4 | @import 'components/tweets'; 5 | @import 'components/pagination'; 6 | @import 'components/modals.scss'; 7 | @import 'components/profile-card'; 8 | @import 'pages/login'; 9 | @import 'pages/home'; 10 | @import 'pages/analytics'; 11 | @import 'pages/messaging'; 12 | @import 'layout/footer'; 13 | 14 | /* Media Queries */ 15 | 16 | #error-stack-trace { 17 | background: yellow; 18 | padding: 20px; 19 | margin-top: 20px; 20 | } 21 | -------------------------------------------------------------------------------- /app/controllers/favorites.js: -------------------------------------------------------------------------------- 1 | // ### Create Favorite 2 | exports.create = (req, res) => { 3 | const tweet = req.tweet; 4 | tweet._favorites = req.user; 5 | tweet.save(err => { 6 | if (err) { 7 | return res.send(400); 8 | } 9 | res.send(201, {}); 10 | }); 11 | }; 12 | 13 | // ### Delete Favorite 14 | exports.destroy = (req, res) => { 15 | const tweet = req.tweet; 16 | 17 | tweet._favorites = req.user; 18 | tweet.save(err => { 19 | if (err) { 20 | return res.send(400); 21 | } 22 | res.send(200); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Hi! Thanks for taking the time. Please fill up the issue in as much detail as possible'' first issue' 13 | pr-message: 'Thanks for making a contribution to the project. One of the maintainers will look into it and get back to you as soon as possible'' first pr' 14 | -------------------------------------------------------------------------------- /app/styles/components/_pagination.scss: -------------------------------------------------------------------------------- 1 | // Pagination 2 | 3 | @import '../abstracts/variables'; 4 | 5 | .pagination { 6 | text-align: center; 7 | 8 | a, 9 | li.out-of-range { 10 | padding: 6px 12px; 11 | } 12 | } 13 | 14 | .pagination { 15 | justify-content: center; 16 | } 17 | 18 | .pagination__list { 19 | display: flex; 20 | 21 | li a { 22 | color: $white; 23 | border-color: $black !important; 24 | } 25 | 26 | .no a { 27 | background-color: $yankees-blue; 28 | color: $white; 29 | } 30 | 31 | .active a { 32 | background-color: $dodger-blue !important; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/views/components/modals/new-message-modal.pug: -------------------------------------------------------------------------------- 1 | .new-message.modal.fade(id=modalId) 2 | .modal-dialog(role='document') 3 | .modal-content 4 | .modal-header 5 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 6 | h4.modal-title Send new message to #{name} 7 | .modal-body 8 | form(method='post', action='/chats') 9 | textarea(type='text', name='body', placeholder='Enter your message here', maxlength='200') 10 | input(type="hidden", name='receiver', value=user._id) 11 | button.new-message__button.btn(type='submit') Send Message 12 | -------------------------------------------------------------------------------- /app/views/pages/activity.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .analytics 5 | h1 Activity 6 | each activity in activities 7 | - var senderName = activity.sender.name ? activity.sender.name : activity.sender.username 8 | - var receiverName = activity.receiver.name ? activity.receiver.name : activity.receiver.username 9 | - var notificationText = senderName + ' ' + activity.activityStream + ' ' + receiverName 10 | p 11 | i(class="far fa-bell notification-icon") 12 | span #{moment(activity.createdAt).format("MMM D, YYYY [at] h:mm a, ")} 13 | span #{notificationText} 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [vinitkumar] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: vinit-kumar 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 2017 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /public/css/fa-brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Brands;font-style:normal;font-weight:400;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:Font Awesome\ 5 Brands} -------------------------------------------------------------------------------- /public/css/fa-solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:Font Awesome\ 5 Free;font-weight:900} -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | ## Issue Addressed (if applicable) 4 | _Issue number being fixed (Format: Fixes #xxx)_ 5 | 6 | ## Type 7 | 8 | - [ ] Bug fix (non-breaking change which fixes an issue) 9 | - [ ] New feature (non-breaking change which adds functionality) 10 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 11 | 12 | ### Screenshots (if applicable): 13 | 14 | 17 | -------------------------------------------------------------------------------- /public/css/fa-regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:Font Awesome\ 5 Free;font-weight:400} -------------------------------------------------------------------------------- /app/models/analytics.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const AnalyticsSchema = new Schema({ 5 | ip: String, 6 | user: { type: Schema.ObjectId, ref: "User" }, 7 | url: String, 8 | createdAt: { type: Date, default: Date.now } 9 | }); 10 | 11 | AnalyticsSchema.statics = { 12 | list: function(options) { 13 | const criteria = options.criteria || {}; 14 | return this.find(criteria) 15 | .populate("user", "name username provider") 16 | .sort({ createdAt: -1 }) 17 | .limit(options.perPage) 18 | .skip(options.perPage * options.page); 19 | } 20 | }; 21 | 22 | mongoose.model("Analytics", AnalyticsSchema); 23 | -------------------------------------------------------------------------------- /app/views/layouts/footer.pug: -------------------------------------------------------------------------------- 1 | footer(role='contentinfo') 2 | .container 3 | p Twitter is a registered trademark of Twitter Inc. This project is just for educational purpose only. 4 | p 5 | | Designed and built with all the love in the world by   6 | a(href='https://github.com/vinitkumar/node-twitter/graphs/contributors', target='_blank') Node Twitter Contributors 7 | | using   8 | a(href='https://nodejs.org', target='_blank') nodejs and other OSS technologies. 9 | | 10 | p 11 | | Code licensed   12 | a(rel='license', href='https://github.com/vinitkumar/node-twitter/blob/master/License', target='_blank') Apache License 2.0 13 | 14 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | sass: { 6 | options: { 7 | sourceMap: true 8 | }, 9 | dist: { 10 | files: { 11 | 'public/css/style.css' : 'app/styles/main.scss' 12 | } 13 | } 14 | }, 15 | watch: { 16 | css: { 17 | files: '**/*.scss', 18 | tasks: ['sass'] 19 | } 20 | } 21 | }); 22 | 23 | // Load Grunt plugins 24 | grunt.loadNpmTasks('grunt-sass'); 25 | grunt.loadNpmTasks('grunt-contrib-watch'); 26 | 27 | // Register custom tasks to run from the terminal 28 | grunt.registerTask('default',['watch']); 29 | }; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /public/css/fa-brands.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Brands'; 7 | font-style: normal; 8 | font-weight: normal; 9 | src: url("../webfonts/fa-brands-400.eot"); 10 | src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } 11 | 12 | .fab { 13 | font-family: 'Font Awesome 5 Brands'; } 14 | -------------------------------------------------------------------------------- /public/css/fa-solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | src: url("../webfonts/fa-solid-900.eot"); 10 | src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } 11 | 12 | .fa, 13 | .fas { 14 | font-family: 'Font Awesome 5 Free'; 15 | font-weight: 900; } 16 | -------------------------------------------------------------------------------- /public/css/fa-regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 400; 9 | src: url("../webfonts/fa-regular-400.eot"); 10 | src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } 11 | 12 | .far { 13 | font-family: 'Font Awesome 5 Free'; 14 | font-weight: 400; } 15 | -------------------------------------------------------------------------------- /app/styles/components/_profile-card.scss: -------------------------------------------------------------------------------- 1 | /* Profile Card */ 2 | 3 | @import '../abstracts/placeholders'; 4 | @import '../abstracts/variables'; 5 | 6 | .profile { 7 | @extend %block-content; 8 | text-align: center; 9 | } 10 | 11 | .profile__handle { 12 | font-size: 18px; 13 | margin-top: 10px; 14 | margin-bottom: 5px; 15 | display: inline-block; 16 | } 17 | 18 | .profile__image { 19 | border: 0; 20 | border-radius: 50%; 21 | width: 150px; 22 | max-width: 100%; 23 | } 24 | 25 | .profile__messaging-options { 26 | .btn { 27 | margin: 10px; 28 | } 29 | } 30 | 31 | .profile__follow { 32 | margin: 10px 0; 33 | } 34 | 35 | .profile__follow-button.following { 36 | background-color: $go-red; 37 | 38 | &:hover { 39 | background-color: lighten($go-red, 5%); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/views/chat/chat.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row.twitter-container 5 | .col-xl-12.col-lg-12.first-column 6 | .logged-user 7 | .row.chatbox 8 | - if (chats.length == 0) 9 | h2 No chats sorry! 10 | ul.chatlist 11 | - if (chats) 12 | each chat in chats 13 | li 14 | - if (chat.sender != null) 15 | img(class='tweet__image', src=chat.sender.github.avatar_url) 16 | p.chatMessage 17 | a(class='sender', href="/users/" + chat.sender._id) 18 | span.sender #{chat.sender.username}: 19 | span.message #{chat.message} 20 | p.msgtime #{moment(chat.createdAt).format("MMM D, YYYY [at] h:mm a")} 21 | 22 | -------------------------------------------------------------------------------- /config/config.example.js: -------------------------------------------------------------------------------- 1 | const path = require("path"), 2 | rootPath = path.normalize(__dirname + "/.."); 3 | module.exports = { 4 | development: { 5 | db: "", 6 | root: rootPath, 7 | app: { 8 | name: "Node Twitter" 9 | }, 10 | github: { 11 | clientID: "", 12 | clientSecret: "", 13 | callbackURL: "" 14 | } 15 | }, 16 | test: { 17 | db: "", 18 | root: rootPath, 19 | app: { 20 | name: "Node Twitter" 21 | }, 22 | github: { 23 | clientID: "", 24 | clientSecret: "", 25 | callbackURL: "" 26 | } 27 | }, 28 | production: { 29 | db: "", 30 | root: rootPath, 31 | app: { 32 | name: "Node Twitter" 33 | }, 34 | github: { 35 | clientID: "", 36 | clientSecret: "", 37 | callbackURL: "" 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /app/models/activity.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const ActivitySchema = new Schema({ 5 | activityStream: { type: String, default: "", maxlength: 400 }, 6 | activityKey: { type: Schema.ObjectId }, 7 | sender: { type: Schema.ObjectId, ref: "User" }, 8 | receiver: { type: Schema.ObjectId, ref: "User" }, 9 | createdAt: { type: Date, default: Date.now } 10 | }); 11 | 12 | ActivitySchema.statics = { 13 | list: function(options) { 14 | const criteria = options.criteria || {}; 15 | return this.find(criteria) 16 | .populate("sender", "name username provider") 17 | .populate("receiver", "name username provider") 18 | .sort({ createdAt: -1 }) 19 | .limit(options.perPage) 20 | .skip(options.perPage * options.page); 21 | } 22 | }; 23 | 24 | mongoose.model("Activity", ActivitySchema); 25 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Feature 4 | 5 | ### User story 6 | 7 | 8 | ### Conditions to be met 9 | 10 | 11 | ### Additonal specifications 12 | 13 | 14 | ---- 15 | 16 | ## Bug 17 | 18 | ### Expected behaviour 19 | 20 | 21 | ### Actual behaviour 22 | 23 | 24 | ### Steps to reproduce 25 | 26 | 27 | ### Screenshots 28 | 29 | 30 | ### Logs 31 | 32 | -------------------------------------------------------------------------------- /app/views/layouts/head.pug: -------------------------------------------------------------------------------- 1 | head 2 | meta(charset='utf-8') 3 | meta(name='viewport', content='width=device-width, initial-scale=1.0') 4 | meta(name='description', content='twitter written in node js') 5 | meta(name='author', content='Vinit Kumar') 6 | 7 | title Node Twitter 8 | 9 | link(href='/css/bootstrap/bootstrap.min.css', rel='stylesheet') 10 | link(href='/css/fontawesome-all.min.css', rel="stylesheet") 11 | link(href='/css/style.css', rel='stylesheet') 12 | // Favicon 13 | link(rel='shortcut icon', href='/img/bird.png') 14 | script(type='text/javascript', src='/js/jquery.min.js') 15 | // Bootstrap depedency 16 | script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.5/umd/popper.min.js') 17 | script(type='text/javascript', src='/js/bootstrap/bootstrap.min.js') 18 | script(type='text/javascript', src='/js/app.js') 19 | -------------------------------------------------------------------------------- /app/views/pages/login.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row 5 | .col-md-4.login-container 6 | h1 Node Twitter 7 | 8 | h3 9 | a(href="/auth/github", class="btn btn-primary btn-lg btn-block login-btn") 10 | span.login-using-github Login using github 11 | span(class="icon-container") 12 | i(class="fab fa-github fa-2x") 13 | 14 | .row.justify-content-center.text-center.stats-parent-container 15 | .col-md-3 16 | span.status-count #{tweetCount} 17 | h5 Tweets Created 18 | 19 | .col-md-3 20 | span.status-count #{userCount} 21 | h5 Unique Users 22 | 23 | .col-md-3 24 | span.status-count #{analyticsCount} 25 | h5 Visitors 26 | 27 | if (typeof errors !== 'undefined') 28 | .fade.in.alert.alert-block.alert-error 29 | a.close(data-dismiss="alert", href="javascript:void(0)") x 30 | ul 31 | each error in errors 32 | li = error.type 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /app/models/chat.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const ChatSchema = new Schema({ 5 | message: { type: String, default: "", trim: true, maxlength: 200 }, 6 | sender: { type: Schema.ObjectId, ref: "User" }, 7 | receiver: { type: Schema.ObjectId, ref: "User" }, 8 | createdAt: { type: Date, default: Date.now } 9 | }); 10 | 11 | ChatSchema.statics = { 12 | load: function(options, cb) { 13 | options.select = options.select || "message sender receiver createdAt"; 14 | return this.findOne(options.criteria) 15 | .select(options.select) 16 | .exec(cb); 17 | }, 18 | list: function(options) { 19 | const criteria = options.criteria || {}; 20 | return this.find(criteria) 21 | .populate("sender", "name username github") 22 | .populate("receiver", "name username github") 23 | .sort({ createdAt: -1 }) 24 | .limit(options.perPage) 25 | .skip(options.perPage * options.page); 26 | } 27 | }; 28 | 29 | mongoose.model("Chat", ChatSchema); 30 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | pull_request: 7 | branches: [ "develop" ] 8 | schedule: 9 | - cron: "15 12 * * 5" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /app/styles/pages/_messaging.scss: -------------------------------------------------------------------------------- 1 | // Messaging 2 | 3 | .chatbox { 4 | padding: 20px; 5 | } 6 | 7 | .chatMessage { 8 | display: inline; 9 | } 10 | 11 | span.sender { 12 | color: red; 13 | margin-left: 5px; 14 | margin-right: 5px; 15 | } 16 | 17 | span.message { 18 | width: fit-content; 19 | padding: 6px; 20 | } 21 | 22 | .msgtime { 23 | font-size: 10px; 24 | } 25 | 26 | .chatlist { 27 | margin-left: 20px; 28 | } 29 | 30 | .chat-image { 31 | margin-left: 5px; 32 | margin-right: 5px 33 | } 34 | 35 | .chatrow { 36 | // border: 1px solid lightslategray; 37 | margin-bottom: 10px; 38 | padding: 4px; 39 | border-radius: 7px; 40 | } 41 | 42 | .inboxlink { 43 | margin-left: 40px; 44 | } 45 | 46 | .message-row { 47 | margin-top: 6px; 48 | } 49 | 50 | .following-image, .follower-image { 51 | margin-right: 6px; 52 | } 53 | 54 | a.logout { 55 | color: red !important; 56 | } 57 | 58 | .btn-message { 59 | margin-right: 12px; 60 | } 61 | 62 | .chatgroup { 63 | margin-bottom: 20px; 64 | } 65 | 66 | .chat-bubble { 67 | margin-bottom: 10px; 68 | } -------------------------------------------------------------------------------- /app/views/components/comments.pug: -------------------------------------------------------------------------------- 1 | .col-12.tweet__comments 2 | if tweet.comments.length > 0 3 | hr 4 | each comment in tweet.comments 5 | if (comment && comment.user) 6 | if (comment.commenterPicture) 7 | img(class='tweets__comment-image', src=comment.commenterPicture) 8 | else 9 | img(class='tweets__comment-image', src='http://via.placeholder.com/20x20') 10 | span.tweet__username-date 11 | span.tweet__username 12 | if (comment.commenterName && comment.user) 13 | a(href="/users/" + comment.user)= comment.commenterName 14 | else if (comment.user) 15 | a(href="/users/" + comment.user)= comment.user 16 | span.tweet__date • #{moment(comment.createdAt).format("MMM D, YYYY [at] h:mm a")} 17 | p.tweet__content= comment.body 18 | hr 19 | form(class="tweet__comment-form" method="post", action="/tweets/"+ tweet._id + "/comments") 20 | textarea(type='text', name="body", placeholder='Enter the comment') 21 | button.btn(type='submit') Comment 22 | -------------------------------------------------------------------------------- /app/styles/components/_modals.scss: -------------------------------------------------------------------------------- 1 | // Any Tweet Modal 2 | 3 | .modal-dialog { 4 | margin-top: 60px; 5 | } 6 | 7 | .modal-content { 8 | background: none; 9 | } 10 | 11 | .modal-header { 12 | background-color: $dark-jungle-green; 13 | justify-content: center; 14 | border-bottom: 0; 15 | 16 | button.close { 17 | color: $grey-blue; 18 | opacity: 1; 19 | text-shadow: none; 20 | position: absolute; 21 | right: 15px; 22 | &:hover { 23 | cursor: pointer; 24 | } 25 | } 26 | } 27 | 28 | .modal-body { 29 | background-color: $yankees-blue; 30 | padding: 20px 15px; 31 | } 32 | 33 | .modal { 34 | padding: 0; 35 | } 36 | 37 | .new-tweet__button, 38 | .new-message__button { 39 | font-size: 14px; 40 | padding: 4px 10px; 41 | background-color: #1da1f2; 42 | color: #fff; 43 | margin: auto; 44 | margin-top: 10px; 45 | display: block; 46 | } 47 | 48 | .delete-confirmation__button{ 49 | font-size: 14px; 50 | padding: 4px 10px; 51 | background-color: firebrick; 52 | color: #fff; 53 | margin: auto; 54 | margin-top: 10px; 55 | display: block; } 56 | 57 | .delete-confirmation__button:hover { 58 | background-color: #ff0000;} -------------------------------------------------------------------------------- /app/views/pages/analytics.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .analytics 5 | h1 Analytics Dashboard 6 | .analytics__stats 7 | ul 8 | li 9 | span.analytics__stats-title Visits: 10 | span.analytics__stats-value #{pageViews} 11 | li 12 | span.analytics__stats-title Users: 13 | span.analytics__stats-value #{userCount} 14 | li 15 | span.analytics__stats-title Tweets: 16 | span.analytics__stats-value #{tweetCount} 17 | table.analytics__table 18 | thead 19 | tr 20 | th.analytics__user-column User 21 | th.analytics__url-column URL 22 | th.analytics__date-column Time 23 | 24 | tbody 25 | each analytic in analytics 26 | tr 27 | - if (analytic.user != null && analytic.url != null) 28 | - var name = analytic.user.name ? analytic.user.name : analytic.user.username 29 | td.analytics__user #{name} 30 | td.analytics__url #{analytic.url} 31 | td.analytics__date #{moment(analytic.createdAt).format("MMM D, YYYY [at] h:mm a")} 32 | include ../components/pagination 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/test_tweets.js: -------------------------------------------------------------------------------- 1 | // Tests 2 | 3 | var expect = require('chai').expect; 4 | 5 | describe('Test parse and extract hashtags function', function() { 6 | var tweets = require('../app/controllers/tweets.js'); 7 | 8 | it('Expected behaivor: extract and store hashtags', function(done) { 9 | var testCase = '#bobo, #, n#, #, # 1, f, #a'; 10 | var expectedResult = ['bobo','a']; 11 | 12 | var result = tweets.parseHashtag(testCase); 13 | 14 | expect(result).to.eql(expectedResult); 15 | done(); 16 | }); 17 | 18 | it('Expected behaivor: return empty array', function(done) { 19 | var testCase = 'python, go, ##'; 20 | 21 | var result = tweets.parseHashtag(testCase); 22 | 23 | expect(result).to.eql([]); 24 | done(); 25 | }); 26 | 27 | it('Expected behaivor: return empty array', function(done) { 28 | var testCase = ''; 29 | 30 | var result = tweets.parseHashtag(testCase); 31 | 32 | expect(result).to.eql([]); 33 | done(); 34 | }); 35 | 36 | it('Expected behaivor: return empty array', function(done) { 37 | var testCase = '## #'; 38 | 39 | var result = tweets.parseHashtag(testCase); 40 | 41 | expect(result).to.eql([]); 42 | done(); 43 | }); 44 | }); -------------------------------------------------------------------------------- /app/styles/pages/_analytics.scss: -------------------------------------------------------------------------------- 1 | /* Analytics Page */ 2 | 3 | @import '../abstracts/placeholders'; 4 | 5 | .analytics { 6 | @extend %block-content; 7 | padding-top: 30px; 8 | padding-bottom: 30px; 9 | 10 | h1 { 11 | color: #fff; 12 | text-align: center; 13 | } 14 | } 15 | 16 | .analytics__table { 17 | table-layout: fixed; 18 | width: 100%; 19 | 20 | th, 21 | td { 22 | border: 1px solid #fff; 23 | padding: .25rem .75rem; 24 | } 25 | 26 | th { 27 | font-weight: bold; 28 | color: #fff; 29 | text-align: center; 30 | } 31 | 32 | td { 33 | word-wrap: break-word; 34 | vertical-align: middle; 35 | } 36 | } 37 | 38 | .analytics__user-column { 39 | width: 25%; 40 | } 41 | 42 | .analytics__stats { 43 | margin: 20px 0; 44 | 45 | ul { 46 | display: flex; 47 | justify-content: space-evenly; 48 | font-weight: bold; 49 | 50 | li { 51 | text-align: center; 52 | } 53 | } 54 | } 55 | 56 | .analytics__stats-title { 57 | font-size: 16px; 58 | } 59 | 60 | .analytics__stats-value { 61 | font-size: 18px; 62 | color: #1da1f2; 63 | display: block; 64 | } 65 | 66 | .analytics__user { 67 | font-weight: bold; 68 | text-align: center; 69 | } 70 | -------------------------------------------------------------------------------- /config/config.local.js: -------------------------------------------------------------------------------- 1 | const path = require("path"), 2 | rootPath = path.normalize(__dirname + "/.."); 3 | 4 | module.exports = { 5 | development: { 6 | db: "mongodb://localhost/ntw2", 7 | root: rootPath, 8 | app: { 9 | name: "Node Twitter" 10 | }, 11 | github: { 12 | clientID: "c2e0f478634366e1289d", 13 | clientSecret: "0bfde82383deeb99b28d0f6a9eac001a0deb798a", 14 | callbackURL: "http://localhost:3000/auth/github/callback" 15 | } 16 | }, 17 | test: { 18 | db: "mongodb://localhost/noobjs_test21", 19 | root: rootPath, 20 | app: { 21 | name: "Nodejs Express Mongoose Demo" 22 | }, 23 | github: { 24 | clientID: "c2e0f478634366e1289d", 25 | clientSecret: "0bfde82383deeb99b28d0f6a9eac001a0deb798a", 26 | callbackURL: "http://localhost:3000/auth/github/callback" 27 | } 28 | }, 29 | production: { 30 | db: "mongodb://localhost/noobjs_prodd", 31 | root: rootPath, 32 | app: { 33 | name: "Nodejs Express Mongoose Demo" 34 | }, 35 | github: { 36 | clientID: "c2e0f478634366e1289d", 37 | clientSecret: "0bfde82383deeb99b28d0f6a9eac001a0deb798a", 38 | callbackURL: "http://localhost:3000/auth/github/callback" 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /app/views/components/tweet.pug: -------------------------------------------------------------------------------- 1 | .tweet(data-tweetId = tweet._id) 2 | .tweet(data-tweetId = tweet._id) 3 | .row 4 | .col-1 5 | img(class='tweet__image', src=tweet.user.github.avatar_url) 6 | .col-11.tweet__description 7 | span.tweet__username-date 8 | span.tweet__username 9 | -var name = tweet.user.name ? tweet.user.name : tweet.user.username 10 | a(href="/users/"+tweet.user._id) #{name} 11 | span.tweet__date 12 | a(data-toggle='modal', href="#tweet-modal-"+tweet._id) • #{moment(tweet.createdAt).format("MMM D, YYYY [at] h:mm a")} 13 | // The same replace method in app.js file 14 | // TODO: find solution to reuse the same code 15 | - 16 | var tweet_m = tweet.body 17 | tweet_m = tweet_m.replace(/#(\w+)/g, '#$1'); 18 | p.tweet__content!= tweet_m 19 | if (tweet.user._id == req.user.id) 20 | form.tweet__form(action="/tweets/"+tweet.id+"?_method=POST", method="post") 21 | a.btn.tweet__edit(href="/tweets/"+tweet._id, type="submit") Edit 22 | form.tweet__form(action="/tweets/"+tweet.id+"?_method=DELETE", method="post") 23 | button.btn.tweet__delete(type="submit") Delete 24 | 25 | include comments 26 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats mongoose errors into proper array 3 | * 4 | * @param {Array} errors 5 | * @return {Array} 6 | * @api public 7 | */ 8 | 9 | exports.errors = errors => { 10 | let keys = Object.keys(errors); 11 | const errs = []; 12 | 13 | // if there is no validation error, just display a generic error 14 | if (!keys) { 15 | return ['Oops! There was an error']; 16 | } 17 | 18 | keys.forEach(key => { 19 | errs.push(errors[key].message); 20 | }); 21 | 22 | return errs; 23 | }; 24 | 25 | /** 26 | * Index of object within an array 27 | * 28 | * @param {Array} arr 29 | * @param {Object} obj 30 | * @return {Number} 31 | * @api public 32 | */ 33 | 34 | /** 35 | * Find object in an array of objects that matches a condition 36 | * 37 | * @param {Array} arr 38 | * @param {Object} obj 39 | * @param {Function} cb - optional 40 | * @return {Object} 41 | * @api public 42 | */ 43 | 44 | exports.findByParam = (arr, obj, cb) => { 45 | let index = exports.indexof(arr, obj); 46 | if (~index && typeof cb === 'function') { 47 | return cb(undefined, arr[index]); 48 | } else if (~index && !cb) { 49 | return arr[index]; 50 | } else if (!~index && typeof cb === 'function') { 51 | return cb('not found'); 52 | } 53 | // else undefined is returned 54 | }; 55 | -------------------------------------------------------------------------------- /config/middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic require login routing middlewares 3 | */ 4 | 5 | exports.requiresLogin = (req, res, next) => { 6 | console.log('authenticated', req.isAuthenticated()); 7 | if (!req.isAuthenticated()) { 8 | return res.redirect('/login'); 9 | } 10 | next(); 11 | }; 12 | 13 | /** 14 | * User authorization routing middleware 15 | */ 16 | 17 | exports.user = { 18 | hasAuthorization: (req, res, next) => { 19 | if (req.profile.id !== req.user.id) { 20 | return res.redirect('/users'+req.profile.id); 21 | } 22 | next(); 23 | } 24 | }; 25 | 26 | exports.tweet = { 27 | hasAuthorization: (req, res, next) => { 28 | if (req.tweet.user.id !== req.user.id) { 29 | return res.redirect('/tweets'+req.tweet.id); 30 | } 31 | next(); 32 | } 33 | }; 34 | 35 | 36 | /** 37 | * Comment authorization routing middleware 38 | */ 39 | 40 | exports.comment = { 41 | hasAuthorization: (req, res, next) => { 42 | // if the current user is comment owner or article owner 43 | // give them authority to delete 44 | if (req.user.id === req.comment.user.id || req.user.id === req.article.user.id) { 45 | next(); 46 | } else { 47 | req.flash('info', 'You are not authorized'); 48 | res.redirect('/articles/' + req.article.id); 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /app/controllers/apiv1.js: -------------------------------------------------------------------------------- 1 | // ## Tweet Controller 2 | const mongoose = require("mongoose"); 3 | const Tweet = mongoose.model("Tweet"); 4 | const User = mongoose.model("User"); 5 | 6 | exports.tweetList = (req, res) => { 7 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 8 | const perPage = 15; 9 | const options = { 10 | perPage: perPage, 11 | page: page 12 | }; 13 | let tweets, count; 14 | Tweet.limitedList(options) 15 | .then(result => { 16 | tweets = result; 17 | return Tweet.countDocuments(); 18 | }) 19 | .then(result => { 20 | count = result; 21 | return res.send(tweets); 22 | }) 23 | .catch(error => { 24 | return res.render("pages/500", { errors: error.errors }); 25 | }); 26 | }; 27 | 28 | exports.usersList = (req, res) => { 29 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 30 | const perPage = 15; 31 | const options = { 32 | perPage: perPage, 33 | page: page 34 | }; 35 | let users, count; 36 | User.list(options) 37 | .then(result => { 38 | users = result; 39 | return User.countDocuments(); 40 | }) 41 | .then(result => { 42 | count = result; 43 | return res.send(users); 44 | }) 45 | .catch(error => { 46 | return res.render("pages/500", { errors: error.errors }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /app/controllers/follows.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const User = mongoose.model("User"); 3 | const Activity = mongoose.model("Activity"); 4 | const logger = require("../middlewares/logger"); 5 | 6 | exports.follow = (req, res) => { 7 | const user = req.user; 8 | const id = req.url.split("/")[2]; 9 | // push the current user in the follower list of the target user 10 | 11 | const currentId = user.id; 12 | 13 | User.findOne({ _id: id }, function(err, user) { 14 | if (user.followers.indexOf(currentId) === -1) { 15 | user.followers.push(currentId); 16 | } 17 | user.save(err => { 18 | if (err) { 19 | logger.error(err); 20 | } 21 | }); 22 | }); 23 | 24 | // Over here, we find the id of the user we want to follow 25 | // and add the user to the following list of the current 26 | // logged in user 27 | User.findOne({ _id: currentId }, function(err, user) { 28 | if (user.following.indexOf(id) === -1) { 29 | user.following.push(id); 30 | } 31 | user.save(err => { 32 | const activity = new Activity({ 33 | activityStream: "followed by", 34 | activityKey: user, 35 | sender: currentId, 36 | receiver: user 37 | }); 38 | 39 | activity.save(err => { 40 | if (err) { 41 | logger.error(err); 42 | res.render("pages/500"); 43 | } 44 | }); 45 | if (err) { 46 | res.status(400); 47 | } 48 | res.status(201).send({}); 49 | }); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /app/controllers/comments.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | const utils = require("../../lib/utils"); 3 | const mongoose = require("mongoose"); 4 | const Activity = mongoose.model("Activity"); 5 | const logger = require("../middlewares/logger"); 6 | 7 | exports.load = (req, res, next, id) => { 8 | const tweet = req.tweet; 9 | utils.findByParam(tweet.comments, { id: id }, (err, comment) => { 10 | if (err) { 11 | return next(err); 12 | } 13 | req.comment = comment; 14 | next(); 15 | }); 16 | }; 17 | 18 | // ### Create Comment 19 | exports.create = (req, res) => { 20 | const tweet = req.tweet; 21 | const user = req.user; 22 | 23 | if (!req.body.body) { 24 | return res.redirect("/"); 25 | } 26 | tweet.addComment(user, req.body, err => { 27 | if (err) { 28 | logger.error(err); 29 | return res.render("pages/500"); 30 | } 31 | const activity = new Activity({ 32 | activityStream: "added a comment", 33 | activityKey: tweet.id, 34 | sender: user, 35 | receiver: req.tweet.user 36 | }); 37 | logger.info(activity); 38 | activity.save(err => { 39 | if (err) { 40 | logger.error(err); 41 | return res.render("pages/500"); 42 | } 43 | }); 44 | res.redirect("/"); 45 | }); 46 | }; 47 | 48 | // ### Delete Comment 49 | exports.destroy = (req, res) => { 50 | // delete a comment here. 51 | const comment = req.comment; 52 | comment.remove(err => { 53 | if (err) { 54 | res.send(400); 55 | } 56 | res.send(200); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | 3 | const request = require('supertest'); 4 | const app = require('../server'); 5 | const assert = require('assert'); 6 | 7 | describe('Test Homepage', function (done) { 8 | it('should return 302', function (done) { 9 | request(app) 10 | .get('/') 11 | .expect(302, done); 12 | }); 13 | }); 14 | 15 | describe('Test Login', function (done) { 16 | it('should return 200', function (done) { 17 | request(app) 18 | .get('/login') 19 | .expect(200, done); 20 | }); 21 | }); 22 | 23 | describe('Test Users API', function (done) { 24 | it('should return 200', function (done) { 25 | request(app) 26 | .get('/apiv1/users') 27 | .expect(200) 28 | .expect('Content-Type', /json/) 29 | .end(function(err, res) { 30 | if (err) return done(err); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('Test logout', function (done) { 37 | it('logout should redirect because there is no active session', function (done) { 38 | request(app) 39 | .get('/logout') 40 | .expect(302) 41 | .end(function (err, res) { 42 | if (err) return done(err); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('Test Tweets API', function (done) { 49 | it('should return 200', function (done) { 50 | request(app) 51 | .get('/apiv1/tweets') 52 | .expect(200) 53 | .expect('Content-Type', /json/) 54 | .end(function(err, res) { 55 | if (err) return done(err); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /app/views/components/profile-card.pug: -------------------------------------------------------------------------------- 1 | - var modalId = user._id + 'ChatModal'; 2 | - var name = user.name ? user.name : user.username 3 | - var currentUserId = req.user._id.toString() 4 | - var userId = user._id.toString() 5 | - var profileURL = "https://github.com/" + user.github.login; 6 | 7 | .profile 8 | - if (user.github !== undefined) 9 | img(class="profile__image", src=user.github.avatar_url) 10 | .profile__user-info 11 | span.profile__handle 12 | - if (user.github !== undefined) 13 | a(href=profileURL) 14 | span 15 | i(class="fab fa-github") #{user.github.login} 16 | .profile__messaging-options 17 | - if (currentUserId !== userId) 18 | a.btn(data-toggle='modal', href='#'+ modalId) Message #{name} 19 | a.btn.profile__follow-button(href="javascript:void(0)", data-userid=user._id, data-logged=req.user.id,title="follow") Follow 20 | if (req.isAuthenticated()) 21 | include modals/new-message-modal 22 | .row 23 | .col-12.user-information__stats 24 | ul 25 | li 26 | span.user-information__stat-title Tweets: 27 | span 28 | a(href="/users/" + user._id) #{tweetCount} 29 | li 30 | span.user-information__stat-title Following: 31 | span 32 | a(href="/users/" + user._id + "/following") #{followingCount} 33 | li 34 | span.user-information__stat-title Followers: 35 | span 36 | a(href="/users/" + user._id + "/followers") #{followerCount} 37 | 38 | 39 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const rootPath = path.normalize(__dirname + "/.."); 3 | 4 | const envPath = process.env.ENVPATH || ".env"; 5 | const dotenv = require("dotenv"); 6 | // Path to the file where environment variables 7 | dotenv.config({path: envPath }); 8 | 9 | module.exports = { 10 | development: { 11 | db: process.env.DB, 12 | port: process.env.PORT, 13 | root: rootPath, 14 | app: { 15 | name: "Node Twitter" 16 | }, 17 | github: { 18 | // GITHUB_CLIENT_SECRET and GITHUB_CLIENT_ID should be defined in .env file 19 | // which is stored locally on your computer or those variables values 20 | // can be passed from Docker container 21 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 22 | clientID: process.env.GITHUB_CLIENT_ID, 23 | callbackURL: "http://localhost:3000/auth/github/callback" 24 | } 25 | }, 26 | test: { 27 | //db: process.env.DB, 28 | // Hack to allow tests run 29 | db: "mongodb://root:volvo76@ds039078.mongolab.com:39078/ntwitter", 30 | root: rootPath, 31 | app: { 32 | name: "Nodejs Express Mongoose Demo" 33 | }, 34 | github: { 35 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 36 | clientID: process.env.GITHUB_CLIENT_ID, 37 | callbackURL: "http://localhost:3000/auth/github/callback" 38 | } 39 | }, 40 | production: { 41 | db: process.env.DB, 42 | root: rootPath, 43 | app: { 44 | name: "Nodejs Express Mongoose Demo" 45 | }, 46 | github: { 47 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 48 | clientID: process.env.GITHUB_CLIENT_ID, 49 | callbackURL: "http://nitter.herokuapp.com/auth/github/callback" 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /app/styles/components/_tweets.scss: -------------------------------------------------------------------------------- 1 | /* Tweets */ 2 | 3 | @import '../abstracts/placeholders'; 4 | @import '../abstracts/variables'; 5 | 6 | .tweet { 7 | @extend %block-content; 8 | color: $white; 9 | } 10 | 11 | .tweet::after { 12 | content: ""; 13 | clear: both; 14 | display: block; 15 | } 16 | 17 | .tweet__image { 18 | border-radius: 50%; 19 | height: 40px; 20 | border: 1px solid $black; 21 | } 22 | 23 | .logout-image { 24 | height: 20px; 25 | } 26 | 27 | .tweet__description { 28 | padding-left: 25px; 29 | } 30 | 31 | .tweet__content { 32 | margin: 5px 0 10px; 33 | word-wrap: break-word; 34 | } 35 | 36 | .tweet__form { 37 | .tweet__edit { 38 | background-color: $jungle-green; 39 | 40 | &:hover { 41 | background-color: lighten($jungle-green, 5%); 42 | } 43 | } 44 | .tweet__delete { 45 | margin-left: 10px; 46 | background-color: $go-red; 47 | 48 | &:hover { 49 | background-color: lighten($go-red, 5%); 50 | } 51 | } 52 | } 53 | 54 | .tweet__username a { 55 | color: $white; 56 | font-weight: bold; 57 | } 58 | 59 | .tweet__date, 60 | .tweet__date a { 61 | color: $grey-blue; 62 | } 63 | 64 | .tweet textarea.edit-tweet { 65 | margin: 10px 0 15px; 66 | } 67 | 68 | .tweet textarea { 69 | font-size: 13px; 70 | background-color: $dark-jungle-green; 71 | border-color: $black; 72 | display: block; 73 | padding: 6px 12px; 74 | color: $white; 75 | border-radius: 4px; 76 | width: 100%; 77 | } 78 | 79 | .tweet__comments .btn { 80 | margin: auto; 81 | display: block; 82 | margin-top: 10px; 83 | } 84 | 85 | .tweets__comment-image { 86 | border-radius: 50%; 87 | height: 20px; 88 | border: 1px solid $black; 89 | margin-right: 6px; 90 | } 91 | 92 | .notification-icon { 93 | margin-right: 10px; 94 | } -------------------------------------------------------------------------------- /app/styles/base/_reset.scss: -------------------------------------------------------------------------------- 1 | /* General */ 2 | 3 | @import '../abstracts/variables'; 4 | 5 | body { 6 | font-size: 13px; 7 | min-height: 100vh; 8 | background-color: $dark-jungle-green; 9 | color: $grey-blue; 10 | } 11 | 12 | body > div.container { 13 | /* Calculate the minimum height by subtracting the header and footer height */ 14 | min-height: calc(100vh - 50px - 150px); 15 | padding-top: 30px; 16 | padding-bottom: 30px; 17 | } 18 | 19 | h4 { 20 | font-size: 1.2rem; 21 | } 22 | 23 | ul { 24 | list-style: none; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | a { 30 | color: $dodger-blue; 31 | 32 | &:hover { 33 | color: darken($dodger-blue, 10%); 34 | text-decoration: none; 35 | } 36 | } 37 | 38 | hr { 39 | border-color: $dark-jungle-green; 40 | } 41 | 42 | .btn { 43 | border-radius: 5px; 44 | font-size: 11px; 45 | padding: 3px 8px; 46 | color: $white; 47 | background-color: $dodger-blue; 48 | 49 | &:hover { 50 | background-color: lighten($dodger-blue, 5%); 51 | cursor: pointer; 52 | color: $white; 53 | } 54 | } 55 | 56 | .btn__delete{ 57 | background-color: firebrick; 58 | 59 | &:hover { 60 | background-color: #ff0000; 61 | cursor: pointer; 62 | color: $white; 63 | } 64 | } 65 | 66 | textarea { 67 | color: $grey-blue; 68 | padding: 5px 10px; 69 | width: 100%; 70 | background-color: $dark-jungle-green; 71 | border: solid 1px $black; 72 | border-radius: 8px; 73 | &::placeholder{ 74 | color: $grey-blue; 75 | } 76 | &:focus { 77 | color: $white; 78 | } 79 | } 80 | 81 | pre { 82 | white-space: pre-wrap; /* css-3 */ 83 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 84 | white-space: -pre-wrap; /* Opera 4-6 */ 85 | white-space: -o-pre-wrap; /* Opera 7 */ 86 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 87 | } 88 | 89 | pre code { 90 | color: black; 91 | } 92 | -------------------------------------------------------------------------------- /app/views/layouts/header.pug: -------------------------------------------------------------------------------- 1 | body 2 | header.navbar(role='banner') 3 | .container 4 | if (req.isAuthenticated()) 5 | .navbar__group.navbar__group_left 6 | ul.navbar__main-navigation 7 | li 8 | a(href="/" title="Home") 9 | i(class="fas fa-home", aria-hidden="true") 10 | span Home 11 | | 12 | li 13 | a(href="/users/"+req.user.id, title="Profile") 14 | i(class="fas fa-user", aria-hidden="true") 15 | span Profile 16 | | 17 | li 18 | a(href="/analytics/", title="analytics") 19 | i(class="fas fa-chart-line", aria-hidden="true") 20 | span Analytics 21 | li 22 | a(href='/chat/get/' + req.user._id, title="Inbox") 23 | i(class="far fa-envelope", aria-hidden="true") 24 | span Inbox 25 | li 26 | a(href="/chat/", title="chat") 27 | i(class="far fa-comments", aria-hidden="true") 28 | span Chat Users 29 | li 30 | a(href="/activities", title="activity") 31 | i(class="far fa-bell", aria-hidden="true") 32 | span.navbar__logo 33 | a(href='/') 34 | img(src='/img/bird.svg') 35 | if (req.isAuthenticated()) 36 | .navbar__group.navbar__group_right 37 | .navbar__new-tweet 38 | a.navbar__new-tweet-button.btn(data-toggle='modal', href='.new-tweet') 39 | span Tweet 40 | .navbar__profile 41 | a(href="/logout", title="logout", class="navbar__profile-logout") 42 | img(class='tweet__image chat-image logout-image', src=req.user.github.avatar_url) 43 | span Logout 44 | if (req.isAuthenticated()) 45 | include ../components/modals/new-tweet-modal 46 | -------------------------------------------------------------------------------- /config/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Analytics = mongoose.model('Analytics'); 3 | const logger = require('../../app/middlewares/logger'); 4 | 5 | 6 | exports.analytics = (req, res, next) => { 7 | // A lot of analytics is missed because users might have 8 | // malinformed IPs. Let's just get rid of the IP data altogether and log user irrepsective 9 | // of that. For backward compatiblity, we will just store a dummy IP for all future users. 10 | // This will also result in lesser code both in complexity and in line count. 11 | const url = req.protocol + '://' + req.get('host') + req.originalUrl; 12 | // cleanup IP to remove unwanted characters 13 | const cleanIp = '129.23.12.1'; 14 | Analytics.findOne({ user: req.user}).sort({ createdAt: -1 }).exec(function (err, analytics) { 15 | let date = new Date(); 16 | if (analytics !== null) { 17 | if (new Date(analytics.createdAt).getDate() !== date.getDate()) { 18 | if (req.get('host').split(':')[0] !== 'localhost') { 19 | const analytics = new Analytics({ 20 | ip: cleanIp, 21 | user: req.user, 22 | url: url 23 | }); 24 | analytics.save(err => { 25 | if (err) { 26 | logger.log(err); 27 | } 28 | }); 29 | } 30 | } else { 31 | logger.log('Not creating a new analytics entry on the same day'); 32 | } 33 | } else { 34 | // it means this user is a new user and do not have a analytics object yet 35 | if (req.get('host').split(':')[0] !== 'localhost') { 36 | const analytics = new Analytics({ 37 | ip: cleanIp, 38 | user: req.user, 39 | url: url 40 | }); 41 | analytics.save(err => { 42 | if (err) { 43 | logger.log(err); 44 | } 45 | }); 46 | } 47 | } 48 | }); 49 | next(); 50 | }; 51 | -------------------------------------------------------------------------------- /public/img/bird.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 11 | 12 | 14 | 15 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/views/chat/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row.twitter-container 5 | .col-xl-12.col-lg-12.first-column 6 | .logged-user 7 | .row 8 | .container 9 | h1 10 | != title 11 | p Click on the name/image to send a message. 12 | div.row.chatgroup 13 | each user in users 14 | div.col-md-3.chat-bubble 15 | - var name = user.name ? user.name : user.username 16 | - var id = user._id + 'ChatModal'; 17 | a(data-toggle='modal', href='#'+ id) 18 | img(class='tweet__image chat-image', src=user.github.avatar_url) 19 | span #{name} 20 | - var currentUserId = req.user._id.toString() 21 | - var userId = user._id.toString() 22 | 23 | if (req.isAuthenticated()) 24 | div(id=id, class='modal fade') 25 | .modal-dialog(role='document') 26 | .modal-content 27 | .modal-header 28 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 29 | h4.modal-title Send new message to #{name} 30 | .modal-body 31 | form(method='post', action='/chats') 32 | .control-group 33 | label.control-label(for='chat') 34 | .controls 35 | textarea.flat.form-control(type='text', name='body', placeholder='Enter your Message here', maxlength='200') 36 | input(type="hidden", name='receiver', value=user._id) 37 | br 38 | .form-actions 39 | button.btn(type='submit') Send Message 40 | include ../components/pagination 41 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cookieParser = require('cookie-parser'); 3 | const cookieSession = require('cookie-session'); 4 | const fs = require('fs'); 5 | const passport = require('passport'); 6 | const env = process.env.NODE_ENV || 'development'; 7 | const config = require('./config/config')[env]; 8 | const auth = require('./config/middlewares/authorization'); 9 | const mongoose = require('mongoose'); 10 | const app = express(); 11 | const port = process.env.PORT || 3000; 12 | const cookieParserKey = process.env.COOKIE_KEY || "super55"; 13 | const sessionKey1 = process.env.SESSIONKEYONE || "key1"; 14 | const sessionKey2 = process.env.SESSIONKEYTWO || "key2"; 15 | const promiseRetry = require('promise-retry'); 16 | 17 | app.use(cookieParser(cookieParserKey)); 18 | app.use(cookieSession({ 19 | name: 'session', 20 | keys: [sessionKey1, sessionKey2] 21 | })); 22 | 23 | const options = { 24 | useNewUrlParser: true, 25 | useUnifiedTopology: true, 26 | reconnectTries: 60, 27 | reconnectInterval: 1000, 28 | poolSize: 10, 29 | bufferMaxEntries: 0 // If not connected, return errors immediately rather than waiting for reconnect 30 | }; 31 | 32 | const promiseRetryOptions = { 33 | retries: options.reconnectTries, 34 | factor: 2, 35 | minTimeout: options.reconnectInterval, 36 | maxTimeout: 5000 37 | }; 38 | 39 | const connect = () => { 40 | return promiseRetry((retry, number) => { 41 | console.log(`MongoClient connecting to ${config.db} - retry number: ${number}`); 42 | return mongoose.connect(config.db, options).catch(retry) 43 | }, promiseRetryOptions); 44 | }; 45 | 46 | const models_path = __dirname+'/app/models'; 47 | fs.readdirSync(models_path).forEach(file => { 48 | require(models_path+'/'+file); 49 | }); 50 | 51 | require('./config/passport')(passport, config); 52 | require('./config/express')(app, config, passport); 53 | require('./config/routes')(app, passport, auth); 54 | 55 | app.listen(port); 56 | console.log('Express app started on port ' + port); 57 | 58 | connect(); 59 | module.exports = app; 60 | -------------------------------------------------------------------------------- /app/styles/pages/_home.scss: -------------------------------------------------------------------------------- 1 | /* Main Dashboard */ 2 | 3 | @import '../abstracts/variables'; 4 | @import '../abstracts/placeholders'; 5 | 6 | .user-information { 7 | @extend %block-content; 8 | } 9 | 10 | .user-information > .row { 11 | align-items: center; 12 | } 13 | 14 | .user-information__image { 15 | padding-right: 0; 16 | 17 | img { 18 | border-radius: 50%; 19 | width: 100px; 20 | max-width: 100%; 21 | float: right; 22 | } 23 | } 24 | 25 | .user-information__text { 26 | text-align: center; 27 | } 28 | 29 | .user-information h4, 30 | .useful-links h4, 31 | .recent-visits h4 { 32 | color: #fff; 33 | } 34 | 35 | .user-information__stats { 36 | ul { 37 | margin-top: 10px; 38 | text-align: center; 39 | display: flex; 40 | justify-content: space-around; 41 | font-weight: bold; 42 | width: 100%; 43 | } 44 | 45 | &_small { 46 | @media #{$large} { 47 | display: none; 48 | } 49 | } 50 | 51 | &_large { 52 | display: none; 53 | 54 | @media #{$large} { 55 | display: block; 56 | } 57 | } 58 | } 59 | 60 | .user-information__stat-title { 61 | display: block; 62 | } 63 | 64 | .user-information__stats span:not(.user-information__stat-title) { 65 | font-size: 18px; 66 | color: #1da1f2; 67 | } 68 | 69 | .useful-links a { 70 | font-weight: bold; 71 | } 72 | 73 | .useful-links, 74 | .recent-visits { 75 | @extend %block-content; 76 | text-align: center; 77 | } 78 | 79 | .first-column { 80 | .recent-visits, 81 | .useful-links { 82 | display: none; 83 | 84 | @media #{$large} { 85 | display: block; 86 | } 87 | } 88 | 89 | .recent-visits { 90 | @media #{$xlarge} { 91 | display: none; 92 | } 93 | } 94 | } 95 | 96 | .third-column { 97 | .recent-visits, 98 | .useful-links { 99 | @media #{$large} { 100 | display: none; 101 | } 102 | } 103 | .recent-visits { 104 | @media #{$xlarge} { 105 | display: block; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nwitter", 3 | "description": "A twitter clone written with Node.js, Express, and MongoDB", 4 | "version": "2.1.0", 5 | "repository": "https://github.com/vinitkumar/node-twitter", 6 | "private": false, 7 | "author": "Vinit Kumar (http://vinitkumar.me)", 8 | "scripts": { 9 | "start": "nodemon server.js", 10 | "test": "make test", 11 | "upgrade-interactive": "npm-check --update", 12 | "postinstall": "opencollective postinstall" 13 | }, 14 | "engines": { 15 | "node": "16.0.0", 16 | "npm": "7.10.0" 17 | }, 18 | "dependencies": { 19 | "async": "3.2.6", 20 | "bcrypt": "^5.1.1", 21 | "body-parser": "^1.20.3", 22 | "compression": "^1.7.1", 23 | "connect-flash": "latest", 24 | "connect-mongo": "latest", 25 | "cookie-parser": "^1.4.7", 26 | "cookie-session": "^1.3.3", 27 | "csurf": "^1.10.0", 28 | "dateformat": "^3.0.3", 29 | "dotenv": "^8.6.0", 30 | "errorhandler": "^1.5.0", 31 | "eslint": "^9.9.1", 32 | "express": "^4.21.2", 33 | "express-session": "^1.18.1", 34 | "forever": "latest", 35 | "method-override": "^3.0.0", 36 | "moment": "^2.30.1", 37 | "mongoose": "^8.9.5", 38 | "morgan": "^1.9.0", 39 | "opencollective": "^1.0.3", 40 | "passport": "^0.7.0", 41 | "passport-github": "latest", 42 | "passport-github2": "^0.1.9", 43 | "passport-google-oauth": "latest", 44 | "passport-local": "latest", 45 | "passport-twitter": "~1.0.2", 46 | "promise-retry": "^2.0.1", 47 | "pug": "^3.0.3", 48 | "raven": "^2.1.2", 49 | "serve-favicon": "^2.4.5", 50 | "underscore": "latest", 51 | "view-helpers": "latest", 52 | "winston": "^3.14.2" 53 | }, 54 | "devDependencies": { 55 | "ava": "latest", 56 | "braces": "^3.0.2", 57 | "chai": "^4.2.0", 58 | "grunt": "^1.3.0", 59 | "grunt-contrib-sass": "latest", 60 | "grunt-contrib-watch": "^1.1.0", 61 | "grunt-sass": "latest", 62 | "js-yaml": "^3.14.0", 63 | "mocha": "^10.2.0", 64 | "nodemon": "latest", 65 | "npm-check": "^5.9.2", 66 | "should": "latest", 67 | "supertest": "^6.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/views/pages/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | .row 5 | .col-xl-3.col-lg-4.first-column 6 | .user-information 7 | .row 8 | .col-4.user-information__image 9 | img(src=req.user.github.avatar_url) 10 | .col-8.user-information__text 11 | a(href="/users/" + req.user.id) 12 | h4 #{req.user.name} 13 | .user-information__stats.user-information__stats_small 14 | ul 15 | li 16 | span.user-information__stat-title Tweets: 17 | span 18 | a(href="/users/" + req.user.id) #{tweetCount} 19 | li 20 | span.user-information__stat-title Following: 21 | span 22 | a(href="/users/" + req.user._id + "/following") #{followingCount} 23 | li 24 | span.user-information__stat-title Followers: 25 | span 26 | a(href="/users/" + req.user._id + "/followers") #{followerCount} 27 | .col-md-12.user-information__stats.user-information__stats_large 28 | ul 29 | li 30 | span.user-information__stat-title Tweets: 31 | span 32 | a(href="/users/" + req.user.id) #{tweetCount} 33 | li 34 | span.user-information__stat-title Following: 35 | span 36 | a(href="/users/" + req.user._id + "/following") #{followingCount} 37 | li 38 | span.user-information__stat-title Followers: 39 | span 40 | a(href="/users/" + req.user._id + "/followers") #{followerCount} 41 | .useful-links 42 | include ../components/useful-links 43 | .recent-visits 44 | include ../components/recent-visits 45 | .col-xl-6.col-lg-8.second-column 46 | include ../components/tweets 47 | include ../components/pagination 48 | 49 | 50 | .col-xl-3.col-lg-4.third-column 51 | .recent-visits.col-xs-6.col-md-12 52 | include ../components/recent-visits 53 | .useful-links.col-xs-6.col-md-12 54 | include ../components/useful-links 55 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const LocalStrategy = require("passport-local").Strategy; 3 | const GitHubStrategy = require("passport-github").Strategy; 4 | const User = mongoose.model("User"); 5 | 6 | module.exports = (passport, config) => { 7 | // require('./initializer') 8 | 9 | // serialize sessions 10 | passport.serializeUser((user, done) => { 11 | done(null, user.id); 12 | }); 13 | 14 | passport.deserializeUser((id, done) => { 15 | User.findOne({ _id: id }, (err, user) => { 16 | done(err, user); 17 | }); 18 | }); 19 | 20 | // use local strategy 21 | passport.use( 22 | new LocalStrategy( 23 | { 24 | usernameField: "email", 25 | passwordField: "password" 26 | }, 27 | (email, password, done) => { 28 | User.findOne({ email: email }, (err, user) => { 29 | if (err) { 30 | return done(err); 31 | } 32 | if (!user) { 33 | return done(null, false, { message: "Unknown user" }); 34 | } 35 | if (!user.authenticate(password)) { 36 | return done(null, false, { message: "Invalid password" }); 37 | } 38 | return done(null, user); 39 | }); 40 | } 41 | ) 42 | ); 43 | 44 | // use github strategy 45 | passport.use( 46 | new GitHubStrategy( 47 | { 48 | clientID: config.github.clientID, 49 | clientSecret: config.github.clientSecret, 50 | callbackURL: config.github.callbackURL 51 | }, 52 | (accessToken, refreshToken, profile, done) => { 53 | const options = { 54 | criteria: { "github.id": parseInt(profile.id) } 55 | }; 56 | User.load(options, (err, user) => { 57 | if (!user) { 58 | user = new User({ 59 | name: profile.displayName, 60 | // email: profile.emails[0].value, 61 | username: profile.username, 62 | provider: "github", 63 | github: profile._json 64 | }); 65 | user.save(err => { 66 | if (err) console.log(err); 67 | return done(err, user); 68 | }); 69 | } else { 70 | User.findOne({ username: profile.username }, function(err, user) { 71 | user.github = profile._json; 72 | user.save(); 73 | return done(err, user); 74 | }); 75 | } 76 | }); 77 | } 78 | ) 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /app/controllers/chat.js: -------------------------------------------------------------------------------- 1 | const createPagination = require("./analytics").createPagination; 2 | const mongoose = require("mongoose"); 3 | const Activity = mongoose.model("Activity"); 4 | const Chat = mongoose.model("Chat"); 5 | const User = mongoose.model("User"); 6 | const logger = require("../middlewares/logger"); 7 | 8 | exports.chat = (req, res, next, id) => { 9 | Chat.load(id, (err, chat) => { 10 | if (err) { 11 | return next(err); 12 | } 13 | if (!chat) { 14 | return next(new Error("Failed to load tweet" + id)); 15 | } 16 | req.chat = chat; 17 | next(); 18 | }); 19 | }; 20 | 21 | exports.index = (req, res) => { 22 | // so basically this is going to be a list of all chats the user had till date. 23 | const page = (req.query.page > 0 ? req.query.page : 1) - 1; 24 | const perPage = 10; 25 | const options = { 26 | perPage: perPage, 27 | page: page, 28 | criteria: { github: { $exists: true } } 29 | }; 30 | let users, count, pagination; 31 | User.list(options) 32 | .then(result => { 33 | users = result; 34 | return User.countDocuments() 35 | }) 36 | .then(result => { 37 | count = result; 38 | pagination = createPagination(req, Math.ceil(result / perPage), page + 1); 39 | res.render("chat/index", { 40 | title: "Chat User List", 41 | users: users, 42 | page: page + 1, 43 | pagination: pagination, 44 | pages: Math.ceil(count / perPage) 45 | }); 46 | }) 47 | .catch(error => { 48 | return res.render("pages/500", { errors: error.errors }); 49 | }); 50 | }; 51 | 52 | exports.show = (req, res) => { 53 | res.send(req.chat); 54 | }; 55 | 56 | exports.getChat = (req, res) => { 57 | const options = { 58 | criteria: { receiver: req.params.userid } 59 | }; 60 | let chats; 61 | Chat.list(options).then(result => { 62 | chats = result; 63 | res.render("chat/chat", { chats: chats }); 64 | }); 65 | }; 66 | 67 | exports.create = (req, res) => { 68 | const chat = new Chat({ 69 | message: req.body.body, 70 | receiver: req.body.receiver, 71 | sender: req.user.id 72 | }); 73 | logger.info("chat instance", chat); 74 | chat.save(err => { 75 | const activity = new Activity({ 76 | activityStream: "sent a message to", 77 | activityKey: chat.id, 78 | receiver: req.body.receiver, 79 | sender: req.user.id 80 | }); 81 | activity.save(err => { 82 | if (err) { 83 | logger.error(err); 84 | res.render("pages/500"); 85 | } 86 | }); 87 | logger.error(err); 88 | if (!err) { 89 | res.redirect(req.header("Referrer")); 90 | } 91 | }); 92 | }; 93 | -------------------------------------------------------------------------------- /app/styles/layout/_navigation.scss: -------------------------------------------------------------------------------- 1 | /* Navbar */ 2 | 3 | @import '../abstracts/variables'; 4 | 5 | // Initially don't display these elements and have the media queries decide if they get displayed 6 | .navbar__main-navigation span, 7 | .navbar__logo { 8 | display: none; 9 | 10 | @media #{$large} { 11 | display: inline-block; 12 | text-align: center; 13 | } 14 | } 15 | 16 | .navbar { 17 | background-color: lighten($yankees-blue, 2.5%); 18 | height: 50px; 19 | margin: 0; 20 | padding: 0; 21 | font-size: 14px; 22 | border-bottom: 1px solid $black; 23 | 24 | .container { 25 | display: flex; 26 | align-items: center; 27 | height: 100%; 28 | 29 | > * { 30 | flex-grow: 1; 31 | } 32 | } 33 | 34 | a { 35 | color: $grey-blue; 36 | } 37 | } 38 | 39 | .navbar__group { 40 | height: 100%; 41 | 42 | &_right { 43 | display: flex; 44 | flex-direction: row; 45 | justify-content: flex-end; 46 | align-items: center; 47 | } 48 | } 49 | 50 | .navbar__main-navigation { 51 | display: flex; 52 | align-items: center; 53 | height: 100%; 54 | margin-right: auto; 55 | 56 | li { 57 | margin-right: 5px; 58 | display: flex; 59 | height: 100%; 60 | align-items: center; 61 | 62 | &:hover { 63 | color: $dodger-blue !important; 64 | } 65 | } 66 | 67 | a { 68 | display: flex; 69 | align-items: center; 70 | height: 100%; 71 | padding-left: 8px; 72 | padding-right: 8px; 73 | 74 | &:hover { 75 | color: $dodger-blue !important; 76 | border-bottom: 2px solid $dodger-blue; 77 | 78 | > * { 79 | margin-bottom: -2px; 80 | } 81 | } 82 | i { 83 | margin-right: 4px; 84 | } 85 | } 86 | 87 | .fa { 88 | font-size: 22px; 89 | display: inline-block; 90 | padding: 0; 91 | padding-right: 10px; 92 | padding-left: 10px; 93 | 94 | @media #{$large} { 95 | padding-left: 0; 96 | } 97 | } 98 | } 99 | 100 | .navbar__logo { 101 | position: absolute; 102 | left: 50%; 103 | margin-left: -13px; 104 | 105 | img { 106 | height: 26px; 107 | } 108 | } 109 | 110 | .navbar__profile { 111 | display: flex; 112 | align-items: center; 113 | margin-right: 20px; 114 | 115 | .navbar__profile-logout { 116 | display: flex; 117 | } 118 | 119 | .fa { 120 | font-size: 20px; 121 | padding-right: 10px; 122 | } 123 | } 124 | 125 | .navbar__new-tweet { 126 | text-align: right; 127 | 128 | .navbar__new-tweet-button{ 129 | color: $white; 130 | font-size: 14px; 131 | padding: 7px 14px; 132 | border-radius: 20px; 133 | 134 | &:hover { 135 | color: $white; 136 | } 137 | } 138 | } 139 | 140 | 141 | .navbar__notifications { 142 | color: red !important; 143 | } 144 | -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | const express = require("express"); 5 | const session = require("express-session"); 6 | const compression = require("compression"); 7 | // const favicon = require('serve-favicon'); 8 | const errorHandler = require("errorhandler"); 9 | const mongoStore = require("connect-mongo")(session); 10 | const flash = require("connect-flash"); 11 | const helpers = require("view-helpers"); 12 | const bodyParser = require("body-parser"); 13 | const methodOverride = require("method-override"); 14 | const cookieParser = require("cookie-parser"); 15 | 16 | const Raven = require("raven"); 17 | // Disable Raven console alerts 18 | Raven.disableConsoleAlerts(); 19 | 20 | const moment = require("moment"); 21 | const morgan = require("morgan"); 22 | 23 | module.exports = (app, config, passport) => { 24 | app.set("showStackError", true); 25 | app.locals.moment = moment; 26 | 27 | // use morgan for logging 28 | app.use( 29 | morgan("dev", { 30 | skip: function(req, res) { 31 | return res.statusCode < 400; 32 | }, 33 | stream: process.stderr 34 | }) 35 | ); 36 | 37 | // use morgan for logging 38 | app.use( 39 | morgan("dev", { 40 | skip: function(req, res) { 41 | return res.statusCode >= 400; 42 | }, 43 | stream: process.stdout 44 | }) 45 | ); 46 | // setup Sentry to get any crashes 47 | if (process.env.SENTRY_DSN !== null) { 48 | Raven.config(process.env.SENTRY_DSN).install(); 49 | app.use(Raven.requestHandler()); 50 | app.use(Raven.errorHandler()); 51 | } 52 | app.use( 53 | compression({ 54 | filter: function(req, res) { 55 | return /json|text|javascript|css/.test(res.getHeader("Content-Type")); 56 | }, 57 | level: 9 58 | }) 59 | ); 60 | // app.use(favicon()); 61 | app.use(express.static(config.root + "/public")); 62 | 63 | if (process.env.NODE_ENV === "development") { 64 | app.use(errorHandler()); 65 | app.locals.pretty = true; 66 | } 67 | 68 | app.set("views", config.root + "/app/views"); 69 | app.set("view engine", "pug"); 70 | 71 | app.use(helpers(config.app.name)); 72 | app.use(cookieParser()); 73 | app.use( 74 | bodyParser.urlencoded({ 75 | extended: true 76 | }) 77 | ); 78 | app.use(bodyParser.json()); 79 | app.use(methodOverride("_method")); 80 | app.use( 81 | session({ 82 | secret: process.env.SECRET, 83 | resave: false, 84 | saveUninitialized: false, 85 | store: new mongoStore({ 86 | url: config.db, 87 | collection: "sessions" 88 | }) 89 | }) 90 | ); 91 | 92 | app.use(flash()); 93 | app.use(passport.initialize()); 94 | app.use(passport.session()); 95 | app.disable('view cache'); 96 | app.use((err, req, res, next) => { 97 | if (err.message.indexOf("not found") !== -1) { 98 | return next(); 99 | } 100 | console.log(err.stack); 101 | 102 | res.status(500).render("pages/500", { error: err.stack }); 103 | }); 104 | }; 105 | -------------------------------------------------------------------------------- /public/css/pygments-manni.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | /*{ background: #f0f3f3; }*/ 3 | .c { color: #999; } /* Comment */ 4 | .err { color: #AA0000; background-color: #FFAAAA } /* Error */ 5 | .k { color: #006699; } /* Keyword */ 6 | .o { color: #555555 } /* Operator */ 7 | .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ 8 | .cp { color: #009999 } /* Comment.Preproc */ 9 | .c1 { color: #999; } /* Comment.Single */ 10 | .cs { color: #999; } /* Comment.Special */ 11 | .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ 12 | .ge { font-style: italic } /* Generic.Emph */ 13 | .gr { color: #FF0000 } /* Generic.Error */ 14 | .gh { color: #003300; } /* Generic.Heading */ 15 | .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ 16 | .go { color: #AAAAAA } /* Generic.Output */ 17 | .gp { color: #000099; } /* Generic.Prompt */ 18 | .gs { } /* Generic.Strong */ 19 | .gu { color: #003300; } /* Generic.Subheading */ 20 | .gt { color: #99CC66 } /* Generic.Traceback */ 21 | .kc { color: #006699; } /* Keyword.Constant */ 22 | .kd { color: #006699; } /* Keyword.Declaration */ 23 | .kn { color: #006699; } /* Keyword.Namespace */ 24 | .kp { color: #006699 } /* Keyword.Pseudo */ 25 | .kr { color: #006699; } /* Keyword.Reserved */ 26 | .kt { color: #007788; } /* Keyword.Type */ 27 | .m { color: #FF6600 } /* Literal.Number */ 28 | .s { color: #d44950 } /* Literal.String */ 29 | .na { color: #4f9fcf } /* Name.Attribute */ 30 | .nb { color: #336666 } /* Name.Builtin */ 31 | .nc { color: #00AA88; } /* Name.Class */ 32 | .no { color: #336600 } /* Name.Constant */ 33 | .nd { color: #9999FF } /* Name.Decorator */ 34 | .ni { color: #999999; } /* Name.Entity */ 35 | .ne { color: #CC0000; } /* Name.Exception */ 36 | .nf { color: #CC00FF } /* Name.Function */ 37 | .nl { color: #9999FF } /* Name.Label */ 38 | .nn { color: #00CCFF; } /* Name.Namespace */ 39 | .nt { color: #2f6f9f; } /* Name.Tag */ 40 | .nv { color: #003333 } /* Name.Variable */ 41 | .ow { color: #000000; } /* Operator.Word */ 42 | .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .mf { color: #FF6600 } /* Literal.Number.Float */ 44 | .mh { color: #FF6600 } /* Literal.Number.Hex */ 45 | .mi { color: #FF6600 } /* Literal.Number.Integer */ 46 | .mo { color: #FF6600 } /* Literal.Number.Oct */ 47 | .sb { color: #CC3300 } /* Literal.String.Backtick */ 48 | .sc { color: #CC3300 } /* Literal.String.Char */ 49 | .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ 50 | .s2 { color: #CC3300 } /* Literal.String.Double */ 51 | .se { color: #CC3300; } /* Literal.String.Escape */ 52 | .sh { color: #CC3300 } /* Literal.String.Heredoc */ 53 | .si { color: #AA0000 } /* Literal.String.Interpol */ 54 | .sx { color: #CC3300 } /* Literal.String.Other */ 55 | .sr { color: #33AAAA } /* Literal.String.Regex */ 56 | .s1 { color: #CC3300 } /* Literal.String.Single */ 57 | .ss { color: #FFCC33 } /* Literal.String.Symbol */ 58 | .bp { color: #336666 } /* Name.Builtin.Pseudo */ 59 | .vc { color: #003333 } /* Name.Variable.Class */ 60 | .vg { color: #003333 } /* Name.Variable.Global */ 61 | .vi { color: #003333 } /* Name.Variable.Instance */ 62 | .il { color: #FF6600 } /* Literal.Number.Integer.Long */ 63 | 64 | .css .o, 65 | .css .o + .nt, 66 | .css .nt + .nt { color: #999; } 67 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('.favorite').on('click', function(e) { 3 | const tweetID = $(e.currentTarget).data('tweetid'); 4 | const url = 'tweets/' + tweetID + '/favorites'; 5 | $.ajax({ 6 | type: 'POST', 7 | url: url, 8 | success: function(data) { 9 | console.log('send a favorite'); 10 | }, 11 | error: function(data) { 12 | console.log('not sent'); 13 | }, 14 | }); 15 | }); 16 | 17 | $('.profile__follow-button').on('click', function(e) { 18 | const userID = $(e.currentTarget).data('userid'); 19 | const url = '/users/' + userID + '/follow'; 20 | if ($(this).hasClass('following')) { 21 | $(this).text('Follow'); 22 | $(this).removeClass('following'); 23 | } else { 24 | $(this).text('Unfollow'); 25 | $(this).addClass('following'); 26 | } 27 | $.ajax({ 28 | type: 'POST', 29 | url: url, 30 | success: function(data) { 31 | console.log('Followed the user'); 32 | }, 33 | error: function(data) { 34 | console.log('not sent'); 35 | }, 36 | }); 37 | }); 38 | 39 | $('.tweet__edit').on('click', function(e) { 40 | e.preventDefault(); 41 | let $editButton = $(e.target); 42 | if ($editButton.hasClass('tweet__edit')) { 43 | // Change "edit" to "save" on the button 44 | $editButton 45 | .text('Save') 46 | .removeClass('tweet__edit') 47 | .addClass('tweet__save'); 48 | // Get the tweet content text 49 | let $originalTweet = $editButton 50 | .parent() 51 | .siblings('.tweet__content'); 52 | let tweetText = $originalTweet.text(); 53 | // Replace the tweet text element with a textarea element 54 | let $modifiedText = $('