Extends the Day 15 JWT auth API with role-based permissions, reusable query dependencies, and a couple of fixes flagged in the Day 15 review.
Built as part of a daily build-in-public series leading up to a capstone project.
- Roles: every user now has a
rolefield —"student"or"teacher", defaulting to"student". require_any_role(*roles): a dependency factory that gates a route to one or more roles. Returns a clean403 Forbidden(not401) when the user is authenticated but not allowed.- Teacher-only course creation:
POST /coursesnow requires theteacherrole. - Paginated, role-open course listing:
GET /coursesis open to any authenticated user (student or teacher) and supportslimit/offsetpagination. - Consistent 403s: a custom
ForbiddenExceptionso every permission failure across the app returns the same JSON shape. SECRET_KEYvia environment variable: no more hardcoded secret. Falls back to a dev-only placeholder locally, and hard-fails on startup if missing in production.
| Endpoint | Who can access | Notes |
|---|---|---|
POST /register |
Anyone | Role optional, defaults to student |
POST /login |
Anyone | Returns a JWT |
GET /me |
Any authenticated user | Returns the caller's own profile |
POST /courses |
Teachers only | 403 for students |
GET /courses |
Any authenticated user | Paginated |
GET /users |
Any authenticated user | Paginated, returns 200 + [] when empty |
GET /users/{user_id} |
Self, or any teacher | See decision below |
DELETE /users/{user_id} |
Self only | A user can only delete their own account |
A user can view their own profile. A teacher can view any user's profile, including their enrolled courses.
This is a deliberate choice: teachers are treated as trusted staff in this model, with oversight visibility over all students rather than only students enrolled in their own courses. The simpler rule was chosen over a course-scoped check to keep the permission model easy to reason about at this stage of the project; a more granular "teacher can only view students in their own courses" rule is a reasonable next iteration but wasn't required here.
- FastAPI — web framework
- SQLAlchemy — ORM, SQLite for local dev
- python-jose — JWT encoding/decoding
- passlib (bcrypt) — password hashing
- Pydantic v2 — request/response schemas
.
├── main.py # routes
├── models.py # SQLAlchemy models (User, Course)
├── schemas.py # Pydantic request/response schemas
├── auth.py # JWT, password hashing, role dependency
├── database.py # engine, session, Base
├── pagination.py # shared pagination dependency
├── Exceptions.py # ForbiddenException (consistent 403s)
└── README.md
-
Clone the repo and install dependencies:
pip install fastapi uvicorn sqlalchemy python-jose passlib[bcrypt] python-multipart
-
Set a
SECRET_KEYenvironment variable (required in production, optional locally):export SECRET_KEY="your-secret-here"
-
Run the server:
uvicorn main:app --reload
-
Open
http://127.0.0.1:8000/docsfor the interactive API docs.
# Register as a teacher
POST /register
{
"name": "Jane Doe",
"email": "jane@example.com",
"password": "secret123",
"role": "teacher"
}
# Log in
POST /login
# returns: { "access_token": "...", "token_type": "bearer" }
# Create a course (requires teacher role)
POST /courses
Authorization: Bearer <token>
{ "title": "Intro to Databases" }The SECRET_KEY exposed in the Day 15 commit history has been rotated and is no longer in use. Going forward, secrets are sourced from environment variables and excluded from version control via .gitignore.
Stretch goals for a future pass:
- Course-scoped permissions for teachers (only view/manage students enrolled in their own courses)
- Refresh tokens
- Role-based filtering on
GET /usersandGET /courses