Abdullahcoder54 commited on
Commit
697c967
·
1 Parent(s): b0d98d6

Push the app

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +7 -0
  2. .gitattributes +0 -35
  3. .gitignore +0 -0
  4. .python-version +1 -0
  5. Dockerfile +20 -0
  6. README.md +0 -11
  7. __pycache__/main.cpython-313.pyc +0 -0
  8. api/__init__.py +0 -0
  9. api/__pycache__/__init__.cpython-313.pyc +0 -0
  10. api/v1/__init__.py +0 -0
  11. api/v1/__pycache__/__init__.cpython-313.pyc +0 -0
  12. api/v1/routes/__init__.py +0 -0
  13. api/v1/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  14. api/v1/routes/__pycache__/auth.cpython-313.pyc +0 -0
  15. api/v1/routes/__pycache__/tasks.cpython-313.pyc +0 -0
  16. api/v1/routes/auth.py +235 -0
  17. api/v1/routes/tasks.py +328 -0
  18. api/v1/routes/users.py +89 -0
  19. auth/__init__.py +0 -0
  20. auth/__pycache__/__init__.cpython-313.pyc +0 -0
  21. auth/__pycache__/jwt_handler.cpython-313.pyc +0 -0
  22. auth/jwt_handler.py +150 -0
  23. config/__init__.py +0 -0
  24. config/__pycache__/__init__.cpython-313.pyc +0 -0
  25. config/__pycache__/settings.cpython-313.pyc +0 -0
  26. config/settings.py +35 -0
  27. database/__init__.py +0 -0
  28. database/__pycache__/__init__.cpython-313.pyc +0 -0
  29. database/__pycache__/session.cpython-313.pyc +0 -0
  30. database/session.py +65 -0
  31. main.py +58 -0
  32. middleware/__init__.py +0 -0
  33. middleware/__pycache__/__init__.cpython-313.pyc +0 -0
  34. middleware/__pycache__/auth_middleware.cpython-313.pyc +0 -0
  35. middleware/auth_middleware.py +69 -0
  36. models/__init__.py +0 -0
  37. models/__pycache__/__init__.cpython-313.pyc +0 -0
  38. models/__pycache__/task.cpython-313.pyc +0 -0
  39. models/__pycache__/user.cpython-313.pyc +0 -0
  40. models/task.py +59 -0
  41. models/user.py +28 -0
  42. pyproject.toml +20 -0
  43. requirements.txt +12 -0
  44. reset_database.py +28 -0
  45. s.py +2 -0
  46. schemas/__init__.py +0 -0
  47. schemas/__pycache__/__init__.cpython-313.pyc +0 -0
  48. schemas/__pycache__/user.cpython-313.pyc +0 -0
  49. schemas/task.py +32 -0
  50. schemas/user.py +24 -0
.env ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ DATABASE_URL=postgresql+asyncpg://neondb_owner:npg_PrJpZot1wWj7@ep-weathered-unit-ad0z27gz-pooler.c-2.us-east-1.aws.neon.tech/neondb?sslmode=require
2
+ DB_ECHO=true
3
+ BETTER_AUTH_SECRET=abfe95adc6a3d85f1d8533a0fbf151b18240d817b471dda39a925555d886549c32c667dbeb184b9e9c73da3227c0dae5f83a
4
+ JWT_ALGORITHM=HS256
5
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
6
+ DEBUG=true
7
+ GEMINI_API_KEY=AIzaSyCIBHuTxHwQQyUtJ_Zbokuu-Qv0mykCUUc
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
File without changes
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN apt-get update && apt-get install -y libgl1 libglib2.0-0
4
+
5
+ # Create user but stay root during install
6
+ RUN useradd -m -u 1000 user
7
+
8
+ WORKDIR /
9
+
10
+ # Install dependencies as root
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
13
+
14
+ # Copy app AFTER installing dependencies
15
+ COPY . /
16
+
17
+ # Switch to non-root user for safety
18
+ USER user
19
+
20
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +0,0 @@
1
- ---
2
- title: Todo App
3
- emoji: ⚡
4
- colorFrom: blue
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
__pycache__/main.cpython-313.pyc ADDED
Binary file (2.79 kB). View file
 
api/__init__.py ADDED
File without changes
api/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (157 Bytes). View file
 
api/v1/__init__.py ADDED
File without changes
api/v1/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (160 Bytes). View file
 
api/v1/routes/__init__.py ADDED
File without changes
api/v1/routes/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (167 Bytes). View file
 
api/v1/routes/__pycache__/auth.cpython-313.pyc ADDED
Binary file (8.4 kB). View file
 
api/v1/routes/__pycache__/tasks.cpython-313.pyc ADDED
Binary file (10.5 kB). View file
 
