Skip to content

Commit 9a3489a

Browse files
committed
added authentication
1 parent 57c5956 commit 9a3489a

14 files changed

Lines changed: 891 additions & 104 deletions

File tree

.gitignore

Lines changed: 228 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,234 @@
1-
# Python-generated files
1+
# Created by https://www.toptal.com/developers/gitignore/api/python,macos,visualstudiocode,dotenv
2+
# Edit at https://www.toptal.com/developers/gitignore?templates=python,macos,visualstudiocode,dotenv
3+
4+
### dotenv ###
5+
.env
6+
7+
### Database Files ###
8+
*.sqlite3
9+
*.db
10+
11+
### macOS ###
12+
# General
13+
.DS_Store
14+
.AppleDouble
15+
.LSOverride
16+
17+
# Icon must end with two \r
18+
Icon
19+
20+
21+
# Thumbnails
22+
._*
23+
24+
# Files that might appear in the root of a volume
25+
.DocumentRevisions-V100
26+
.fseventsd
27+
.Spotlight-V100
28+
.TemporaryItems
29+
.Trashes
30+
.VolumeIcon.icns
31+
.com.apple.timemachine.donotpresent
32+
33+
# Directories potentially created on remote AFP share
34+
.AppleDB
35+
.AppleDesktop
36+
Network Trash Folder
37+
Temporary Items
38+
.apdisk
39+
40+
### macOS Patch ###
41+
# iCloud generated files
42+
*.icloud
43+
44+
### Python ###
45+
# Byte-compiled / optimized / DLL files
246
__pycache__/
3-
*.py[oc]
47+
*.py[cod]
48+
*$py.class
49+
50+
# C extensions
51+
*.so
52+
53+
# Distribution / packaging
54+
.Python
455
build/
56+
develop-eggs/
557
dist/
58+
downloads/
59+
eggs/
60+
.eggs/
61+
lib/
62+
lib64/
63+
parts/
64+
sdist/
65+
var/
666
wheels/
7-
*.egg-info
67+
share/python-wheels/
68+
*.egg-info/
69+
.installed.cfg
70+
*.egg
71+
MANIFEST
72+
73+
# PyInstaller
74+
# Usually these files are written by a python script from a template
75+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
76+
*.manifest
77+
*.spec
78+
79+
# Installer logs
80+
pip-log.txt
81+
pip-delete-this-directory.txt
82+
83+
# Unit test / coverage reports
84+
htmlcov/
85+
.tox/
86+
.nox/
87+
.coverage
88+
.coverage.*
89+
.cache
90+
nosetests.xml
91+
coverage.xml
92+
*.cover
93+
*.py,cover
94+
.hypothesis/
95+
.pytest_cache/
96+
cover/
97+
98+
# Translations
99+
*.mo
100+
*.pot
101+
102+
# Django stuff:
103+
*.log
104+
local_settings.py
105+
db.sqlite3
106+
db.sqlite3-journal
107+
108+
# Flask stuff:
109+
instance/
110+
.webassets-cache
111+
112+
# Scrapy stuff:
113+
.scrapy
114+
115+
# Sphinx documentation
116+
docs/_build/
117+
118+
# PyBuilder
119+
.pybuilder/
120+
target/
121+
122+
# Jupyter Notebook
123+
.ipynb_checkpoints
124+
125+
# IPython
126+
profile_default/
127+
ipython_config.py
8128

