├── .gitignore ├── README.md ├── database ├── connect-database ├── create-database ├── destroy-database ├── scripts │ └── 001-create-db.sql └── src │ ├── database │ └── functions ├── db.js ├── package-lock.json ├── package.json └── src ├── app.js ├── controllers ├── authController.js ├── commentController.js ├── hashtagsController.js ├── postController.js ├── repostController.js ├── searchBarController.js └── usersController.js ├── middlewares ├── validateHashtagMiddleware.js ├── validateSchemaMiddleware.js └── validateTokenMiddleware.js ├── repositories ├── authRepository.js ├── commentRepository.js ├── hashtagRepository.js ├── postRepository.js ├── repostRepository.js ├── searchBarRepository.js └── userRepository.js ├── routes ├── authRouter.js ├── commentRouter.js ├── hashtagsRouter.js ├── index.js ├── postRouter.js ├── repostRouter.js ├── searchBarRouter.js └── userRouter.js ├── schemas ├── commentSchema.js ├── followSchema.js ├── postSchema.js ├── signinSchema.js └── signupSchema.js └── utils └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linkr ⛓️ 2 | ## Share and tag links! 3 | 4 | ### :computer: Tech used 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 | ## Overview 15 | This is the API with which the [Linktr App](https://github.com/blarth/linktr-react) interacts. 16 | 17 | ## :hammer_and_wrench: Installation 18 | ### Make sure you have the following tools installed before you begin: 19 |

20 | 21 | 22 | 23 | 24 |

25 |

Not needed but recommended: VSCode

26 | 27 | #### This API was designed to work with [Linktr App](https://github.com/blarth/linktr-react) 28 | 29 | Use a terminal interface such as bash or zsh, and enter the following: 30 | ```bash 31 | #download 32 | gh repo clone blarth/linktr-api 33 | 34 | #access the folder you downloaded it to 35 | cd linktr-api 36 | 37 | #install dependencies 38 | npm i 39 | ``` 40 | The app will run locally and you must configure a ```.env``` file with a port of your choosing. The default one is 3000. 41 | 42 | ## :gear:Running 43 | ```bash 44 | #you can run the server with 45 | npm run dev 46 | ``` 47 | In another terminal instance, run the db 48 | ```bash 49 | #you can start the db with 50 | psql 51 | ``` 52 | 53 | ## :scroll:Documentation 54 | 55 | ### All requests are HTTP based 56 | 57 | #### ```POST /signup, /signin, DELETE /signout``` 58 | 59 | Authenticating related routes. Send requests of the respective types with the following: 60 | 61 | To signup: 62 | 63 | { 64 | 65 | email: any email, 66 | password: no restrictions, it will be stored encrypted, 67 | username: name with which the user will be identified, 68 | image: link to an image file, it will be shown as the user's avatar, supported extensions are png, jpg, jpeg, jfif, gif 69 | 70 | } 71 | 72 | To signin: 73 | 74 | { 75 | 76 | email: email used to signup, 77 | password: password chosen upon signup 78 | 79 | } 80 | 81 | The server will respond with a token, to be used on every other route where authetication is required. Use in the headers of your requests as: 82 | 83 | { 84 | 85 | Authorization: Bearer 86 | 87 | } 88 | 89 | To signout, simply send the request with a header containing the Authorization key above. 90 | 91 | #### ```GET /users``` 92 | 93 | Send the request with auth header to get info about you, like profile picture and name. 94 | 95 | #### ```GET /timeline:offset?, /hashtags, POST /timeline``` 96 | 97 | Get timeline route with optional offset parameter, other routes without params. 98 | 99 | With the auth header, send the request to timeline to get the ten last posts from users you follow, including their reposts, most recent first, and hashtags, to get the trending hashtags, ten most used first. The optional parameter lets you offset messages to get starting from the n-th last message. To post a link, send request to timeline, as a logged user, with the following: 100 | 101 | { 102 | 103 | link: the link you want to share, 104 | postText: the text to give it context, you can post hashtags by using # before a word. 105 | 106 | } 107 | 108 | #### ```POST /users/follow``` 109 | 110 | To follow a user, send the request with the auth and a body with: 111 | 112 | { 113 | 114 | followedUserId: id of user you want to follow 115 | 116 | } 117 | 118 | #### ```GET /users/following``` 119 | 120 | With the auth header, get all users you're following. 121 | 122 | Resquests with params: 123 | 124 | #### ```GET /users/follow/:id``` 125 | 126 | The request is used to check if user with id <:ID> is your follower. 127 | 128 | #### ```PUT /posts/:id/:status``` 129 | 130 | Send a request with the post id and the current status it has, liked or not. The server will like the post or remove it, adding or removing your user from the list of people who liked it. 131 | 132 | #### ```GET /posts/hashtags/:name``` 133 | 134 | Get all posts containing the <:NAME> hashtag (without the #) 135 | 136 | #### ```GET /posts/user/:name``` 137 | 138 | Get all posts from user <:NAME> 139 | 140 | #### ```DELETE /deletepost/:id``` 141 | 142 | As a logged user and publisher of the post, send the request to delete it, with the id given. 143 | 144 | #### ```PATCH /posts/edit/:id``` 145 | 146 | As a logged user and publisher of the post, you can edit it by sending a request with it's id. Any hashtags removed will lose a score in the trending ranking, any added will gain a score, and remaining ones are unaltered. 147 | 148 | #### ```GET /likes/:id``` 149 | 150 | Get all likes from post with id <:ID> 151 | 152 | #### ```GET /users/:id``` 153 | 154 | Get all posts from user's id <:ID> 155 | 156 | #### ```GET and POST /comments/:id``` 157 | 158 | As a logged user, with the auth header setting, get the comments from post with <:ID>, info about commentator, such as if it's the original post's author or if it's followed by the logged user is also present. To post, send a request with the auth header shown above and a body with: 159 | 160 | { 161 | 162 | comment: your comment 163 | 164 | } 165 | 166 | #### ```GET and POST /repost/:id``` 167 | 168 | Send the post request with the auth header and the post's id to mark the post as reposted by you. You follower's will be able to see it if they don't follow the original poster and all comments of the original post are loaded. You'll also increase the repost count of that post. 169 | 170 | To check whether it has been reposted or not, send the get request. 171 | 172 | #### ```GET searchbar/:name``` 173 | 174 | Get all users with names starting with <:NAME>, as a logged user, the people you follow being shown up first. 175 | 176 | ### :man_technologist: Authors 177 |

Made with care by