api/v1/routes/auth.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlmodel.ext.asyncio.session import AsyncSession
3
+ from pydantic import BaseModel
4
+
5
+ from database.session import get_session_dep
6
+ from models.user import UserCreate, User
7
+ from services.user_service import UserService
8
+ from auth.jwt_handler import create_access_token, create_refresh_token, verify_token
9
+ from utils.logging import get_logger
10
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
+ import logging
12
+
13
+ router = APIRouter()
14
+ logger = get_logger(__name__)
15
+
16
+ # Models for auth endpoints
17
+ class UserLogin(BaseModel):
18
+ email: str
19
+ password: str # In a real app, this would be hashed, but for this demo we'll keep it simple
20
+
21
+ class UserRegister(BaseModel):
22
+ email: str
23
+ password: str # In a real app, this would be hashed
24
+ name: str
25
+
26
+ class AuthResponse(BaseModel):
27
+ user: dict
28
+ token: str
29
+ refresh_token: str = None
30
+
31
+ # Initialize security for token verification (for logout)
32
+ security = HTTPBearer()
33
+
34
+ @router.post("/auth/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
35
+ async def register_user(
36
+ user_data: UserRegister,
37
+ session: AsyncSession = Depends(get_session_dep)
38
+ ):
39
+ """
40
+ Register a new user and return JWT token.
41
+
42
+ Args:
43
+ user_data: User registration data (email, password, name)
44
+ session: Database session
45
+
46
+ Returns:
47
+ AuthResponse with user data and JWT token
48
+ """
49
+ try:
50
+ # Create user data object for the service
51
+ user_create_data = UserCreate(
52
+ email=user_data.email,
53
+ name=user_data.name
54
+ )
55
+
56
+ # Create user in database
57
+ created_user = await UserService.create_user(session, user_create_data)
58
+
59
+ # Create JWT tokens
60
+ token_data = {"sub": str(created_user.id), "email": created_user.email}
61
+ token = create_access_token(data=token_data)
62
+ refresh_token = create_refresh_token(data=token_data)
63
+
64
+ logger.info(f"Successfully registered user {created_user.id} with email {created_user.email}")
65
+
66
+ return AuthResponse(
67
+ user=created_user.model_dump(),
68
+ token=token,
69
+ refresh_token=refresh_token
70
+ )
71
+
72
+ except HTTPException:
73
+ # Re-raise HTTP exceptions (like 400 for duplicate email)
74
+ raise
75
+ except Exception as e:
76
+ logger.error(f"Error registering user with email {user_data.email}: {str(e)}")
77
+ raise HTTPException(
78
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
79
+ detail="Error registering user"
80
+ )
81
+
82
+
83
+ @router.post("/auth/login", response_model=AuthResponse)
84
+ async def login_user(
85
+ user_data: UserLogin,
86
+ session: AsyncSession = Depends(get_session_dep)
87
+ ):
88
+ """
89
+ Login a user and return JWT token.
90
+
91
+ Args:
92
+ user_data: User login data (email, password)
93
+ session: Database session
94
+
95
+ Returns:
96
+ AuthResponse with user data and JWT token
97
+ """
98
+ try:
99
+ # Find user by email
100
+ user = await UserService.get_user_by_email(session, user_data.email)
101
+
102
+ if not user:
103
+ logger.warning(f"Login attempt with non-existent email: {user_data.email}")
104
+ raise HTTPException(
105
+ status_code=status.HTTP_401_UNAUTHORIZED,
106
+ detail="Invalid email or password"
107
+ )
108
+
109
+ # In a real app, we would verify the password here.
110
+ # For this implementation, we'll just proceed with login.
111
+
112
+ # Create JWT tokens
113
+ token_data = {"sub": str(user.id), "email": user.email}
114
+ token = create_access_token(data=token_data)
115
+ refresh_token = create_refresh_token(data=token_data)
116
+
117
+ logger.info(f"Successfully logged in user {user.id} with email {user.email}")
118
+
119
+ # Convert user to dict for response
120
+ user_dict = {
121
+ "id": user.id,
122
+ "email": user.email,
123
+ "name": user.name,
124
+ "created_at": user.created_at
125
+ }
126
+
127
+ return AuthResponse(
128
+ user=user_dict,
129
+ token=token,
130
+ refresh_token=refresh_token
131
+ )
132
+
133
+ except HTTPException:
134
+ # Re-raise HTTP exceptions (like 401 for invalid credentials)
135
+ raise
136
+ except Exception as e:
137
+ logger.error(f"Error logging in user with email {user_data.email}: {str(e)}")
138
+ raise HTTPException(
139
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
140
+ detail="Error during login"
141
+ )
142
+
143
+
144
+ @router.post("/auth/logout")
145
+ async def logout_user(
146
+ token: HTTPAuthorizationCredentials = Depends(security)
147
+ ):
148
+ """
149
+ Logout endpoint.
150
+ In a real application, this would add the token to a blacklist/jti store.
151
+ For this implementation, we'll just return a success message.
152
+ """
153
+ try:
154
+ # In a real app, you would add the token to a blacklist or token revocation store
155
+ # For this demo, we'll just return a success message
156
+ logger.info(f"User logged out successfully")
157
+ return {"message": "Successfully logged out"}
158
+ except Exception as e:
159
+ logger.error(f"Error during logout: {str(e)}")
160
+ raise HTTPException(
161
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
162
+ detail="Error during logout"
163
+ )
164
+
165
+
166
+ class RefreshTokenRequest(BaseModel):
167
+ refresh_token: str
168
+
169
+
170
+ @router.post("/auth/refresh", response_model=AuthResponse)
171
+ async def refresh_token(
172
+ refresh_request: RefreshTokenRequest
173
+ ):
174
+ """
175
+ Refresh access token using a valid refresh token.
176
+
177
+ Args:
178
+ refresh_request: Contains the refresh token to use for generating a new access token
179
+
180
+ Returns:
181
+ AuthResponse with new access token and refresh token
182
+ """
183
+ try:
184
+ # Verify the refresh token
185
+ payload = verify_token(refresh_request.refresh_token)
186
+
187
+ # Check if this is a refresh token (not an access token)
188
+ token_type = payload.get("type")
189
+ if token_type != "refresh":
190
+ raise HTTPException(
191
+ status_code=status.HTTP_401_UNAUTHORIZED,
192
+ detail="Invalid token type for refresh",
193
+ headers={"WWW-Authenticate": "Bearer"},
194
+ )
195
+
196
+ # Extract user data from the refresh token
197
+ user_id = payload.get("sub")
198
+ user_email = payload.get("email")
199
+
200
+ if not user_id or not user_email:
201
+ raise HTTPException(
202
+ status_code=status.HTTP_401_UNAUTHORIZED,
203
+ detail="Invalid refresh token",
204
+ headers={"WWW-Authenticate": "Bearer"},
205
+ )
206
+
207
+ # Create new access and refresh tokens
208
+ token_data = {"sub": user_id, "email": user_email}
209
+ new_access_token = create_access_token(data=token_data)
210
+ new_refresh_token = create_refresh_token(data=token_data)
211
+
212
+ logger.info(f"Successfully refreshed token for user {user_id}")
213
+
214
+ # Return new tokens with minimal user data (we don't have full user details here)
215
+ user_dict = {
216
+ "id": user_id,
217
+ "email": user_email
218
+ }
219
+
220
+ return AuthResponse(
221
+ user=user_dict,
222
+ token=new_access_token,
223
+ refresh_token=new_refresh_token
224
+ )
225
+
226
+ except HTTPException:
227
+ # Re-raise HTTP exceptions
228
+ raise
229
+ except Exception as e:
230
+ logger.error(f"Error refreshing token: {str(e)}")
231
+ raise HTTPException(
232
+ status_code=status.HTTP_401_UNAUTHORIZED,
233
+ detail="Could not refresh token",
234
+ headers={"WWW-Authenticate": "Bearer"},
235
+ )
api/v1/routes/tasks.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
2
+ from sqlmodel.ext.asyncio.session import AsyncSession
3
+ from typing import List
4
+ from database.session import get_session_dep
5
+ from models.task import TaskRead, TaskCreate, TaskUpdate, TaskComplete
6
+ from services.task_service import TaskService
7
+ from middleware.auth_middleware import validate_user_id_from_token
8
+ from auth.jwt_handler import get_user_id_from_token
9
+ from utils.logging import get_logger
10
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
11
+ import logging
12
+
13
+ router = APIRouter()
14
+ logger = get_logger(__name__)
15
+
16
+ # Initialize security for token extraction
17
+ security = HTTPBearer()
18
+
19
+
20
+ @router.get("/tasks", response_model=List[TaskRead])
21
+ async def get_tasks(
22
+ request: Request,
23
+ user_id: int,
24
+ token: HTTPAuthorizationCredentials = Depends(security),
25
+ session: AsyncSession = Depends(get_session_dep)
26
+ ):
27
+ """
28
+ Retrieve all tasks for the specified user.
29
+
30
+ Args:
31
+ request: FastAPI request object
32
+ user_id: The ID of the user whose tasks to retrieve
33
+ token: JWT token for authentication
34
+ session: Database session
35
+
36
+ Returns:
37
+ List of TaskRead objects
38
+
39
+ Raises:
40
+ HTTPException: If authentication fails or user_id validation fails
41
+ """
42
+ try:
43
+ # Extract and validate token
44
+ token_user_id = get_user_id_from_token(token.credentials)
45
+
46
+ # Validate that token user_id matches URL user_id
47
+ validate_user_id_from_token(
48
+ request=request,
49
+ token_user_id=token_user_id,
50
+ url_user_id=user_id
51
+ )
52
+
53
+ # Get tasks for the user
54
+ tasks = await TaskService.get_tasks_by_user_id(session, user_id)
55
+
56
+ logger.info(f"Successfully retrieved {len(tasks)} tasks for user {user_id}")
57
+ return tasks
58
+
59
+ except HTTPException:
60
+ # Re-raise HTTP exceptions (like 401, 403, 404)
61
+ raise
62
+ except Exception as e:
63
+ logger.error(f"Error retrieving tasks for user {user_id}: {str(e)}")
64
+ raise HTTPException(
65
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
66
+ detail="Error retrieving tasks"
67
+ )
68
+
69
+
70
+ @router.post("/tasks", response_model=TaskRead, status_code=status.HTTP_201_CREATED)
71
+ async def create_task(
72
+ request: Request,
73
+ user_id: int,
74
+ task_data: TaskCreate,
75
+ token: HTTPAuthorizationCredentials = Depends(security),
76
+ session: AsyncSession = Depends(get_session_dep)
77
+ ):
78
+ """
79
+ Create a new task for the specified user.
80
+
81
+ Args:
82
+ request: FastAPI request object
83
+ user_id: The ID of the user creating the task
84
+ task_data: Task creation data
85
+ token: JWT token for authentication
86
+ session: Database session
87
+
88
+ Returns:
89
+ Created TaskRead object
90
+
91
+ Raises:
92
+ HTTPException: If authentication fails, user_id validation fails, or task creation fails
93
+ """
94
+ try:
95
+ # Extract and validate token
96
+ token_user_id = get_user_id_from_token(token.credentials)
97
+
98
+ # Validate that token user_id matches URL user_id
99
+ validate_user_id_from_token(
100
+ request=request,
101
+ token_user_id=token_user_id,
102
+ url_user_id=user_id
103
+ )
104
+
105
+ # Create the task
106
+ created_task = await TaskService.create_task(session, user_id, task_data)
107
+
108
+ logger.info(f"Successfully created task {created_task.id} for user {user_id}")
109
+ return created_task
110
+
111
+ except HTTPException:
112
+ # Re-raise HTTP exceptions (like 401, 403, 400)
113
+ raise
114
+ except Exception as e:
115
+ logger.error(f"Error creating task for user {user_id}: {str(e)}")
116
+ raise HTTPException(
117
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
118
+ detail="Error creating task"
119
+ )
120
+
121
+
122
+ @router.get("/tasks/{task_id}", response_model=TaskRead)
123
+ async def get_task(
124
+ request: Request,
125
+ user_id: int,
126
+ task_id: int,
127
+ token: HTTPAuthorizationCredentials = Depends(security),
128
+ session: AsyncSession = Depends(get_session_dep)
129
+ ):
130
+ """
131
+ Retrieve a specific task by ID for the specified user.
132
+
133
+ Args:
134
+ request: FastAPI request object
135
+ user_id: The ID of the user
136
+ task_id: The ID of the task to retrieve
137
+ token: JWT token for authentication
138
+ session: Database session
139
+
140
+ Returns:
141
+ TaskRead object
142
+
143
+ Raises:
144
+ HTTPException: If authentication fails, user_id validation fails, or task not found
145
+ """
146
+ try:
147
+ # Extract and validate token
148
+ token_user_id = get_user_id_from_token(token.credentials)
149
+
150
+ # Validate that token user_id matches URL user_id
151
+ validate_user_id_from_token(
152
+ request=request,
153
+ token_user_id=token_user_id,
154
+ url_user_id=user_id
155
+ )
156
+
157
+ # Get the specific task
158
+ task = await TaskService.get_task_by_id(session, user_id, task_id)
159
+
160
+ logger.info(f"Successfully retrieved task {task_id} for user {user_id}")
161
+ return task
162
+
163
+ except HTTPException:
164
+ # Re-raise HTTP exceptions (like 401, 403, 404)
165
+ raise
166
+ except Exception as e:
167
+ logger.error(f"Error retrieving task {task_id} for user {user_id}: {str(e)}")
168
+ raise HTTPException(
169
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
170
+ detail="Error retrieving task"
171
+ )
172
+
173
+
174
+ @router.put("/tasks/{task_id}", response_model=TaskRead)
175
+ async def update_task(
176
+ request: Request,
177
+ user_id: int,
178
+ task_id: int,
179
+ task_data: TaskUpdate,
180
+ token: HTTPAuthorizationCredentials = Depends(security),
181
+ session: AsyncSession = Depends(get_session_dep)
182
+ ):
183
+ """
184
+ Update a specific task for the specified user.
185
+
186
+ Args:
187
+ request: FastAPI request object
188
+ user_id: The ID of the user
189
+ task_id: The ID of the task to update
190
+ task_data: Task update data
191
+ token: JWT token for authentication
192
+ session: Database session
193
+
194
+ Returns:
195
+ Updated TaskRead object
196
+
197
+ Raises:
198
+ HTTPException: If authentication fails, user_id validation fails, or task not found
199
+ """
200
+ try:
201
+ # Extract and validate token
202
+ token_user_id = get_user_id_from_token(token.credentials)
203
+
204
+ # Validate that token user_id matches URL user_id
205
+ validate_user_id_from_token(
206
+ request=request,
207
+ token_user_id=token_user_id,
208
+ url_user_id=user_id
209
+ )
210
+
211
+ # Update the task
212
+ updated_task = await TaskService.update_task(session, user_id, task_id, task_data)
213
+
214
+ logger.info(f"Successfully updated task {task_id} for user {user_id}")
215
+ return updated_task
216
+
217
+ except HTTPException:
218
+ # Re-raise HTTP exceptions (like 401, 403, 404)
219
+ raise
220
+ except Exception as e:
221
+ logger.error(f"Error updating task {task_id} for user {user_id}: {str(e)}")
222
+ raise HTTPException(
223
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
224
+ detail="Error updating task"
225
+ )
226
+
227
+
228
+ @router.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
229
+ async def delete_task(
230
+ request: Request,
231
+ user_id: int,
232
+ task_id: int,
233
+ token: HTTPAuthorizationCredentials = Depends(security),
234
+ session: AsyncSession = Depends(get_session_dep)
235
+ ):
236
+ """
237
+ Delete a specific task for the specified user.
238
+
239
+ Args:
240
+ request: FastAPI request object
241
+ user_id: The ID of the user
242
+ task_id: The ID of the task to delete
243
+ token: JWT token for authentication
244
+ session: Database session
245
+
246
+ Raises:
247
+ HTTPException: If authentication fails, user_id validation fails, or task not found
248
+ """
249
+ try:
250
+ # Extract and validate token
251
+ token_user_id = get_user_id_from_token(token.credentials)
252
+
253
+ # Validate that token user_id matches URL user_id
254
+ validate_user_id_from_token(
255
+ request=request,
256
+ token_user_id=token_user_id,
257
+ url_user_id=user_id
258
+ )
259
+
260
+ # Delete the task
261
+ await TaskService.delete_task(session, user_id, task_id)
262
+
263
+ logger.info(f"Successfully deleted task {task_id} for user {user_id}")
264
+ return
265
+
266
+ except HTTPException:
267
+ # Re-raise HTTP exceptions (like 401, 403, 404)
268
+ raise
269
+ except Exception as e:
270
+ logger.error(f"Error deleting task {task_id} for user {user_id}: {str(e)}")
271
+ raise HTTPException(
272
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
273
+ detail="Error deleting task"
274
+ )
275
+
276
+
277
+ @router.patch("/tasks/{task_id}/complete", response_model=TaskRead)
278
+ async def update_task_completion(
279
+ request: Request,
280
+ user_id: int,
281
+ task_id: int,
282
+ completion_data: TaskComplete,
283
+ token: HTTPAuthorizationCredentials = Depends(security),
284
+ session: AsyncSession = Depends(get_session_dep)
285
+ ):
286
+ """
287
+ Update the completion status of a specific task for the specified user.
288
+
289
+ Args:
290
+ request: FastAPI request object
291
+ user_id: The ID of the user
292
+ task_id: The ID of the task to update
293
+ completion_data: Task completion data
294
+ token: JWT token for authentication
295
+ session: Database session
296
+
297
+ Returns:
298
+ Updated TaskRead object
299
+
300
+ Raises:
301
+ HTTPException: If authentication fails, user_id validation fails, or task not found
302
+ """
303
+ try:
304
+ # Extract and validate token
305
+ token_user_id = get_user_id_from_token(token.credentials)
306
+
307
+ # Validate that token user_id matches URL user_id
308
+ validate_user_id_from_token(
309
+ request=request,
310
+ token_user_id=token_user_id,
311
+ url_user_id=user_id
312
+ )
313
+
314
+ # Update task completion status
315
+ updated_task = await TaskService.update_task_completion(session, user_id, task_id, completion_data)
316
+
317
+ logger.info(f"Successfully updated completion status for task {task_id} for user {user_id}")
318
+ return updated_task
319
+
320
+ except HTTPException:
321
+ # Re-raise HTTP exceptions (like 401, 403, 404)
322
+ raise
323
+ except Exception as e:
324
+ logger.error(f"Error updating completion status for task {task_id} for user {user_id}: {str(e)}")
325
+ raise HTTPException(
326
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
327
+ detail="Error updating task completion status"
328
+ )
api/v1/routes/users.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
2
+ from sqlmodel.ext.asyncio.session import AsyncSession
3
+ from ...database.session import get_session_dep
4
+ from ...schemas.user import UserRead, UserCreate
5
+ from ...services.user_service import UserService
6
+ from ...utils.logging import get_logger
7
+
8
+ router = APIRouter()
9
+ logger = get_logger(__name__)
10
+
11
+ @router.get("/users/{user_id}", response_model=UserRead)
12
+ async def get_user(
13
+ request: Request,
14
+ user_id: int,
15
+ session: AsyncSession = Depends(get_session_dep)
16
+ ):
17
+ """
18
+ Retrieve a specific user by ID.
19
+
20
+ Args:
21
+ request: FastAPI request object
22
+ user_id: The ID of the user to retrieve
23
+ session: Database session
24
+
25
+ Returns:
26
+ UserRead object
27
+
28
+ Raises:
29
+ HTTPException: If user not found
30
+ """
31
+ try:
32
+ user = await UserService.get_user_by_id(session, user_id)
33
+
34
+ if not user:
35
+ logger.warning(f"User {user_id} not found")
36
+ raise HTTPException(
37
+ status_code=status.HTTP_404_NOT_FOUND,
38
+ detail="User not found"
39
+ )
40
+
41
+ logger.info(f"Successfully retrieved user {user_id}")
42
+ return user
43
+
44
+ except HTTPException:
45
+ # Re-raise HTTP exceptions
46
+ raise
47
+ except Exception as e:
48
+ logger.error(f"Error retrieving user {user_id}: {str(e)}")
49
+ raise HTTPException(
50
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
51
+ detail="Error retrieving user"
52
+ )
53
+
54
+
55
+ @router.post("/users", response_model=UserRead, status_code=status.HTTP_201_CREATED)
56
+ async def create_user(
57
+ request: Request,
58
+ user_data: UserCreate,
59
+ session: AsyncSession = Depends(get_session_dep)
60
+ ):
61
+ """
62
+ Create a new user.
63
+
64
+ Args:
65
+ request: FastAPI request object
66
+ user_data: User creation data
67
+ session: Database session
68
+
69
+ Returns:
70
+ Created UserRead object
71
+
72
+ Raises:
73
+ HTTPException: If user creation fails
74
+ """
75
+ try:
76
+ created_user = await UserService.create_user(session, user_data)
77
+
78
+ logger.info(f"Successfully created user {created_user.id}")
79
+ return created_user
80
+
81
+ except HTTPException:
82
+ # Re-raise HTTP exceptions
83
+ raise
84
+ except Exception as e:
85
+ logger.error(f"Error creating user: {str(e)}")
86
+ raise HTTPException(
87
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
88
+ detail="Error creating user"
89
+ )
auth/__init__.py ADDED
File without changes
auth/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (158 Bytes). View file
 
auth/__pycache__/jwt_handler.cpython-313.pyc ADDED
Binary file (4.98 kB). View file
 
auth/jwt_handler.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jwt
2
+ from datetime import datetime, timedelta
3
+ from typing import Optional, Dict, Any
4
+ from fastapi import HTTPException, status
5
+ from config.settings import settings
6
+ from models.user import UserRead
7
+
8
+ def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
9
+ """
10
+ Create a new access token with the provided data.
11
+
12
+ Args:
13
+ data: Dictionary containing the data to encode in the token
14
+ expires_delta: Optional timedelta for token expiration (defaults to settings value)
15
+
16
+ Returns:
17
+ Encoded JWT token as string
18
+ """
19
+ to_encode = data.copy()
20
+
21
+ if expires_delta:
22
+ expire = datetime.utcnow() + expires_delta
23
+ else:
24
+ expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
25
+
26
+ to_encode.update({"exp": expire, "iat": datetime.utcnow(), "type": "access"})
27
+
28
+ encoded_jwt = jwt.encode(
29
+ to_encode,
30
+ settings.jwt_secret,
31
+ algorithm=settings.jwt_algorithm
32
+ )
33
+
34
+ return encoded_jwt
35
+
36
+
37
+ def create_refresh_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
38
+ """
39
+ Create a new refresh token with the provided data.
40
+
41
+ Args:
42
+ data: Dictionary containing the data to encode in the token
43
+ expires_delta: Optional timedelta for token expiration (defaults to settings value)
44
+
45
+ Returns:
46
+ Encoded JWT token as string
47
+ """
48
+ to_encode = data.copy()
49
+
50
+ if expires_delta:
51
+ expire = datetime.utcnow() + expires_delta
52
+ else:
53
+ # Default refresh token expiration to 7 days
54
+ expire = datetime.utcnow() + timedelta(days=settings.refresh_token_expire_days)
55
+
56
+ to_encode.update({"exp": expire, "iat": datetime.utcnow(), "type": "refresh"})
57
+
58
+ encoded_jwt = jwt.encode(
59
+ to_encode,
60
+ settings.jwt_secret,
61
+ algorithm=settings.jwt_algorithm
62
+ )
63
+
64
+ return encoded_jwt
65
+
66
+ def verify_token(token: str) -> Dict[str, Any]:
67
+ """
68
+ Verify and decode a JWT token.
69
+
70
+ Args:
71
+ token: JWT token string to verify
72
+
73
+ Returns:
74
+ Decoded token payload as dictionary
75
+
76
+ Raises:
77
+ HTTPException: If token is invalid, expired, or cannot be decoded
78
+ """
79
+ try:
80
+ payload = jwt.decode(
81
+ token,
82
+ settings.jwt_secret,
83
+ algorithms=[settings.jwt_algorithm]
84
+ )
85
+
86
+ # Check if token is expired
87
+ if "exp" in payload:
88
+ exp_timestamp = payload["exp"]
89
+ if datetime.fromtimestamp(exp_timestamp) < datetime.utcnow():
90
+ raise HTTPException(
91
+ status_code=status.HTTP_401_UNAUTHORIZED,
92
+ detail="Token has expired",
93
+ headers={"WWW-Authenticate": "Bearer"},
94
+ )
95
+
96
+ return payload
97
+
98
+ except jwt.ExpiredSignatureError:
99
+ raise HTTPException(
100
+ status_code=status.HTTP_401_UNAUTHORIZED,
101
+ detail="Token has expired",
102
+ headers={"WWW-Authenticate": "Bearer"},
103
+ )
104
+ except jwt.InvalidTokenError:
105
+ raise HTTPException(
106
+ status_code=status.HTTP_401_UNAUTHORIZED,
107
+ detail="Could not validate credentials",
108
+ headers={"WWW-Authenticate": "Bearer"},
109
+ )
110
+ except Exception:
111
+ raise HTTPException(
112
+ status_code=status.HTTP_401_UNAUTHORIZED,
113
+ detail="Could not validate credentials",
114
+ headers={"WWW-Authenticate": "Bearer"},
115
+ )
116
+
117
+ def get_user_id_from_token(token: str) -> int:
118
+ """
119
+ Extract user ID from JWT token.
120
+
121
+ Args:
122
+ token: JWT token string
123
+
124
+ Returns:
125
+ User ID as integer
126
+
127
+ Raises:
128
+ HTTPException: If token is invalid or user_id is not in token
129
+ """
130
+ payload = verify_token(token)
131
+
132
+ user_id = payload.get("sub")
133
+ if user_id is None:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_401_UNAUTHORIZED,
136
+ detail="Could not validate credentials",
137
+ headers={"WWW-Authenticate": "Bearer"},
138
+ )
139
+
140
+ # Ensure user_id is an integer
141
+ try:
142
+ user_id = int(user_id)
143
+ except (ValueError, TypeError):
144
+ raise HTTPException(
145
+ status_code=status.HTTP_401_UNAUTHORIZED,
146
+ detail="Invalid user ID in token",
147
+ headers={"WWW-Authenticate": "Bearer"},
148
+ )
149
+
150
+ return user_id
config/__init__.py ADDED
File without changes
config/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (160 Bytes). View file
 
