├── 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 |
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 |
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 |
--------------------------------------------------------------------------------