├── backend ├── src │ ├── __init__.py │ ├── models │ │ ├── books.py │ │ ├── __init__.py │ │ └── user.py │ ├── config │ │ ├── __init__.py │ │ ├── storage.py │ │ ├── database.py │ │ └── settings.py │ ├── routes │ │ ├── __init__.py │ │ ├── books.py │ │ ├── user.py │ │ └── auth.py │ ├── utils.py │ ├── main.py │ └── oauth2.py ├── Dockerfile └── requirements.txt ├── frontend ├── src │ ├── index.css │ ├── setupTests.js │ ├── index.js │ ├── App.css │ ├── App.js │ ├── logo.svg │ ├── LoginForm.jsx │ └── RegisterForm.jsx ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── .gitignore ├── Dockerfile ├── package.json └── README.md ├── envExample ├── README.md ├── docker-compose.yml └── .gitignore /backend/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/models/books.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/config/storage.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/routes/books.py: -------------------------------------------------------------------------------- 1 | books.py -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipecolladavid/FastAPI-ReactJS-MongoDB-JWT/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipecolladavid/FastAPI-ReactJS-MongoDB-JWT/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipecolladavid/FastAPI-ReactJS-MongoDB-JWT/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | WORKDIR /app 4 | 5 | COPY ./requirements.txt /app/requirements.txt 6 | 7 | RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt 8 | 9 | COPY ./src /app/src 10 | 11 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] 12 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import 'bootstrap/dist/css/bootstrap.min.css'; 5 | import App from './App'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /envExample: -------------------------------------------------------------------------------- 1 | MONGO_INITDB_ROOT_USERNAME=admin 2 | MONGO_INITDB_ROOT_PASSWORD=password 3 | MONGO_INITDB_DATABASE=dbname 4 | 5 | DATABASE_URL=mongodb://admin:password@mongo:27017/dbname?authSource=admin 6 | 7 | ACCESS_TOKEN_EXPIRES_IN=15 8 | REFRESH_TOKEN_EXPIRES_IN=60 9 | JWT_ALGORITHM=RS256 10 | 11 | CLIENT_ORIGIN=http://localhost:3000 12 | 13 | JWT_PRIVATE_KEY= GENERATE RESPECTIVE PRIVATE KEY 14 | JWT_PUBLIC_KEY= GENERATE PUBLIC KEY 15 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | # Pull the base image 3 | FROM node:13.12.0-alpine 4 | # Set the working directory 5 | WORKDIR /react-docker 6 | # Copy app dependencies to container 7 | COPY ./package*.json ./ 8 | # Add `/app/node_modules/.bin` to $PATH 9 | ENV PATH /app/node_modules/.bin:$PATH 10 | # Install dependencies 11 | RUN npm install 12 | # Deploy app for local development 13 | CMD npm start --host 0.0.0.0 --port 3000 --disableHostCheck true -------------------------------------------------------------------------------- /backend/src/config/database.py: -------------------------------------------------------------------------------- 1 | from .settings import settings 2 | from ..models.user import User 3 | 4 | from motor.motor_asyncio import AsyncIOMotorClient 5 | 6 | from beanie import init_beanie 7 | 8 | 9 | # Call this from within your event loop to get beanie setup. 10 | async def startDB(): 11 | # Create Motor client 12 | client = AsyncIOMotorClient(settings.DATABASE_URL) 13 | 14 | # Init beanie with the Product document class 15 | await init_beanie(database=client.db_name, document_models=[User]) -------------------------------------------------------------------------------- /backend/src/routes/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | 3 | from ..models.user import User, UserResponse 4 | from .. import oauth2 5 | 6 | router = APIRouter() 7 | 8 | 9 | @router.get('/me', response_model=UserResponse) 10 | async def get_me(user_id: str = Depends(oauth2.require_user)): 11 | 12 | user = await User.get(str(user_id)) 13 | r_user = UserResponse( 14 | username=user.username, 15 | email=user.email, 16 | pic_url=str(user.pic_url) 17 | ) 18 | return r_user 19 | -------------------------------------------------------------------------------- /backend/src/utils.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | import re 3 | 4 | 5 | def is_valid_email(email: str) -> bool: 6 | pat = "^[a-zA-Z0-9-_]+@[a-zA-Z0-9]+\.[a-z]{1,3}$" 7 | if re.match(pat, email): 8 | return True 9 | return False 10 | 11 | 12 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 13 | 14 | 15 | def hash_password(password: str): 16 | return pwd_context.hash(password) 17 | 18 | 19 | def verify_password(password: str, hashed_password: str): 20 | return pwd_context.verify(password, hashed_password) 21 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/config/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseSettings 2 | import os 3 | 4 | class Settings(BaseSettings): 5 | DATABASE_URL: str = os.environ["DATABASE_URL"] 6 | MONGO_INITDB_DATABASE: str = os.environ["MONGO_INITDB_DATABASE"] 7 | 8 | JWT_PUBLIC_KEY: str = os.environ["JWT_PUBLIC_KEY"] 9 | JWT_PRIVATE_KEY: str = os.environ["JWT_PRIVATE_KEY"] 10 | REFRESH_TOKEN_EXPIRES_IN: int = os.environ["REFRESH_TOKEN_EXPIRES_IN"] 11 | ACCESS_TOKEN_EXPIRES_IN: int = os.environ["ACCESS_TOKEN_EXPIRES_IN"] 12 | JWT_ALGORITHM: str = os.environ["JWT_ALGORITHM"] 13 | 14 | CLIENT_ORIGIN: str = os.environ["CLIENT_ORIGIN"] 15 | 16 | settings = Settings() -------------------------------------------------------------------------------- /backend/src/models/user.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pydantic import BaseModel 3 | from typing import Optional 4 | from beanie import Document 5 | from src import utils 6 | 7 | class Register(BaseModel): 8 | username: str 9 | email: str 10 | password: str 11 | 12 | 13 | class Login(BaseModel): 14 | username: str 15 | password: str 16 | 17 | 18 | class UserResponse(BaseModel): 19 | username: str 20 | email: str 21 | pic_url: str 22 | 23 | # This is the model that will be saved to the database 24 | 25 | 26 | class User(Document): 27 | username: str 28 | email: str 29 | password: str 30 | created_at: Optional[datetime] = None 31 | pic_url: Optional[str] = None 32 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from src.config.settings import settings 5 | from src.config.database import startDB 6 | 7 | from src.routes import auth, user 8 | 9 | app = FastAPI() 10 | 11 | origins = [ 12 | settings.CLIENT_ORIGIN, 13 | ] 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 | @app.on_event("startup") 25 | async def start_dependencies(): 26 | await startDB() 27 | # await startMinio() 28 | 29 | 30 | app.include_router(auth.router, tags=['Auth'], prefix='/api/auth') 31 | app.include_router(user.router, tags=['Users'], prefix='/api/users') 32 | 33 | 34 | @app.get("/api/healthchecker") 35 | def root(): 36 | return {"message": "Welcome to FastAPI with MongoDB"} 37 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.6.2 2 | autopep8==2.0.1 3 | bcrypt==4.0.1 4 | beanie==1.17.0 5 | certifi==2022.12.7 6 | cffi==1.15.1 7 | click==8.1.3 8 | cryptography==3.4.8 9 | dnspython==2.3.0 10 | email-validator==1.3.1 11 | fastapi==0.92.0 12 | fastapi-jwt-auth==0.5.0 13 | h11==0.14.0 14 | httpcore==0.16.3 15 | httptools==0.5.0 16 | httpx==0.23.3 17 | idna==3.4 18 | itsdangerous==2.1.2 19 | Jinja2==3.1.2 20 | lazy-model==0.0.5 21 | MarkupSafe==2.1.2 22 | motor==3.1.1 23 | orjson==3.8.6 24 | passlib==1.7.4 25 | pycodestyle==2.10.0 26 | pycparser==2.21 27 | pydantic==1.10.5 28 | PyJWT==1.7.1 29 | pymongo==4.3.3 30 | python-dotenv==1.0.0 31 | python-multipart==0.0.5 32 | PyYAML==6.0 33 | rfc3986==1.5.0 34 | six==1.16.0 35 | sniffio==1.3.0 36 | starlette==0.25.0 37 | toml==0.10.2 38 | tomli==2.0.1 39 | typing_extensions==4.5.0 40 | ujson==5.7.0 41 | uvicorn==0.20.0 42 | uvloop==0.17.0 43 | watchfiles==0.18.1 44 | websockets==10.4 45 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "bootstrap": "^5.2.3", 10 | "react": "^18.2.0", 11 | "react-bootstrap": "^2.7.2", 12 | "react-dom": "^18.2.0", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Template: FastAPI - ReactJS - MongoDB with JWT Authentication 2 | 3 | Template for starting a project with FARM stack. Dockerized, ready for deployment 4 | 5 | ## Development 6 | 7 | 1. Go to the directory where the frontend folder is located and run 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 2. Go back to the root of the project and start the container 13 | 14 | ```bash 15 | docker-compose up 16 | ``` 17 | 18 | Both the frontend and the backend have auto-reload enabled, open the project on your IDE of choice and start development. 19 | 20 | ## Environment Variables 21 | 22 | To run this project, you will need to add the following environment variables to your .env file 23 | 24 | MongoDB 25 | 26 | `MONGO_INITDB_ROOT_USERNAME` 27 | 28 | `MONGO_INITDB_ROOT_PASSWORD` 29 | 30 | `MONGO_INITDB_DATABASE` 31 | 32 | `DATABASE_URL` 33 | 34 | 35 | JWT 36 | 37 | `ACCESS_TOKEN_EXPIRES_IN` 38 | 39 | `REFRESH_TOKEN_EXPIRES_IN` 40 | 41 | `JWT_ALGORITHM` 42 | 43 | `JWT_PRIVATE_KEY` 44 | 45 | `JWT_PUBLIC_KEY` 46 | 47 | CORS bypass for origin 48 | 49 | `CLIENT_ORIGIN` 50 | 51 | (See envExample) 52 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mongo: 4 | image: mongo:latest 5 | container_name: mongo 6 | env_file: 7 | - ./.env 8 | environment: 9 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME} 10 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} 11 | MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE} 12 | volumes: 13 | - mongo:/data/db 14 | ports: 15 | - '6000:27017' 16 | 17 | backend: 18 | build: ./backend 19 | container_name: backend 20 | env_file: 21 | - ./.env 22 | command: uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload 23 | ports: 24 | - 8000:8000 25 | volumes: 26 | - ./backend:/app/ 27 | depends_on: 28 | - mongo 29 | 30 | frontend: 31 | build: 32 | context: ./frontend 33 | dockerfile: Dockerfile 34 | volumes: 35 | - ./frontend:/react-docker:delegated 36 | - /node_modules 37 | ports: 38 | - 3000:3000 39 | environment: 40 | - CHOKIDAR_USEPOLLING=true 41 | depends_on: 42 | - mongo 43 | - backend 44 | 45 | volumes: 46 | mongo: 47 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import "./App.css"; 3 | import { Container, Spinner } from "react-bootstrap"; 4 | import RegisterForm from "./RegisterForm"; 5 | import LoginForm from "./LoginForm"; 6 | 7 | export const BASEURL = "http://0.0.0.0:8000/api/"; 8 | 9 | function App() { 10 | const [loading, setLoading] = useState(false); 11 | const [register, setRegister] = useState(true); 12 | const [auth, setAuth] = useState(false); 13 | const [response, setResponse] = useState(null); 14 | 15 | return ( 16 | 17 | {auth ? ( 18 | <>Authenticated 19 | ) : loading ? ( 20 | 21 | Loading... 22 | 23 | ) : register ? ( 24 | 29 | ) : ( 30 | <> 31 | 37 | {response && <>{response}} 38 | 39 | )} 40 | 41 | ); 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /backend/src/oauth2.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from typing import List 3 | from fastapi import Depends, HTTPException, status 4 | from fastapi_jwt_auth import AuthJWT 5 | from pydantic import BaseModel 6 | 7 | from .models.user import User 8 | 9 | from .config.settings import settings 10 | 11 | class Settings(BaseModel): 12 | authjwt_algorithm: str = settings.JWT_ALGORITHM 13 | authjwt_decode_algorithms: List[str] = [settings.JWT_ALGORITHM] 14 | authjwt_token_location: set = {'cookies', 'headers'} 15 | authjwt_access_cookie_key: str = 'access_token' 16 | authjwt_refresh_cookie_key: str = 'refresh_token' 17 | authjwt_cookie_csrf_protect: bool = False 18 | authjwt_public_key: str = base64.b64decode( 19 | settings.JWT_PUBLIC_KEY).decode('utf-8') 20 | authjwt_private_key: str = base64.b64decode( 21 | settings.JWT_PRIVATE_KEY).decode('utf-8') 22 | 23 | 24 | @AuthJWT.load_config 25 | def get_config(): 26 | return Settings() 27 | 28 | class NotVerified(Exception): 29 | pass 30 | 31 | 32 | class UserNotFound(Exception): 33 | pass 34 | 35 | 36 | async def require_user(Authorize: AuthJWT = Depends()): 37 | try: 38 | Authorize.jwt_required() 39 | user_id = Authorize.get_jwt_subject() 40 | 41 | user = await User.get(str(user_id)) 42 | 43 | if not user: 44 | raise UserNotFound('User no longer exist') 45 | 46 | # if not user["verified"]: 47 | # raise NotVerified('You are not verified') 48 | 49 | except Exception as e: 50 | error = e.__class__.__name__ 51 | print(e) 52 | if error == 'MissingTokenError': 53 | raise HTTPException( 54 | status_code=status.HTTP_401_UNAUTHORIZED, detail='You are not logged in') 55 | if error == 'UserNotFound': 56 | raise HTTPException( 57 | status_code=status.HTTP_401_UNAUTHORIZED, detail='User no longer exist') 58 | if error == 'NotVerified': 59 | raise HTTPException( 60 | status_code=status.HTTP_401_UNAUTHORIZED, detail='Please verify your account') 61 | raise HTTPException( 62 | status_code=status.HTTP_401_UNAUTHORIZED, detail='Token is invalid or has expired') 63 | return user_id -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Button, Form } from "react-bootstrap"; 3 | 4 | import { BASEURL } from "./App"; 5 | 6 | const LoginForm = ({ setRegister, setLoading, setResponse, setAuth }) => { 7 | 8 | const [values, setValues] = useState(null); 9 | const [errorMessage, setErrorMessage] = useState(null); 10 | const [usernameNotAvail, setUsernameNotAvail] = useState(false); 11 | 12 | const [validated, setValidated] = useState(false); 13 | 14 | const onFormChange = (e) => { 15 | const name = e.target.name; 16 | const value = e.target.value; 17 | setValues({ ...values, [name]: value }); 18 | }; 19 | 20 | const onSubmit = (event) => { 21 | 22 | event.preventDefault(); 23 | 24 | const form = event.currentTarget; 25 | if (form.checkValidity() === false) { 26 | event.preventDefault(); 27 | event.stopPropagation(); 28 | } 29 | else { 30 | setErrorMessage(null); 31 | setLoading(true); 32 | fetch(BASEURL + 'auth/login', { 33 | method: 'POST', 34 | headers: { 35 | 'accept': 'application/json', 36 | 'Content-Type': 'application/json' 37 | }, 38 | body: JSON.stringify(values) 39 | }).then((response => { 40 | if (!response.ok) { 41 | if (response.status === 400) { 42 | setUsernameNotAvail(true); 43 | setValidated(false); 44 | throw new Error("Username or password wrong"); 45 | } 46 | if (response.status === 404) { 47 | setUsernameNotAvail(true); 48 | throw new Error("Not found"); 49 | } 50 | else throw new Error("Something went wrong"); 51 | } 52 | return response.json(); 53 | })).then((data) => { 54 | setRegister(false); 55 | setResponse(data); 56 | setAuth(true); 57 | setValidated(true); 58 | }).catch((err) => { 59 | setErrorMessage(err.message); 60 | }) 61 | setLoading(false); 62 | } 63 | } 64 | 65 | return ( 66 |
67 |