config/__pycache__/settings.cpython-313.pyc ADDED
Binary file (1.96 kB). View file
 
config/settings.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ from typing import Optional
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ # Load environment variables from .env file
7
+ load_dotenv()
8
+
9
+ class Settings(BaseSettings):
10
+ """
11
+ Application settings loaded from environment variables.
12
+ """
13
+ # Database settings
14
+ database_url: str = os.getenv("DATABASE_URL", "postgresql+asyncpg://username:password@localhost:5432/todo_app")
15
+ db_echo: bool = os.getenv("DB_ECHO", "False").lower() == "true"
16
+
17
+ # JWT settings
18
+ jwt_secret: str = os.getenv("BETTER_AUTH_SECRET", "your-super-secret-jwt-signing-key-here")
19
+ jwt_algorithm: str = os.getenv("JWT_ALGORITHM", "HS256")
20
+ access_token_expire_minutes: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
21
+ refresh_token_expire_days: int = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "7"))
22
+
23
+ # Application settings
24
+ app_name: str = "Todo List API"
25
+ app_version: str = "1.0.0"
26
+ debug: bool = os.getenv("DEBUG", "False").lower() == "true"
27
+
28
+ model_config = {
29
+ "env_file": ".env",
30
+ "case_sensitive": True,
31
+ "extra": "allow"
32
+ }
33
+
34
+ # Create a settings instance
35
+ settings = Settings()
database/__init__.py ADDED
File without changes
database/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (162 Bytes). View file
 
