Role-Based Auth with FastAPI and MySQL
Build a secure login system with JWT auth and role-based dashboards.
Introduction
30 Second Summary
Think about how a school gym coordinator needs to log in and see their event schedule, while students only see upcoming events they can join. That separation of access is what makes shared systems usable.
In this project, you will build the authentication layer of a gymnasium event management system using FastAPI, MySQL, and Tailwind CSS. Users will register, choose a role, log in securely, and land on a dashboard that matches their permissions.
What You'll Build
When you finish, you can open your app, register a new user, log in, and watch the system route you straight to the correct dashboard for your role.
By the end of this project, you'll have:
- A registration form where new users create an account and choose their role (organizer or student).
- A login system that issues a JWT token stored in a cookie, keeping users signed in across page loads.
- Three role-based dashboards (organizer, admin, student) with route protection that bounces unauthorized users back to login.
- Secret Mission: Add a logout button and an admin panel that lists all registered users with their roles.
Are there any prerequisites?
You need Python and MySQL installed locally, plus basic Python knowledge. Step 1 walks you through the virtual environment and package setup.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
You are building the authentication backbone of a gymnasium event management system. This is the part that makes everything else possible: without knowing who a user is and what they are allowed to do, you cannot build event creation, approval workflows, or personalized views.
Set Up the Project and Dependencies
Every web application starts with its foundation: the environment that runs your code and the libraries that give your code superpowers.
In this step, you will create the project folder, install the Python packages your app needs, write a tiny test app, and set up the MySQL database that will store user accounts.
In this step, get ready to:
- Create the project folder structure and a Python virtual environment with all dependencies installed.
- Create a minimal FastAPI app and confirm it responds in the browser.
- Create the MySQL database that will store user data.
Create the project folder and virtual environment
Your project needs its own isolated folder and a virtual environment so that installed packages do not interfere with other Python projects on your machine.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder with its subdirectories and move into it by running these commands:
mkdir gymnasium_auth
cd gymnasium_auth
mkdir templates static
- Confirm the folders exist by running:
ls
You should see templates and static listed in the output.
- Create a Python virtual environment by running:
python -m venv venv
- Activate the virtual environment. The command depends on your operating system:
macOS
- Activate by running:
source venv/bin/activate
Windows
- Activate by running:
venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Not seeing (venv) in your prompt?
On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try activating again.
On macOS, make sure you typed source at the start. Without it, the command will not work.
- Install all the project dependencies by running:
pip install fastapi==0.139.2 uvicorn==0.51.0 sqlalchemy==2.0.51 pymysql==1.2.0 "python-jose[cryptography]==3.5.0" "passlib[argon2]==1.7.4" python-multipart==0.0.18 jinja2==3.1.6
After a few seconds, you should see Successfully installed followed by a list of package names at the end of the output.
What are all these packages for?
- fastapi is the web framework that handles HTTP requests and responses.
- uvicorn is the server that actually runs your FastAPI app.
- sqlalchemy and pymysql let your Python code talk to the MySQL database.
- python-jose handles JWT tokens for authentication.
- passlib securely hashes passwords with the Argon2 algorithm.
- python-multipart allows FastAPI to read HTML form data.
- jinja2 is the template engine that renders HTML pages.
Seeing installation errors?
Make sure your virtual environment is active (you should see (venv) in your prompt). If you see permission errors, never use sudo pip install. Deactivate and recreate the venv instead.
If argon2-cffi fails to build, make sure you have a C compiler installed. On macOS, run xcode-select --install. On Windows, install the Visual C++ Build Tools.
help me fix pip install errors for passlib argon2
Create a minimal FastAPI app
With packages installed, you can write a tiny FastAPI app that proves everything is wired up correctly.
- Open VS Code. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the gymnasium_auth folder, then click Open.
- Create a new file by clicking File then New File.
- Save it immediately as main.py in the gymnasium_auth folder by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Copy the following code into main.py:
from fastapi import FastAPI
# Create the FastAPI application instance
app = FastAPI()
# A simple test route to confirm the server is running
@app.get("/")
def root():
return {"message": "Hello Gymnasium"}
What does this code do?
- from fastapi import FastAPI imports the framework so you can create a web application.
- app = FastAPI() creates the application object that handles all incoming requests.
- @app.get("/") registers the function below it to respond when someone visits the root URL.
- The function returns a Python dictionary, which FastAPI automatically converts to a JSON response.
- Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Switch back to your terminal where the virtual environment is active.
Before you run the server, what do you think you will see when you visit the URL in a browser? Will it show HTML or something else?
- Start the Uvicorn development server by running:
python -m uvicorn main:app --reload
You should see output including Uvicorn running on http://127.0.0.1:8000 in your terminal.
- Open your browser and navigate to http://127.0.0.1:8000.
You should see the JSON response {"message":"Hello Gymnasium"} displayed in your browser. That is your FastAPI app running and responding.
Not seeing the JSON response?
If you see ModuleNotFoundError: No module named 'fastapi', your virtual environment is not active. Check for (venv) in your terminal prompt. If it is missing, activate it again and rerun the command.
If you see Error loading ASGI app, make sure you are running the command from inside the gymnasium_auth folder where main.py lives.
help me fix uvicorn not starting for my FastAPI app
✔️ Awesome, I've got everything!
Great. Make sure you have saved main.py and leave the Uvicorn server running in your terminal.
ⓧ I'd like to double check the full code
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"message": "Hello Gymnasium"}
Create the MySQL database
Your app will store user accounts in a MySQL database. You need to create that database now so it is ready when you connect to it in the next step.
- Open a new terminal window or tab. Your Uvicorn server is using the current terminal, so you need a separate one.
- Open the MySQL shell by running:
mysql -u root -p
You will be prompted to enter your MySQL root password. Type it and press Enter. The characters will not appear as you type.
- Create the database by running this SQL command:
CREATE DATABASE gymnasium_auth;
- Confirm the database was created by running:
SHOW DATABASES;
You should see gymnasium_auth in the list of databases.
Access denied or command not found?
If you see Access denied for user 'root', double-check the password you set when you installed MySQL. If you never set one, try pressing Enter with no password.
If you see command not found: mysql, MySQL is not in your system PATH. On macOS, try /usr/local/mysql/bin/mysql -u root -p. On Windows, check that the MySQL bin directory is added to your PATH environment variable.
help me connect to my MySQL shell
- Exit the MySQL shell by running:
EXIT;
Your environment is fully ready. You have a running FastAPI server, all your dependencies installed, and a MySQL database waiting for data. Next up, you will connect your app to that database and create the User model.
Connect to MySQL and Create the User Model
Your FastAPI app is running and responding to requests. Now it is time to give it persistent memory.
Without a database connection and a User table, you have nowhere to store user accounts. This step connects your app to MySQL through SQLAlchemy and defines the schema that holds usernames, emails, hashed passwords, and roles.
In this step, get ready to:
- Connect to MySQL using SQLAlchemy.
- Define a User model with role support.
- Create the users table and verify it in MySQL.
Create the database connection file
SQLAlchemy needs three things to talk to your MySQL database: a connection URL that tells it where the database lives, a session factory to manage individual conversations with the database, and a base class that your models inherit from.
- Create a new file called database.py in your gymnasium_auth/ folder by running this command:
touch database.py
- Open database.py in VS Code and paste the following code:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
# Connection URL: tells SQLAlchemy which database driver, user, password, host, and database name to use
DATABASE_URL = "mysql+pymysql://root:your-password-here@localhost:3306/gymnasium_auth"
# Create the engine that manages the actual database connection
engine = create_engine(DATABASE_URL, echo=False)
# Session factory: each request gets its own session to interact with the database
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base class that all your models will inherit from
Base = declarative_base()
def get_db():
# Dependency generator: opens a session, yields it, then closes it when done
db = SessionLocal()
try:
yield db
finally:
db.close()
What does this code do?
DATABASE_URL tells SQLAlchemy to use the PyMySQL driver to connect as root on localhost:3306 to the gymnasium_auth database you created in Step 1.
The get_db() function is a dependency generator. FastAPI calls it before each request to open a database session, then closes the session automatically after the response is sent. This prevents leaked connections.
Replace the password placeholder
Replace your-password-here in the DATABASE_URL string with your actual MySQL root password. If your root user has no password, use an empty string: root:@localhost.
- Save database.py.
- Confirm the file was created by running:
ls
You should see database.py listed alongside main.py, templates/, and static/.
Don't see database.py?
Make sure your terminal is inside the gymnasium_auth/ folder. Run pwd to check your current directory.
help me find my database.py file
✔️ Awesome, I've got everything!
Great. Make sure you saved the file before moving on.
ⓧ I'd like to double check the full code
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
DATABASE_URL = "mysql+pymysql://root:your-password-here@localhost:3306/gymnasium_auth"
engine = create_engine(DATABASE_URL, echo=False)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Define the User model
An ORM model maps a Python class to a database table. Each attribute on the class becomes a column in the table.
- Create a new file called models.py in your gymnasium_auth/ folder by running:
touch models.py
- Open models.py in VS Code and paste the following code:
from sqlalchemy import Column, Integer, String
from database import Base
class User(Base):
# The name of the actual MySQL table
__tablename__ = "users"
# Primary key with auto-incrementing ID
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
# Unique username limited to 100 characters
username = Column(String(100), unique=True, index=True, nullable=False)
# Unique email limited to 255 characters
email = Column(String(255), unique=True, index=True, nullable=False)
# Stores the hashed password (never plain text)
hashed_password = Column(String(500), nullable=False)
# Role determines access level: organizer, admin, or student
role = Column(String(20), nullable=False, default="student")
What does this code do?
- User inherits from Base (defined in database.py), which registers it as a table SQLAlchemy can create.
- unique=True on username and email prevents duplicate accounts at the database level.
- hashed_password is String(500) because Argon2 hashes are longer than typical bcrypt hashes.
- role defaults to "student" so every new user gets the lowest-privilege role unless specified otherwise.
- Save models.py.
- Verify that both files import correctly by running this quick check:
python -c "from models import User; print(User.__tablename__)"
You should see users printed in the terminal. This confirms models.py can import Base from database.py and the model is properly defined.
Seeing a ModuleNotFoundError?
If you see ModuleNotFoundError: No module named 'database', make sure you are running the command from inside the gymnasium_auth/ folder where both database.py and models.py live.
If you see ModuleNotFoundError: No module named 'sqlalchemy', your virtual environment may not be active. Activate it again before retrying.
help me fix this import error in models.py
✔️ Awesome, I've got everything!
Great. Make sure you saved the file before moving on.
ⓧ I'd like to double check the full code
from sqlalchemy import Column, Integer, String
from database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
username = Column(String(100), unique=True, index=True, nullable=False)
email = Column(String(255), unique=True, index=True, nullable=False)
hashed_password = Column(String(500), nullable=False)
role = Column(String(20), nullable=False, default="student")
Add a setup-db endpoint and create the table
Your model defines the table shape, but MySQL does not know about it yet. You need an endpoint that tells SQLAlchemy to create all registered tables in the database.
- Open main.py in VS Code.
- Find the line from fastapi import FastAPI at the top of the file.
- Add this import directly below it:
from database import engine, Base
- Then add this new endpoint at the bottom of the file, below the existing root() function:
@app.get("/setup-db")
def setup_db():
# Tell SQLAlchemy to create all tables registered with Base
Base.metadata.create_all(bind=engine)
return {"message": "Tables created successfully"}
What does this code do?
Base.metadata.create_all(bind=engine) scans every class that inherits from Base (your User model) and creates the corresponding table in MySQL if it does not already exist.
This is a temporary endpoint for quick setup. In later steps you will replace it with automatic table creation on startup.
Why import engine and Base here?
Importing models.py is not needed in main.py yet. SQLAlchemy discovers the User model automatically because User inherits from Base. As long as models.py has been imported anywhere in the process before create_all runs, the table gets created. Python's module loading handles this when uvicorn starts.
- Save main.py.
Before you run this, what do you expect to see when you visit /setup-db in the browser?
- Start the server by running:
python -m uvicorn main:app --reload
- Open your browser and go to http://127.0.0.1:8000/setup-db.
You should see the JSON response:
{"message": "Tables created successfully"}
- Now verify the table was actually created in MySQL. Open a new terminal and launch the MySQL shell by running:
mysql -u root -p
- Enter your MySQL root password when prompted.
- Switch to your database and describe the table by running:
USE gymnasium_auth;
DESCRIBE users;
You should see five columns listed: id, username, email, hashed_password, and role. This confirms SQLAlchemy translated your Python model into a real MySQL table.
Seeing a connection refused error?
If the browser shows a connection error when you visit /setup-db, double check that the password in your DATABASE_URL inside database.py matches your actual MySQL root password.
Also confirm your MySQL server is running. On macOS, check System Preferences > MySQL. On Windows, check Services for the MySQL service status.
help me debug my SQLAlchemy database connection error
✔️ Awesome, I've got everything!
Great. Double check you have saved both database.py and main.py before moving on.
ⓧ I'd like to double check the full code
Here is the full main.py at this point:
from fastapi import FastAPI
from database import engine, Base
app = FastAPI()
@app.get("/")
def root():
return {"message": "Hello Gymnasium"}
@app.get("/setup-db")
def setup_db():
Base.metadata.create_all(bind=engine)
return {"message": "Tables created successfully"}
Your app now has persistent memory. Next up, you will build a registration page so users can create accounts and pick their role.
Build the Registration Page
Your database has a users table ready, but there is no way for people to actually create accounts.
Registration is the front door to your app. In this step, you will build a password hashing module, a styled registration form, and the backend routes that validate input and save new users to MySQL.
In this step, get ready to:
- Create password hashing utilities with Argon2.
- Build a styled registration form with Tailwind CSS.
- Add registration routes that validate input and save users.
Create the password hashing module
Storing passwords in plain text is a critical security flaw. Instead, you will hash passwords before saving them so that even if the database leaks, the original passwords remain protected.
- Create a new file called auth.py in your gymnasium_auth/ folder and add the following code:
from passlib.context import CryptContext
# Set up Argon2 as the hashing algorithm
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
def hash_password(password: str) -> str:
# Turn a plain-text password into a secure hash
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
# Check a plain-text password against a stored hash
return pwd_context.verify(plain_password, hashed_password)
What does this code do?
This module wraps passlib's CryptContext to give you two clean functions for the rest of the app.
- hash_password takes a plain string and returns an Argon2 hash. This is a one-way operation.
- verify_password compares a plain password against a stored hash and returns True or False.
- Save auth.py.
- Verify the module works by running this command from your gymnasium_auth/ folder:
python -c "from auth import hash_password; print(hash_password('test'))"
You should see a long string starting with $argon2 printed to the terminal.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is activated. You should see the (venv) prefix in your terminal prompt.
- Confirm you are running the command from inside the gymnasium_auth/ folder where auth.py lives.
Need more help? Why is my auth.py import failing?
✔️ Awesome, I've got everything!
Great. Double check you have saved auth.py and move on to building the registration form.
ⓧ I'd like to double check the full code
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
Create the registration template
Your registration page needs an HTML form styled with Tailwind CSS. You will load Tailwind from a CDN so there is no build step required.
- Create a new file called register.html inside your templates/ folder.
- Add the page structure with the Tailwind CDN, error and success message areas, and the form opening tag:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - Gymnasium</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
<h1 class="text-2xl font-bold text-center mb-6">Create Account</h1>
{% if error %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{{ error }}
</div>
{% endif %}
{% if success %}
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ success }}
</div>
{% endif %}
<form method="POST" action="/register">
What does this code do?
- The <script> tag loads Tailwind CSS directly from a CDN. No npm install needed.
- The {% if error %} and {% if success %} blocks are Jinja2 conditionals. They show colored alert boxes only when the server passes an error or success message.
- The form posts to /register so FastAPI can handle the submission on the server side.
- Now add the form fields, submit button, and closing tags directly below the <form> opening tag:
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">Username</label>
<input type="text" id="username" name="username" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="email">Email</label>
<input type="email" id="email" name="email" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">Password</label>
<input type="password" id="password" name="password" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="role">Role</label>
<select id="role" name="role" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
<option value="organizer">Organizer</option>
<option value="student">Student</option>
</select>
</div>
<button type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline w-full">
Register
</button>
</form>
<p class="text-center text-gray-600 text-sm mt-4">
Already have an account? <a href="/login" class="text-blue-500 hover:text-blue-700">Log in</a>
</p>
</div>
</body>
</html>
What do these form fields do?
- Four inputs collect the username, email, password, and role. Each has a name attribute that FastAPI reads as form data.
- The role field is a <select> dropdown restricted to "organizer" and "student". Admins cannot self-register.
- The required attribute on each field prevents the browser from submitting empty values.
- Save templates/register.html.
Seeing a syntax error in the HTML?
- Make sure the opening <form> tag from the first chunk connects directly to the form fields in the second chunk. There should be no gap or duplicate tags.
- Check that every <div> has a matching </div> and that the final </html> tag is present.
Need help? My register.html has a rendering issue. Can you help me spot the problem?
✔️ Awesome, I've got everything!
Great. Double check you have saved templates/register.html and move on to wiring up the routes.
ⓧ I'd like to double check the full code
Your register.html is made of both chunks above combined. Scroll up and confirm you have the page structure (the first block ending at <form method="POST" action="/register">) followed immediately by the form fields (the second block starting with the username <div> and ending with </html>). Both blocks live in the same file with no gap between them.
Add the registration routes
The template and hashing module are ready. Now you need routes in main.py to serve the form and handle submissions.
- Replace the entire contents of main.py with the following imports, app setup, and GET route:
from fastapi import FastAPI, Request, Depends, Form
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import engine, get_db, Base
from models import User
from auth import hash_password
app = FastAPI()
# Point Jinja2 at the templates folder
templates = Jinja2Templates(directory="templates")
@app.get("/setup-db")
def setup_db():
Base.metadata.create_all(bind=engine)
return {"message": "Tables created successfully"}
@app.get("/register", response_class=HTMLResponse)
async def register_page(request: Request):
# Render the blank registration form
return templates.TemplateResponse(request=request, name="register.html", context={"error": None})
What changed in main.py?
- The imports now include Request, Depends, Form, HTMLResponse, and Jinja2Templates. These let FastAPI render HTML and read form data.
- Jinja2Templates(directory="templates") tells FastAPI where to find your HTML files.
- The GET /register route renders register.html with no error message. This is what users see when they first visit the page.
- The old / root route is removed since you no longer need the JSON hello-world.
- Save main.py and restart the server by running:
python -m uvicorn main:app --reload
- Open http://127.0.0.1:8000/register in your browser.
You should see a clean white card in the center of the page with a "Create Account" heading, four form fields (Username, Email, Password, Role), and a blue Register button.
Seeing a TemplateNotFoundError?
- Confirm your file is at templates/register.html (not template/register.html without the 's').
- Make sure you are running uvicorn from the gymnasium_auth/ folder where the templates/ directory lives.
Still stuck? Help me fix my TemplateNotFoundError in FastAPI.
The form displays but does not do anything when you click Register yet. Time to add the POST route that processes submissions.
- In main.py, add the following POST route below your GET /register route. This is the first half of the function, handling input validation:
@app.post("/register", response_class=HTMLResponse)
async def register(
request: Request,
username: str = Form(...),
email: str = Form(...),
password: str = Form(...),
role: str = Form(...),
db: Session = Depends(get_db)
):
# Check if username or email is already taken
existing_user = db.query(User).filter(
(User.username == username) | (User.email == email)
).first()
if existing_user:
return templates.TemplateResponse(
request=request,
name="register.html",
context={"error": "Username or email already exists"}
)
if role not in ("organizer", "student"):
return templates.TemplateResponse(
request=request,
name="register.html",
context={"error": "Invalid role selected"}
)
What does the validation logic do?
- Form(...) extracts each field from the submitted form data. The ... means the field is required.
- Depends(get_db) injects a database session. FastAPI calls your get_db generator automatically.
- The query checks if any existing user already has the same username OR email. If so, it re-renders the form with an error message.
- The role check ensures only "organizer" or "student" are accepted. This prevents someone from injecting "admin" via the form.
- Still inside the same register function, add the user creation logic directly below the role check (at the same indentation level):
# Hash the password and save the new user
new_user = User(
username=username,
email=email,
hashed_password=hash_password(password),
role=role
)
db.add(new_user)
db.commit()
return templates.TemplateResponse(
request=request,
name="register.html",
context={"error": None, "success": "Account created! You can now log in."}
)
What does the user creation logic do?
- hash_password(password) turns the plain-text password into an Argon2 hash before storing it.
- db.add() stages the new user and db.commit() writes it to MySQL.
- On success, the form re-renders with a green success message instead of an error.
- Save main.py. If you used --reload, uvicorn restarts automatically.
Before you test, what do you think will happen when you submit the form with a username, email, password, and the "organizer" role selected?
- Go back to http://127.0.0.1:8000/register in your browser.
- Enter testuser for username.
- Enter test@example.com for email.
- Enter password123 for password.
- Select Organizer from the role dropdown.
- Click Register.
You should see a green banner that says "Account created! You can now log in." at the top of the form.
- Confirm the user was saved to MySQL by running this in a new terminal:
mysql -u root -p -e "SELECT username, role FROM gymnasium_auth.users;"
You should see testuser with role organizer in the output.
Form submits but no success message appears?
- Check the terminal running uvicorn for error messages. A common cause is a typo in the DATABASE_URL in database.py (wrong password or database name).
- Verify your users table exists by running SHOW TABLES; in the MySQL shell for the gymnasium_auth database.
- Make sure the POST route's indentation places the user creation logic inside the register function (4 spaces in from the function body).
Still stuck? My registration form submits but I don't see a success message or database entry. Help me debug.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and confirmed the user in MySQL.
ⓧ I'd like to double check the full code
Your main.py is built from the chunks above. Scroll up and confirm you have:
- The imports, app setup, /setup-db route, and GET /register route (first code block).
- The POST /register function signature and validation logic (second code block).
- The user creation and commit logic continuing inside the same function (third code block).
All three blocks belong in one file with no blank lines between the validation section and the user creation section.
Your app now has a working registration flow. Users can create accounts, and their passwords are securely hashed before hitting the database. Next up, you will build the login page that lets these users prove who they are with JWT authentication.
Build the Login Page with JWT Authentication
You have a registration form that saves users to the database. But registration alone does not help anyone get into the app. Try visiting a dashboard URL directly and you get nothing useful.
Users need to prove who they are before the server shows them anything personalized. This step adds a login page that issues a JWT token stored in a browser cookie. The cookie travels with every subsequent request so the server remembers the user across page loads.
In this step, get ready to:
- Add JWT token creation and decoding functions to auth.py.
- Create a Tailwind-styled login page with username and password fields.
- Wire up the POST login route that verifies credentials, mints a token, and redirects users.
Add JWT token creation to auth.py
A JSON Web Token (JWT) is a compact, signed string the server generates after a successful login. It encodes who the user is and what role they hold, so the server can verify identity on later requests without querying the database every time.
- In auth.py, find the existing import line at the top:
from passlib.context import CryptContext
- Replace that single line with these three imports and configuration constants:
from datetime import datetime, timedelta, timezone
from passlib.context import CryptContext
from jose import jwt, JWTError
SECRET_KEY = "your-secret-key-here"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
What do these constants do?
- SECRET_KEY is the private string used to sign tokens. Anyone with this key can forge tokens, so in production you would load it from an environment variable.
- ALGORITHM specifies HS256 (HMAC-SHA256), a symmetric signing algorithm.
- ACCESS_TOKEN_EXPIRE_MINUTES sets tokens to expire after 30 minutes, forcing re-authentication.
- Below your existing verify_password function, add the create_access_token function by pasting this code:
def create_access_token(data: dict) -> str:
# Copy the input so we don't mutate the original
to_encode = data.copy()
# Set the token to expire 30 minutes from now
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
# Sign and return the JWT string
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
What does this code do?
This function accepts a dictionary (containing the username and role), stamps it with an expiration time, and signs it into a JWT string. The token can be decoded later to recover the original data without a database query.
- Save auth.py and confirm the new import works by running this in your terminal:
python -c "from auth import create_access_token; print('Token creation ready')"
You should see Token creation ready printed with no errors.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is activated. You should see (venv) or similar at the start of your terminal prompt.
- Confirm python-jose is installed by running pip list | grep jose. If it is missing, re-run pip install "python-jose[cryptography]==3.5.0".
- Help me debug a ModuleNotFoundError for jose in my FastAPI project.
- Now add the decode_access_token function below create_access_token by pasting this code:
def decode_access_token(token: str):
try:
# Decode and verify the token signature and expiration
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
role: str = payload.get("role")
if username is None:
return None
return {"sub": username, "role": role}
except JWTError:
return None
What does this code do?
This function takes a token string, verifies its signature and expiration, then extracts the sub (username) and role claims. If anything is invalid or expired, it returns None instead of raising an error.
- Save auth.py and verify both functions load correctly:
python -c "from auth import create_access_token, decode_access_token; print('Both JWT functions ready')"
You should see Both JWT functions ready with no errors.
✔️ Awesome, I've got everything!
Great. Double check you have saved auth.py.
ⓧ I'd like to double check the full code
Your auth.py should contain these sections in order: the three import lines, the three constants, pwd_context, hash_password, verify_password, create_access_token, and decode_access_token. Scroll up and confirm each one is present in that order.
Create the login template and route
Your login page needs two fields (username and password) and a form that POSTs to /login. It follows the same Tailwind styling pattern you used in the registration template.
- Create a new file templates/login.html and paste this page structure into it:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Gymnasium</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
<h1 class="text-2xl font-bold text-center mb-6">Log In</h1>
{% if error %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{{ error }}
</div>
{% endif %}
What does this code do?
- The Tailwind CDN script loads styling without needing a build step.
- The {% if error %} block conditionally shows a red error banner when login fails.
- The card layout (white background, rounded, shadow) matches your registration page.
- Directly below the code you just pasted (still in templates/login.html), add the form and closing tags:
<form method="POST" action="/login">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">Username</label>
<input type="text" id="username" name="username" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">Password</label>
<input type="password" id="password" name="password" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<button type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline w-full">
Log In
</button>
</form>
<p class="text-center text-gray-600 text-sm mt-4">
No account? <a href="/register" class="text-blue-500 hover:text-blue-700">Register here</a>
</p>
</div>
</body>
</html>
What does this code do?
- The form POSTs to /login with username and password fields.
- The password input uses type="password" to mask characters as the user types.
- A link at the bottom sends new users to the registration page.
- Save templates/login.html.
- Confirm the file exists by running:
ls templates/
You should see both login.html and register.html listed.
Now you need a route in main.py to serve this template. First, update the imports to include RedirectResponse (for the POST route later) and the new auth functions.
- In main.py, find this line:
from fastapi.responses import HTMLResponse
- Replace it with:
from fastapi.responses import HTMLResponse, RedirectResponse
- Now find this import line:
from auth import hash_password
- Replace it with:
from auth import hash_password, verify_password, create_access_token
- Next, delete the temporary /setup-db route. Find and remove this entire block:
@app.get("/setup-db")
def setup_db():
Base.metadata.create_all(bind=engine)
return {"message": "Tables created successfully"}
- Below the POST /register route, add the GET /login route by pasting this code:
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
return templates.TemplateResponse(request=request, name="login.html", context={"error": None})
What does this code do?
This route renders the login template with no error message by default. When the learner visits /login in the browser, they see the clean login form.
- Save main.py and restart your uvicorn server:
python -m uvicorn main:app --reload
- Open http://127.0.0.1:8000/login in your browser.
You should see a centered white card with a "Log In" heading, username and password fields, and a blue submit button.
Seeing a 500 error or blank page?
- Check your terminal for the error traceback. A TemplateNotFound error means login.html is not in the templates/ folder. Confirm the file name is exactly login.html (lowercase).
- If you see an import error, double check that your updated import line in main.py has no typos.
- Help me fix a 500 error when loading my FastAPI login page template.
Add the POST /login route
The login form submits a username and password to POST /login. The route needs to look up the user, verify their password, mint a JWT token, and redirect them to their role-specific dashboard.
- In main.py, below the GET /login route you just added, paste this POST route:
@app.post("/login")
async def login(
request: Request,
username: str = Form(...),
password: str = Form(...),
db: Session = Depends(get_db)
):
# Look up the user by username
user = db.query(User).filter(User.username == username).first()
# Check credentials
if not user or not verify_password(password, user.hashed_password):
return templates.TemplateResponse(
request=request,
name="login.html",
context={"error": "Invalid username or password"}
)
# Create a JWT with the username and role
token = create_access_token(data={"sub": user.username, "role": user.role})
# Redirect to the user's dashboard
response = RedirectResponse(url=f"/dashboard/{user.role}", status_code=303)
# Store the token in an HTTP-only cookie
response.set_cookie(key="access_token", value=token, httponly=True)
return response
What does this code do?
- It queries the database for a user matching the submitted username.
- If the user does not exist or the password hash does not match, it re-renders the login page with an error message.
- On success, it creates a JWT containing sub (username) and role claims, stores it in an HTTP-only cookie named access_token, and redirects to /dashboard/{role}.
- The httponly=True flag prevents JavaScript from reading the cookie, adding a layer of security against XSS attacks.
- Save main.py. Uvicorn auto-reloads since you started it with --reload.
Before you test, think about what will happen. You are logging in as the organizer user you registered in the previous step. The login route redirects to /dashboard/organizer. Do you think that page exists yet?
- Open http://127.0.0.1:8000/login in your browser.
- Enter the username and password of the test organizer you registered in Step 3.
- Click Log In.
Your browser redirects to /dashboard/organizer and you see a JSON response: {"detail":"Not Found"}. That 404 is expected. The dashboard route does not exist yet. What matters is that the redirect happened, proving your login logic and JWT cookie are working correctly.
Why is a 404 a success here?
The login route did its job: it verified the password, created a signed token, stored it in a cookie, and redirected to the correct URL. The missing dashboard page is the problem the next step solves.
Not getting redirected?
- If you see "Invalid username or password" on the login page, double check you are using the exact username and password you registered with. Passwords are case-sensitive.
- If the page reloads without any error message, confirm your POST route path is /login and the form's action attribute also points to /login.
- Help me debug why my FastAPI login form is not redirecting after submission.
✔️ Awesome, I've got everything!
Great. Double check you have saved both auth.py and main.py.
ⓧ I'd like to double check the full code
Your main.py should have the updated imports at the top, the register routes, then the GET and POST /login routes. The /setup-db route should be gone. Scroll up through your file and confirm each section is present in order.
Your auth.py should have: three import lines, three constants, pwd_context, hash_password, verify_password, create_access_token, decode_access_token. Confirm each is present in that order.
Your login flow is working. The token is being minted and stored. Next up, you will build the dashboards that each role lands on and add route protection so users cannot sneak into pages that do not belong to them.
Add Role-Based Dashboards with Route Protection
Your login form works. Users can authenticate and the server issues a JWT cookie. But right now, the cookie just redirects into a void.
Anyone who knows a dashboard URL could type it into the browser and land wherever they want. This step adds the guard that reads the JWT, extracts the user's role, and only allows access if it matches the requested dashboard.
In this step, get ready to:
- Create a get_current_user helper that reads the JWT cookie and identifies the logged-in user.
- Build three Tailwind-styled dashboard templates for organizer, admin, and student roles.
- Wire up protected routes that enforce role matching and redirect unauthorized users.
Create the user helper and seed an admin account
Before building dashboard pages, you need a way to identify who is making each request. You also need an admin user in the database for testing.
- In main.py, find the import lines at the top of the file:
from fastapi import FastAPI, Request, Depends, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import engine, get_db, Base
from models import User
from auth import hash_password, verify_password, create_access_token
- Replace those imports with this updated block that adds HTTPException, StaticFiles, and decode_access_token:
from fastapi import FastAPI, Request, Depends, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import engine, get_db, Base
from models import User
from auth import hash_password, verify_password, create_access_token, decode_access_token
- Next, add the startup event handler below the templates = Jinja2Templates(directory="templates") line. This function runs every time the server starts, creates database tables, and seeds a default admin user:
@app.on_event("startup")
def on_startup():
# Create all database tables on server start
Base.metadata.create_all(bind=engine)
from database import SessionLocal
db = SessionLocal()
try:
# Seed a default admin user if one doesn't exist
admin = db.query(User).filter(User.username == "admin").first()
if not admin:
admin_user = User(
username="admin",
email="admin@gymnasium.local",
hashed_password=hash_password("admin123"),
role="admin"
)
db.add(admin_user)
db.commit()
finally:
db.close()
What does this code do?
The @app.on_event("startup") decorator tells FastAPI to run this function once when the server boots up.
It creates the database tables (idempotent, so safe to call repeatedly) and checks if an admin user already exists. If not, it inserts one with username admin and a hashed password. This gives you an admin account for testing without needing to register one manually.
- Now add the get_current_user helper function and a home route below the on_startup function:
def get_current_user(request: Request):
# Read JWT from the cookie and decode it
token = request.cookies.get("access_token")
if not token:
return None
payload = decode_access_token(token)
if payload is None:
return None
return payload
@app.get("/", response_class=HTMLResponse)
async def home():
return RedirectResponse(url="/login")
What does get_current_user do?
This function reads the access_token cookie from the incoming request. If the cookie is missing or the token is invalid/expired, it returns None. Otherwise it returns a dictionary with the username and role extracted from the JWT.
The home route at / simply redirects visitors to the login page.
- Save main.py.
- Restart your server by pressing Ctrl+C in the terminal, then run:
python -m uvicorn main:app --reload
- Open http://127.0.0.1:8000 in your browser. You should be redirected to the login page.
- Verify the admin user was seeded by switching to a new terminal tab and running:
mysql -u root -p -e "SELECT username, role FROM users WHERE username='admin';" gymnasium_auth
You should see a row with username admin and role admin.
Don't see the admin user?
Make sure you saved main.py before restarting the server. The on_startup function only runs when uvicorn starts.
Check your terminal for import errors. If you see ImportError, confirm the decode_access_token import was added to the auth import line.
Still stuck? Help me debug why my on_startup function isn't seeding the admin user.
Create the three dashboard templates
Each role gets its own dashboard page with a colored nav bar, a welcome message, and placeholder content for future features.
- Create a new file templates/dashboard_organizer.html with this content:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Organizer Dashboard - Gymnasium</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<nav class="bg-blue-600 text-white p-4 flex justify-between items-center">
<h1 class="text-xl font-bold">Gymnasium - Organizer</h1>
<div class="flex items-center gap-4">
<span>Welcome, {{ username }}</span>
<form method="POST" action="/logout">
<button type="submit" class="bg-red-500 hover:bg-red-700 text-white px-3 py-1 rounded text-sm">Logout</button>
</form>
</div>
</nav>
<main class="max-w-4xl mx-auto mt-8 p-4">
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-4">Your Events</h2>
<p class="text-gray-500">Your events will appear here. Event creation is coming in Project 2.</p>
</div>
</main>
</body>
</html>
What does this template do?
The organizer dashboard has a blue nav bar with the user's name (injected via {{ username }}) and a logout button. The main area shows placeholder text for future event management features.
Each dashboard uses a different nav bar color so you can instantly tell which role you are logged in as.
- Save templates/dashboard_organizer.html.
- Create a new file templates/dashboard_admin.html with this content:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - Gymnasium</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<nav class="bg-purple-600 text-white p-4 flex justify-between items-center">
<h1 class="text-xl font-bold">Gymnasium - Admin</h1>
<div class="flex items-center gap-4">
<a href="/admin/users" class="hover:underline">Manage Users</a>
<span>Welcome, {{ username }}</span>
<form method="POST" action="/logout">
<button type="submit" class="bg-red-500 hover:bg-red-700 text-white px-3 py-1 rounded text-sm">Logout</button>
</form>
</div>
</nav>
<main class="max-w-4xl mx-auto mt-8 p-4">
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-4">Pending Approvals</h2>
<p class="text-gray-500">Pending event approvals will appear here. The approval workflow is coming in Project 3.</p>
</div>
</main>
</body>
</html>
- Save templates/dashboard_admin.html.
- Create a new file templates/dashboard_student.html with this content:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Student Dashboard - Gymnasium</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<nav class="bg-green-600 text-white p-4 flex justify-between items-center">
<h1 class="text-xl font-bold">Gymnasium - Student</h1>
<div class="flex items-center gap-4">
<span>Welcome, {{ username }}</span>
<form method="POST" action="/logout">
<button type="submit" class="bg-red-500 hover:bg-red-700 text-white px-3 py-1 rounded text-sm">Logout</button>
</form>
</div>
</nav>
<main class="max-w-4xl mx-auto mt-8 p-4">
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-4">Upcoming Events</h2>
<p class="text-gray-500">Upcoming gymnasium events will appear here. Event viewing is coming in Project 2.</p>
</div>
</main>
</body>
</html>
- Save templates/dashboard_student.html.
- Confirm all three templates were created by running:
ls templates/dashboard_*
You should see dashboard_admin.html, dashboard_organizer.html, and dashboard_student.html listed.
Files not showing up?
Make sure you created the files inside the templates/ folder, not in the project root. The filenames must start with dashboard_ (with an underscore, not a hyphen).
Need help? My dashboard template files aren't appearing in the templates folder.
Wire up the protected dashboard routes
The templates exist, but no routes serve them yet. Each route will call get_current_user to check the JWT, verify the role matches, and redirect if it does not.
- In main.py, add the organizer dashboard route below the POST /login route:
@app.get("/dashboard/organizer", response_class=HTMLResponse)
async def organizer_dashboard(request: Request):
user = get_current_user(request)
# Redirect to login if no valid token
if user is None:
return RedirectResponse(url="/login")
# Redirect to correct dashboard if role doesn't match
if user["role"] != "organizer":
return RedirectResponse(url=f"/dashboard/{user['role']}")
return templates.TemplateResponse(
request=request,
name="dashboard_organizer.html",
context={"username": user["sub"]}
)
How does route protection work here?
The route calls get_current_user(request) to extract the user from the cookie. If the result is None (no cookie or invalid token), the user gets redirected to /login.
If the user is authenticated but their role does not match organizer, they get redirected to their own dashboard. This is role-based access control in action.
- Save main.py and open http://127.0.0.1:8000/login in your browser.
- Log in with the organizer test user you registered in Step 3.
You should see the organizer dashboard with a blue nav bar, your username, and the placeholder text "Your events will appear here."
- Now add the admin dashboard route below the organizer route:
@app.get("/dashboard/admin", response_class=HTMLResponse)
async def admin_dashboard(request: Request):
user = get_current_user(request)
if user is None:
return RedirectResponse(url="/login")
if user["role"] != "admin":
return RedirectResponse(url=f"/dashboard/{user['role']}")
return templates.TemplateResponse(
request=request,
name="dashboard_admin.html",
context={"username": user["sub"]}
)
- Add the student dashboard route below the admin route:
@app.get("/dashboard/student", response_class=HTMLResponse)
async def student_dashboard(request: Request):
user = get_current_user(request)
if user is None:
return RedirectResponse(url="/login")
if user["role"] != "student":
return RedirectResponse(url=f"/dashboard/{user['role']}")
return templates.TemplateResponse(
request=request,
name="dashboard_student.html",
context={"username": user["sub"]}
)
Why three separate routes instead of one?
Each route has its own role check and renders its own template. This pattern makes it easy to add role-specific logic later (like querying events for organizers or approvals for admins) without a single route becoming a tangled mess of conditionals.
- Save main.py.
Time to test the role-based protection. You are still logged in as the organizer from a moment ago.
- Before you try this: what do you think will happen if you manually type /dashboard/admin in your browser's address bar while logged in as an organizer?
- Type http://127.0.0.1:8000/dashboard/admin directly into your browser's address bar and press Enter.
You get redirected back to /dashboard/organizer. The guard saw your role is organizer, not admin, and bounced you to your own dashboard.
- Now test unauthenticated access. Open your browser's DevTools (Cmd+Option+I on macOS or Ctrl+Shift+I on Windows).
- Navigate to the Application tab (Chrome) or Storage tab (Firefox).
- Find Cookies in the sidebar, expand your site, right-click the access_token cookie and delete it.
- Now try navigating to http://127.0.0.1:8000/dashboard/organizer.
You get redirected to /login. Without a valid cookie, the app treats you as unauthenticated and blocks access to all dashboards.
Not getting redirected?
If you still see the dashboard after deleting the cookie, try a hard refresh (Cmd+Shift+R or Ctrl+Shift+R). The browser may have cached the page.
If typing /dashboard/admin as the organizer still shows the admin page, check that your admin_dashboard route includes the role check. The line if user["role"] != "admin": must be present.
Still stuck? Help me debug my route protection logic.
✔️ Awesome, I've got everything!
Double check you have saved all files: main.py, dashboard_organizer.html, dashboard_admin.html, and dashboard_student.html.
ⓧ I'd like to double check the full code
Your main.py is now quite long. Scroll through and confirm these sections are present in this order:
- Updated imports (8 lines including HTTPException, StaticFiles, decode_access_token).
- The app = FastAPI() and templates setup lines.
- The @app.on_event("startup") function that seeds the admin user.
- The get_current_user helper function.
- The / home route redirecting to /login.
- The GET and POST /register routes (from Step 3).
- The GET and POST /login routes (from Step 4).
- The three dashboard routes: /dashboard/organizer, /dashboard/admin, /dashboard/student.
The dashboard templates should match exactly what you pasted above. Each has a nav bar, a welcome message using {{ username }}, and a placeholder paragraph.
Your authentication system is now fully gated. Each role sees only their own dashboard, and strangers get sent to the login page. Next up, you will extend this with a Secret Mission: a logout button and an admin panel showing all registered users.
Secret mission
Add Logout and an Admin User Panel
Your dashboards have logout buttons, but they point to a route that does not exist yet. And admins have no way to see who has registered. Wire up real logout functionality and build an admin-only user management page with a color-coded table of all registered users.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local MySQL database gymnasium_auth with a users table.
- Project folder gymnasium_auth/ on your Desktop containing Python files, templates, and a virtual environment.
Keep everything running
No action needed. Choose this if you plan to continue building on this project in Project 2 (event management).
- Your MySQL database persists between reboots. The data stays until you explicitly drop it.
- Your project folder and virtual environment remain on your Desktop ready to use.
- To start the app again later, activate the virtual environment and run python -m uvicorn main:app --reload from inside the project folder.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your database and files remain intact for next time.
- Press Ctrl+C in the terminal where uvicorn is running to stop the server.
- The MySQL database persists automatically. No need to stop MySQL itself.
- Your project folder and virtual environment stay on your Desktop.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone.
- Press Ctrl+C in the terminal to stop the uvicorn server if it is still running.
- Drop the MySQL database by opening your MySQL shell and running:
mysql -u root -p -e "DROP DATABASE gymnasium_auth;"
- Delete the project folder by running the following from your Desktop:
macOS
cd ~/Desktop
rm -rf gymnasium_auth
Windows
cd ~\Desktop
rmdir /s /q gymnasium_auth
Nice Work!
Nice Work!
Nice work. You just built a secure, role-gated authentication system for a gymnasium event management app using FastAPI, MySQL, and Tailwind CSS.
You've learned how to:
- Build a FastAPI web application with server-side rendered templates and Tailwind CSS styling, connected to a MySQL database via SQLAlchemy ORM.
- Implement secure password hashing with Argon2 and JWT-based authentication with HTTP-only cookie storage for session management.
- Create role-based route protection that restricts access based on user roles (organizer, admin, student) and redirects unauthorized users.
- Secret Mission: Added logout functionality and an admin user management panel listing all registered users.
Ready to quiz yourself?