Skip to content
Home » Building a Full-Stack Authentication Module with FastAPI and React

Building a Full-Stack Authentication Module with FastAPI and React

As someone who has led data engineering teams through the evolution from traditional ETL to modern AI-powered data platforms, I’ve observed a clear shift in what makes data professionals truly effective in today’s landscape. See the article on The Modern Data Stack: Why Data and AI Engineers Should Master FastAPI and React

Data and AI engineers who can build complete solutions that include API endpoints with a frontend for user engagement and a robust authentication system bring tremendous value to organizations.

In this tutorial, we will build a full-stack authentication module using FastAPI for the backend and React for the frontend. This project includes user authentication, role-based access control (RBAC), and profile management with support for uploading profile photos. By the end of this guide, you will have a fully functional authentication module that can be extended for real-world applications. This can be used as a starter template that you can extend for your use case. A link to the repository with the full code implementation is included at the end of the tutorial.


Project Overview

Features

  1. User Authentication: Login, signup, and token-based authentication.
  2. Role-Based Access Control: Differentiate between admin and regular users.
  3. Profile Management: Users can update their profile, including uploading profile photos and adding a profile summary.
  4. Dark Mode Support: Users can toggle between light and dark themes.
  5. Modern UI: Built with React, TailwindCSS, and React Router.

Tech Stack

  • Backend: FastAPI, SQLAlchemy, Alembic, PostgreSQL.
  • Frontend: React, TailwindCSS, React Router, Axios.
  • Database: PostgreSQL.
  • Deployment: Docker Compose for containerized services.

Step 1: Setting Up the Backend

1.1 Install Dependencies

Create a Python virtual environment and install the required dependencies

python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -r backend/requirements.txt

1.2 Database Configuration

Set up the PostgreSQL database in docker-compose.yml:

services:
  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=auth_module
      - POSTGRES_USER=auth_admin
      - POSTGRES_PASSWORD=auth12345
    ports:
      - "5435:5435"

Start the database service:

docker-compose up -d

1.3 Backend Directory Structure

The backend is organized as follows:

backend/
    app/
        __init__.py
        auth.py
        crud.py
        database.py
        main.py
        models.py
        schemas.py
    migrations/
    requirements.txt

1.4 Models and Database

Define the User model in models.py:

from sqlalchemy import Column, Integer, String, Text
from .database import Base

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    role = Column(String, default="user")
    email = Column(String, unique=True, nullable=True)
    profile_photo = Column(String, nullable=True)
    profile_summary = Column(Text, nullable=True)

Run migrations to create the database schema:

cd backend
alembic revision --autogenerate -m "Initial migration"
alembic upgrade head

1.5 Authentication

Implement JWT-based authentication in auth.py:

from passlib.context import CryptContext
from jose import jwt
from datetime import datetime, timedelta

SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def get_password_hash(password):
    return pwd_context.hash(password)

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def create_access_token(data: dict, expires_delta: timedelta = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

1.6 Profile Photo Upload

Add an endpoint in main.py to handle profile updates, including photo uploads:

@app.put("/update-profile", response_model=schemas.UserOut)
async def update_profile(
    username: str = None,
    email: str = None,
    profile_summary: str = None,
    profile_photo: UploadFile = File(None),
    db: AsyncSession = Depends(get_db),
    current_user: models.User = Depends(get_current_user)
):
    user = await crud.get_user_by_username(db, current_user.username)
    if username:
        user.username = username
    if email:
        user.email = email
    if profile_summary:
        user.profile_summary = profile_summary
    if profile_photo:
        file_location = f"uploads/{profile_photo.filename}"
        with open(file_location, "wb") as file:
            file.write(await profile_photo.read())
        user.profile_photo = file_location
    await db.commit()
    return user

Step 2: Setting Up the Frontend

2.1 Install Dependencies

Navigate to the auth_module directory and install dependencies:

npm install @babel/runtime axios jwt-decode prop-types react react-avatar react-dom react-router-dom react-toastify

2.2 Directory Structure

The frontend is organized as follows:

frontend/auth_module/
├── src/
│   ├── components/
│   │   ├── Layout.jsx       # Layout component for consistent UI
│   │   ├── ProtectedRoute.jsx # Component to protect routes based on authentication
│   ├── pages/
│   │   ├── Login.jsx          # Login page
│   │   ├── Dashboard.jsx     # Dashboard page
│   │   ├── AdminDashboard.jsx  # Admin dashboard page
│   │   ├── UserProfile.jsx    # User profile page
│   ├── services/
│   │   ├── AuthService.jsx     # Authentication-related API calls
│   │   ├── AdminService.jsx    # Admin-related API calls
│   ├── App.jsx                 # Main application component
│   ├── main.jsx                # Entry point for the React app
│   ├── index.css               # Global styles
│   └── tailwind.config.js      # TailwindCSS configuration
├── index.html                  # HTML template
├── package.json                # Project dependencies and scripts
└── vite.config.js              # Vite configuration

2.3 User Profile Component

Create the UserProfile.jsx component to display and update user profiles:

// src/pages/UserProfile.jsx
import { useState, useEffect } from "react";
import axios from "axios";
import { toast } from "react-toastify";
import Avatar from "react-avatar";

function UserProfile() {
    const [username, setUsername] = useState("");
    const [email, setEmail] = useState("");
    const [profilePhoto, setProfilePhoto] = useState("");
    const [profileSummary, setProfileSummary] = useState("");
    const [selectedFile, setSelectedFile] = useState(null);

    useEffect(() => {
        async function fetchProfile() {
            const token = localStorage.getItem("access_token");
            try {
                const response = await axios.get("http://localhost:8000/profile", {
                    headers: { Authorization: `Bearer ${token}` },
                });
                const { username, email, profile_photo, profile_summary } = response.data;
                setUsername(username);
                setEmail(email);
                setProfilePhoto(profile_photo);
                setProfileSummary(profile_summary);
            } catch (error) {
                console.error("Failed to fetch profile:", error);
                toast.error("Failed to load profile.");
            }
        }
        fetchProfile();
    }, []);

    async function handleSubmit(e) {
        e.preventDefault();
        const token = localStorage.getItem("access_token");
        const formData = new FormData();
        formData.append("username", username);
        formData.append("email", email);
        formData.append("profile_summary", profileSummary);
        if (selectedFile) {
            formData.append("profile_photo", selectedFile);
        }
        await axios.put("http://localhost:8000/update-profile", formData, {
            headers: {
                Authorization: `Bearer ${token}`,
                "Content-Type": "multipart/form-data",
            },
        });
        toast.success("Profile updated successfully!");
    }

    return (
        <div className="flex items-center justify-center min-h-screen bg-background dark:bg-neutral">
            <div className="card w-full max-w-md">
                <h1 className="text-2xl font-bold mb-6 text-center text-primary">
                    Update Profile
                </h1>
                <div className="flex justify-center mb-4">
                    {profilePhoto ? (
                        <img
                            src={profilePhoto}
                            alt="Profile"
                            className="w-24 h-24 rounded-full"
                        />
                    ) : (
                        <Avatar
                            name={username || "User"}
                            size="96"
                            round={true}
                            className="w-24 h-24"
                        />
                    )}
                </div>
                <form onSubmit={handleSubmit} className="space-y-4">
                    <div>
                        <label className="block text-secondary font-medium mb-1">
                            Username
                        </label>
                        <input
                            type="text"
                            value={username || ""}
                            onChange={(e) => setUsername(e.target.value)}
                            className="w-full p-2 border rounded text-primary focus:outline-none focus:ring-2 focus:ring-accent"
                        />
                    </div>
                    <div>
                        <label className="block text-secondary font-medium mb-1">
                            Email
                        </label>
                        <input
                            type="email"
                            value={email || ""}
                            onChange={(e) => setEmail(e.target.value)}
                            className="w-full p-2 border rounded text-primary focus:outline-none focus:ring-2 focus:ring-accent"
                        />
                    </div>
                    <div>
                        <label className="block text-secondary font-medium mb-1">
                            Profile Photo
                        </label>
                        <input
                            type="file"
                            accept="image/*"
                            onChange={(e) => setSelectedFile(e.target.files[0])}
                            className="w-full p-2 border rounded text-primary dark:text-darkText focus:outline-none focus:ring-2 focus:ring-accent"
                        />
                    </div>
                    <div>
                        <label className="block text-secondary font-medium mb-1">
                            Profile Summary
                        </label>
                        <textarea
                            value={profileSummary || ""}
                            onChange={(e) => setProfileSummary(e.target.value)}
                            className="w-full p-2 border rounded text-primary focus:outline-none focus:ring-2 focus:ring-accent"
                        />
                    </div>
                    <button
                        type="submit"
                        className="w-full bg-primary text-white py-2 rounded hover:bg-secondary"
                    >
                        Save Changes
                    </button>
                </form>
            </div>
        </div>
    );
}

export default UserProfile;

Step 3: Styling with TailwindCSS

Add TailwindCSS to the project for modern styling. Update tailwind.config.js:

module.exports = {
  content: ["./src/**/*.{js,jsx}"],
  theme: {
    extend: {
      colors: {
        primary: "#1C3F60",
        secondary: "#007BFF",
        background: "#E9F1FA",
        card: "#FDFEFF",
        neutral: "#4A5568",
        success: "#00D97E",
        warning: "#FFB547",
        cta: "#6B46C1",
      },
    },
  },
};

Step 4: Running the Application

4.1 Start the Backend

Run the FastAPI backend:

cd backend
uvicorn app.main:app --reload

4.2 Start the Frontend

Run the React frontend:

cd frontend/auth_module
npm run dev

Conclusion

Congratulations! You’ve built a full-stack authentication system with FastAPI and React. This project is a great starting point for building scalable, secure, and modern web applications. You can extend this project by adding features like email verification, password reset, or advanced role-based access control.

For the full project check out the repository on GitHub:

Starter Project Repository


Discover more from The Data Lead

Subscribe to get the latest posts sent to your email.