database/__pycache__/session.cpython-313.pyc ADDED
Binary file (2.69 kB). View file
 
database/session.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel.ext.asyncio.session import AsyncSession
2
+ from sqlalchemy.ext.asyncio import create_async_engine
3
+ from typing import AsyncGenerator
4
+ from contextlib import asynccontextmanager
5
+ import os
6
+ from config.settings import settings
7
+
8
+ # Create the async database engine
9
+ db_url = settings.database_url
10
+
11
+ if db_url.startswith("postgresql://"):
12
+ # Convert to asyncpg format
13
+ db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1)
14
+ elif db_url.startswith("postgresql+asyncpg://"):
15
+ # Already in correct format
16
+ db_url = db_url
17
+ elif db_url.startswith("sqlite://") and not db_url.startswith("sqlite+aiosqlite"):
18
+ # Convert to aiosqlite format
19
+ db_url = db_url.replace("sqlite://", "sqlite+aiosqlite://", 1)
20
+ elif db_url.startswith("sqlite+aiosqlite"):
21
+ # Already in correct format
22
+ db_url = db_url
23
+
24
+ # For Neon PostgreSQL with asyncpg, SSL is handled automatically
25
+ # The issue is with URL parameters that asyncpg doesn't expect
26
+ if "postgresql+asyncpg" in db_url and "?sslmode=" in db_url:
27
+ # Extract the base URL without query parameters
28
+ base_url = db_url.split('?')[0]
29
+ # For Neon, we often just need the base URL as asyncpg handles SSL automatically
30
+ db_url = base_url
31
+
32
+ # Set appropriate engine options based on database type
33
+ if "postgresql" in db_url:
34
+ # For PostgreSQL, use asyncpg with proper SSL handling
35
+ async_engine = create_async_engine(
36
+ db_url,
37
+ echo=settings.db_echo, # Set to True for SQL query logging during development
38
+ pool_pre_ping=True, # Verify connections before use
39
+ pool_recycle=300, # Recycle connections every 5 minutes
40
+ # SSL is handled automatically by asyncpg for Neon
41
+ )
42
+ else: # SQLite
43
+ async_engine = create_async_engine(
44
+ db_url,
45
+ echo=settings.db_echo, # Set to True for SQL query logging during development
46
+ )
47
+
48
+ @asynccontextmanager
49
+ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
50
+ """
51
+ Async context manager for database sessions.
52
+ Ensures the session is properly closed after use.
53
+ """
54
+ async with AsyncSession(async_engine) as session:
55
+ try:
56
+ yield session
57
+ finally:
58
+ await session.close()
59
+
60
+ async def get_session_dep():
61
+ """
62
+ Dependency function for FastAPI to provide async database sessions.
63
+ """
64
+ async with AsyncSession(async_engine) as session:
65
+ yield session
main.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.exceptions import RequestValidationError
3
+ from starlette.exceptions import HTTPException as StarletteHTTPException
4
+ from starlette.middleware.cors import CORSMiddleware
5
+ from api.v1.routes import tasks
6
+ from api.v1.routes import auth
7
+ from database.session import async_engine
8
+ from models import task, user # Import models to register them with SQLModel
9
+ from utils.exception_handlers import (
10
+ http_exception_handler,
11
+ validation_exception_handler,
12
+ general_exception_handler
13
+ )
14
+ import sqlmodel
15
+
16
+ app = FastAPI(title="Todo List API", version="1.0.0")
17
+
18
+ # Add CORS middleware
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"], # In production, replace with specific origins
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+ # Register exception handlers
28
+ app.add_exception_handler(StarletteHTTPException, http_exception_handler)
29
+ app.add_exception_handler(RequestValidationError, validation_exception_handler)
30
+ app.add_exception_handler(Exception, general_exception_handler)
31
+
32
+ @app.on_event("startup")
33
+ async def startup():
34
+ """Create database tables on startup"""
35
+ async with async_engine.begin() as conn:
36
+ await conn.run_sync(sqlmodel.SQLModel.metadata.create_all)
37
+
38
+ # Include API routes
39
+ app.include_router(tasks.router, prefix="/api/{user_id}", tags=["tasks"])
40
+ app.include_router(auth.router, prefix="/api", tags=["auth"])
41
+
42
+ @app.get("/")
43
+ def read_root():
44
+ return {"message": "Todo List API - Phase II Backend"}
45
+
46
+ @app.get("/health")
47
+ def health_check():
48
+ return {"status": "healthy"}
49
+
50
+ if __name__ == "__main__":
51
+ import uvicorn
52
+
53
+ uvicorn.run(
54
+ "main:app",
55
+ host="127.0.0.1",
56
+ port=8000,
57
+ reload=True,
58
+ )
middleware/__init__.py ADDED
File without changes
middleware/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (164 Bytes). View file
 