9-
# Virtual environments
129+
# pyenv
130+
# For a library or package, you might want to ignore these files since the code is
131+
# intended to run in multiple environments; otherwise, check them in:
132+
# .python-version
133+
134+
# pipenv
135+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
136+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
137+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
138+
# install all needed dependencies.
139+
#Pipfile.lock
140+
141+
# poetry
142+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
143+
# This is especially recommended for binary packages to ensure reproducibility, and is more
144+
# commonly ignored for libraries.
145+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
146+
#poetry.lock
147+
148+
# pdm
149+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
150+
#pdm.lock
151+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
152+
# in version control.
153+
# https://pdm.fming.dev/#use-with-ide
154+
.pdm.toml
155+
156+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
157+
__pypackages__/
158+
159+
# Celery stuff
160+
celerybeat-schedule
161+
celerybeat.pid
162+
163+
# SageMath parsed files
164+
*.sage.py
165+
166+
# Environments
10167
.venv
168+
env/
169+
venv/
170+
ENV/
171+
env.bak/
172+
venv.bak/
173+
174+
# Spyder project settings
175+
.spyderproject
176+
.spyproject
177+
178+
# Rope project settings
179+
.ropeproject
180+
181+
# mkdocs documentation
182+
/site
183+
184+
# mypy
185+
.mypy_cache/
186+
.dmypy.json
187+
dmypy.json
188+
189+
# Pyre type checker
190+
.pyre/
191+
192+
# pytype static type analyzer
193+
.pytype/
194+
195+
# Cython debug symbols
196+
cython_debug/
197+
198+
# PyCharm
199+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
200+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
201+
# and can be added to the global gitignore or merged into this file. For a more nuclear
202+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
203+
#.idea/
204+
205+
### Python Patch ###
206+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
207+
poetry.toml
208+
209+
# ruff
210+
.ruff_cache/
211+
212+
# LSP config files
213+
pyrightconfig.json
214+
215+
### VisualStudioCode ###
216+
.vscode/*
217+
!.vscode/settings.json
218+
!.vscode/tasks.json
219+
!.vscode/launch.json
220+
!.vscode/extensions.json
221+
!.vscode/*.code-snippets
222+
223+
# Local History for Visual Studio Code
224+
.history/
225+
226+
# Built Visual Studio Code Extensions
227+
*.vsix
228+
229+
### VisualStudioCode Patch ###
230+
# Ignore all local history of files
231+
.history
232+
.ionide
233+
234+
# End of https://www.toptal.com/developers/gitignore/api/python,macos,visualstudiocode,dotenv

auth.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from datetime import UTC, datetime, timedelta
2+
3+
import jwt
4+
from fastapi.security import OAuth2PasswordBearer
5+
from pwdlib import PasswordHash
6+
7+
from config import settings
8+
9+
password_hash = PasswordHash.recommended()
10+
11+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/users/token")
12+
13+
14+
def hash_password(password: str) -> str:
15+
return password_hash.hash(password)
16+
17+
18+
def verify_password(plain_password: str, hashed_password: str) -> bool:
19+
return password_hash.verify(plain_password, hashed_password)
20+
21+
22+
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
23+
"""Create a JWT access token."""
24+
to_encode = data.copy()
25+
if expires_delta:
26+
expire = datetime.now(UTC) + expires_delta
27+
else:
28+
expire = datetime.now(UTC) + timedelta(
29+
minutes=settings.access_token_expire_minutes,
30+
)
31+
to_encode.update({"exp": expire})
32+
encoded_jwt = jwt.encode(
33+
to_encode,
34+
settings.secret_key.get_secret_value(),
35+
algorithm=settings.algorithm,
36+
)
37+
return encoded_jwt
38+
39+
40+
def verify_access_token(token: str) -> str | None:
41+
"""Verify a JWT access token and return the subject (user id) if valid."""
42+
try:
43+
payload = jwt.decode(
44+
token,
45+
settings.secret_key.get_secret_value(),
46+
algorithms=[settings.algorithm],
47+
options={"require": ["exp", "sub"]},
48+
)
49+
except jwt.InvalidTokenError:
50+
return None
51+
else:
52+
return payload.get("sub")

blog.db

0 Bytes
Binary file not shown.

config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from pydantic import SecretStr
2+
from pydantic_settings import BaseSettings, SettingsConfigDict
3+
4+
class Settings(BaseSettings):
5+
model_config = SettingsConfigDict(
6+
env_file=".env",
7+
env_file_encoding="utf-8"
8+
)
9+
10+
secret_key: SecretStr
11+
algorithm: str = "HS256"
12+
access_token_expire_minutes: int = 30
13+
14+
15+
settings = Settings() # Loaded from .env file

main.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,24 @@ async def user_posts_page(
106106
)
107107

108108

109+
@app.get("/login", include_in_schema=False)
110+
async def login_page(request: Request):
111+
return templates.TemplateResponse(
112+
request,
113+
"login.html",
114+
{"title": "Login"},
115+
)
116+
117+
118+
@app.get("/register", include_in_schema=False)
119+
async def register_page(request: Request):
120+
return templates.TemplateResponse(
121+
request,
122+
"register.html",
123+
{"title": "Register"},
124+
)
125+
126+
109127
@app.exception_handler(StarletteHTTPException)
110128
async def general_http_exception_handler(
111129
request: Request,

models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class User(Base):
1414
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
1515
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
1616
email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
17+
password_hash: Mapped[str] = mapped_column(String(200), nullable=False)
1718
image_file: Mapped[str | None] = mapped_column(
1819
String(200),
1920
nullable=True,

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ dependencies = [
88
"aiosqlite>=0.22.1",
99
"fastapi[standard]>=0.128.1",
1010
"greenlet>=3.3.1",
11+
"pwdlib[argon2]>=0.3.0",
12+
"pydantic-settings>=2.12.0",
13+
"pyjwt>=2.11.0",
1114
"sqlalchemy>=2.0.46",
1215
]

0 commit comments

Comments
 (0)