Skip to content

Commit eb25824

Browse files
Initial commit
0 parents  commit eb25824

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1011
-0
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
SECRET_KEY=
2+
DB_USER=
3+
DB_PASSWORD=
4+
DB_HOST=localhost
5+
DB_PORT=5432
6+
DB_NAME=

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.devcontainer
2+
.gpteng
3+
prompt
4+
__pycache__
5+
*.pyc
6+
.env

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# FastAPI, Jinja2, PostgreSQL Webapp
2+
3+
This project is still under development.
4+
5+
## Start development database
6+
7+
`docker compose up -d`
8+
9+
## Set environment variables
10+
11+
Copy .env.example to .env with `cp .env.example .env`.
12+
13+
Generate a 256 bit secret key with `openssl rand -base64 32` and paste it into the .env file.
14+
15+
Set your desired database name, username, and password in the .env file.
16+
17+
## Run the development server
18+
19+
`uvicorn main:app --host 0.0.0.0 --port 8000 --reload`
20+
21+
Navigate to http://localhost:8000/
22+
23+
## License
24+
25+
This project is licensed under the GPLv3 License. See the LICENSE file for more details.

docker-compose.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
db:
3+
image: postgres:latest
4+
environment:
5+
POSTGRES_USER: postgres
6+
POSTGRES_PASSWORD: postgres
7+
POSTGRES_DB: round_robin_db
8+
ports:
9+
- "5433:5432"
10+
volumes:
11+
- postgres_data:/var/lib/postgresql/data
12+
13+
volumes:
14+
postgres_data:

main.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import logging
2+
from typing import Optional
3+
from contextlib import asynccontextmanager
4+
from fastapi import FastAPI, Request, Depends
5+
from fastapi.responses import RedirectResponse
6+
from fastapi.staticfiles import StaticFiles
7+
from fastapi.templating import Jinja2Templates
8+
from sqlmodel import SQLModel, create_engine
9+
from fastapi.exceptions import RequestValidationError, StarletteHTTPException
10+
from routers import auth, organization, score, template, version
11+
from utils import get_current_user, get_connection_url
12+
from models import User
13+
14+
15+
logger = logging.getLogger("uvicorn.error")
16+
17+
18+
@asynccontextmanager
19+
async def lifespan(app: FastAPI):
20+
# Startup logic
21+
engine = create_engine(get_connection_url())
22+
SQLModel.metadata.create_all(engine)
23+
engine.dispose()
24+
yield
25+
# Shutdown logic
26+
27+
28+
app = FastAPI(lifespan=lifespan)
29+
30+
# Mount static files (e.g., CSS, JS)
31+
app.mount("/static", StaticFiles(directory="static"), name="static")
32+
33+
# Initialize Jinja2 templates
34+
templates = Jinja2Templates(directory="templates")
35+
36+
37+
@app.exception_handler(StarletteHTTPException)
38+
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
39+
return templates.TemplateResponse(
40+
"errors/error.html",
41+
{"request": request, "status_code": exc.status_code, "detail": exc.detail},
42+
status_code=exc.status_code,
43+
)
44+
45+
46+
@app.exception_handler(RequestValidationError)
47+
async def validation_exception_handler(request: Request, exc: RequestValidationError):
48+
return templates.TemplateResponse(
49+
"errors/error.html",
50+
{"request": request, "status_code": 422, "detail": str(exc)},
51+
status_code=422,
52+
)
53+
54+
55+
@app.get("/")
56+
async def read_home(
57+
request: Request,
58+
user: Optional[User] = Depends(get_current_user),
59+
error_message: Optional[str] = None,
60+
):
61+
if user:
62+
return RedirectResponse(url="/dashboard", status_code=302)
63+
return templates.TemplateResponse(
64+
"index.html", {"request": request, "user": user, "error_message": error_message}
65+
)
66+
67+
68+
@app.get("/login")
69+
async def read_login(
70+
request: Request,
71+
user: Optional[User] = Depends(get_current_user),
72+
error_message: Optional[str] = None,
73+
):
74+
if user:
75+
return RedirectResponse(url="/dashboard", status_code=302)
76+
return templates.TemplateResponse(
77+
"authentication/login.html",
78+
{"request": request, "user": user, "error_message": error_message},
79+
)
80+
81+
82+
@app.get("/register")
83+
async def read_register(
84+
request: Request,
85+
user: Optional[User] = Depends(get_current_user),
86+
error_message: Optional[str] = None,
87+
):
88+
if user:
89+
return RedirectResponse(url="/dashboard", status_code=302)
90+
return templates.TemplateResponse(
91+
"authentication/register.html",
92+
{"request": request, "user": user, "error_message": error_message},
93+
)
94+
95+
96+
@app.get("/forgot_password")
97+
async def read_forgot_password(
98+
request: Request,
99+
user: Optional[User] = Depends(get_current_user),
100+
error_message: Optional[str] = None,
101+
):
102+
if user:
103+
return RedirectResponse(url="/dashboard", status_code=302)
104+
return templates.TemplateResponse(
105+
"authentication/forgot_password.html",
106+
{"request": request, "user": user, "error_message": error_message},
107+
)
108+
109+
110+
@app.get("/reset_password")
111+
async def read_reset_password(
112+
request: Request,
113+
token: str,
114+
user: Optional[User] = Depends(get_current_user),
115+
error_message: Optional[str] = None,
116+
):
117+
if user:
118+
return RedirectResponse(url="/dashboard", status_code=302)
119+
# TODO: Validate the token here?
120+
return templates.TemplateResponse(
121+
"authentication/reset_password.html",
122+
{
123+
"request": request,
124+
"token": token,
125+
"user": user,
126+
"error_message": error_message,
127+
},
128+
)
129+
130+
131+
@app.get("/dashboard")
132+
async def read_dashboard(
133+
request: Request,
134+
user: Optional[User] = Depends(get_current_user),
135+
error_message: Optional[str] = None,
136+
):
137+
if not user:
138+
return RedirectResponse(url="/", status_code=302)
139+
return templates.TemplateResponse(
140+
"dashboard/index.html",
141+
{"request": request, "user": user, "error_message": error_message},
142+
)
143+
144+
145+
@app.get("/user_profile")
146+
async def read_user_profile(
147+
request: Request,
148+
user: Optional[User] = Depends(get_current_user),
149+
error_message: Optional[str] = None,
150+
):
151+
if not user:
152+
return RedirectResponse(url="/", status_code=302)
153+
return templates.TemplateResponse(
154+
"user_profile/index.html",
155+
{"request": request, "user": user, "error_message": error_message},
156+
)
157+
158+
159+
@app.get("/about")
160+
async def read_about(
161+
request: Request,
162+
user: Optional[User] = Depends(get_current_user),
163+
error_message: Optional[str] = None,
164+
):
165+
return templates.TemplateResponse(
166+
"about.html", {"request": request, "user": user, "error_message": error_message}
167+
)
168+
169+
170+
@app.get("/privacy_policy")
171+
async def read_privacy_policy(
172+
request: Request,
173+
user: Optional[User] = Depends(get_current_user),
174+
error_message: Optional[str] = None,
175+
):
176+
return templates.TemplateResponse(
177+
"privacy_policy.html",
178+
{"request": request, "user": user, "error_message": error_message},
179+
)
180+
181+
182+
@app.get("/terms_of_service")
183+
async def read_terms_of_service(
184+
request: Request,
185+
user: Optional[User] = Depends(get_current_user),
186+
error_message: Optional[str] = None,
187+
):
188+
return templates.TemplateResponse(
189+
"terms_of_service.html",
190+
{"request": request, "user": user, "error_message": error_message},
191+
)
192+
193+
194+
app.include_router(auth.router)
195+
app.include_router(organization.router)
196+
197+
198+
if __name__ == "__main__":
199+
import uvicorn
200+
201+
uvicorn.run(app, host="0.0.0.0", port=8000)