178 | João Marcos Inocente 179 | 180 | [![Gmail Badge](https://img.shields.io/badge/-jminocente@gmail.com-c14438?style=flat&logo=Gmail&logoColor=white&link=mailto:jminocente@gmail.com)](mailto:jminocente@gmail.com) 181 | 182 | [![Linkedin Badge](https://img.shields.io/badge/-João-Inocente?style=flat&logo=Linkedin&logoColor=white&color=blue&link=https://www.linkedin.com/in/joão-marcos-inocente-pavão-899961142/)](https://www.linkedin.com/in/joão-marcos-inocente-pavão-899961142/) 183 | 184 | Felipe Ventura 185 | 186 | [![Gmail Badge](https://img.shields.io/badge/-fmagven93@gmail.com-c14438?style=flat&logo=Gmail&logoColor=white&link=mailto:fmagven93@gmail.com)](mailto:fmagven93@gmail.com) 187 | 188 | [![Linkedin Badge](https://img.shields.io/badge/-Felipe-Ventura?style=flat&logo=Linkedin&logoColor=white&color=blue&link=https://www.linkedin.com/in/fmagven/)](https://www.linkedin.com/in/fmagven/) 189 | 190 | Rayane Ventura 191 | 192 | [![Gmail Badge](https://img.shields.io/badge/-rayyventura@gmail.com-c14438?style=flat&logo=Gmail&logoColor=white&link=mailto:rayyventura@gmail.com)](mailto:rayyventura@gmail.com) 193 | 194 | [![Linkedin Badge](https://img.shields.io/badge/-Rayane-Ventura?style=flat&logo=Linkedin&logoColor=white&color=blue&link=http://www.linkedin.com/in/rayane-ventura27)](http://www.linkedin.com/in/rayane-ventura27/) 195 | 196 | Lucas Tadeu 197 | 198 | [![Gmail Badge](https://img.shields.io/badge/-lucastadeuvaz@gmail.com-c14438?style=flat&logo=Gmail&logoColor=white&link=mailto:lucastadeuvaz@gmail.com)](mailto:lucastadeuvaz@gmail.com) 199 | 200 | [![Linkedin Badge](https://img.shields.io/badge/-Lucas-Tadeu?style=flat&logo=Linkedin&logoColor=white&color=blue&link=https://www.linkedin.com/in/lucas-tadeu-vaz-90186b20b/)](https://www.linkedin.com/in/lucas-tadeu-vaz-90186b20b/) 201 | 202 |

Contact us anytime!

203 | -------------------------------------------------------------------------------- /database/connect-database: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source src/functions; 4 | 5 | DATABASE=$(cat src/database); 6 | 7 | echo "Checking postgres status..." && 8 | checkPostgres && 9 | 10 | enterPostgresCli $DATABASE; 11 | -------------------------------------------------------------------------------- /database/create-database: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source src/functions; 4 | 5 | DATABASE=$(cat src/database); 6 | 7 | echo "Checking postgres status..." && 8 | checkPostgres && 9 | 10 | echo "Creating database..." && 11 | createDatabase $DATABASE && 12 | 13 | echo "Running scripts..." && 14 | runScripts $DATABASE; 15 | -------------------------------------------------------------------------------- /database/destroy-database: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source src/functions; 4 | 5 | DATABASE=$(cat src/database); 6 | 7 | echo "Checking postgres status..." && 8 | checkPostgres && 9 | 10 | echo "Destroying database..." && 11 | destroyDatabase $DATABASE; 12 | -------------------------------------------------------------------------------- /database/scripts/001-create-db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "users" ( 2 | "id" SERIAL PRIMARY KEY, 3 | "name" TEXT NOT NULL, 4 | "email" TEXT NOT NULL UNIQUE, 5 | "password" TEXT NOT NULL, 6 | "image" TEXT NOT NULL 7 | ); 8 | 9 | CREATE TABLE "posts" ( 10 | "id" SERIAL PRIMARY KEY, 11 | "link" TEXT NOT NULL, 12 | "postText" TEXT, 13 | "userId" INTEGER NOT NULL REFERENCES "users"("id") 14 | ); 15 | 16 | CREATE TABLE "hashtags" ( 17 | "id" SERIAL PRIMARY KEY, 18 | "name" TEXT NOT NULL UNIQUE 19 | ); 20 | 21 | CREATE TABLE "hashtagsPosts" ( 22 | "id" SERIAL PRIMARY KEY, 23 | "hashtagId" INTEGER NOT NULL REFERENCES "hashtags"("id"), 24 | "postId" INTEGER NOT NULL REFERENCES "posts"("id") 25 | ); 26 | 27 | CREATE TABLE "likesPosts" ( 28 | "id" serial NOT NULL, 29 | "userId" INTEGER NOT NULL REFERENCES "users"("id"), 30 | "postId" INTEGER NOT NULL REFERENCES "posts"("id"), 31 | "like" BOOLEAN NOT NULL 32 | ); 33 | 34 | CREATE TABLE "sessions" ( 35 | "id" SERIAL PRIMARY KEY, 36 | "token" TEXT NOT NULL UNIQUE, 37 | "userId" INTEGER NOT NULL REFERENCES "users"("id") 38 | ); 39 | 40 | CREATE TABLE "metaData" ( 41 | "id" SERIAL PRIMARY KEY, 42 | "postId" INTEGER NOT NULL REFERENCES "posts"("id"), 43 | "url" TEXT NOT NULL, 44 | "title" TEXT, 45 | "description" TEXT NOT NULL, 46 | "image" TEXT NOT NULL 47 | ); 48 | 49 | CREATE TABLE "followers"( 50 | "id" SERIAL PRIMARY KEY, 51 | "userId" INTEGER NOT NULL REFERENCES "users"("id"), 52 | "followedByUserId" INTEGER NOT NULL REFERENCES "users"("id") 53 | ); 54 | 55 | CREATE TABLE "comments"( 56 | "id" SERIAL PRIMARY KEY, 57 | "postId" INTEGER NOT NULL REFERENCES "posts"("id"), 58 | "comment" TEXT NOT NULL, 59 | "userId" INTEGER NOT NULL REFERENCES "users"("id") 60 | ); 61 | 62 | CREATE TABLE "shares"( 63 | "id" SERIAL PRIMARY KEY, 64 | "postId" INTEGER NOT NULL REFERENCES "posts"("id"), 65 | "userId" INTEGER NOT NULL REFERENCES "users"("id"), 66 | "userName" TEXT NOT NULL 67 | ); -------------------------------------------------------------------------------- /database/src/database: -------------------------------------------------------------------------------- 1 | linktr 2 | -------------------------------------------------------------------------------- /database/src/functions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | checkPostgres () { 4 | if [[ $(id -u postgres 2> /dev/null) = "" ]]; then 5 | echo "It looks like postgres isnt installed!"; 6 | echo "Run: sudo apt update && sudo apt install postgresql postgresql-contrib"; 7 | exit; 8 | fi 9 | 10 | if [[ $(pgrep -u postgres -fa -- -D) = "" ]]; then 11 | echo "PostgreSQL isnt running, trying to restart..."; 12 | sudo service postgresql start; 13 | fi 14 | } 15 | 16 | createDatabase () { 17 | sudo su -c "psql -c \"CREATE DATABASE $1\";" postgres; 18 | } 19 | 20 | destroyDatabase () { 21 | sudo su -c "psql -c \"DROP DATABASE $1\";" postgres; 22 | } 23 | 24 | runScripts () { 25 | for f in scripts/*; do 26 | echo "Running script $f..."; 27 | sudo su -c "psql -d $1 -f $f" postgres; 28 | done 29 | } 30 | 31 | enterPostgresCli () { 32 | sudo su -c "psql -d $1" postgres; 33 | } 34 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | import pg from "pg"; 2 | import "dotenv/config"; 3 | 4 | const { Pool } = pg; 5 | 6 | const connection = new Pool({ 7 | connectionString: process.env.DATABASE_URL, 8 | ssl: process.env.NODE_ENV === "production" ?{ 9 | rejectUnauthorized: false, 10 | }:false 11 | }); 12 | 13 | export default connection; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linktr-api", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "nodemon src/app.js", 10 | "start": "node src/app.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcrypt": "^5.0.1", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.0.0", 19 | "express": "^4.17.3", 20 | "joi": "^17.6.0", 21 | "jsonwebtoken": "^8.5.1", 22 | "nodemon": "^2.0.15", 23 | "pg": "^8.7.3", 24 | "string-strip-html": "^9.1.7", 25 | "url-metadata": "^2.5.0", 26 | "uuid": "^8.3.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import cors from "cors"; 2 | import express, { json } from "express"; 3 | import router from "./routes/index.js"; 4 | import "dotenv/config"; 5 | 6 | const app = express(); 7 | 8 | app.use(cors()); 9 | app.use(json()); 10 | app.use(router); 11 | 12 | export default router; 13 | 14 | app.listen(process.env.PORT, () => { 15 | console.log("Server is listening on port " + process.env.PORT); 16 | }); 17 | -------------------------------------------------------------------------------- /src/controllers/authController.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | import { v4 as uuid } from "uuid"; 3 | import { 4 | verifyExistingUser, 5 | createSession, 6 | deleteSession, 7 | } from "../repositories/authRepository.js"; 8 | 9 | export async function signin(req, res) { 10 | const { email, password } = req.body; 11 | try { 12 | const { rows: users } = await verifyExistingUser(email); 13 | const [user] = users; 14 | if (!user) { 15 | return res.sendStatus(401); 16 | } 17 | 18 | if (bcrypt.compareSync(password, user.password)) { 19 | const token = uuid(); 20 | await createSession(token, user); 21 | return res.send({ token: token, user: user }); 22 | } 23 | 24 | res.sendStatus(401); 25 | } catch (error) { 26 | console.log(error); 27 | res.sendStatus(500); 28 | } 29 | } 30 | export async function signout(req, res) { 31 | const { user } = res.locals; 32 | 33 | try { 34 | await deleteSession(user); 35 | res.sendStatus(200); 36 | } catch (error) { 37 | console.log(error); 38 | res.sendStatus(500); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/controllers/commentController.js: -------------------------------------------------------------------------------- 1 | import { fetchComments, postComment } from "../repositories/commentRepository.js"; 2 | 3 | export async function getComments(req, res){ 4 | try{ 5 | const { user } = res.locals; 6 | const { id } = req.params; 7 | const { rows: comments } = await fetchComments(id, user.id) 8 | res.status(200).send(comments); 9 | }catch(error){ 10 | console.log(error); 11 | res.sendStatus(500); 12 | } 13 | } 14 | 15 | export async function postComments(req, res){ 16 | const {user} = res.locals 17 | const { comment } = req.body; 18 | const {id} = req.params; 19 | try{ 20 | await postComment(id, comment, user.id); 21 | res.sendStatus(201); 22 | }catch(error){ 23 | console.log(error); 24 | res.sendStatus(500); 25 | } 26 | } -------------------------------------------------------------------------------- /src/controllers/hashtagsController.js: -------------------------------------------------------------------------------- 1 | import { fetchTendingHashtags } from '../repositories/hashtagRepository.js'; 2 | 3 | export async function getTrendingHashtags(_, res){ 4 | try{ 5 | const { rows: hashtags } = await fetchTendingHashtags() 6 | res.status(200).send(hashtags) 7 | }catch(error){ 8 | console.log(error) 9 | res.sendStatus(500) 10 | } 11 | } -------------------------------------------------------------------------------- /src/controllers/postController.js: -------------------------------------------------------------------------------- 1 | import { 2 | createPost, 3 | getPosts, 4 | createMetaData, 5 | getLastPost, 6 | selectLikeRelation, 7 | updateLikeStatus, 8 | createLikeRelation, 9 | getPostsById, 10 | editPostText, 11 | verifyPostOwner, 12 | getPostsByHashtag, 13 | getLikes, 14 | deleteMetaData, 15 | deleteHashtagsPost, 16 | deleteLikesPost, 17 | deletePost, 18 | deleteShare, 19 | } from "../repositories/postRepository.js"; 20 | import { 21 | getExistingHashtags, 22 | insertHashtags, 23 | insertHashtagsLinksMiddleTable, 24 | getPreviousHashtags, 25 | deleteHashtagsFromMiddleTable, 26 | } from "../repositories/hashtagRepository.js"; 27 | 28 | import { getUserById } from "../repositories/userRepository.js"; 29 | 30 | export async function postLink(req, res) { 31 | const { link, postText } = req.body; 32 | const { user } = res.locals; 33 | const { regex } = res.locals; 34 | 35 | try { 36 | await createPost(link, postText, user.id); 37 | const { rows: lastPost } = await getLastPost(user.id); 38 | await createMetaData(lastPost); 39 | if (regex.length > 0) postHashtags(lastPost[0].id, res); 40 | else return res.sendStatus(201); 41 | } catch (error) { 42 | console.log(error); 43 | return res.sendStatus(500); 44 | } 45 | } 46 | 47 | async function postHashtags(postId, res) { 48 | try { 49 | const { regex } = res.locals; 50 | for (let i = 0; i < regex.length; i++) { 51 | if (i != regex.indexOf(regex[i])) { 52 | regex.splice(i, 1); 53 | i--; 54 | } 55 | } 56 | let str = "WHERE "; 57 | let firstTime = true; 58 | let arr = []; 59 | for (let i = 0; i < regex.length; i++) { 60 | if (firstTime) { 61 | arr.push(regex[0]); 62 | str += `name = $${arr.length}`; 63 | firstTime = false; 64 | } else { 65 | arr.push(regex[i]); 66 | str += ` OR name = $${arr.length}`; 67 | } 68 | } 69 | const { rows: existingHashtags } = await getExistingHashtags(str, arr); 70 | const hashtagsToAdd = [...regex]; 71 | for (let i = 0; i < existingHashtags.length; i++) { 72 | hashtagsToAdd.splice(hashtagsToAdd.indexOf(existingHashtags[i].name), 1); 73 | } 74 | 75 | if (hashtagsToAdd.length > 0) { 76 | str = "VALUES "; 77 | arr = []; 78 | for (let i = 0; i < hashtagsToAdd.length; i++) { 79 | arr.push(hashtagsToAdd[i]); 80 | if (i < hashtagsToAdd.length - 1) { 81 | str += `($${arr.length}), `; 82 | } else { 83 | str += `($${arr.length})`; 84 | } 85 | } 86 | const { rows: newHashtags } = await insertHashtags(str, arr); 87 | const allHashtags = [...existingHashtags, ...newHashtags]; 88 | postHashtagsLinks(postId, allHashtags, res); 89 | } else { 90 | postHashtagsLinks(postId, existingHashtags, res); 91 | } 92 | } catch (error) { 93 | console.log(error); 94 | res.sendStatus(500); 95 | } 96 | } 97 | 98 | async function postHashtagsLinks(postId, hashtags, res) { 99 | try { 100 | let str = "VALUES "; 101 | const arr = []; 102 | for (let i = 0; i < hashtags.length; i++) { 103 | arr.push(hashtags[i].id); 104 | if (i < hashtags.length - 1) { 105 | str += `($${arr.length}, `; 106 | arr.push(postId); 107 | str += `$${arr.length}), `; 108 | } else { 109 | str += `($${arr.length}, `; 110 | arr.push(postId); 111 | str += `$${arr.length})`; 112 | } 113 | } 114 | await insertHashtagsLinksMiddleTable(str, arr); 115 | res.sendStatus(201); 116 | } catch (error) { 117 | console.log(error); 118 | res.sendStatus(500); 119 | } 120 | } 121 | 122 | export async function posts(req, res) { 123 | const { user } = res.locals; 124 | const { offset } = req.params; 125 | const offsetString = `OFFSET ${offset}`; 126 | try { 127 | const result = await getPosts(user, offsetString); 128 | 129 | res.send( 130 | result.rows.map((row) => { 131 | const [ 132 | id, 133 | link, 134 | postText, 135 | userId, 136 | mtPostId, 137 | url, 138 | title, 139 | description, 140 | image, 141 | reposterName, 142 | reposterId, 143 | userName, 144 | userOwnerId, 145 | userImage, 146 | isLike, 147 | numberReposts, 148 | ] = row; 149 | 150 | return { 151 | id, 152 | link, 153 | postText, 154 | userId, 155 | reposterName, 156 | reposterId, 157 | metadata: { url, title, description, image }, 158 | userName, 159 | userImage, 160 | isLike: isLike, 161 | numberReposts, 162 | }; 163 | }) 164 | ); 165 | } catch (error) { 166 | console.log(error); 167 | res.sendStatus(500); 168 | } 169 | } 170 | export async function likePost(req, res) { 171 | let { id, status } = req.params; 172 | const { user } = res.locals; 173 | 174 | status == "true" ? (status = true) : (status = false); 175 | 176 | try { 177 | const { rows: likeRelations } = await selectLikeRelation(id, user); 178 | const [likeRelation] = likeRelations; 179 | if (!likeRelation) { 180 | createLikeRelation(id, user, status); 181 | } else { 182 | await updateLikeStatus(id, user, status); 183 | } 184 | 185 | res.sendStatus(200); 186 | } catch (error) { 187 | console.log(error); 188 | res.sendStatus(500); 189 | } 190 | } 191 | 192 | export async function postsById(req, res) { 193 | const { id } = req.params; 194 | const { user } = res.locals; 195 | try { 196 | const result = await getPostsById(user.id, id); 197 | const { rows: data } = await getUserById(id); 198 | const userData = data[0]; 199 | const answer = result.rows.map((row) => { 200 | const [ 201 | id, 202 | link, 203 | postText, 204 | userId, 205 | mtPostId, 206 | url, 207 | title, 208 | description, 209 | image, 210 | reposterName, 211 | reposterId, 212 | userName, 213 | userOwnerId, 214 | userImage, 215 | isLike, 216 | numberReposts, 217 | ] = row; 218 | 219 | return { 220 | id, 221 | link, 222 | postText, 223 | userId, 224 | reposterName, 225 | reposterId, 226 | metadata: { url, title, description, image }, 227 | userName, 228 | userImage, 229 | isLike: isLike, 230 | numberReposts, 231 | }; 232 | }); 233 | res.send({ answer, userData }); 234 | } catch (error) { 235 | console.log(error); 236 | res.sendStatus(500); 237 | } 238 | } 239 | 240 | export async function postsByHashtag(req, res) { 241 | const { user } = res.locals; 242 | let { name: hashtag } = req.params; 243 | hashtag.trim(); 244 | hashtag = `#${hashtag}`; 245 | try { 246 | const result = await getPostsByHashtag(hashtag, user); 247 | 248 | res.send( 249 | result.rows.map((row) => { 250 | const [ 251 | id, 252 | link, 253 | postText, 254 | userId, 255 | mtPostId, 256 | postId, 257 | url, 258 | title, 259 | description, 260 | image, 261 | userName, 262 | userImage, 263 | isLike, 264 | hashtagId, 265 | , 266 | numberReposts, 267 | ] = row; 268 | 269 | return { 270 | id, 271 | link, 272 | postText, 273 | userId, 274 | metadata: { url, title, description, image }, 275 | userName, 276 | userImage, 277 | isLike, 278 | numberReposts, 279 | }; 280 | }) 281 | ); 282 | } catch (error) { 283 | console.log(error); 284 | res.sendStatus(500); 285 | } 286 | } 287 | 288 | export async function getAllLikes(req, res) { 289 | const { id } = req.params; 290 | const { rows: peopleLikes } = await getLikes(id); 291 | res.send(peopleLikes); 292 | } 293 | 294 | export async function deletePosts(req, res) { 295 | const { id } = req.params; 296 | try { 297 | await deleteMetaData(id); 298 | await deleteHashtagsPost(id); 299 | await deleteLikesPost(id); 300 | await deleteShare(id); 301 | await deletePost(id); 302 | res.sendStatus(200); 303 | } catch (error) { 304 | console.log(error); 305 | res.sendStatus(500); 306 | } 307 | } 308 | 309 | export async function editPost(req, res) { 310 | try { 311 | const user = res.locals.user; 312 | const verified = await verifyPostOwner(user.id, req.params.id); 313 | if (verified.rowCount < 1) { 314 | return res.sendStatus(401); 315 | } 316 | const { regex } = res.locals; 317 | for (let i = 0; i < regex.length; i++) { 318 | if (i != regex.indexOf(regex[i])) { 319 | regex.splice(i, 1); 320 | i--; 321 | } 322 | } 323 | const { rows: lastHashtags } = await getPreviousHashtags(req.params.id); 324 | 325 | const hashtagsToAdd = [...regex]; 326 | for (let i = 0; i < lastHashtags.length; i++) { 327 | if (hashtagsToAdd.includes(lastHashtags[i].name)) { 328 | hashtagsToAdd.splice(hashtagsToAdd.indexOf(lastHashtags[i].name), 1); 329 | } 330 | } 331 | 332 | let str = "WHERE "; 333 | let firstTime = true; 334 | let arr = []; 335 | for (let i = 0; i < hashtagsToAdd.length; i++) { 336 | if (firstTime) { 337 | arr.push(hashtagsToAdd[0]); 338 | str += `name = $${arr.length}`; 339 | firstTime = false; 340 | } else { 341 | arr.push(hashtagsToAdd[i]); 342 | str += ` OR name = $${arr.length}`; 343 | } 344 | } 345 | 346 | if (hashtagsToAdd.length > 0) { 347 | const { rows: existingHashtags } = await getExistingHashtags(str, arr); 348 | 349 | const hashtagsToAddMiddleOnly = []; 350 | for (let i = 0; i < existingHashtags.length; i++) { 351 | hashtagsToAddMiddleOnly.push(existingHashtags[i].name); 352 | } 353 | 354 | const hashtagsToAddToDbAndMiddle = []; 355 | for (let i = 0; i < hashtagsToAdd.length; i++) { 356 | if (!hashtagsToAddMiddleOnly.includes(hashtagsToAdd[i])) { 357 | hashtagsToAddToDbAndMiddle.push(hashtagsToAdd[i]); 358 | } 359 | } 360 | 361 | let finalHashtags = [...existingHashtags]; 362 | if (hashtagsToAddToDbAndMiddle.length > 0) { 363 | str = "VALUES "; 364 | arr = []; 365 | for (let i = 0; i < hashtagsToAddToDbAndMiddle.length; i++) { 366 | arr.push(hashtagsToAddToDbAndMiddle[i]); 367 | if (i < hashtagsToAddToDbAndMiddle.length - 1) { 368 | str += `($${arr.length}), `; 369 | } else { 370 | str += `($${arr.length})`; 371 | } 372 | } 373 | 374 | const { rows: newHashtags } = await insertHashtags(str, arr); 375 | finalHashtags = [...finalHashtags, ...newHashtags]; 376 | } 377 | 378 | str = "VALUES "; 379 | arr = []; 380 | for (let i = 0; i < finalHashtags.length; i++) { 381 | arr.push(finalHashtags[i].id); 382 | if (i < finalHashtags.length - 1) { 383 | str += `($${arr.length}, `; 384 | arr.push(parseInt(req.params.id)); 385 | str += `$${arr.length}), `; 386 | } else { 387 | str += `($${arr.length}, `; 388 | arr.push(parseInt(req.params.id)); 389 | str += `$${arr.length})`; 390 | } 391 | } 392 | await insertHashtagsLinksMiddleTable(str, arr); 393 | } 394 | 395 | const hashtagsToRemove = []; 396 | for (let i = 0; i < lastHashtags.length; i++) { 397 | if (!regex.includes(lastHashtags[i].name)) { 398 | hashtagsToRemove.push(lastHashtags[i]); 399 | } 400 | } 401 | 402 | if (hashtagsToRemove.length > 0) { 403 | str = "("; 404 | arr = []; 405 | firstTime = true; 406 | for (let i = 0; i < hashtagsToRemove.length; i++) { 407 | if (i == hashtagsToRemove.length - 1) { 408 | arr.push(hashtagsToRemove[i].hashtagId); 409 | str += `$${arr.length})`; 410 | } else { 411 | arr.push(hashtagsToRemove[i].hashtagId); 412 | str += `$${arr.length}, `; 413 | } 414 | } 415 | await deleteHashtagsFromMiddleTable(str, arr, req.params.id); 416 | } 417 | 418 | await editPostText(req.body.postText, req.params.id); 419 | 420 | res.sendStatus(200); 421 | } catch (error) { 422 | console.log(error); 423 | res.sendStatus(500); 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/controllers/repostController.js: -------------------------------------------------------------------------------- 1 | 2 | import { createRepost, deleteRepost, verifyRepost } from "../repositories/repostRepository.js"; 3 | 4 | 5 | export async function createReposts(req, res){ 6 | const {user} = res.locals 7 | const {id} = req.params 8 | 9 | try { 10 | const {rows : [verifyAlreadyRepost]} = await verifyRepost(id, user.id) 11 | if(verifyAlreadyRepost){ 12 | await deleteRepost(id, user.id) 13 | return res.sendStatus(200) 14 | } 15 | await createRepost(id, user.id) 16 | res.sendStatus(201) 17 | } catch (error) { 18 | console.log(error); 19 | res.sendStatus(500); 20 | } 21 | 22 | } 23 | 24 | 25 | export async function verifyAlreadyRepost(req, res){ 26 | const {user} = res.locals 27 | const {id} = req.params 28 | 29 | try { 30 | const {rows : [verifyAlreadyRepost]} = await verifyRepost(id, user.id) 31 | if(verifyAlreadyRepost){ 32 | return res.send(true) 33 | 34 | } 35 | res.send(false) 36 | } catch (error) { 37 | console.log(error); 38 | res.sendStatus(500); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/controllers/searchBarController.js: -------------------------------------------------------------------------------- 1 | import { getUsersToSearchBar } from "../repositories/searchBarRepository.js"; 2 | 3 | export async function getSearchBar(req, res) { 4 | const { name } = req.params; 5 | const { user } = res.locals; 6 | try { 7 | const result = await getUsersToSearchBar(name, user.id); 8 | res.send(result.rows); 9 | return result.rows; 10 | } catch (error) { 11 | console.log(error); 12 | res.sendStatus(500); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/controllers/usersController.js: -------------------------------------------------------------------------------- 1 | import { verifyExistingUser } from "../repositories/authRepository.js"; 2 | import { 3 | addUser, 4 | followUserById, 5 | verifyFollower, 6 | unfollowUser, 7 | checkFollowing, 8 | } from "../repositories/userRepository.js"; 9 | import bcrypt from "bcrypt"; 10 | 11 | export async function createUser(req, res) { 12 | const user = req.body; 13 | try { 14 | const existingUsers = await verifyExistingUser(user.email); 15 | if (existingUsers.rowCount > 0) { 16 | return res.sendStatus(409); 17 | } 18 | 19 | const passwordHash = bcrypt.hashSync(user.password, 10); 20 | 21 | await addUser(user, passwordHash); 22 | res.sendStatus(201); 23 | } catch (error) { 24 | console.log(error); 25 | return res.sendStatus(500); 26 | } 27 | } 28 | 29 | export async function getUser(req, res) { 30 | const { user } = res.locals; 31 | 32 | res.send({ id: user.id, image: user.image, name: user.name }); 33 | } 34 | 35 | export async function followUser(req, res) { 36 | const { user } = res.locals; 37 | const { followedUserId } = req.body; 38 | try { 39 | const { rows: followers } = await verifyFollower(followedUserId, user.id); 40 | const follower = followers[0]; 41 | if (follower) { 42 | await unfollowUser(followedUserId, user.id); 43 | return res.sendStatus(200); 44 | } 45 | await followUserById(followedUserId, user.id); 46 | res.sendStatus(201); 47 | } catch (error) { 48 | console.log(error); 49 | res.sendStatus(500); 50 | } 51 | } 52 | 53 | export async function getFollower(req, res) { 54 | const { user } = res.locals; 55 | const { id } = req.params; 56 | 57 | try { 58 | const { rows: followers } = await verifyFollower(id, user.id); 59 | res.send(followers); 60 | } catch (error) { 61 | console.log(error); 62 | res.sendStatus(500); 63 | } 64 | } 65 | export async function checkFollowings(req, res) { 66 | const { user } = res.locals; 67 | 68 | try { 69 | const { rows: result } = await checkFollowing(user.id); 70 | res.send(result[0]); 71 | } catch (error) { 72 | console.log(error); 73 | res.sendStatus(500); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/middlewares/validateHashtagMiddleware.js: -------------------------------------------------------------------------------- 1 | export default function hashtagsRegex(req, res, next){ 2 | const {postText} = req.body 3 | 4 | const regexp = /#+[a-zA-Z0-9A-Za-zÀ-ÖØ-öø-ʸ(_)]{1,}/g; 5 | 6 | const array = [...postText.matchAll(regexp)]; 7 | 8 | for(let i = 0; i < array.length; i++){ 9 | array[i] = array[i][0] 10 | } 11 | 12 | res.locals.regex = array 13 | 14 | next() 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/middlewares/validateSchemaMiddleware.js: -------------------------------------------------------------------------------- 1 | export default function validateSchemaMiddleware(schema) { 2 | return (req, res, next) => { 3 | const validation = schema.validate(req.body); 4 | if (validation.error) { 5 | console.log(validation.error.message); 6 | return res.status(422).send(validation.error.message); 7 | } 8 | next(); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/middlewares/validateTokenMiddleware.js: -------------------------------------------------------------------------------- 1 | import { getSession, getUserSession } from "../repositories/authRepository.js"; 2 | 3 | export async function validateTokenMiddleware(req, res, next) { 4 | const authorization = req.headers.authorization; 5 | const token = authorization?.replace("Bearer ", ""); 6 | 7 | if (!token) { 8 | return res.sendStatus(401); 9 | } 10 | 11 | const { rows: sessions } = await getSession(token); 12 | const [session] = sessions; 13 | 14 | if (!session) { 15 | return res.sendStatus(401); 16 | } 17 | 18 | const { rows: users } = await getUserSession(session); 19 | const [user] = users; 20 | 21 | if (!user) { 22 | return res.sendStatus(401); 23 | } 24 | 25 | res.locals.user = user; 26 | next(); 27 | } 28 | -------------------------------------------------------------------------------- /src/repositories/authRepository.js: -------------------------------------------------------------------------------- 1 | import connection from "../../db.js"; 2 | 3 | export async function verifyExistingUser(email) { 4 | return connection.query("SELECT * FROM users WHERE email=$1", [email]); 5 | } 6 | 7 | export async function createSession(token, user) { 8 | return connection.query( 9 | 'INSERT INTO sessions (token, "userId") VALUES ($1, $2)', 10 | [token, user.id] 11 | ); 12 | } 13 | export async function deleteSession(user) { 14 | return connection.query('DELETE FROM sessions WHERE "userId"=$1', [user.id]); 15 | } 16 | 17 | export async function getSession(token) { 18 | return connection.query(`SELECT * FROM sessions WHERE token=$1`, [token]); 19 | } 20 | 21 | export async function getUserSession(session) { 22 | return connection.query(`SELECT * FROM users WHERE id=$1`, [session.userId]); 23 | } 24 | -------------------------------------------------------------------------------- /src/repositories/commentRepository.js: -------------------------------------------------------------------------------- 1 | import connection from "../../db.js"; 2 | 3 | export async function fetchComments(postId, userId){ 4 | return connection.query(` 5 | SELECT comments.*, users.name, users.image, followers."userId" as following 6 | FROM comments 7 | JOIN users ON comments."userId"=users.id 8 | LEFT JOIN followers ON followers."followedByUserId"=$2 AND followers."userId"=comments."userId" 9 | WHERE comments."postId"=$1 10 | ORDER BY comments.id DESC 11 | `, [postId, userId]) 12 | } 13 | 14 | // export async function getNotFollowers(name, user){ 15 | // return connection.query(` 16 | // SELECT u.id, u.name, u.image FROM users u 17 | // LEFT JOIN "followers" f ON f."userId" = u.id 18 | // WHERE name like $1 19 | // GROUP BY f."followedByUserId" = $2, u.id 20 | // `,[name, user]); 21 | // } 22 | 23 | export async function postComment(postId, comment, user){ 24 | return connection.query(` 25 | INSERT INTO 26 | "comments" ("postId", comment, "userId") 27 | VALUES ($1, $2, $3) 28 | `,[postId, comment, user]); 29 | } -------------------------------------------------------------------------------- /src/repositories/hashtagRepository.js: -------------------------------------------------------------------------------- 1 | import connection from "../../db.js"; 2 | 3 | export async function getExistingHashtags(str, arr){ 4 | return connection.query(` 5 | SELECT * FROM hashtags ${str} 6 | `,arr); 7 | 8 | } 9 | 10 | export async function insertHashtags(str, arr){ 11 | return connection.query(` 12 | INSERT INTO hashtags (name) 13 | ${str} 14 | RETURNING * 15 | `, arr) 16 | } 17 | 18 | export async function insertHashtagsLinksMiddleTable(str, arr){ 19 | return connection.query(` 20 | INSERT INTO "hashtagsPosts" ("hashtagId", "postId") 21 | ${str} 22 | `, arr) 23 | } 24 | 25 | export async function getPreviousHashtags(id){ 26 | return connection.query(` 27 | SELECT "hashtagId", name 28 | FROM "hashtagsPosts" 29 | JOIN hashtags ON "hashtagsPosts"."hashtagId"=hashtags.id 30 | WHERE "hashtagsPosts"."postId"=$1 31 | `,[id]) 32 | } 33 | 34 | export async function deleteHashtagsFromMiddleTable(str, arr, id){ 35 | return connection.query(` 36 | DELETE FROM "hashtagsPosts" 37 | WHERE "hashtagId" IN ${str} 38 | AND "postId" IN ($${arr.length + 1}) 39 | `, [...arr, id]) 40 | } 41 | 42 | export async function fetchTendingHashtags(){ 43 | return connection.query(` 44 | SELECT COUNT("hashtagId") as uses, name 45 | FROM "hashtagsPosts" 46 | JOIN hashtags ON "hashtagId"=hashtags.id 47 | GROUP BY name 48 | ORDER BY uses DESC 49 | LIMIT 10 50 | `) 51 | } -------------------------------------------------------------------------------- /src/repositories/postRepository.js: -------------------------------------------------------------------------------- 1 | import connection from "../../db.js"; 2 | import urlMetadata from "url-metadata"; 3 | 4 | export async function createPost(link, postText, id) { 5 | return connection.query( 6 | ` 7 | INSERT INTO 8 | posts (link, "postText", "userId") 9 | VALUES ($1, $2, $3) 10 | RETURNING * 11 | `, 12 | [link, postText, id] 13 | ); 14 | } 15 | 16 | export async function createMetaData([post]) { 17 | try { 18 | const metadata = await urlMetadata(post.link); 19 | 20 | return connection.query( 21 | ` 22 | INSERT INTO 23 | "metaData" ("postId", url, title, description, image) 24 | VALUES ($1, $2, $3, $4, $5) 25 | 26 | `, 27 | [ 28 | post.id, 29 | metadata.url, 30 | metadata.title, 31 | metadata.description, 32 | metadata.image, 33 | ] 34 | ); 35 | } catch (error) { 36 | console.log(error); 37 | } 38 | } 39 | 40 | export async function getLastPost(id) { 41 | return connection.query( 42 | `SELECT * 43 | FROM posts 44 | WHERE posts."userId"=$1 45 | ORDER BY posts.id DESC 46 | LIMIT 1`, 47 | [id] 48 | ); 49 | } 50 | //LEFT JOIN shares ON shares."postId"=posts.id 51 | 52 | export async function getPosts(user, offset) { 53 | return connection.query( 54 | { 55 | text: ` 56 | SELECT * FROM ( 57 | SELECT 58 | pt.*, 59 | mt."postId", mt.url, mt.title, mt.description, mt.image, 60 | null as userNameRepost, 61 | null as userIdRepost, 62 | upost.name as userNamePost, 63 | upost.id as userNamePostId, 64 | upost.image as "userImage", 65 | "likesPosts".like, 66 | ( select Count(*) from shares where shares."postId" = pt.id) as "numberReposts" 67 | from 68 | posts pt 69 | join "metaData" mt on 70 | pt.id = mt."postId" 71 | join users upost on 72 | upost.id = pt."userId" 73 | LEFT JOIN "likesPosts" ON pt.id = "likesPosts"."postId" AND "likesPosts"."userId"= $1 74 | where 75 | pt."userId" in ( 76 | select 77 | "userId" 78 | from 79 | followers f 80 | where 81 | f."followedByUserId" = $1 82 | ) 83 | UNION ALL 84 | SELECT 85 | pt.*, 86 | mt."postId", mt.url, mt.title, mt.description, mt.image, 87 | uRepost.name as userNameRepost, 88 | uRepost.id as userIdRepost, 89 | upost.name as userNamePost, 90 | upost.id as userNamePostId, 91 | upost.image as "userImage", 92 | "likesPosts".like, 93 | ( select Count(*) from shares where shares."postId" = s."postId") as "numberReposts" 94 | from 95 | shares s 96 | join posts pt on 97 | pt.id = s."postId" 98 | join users uRepost 99 | on 100 | uRepost.id = s."userId" 101 | join users upost 102 | on 103 | upost.id = pt."userId" 104 | join "metaData" mt on 105 | s."postId" = mt."postId" 106 | LEFT JOIN "likesPosts" ON s."postId" = "likesPosts"."postId" AND "likesPosts"."userId"= $1 107 | where 108 | s."userId" in ( 109 | select 110 | "userId" 111 | from 112 | followers f 113 | where 114 | f."followedByUserId" = $1 115 | ) 116 | ) "mainTable" 117 | ORDER BY "mainTable"."id" DESC 118 | LIMIT 10 119 | ${offset} 120 | 121 | `, 122 | rowMode: "array", 123 | }, 124 | [user.id] 125 | ); 126 | } 127 | 128 | export async function getLikes(postId) { 129 | return connection.query( 130 | `SELECT posts.id, "likesPosts".like, users.name 131 | FROM "likesPosts" 132 | LEFT JOIN users 133 | ON users.id="likesPosts"."userId" 134 | LEFT JOIN posts 135 | ON posts.id="likesPosts"."postId" 136 | WHERE "likesPosts".like='t' AND posts.id=$1 137 | GROUP BY users.name, posts.id, "likesPosts".like 138 | `, 139 | [postId] 140 | ); 141 | } 142 | 143 | export async function createLikeRelation(id, user, status) { 144 | return connection.query( 145 | ` 146 | INSERT INTO "likesPosts" ("userId","postId","like") VALUES ($1,$2,$3) 147 | `, 148 | [user.id, id, status] 149 | ); 150 | } 151 | 152 | export async function selectLikeRelation(id, user) { 153 | return connection.query( 154 | ` 155 | SELECT "likesPosts".*, posts.id FROM posts 156 | JOIN "likesPosts" 157 | ON posts.id="likesPosts"."postId" AND "likesPosts"."userId"=$1 158 | WHERE posts.id=$2`, 159 | [user.id, id] 160 | ); 161 | } 162 | 163 | export async function updateLikeStatus(id, user, status) { 164 | return connection.query( 165 | ` 166 | UPDATE "likesPosts" 167 | SET "like"=$1 168 | WHERE "likesPosts"."postId"=$2 AND "likesPosts"."userId"=$3 169 | `, 170 | [status, id, user.id] 171 | ); 172 | } 173 | 174 | export async function getPostsById(userId, id) { 175 | return connection.query( 176 | { 177 | text: ` SELECT * FROM ( 178 | SELECT 179 | pt.*, 180 | mt."postId", mt.url, mt.title, mt.description, mt.image, 181 | null as userNameRepost, 182 | null as userIdRepost, 183 | upost.name as userNamePost, 184 | upost.id as userNamePostId, 185 | upost.image as "userImage", 186 | "likesPosts".like, 187 | ( select Count(*) from shares where shares."postId" = pt.id) as "numberReposts" 188 | from 189 | posts pt 190 | join "metaData" mt on 191 | pt.id = mt."postId" 192 | join users upost on 193 | upost.id = pt."userId" 194 | LEFT JOIN "likesPosts" ON pt.id = "likesPosts"."postId" AND "likesPosts"."userId"= $1 195 | WHERE upost.id=$2 196 | UNION ALL 197 | SELECT 198 | pt.*, 199 | mt."postId", mt.url, mt.title, mt.description, mt.image, 200 | uRepost.name as userNameRepost, 201 | uRepost.id as userIdRepost, 202 | upost.name as userNamePost, 203 | upost.id as userNamePostId, 204 | upost.image as "userImage", 205 | "likesPosts".like, 206 | ( select Count(*) from shares where shares."postId" = s."postId") as "numberReposts" 207 | from 208 | shares s 209 | join posts pt on 210 | pt.id = s."postId" 211 | join users uRepost 212 | on 213 | uRepost.id = s."userId" 214 | join users upost 215 | on 216 | upost.id = pt."userId" 217 | join "metaData" mt on 218 | s."postId" = mt."postId" 219 | LEFT JOIN "likesPosts" ON s."postId" = "likesPosts"."postId" AND "likesPosts"."userId"= $1 220 | WHERE uRepost.id=$2 221 | ) "mainTable" 222 | ORDER BY "mainTable"."id" DESC 223 | `, 224 | rowMode: "array", 225 | }, 226 | [userId, id] 227 | ); 228 | } 229 | 230 | export async function getPostsByHashtag(hashtag, user) { 231 | return connection.query( 232 | { 233 | text: `SELECT posts.*, "metaData".*, users.name, users.image AS "userImage","likesPosts".like,hashtags.id,"hashtagsPosts".id, (SELECT Count(*) FROM shares WHERE shares."postId" = posts.id) AS "numberReposts" 234 | FROM posts 235 | JOIN "metaData" 236 | ON posts.id="metaData"."postId" 237 | JOIN users 238 | ON posts."userId"=users.id 239 | JOIN "hashtagsPosts" ON "hashtagsPosts"."postId"=posts.id 240 | JOIN hashtags ON hashtags.id="hashtagsPosts"."hashtagId" AND hashtags.name ILIKE $1 241 | LEFT JOIN "likesPosts" 242 | ON posts.id="likesPosts"."postId" and "likesPosts"."userId"=$2 243 | ORDER BY posts.id DESC 244 | LIMIT 20`, 245 | rowMode: "array", 246 | }, 247 | [hashtag, user.id] 248 | ); 249 | } 250 | 251 | export async function editPostText(txt, id) { 252 | return connection.query( 253 | ` 254 | UPDATE "posts" 255 | SET "postText" = $1 256 | WHERE "id" = $2 257 | `, 258 | [txt, id] 259 | ); 260 | } 261 | 262 | export async function verifyPostOwner(userId, postId) { 263 | return connection.query( 264 | ` 265 | SELECT * FROM "posts" 266 | WHERE "userId" = $1 267 | AND id = $2 268 | `, 269 | [userId, postId] 270 | ); 271 | } 272 | 273 | export async function deleteMetaData(id) { 274 | return connection.query( 275 | ` 276 | DELETE FROM "metaData" WHERE "postId" = $1 277 | `, 278 | [id] 279 | ); 280 | } 281 | 282 | export async function deleteHashtagsPost(id) { 283 | return connection.query( 284 | ` 285 | DELETE FROM "hashtagsPosts" WHERE "postId" = $1 286 | `, 287 | [id] 288 | ); 289 | } 290 | 291 | export async function deleteLikesPost(id) { 292 | return connection.query( 293 | ` 294 | DELETE FROM "likesPosts" WHERE "postId" = $1 295 | `, 296 | [id] 297 | ); 298 | } 299 | export async function deleteShare(id) { 300 | return connection.query( 301 | ` 302 | DELETE FROM shares WHERE "postId" = $1 303 | `, 304 | [id] 305 | ); 306 | } 307 | 308 | export async function deletePost(id) { 309 | return connection.query( 310 | ` 311 | DELETE FROM posts WHERE id = $1 312 | `, 313 | [id] 314 | ); 315 | } 316 | -------------------------------------------------------------------------------- /src/repositories/repostRepository.js: -------------------------------------------------------------------------------- 1 | import connection from "../../db.js"; 2 | 3 | 4 | export async function createRepost(postId, userId) { 5 | return connection.query( 6 | ` 7 | INSERT INTO 8 | shares ("postId", "userId", "userName") 9 | VALUES ($1, $2, (SELECT users.name from users where users.id=$2)) 10 | `, 11 | [postId, userId] 12 | ); 13 | } 14 | 15 | export async function verifyRepost(postId, userId){ 16 | return connection.query( 17 | ` 18 | SELECT * 19 | FROM 20 | shares 21 | WHERE "postId"=$1 AND "userId"=$2 22 | `, 23 | [postId, userId] 24 | ); 25 | } 26 | export async function deleteRepost(postId, userId){ 27 | return connection.query( 28 | ` 29 | DELETE 30 | FROM shares 31 | WHERE "postId"=$1 AND "userId"=$2 32 | `, 33 | [postId, userId] 34 | ); 35 | } 36 | 37 | 38 | 39 | export async function numberReposts([postId]){ 40 | return connection.query(` 41 | SELECT COUNT(*) AS "numberReposts", 42 | FROM shares 43 | WHERE shares."postId"=$1 44 | GROUP BY "numberReposts" 45 | `,[postId]) 46 | } -------------------------------------------------------------------------------- /src/repositories/searchBarRepository.js: -------------------------------------------------------------------------------- 1 | import connection from "../../db.js"; 2 | 3 | export async function getUsersToSearchBar(name, user) { 4 | return connection.query( 5 | ` 6 | 7 | SELECT u.id, u.name, u.image, f."followedByUserId" AS "followed" FROM users u 8 | LEFT JOIN "followers" f ON f."userId" = u.id AND f."followedByUserId" = $2 9 | WHERE name ilike $1 10 | ORDER BY "followed" 11 | `, 12 | [`${name}%`, user] 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/repositories/userRepository.js: -------------------------------------------------------------------------------- 1 | import connection from "../../db.js"; 2 | 3 | export async function addUser(user, passwordHash) { 4 | return connection.query( 5 | ` 6 | INSERT INTO 7 | users (name, email, password ,image) 8 | VALUES ($1, $2, $3,$4) 9 | `, 10 | [user.username, user.email, passwordHash, user.image] 11 | ); 12 | } 13 | 14 | export async function followUserById(idFollowed, idFollower) { 15 | return connection.query( 16 | ` 17 | INSERT INTO followers("userId","followedByUserId") VALUES($1,$2) 18 | `, 19 | [idFollowed, idFollower] 20 | ); 21 | } 22 | export async function unfollowUser(idFollowed, idFollower) { 23 | return connection.query( 24 | ` 25 | DELETE FROM followers WHERE "userId"=$1 AND "followedByUserId"=$2 26 | `, 27 | [idFollowed, idFollower] 28 | ); 29 | } 30 | 31 | export async function verifyFollower(idFollowed, idFollower) { 32 | return connection.query( 33 | ` 34 | SELECT id FROM followers WHERE "userId"=$1 AND "followedByUserId"=$2 35 | `, 36 | [idFollowed, idFollower] 37 | ); 38 | } 39 | 40 | export async function checkFollowing(id) { 41 | return connection.query( 42 | `SELECT id FROM followers WHERE "followedByUserId"=$1`, 43 | [id] 44 | ); 45 | } 46 | export async function getUserById(id) { 47 | return connection.query( 48 | `SELECT id, name AS "userName", users.image AS "userImage" FROM users WHERE id=$1`, 49 | [id] 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/routes/authRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { signin, signout } from "../controllers/authController.js"; 3 | import validateSchemaMiddleware from "../middlewares/validateSchemaMiddleware.js"; 4 | import { validateTokenMiddleware } from "../middlewares/validateTokenMiddleware.js"; 5 | import signinSchema from "../schemas/signinSchema.js"; 6 | 7 | const authRouter = Router(); 8 | authRouter.post("/signin", validateSchemaMiddleware(signinSchema), signin); 9 | authRouter.delete("/signout", validateTokenMiddleware, signout); 10 | export default authRouter; 11 | -------------------------------------------------------------------------------- /src/routes/commentRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { getComments, postComments } from "../controllers/commentController.js"; 3 | import { validateTokenMiddleware } from "../middlewares/validateTokenMiddleware.js" 4 | import validateSchemaMiddleware from "../middlewares/validateSchemaMiddleware.js"; 5 | import commentSchema from "../schemas/commentSchema.js"; 6 | const commentRouter = Router(); 7 | 8 | commentRouter.get("/comments/:id", validateTokenMiddleware, getComments); 9 | commentRouter.post("/comments/:id", validateTokenMiddleware,validateSchemaMiddleware(commentSchema),postComments); 10 | 11 | export default commentRouter; -------------------------------------------------------------------------------- /src/routes/hashtagsRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { getTrendingHashtags } from "../controllers/hashtagsController.js"; 3 | 4 | const hashtagsRouter = Router(); 5 | 6 | hashtagsRouter.get("/hashtags", getTrendingHashtags); 7 | 8 | export default hashtagsRouter; 9 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import authRouter from "./authRouter.js"; 3 | import userRouter from "./userRouter.js"; 4 | import postRouter from "./postRouter.js"; 5 | import hashtagsRouter from "./hashtagsRouter.js"; 6 | import searchBarRouter from "./searchBarRouter.js"; 7 | import commentRouter from "./commentRouter.js"; 8 | import repostRouter from "./repostRouter.js"; 9 | 10 | const router = Router(); 11 | router.use(authRouter); 12 | router.use(userRouter); 13 | router.use(postRouter); 14 | router.use(hashtagsRouter); 15 | router.use(searchBarRouter); 16 | router.use(commentRouter); 17 | router.use(repostRouter) 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /src/routes/postRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { 3 | getAllLikes, 4 | likePost, 5 | postLink, 6 | posts, 7 | postsByHashtag, 8 | postsById, 9 | editPost, 10 | deletePosts 11 | } from "../controllers/postController.js"; 12 | import postSchema from "../schemas/postSchema.js"; 13 | import { validateTokenMiddleware } from "../middlewares/validateTokenMiddleware.js"; 14 | import validateSchemaMiddleware from "../middlewares/validateSchemaMiddleware.js"; 15 | import hashtagsRegex from "../middlewares/validateHashtagMiddleware.js"; 16 | 17 | 18 | const postRouter = Router(); 19 | 20 | postRouter.get("/timeline/:offset?", validateTokenMiddleware, posts); 21 | 22 | postRouter.post( 23 | "/timeline", 24 | validateTokenMiddleware, 25 | validateSchemaMiddleware(postSchema), 26 | hashtagsRegex, 27 | postLink 28 | ); 29 | 30 | postRouter.put("/posts/:id/:status", validateTokenMiddleware, likePost); 31 | postRouter.get( 32 | "/posts/hashtags/:name", 33 | validateTokenMiddleware, 34 | postsByHashtag 35 | ); 36 | postRouter.get("/user/:id", validateTokenMiddleware, postsById); 37 | postRouter.delete("/deletepost/:id", validateTokenMiddleware, deletePosts); 38 | 39 | 40 | postRouter.patch("/posts/edit/:id", validateTokenMiddleware, validateSchemaMiddleware(postSchema), hashtagsRegex, editPost) 41 | postRouter.get("/likes/:id", getAllLikes) 42 | 43 | export default postRouter; 44 | -------------------------------------------------------------------------------- /src/routes/repostRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createReposts, verifyAlreadyRepost } from "../controllers/repostController.js"; 3 | import { validateTokenMiddleware } from "../middlewares/validateTokenMiddleware.js"; 4 | 5 | 6 | const repostRouter = Router(); 7 | 8 | repostRouter.post("/repost/:id", validateTokenMiddleware, createReposts); 9 | /* repostRouter.get("/repost/:offset", validateTokenMiddleware, reposts) */ 10 | repostRouter.get("/repost/:id", validateTokenMiddleware, verifyAlreadyRepost) 11 | export default repostRouter; -------------------------------------------------------------------------------- /src/routes/searchBarRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { validateTokenMiddleware } from "../middlewares/validateTokenMiddleware.js"; 3 | import { getSearchBar } from "../controllers/searchBarController.js"; 4 | 5 | const searchBarRouter = Router(); 6 | searchBarRouter.get("/searchbar/:name", validateTokenMiddleware, getSearchBar); 7 | 8 | 9 | export default searchBarRouter; 10 | -------------------------------------------------------------------------------- /src/routes/userRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { 3 | checkFollowings, 4 | createUser, 5 | followUser, 6 | getFollower, 7 | getUser, 8 | } from "../controllers/usersController.js"; 9 | import validateSchemaMiddleware from "../middlewares/validateSchemaMiddleware.js"; 10 | import { validateTokenMiddleware } from "../middlewares/validateTokenMiddleware.js"; 11 | import followSchema from "../schemas/followSchema.js"; 12 | import signupSchema from "../schemas/signupSchema.js"; 13 | 14 | const userRouter = Router(); 15 | userRouter.post("/signup", validateSchemaMiddleware(signupSchema), createUser); 16 | userRouter.get("/users", validateTokenMiddleware, getUser); 17 | userRouter.post( 18 | "/users/follow", 19 | validateTokenMiddleware, 20 | validateSchemaMiddleware(followSchema), 21 | followUser 22 | ); 23 | userRouter.get("/users/follow/:id", validateTokenMiddleware, getFollower); 24 | userRouter.get("/users/following", validateTokenMiddleware, checkFollowings); 25 | export default userRouter; 26 | -------------------------------------------------------------------------------- /src/schemas/commentSchema.js: -------------------------------------------------------------------------------- 1 | import joi from "joi"; 2 | 3 | const commentSchema = joi.object({ 4 | comment: joi.string().required(), 5 | }); 6 | 7 | export default commentSchema; 8 | -------------------------------------------------------------------------------- /src/schemas/followSchema.js: -------------------------------------------------------------------------------- 1 | import joi from "joi"; 2 | 3 | const followSchema = joi.object({ 4 | followedUserId: joi.required(), 5 | }); 6 | 7 | export default followSchema; 8 | -------------------------------------------------------------------------------- /src/schemas/postSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | const postSchema = Joi.object({ 4 | link: Joi.string().uri().required(), 5 | postText: Joi.string() 6 | }); 7 | 8 | export default postSchema; 9 | -------------------------------------------------------------------------------- /src/schemas/signinSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | const signinSchema = Joi.object({ 4 | email: Joi.string().email().required(), 5 | password: Joi.string().required(), 6 | }); 7 | 8 | export default signinSchema; 9 | -------------------------------------------------------------------------------- /src/schemas/signupSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | const signupSchema = Joi.object({ 4 | email: Joi.string().email().required(), 5 | password: Joi.string().required(), 6 | username: Joi.string().required(), 7 | image: Joi.string() 8 | .uri() 9 | .pattern(/(https?:\/\/.*\.(?:png|jpg|jpeg|jfif|gif))/i) 10 | .required(), 11 | }); 12 | 13 | export default signupSchema; 14 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blarth/linkr-api/14a6d7cbf6ed73080eb07a7a497d7a1295175046/src/utils/index.js --------------------------------------------------------------------------------