├── .gitattributes ├── test.js ├── data └── profile_photos │ └── default_photo.jpg ├── routes ├── index.js ├── data.js ├── tweets.js ├── auth.js └── tweet.js ├── postgres_tables ├── package.json ├── app.js ├── helper.js ├── LICENSE ├── .gitignore ├── db └── index.js └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // getting current timestamp in seconds 2 | console.log(Math.round(new Date().getTime()/1000)) -------------------------------------------------------------------------------- /data/profile_photos/default_photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeanEncoded/Twedit-API/HEAD/data/profile_photos/default_photo.jpg -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | // routes 2 | const auth = require('./auth') 3 | const tweet = require('./tweet') 4 | const tweets = require('./tweets') 5 | const data = require('./data') 6 | 7 | module.exports = app => { 8 | app.use('/auth', auth), 9 | app.use('/tweet',tweet), 10 | app.use('/tweets',tweets), 11 | app.use('/data',data) 12 | } -------------------------------------------------------------------------------- /postgres_tables: -------------------------------------------------------------------------------- 1 | // users table 2 | CREATE TABLE users ( 3 | ID SERIAL PRIMARY KEY, 4 | name VARCHAR(30), 5 | username VARCHAR(30), 6 | password VARCHAR(250), 7 | access_token VARCHAR(50), 8 | verified BOOLEAN NOT NULL DEFAULT false 9 | ); 10 | 11 | // tweets table 12 | CREATE TABLE tweets ( 13 | ID SERIAL PRIMARY KEY, 14 | tweet_by INT, 15 | tweet_text VARCHAR(280), 16 | tweet_time INT, 17 | edited BOOLEAN NOT NULL DEFAULT false 18 | ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twedit-api", 3 | "version": "0.0.1", 4 | "description": "REST Api that works with the Twedit android app", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "nodemon app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "DeanEncoded", 11 | "license": "MIT", 12 | "dependencies": { 13 | "bcryptjs": "^2.4.3", 14 | "express": "^4.18.2", 15 | "express-promise-router": "^3.0.3", 16 | "pg": "^7.12.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const bodyParser = require('body-parser') 3 | const mountRoutes = require('./routes') 4 | const app = express() 5 | const port = 3000 6 | 7 | app.use(bodyParser.json()) 8 | app.use( 9 | bodyParser.urlencoded({ 10 | extended: true, 11 | }) 12 | ) 13 | 14 | app.get('/', (request, response) => { 15 | response.json({ 16 | info: 'Tweets, but editable' 17 | }) 18 | }) 19 | mountRoutes(app) 20 | 21 | app.listen(port, () => { 22 | console.log(`App running on port ${port}.`) 23 | }) -------------------------------------------------------------------------------- /helper.js: -------------------------------------------------------------------------------- 1 | const getCurrentTimestamp = Math.round(new Date().getTime()/1000) 2 | 3 | const generateToken = (length) => { 4 | // allowed characters in the generated token 5 | var a = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!$%^&*@?#[]<>".split(""); 6 | var b = []; 7 | for (var i=0; i { 17 | // send the profile photo 18 | // BUT WE SHOULD ALSO TRY DO SOMETHING ABOUT EXTENSIONS. WHAT IF A USER UPLOADS AN IMAGE WITH A DIFFERENT FILE EXTENSION? 19 | const id = req.params.id 20 | var photoPath = path.resolve(__dirname + "/../data/profile_photos/" + id + ".jpg") 21 | if (fs.existsSync(photoPath)) { 22 | res.sendFile(photoPath) 23 | }else{ 24 | res.status(404).send("No data") 25 | } 26 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dean Tanya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /routes/tweets.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); 2 | const helper = require('../helper') 3 | const Router = require('express-promise-router') 4 | const db = require('../db') 5 | 6 | // create a new express-promise-router 7 | // this has the same API as the normal express router except 8 | // it allows you to use async functions as route handlers 9 | const router = new Router() 10 | // export our router to be mounted by the parent application 11 | module.exports = router 12 | 13 | router.post('/all', async (req,res) => { 14 | // Get all tweets 15 | var r = {'success':true} 16 | const { 17 | id, 18 | access_token 19 | } = req.body 20 | 21 | const tokenValid = await db.validateAccessToken(id,access_token); 22 | if(tokenValid){ 23 | // the access token is valid. 24 | // this user is able to get tweets 25 | 26 | const { rows } = await db.query('SELECT tweets.id, tweets.tweet_by, tweets.tweet_text, tweets.tweet_time, tweets.edited, users.username,users.name FROM tweets, users WHERE users.id = tweets.tweet_by ORDER BY tweets.id DESC;') 27 | if(rows.length > 0){ 28 | // we have some tweets 29 | r.tweets = rows 30 | res.status(200).json(r) 31 | }else{ 32 | // no tweets 33 | 34 | r.success = false 35 | r.message = "No tweets available yet" 36 | res.status(500).json(r) 37 | } 38 | }else{ 39 | r.success = false 40 | r.message = "Invalid token" 41 | r.tokenStatus = "invalid" 42 | res.status(401).json(r) 43 | } 44 | }) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | const Pool = require('pg').Pool 2 | const helper = require('../helper') 3 | 4 | const pool = new Pool({ 5 | user: 'me', 6 | host: 'localhost', 7 | database: 'twedit', 8 | password: 'password', 9 | port: 5432, 10 | }) 11 | 12 | // access token validator 13 | async function validateAccessToken(id,token){ 14 | const { rows } = await pool.query("SELECT access_token FROM users WHERE ID = $1",[id]) 15 | const accessToken = rows[0]['access_token'] 16 | if(accessToken == token) return true 17 | else return false 18 | } 19 | 20 | // tweet ownership verification 21 | async function verifyTweetOwnership(user_id,tweet_id){ 22 | const { rows } = await pool.query("SELECT tweet_by FROM tweets WHERE ID = $1",[tweet_id]) 23 | const tweet_by = rows[0]['tweet_by'] 24 | if(tweet_by == user_id) return true 25 | else return false 26 | } 27 | 28 | // is a tweet editable or not? 29 | // a tweet is only editable in the first 60 seconds of it being posted. 30 | // the client should also have their own handle to this. 31 | async function tweetEditable(tweet_id){ 32 | const { rows } = await pool.query("SELECT tweet_time FROM tweets WHERE ID = $1",[tweet_id]) 33 | const tweet_time= rows[0]['tweet_time'] 34 | // get the current time now 35 | const time_now = helper.getCurrentTimestamp 36 | 37 | // remove 60 seconds from time_now... 38 | // if that value is less than tweet_time. Allow the edit. If not.. then don't 39 | if((time_now - 60) < tweet_time){ 40 | return true 41 | }else{ return false } 42 | 43 | } 44 | 45 | // verify if tweet exists (with ID)(might use UID's later) 46 | async function verifyTweetExistance(tweet_id){ 47 | const { rowCount } = await pool.query("SELECT ID FROM tweets WHERE ID = $1",[tweet_id]) 48 | if(rowCount) return true 49 | else return false 50 | } 51 | 52 | module.exports = { 53 | query: (text, params, callback) => { 54 | return pool.query(text, params, callback) 55 | }, 56 | validateAccessToken, 57 | verifyTweetOwnership, 58 | verifyTweetExistance, 59 | tweetEditable 60 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twedit-API 2 | An ExpressJS REST API for the Twitter clone, Twedit. 3 | 4 | You can find the repository for the Twedit app [here](https://github.com/DeanEncoded/Twedit). 5 | 6 | ## API ENDPOINTS 7 | #### data endpoints 8 | */data/:id/profilephoto* - returns a user's profile photo 9 | 10 | #### user authentication 11 | */auth/login* - logs in a user and returns their user data + access token 12 | FORM DATA: `username, password` 13 | 14 | */auth/signup* - creates a users account and returns their user data + access token 15 | FORM DATA: `name, username, password` 16 | 17 | ### tweet 18 | */tweet/new* - allows a user to post a tweet 19 | FORM DATA: `id, access_token, tweet_text` 20 | 21 | */tweet/edit* - allows a user to edit a tweet (Within the first 60 seconds of posting) 22 | FORM DATA: `id, access_token, tweet_id, tweet_text` 23 | 24 | */tweet/delete* - allows a user to delete a tweet 25 | FORM DATA: `id, access_token, tweet_id` 26 | 27 | ### tweets 28 | */tweets/all* - returns all tweets available in the tweets table 29 | FORM DATA: `id, access_token` 30 | 31 | ## Running Twedit-API yourself 32 | If you wish to run the Twedit in your local environment, here are the instructions. 33 | Make sure you have these prerequisites installed: 34 | - **NodeJS (with npm, express and nodemon)** 35 | - **PostgreSQL** 36 | 37 | ### Setup Postgres database 38 | You'll need to setup a postgres database for the API to use. 39 | Create a postgres database named *twedit* (Or anything else as long as you change it later in the database config) 40 | 41 | You'll need only two tables in your database - one for user information and the other for storing tweets. 42 | Here are the queries to easily create these tables (These queries are also available in the **postgres_tables** file in the repository): 43 | 44 | ``` 45 | // users table 46 | CREATE TABLE users ( 47 | ID SERIAL PRIMARY KEY, 48 | name VARCHAR(30), 49 | username VARCHAR(30), 50 | password VARCHAR(250), 51 | access_token VARCHAR(50), 52 | verified BOOLEAN NOT NULL DEFAULT false 53 | ); 54 | 55 | // tweets table 56 | CREATE TABLE tweets ( 57 | ID SERIAL PRIMARY KEY, 58 | tweet_by INT, 59 | tweet_text VARCHAR(280), 60 | tweet_time INT, 61 | edited BOOLEAN NOT NULL DEFAULT false 62 | ); 63 | ``` 64 | 65 | Once your database is up and running, clone the repository: 66 | ```console 67 | git clone https:\\github.com\DeanEncoded\Twedit-API.git 68 | ``` 69 | 70 | cd into the cloned directory and run `npm install` 71 | 72 | **NOTE** : Don't forget to input your postgres connection info in the file `db/index.js` 73 | 74 | Once all that's done, just run `npm start` and the api should be up and running at the chosen port (in our case port 3000) 75 | 76 | -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); 2 | const helper = require('../helper') 3 | const Router = require('express-promise-router') 4 | const db = require('../db') 5 | var fs = require('fs') // for handling data files such as photos 6 | var path = require('path'); // 7 | 8 | // create a new express-promise-router 9 | // this has the same API as the normal express router except 10 | // it allows you to use async functions as route handlers 11 | const router = new Router() 12 | // export our router to be mounted by the parent application 13 | module.exports = router 14 | 15 | router.post('/signup', async (req,res) => { 16 | var r = {'success':true} 17 | const { 18 | name, 19 | username, 20 | password 21 | } = req.body 22 | 23 | // checking whether the username exists 24 | const { rows } = await db.query('SELECT name FROM users WHERE username = $1',[username]) 25 | if(rows < 1){ 26 | // the username is free 27 | var pwdsalt = bcrypt.genSaltSync(10) 28 | var hashedpass = bcrypt.hashSync(password, pwdsalt) 29 | 30 | const accessToken = helper.generateToken(20) 31 | 32 | try{ 33 | const { rows } = await db.query('INSERT INTO users (name, username, password, access_token) VALUES ($1, $2, $3, $4) RETURNING *', [name, username, hashedpass, accessToken]) 34 | // if there is one row the user was created 35 | if(rows.length > 0){ 36 | // go ahead and send the user their data 37 | 38 | const userID = rows[0]["id"] 39 | 40 | // copy the default profile photo and set it as this users photo in data/profile_photos 41 | // this is not a good system of handling profile photos 42 | fs.createReadStream(path.resolve(__dirname + "/../data/profile_photos/default_photo.jpg")).pipe(fs.createWriteStream(path.resolve(__dirname + "/../data/profile_photos/" + userID + ".jpg"))) 43 | 44 | var userdata = rows[0] 45 | delete(userdata['password']) // removing password from userdata 46 | r.userdata = userdata 47 | res.status(200).json(r) 48 | } 49 | }catch(error){ 50 | console.log(error) 51 | req.status(500).send("Not looking too good. Couldn't create your account") 52 | } 53 | }else{ 54 | // username is already in use 55 | r.success = false 56 | r.message = "That username is taken" 57 | res.status(200).json(r) 58 | } 59 | }) 60 | 61 | router.post('/login', async (req,res) => { 62 | var r = {'success':true} 63 | const { 64 | username, 65 | password 66 | } = req.body 67 | 68 | async function updateAccessToken(token,username){ 69 | try{ 70 | const { rowCount } = await db.query('UPDATE users SET access_token = $1 WHERE username = $2 RETURNING ID;', [token,username]) 71 | console.log(rowCount) 72 | if (rowCount> 0) return true 73 | }catch(error){ 74 | console.log(error) 75 | return false 76 | } 77 | } 78 | 79 | db.query('SELECT * FROM users WHERE username = $1', [username], (error, results) => { 80 | if (error || results.rows == 0) { 81 | r.success = false 82 | if(results.rows == 0) r.message = "That user doesn't exist." 83 | else r.message = "Something went wrong back here. Can you try again?" 84 | res.status(200).json(r) // these responses can be handled quite better than this 85 | }else{ 86 | // the user does exist 87 | // check their password authenticity 88 | if(bcrypt.compareSync(password, results.rows[0]['password'])){ 89 | // the user can login 90 | // generate an access token, update it the users table entry and send it to them 91 | const accessToken = helper.generateToken(20) 92 | 93 | // updating the access token 94 | if(updateAccessToken(accessToken,username)){ 95 | // we're good 96 | var userdata = results.rows[0] 97 | userdata['access_token'] = accessToken // updating the token 98 | delete(userdata['password']) // removing password from userdata 99 | r.userdata = results.rows[0] 100 | res.status(200).json(r) 101 | }else{ 102 | // something went wrong 103 | r.success = false; 104 | r.message = "Something went wrong. Try again maybe?" 105 | res.status(500).json(r) 106 | } 107 | }else{ 108 | // password is incorrect 109 | r.success = false; 110 | r.message = "Password is incorrect." 111 | res.status(200).json(r) 112 | } 113 | } 114 | }) 115 | 116 | }) 117 | 118 | 119 | router.get('/test', async (req,res) => { 120 | try{ 121 | const { rows } = await db.query('SELECT * FROM users') 122 | console.log(rows) 123 | res.status(200).json(rows) 124 | }catch(error){ 125 | console.log(error) 126 | res.status(500).send("Hmmm...") 127 | } 128 | }) -------------------------------------------------------------------------------- /routes/tweet.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); 2 | const helper = require('../helper') 3 | const Router = require('express-promise-router') 4 | const db = require('../db') 5 | 6 | // create a new express-promise-router 7 | // this has the same API as the normal express router except 8 | // it allows you to use async functions as route handlers 9 | const router = new Router() 10 | // export our router to be mounted by the parent application 11 | module.exports = router 12 | 13 | router.post('/new', async (req,res) => { 14 | // let's create a new tweet 15 | var r = {'success':true} 16 | const { 17 | id, 18 | access_token, 19 | tweet_text 20 | } = req.body 21 | 22 | const tokenValid = await db.validateAccessToken(id,access_token); 23 | if(tokenValid){ 24 | // the access token is valid. let's send out the tweet 25 | 26 | // get current timestamp 27 | const time = helper.getCurrentTimestamp 28 | 29 | const { rows } = await db.query('INSERT INTO tweets (tweet_by,tweet_text,tweet_time) VALUES($1, $2, $3) RETURNING ID',[id,tweet_text,time]) 30 | if(rows.length > 0){ 31 | // tweet was sent. 32 | r.tweet_id = rows[0]['id'] 33 | res.status(201).json(r) 34 | }else{ 35 | r.success = false 36 | r.message = "Couldn't send tweet. Maybe try again" 37 | res.status(500).json(r) 38 | } 39 | }else{ 40 | r.success = false 41 | r.message = "Invalid token" 42 | r.tokenStatus = "invalid" 43 | res.status(401).json(r) 44 | } 45 | }) 46 | 47 | router.delete('/delete', async (req,res) => { 48 | // let's create a new tweet 49 | var r = {'success':true} 50 | const { 51 | id, 52 | access_token, 53 | tweet_id 54 | } = req.body 55 | 56 | // lets check if the tweet exists 57 | const tweetExists = await db.verifyTweetExistance(tweet_id) 58 | if(tweetExists){ 59 | const tokenValid = await db.validateAccessToken(id,access_token); 60 | const tweetValid = await db.verifyTweetOwnership(id,tweet_id) 61 | // also check if the tweet is their's... or else they'll be able to delete any tweet. 62 | if(tokenValid){ 63 | if(tweetValid){ 64 | // delete the tweet 65 | await db.query('DELETE FROM tweets WHERE ID = $1',[tweet_id],(error,results)=>{ 66 | if(error){ 67 | // we have a problem 68 | r.success = false 69 | r.message = "Something went wrong" 70 | res.status(500).json(r) 71 | }else{ 72 | // tweet deleted I guess? 73 | r.message = "Tweet deleted" 74 | res.status(200).json(r) 75 | } 76 | }) 77 | }else{ 78 | r.success = false 79 | r.message = "That's not your tweet buddy. Can't fool me" 80 | res.status(401).json(r) 81 | } 82 | 83 | }else{ 84 | r.success = false 85 | r.message = "Invalid token" 86 | r.tokenStatus = "invalid" 87 | res.status(401).json(r) 88 | } 89 | }else{ 90 | r.success = false 91 | r.message = "Tweet doesn't exist" 92 | res.status(404).json(r) 93 | } 94 | }) 95 | 96 | router.put('/edit', async (req,res) => { 97 | // editing a tweet 98 | var r = {'success':true} 99 | const { 100 | id, 101 | access_token, 102 | tweet_id, 103 | tweet_text 104 | } = req.body 105 | 106 | // lets check if the tweet exists.. 107 | const tweetExists = await db.verifyTweetExistance(tweet_id) 108 | if(tweetExists){ 109 | const tokenValid = await db.validateAccessToken(id,access_token); 110 | const tweetValid = await db.verifyTweetOwnership(id,tweet_id) 111 | const tweetEditable = await db.tweetEditable(tweet_id) 112 | 113 | // also check if the tweet is their's... or else they'll be able to edit any tweet. 114 | if(tokenValid){ 115 | if(tweetValid){ 116 | // edit the tweet... only if its editable 117 | if(tweetEditable){ 118 | await db.query('UPDATE tweets SET tweet_text = $1, edited = TRUE WHERE ID = $2',[tweet_text,tweet_id],(error,results)=>{ 119 | if(error){ 120 | // we have a problem 121 | r.success = false 122 | r.message = "Something went wrong" 123 | res.status(500).json(r) 124 | }else{ 125 | // tweet deleted I guess? 126 | r.message = "Tweet edited successfully!" 127 | res.status(200).json(r) 128 | } 129 | }) 130 | }else{ 131 | console.log("WHAT?") // sometimes you just get a little bit confused 132 | r.success = false 133 | r.message = "Tweet can't be edited" 134 | res.status(200).json(r) 135 | } 136 | }else{ 137 | r.success = false 138 | r.message = "That's not your tweet buddy. Can't fool me" 139 | res.status(200).json(r) 140 | } 141 | 142 | }else{ 143 | r.success = false 144 | r.message = "Invalid token" 145 | r.tokenStatus = "invalid" 146 | res.status(401).json(r) 147 | } 148 | }else{ 149 | r.success = false 150 | r.message = "Tweet doesn't exist" 151 | res.status(404).json(r) 152 | } 153 | }) --------------------------------------------------------------------------------