├── backend ├── app │ ├── __init__.py │ ├── server │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── like.py │ │ │ ├── comment.py │ │ │ ├── post.py │ │ │ └── user.py │ │ ├── routes │ │ │ ├── __init__.py │ │ │ ├── verify.py │ │ │ ├── suggestions.py │ │ │ ├── like.py │ │ │ ├── comment.py │ │ │ ├── post.py │ │ │ └── user.py │ │ ├── database.py │ │ ├── controllers │ │ │ ├── upload.py │ │ │ ├── steganography │ │ │ │ ├── decode.py │ │ │ │ ├── hash.py │ │ │ │ ├── transforms.py │ │ │ │ └── encode.py │ │ │ ├── auth.py │ │ │ ├── verify.py │ │ │ ├── like.py │ │ │ ├── comment.py │ │ │ ├── user.py │ │ │ └── post.py │ │ ├── config.py │ │ └── app.py │ └── main.py ├── package-lock.json ├── Procfile ├── requirements.txt └── .env.example ├── .gitignore ├── .github ├── linters │ └── .flake8 └── workflows │ ├── greetings.yml │ └── lints.yml ├── frontend ├── public │ ├── _redirects │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── src │ ├── Styles │ │ ├── index.css │ │ ├── loader.css │ │ ├── modal.css │ │ └── verification.css │ ├── Components │ │ ├── Navbars │ │ │ ├── PublicNavbar.js │ │ │ ├── UserNavbar.js │ │ │ └── Navbar.js │ │ ├── Utils │ │ │ └── validations.js │ │ ├── Post │ │ │ ├── Verification.js │ │ │ ├── Comment.js │ │ │ ├── CreateComment.js │ │ │ ├── CreatePost.js │ │ │ └── Post.js │ │ ├── Common │ │ │ ├── Loader.js │ │ │ └── Home.js │ │ ├── Suggestions.js │ │ ├── UserFeed.js │ │ ├── Account │ │ │ ├── Login.js │ │ │ └── Register.js │ │ └── UserProfile.js │ ├── index.js │ ├── Router │ │ ├── PublicRouter.js │ │ └── PrivateRouter.js │ ├── App.js │ ├── Context │ │ └── AuthContext.js │ ├── constants.js │ ├── service-worker.js │ └── serviceWorkerRegistration.js ├── craco.config.js ├── tailwind.config.js ├── .gitignore ├── package.json └── README.md ├── LICENSE ├── CONTRIBUTING.md └── README.md /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/server/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/server/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *__pycache__ 3 | .env -------------------------------------------------------------------------------- /.github/linters/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501 -------------------------------------------------------------------------------- /frontend/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn app.server.app:app --host "0.0.0.0" --port ${PORT:-8000} -------------------------------------------------------------------------------- /frontend/src/Styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("server.app:app", host="0.0.0.0", port=8000, reload=True) 5 | -------------------------------------------------------------------------------- /frontend/craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | postcss: { 4 | plugins: [require("tailwindcss"), require("autoprefixer")], 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.63.0 2 | uvicorn==0.13.4 3 | email-validator==1.1.2 4 | motor==2.2.0 5 | passlib==1.7.4 6 | bcrypt==3.2.0 7 | PyJWT==2.0.1 8 | python-dotenv==0.15.0 9 | python-multipart==0.0.5 10 | cloudinary==1.25.0 11 | flake8==3.8.4 12 | numpy==1.19.4 13 | opencv-python==4.4.0.46 14 | Pillow==8.2.0 15 | requests==2.25.1 -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY = 2 | ALGORITHM = 3 | ACCESS_TOKEN_EXPIRE_MINUTES = 4 | CLOUD_NAME = 5 | CLOUDINARY_API = 6 | CLOUDINARY_API_SECRET = 7 | MONGODB_URL = 8 | DEFAULT_AVATAR = 9 | THRESHOLD = -------------------------------------------------------------------------------- /frontend/src/Components/Navbars/PublicNavbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Navbar from "./Navbar"; 3 | 4 | export default function PublicNavbar() { 5 | const links = [ 6 | { 7 | link: "/", 8 | title: "Home", 9 | }, 10 | { 11 | link: "/register", 12 | title: "Register", 13 | }, 14 | { 15 | link: "/login", 16 | title: "Login", 17 | }, 18 | ]; 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /backend/app/server/database.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from .config import config 3 | 4 | MONGO_DETAILS = config["MONGODB_URL"] 5 | 6 | client = motor.motor_asyncio.AsyncIOMotorClient(MONGO_DETAILS) 7 | 8 | database = client.users 9 | 10 | users_collection = database.get_collection("users_collection") 11 | posts_collection = database.get_collection("posts_collection") 12 | likes_collection = database.get_collection("likes_collection") 13 | comments_collection = database.get_collection("comments_collection") 14 | -------------------------------------------------------------------------------- /frontend/src/Components/Navbars/UserNavbar.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Navbar from "./Navbar"; 3 | import { AuthContext } from "../../Context/AuthContext"; 4 | 5 | export default function UserNavbar() { 6 | const { user } = useContext(AuthContext); 7 | const id = user[0].user_id; 8 | const links = [ 9 | { 10 | link: "/", 11 | title: "Home", 12 | }, 13 | { 14 | link: `/user/${id}`, 15 | title: "Profile", 16 | }, 17 | ]; 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/Components/Utils/validations.js: -------------------------------------------------------------------------------- 1 | export const validateEmailAddress = (email) => { 2 | const pattern = /^(([^<>()\\[\]\\.,;:\s@"]+(\.[^<>()\\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 3 | return pattern.test(email); 4 | }; 5 | 6 | export const validatePassword = (password) => { 7 | const pattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@!%*?%#`^()_=+{}[|\\\];:'"<>,./?~&])[A-Za-z\d$@!%*?%#`^()_=+{}[|\\\];:'"<>,./?~&]{8,49}/; 8 | return pattern.test(password); 9 | }; 10 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, 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: >+ 13 | Thanks for raising an issue! 14 | Let us know what went wrong and we'd love to crunch it (only if it's a bug :p) 15 | pr-message: >+ 16 | Thanks for submitting the Pull Request! 17 | Do not worry if your PR breaks things, but do raise an issue if it does. :p -------------------------------------------------------------------------------- /backend/app/server/controllers/upload.py: -------------------------------------------------------------------------------- 1 | from fastapi import File, UploadFile 2 | import cloudinary 3 | import cloudinary.uploader 4 | import cloudinary.api 5 | from ..config import config 6 | 7 | 8 | cloudinary.config( 9 | cloud_name=config["CLOUD_NAME"], 10 | api_key=config["CLOUDINARY_API"], 11 | api_secret=config["CLOUDINARY_API_SECRET"] 12 | ) 13 | 14 | 15 | def upload_image(image: UploadFile = File(...)): 16 | result = cloudinary.uploader.upload(image.file) 17 | return result["url"] 18 | 19 | 20 | def upload_image_path(file_path: str): 21 | result = cloudinary.uploader.upload(file_path) 22 | return result["url"] 23 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./Styles/index.css"; 4 | import App from "./App"; 5 | import { AuthProvider } from "./Context/AuthContext"; 6 | import * as serviceWorkerRegistration from "./serviceWorkerRegistration"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://cra.link/PWA 18 | serviceWorkerRegistration.register(); 19 | -------------------------------------------------------------------------------- /backend/app/server/controllers/steganography/decode.py: -------------------------------------------------------------------------------- 1 | ColRange = 16 2 | RowRange = 16 3 | 4 | 5 | def sha_extract(image): 6 | s = "" 7 | count = 1 8 | num = 0 9 | for i in range(RowRange): 10 | for j in range(ColRange): 11 | if count == 4: 12 | count = 1 13 | num = num * 2 + image[i, j][1] % 2 14 | if num <= 9: 15 | s = s + chr(int(num + 48)) 16 | else: 17 | 18 | s = s + chr(int(num + 87)) 19 | num = 0 20 | else: 21 | num = num * 2 + image[i, j][1] % 2 22 | count += 1 23 | return s 24 | -------------------------------------------------------------------------------- /backend/app/server/models/like.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | 4 | class LikeSchema(BaseModel): 5 | post_id: str = Field(...) 6 | user_id: str = Field(None) 7 | is_liked: bool = Field(True) 8 | 9 | class Config: 10 | schema_extra = { 11 | "example": { 12 | "post_id": " to like/unlike", 13 | } 14 | } 15 | 16 | 17 | def ResponseModel(data, message): 18 | return { 19 | "data": data, 20 | "code": 200, 21 | "message": message, 22 | } 23 | 24 | 25 | def ErrorResponseModel(error, code, message): 26 | return {"error": error, "code": code, "message": message} 27 | -------------------------------------------------------------------------------- /backend/app/server/routes/verify.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends 2 | from ..models.post import ResponseModel 3 | from ..controllers.auth import auth_handler 4 | from ..controllers.verify import verify_post 5 | 6 | router = APIRouter() 7 | 8 | 9 | @router.post("/", response_description="Verify the post's authenticity") 10 | async def verify_post_data(post_id: str = Body(..., embed=True), current_user=Depends(auth_handler.auth_wrapper)): 11 | verified_post = await verify_post(post_id) 12 | if verified_post["is_authentic"]: 13 | return ResponseModel(verified_post, "Verification successful!") 14 | else: 15 | return ResponseModel(verified_post, "Verification failed!") 16 | -------------------------------------------------------------------------------- /backend/app/server/controllers/steganography/hash.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def hash_sha_info(user_id: str) -> list: 5 | hash_method = hashlib.sha256() 6 | hash_method.update(user_id.encode("utf-8")) 7 | binary = [] 8 | temp = [] 9 | result = [] 10 | 11 | string = hash_method.hexdigest() 12 | 13 | for i in string: 14 | if i >= "0" and i <= "9": 15 | b = ord(i) - 48 16 | else: 17 | b = ord(i) - 55 18 | while len(temp) < 4: 19 | temp.append(b % 2) 20 | b //= 2 21 | while len(temp) > 0: 22 | binary.append(temp.pop()) 23 | 24 | result.append(string) 25 | result.append(binary) 26 | return result 27 | -------------------------------------------------------------------------------- /backend/app/server/routes/suggestions.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from ..controllers.user import retrieve_users 3 | from ..models.user import ResponseModel 4 | 5 | router = APIRouter() 6 | 7 | 8 | @router.get("/", response_description="Get all lightweight user details from the database") 9 | async def get_all_users_lightweight(): 10 | all_users = await retrieve_users(lightweight=True) 11 | return ResponseModel(all_users, "Got lightweight user details successfully.") 12 | 13 | 14 | @router.get("/full", response_description="Get all user details from the database") 15 | async def get_all_users_full(): 16 | all_users = await retrieve_users(lightweight=False) 17 | return ResponseModel(all_users, "Got full user details successfully.") 18 | -------------------------------------------------------------------------------- /backend/app/server/routes/like.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends 2 | from fastapi.encoders import jsonable_encoder 3 | from ..controllers.auth import auth_handler 4 | 5 | from ..controllers.like import ( 6 | like_unlike_post 7 | ) 8 | from ..models.like import ( 9 | ResponseModel, 10 | LikeSchema, 11 | ) 12 | 13 | router = APIRouter() 14 | 15 | 16 | @router.post("/like_unlike", response_description="Like/Unlike the post") 17 | async def like_unlike_post_data(like_details: LikeSchema = Body(...), current_user=Depends(auth_handler.auth_wrapper)): 18 | like_details = jsonable_encoder(like_details) 19 | new_like = await like_unlike_post(current_user, like_details) 20 | return ResponseModel(new_like, "Post liked/unliked successfully.") 21 | -------------------------------------------------------------------------------- /backend/app/server/models/comment.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | 4 | class CommentSchema(BaseModel): 5 | post_id: str = Field(...) 6 | user_id: str = Field(None) 7 | payload: str = Field(...) 8 | is_commented: bool = Field(True) 9 | 10 | class Config: 11 | schema_extra = { 12 | "example": { 13 | "post_id": " to comment", 14 | "payload": "A nice comment" 15 | } 16 | } 17 | 18 | 19 | def ResponseModel(data, message): 20 | return { 21 | "data": data, 22 | "code": 200, 23 | "message": message, 24 | } 25 | 26 | 27 | def ErrorResponseModel(error, code, message): 28 | return {"error": error, "code": code, "message": message} 29 | -------------------------------------------------------------------------------- /backend/app/server/controllers/steganography/transforms.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def arnold(image, height, width): 5 | An = np.zeros([height, width, 3]) 6 | a = 3 7 | b = 5 8 | N = min(height, width) 9 | for i in range(1, 2): 10 | for y in range(N): 11 | for x in range(N): 12 | xx = (x + b * y) % N 13 | yy = (a * x + (a * b + 1) * y) % N 14 | An[yy][xx] = image[y][x] 15 | if height > width: 16 | for y in range(N, height): 17 | for x in range(N): 18 | An[y][x] = image[y][x] 19 | elif height < width: 20 | for y in range(N): 21 | for x in range(N, width): 22 | An[y][x] = image[y][x] 23 | 24 | return An 25 | -------------------------------------------------------------------------------- /frontend/src/Router/PublicRouter.js: -------------------------------------------------------------------------------- 1 | import { useRoutes, useRedirect, navigate } from "hookrouter"; 2 | import React from "react"; 3 | import Home from "../Components/Common/Home"; 4 | import Register from "../Components/Account/Register"; 5 | 6 | const routes = { 7 | "/home": () => , 8 | "/register": () => , 9 | }; 10 | 11 | const PublicRouter = () => { 12 | useRedirect("/", "/home"); 13 | const pages = useRoutes(routes); 14 | !pages && navigate("/"); 15 | return ( 16 |
17 | {pages} 18 | {!pages && ( 19 |
20 | Error 404: Page not found 21 |
22 | )} 23 |
24 | ); 25 | }; 26 | export default PublicRouter; 27 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import PublicRouter from "./Router/PublicRouter"; 2 | import PrivateRouter from "./Router/PrivateRouter"; 3 | import { ToastContainer } from "react-toastify"; 4 | import { injectStyle } from "react-toastify/dist/inject-style"; 5 | 6 | function App() { 7 | const access = localStorage.getItem("access_token"); 8 | injectStyle(); 9 | 10 | return ( 11 |
12 | {access ? : } 13 | 24 |
25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /.github/workflows/lints.yml: -------------------------------------------------------------------------------- 1 | name: Lint Code Base 2 | 3 | on: 4 | [push, pull_request] 5 | 6 | jobs: 7 | lint: 8 | name: Run Linters 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v2 14 | with: 15 | # Full git history is needed to get a proper list of changed files within `super-linter` 16 | fetch-depth: 0 17 | 18 | - name: Run Individual Linters 19 | uses: github/super-linter@v3 20 | env: 21 | VALIDATE_ALL_CODEBASE: false 22 | VALIDATE_PYTHON_FLAKE8: true 23 | VALIDATE_JAVASCRIPT_ES: true 24 | JAVASCRIPT_DEFAULT_STYLE: prettier 25 | VALIDATE_JAVASCRIPT_PRETTIER: true 26 | DEFAULT_BRANCH: main 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /backend/app/server/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Heroku uses Environment Variables instead of dot_env. 4 | if os.getenv("HEROKU_DEPLOYMENT") is True: 5 | config = { 6 | "SECRET_KEY": os.getenv("SECRET_KEY"), 7 | "ALGORITHM": os.getenv("ALGORITHM"), 8 | "ACCESS_TOKEN_EXPIRE_MINUTES": os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES"), 9 | "CLOUD_NAME": os.getenv("CLOUD_NAME"), 10 | "CLOUDINARY_API": os.getenv("CLOUDINARY_API"), 11 | "CLOUDINARY_API_SECRET": os.getenv("CLOUDINARY_API_SECRET"), 12 | "MONGODB_URL": os.getenv("MONGODB_URL"), 13 | "DEFAULT_AVATAR": os.getenv("DEFAULT_AVATAR"), 14 | "THRESHOLD": os.getenv("THRESHOLD") 15 | } 16 | 17 | else: 18 | # strict imports 19 | from dotenv import dotenv_values, find_dotenv 20 | config = dotenv_values(find_dotenv()) 21 | -------------------------------------------------------------------------------- /frontend/src/Components/Post/Verification.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../Styles/modal.css"; 3 | import PropTypes from "prop-types"; 4 | 5 | export default class Modal extends React.Component { 6 | onClose = (e) => { 7 | this.props.onClose && this.props.onClose(e); 8 | }; 9 | render() { 10 | if (!this.props.show) { 11 | return null; 12 | } 13 | return ( 14 | 23 | ); 24 | } 25 | } 26 | Modal.propTypes = { 27 | onClose: PropTypes.func.isRequired, 28 | show: PropTypes.bool.isRequired, 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/src/Router/PrivateRouter.js: -------------------------------------------------------------------------------- 1 | import { useRoutes, useRedirect, navigate } from "hookrouter"; 2 | import React from "react"; 3 | import UserNavbar from "../Components/Navbars/UserNavbar"; 4 | import UserFeed from "../Components/UserFeed"; 5 | import UserProfile from "../Components/UserProfile"; 6 | 7 | const routes = { 8 | "/home": () => , 9 | "/user/:id": ({ id }) => , 10 | }; 11 | 12 | const PrivateRouter = () => { 13 | useRedirect("/", "/home"); 14 | const pages = useRoutes(routes); 15 | !pages && navigate("/"); 16 | return ( 17 |
18 | 19 | {pages} 20 | {!pages && ( 21 |
22 | Error 404: Page not found 23 |
24 | )} 25 |
26 | ); 27 | }; 28 | export default PrivateRouter; 29 | -------------------------------------------------------------------------------- /frontend/src/Styles/loader.css: -------------------------------------------------------------------------------- 1 | .loader-dots div { 2 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 3 | } 4 | .loader-dots div:nth-child(1) { 5 | left: 8px; 6 | animation: loader-dots1 0.6s infinite; 7 | } 8 | .loader-dots div:nth-child(2) { 9 | left: 8px; 10 | animation: loader-dots2 0.6s infinite; 11 | } 12 | .loader-dots div:nth-child(3) { 13 | left: 32px; 14 | animation: loader-dots2 0.6s infinite; 15 | } 16 | .loader-dots div:nth-child(4) { 17 | left: 56px; 18 | animation: loader-dots3 0.6s infinite; 19 | } 20 | @keyframes loader-dots1 { 21 | 0% { 22 | transform: scale(0); 23 | } 24 | 100% { 25 | transform: scale(1); 26 | } 27 | } 28 | @keyframes loader-dots3 { 29 | 0% { 30 | transform: scale(1); 31 | } 32 | 100% { 33 | transform: scale(0); 34 | } 35 | } 36 | @keyframes loader-dots2 { 37 | 0% { 38 | transform: translate(0, 0); 39 | } 40 | 100% { 41 | transform: translate(24px, 0); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/Context/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, createContext } from "react"; 2 | import axios from "axios"; 3 | import { CURRENT_USER_URL } from "../constants"; 4 | export const AuthContext = createContext(); 5 | 6 | export const AuthProvider = (props) => { 7 | const [user, setUser] = useState(""); 8 | const access = localStorage.getItem("access_token"); 9 | useEffect(() => { 10 | if (access) { 11 | axios 12 | .get(CURRENT_USER_URL, { 13 | headers: { 14 | Authorization: "Bearer " + access, 15 | }, 16 | }) 17 | .then((res) => { 18 | if (res && res.status === 200) { 19 | setUser(res.data.data); 20 | } 21 | }); 22 | } 23 | // eslint-disable-next-line 24 | }, []); 25 | 26 | return ( 27 | 28 | {props.children} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SocioMark", 3 | "short_name": "SocioMark", 4 | "theme_color": "#000000", 5 | "background_color": "#000000", 6 | "display": "standalone", 7 | "scope": "/", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "https://user-images.githubusercontent.com/34866653/114776270-e999ff80-9d8f-11eb-818c-d0b630694f93.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "https://user-images.githubusercontent.com/34866653/114776064-acce0880-9d8f-11eb-9be5-a77550499fc6.png", 17 | "sizes": "256x256", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "https://user-images.githubusercontent.com/34866653/114776161-c8d1aa00-9d8f-11eb-9f60-feccb054f200.png", 22 | "sizes": "384x384", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "https://user-images.githubusercontent.com/34866653/114776167-ca02d700-9d8f-11eb-997e-c380797663f2.png", 27 | "sizes": "512x512", 28 | "type": "image/png" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /backend/app/server/models/post.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import BaseModel, Field 3 | 4 | 5 | class PostSchema(BaseModel): 6 | user_id: str = Field(None) 7 | report_counter: int = Field(0, ge=0) 8 | image: str = Field(...) 9 | user_sha: str = Field(None) 10 | description: str = Field(None) 11 | 12 | class Config: 13 | schema_extra = { 14 | "example": { 15 | "image": "url_of_image", 16 | "description": "This is a post by " 17 | } 18 | } 19 | 20 | 21 | class UpdatePostModel(BaseModel): 22 | post_id: str 23 | description: Optional[str] 24 | 25 | class Config: 26 | schema_extra = { 27 | "example": { 28 | "description": "This is a post by " 29 | } 30 | } 31 | 32 | 33 | def ResponseModel(data, message): 34 | return { 35 | "data": data, 36 | "code": 200, 37 | "message": message, 38 | } 39 | 40 | 41 | def ErrorResponseModel(error, code, message): 42 | return {"error": error, "code": code, "message": message} 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Open Sourced Olaf | MLH Fellowship 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 | -------------------------------------------------------------------------------- /backend/app/server/routes/comment.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends 2 | from fastapi.encoders import jsonable_encoder 3 | from ..controllers.auth import auth_handler 4 | 5 | from ..controllers.comment import ( 6 | add_comment, 7 | delete_comment 8 | ) 9 | from ..models.comment import ( 10 | ResponseModel, 11 | CommentSchema, 12 | ) 13 | 14 | router = APIRouter() 15 | 16 | 17 | @router.post("/comment", response_description="Add a comment to a post") 18 | async def add_comment_post_data(comment_details: CommentSchema = Body(...), current_user=Depends(auth_handler.auth_wrapper)): 19 | comment_details = jsonable_encoder(comment_details) 20 | new_comment = await add_comment(current_user, comment_details) 21 | return ResponseModel(new_comment, "Comment added successfully.") 22 | 23 | 24 | @router.delete("/uncomment", response_description="Delete a comment from a post") 25 | async def delete_comment_post_data(comment_id: str = Body(..., embed=True), current_user=Depends(auth_handler.auth_wrapper)): 26 | new_comment = await delete_comment(current_user, comment_id) 27 | return ResponseModel(new_comment, "Comment deleted successfully.") 28 | -------------------------------------------------------------------------------- /backend/app/server/controllers/steganography/encode.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | ColRange = 16 5 | RowRange = 16 6 | 7 | 8 | def recover(image, height, width): 9 | An = np.zeros([height, width, 3]) 10 | a = 3 11 | b = 5 12 | N = min(height, width) 13 | for i in range(1, 2): 14 | for y in range(N): 15 | for x in range(N): 16 | xx = ((a * b + 1) * x - b * y) % N 17 | yy = (a * x * -1 + y) % N 18 | An[yy, xx] = image[y][x] 19 | 20 | if height > width: 21 | for y in range(N, height): 22 | for x in range(N): 23 | An[y][x] = image[y][x] 24 | elif height < width: 25 | for y in range(N): 26 | for x in range(N, width): 27 | An[y][x] = image[y][x] 28 | return An 29 | 30 | 31 | def watermark(image, info): 32 | index = 0 33 | for i in range(RowRange): 34 | for j in range(ColRange): 35 | a = image[i, j] 36 | digit = info[index] 37 | index += 1 38 | 39 | if a[1] % 2 == 0 and digit == 1: 40 | a[1] = a[1] + 1 41 | 42 | elif a[1] % 2 == 1 and digit == 0: 43 | a[1] = a[1] - 1 44 | image[i, j] = a 45 | -------------------------------------------------------------------------------- /frontend/src/Styles/modal.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | width: 500px; 3 | background: white; 4 | border: 1px solid #ccc; 5 | transition: 1.1s ease-out; 6 | box-shadow: -2rem 2rem 2rem rgba(0, 0, 0, 0.2); 7 | filter: blur(0); 8 | transform: scale(1); 9 | opacity: 1; 10 | visibility: visible; 11 | } 12 | .modal.off { 13 | opacity: 0; 14 | visibility: hidden; 15 | filter: blur(8px); 16 | transform: scale(0.33); 17 | box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); 18 | } 19 | @supports (offset-rotation: 0deg) { 20 | offset-rotation: 0deg; 21 | offset-path: path("M 250,100 S -300,500 -700,-200"); 22 | .modal.off { 23 | offset-distance: 100%; 24 | } 25 | } 26 | @media (prefers-reduced-motion) { 27 | .modal { 28 | offset-path: none; 29 | } 30 | } 31 | .modal h2 { 32 | border-bottom: 1px solid #ccc; 33 | padding: 1rem; 34 | margin: 0; 35 | } 36 | .modal .content { 37 | padding: 1rem; 38 | } 39 | .modal .actions { 40 | border-top: 1px solid #ccc; 41 | background: #eee; 42 | padding: 0.5rem 1rem; 43 | } 44 | .modal .actions button { 45 | border: 0; 46 | background: #78f89f; 47 | border-radius: 5px; 48 | padding: 0.5rem 1rem; 49 | font-size: 0.8rem; 50 | line-height: 1; 51 | } 52 | #centered-toggle-button { 53 | position: absolute; 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/constants.js: -------------------------------------------------------------------------------- 1 | const ENV = process.env.NODE_ENV; 2 | let BASE_URL; 3 | 4 | switch (ENV) { 5 | case "production": 6 | BASE_URL = "https://sociomark-backend.herokuapp.com"; 7 | break; 8 | default: 9 | BASE_URL = "http://localhost:8000"; 10 | break; 11 | } 12 | 13 | export const LOGIN_URL = `${BASE_URL}/user/login`; 14 | export const REGISTER_URL = `${BASE_URL}/user/register`; 15 | export const CURRENT_USER_URL = `${BASE_URL}/user/current_user`; 16 | export const POST_CREATE_URL = `${BASE_URL}/post/create`; 17 | export const POST_EDIT_URL = `${BASE_URL}/post/update`; 18 | export const POST_DELETE_URL = `${BASE_URL}/post/delete`; 19 | export const POST_GET_ALL_URL = `${BASE_URL}/post/all`; 20 | export const POST_COMMENT_URL = `${BASE_URL}/post/comment`; 21 | export const POST_UNCOMMENT_URL = `${BASE_URL}/post/uncomment`; 22 | export const USER_SUGGESTIONS_URL = `${BASE_URL}/suggestions`; 23 | export const GET_USER_DETAILS_URL = `${BASE_URL}/user/details?user_id=`; 24 | export const POST_LIKE_UNLIKE_URL = `${BASE_URL}/post/like_unlike`; 25 | export const POST_REPORT_URL = `${BASE_URL}/post/report`; 26 | export const POST_VERIFY_URL = `${BASE_URL}/verify`; 27 | 28 | // Cooldown before post can be reported again 29 | export const POST_REPORT_COOLDOWN = 20000; 30 | -------------------------------------------------------------------------------- /frontend/src/Components/Common/Loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../Styles/loader.css"; 3 | export const Loading = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Please wait... 16 |
17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export const FullLoading = ({ color = "gray-200" }) => { 24 | return ( 25 |
28 | 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/src/Components/Post/Comment.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { AuthContext } from "../../Context/AuthContext"; 3 | import { A } from "hookrouter"; 4 | 5 | export default function Comment({ handleDeleteComment, comment }) { 6 | const { user } = useContext(AuthContext); 7 | 8 | return ( 9 |
10 |
11 | 12 | {comment.name}: 13 | 14 | 15 | {comment.payload} 16 |
17 | {user[0].user_id === comment.user_id && ( 18 | handleDeleteComment(comment.comment_id)}> 19 | 27 | 28 | 29 | 30 | 31 | )} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.1.1", 7 | "@testing-library/jest-dom": "^5.11.10", 8 | "@testing-library/react": "^11.2.6", 9 | "@testing-library/user-event": "^12.8.3", 10 | "axios": "^0.21.1", 11 | "eslint": "^7.23.0", 12 | "hookrouter": "^1.2.5", 13 | "prettier": "^2.2.1", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-scripts": "4.0.3", 17 | "react-toastify": "^7.0.3", 18 | "typescript": "^4.2.3", 19 | "web-vitals": "^1.1.1" 20 | }, 21 | "scripts": { 22 | "start": "cross-env NODE_ENV=development craco start", 23 | "build": "cross-env NODE_ENV=production craco build", 24 | "test": "craco test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@tailwindcss/postcss7-compat": "^2.0.4", 47 | "autoprefixer": "^9.8.6", 48 | "cross-env": "^7.0.3", 49 | "postcss": "^7.0.35", 50 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/Styles/verification.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | } 4 | 5 | .post { 6 | opacity: 1; 7 | display: block; 8 | width: 100%; 9 | height: auto; 10 | transition: 0.5s ease; 11 | backface-visibility: hidden; 12 | } 13 | 14 | .middle { 15 | transition: 0.5s ease; 16 | opacity: 0; 17 | position: absolute; 18 | top: 50%; 19 | left: 50%; 20 | transform: translate(-50%, -50%); 21 | -ms-transform: translate(-50%, -50%); 22 | text-align: center; 23 | } 24 | 25 | .container:hover .post { 26 | opacity: 0.3; 27 | } 28 | 29 | .container:hover .middle { 30 | opacity: 1; 31 | } 32 | 33 | .button { 34 | border-radius: 4px; 35 | background-color: #f4511e; 36 | border: none; 37 | color: #ffffff; 38 | text-align: center; 39 | font-size: 20px; 40 | padding: 10px; 41 | width: 125px; 42 | transition: all 0.5s; 43 | cursor: pointer; 44 | margin: 5px; 45 | } 46 | 47 | .button span { 48 | cursor: pointer; 49 | display: inline-block; 50 | position: relative; 51 | transition: 0.5s; 52 | } 53 | 54 | .button span:after { 55 | content: "\00bb"; 56 | position: absolute; 57 | opacity: 0; 58 | top: 0; 59 | right: -20px; 60 | transition: 0.5s; 61 | } 62 | 63 | .button:hover span { 64 | padding-right: 25px; 65 | } 66 | 67 | .button:hover span:after { 68 | opacity: 1; 69 | right: 0; 70 | } 71 | 72 | .disable { 73 | background-color: darkgray; 74 | cursor: not-allowed; 75 | pointer-events: none; 76 | } 77 | 78 | .clickable{ 79 | cursor:pointer; 80 | padding:2px; 81 | } 82 | 83 | .edit-post-area{ 84 | margin-top:4px; 85 | } -------------------------------------------------------------------------------- /backend/app/server/models/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import BaseModel, EmailStr, Field 3 | 4 | 5 | class UserSchema(BaseModel): 6 | name: str = Field(...) 7 | email: EmailStr = Field(...) 8 | password: str = Field(...) 9 | profile_picture: str = Field(None) 10 | description: str = Field(None) 11 | 12 | class Config: 13 | schema_extra = { 14 | "example": { 15 | "name": "John Doe", 16 | "email": "jdoe@x.edu.ng", 17 | "password": "password", 18 | "profile_picture": "url_of_image", 19 | "description": "This is John Doe" 20 | } 21 | } 22 | 23 | 24 | class LoginSchema(BaseModel): 25 | email: EmailStr = Field(...) 26 | password: str = Field(...) 27 | 28 | class Config: 29 | schema_extra = { 30 | "example": { 31 | "email": "jdoe@s.edu.org", 32 | "password": "Jdoe@123" 33 | } 34 | } 35 | 36 | 37 | class UpdateUserModel(BaseModel): 38 | name: Optional[str] 39 | password: Optional[str] 40 | profile_picture: Optional[str] 41 | description: Optional[str] 42 | 43 | class Config: 44 | schema_extra = { 45 | "example": { 46 | "name": "John Doe", 47 | "password": "password", 48 | "profile_picture": "url_of_image", 49 | "description": "This is John Doe" 50 | } 51 | } 52 | 53 | 54 | def ResponseModel(data, message): 55 | return { 56 | "data": data, 57 | "code": 200, 58 | "message": message, 59 | } 60 | 61 | 62 | def ErrorResponseModel(error, code, message): 63 | return {"error": error, "code": code, "message": message} 64 | -------------------------------------------------------------------------------- /backend/app/server/controllers/auth.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from datetime import datetime, timedelta 3 | from passlib.context import CryptContext 4 | from fastapi.security import OAuth2PasswordBearer, HTTPAuthorizationCredentials, HTTPBearer 5 | from fastapi import HTTPException, Security 6 | from ..config import config 7 | 8 | 9 | class AuthHandler(): 10 | 11 | security = HTTPBearer() 12 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 13 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") 14 | 15 | def get_password_hash(self, password): 16 | return self.pwd_context.hash(password) 17 | 18 | def verify_password(self, plain_password, hashed_password): 19 | return self.pwd_context.verify(plain_password, hashed_password) 20 | 21 | # create jwt access token 22 | def create_access_token(self, email): 23 | payload = { 24 | 'exp': datetime.utcnow() + timedelta(int(config["ACCESS_TOKEN_EXPIRE_MINUTES"])), 25 | 'sub': email 26 | } 27 | return jwt.encode( 28 | payload, 29 | config["SECRET_KEY"], 30 | algorithm=config["ALGORITHM"] 31 | ) 32 | 33 | def decode_token(self, token): 34 | try: 35 | payload = jwt.decode(token, config["SECRET_KEY"], algorithms=[config["ALGORITHM"]]) 36 | return payload["sub"] 37 | except jwt.ExpiredSignatureError: 38 | raise HTTPException(status_code=401, detail='Signature has expired') 39 | except jwt.InvalidTokenError: 40 | raise HTTPException(status_code=401, detail='Invalid Token') 41 | 42 | def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): 43 | return self.decode_token(auth.credentials) 44 | 45 | 46 | auth_handler = AuthHandler() 47 | -------------------------------------------------------------------------------- /frontend/src/Components/Suggestions.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { A } from "hookrouter"; 3 | 4 | export default function Suggestions({ user }) { 5 | return ( 6 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/Components/Post/CreateComment.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import axios from "axios"; 3 | import { AuthContext } from "../../Context/AuthContext"; 4 | import { toast } from "react-toastify"; 5 | import { POST_COMMENT_URL } from "../../constants"; 6 | 7 | export default function CreateComment({ handleCreateComment, post }) { 8 | const [comment, setComment] = useState(""); 9 | const { token } = useContext(AuthContext); 10 | const [access] = token; 11 | const handleChange = (e) => { 12 | setComment(e.target.value); 13 | }; 14 | 15 | const FormData = { 16 | post_id: post.post_id, 17 | payload: comment, 18 | }; 19 | 20 | const handleSubmit = (e) => { 21 | e.preventDefault(); 22 | setComment(""); 23 | axios 24 | .post(POST_COMMENT_URL, FormData, { 25 | headers: { 26 | Authorization: "Bearer " + access, 27 | }, 28 | }) 29 | .then((res) => { 30 | toast.success(JSON.stringify(res.data.message)); 31 | handleCreateComment(res.data.data); 32 | }) 33 | .catch((err) => { 34 | toast.error(JSON.stringify(err)); 35 | }); 36 | }; 37 | return ( 38 |
39 |
40 |
41 | 49 | 52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /backend/app/server/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.responses import HTMLResponse 3 | from .routes.user import router as UserRouter 4 | from .routes.post import router as PostRouter 5 | from .routes.suggestions import router as SuggestionsRouter 6 | from .routes.like import router as LikeRouter 7 | from .routes.comment import router as CommentRouter 8 | from .routes.verify import router as VerificationRouter 9 | from fastapi.middleware.cors import CORSMiddleware 10 | 11 | app = FastAPI() 12 | 13 | origins = ["*"] 14 | 15 | app.add_middleware( 16 | CORSMiddleware, 17 | allow_origins=origins, 18 | allow_credentials=True, 19 | allow_methods=["*"], 20 | allow_headers=["*"], 21 | ) 22 | 23 | 24 | # Include Likes and Comments Router within Post 25 | PostRouter.include_router(LikeRouter, prefix="") 26 | PostRouter.include_router(CommentRouter, prefix="") 27 | 28 | # Three major tags 29 | app.include_router(UserRouter, tags=["User"], prefix="/user") 30 | app.include_router(PostRouter, tags=["Post"], prefix="/post") 31 | app.include_router(SuggestionsRouter, tags=["Suggestions"], prefix="/suggestions") 32 | app.include_router(VerificationRouter, tags=["Verification"], prefix="/verify") 33 | 34 | 35 | @app.get("/", tags=["Root"]) 36 | async def read_root(): 37 | return HTMLResponse( 38 | """ 39 | 51 | 52 | 53 | 54 |
55 |

You are visiting SocioMark's Backend.

56 |

visit /docs for more info

57 |

58 | """ 59 | ) 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution Guidelines: 2 | 3 | SocioMark is a social media platform that lets you upload images and secures them by embedding a personalised hash. 4 | 5 | ## Contribution Steps: 6 | 7 | - If you want to work on an existing issue to make a contribution head to the [issue section](https://github.com/MLH-Fellowship/SocioMark/issues) for the project. 8 | If you are working on a pre-existing issue, let us know by assigning it to yourself or let us know in the comment. 9 | - You can also submit a issue to work on. The issue can be realted to any topic from bug, refactoring, UI changes, etc. 10 | - If you have any question regarding the issues, feel free to ask in the issue's comment section. 11 | 12 | - ### In order to start working on the issue: 13 | - Create a fork of the project. 14 | - Clone the repo locally with `git clone https://github.com//SocioMark.git` 15 | - Checkout in a new branch with `git checkout -b ` 16 | - Make the required changes 17 | - Add and commit your changes with `git add ` and `git commit -m "your commit message"` 18 | - Then push the changes into your branch `git push origin branch_name` 19 | - Before making a PR, make sure your copy of the project is up-to-date with the main project. 20 | - If forked project is up-to-date with the main project, go ahead and make the PR. 21 | - #### If there has been any commit since you forked the project, there might be some conflicts while making the PR. In case of any conflicts: 22 | - Set an upstream with `git remote add upstream https://github.com/MLH-Fellowship/SocioMark.git` 23 | - Checkout to main branch and `git pull upstream main` 24 | - Checkout to the branch you were working on 25 | - Rebase it with the main branch with `git rebase main` 26 | - If there are any conflicts while rebasing, resolve them locally, and continue the rebase with `git rebase --continue` 27 | - Test it locally 28 | - If everything is working as expected, push it to the origin 29 | - Make a PR to the main project 30 | - Once the PR is made, someone from our team will review it and approve/request changes accordingly. 31 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 15 | 19 | 23 | 24 | 33 | SocioMark 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /frontend/src/Components/Common/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Login from "../Account/Login"; 3 | import Register from "../Account/Register"; 4 | export default function Home() { 5 | const [selection, setSelection] = useState("login"); 6 | return ( 7 |
8 |
9 | People working on laptops 14 |
18 |
19 |
20 |
21 |

22 | SocioMark 27 | 28 | The Social Media Platform We All Deserve 29 | 30 |

31 |

32 | SocioMark is an all-new platform that lets you upload images and 33 | secure them with a personalized encryption. Your assets will always 34 | be yours! 35 |

36 |
37 |
38 | {selection === "login" ? ( 39 | { 41 | setSelection("register"); 42 | }} 43 | /> 44 | ) : ( 45 | { 47 | setSelection("login"); 48 | }} 49 | /> 50 | )} 51 |
52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /backend/app/server/controllers/verify.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import requests 3 | import numpy as np 4 | import cv2 5 | from PIL import Image 6 | from bson import ObjectId 7 | from ..database import posts_collection, users_collection 8 | from .steganography.transforms import arnold 9 | from .steganography.encode import watermark, recover 10 | from .steganography.decode import sha_extract 11 | from .steganography.hash import hash_sha_info 12 | 13 | 14 | # helpers 15 | 16 | 17 | def verify_helper(post, user, is_authentic) -> dict: 18 | return { 19 | "post_id": str(post["_id"]), 20 | "author_id": str(user["_id"]), 21 | "author_name": user["name"], 22 | "author_email": user["email"], 23 | "author_image": user["profile_picture"], 24 | "is_authentic": is_authentic 25 | } 26 | 27 | 28 | async def encode_image(rgb_image, info: list): 29 | height = rgb_image.shape[0] 30 | width = rgb_image.shape[1] 31 | 32 | # do an arnold transformation 33 | arnold_transformed = arnold(rgb_image, height, width) 34 | 35 | # watermark the image with user's info 36 | watermark(arnold_transformed, info) 37 | encoded_image = recover(arnold_transformed, height, width) 38 | 39 | return encoded_image 40 | 41 | 42 | async def decode_image(rgb_image): 43 | height = rgb_image.shape[0] 44 | width = rgb_image.shape[1] 45 | 46 | # do an arnold transformation 47 | arnold_transformed = arnold(rgb_image, height, width) 48 | embedded_sha = sha_extract(arnold_transformed) 49 | 50 | return embedded_sha 51 | 52 | 53 | async def verify_post(post_id: str): 54 | post = await posts_collection.find_one({"_id": ObjectId(post_id)}) 55 | user_sha, _ = hash_sha_info(str(post["user_id"])) 56 | 57 | response = requests.get(post["image"]) 58 | pil_image = Image.open(BytesIO(response.content)) 59 | rgb_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) 60 | 61 | embedded_sha = await decode_image(rgb_image) 62 | 63 | if embedded_sha == user_sha: 64 | user = await users_collection.find_one({"_id": post["user_id"]}) 65 | return verify_helper(post, user, True) 66 | else: 67 | original_post_exists = await posts_collection.find_one({"user_sha": embedded_sha}) 68 | if not original_post_exists: 69 | user = await users_collection.find_one({"_id": post["user_id"]}) 70 | return verify_helper(post, user, True) 71 | user = await users_collection.find_one({"_id": original_post_exists["user_id"]}) 72 | return verify_helper(original_post_exists, user, False) 73 | -------------------------------------------------------------------------------- /backend/app/server/routes/post.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends, status 2 | from fastapi.encoders import jsonable_encoder 3 | from fastapi import UploadFile, File, Form 4 | from ..controllers.auth import auth_handler 5 | 6 | from ..controllers.post import ( 7 | add_post, 8 | delete_post, 9 | retrieve_post, 10 | retrieve_all_posts, 11 | update_post, 12 | report_post 13 | ) 14 | from ..models.post import ( 15 | ResponseModel, 16 | UpdatePostModel, 17 | ) 18 | 19 | router = APIRouter() 20 | 21 | 22 | @router.post("/create", response_description="Post added into the database", status_code=status.HTTP_201_CREATED) 23 | async def add_post_data(image: UploadFile = File(...), description: str = Form(None), current_user=Depends(auth_handler.auth_wrapper)): 24 | post = { 25 | "report_counter": 0, 26 | "image": image, 27 | "description": description 28 | } 29 | new_post = await add_post(current_user, post) 30 | return ResponseModel(new_post, "Post created successfully.") 31 | 32 | 33 | @router.delete("/delete", response_description="Delete post from the database") 34 | async def delete_post_data(post_id: str = Body(..., embed=True), current_user=Depends(auth_handler.auth_wrapper)): 35 | new_post = await delete_post(current_user, post_id) 36 | return ResponseModel(new_post, "Post deleted successfully.") 37 | 38 | 39 | @router.patch("/update", response_description="Update post") 40 | async def update_post_data(updated_post: UpdatePostModel = Body(...), current_user=Depends(auth_handler.auth_wrapper)): 41 | updated_post = jsonable_encoder(updated_post) 42 | new_post = await update_post(current_user, updated_post["post_id"], updated_post) 43 | return ResponseModel(new_post, "Post updated successfully.") 44 | 45 | 46 | @router.post("/report", response_description="Report post") 47 | async def report_post_data(post_id: str = Body(..., embed=True)): 48 | new_post = await report_post(post_id) 49 | return ResponseModel(new_post, "Post reported successfully.") 50 | 51 | 52 | @router.get("/details", response_description="Get post details from the database") 53 | async def details_post_data(post_id: str = Body(..., embed=True)): 54 | new_post = await retrieve_post(post_id) 55 | return ResponseModel(new_post, "Got post details successfully.") 56 | 57 | 58 | @router.get("/all", response_description="Get all the posts from the database") 59 | async def details_all_posts_data(current_user=Depends(auth_handler.auth_wrapper)): 60 | all_posts = await retrieve_all_posts() 61 | return ResponseModel(all_posts, "Got all posts successfully.") 62 | -------------------------------------------------------------------------------- /backend/app/server/controllers/like.py: -------------------------------------------------------------------------------- 1 | from bson.objectid import ObjectId 2 | from ..database import likes_collection, users_collection 3 | 4 | # helpers 5 | 6 | 7 | # lightweight mode defined here due to circular dependencies 8 | # besides, only likes API should return lightweight user details 9 | def user_helper_lightweight(user, like_id: ObjectId = None) -> dict: 10 | if not like_id: 11 | return { 12 | "user_id": str(user["_id"]), 13 | "name": user["name"], 14 | "email": user["email"], 15 | "profile_picture": user["profile_picture"] 16 | } 17 | return { 18 | "like_id": str(like_id), 19 | "user_id": str(user["_id"]), 20 | "name": user["name"], 21 | "profile_picture": user["profile_picture"] 22 | } 23 | 24 | 25 | async def retrieve_user_lightweight(like_id: ObjectId, user_id: ObjectId): 26 | user = await users_collection.find_one({"_id": user_id}) 27 | if user: 28 | return user_helper_lightweight(user, like_id) 29 | 30 | 31 | def like_helper(like) -> dict: 32 | return { 33 | "like_id": str(like["_id"]), 34 | "to_delete": like["is_liked"] 35 | } 36 | 37 | 38 | async def get_all_likes_on_post(post_id: ObjectId): 39 | likes = [] 40 | async for like in likes_collection.find({"post_id": post_id}, {"user_id", "is_liked"}): 41 | if like["is_liked"] is True: 42 | likes.append(await retrieve_user_lightweight(like["_id"], like["user_id"])) 43 | return likes 44 | 45 | 46 | async def initialize_like(user_id: ObjectId, post_id: ObjectId, like_details: dict) -> dict: 47 | like_details["user_id"] = user_id 48 | like_details["post_id"] = post_id 49 | like_details["is_liked"] = True 50 | return like_details 51 | 52 | 53 | # Add a post id to the like model 54 | async def like_unlike_post(email: str, like_details: dict): 55 | user = await users_collection.find_one({"email": email}) 56 | entry_exists = await likes_collection.find_one({"post_id": ObjectId(like_details["post_id"]), "user_id": user["_id"]}) 57 | if not entry_exists: 58 | # First Time: create entry 59 | like_details = await initialize_like(user["_id"], ObjectId(like_details["post_id"]), like_details) 60 | new_like = await likes_collection.insert_one(like_details) 61 | return user_helper_lightweight(user, new_like.inserted_id) 62 | else: 63 | # Next Time: update the is_liked label 64 | await likes_collection.update_one( 65 | {"post_id": ObjectId(like_details["post_id"]), "user_id": entry_exists["user_id"]}, {"$set": {"is_liked": not entry_exists["is_liked"]}} 66 | ) 67 | if not entry_exists["is_liked"]: 68 | return user_helper_lightweight(user, entry_exists["_id"]) 69 | return like_helper(entry_exists) 70 | -------------------------------------------------------------------------------- /frontend/src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | // This service worker can be customized! 4 | // See https://developers.google.com/web/tools/workbox/modules 5 | // for the list of available Workbox modules, or add any other 6 | // code you'd like. 7 | // You can also remove this file if you'd prefer not to use a 8 | // service worker, and the Workbox build step will be skipped. 9 | 10 | import { clientsClaim } from "workbox-core"; 11 | import { ExpirationPlugin } from "workbox-expiration"; 12 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching"; 13 | import { registerRoute } from "workbox-routing"; 14 | import { StaleWhileRevalidate } from "workbox-strategies"; 15 | 16 | clientsClaim(); 17 | 18 | // Precache all of the assets generated by your build process. 19 | // Their URLs are injected into the manifest variable below. 20 | // This variable must be present somewhere in your service worker file, 21 | // even if you decide not to use precaching. See https://cra.link/PWA 22 | precacheAndRoute(self.__WB_MANIFEST); 23 | 24 | // Set up App Shell-style routing, so that all navigation requests 25 | // are fulfilled with your index.html shell. Learn more at 26 | // https://developers.google.com/web/fundamentals/architecture/app-shell 27 | const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$"); 28 | registerRoute( 29 | // Return false to exempt requests from being fulfilled by index.html. 30 | ({ request, url }) => { 31 | // If this isn't a navigation, skip. 32 | if (request.mode !== "navigate") { 33 | return false; 34 | } // If this is a URL that starts with /_, skip. 35 | 36 | if (url.pathname.startsWith("/_")) { 37 | return false; 38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip. 39 | 40 | if (url.pathname.match(fileExtensionRegexp)) { 41 | return false; 42 | } // Return true to signal that we want to use the handler. 43 | 44 | return true; 45 | }, 46 | createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html") 47 | ); 48 | 49 | // An example runtime caching route for requests that aren't handled by the 50 | // precache, in this case same-origin .png requests like those from in public/ 51 | registerRoute( 52 | // Add in any other file extensions or routing criteria as needed. 53 | ({ url }) => 54 | url.origin === self.location.origin && url.pathname.endsWith(".png"), // Customize this strategy as needed, e.g., by changing to CacheFirst. 55 | new StaleWhileRevalidate({ 56 | cacheName: "images", 57 | plugins: [ 58 | // Ensure that once this runtime cache reaches a maximum size the 59 | // least-recently used images are removed. 60 | new ExpirationPlugin({ maxEntries: 50 }), 61 | ], 62 | }) 63 | ); 64 | 65 | // This allows the web app to trigger skipWaiting via 66 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 67 | self.addEventListener("message", (event) => { 68 | if (event.data && event.data.type === "SKIP_WAITING") { 69 | self.skipWaiting(); 70 | } 71 | }); 72 | 73 | // Any other custom service worker logic can go here. 74 | -------------------------------------------------------------------------------- /backend/app/server/controllers/comment.py: -------------------------------------------------------------------------------- 1 | from bson.objectid import ObjectId 2 | from fastapi import HTTPException 3 | from ..database import comments_collection, users_collection 4 | 5 | # helpers 6 | 7 | 8 | # lightweight modes defined here due to circular dependencies 9 | # Only Likes and Comments APIs should return lightweight user details 10 | def user_helper_lightweight(user, comment) -> dict: 11 | if "post_id" in comment.keys(): 12 | return { 13 | "comment_id": str(comment["_id"]), 14 | "post_id": str(comment["post_id"]), 15 | "user_id": str(user["_id"]), 16 | "name": user["name"], 17 | "profile_picture": user["profile_picture"], 18 | "payload": comment["payload"], 19 | "is_commented": comment["is_commented"] 20 | } 21 | return { 22 | "comment_id": str(comment["_id"]), 23 | "user_id": str(user["_id"]), 24 | "name": user["name"], 25 | "profile_picture": user["profile_picture"], 26 | "payload": comment["payload"] 27 | } 28 | 29 | 30 | async def retrieve_user_lightweight(user_id: ObjectId, comment_id: ObjectId, comment_payload: str): 31 | user = await users_collection.find_one({"_id": user_id}) 32 | if user: 33 | return user_helper_lightweight(user, {"_id": comment_id, "payload": comment_payload}) 34 | 35 | 36 | async def get_all_comments_on_post(post_id: ObjectId): 37 | comments = [] 38 | async for comment in comments_collection.find({"post_id": post_id}, {"user_id", "payload", "is_commented"}): 39 | if comment["is_commented"] is True: 40 | comments.append(await retrieve_user_lightweight(comment["user_id"], comment["_id"], comment["payload"])) 41 | return comments 42 | 43 | 44 | async def initialize_comment(user_id: ObjectId, post_id: ObjectId, comment_details: dict) -> dict: 45 | comment_details["user_id"] = user_id 46 | comment_details["post_id"] = post_id 47 | comment_details["is_commented"] = True 48 | return comment_details 49 | 50 | 51 | # Add a post id to the comment model 52 | async def add_comment(email: str, comment_details: dict): 53 | user = await users_collection.find_one({"email": email}) 54 | # create entry 55 | comment_details = await initialize_comment(user["_id"], ObjectId(comment_details["post_id"]), comment_details) 56 | new_comment = await comments_collection.insert_one(comment_details) 57 | return_comment = await comments_collection.find_one({"_id": new_comment.inserted_id}) 58 | return user_helper_lightweight(user, return_comment) 59 | 60 | 61 | # Delete the post from comment model 62 | async def delete_comment(email: str, comment_id: str): 63 | user = await users_collection.find_one({"email": email}) 64 | comment = await comments_collection.find_one({"_id": ObjectId(comment_id)}) 65 | 66 | if comment and user["_id"] != comment["user_id"]: 67 | raise HTTPException(status_code=403, detail='User not authorized.') 68 | 69 | if comment["is_commented"] is True: 70 | updated_post = await comments_collection.update_one( 71 | {"_id": ObjectId(comment_id)}, {"$set": {"is_commented": False}} 72 | ) 73 | if updated_post: 74 | return True 75 | else: 76 | return False 77 | else: 78 | raise HTTPException(status_code=404, detail='Comment does not exist.') 79 | -------------------------------------------------------------------------------- /backend/app/server/routes/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, status, Depends 2 | from fastapi.encoders import jsonable_encoder 3 | from fastapi.security import OAuth2PasswordBearer 4 | from fastapi import UploadFile, File, Form 5 | from pydantic import EmailStr 6 | from ..config import config 7 | 8 | from ..controllers.user import ( 9 | add_user, 10 | login, 11 | delete_user, 12 | retrieve_user, 13 | update_user, 14 | get_current_user 15 | ) 16 | from ..controllers.post import retrieve_posts 17 | 18 | from ..controllers.upload import upload_image 19 | from ..controllers.auth import auth_handler 20 | from ..models.user import ( 21 | ResponseModel, 22 | LoginSchema, 23 | UpdateUserModel, 24 | ) 25 | router = APIRouter() 26 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") 27 | 28 | 29 | @router.post("/register", response_description="User data added into the database", status_code=status.HTTP_201_CREATED) 30 | async def add_user_data(name: str = Form(...), email: EmailStr = Form(...), password: str = Form(...), confirm_password: str = Form(...), description: str = Form(None), image: UploadFile = File(None)): 31 | image_url = upload_image(image) if image else config["DEFAULT_AVATAR"] 32 | user = { 33 | "name": name, 34 | "email": email, 35 | "password": password, 36 | "confirm_password": confirm_password, 37 | "description": description, 38 | "profile_picture": image_url 39 | } 40 | new_user = await add_user(user) 41 | return ResponseModel(new_user, "User added successfully.") 42 | 43 | 44 | @router.post("/login", response_description="User Logged in Successfully", status_code=status.HTTP_201_CREATED) 45 | async def user_login(user_details: LoginSchema = Body(...)): 46 | user_details = jsonable_encoder(user_details) 47 | login_response = await login(user_details) 48 | return ResponseModel(login_response, "User Logged in Successfully") 49 | 50 | 51 | @router.delete("/delete", response_description="Delete user from the database") 52 | async def delete_user_data(current_user=Depends(auth_handler.auth_wrapper)): 53 | new_user = await delete_user(current_user) 54 | return ResponseModel(new_user, "User deleted successfully.") 55 | 56 | 57 | @router.patch("/update", response_description="Update user from the database") 58 | async def update_user_data(updated_user: UpdateUserModel = Body(...), current_user=Depends(auth_handler.auth_wrapper)): 59 | updated_user = jsonable_encoder(updated_user) 60 | new_user = await update_user(current_user, updated_user) 61 | return ResponseModel(new_user, "User updated successfully.") 62 | 63 | 64 | @router.get("/details", response_description="Get user details from the database") 65 | async def details_user_data(user_id: str): 66 | new_user = await retrieve_user(user_id) 67 | return ResponseModel(new_user, "Got user details successfully.") 68 | 69 | 70 | @router.get("/current_user", response_description="Get current_user details from the database") 71 | async def current_user_data(current_user=Depends(auth_handler.auth_wrapper)): 72 | current_user = await get_current_user(current_user) 73 | return ResponseModel(current_user, "Current user details fetched successfully.") 74 | 75 | 76 | @router.get("/posts", response_description="Get all posts by user from the database") 77 | async def get_all_posts(user_id: str): 78 | all_posts = await retrieve_posts(user_id) 79 | return ResponseModel(all_posts, "Got all post details successfully.") 80 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/src/Components/UserFeed.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import axios from "axios"; 3 | import Post from "./Post/Post"; 4 | import CreatePost from "./Post/CreatePost"; 5 | import { AuthContext } from "../Context/AuthContext"; 6 | import { Loading } from "../Components/Common/Loader"; 7 | import { toast } from "react-toastify"; 8 | import { 9 | POST_GET_ALL_URL, 10 | USER_SUGGESTIONS_URL, 11 | POST_DELETE_URL, 12 | } from "../constants"; 13 | import Suggestions from "./Suggestions"; 14 | 15 | export default function UserFeed() { 16 | const { token } = useContext(AuthContext); 17 | const [access] = token; 18 | const [posts, setPosts] = useState([]); 19 | const [loading, setLoading] = useState(false); 20 | const [suggestionLoading, setSuggestionLoading] = useState(false); 21 | const [suggestedUsers, setSuggestedUsers] = useState([]); 22 | 23 | useEffect(() => { 24 | setLoading(true); 25 | axios 26 | .get(POST_GET_ALL_URL, { 27 | headers: { 28 | Authorization: "Bearer " + access, 29 | }, 30 | }) 31 | .then((res) => { 32 | setPosts(res.data.data.reverse()); 33 | setLoading(false); 34 | }); 35 | // eslint-disable-next-line 36 | }, []); 37 | 38 | useEffect(() => { 39 | setSuggestionLoading(true); 40 | axios.get(USER_SUGGESTIONS_URL).then((res) => { 41 | setSuggestedUsers(res.data.data); 42 | setSuggestionLoading(false); 43 | }); 44 | // eslint-disable-next-line 45 | }, []); 46 | 47 | const handleCreatePost = (new_post) => { 48 | const new_posts = [new_post, ...posts]; 49 | setPosts(new_posts); 50 | }; 51 | 52 | const handleDeletePost = (post_id) => { 53 | axios 54 | .delete(POST_DELETE_URL, { 55 | headers: { 56 | accept: "application/json", 57 | Authorization: "Bearer " + access, 58 | }, 59 | data: { post_id }, 60 | }) 61 | .then((res) => { 62 | toast.info(JSON.stringify(res.data.message)); 63 | const filtered_posts = posts.filter(function (el) { 64 | return el.post_id !== post_id; 65 | }); 66 | setPosts(filtered_posts); 67 | }) 68 | .catch(({ response }) => { 69 | if (response) { 70 | toast.error(JSON.stringify(response.data.detail)); 71 | } 72 | }); 73 | }; 74 | 75 | return ( 76 |
77 | {loading || suggestionLoading ? ( 78 | 79 | ) : ( 80 |
81 |
82 | 83 | {posts.map((post) => { 84 | return ( 85 | 90 | ); 91 | })} 92 |
93 |
94 |
95 |

Suggestions

96 |
97 | {suggestedUsers.map((user) => { 98 | return ; 99 | })} 100 |
101 |
102 | )} 103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /backend/app/server/controllers/user.py: -------------------------------------------------------------------------------- 1 | from bson.objectid import ObjectId 2 | from ..database import users_collection 3 | from fastapi import HTTPException, status 4 | from .auth import auth_handler 5 | from .post import retrieve_posts 6 | from .like import user_helper_lightweight 7 | 8 | # helpers 9 | 10 | 11 | def user_helper(user, posts=None) -> dict: 12 | if not posts: 13 | posts = [] 14 | 15 | return { 16 | "user_id": str(user["_id"]), 17 | "name": user["name"], 18 | "email": user["email"], 19 | "profile_picture": user["profile_picture"], 20 | "description": user["description"], 21 | "posts": posts 22 | } 23 | 24 | 25 | # Retrieve all users present in the database 26 | async def retrieve_users(lightweight: bool = False): 27 | users = [] 28 | async for user in users_collection.find(): 29 | if lightweight: 30 | users.append(user_helper_lightweight(user)) 31 | else: 32 | posts_by_user = await retrieve_posts(user["_id"]) 33 | users.append(user_helper(user, posts=posts_by_user)) 34 | return users 35 | 36 | 37 | # Add a new user into to the database 38 | async def add_user(user_data: dict) -> dict: 39 | if await users_collection.find_one({"email": user_data['email']}): 40 | raise HTTPException(status_code=409, detail='Account already exists with the email') 41 | if user_data["password"] != user_data["confirm_password"]: 42 | raise HTTPException(status_code=422, detail='Password and Confirm Password do not match') 43 | hashed_password = auth_handler.get_password_hash(user_data["password"]) 44 | user_data["password"] = hashed_password 45 | del user_data["confirm_password"] 46 | user = await users_collection.insert_one(user_data) 47 | new_user = await users_collection.find_one({"_id": user.inserted_id}) 48 | return user_helper(new_user) 49 | 50 | 51 | # Login Functionality 52 | async def login(user_data: dict) -> dict: 53 | getUser = await users_collection.find_one({"email": user_data["email"]}) 54 | if (getUser is None) or (not auth_handler.verify_password(user_data["password"], getUser["password"])): 55 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid Credentials') 56 | token = auth_handler.create_access_token(str(getUser["email"])) 57 | return {"access_token": token, "token_type": "bearer"} 58 | 59 | 60 | # Retrieve a user with a matching ID 61 | async def retrieve_user(id: str) -> dict: 62 | user = await users_collection.find_one({"_id": ObjectId(id)}) 63 | if user: 64 | posts_by_user = await retrieve_posts(user["_id"]) 65 | return user_helper(user, posts=posts_by_user) 66 | 67 | 68 | # Retrieve details of current_user 69 | async def get_current_user(email: str) -> dict: 70 | user = await users_collection.find_one({"email": email}) 71 | if user: 72 | posts_by_user = await retrieve_posts(user["_id"]) 73 | return user_helper(user, posts=posts_by_user) 74 | 75 | 76 | # Update a user with a matching ID 77 | async def update_user(email: str, data: dict): 78 | # Return false if an empty request body is sent. 79 | if len(data) < 1: 80 | return False 81 | user = await users_collection.find_one({"email": email}) 82 | if user: 83 | updated_user = await users_collection.update_one( 84 | {"email": email}, {"$set": data} 85 | ) 86 | if updated_user: 87 | return True 88 | return False 89 | 90 | 91 | # Delete a user from the database 92 | async def delete_user(email: str): 93 | user = await users_collection.find_one({"email": email}) 94 | if user: 95 | await users_collection.delete_one({"email": email}) 96 | return True 97 | -------------------------------------------------------------------------------- /frontend/src/Components/Post/CreatePost.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import axios from "axios"; 3 | import { toast } from "react-toastify"; 4 | import { Loading } from "../Common/Loader"; 5 | import { AuthContext } from "../../Context/AuthContext"; 6 | import { POST_CREATE_URL } from "../../constants"; 7 | 8 | export default function CreatePost({ handleCreatePost }) { 9 | const [description, setDescription] = useState(""); 10 | const [file, setFile] = useState({ fileUpload: null }); 11 | const { token } = useContext(AuthContext); 12 | const [access] = token; 13 | const [loading, setLoading] = useState(false); 14 | 15 | const handleChange = (e) => { 16 | setDescription(e.target.value); 17 | }; 18 | 19 | const handleFileUpload = (e) => { 20 | const file = e.target.files[0]; 21 | setFile({ 22 | fileUpload: file, 23 | }); 24 | }; 25 | 26 | const handleSubmit = (e) => { 27 | e.preventDefault(); 28 | setLoading(true); 29 | var bodyFormData = new FormData(); 30 | bodyFormData.append("description", description); 31 | if (file.fileUpload) { 32 | bodyFormData.append("image", file.fileUpload); 33 | } 34 | axios 35 | .post(POST_CREATE_URL, bodyFormData, { 36 | headers: { 37 | accept: "application/json", 38 | "Content-Type": "multipart/form-data", 39 | Authorization: "Bearer " + access, 40 | }, 41 | }) 42 | .then((res) => { 43 | setDescription(""); 44 | setLoading(false); 45 | toast.success(JSON.stringify(res.data.message)); 46 | handleCreatePost(res.data.data); 47 | }) 48 | .catch(({ response }) => { 49 | if (response) { 50 | toast.error(JSON.stringify(response.data.detail)); 51 | } 52 | setLoading(false); 53 | }); 54 | }; 55 | 56 | return ( 57 |
58 | {loading ? ( 59 | 60 | ) : ( 61 |
62 |
63 |
67 |