middleware/__pycache__/auth_middleware.cpython-313.pyc ADDED
Binary file (3.28 kB). View file
 
middleware/auth_middleware.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, HTTPException, status
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from auth.jwt_handler import verify_token, get_user_id_from_token
4
+ from typing import Optional, Dict, Any
5
+ import logging
6
+
7
+ # Set up logger
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class JWTBearer(HTTPBearer):
11
+ """
12
+ Custom JWT Bearer authentication scheme.
13
+ This class handles extracting and validating JWT tokens from request headers.
14
+ """
15
+ def __init__(self, auto_error: bool = True):
16
+ super(JWTBearer, self).__init__(auto_error=auto_error)
17
+
18
+ async def __call__(self, request: Request) -> Optional[Dict[str, Any]]:
19
+ """
20
+ Extract and validate JWT token from request.
21
+
22
+ Args:
23
+ request: FastAPI request object
24
+
25
+ Returns:
26
+ Token payload if valid, None if auto_error is False and no token
27
+
28
+ Raises:
29
+ HTTPException: If token is invalid or missing (when auto_error=True)
30
+ """
31
+ credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
32
+
33
+ if credentials:
34
+ if not credentials.scheme == "Bearer":
35
+ raise HTTPException(
36
+ status_code=status.HTTP_401_UNAUTHORIZED,
37
+ detail="Invalid authentication scheme",
38
+ )
39
+
40
+ token = credentials.credentials
41
+ return verify_token(token)
42
+ else:
43
+ raise HTTPException(
44
+ status_code=status.HTTP_401_UNAUTHORIZED,
45
+ detail="Invalid authorization code",
46
+ )
47
+
48
+ def validate_user_id_from_token(request: Request, token_user_id: int, url_user_id: int) -> bool:
49
+ """
50
+ Validate that the user_id in the JWT token matches the user_id in the URL.
51
+
52
+ Args:
53
+ request: FastAPI request object (for logging)
54
+ token_user_id: User ID extracted from JWT token
55
+ url_user_id: User ID from the URL path parameter
56
+
57
+ Returns:
58
+ True if user IDs match, raises HTTPException if they don't match
59
+ """
60
+ if token_user_id != url_user_id:
61
+ logger.warning(
62
+ f"User ID mismatch - Token: {token_user_id}, URL: {url_user_id}, Path: {request.url.path}"
63
+ )
64
+ raise HTTPException(
65
+ status_code=status.HTTP_403_FORBIDDEN,
66
+ detail="User ID in token does not match user ID in URL",
67
+ )
68
+
69
+ return True
models/__init__.py ADDED
File without changes
models/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (160 Bytes). View file
 
