Skip to content

Commit edbba8c

Browse files
committed
added authorization
1 parent e0a99e7 commit edbba8c

8 files changed

Lines changed: 590 additions & 230 deletions

File tree

auth.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
from ast import mod
12
from datetime import UTC, datetime, timedelta
3+
from unittest import result
24

35
import jwt
46
from fastapi.security import OAuth2PasswordBearer
57
from pwdlib import PasswordHash
68

79
from config import settings
810

11+
from typing import Annotated
12+
from fastapi import Depends, HTTPException, status
13+
from sqlalchemy.ext.asyncio import AsyncSession
14+
from database import get_db
15+
from sqlalchemy import select
16+
import models
17+
18+
919
password_hash = PasswordHash.recommended()
1020

1121
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/users/token")
@@ -49,4 +59,42 @@ def verify_access_token(token: str) -> str | None:
4959
except jwt.InvalidTokenError:
5060
return None
5161
else:
52-
return payload.get("sub")
62+
return payload.get("sub")
63+
64+
65+
async def get_current_user(
66+
token: Annotated[str, Depends(oauth2_scheme)],
67+
db: Annotated[AsyncSession, Depends(get_db)]
68+
) -> models.User:
69+
user_id = verify_access_token(token)
70+
if user_id is None:
71+
raise HTTPException(
72+
status_code=status.HTTP_401_UNAUTHORIZED,
73+
detail="Invalid or expired token",
74+
headers={"WWW-Authenticate": "Bearer"},
75+
)
76+
77+
try:
78+
user_id_int = int(user_id)
79+
80+
except (TypeError, ValueError):
81+
raise HTTPException(
82+
status_code=status.HTTP_401_UNAUTHORIZED,
83+
detail="Invalid or expired token",
84+
headers={"WWW-Authenticate": "Bearer"},
85+
)
86+
87+
result = await db.execute(
88+
select(models.User).where(models.User.id == user_id_int)
89+
)
90+
91+
user = result.scalars().first()
92+
if not user:
93+
raise HTTPException(
94+
status_code=status.HTTP_401_UNAUTHORIZED,
95+
detail="User not found",
96+
headers={"WWW-Authenticate": "Bearer"},
97+
)
98+
return user
99+
100+
CurrentUser = Annotated[models.User, Depends(get_current_user)]

main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ async def register_page(request: Request):
123123
{"title": "Register"},
124124
)
125125

126+
@app.get("/account", include_in_schema=False)
127+
async def account_page(request: Request):
128+
return templates.TemplateResponse(
129+
request,
130+
"account.html",
131+
{"title": "Account"},
132+
)
133+
126134

127135
@app.exception_handler(StarletteHTTPException)
128136
async def general_http_exception_handler(

routers/posts.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from database import get_db
1010
from schemas import PostCreate, PostResponse, PostUpdate
1111

12+
from auth import CurrentUser
13+
1214
router = APIRouter()
1315

1416
@router.get("", response_model=list[PostResponse])
@@ -27,21 +29,12 @@ async def get_posts(db: Annotated[AsyncSession, Depends(get_db)]):
2729
response_model=PostResponse,
2830
status_code=status.HTTP_201_CREATED,
2931
)
30-
async def create_post(post: PostCreate, db: Annotated[AsyncSession, Depends(get_db)]):
31-
result = await db.execute(
32-
select(models.User).where(models.User.id == post.user_id)
33-
)
34-
user = result.scalars().first()
35-
if not user:
36-
raise HTTPException(
37-
status_code=status.HTTP_404_NOT_FOUND,
38-
detail="User not found",
39-
)
32+
async def create_post(post: PostCreate, current_user: CurrentUser, db: Annotated[AsyncSession, Depends(get_db)]):
4033