models.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from sqlalchemy import CheckConstraint
2+
from sqlmodel import SQLModel, Field, Relationship, Column, Integer
3+
from typing import Optional, List
4+
from datetime import datetime, UTC
5+
6+
7+
def utc_time():
8+
return datetime.now(UTC)
9+
10+
11+
class Organization(SQLModel, table=True):
12+
id: Optional[int] = Field(default=None, primary_key=True)
13+
name: str
14+
created_at: datetime = Field(default_factory=utc_time)
15+
updated_at: datetime = Field(default_factory=utc_time)
16+
deleted: bool = Field(default=False)
17+
18+
users: List["User"] = Relationship(back_populates="organization")
19+
20+
21+
class User(SQLModel, table=True):
22+
id: Optional[int] = Field(default=None, primary_key=True)
23+
name: str
24+
email: str = Field(index=True, unique=True)
25+
hashed_password: str
26+
avatar_url: Optional[str] = None
27+
organization_id: Optional[int] = Field(default=None, foreign_key="organization.id")
28+
created_at: datetime = Field(default_factory=utc_time)
29+
updated_at: datetime = Field(default_factory=utc_time)
30+
deleted: bool = Field(default=False)
31+
32+
organization: Optional["Organization"] = Relationship(back_populates="users")

pyproject.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[tool.poetry]
2+
name = "fastapi-jinja2-postgres-webapp"
3+
version = "0.1.0"
4+
description = "A template webapp with a pure-Python FastAPI backend, frontend templating with Jinja2, and a Postgres database to power user auth"
5+
authors = ["Christopher Carroll Smith <[email protected]>"]
6+
readme = "README.md"
7+
package-mode = false
8+
9+
[tool.poetry.dependencies]
10+
python = "^3.12"
11+
sqlmodel = "^0.0.22"
12+
mypy = "^1.11.2"
13+
fastapi = "^0.114.1"
14+
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
15+
pyjwt = "^2.9.0"
16+
jinja2 = "^3.1.4"
17+
uvicorn = "^0.30.6"
18+
psycopg2 = "^2.9.9"
19+
pydantic = {extras = ["email"], version = "^2.9.1"}
20+
python-multipart = "^0.0.9"
21+
python-dotenv = "^1.0.1"
22+
23+
24+
[build-system]
25+
requires = ["poetry-core"]
26+
build-backend = "poetry.core.masonry.api"

routers/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)