models/__pycache__/task.cpython-313.pyc ADDED
Binary file (3.79 kB). View file
 
models/__pycache__/user.cpython-313.pyc ADDED
Binary file (1.9 kB). View file
 
models/task.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from typing import Optional
3
+ from enum import Enum
4
+ from datetime import datetime
5
+ from .user import User
6
+
7
+ class PriorityEnum(str, Enum):
8
+ high = "high"
9
+ medium = "medium"
10
+ low = "low"
11
+
12
+ class TaskBase(SQLModel):
13
+ title: str = Field(nullable=False, max_length=255)
14
+ description: Optional[str] = Field(default=None, max_length=1000)
15
+ completed: Optional[bool] = Field(default=False) # Made optional to match frontend
16
+ priority: Optional[PriorityEnum] = Field(default=None) # Changed default to None to match frontend
17
+ due_date: Optional[str] = Field(default=None, max_length=50) # Changed from datetime to string to match frontend, added max_length for DB
18
+
19
+ class Task(TaskBase, table=True):
20
+ """
21
+ Represents a user's todo item with properties for content, status, and ownership.
22
+ """
23
+ id: Optional[int] = Field(default=None, primary_key=True)
24
+ user_id: int = Field(foreign_key="users.id", nullable=False) # Updated to match new table name
25
+ created_at: datetime = Field(default_factory=datetime.utcnow)
26
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
27
+
28
+ # Relationship to User
29
+ user: Optional[User] = Relationship(back_populates="tasks")
30
+
31
+ class TaskCreate(TaskBase):
32
+ """Schema for creating a new task."""
33
+ # Explicitly define fields to ensure they're properly inherited
34
+ title: str
35
+ description: Optional[str] = None
36
+ completed: Optional[bool] = False
37
+ priority: Optional[PriorityEnum] = None
38
+ due_date: Optional[str] = Field(default=None, max_length=50)
39
+
40
+ class TaskRead(TaskBase):
41
+ """Schema for reading task data."""
42
+ id: int
43
+ user_id: int
44
+ created_at: datetime
45
+ updated_at: datetime
46
+ priority: Optional[PriorityEnum] = None # Explicitly include new field
47
+ due_date: Optional[str] = Field(default=None, max_length=50) # Changed from datetime to string to match frontend
48
+
49
+ class TaskUpdate(SQLModel):
50
+ """Schema for updating a task."""
51
+ title: Optional[str] = None
52
+ description: Optional[str] = None
53
+ completed: Optional[bool] = None
54
+ priority: Optional[PriorityEnum] = None
55
+ due_date: Optional[str] = Field(default=None, max_length=50) # Changed from datetime to string to match frontend
56
+
57
+ class TaskComplete(SQLModel):
58
+ """Schema for updating task completion status."""
59
+ completed: bool
models/user.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from typing import Optional, List
3
+ from datetime import datetime
4
+
5
+ class UserBase(SQLModel):
6
+ email: str = Field(unique=True, nullable=False, max_length=255)
7
+ name: str = Field(nullable=False, max_length=255)
8
+
9
+ class User(UserBase, table=True):
10
+ """
11
+ Represents a registered user in the system with authentication information.
12
+ """
13
+ __tablename__ = "users" # Use 'users' instead of 'user' to avoid PostgreSQL reserved keyword
14
+
15
+ id: Optional[int] = Field(default=None, primary_key=True)
16
+ created_at: datetime = Field(default_factory=datetime.utcnow)
17
+
18
+ # Relationship to tasks
19
+ tasks: List["Task"] = Relationship(back_populates="user")
20
+
21
+ class UserCreate(UserBase):
22
+ """Schema for creating a new user."""
23
+ pass
24
+
25
+ class UserRead(UserBase):
26
+ """Schema for reading user data."""
27
+ id: int
28
+ created_at: datetime
pyproject.toml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "backend"
3
+ version = "0.1.0"
4
+ description = "Todo List API Backend - Phase II"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "fastapi>=0.115.0",
9
+ "sqlmodel>=0.0.22",
10
+ "pydantic>=2.9.2",
11
+ "pydantic-settings>=2.6.1",
12
+ "pyjwt>=2.9.0",
13
+ "python-multipart>=0.0.12",
14
+ "uvicorn[standard]>=0.32.0",
15
+ "asyncpg>=0.30.0",
16
+ "python-dotenv>=1.0.1",
17
+ "pytest>=8.3.3",
18
+ "pytest-asyncio>=0.23.7",
19
+ "httpx>=0.27.2",
20
+ ]
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ sqlmodel==0.0.22
3
+ pydantic==2.9.2
4
+ pydantic-settings==2.6.1
5
+ pyjwt==2.9.0
6
+ python-multipart==0.0.12
7
+ uvicorn[standard]==0.32.0
8
+ asyncpg==0.30.0
9
+ python-dotenv==1.0.1
10
+ pytest==8.3.3
11
+ pytest-asyncio==0.23.7
12
+ httpx==0.27.2
reset_database.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple script to recreate database tables
3
+ """
4
+
5
+ import asyncio
6
+ from sqlmodel import SQLModel
7
+ from database.session import async_engine
8
+ from models.user import User
9
+ from models.task import Task
10
+
11
+ async def reset_database():
12
+ print("Dropping and recreating database tables...")
13
+
14
+ # Drop all tables first
15
+ async with async_engine.begin() as conn:
16
+ await conn.run_sync(SQLModel.metadata.drop_all)
17
+
18
+ print("Tables dropped.")
19
+
20
+ # Create all tables
21
+ async with async_engine.begin() as conn:
22
+ await conn.run_sync(SQLModel.metadata.create_all)
23
+
24
+ print("Tables recreated successfully!")
25
+ print("Database reset complete.")
26
+
27
+ if __name__ == "__main__":
28
+ asyncio.run(reset_database())
s.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ import secrets
2
+ print(secrets.token_hex(50))
schemas/__init__.py ADDED
File without changes
schemas/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (161 Bytes). View file
 
schemas/__pycache__/user.cpython-313.pyc ADDED
Binary file (1.64 kB). View file
 
schemas/task.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ class TaskBase(BaseModel):
6
+ title: str
7
+ description: Optional[str] = None
8
+ completed: bool = False
9
+
10
+ class TaskCreate(TaskBase):
11
+ """Schema for creating a new task."""
12
+ pass
13
+
14
+ class TaskRead(TaskBase):
15
+ """Schema for reading task data."""
16
+ id: int
17
+ user_id: int
18
+ created_at: datetime
19
+ updated_at: datetime
20
+
21
+ class Config:
22
+ from_attributes = True
23
+
24
+ class TaskUpdate(BaseModel):
25
+ """Schema for updating a task."""
26
+ title: Optional[str] = None
27
+ description: Optional[str] = None
28
+ completed: Optional[bool] = None
29
+
30
+ class TaskComplete(BaseModel):
31
+ """Schema for updating task completion status."""
32
+ completed: bool
schemas/user.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ class UserBase(BaseModel):
6
+ email: str
7
+ name: str
8
+
9
+ class UserCreate(UserBase):
10
+ """Schema for creating a new user."""
11
+ pass
12
+
13
+ class UserRead(UserBase):
14
+ """Schema for reading user data."""
15
+ id: int
16
+ created_at: datetime
17
+
18
+ class Config:
19
+ from_attributes = True
20
+
21
+ class UserUpdate(BaseModel):
22
+ """Schema for updating user data."""
23
+ email: Optional[str] = None
24
+ name: Optional[str] = None