4134
new_post = models.Post(
4235
title=post.title,
4336
content=post.content,
44-
user_id=post.user_id,
37+
user_id=current_user.id,
4538
)
4639
db.add(new_post)
4740
await db.commit()
@@ -66,6 +59,7 @@ async def get_post(post_id: int, db: Annotated[AsyncSession, Depends(get_db)]):
6659
async def update_post_full(
6760
post_id: int,
6861
post_data: PostCreate,
62+
current_user: CurrentUser,
6963
db: Annotated[AsyncSession, Depends(get_db)],
7064
):
7165
result = await db.execute(select(models.Post).where(models.Post.id == post_id))
@@ -75,20 +69,15 @@ async def update_post_full(
7569
status_code=status.HTTP_404_NOT_FOUND,
7670
detail="Post not found",
7771
)
78-
if post_data.user_id != post.user_id:
79-
result = await db.execute(
80-
select(models.User).where(models.User.id == post_data.user_id),
72+
73+
if post.user_id != current_user.id:
74+
raise HTTPException(
75+
status_code=status.HTTP_403_FORBIDDEN,
76+
detail="Not authorized to update this post",
8177
)
82-
user = result.scalars().first()
83-
if not user:
84-
raise HTTPException(
85-
status_code=status.HTTP_404_NOT_FOUND,
86-
detail="User not found",
87-
)
8878

8979
post.title = post_data.title
9080
post.content = post_data.content
91-
post.user_id = post_data.user_id
9281

9382
await db.commit()
9483
await db.refresh(post, attribute_names=["author"])
@@ -99,6 +88,7 @@ async def update_post_full(
9988
async def update_post_partial(
10089
post_id: int,
10190
post_data: PostUpdate,
91+
current_user: CurrentUser,
10292
db: Annotated[AsyncSession, Depends(get_db)],
10393
):
10494
result = await db.execute(select(models.Post).where(models.Post.id == post_id))
@@ -109,6 +99,12 @@ async def update_post_partial(
10999
detail="Post not found",
110100
)
111101

102+
if post.user_id != current_user.id:
103+
raise HTTPException(
104+
status_code=status.HTTP_403_FORBIDDEN,
105+
detail="Not authorized to update this post",
106+
)
107+
112108
update_data = post_data.model_dump(exclude_unset=True)
113109
for field, value in update_data.items():
114110
setattr(post, field, value)
@@ -119,7 +115,10 @@ async def update_post_partial(
119115

120116

121117
@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
122-
async def delete_post(post_id: int, db: Annotated[AsyncSession, Depends(get_db)]):
118+
async def delete_post(
119+
post_id: int,
120+
current_user: CurrentUser,
121+
db: Annotated[AsyncSession, Depends(get_db)]):
123122
result = await db.execute(select(models.Post).where(models.Post.id == post_id))
124123
post = result.scalars().first()
125124
if not post:
@@ -128,5 +127,11 @@ async def delete_post(post_id: int, db: Annotated[AsyncSession, Depends(get_db)]
128127
detail="Post not found",
129128
)
130129

130+
if post.user_id != current_user.id:
131+
raise HTTPException(
132+
status_code=status.HTTP_403_FORBIDDEN,
133+
detail="Not authorized to delete this post",
134+
)
135+
131136
await db.delete(post)
132137
await db.commit()

routers/users.py

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from database import get_db
1515
from schemas import PostResponse, UserCreate, UserPrivate, UserPublic, UserUpdate, Token
1616

17-
from auth import create_access_token, verify_access_token, oauth2_scheme, hash_password, verify_password
17+
from auth import create_access_token, hash_password, verify_password, CurrentUser
1818

1919
router = APIRouter()
2020

@@ -88,36 +88,8 @@ async def login_for_access_token(
8888

8989

9090
@router.get("/me", response_model=UserPrivate)
91-
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[AsyncSession, Depends(get_db)]):
92-
"""Get the currently authenticated user."""
93-
user_id = verify_access_token(token)
94-
if user_id is None:
95-
raise HTTPException(
96-
status_code=status.HTTP_401_UNAUTHORIZED,
97-
detail="Invalid or expired token",
98-
headers={"WWW-Authenticate": "Bearer"},
99-
)
100-
101-
# Validate user_id is a valid integer (defense against malformed JWT)
102-
try:
103-
user_id_int = int(user_id)
104-
except (TypeError, ValueError):
105-
raise HTTPException(
106-
status_code=status.HTTP_401_UNAUTHORIZED,
107-
detail="Invalid or expired token",
108-
headers={"WWW-Authenticate": "Bearer"},
109-
)
110-
111-
result = await db.execute(select(models.User).where(models.User.id == user_id_int))
112-
113-
user = result.scalars().first()
114-
if not user:
115-
raise HTTPException(
116-
status_code=status.HTTP_401_UNAUTHORIZED,
117-
detail="User not found",
118-
headers={"WWW-Authenticate": "Bearer"},
119-
)
120-
return user
91+
async def get_current_user(current_user: CurrentUser):
92+
return current_user
12193

12294

12395
@router.get("/{user_id}", response_model=UserPublic)
@@ -152,8 +124,15 @@ async def get_user_posts(user_id: int, db: Annotated[AsyncSession, Depends(get_d
152124
async def update_user(
153125
user_id: int,
154126
user_update: UserUpdate,
127+
current_user: CurrentUser,
155128
db: Annotated[AsyncSession, Depends(get_db)],
156129
):
130+
131+
if user_id != current_user.id:
132+
raise HTTPException(
133+
status_code=status.HTTP_403_FORBIDDEN,
134+
detail="Not authorized to update this user",
135+
)
157136
result = await db.execute(select(models.User).where(models.User.id == user_id))
158137
user = result.scalars().first()
159138
if not user:
@@ -195,7 +174,16 @@ async def update_user(
195174

196175

197176
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
198-
async def delete_user(user_id: int, db: Annotated[AsyncSession, Depends(get_db)]):
177+
async def delete_user(
178+
user_id: int,
179+
current_user: CurrentUser,
180+
db: Annotated[AsyncSession, Depends(get_db)]
181+
):
182+
if user_id != current_user.id:
183+
raise HTTPException(
184+
status_code=status.HTTP_403_FORBIDDEN,
185+
detail="Not authorized to delete this post",
186+
)
199187
result = await db.execute(select(models.User).where(models.User.id == user_id))
200188
user = result.scalars().first()
201189
if not user:

schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class PostBase(BaseModel):
3636
content: str = Field(min_length=1)
3737

3838
class PostCreate(PostBase):
39-
user_id: int
39+
pass
4040

4141
class PostUpdate(BaseModel):
4242
title: str | None = Field(default=None, min_length=1, max_length=100)

0 commit comments

Comments
 (0)