Login

68 | 69 | Username 70 | 77 | {errorMessage} 78 | 79 | 80 | Password 81 | 89 | {errorMessage} 90 | 91 | 92 | 93 | 94 | 95 | {errorMessage} 96 |
97 | ); 98 | } 99 | export default LoginForm; 100 | -------------------------------------------------------------------------------- /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 your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may 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/RegisterForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Form, Button } from "react-bootstrap"; 3 | import { BASEURL } from "./App"; 4 | const RegisterForm = ({ setRegister, setLoading, setResponse }) => { 5 | 6 | const [values, setValues] = useState(null); 7 | const [errorMessage, setErrorMessage] = useState(null); 8 | const [mailInvalid, setMailInvalid] = useState(false); 9 | const [usernameNotAvail, setUsernameNotAvail] = useState(false); 10 | 11 | const [validated, setValidated] = useState(false); 12 | 13 | const onFormChange = (e) => { 14 | const name = e.target.name; 15 | const value = e.target.value; 16 | setValues({ ...values, [name]: value }); 17 | }; 18 | 19 | const onSubmit = (event) => { 20 | 21 | event.preventDefault(); 22 | 23 | const form = event.currentTarget; 24 | if (form.checkValidity() === false) { 25 | event.preventDefault(); 26 | event.stopPropagation(); 27 | } 28 | else { 29 | setMailInvalid(false); 30 | setErrorMessage(null); 31 | setLoading(true); 32 | fetch(BASEURL + 'auth/register', { 33 | method: 'POST', 34 | headers: { 35 | 'accept': 'application/json', 36 | 'Content-Type': 'application/json' 37 | }, 38 | body: JSON.stringify(values) 39 | }).then((response => { 40 | if (!response.ok) { 41 | if (response.status === 400) { 42 | setMailInvalid(true); 43 | setValidated(false); 44 | throw new Error("Invalid Email"); 45 | } 46 | if (response.status === 409) { 47 | setUsernameNotAvail(true); 48 | throw new Error("Username or email already registred"); 49 | } 50 | else throw new Error("Something went wrong"); 51 | } 52 | return response.json(); 53 | })).then((data) => { 54 | setRegister(false); 55 | setResponse(data.username + " registred with success"); 56 | }).catch((err) => { 57 | setErrorMessage(err.message); 58 | }) 59 | setLoading(false); 60 | } 61 | } 62 | 63 | return ( 64 |
65 |

Register

66 | 67 | Username 68 | 75 | Invalid username 76 | 77 | 78 | Email 79 | 86 | Invalid email 87 | 88 | 89 | Password 90 | 91 | 92 | 93 | 94 | 95 | 96 | {errorMessage} 97 |
98 | ); 99 | } 100 | 101 | export default RegisterForm; 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | # End of https://www.toptal.com/developers/gitignore/api/python -------------------------------------------------------------------------------- /backend/src/routes/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from bson.objectid import ObjectId 3 | from fastapi import APIRouter, Response, status, Depends, HTTPException 4 | 5 | from src import oauth2 6 | from ..models.user import User, Login, Register, UserResponse 7 | from .. import utils 8 | from src.oauth2 import AuthJWT 9 | from ..config.settings import settings 10 | 11 | 12 | router = APIRouter() 13 | ACCESS_TOKEN_EXPIRES_IN = settings.ACCESS_TOKEN_EXPIRES_IN 14 | REFRESH_TOKEN_EXPIRES_IN = settings.REFRESH_TOKEN_EXPIRES_IN 15 | 16 | 17 | # Register new User - to be removed if webapp is private 18 | @router.post('/register', status_code=status.HTTP_201_CREATED, response_model=UserResponse) 19 | async def create_user(credentials: Register): 20 | 21 | if not utils.is_valid_email(credentials.email): 22 | raise HTTPException( 23 | status_code=status.HTTP_400_BAD_REQUEST, 24 | detail='Invalid email' 25 | ) 26 | 27 | user_exists = await User.find_one(User.username == credentials.username) 28 | email_exists = await User.find_one(User.email == credentials.email) 29 | if user_exists or email_exists: 30 | raise HTTPException( 31 | status_code=status.HTTP_409_CONFLICT, 32 | detail='Account already exists' 33 | ) 34 | 35 | new_user = User( 36 | username=credentials.username, 37 | email=credentials.email.lower(), 38 | password=utils.hash_password(credentials.password), 39 | created_at=datetime.utcnow() 40 | ) 41 | 42 | await new_user.create() 43 | 44 | r_user = UserResponse( 45 | username=new_user.username, 46 | email=new_user.email, 47 | pic_url=str(new_user.pic_url) 48 | ) 49 | 50 | return r_user 51 | 52 | 53 | # Sign In user 54 | @router.post('/login') 55 | async def login(credentials: Login, response: Response, Authorize: AuthJWT = Depends()): 56 | user = await User.find_one(User.username == credentials.username) 57 | 58 | if not user: 59 | raise HTTPException( 60 | status_code=status.HTTP_404_NOT_FOUND, 61 | detail='User not found' 62 | ) 63 | 64 | if not utils.verify_password(credentials.password, user.password): 65 | raise HTTPException( 66 | status_code=status.HTTP_400_BAD_REQUEST, 67 | detail='Incorrect username or password' 68 | ) 69 | 70 | # Create access token 71 | 72 | access_token = Authorize.create_access_token( 73 | subject=str(user.id), 74 | expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN) 75 | ) 76 | 77 | refresh_token = Authorize.create_refresh_token( 78 | subject=str(user.id), 79 | expires_time=timedelta(minutes=REFRESH_TOKEN_EXPIRES_IN) 80 | ) 81 | 82 | # Store refresh and access tokens in cookie 83 | response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60, 84 | ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') 85 | response.set_cookie('refresh_token', refresh_token, 86 | REFRESH_TOKEN_EXPIRES_IN * 60, REFRESH_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') 87 | response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60, 88 | ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax') 89 | 90 | # Send both access 91 | return {'status': 'success', 'access_token': access_token} 92 | 93 | 94 | # Refresh Acess Token 95 | @router.get('/refresh') 96 | async def refresh_token(response: Response, Authorize: AuthJWT = Depends()): 97 | try: 98 | Authorize.jwt_refresh_token_required() 99 | 100 | user_id = Authorize.get_jwt_subject() 101 | 102 | if not user_id: 103 | raise HTTPException( 104 | status_code=status.HTTP_401_UNAUTHORIZED, 105 | detail='Could not refresh access token' 106 | ) 107 | 108 | user = await User.get(user_id) 109 | 110 | if not user: 111 | raise HTTPException( 112 | status_code=status.HTTP_401_UNAUTHORIZED, 113 | detail='The user belonging to this token no logger exist' 114 | ) 115 | access_token = Authorize.create_access_token( 116 | subject=str(user.id), 117 | expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN) 118 | ) 119 | except Exception as e: 120 | error = e.__class__.__name__ 121 | if error == 'MissingTokenError': 122 | raise HTTPException( 123 | status_code=status.HTTP_400_BAD_REQUEST, 124 | detail='Please provide refresh token' 125 | ) 126 | raise HTTPException( 127 | status_code=status.HTTP_400_BAD_REQUEST, 128 | detail=error 129 | ) 130 | 131 | response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60, 132 | ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') 133 | response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60, 134 | ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax') 135 | 136 | return {'access_token': access_token} 137 | 138 | 139 | # Logout user 140 | @router.get('/logout', status_code=status.HTTP_200_OK) 141 | def logout(response: Response, Authorize: AuthJWT = Depends(), user_id: str = Depends(oauth2.require_user)): 142 | Authorize.unset_jwt_cookies() 143 | response.set_cookie('logged_in', '', -1) 144 | 145 | return {'status': 'success'} 146 | --------------------------------------------------------------------------------