suhail commited on
Commit
7ffe51d
·
1 Parent(s): 7cebe69

Initial deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +45 -0
  2. .env +13 -0
  3. .env.example +12 -0
  4. Dockerfile +26 -0
  5. README.md +309 -11
  6. README_HF.md +33 -0
  7. alembic.ini +112 -0
  8. alembic/__pycache__/env.cpython-313.pyc +0 -0
  9. alembic/env.py +63 -0
  10. alembic/script.py.mako +24 -0
  11. alembic/versions/001_initial.py +63 -0
  12. alembic/versions/002_add_user_password.py +26 -0
  13. alembic/versions/__pycache__/001_initial.cpython-313.pyc +0 -0
  14. alembic/versions/__pycache__/002_add_user_password.cpython-313.pyc +0 -0
  15. deploy-prepare.ps1 +129 -0
  16. deploy-prepare.sh +125 -0
  17. requirements.txt +12 -0
  18. src/__pycache__/main.cpython-313.pyc +0 -0
  19. src/api/__init__.py +1 -0
  20. src/api/__pycache__/__init__.cpython-313.pyc +0 -0
  21. src/api/__pycache__/deps.cpython-313.pyc +0 -0
  22. src/api/deps.py +55 -0
  23. src/api/routes/__init__.py +1 -0
  24. src/api/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  25. src/api/routes/__pycache__/auth.cpython-313.pyc +0 -0
  26. src/api/routes/__pycache__/tasks.cpython-313.pyc +0 -0
  27. src/api/routes/auth.py +88 -0
  28. src/api/routes/tasks.py +179 -0
  29. src/core/__init__.py +1 -0
  30. src/core/__pycache__/__init__.cpython-313.pyc +0 -0
  31. src/core/__pycache__/config.cpython-313.pyc +0 -0
  32. src/core/__pycache__/database.cpython-313.pyc +0 -0
  33. src/core/__pycache__/security.cpython-313.pyc +0 -0
  34. src/core/config.py +27 -0
  35. src/core/database.py +17 -0
  36. src/core/security.py +110 -0
  37. src/main.py +34 -0
  38. src/models/__init__.py +5 -0
  39. src/models/__pycache__/__init__.cpython-313.pyc +0 -0
  40. src/models/__pycache__/task.cpython-313.pyc +0 -0
  41. src/models/__pycache__/user.cpython-313.pyc +0 -0
  42. src/models/task.py +17 -0
  43. src/models/user.py +16 -0
  44. src/schemas/__init__.py +1 -0
  45. src/schemas/__pycache__/__init__.cpython-313.pyc +0 -0
  46. src/schemas/__pycache__/auth.cpython-313.pyc +0 -0
  47. src/schemas/__pycache__/task.cpython-313.pyc +0 -0
  48. src/schemas/auth.py +47 -0
  49. src/schemas/task.py +102 -0
  50. src/services/__init__.py +1 -0
.dockerignore ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ ENV/
10
+ .venv
11
+
12
+ # Testing
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+ *.cover
17
+
18
+ # IDE
19
+ .vscode/
20
+ .idea/
21
+ *.swp
22
+ *.swo
23
+ *~
24
+
25
+ # Environment
26
+ .env
27
+ .env.local
28
+ .env.*.local
29
+
30
+ # Database
31
+ *.db
32
+ *.sqlite
33
+
34
+ # Logs
35
+ *.log
36
+
37
+ # OS
38
+ .DS_Store
39
+ Thumbs.db
40
+
41
+ # Alembic
42
+ alembic/versions/*.pyc
43
+
44
+ # Documentation
45
+ docs/_build/
.env ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Database Configuration
2
+ # For local PostgreSQL: postgresql://user:password@localhost:5432/todo_db
3
+ # For Neon: Use your Neon connection string from the dashboard
4
+ DATABASE_URL=postgresql://neondb_owner:npg_MmFvJBHT8Y0k@ep-silent-thunder-ab0rbvrp-pooler.eu-west-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require
5
+ # Application Settings
6
+ APP_NAME=Task CRUD API
7
+ DEBUG=True
8
+ CORS_ORIGINS=http://localhost:3000
9
+
10
+ # Authentication
11
+ BETTER_AUTH_SECRET=zMdW1P03wJvWJnLKzQ8YYO26vHeinqmR
12
+ JWT_ALGORITHM=HS256
13
+ JWT_EXPIRATION_DAYS=7
.env.example ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Database Configuration
2
+ DATABASE_URL=postgresql://user:password@host:5432/database
3
+
4
+ # Application Settings
5
+ APP_NAME=Task CRUD API
6
+ DEBUG=True
7
+ CORS_ORIGINS=http://localhost:3000
8
+
9
+ # Authentication (Placeholder for Spec 2)
10
+ # JWT_SECRET=your-secret-key-here
11
+ # JWT_ALGORITHM=HS256
12
+ # JWT_EXPIRATION_MINUTES=1440
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 slim image
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ gcc \
10
+ postgresql-client \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements first for better caching
14
+ COPY requirements.txt .
15
+
16
+ # Install Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application code
20
+ COPY . .
21
+
22
+ # Expose port 7860 (Hugging Face Spaces default)
23
+ EXPOSE 7860
24
+
25
+ # Run database migrations and start the application
26
+ CMD alembic upgrade head && uvicorn src.main:app --host 0.0.0.0 --port 7860
README.md CHANGED
@@ -1,11 +1,309 @@
1
- ---
2
- title: Taskflow Api
3
- emoji: 🦀
4
- colorFrom: green
5
- colorTo: indigo
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Task CRUD API - Backend
2
+
3
+ FastAPI backend for the Task Manager application with full CRUD operations, filtering, and sorting.
4
+
5
+ ## Tech Stack
6
+
7
+ - **Python**: 3.11+
8
+ - **FastAPI**: 0.104+ (Web framework)
9
+ - **SQLModel**: 0.0.14+ (ORM)
10
+ - **Alembic**: 1.13.0 (Database migrations)
11
+ - **PostgreSQL**: Neon Serverless or local PostgreSQL
12
+ - **Pydantic**: 2.x (Data validation)
13
+
14
+ ## Project Structure
15
+
16
+ ```
17
+ backend/
18
+ ├── src/
19
+ │ ├── api/
20
+ │ │ ├── deps.py # Dependency injection (DB session, auth stub)
21
+ │ │ └── routes/
22
+ │ │ └── tasks.py # Task CRUD endpoints
23
+ │ ├── core/
24
+ │ │ ├── config.py # Application settings
25
+ │ │ └── database.py # Database connection
26
+ │ ├── models/
27
+ │ │ ├── user.py # User model (stub)
28
+ │ │ └── task.py # Task model
29
+ │ ├── schemas/
30
+ │ │ └── task.py # Pydantic schemas
31
+ │ ├── services/
32
+ │ │ └── task_service.py # Business logic
33
+ │ └── main.py # FastAPI application
34
+ ├── alembic/
35
+ │ ├── versions/
36
+ │ │ └── 001_initial.py # Initial migration
37
+ │ └── env.py # Alembic configuration
38
+ ├── tests/ # Test directory (to be implemented)
39
+ ├── .env # Environment variables
40
+ ├── .env.example # Environment template
41
+ ├── alembic.ini # Alembic configuration
42
+ └── requirements.txt # Python dependencies
43
+ ```
44
+
45
+ ## Setup Instructions
46
+
47
+ ### 1. Install Dependencies
48
+
49
+ ```bash
50
+ cd backend
51
+ pip install -r requirements.txt
52
+ ```
53
+
54
+ ### 2. Configure Environment
55
+
56
+ Copy `.env.example` to `.env` and configure:
57
+
58
+ ```bash
59
+ cp .env.example .env
60
+ ```
61
+
62
+ Edit `.env`:
63
+
64
+ ```env
65
+ # For Neon PostgreSQL (recommended)
66
+ DATABASE_URL=postgresql://user:password@ep-xxx.neon.tech/dbname?sslmode=require
67
+
68
+ # OR for local PostgreSQL
69
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/todo_db
70
+
71
+ APP_NAME=Task CRUD API
72
+ DEBUG=True
73
+ CORS_ORIGINS=http://localhost:3000
74
+
75
+ # Authentication (REQUIRED)
76
+ BETTER_AUTH_SECRET=<generate-32-char-random-string>
77
+ JWT_ALGORITHM=HS256
78
+ JWT_EXPIRATION_DAYS=7
79
+ ```
80
+
81
+ **Generate BETTER_AUTH_SECRET:**
82
+ ```bash
83
+ # Use Python to generate a secure random secret
84
+ python -c "import secrets; print(secrets.token_urlsafe(32))"
85
+
86
+ # IMPORTANT: Use the SAME secret in both backend/.env and frontend/.env.local
87
+ ```
88
+
89
+ ### 3. Run Database Migrations
90
+
91
+ ```bash
92
+ # Apply migrations to create tables
93
+ python -m alembic upgrade head
94
+ ```
95
+
96
+ ### 4. Start Development Server
97
+
98
+ ```bash
99
+ # Start with auto-reload
100
+ uvicorn src.main:app --reload
101
+
102
+ # Server runs at http://localhost:8000
103
+ ```
104
+
105
+ ## API Endpoints
106
+
107
+ ### Authentication
108
+
109
+ | Method | Endpoint | Description | Auth Required |
110
+ |--------|----------|-------------|---------------|
111
+ | POST | `/api/auth/signup` | Register new user account | No |
112
+ | POST | `/api/auth/signin` | Authenticate and receive JWT token | No |
113
+ | GET | `/api/auth/me` | Get current user profile | Yes |
114
+
115
+ ### Tasks
116
+
117
+ | Method | Endpoint | Description | Auth Required |
118
+ |--------|----------|-------------|---------------|
119
+ | GET | `/api/tasks` | List tasks with filtering and sorting | Yes |
120
+ | POST | `/api/tasks` | Create a new task | Yes |
121
+ | GET | `/api/tasks/{id}` | Get a single task | Yes |
122
+ | PUT | `/api/tasks/{id}` | Update task (replace all fields) | Yes |
123
+ | PATCH | `/api/tasks/{id}` | Partially update task | Yes |
124
+ | DELETE | `/api/tasks/{id}` | Delete a task | Yes |
125
+
126
+ ### Query Parameters (GET /api/tasks)
127
+
128
+ - `completed`: Filter by status (true/false/null for all)
129
+ - `sort`: Sort field (created_at or updated_at)
130
+ - `order`: Sort order (asc or desc)
131
+ - `limit`: Maximum number of results
132
+ - `offset`: Number of results to skip
133
+
134
+ ### Example Requests
135
+
136
+ **Sign Up:**
137
+ ```bash
138
+ curl -X POST http://localhost:8000/api/auth/signup \
139
+ -H "Content-Type: application/json" \
140
+ -d '{
141
+ "email": "user@example.com",
142
+ "password": "SecurePass123",
143
+ "name": "John Doe"
144
+ }'
145
+ ```
146
+
147
+ **Sign In:**
148
+ ```bash
149
+ curl -X POST http://localhost:8000/api/auth/signin \
150
+ -H "Content-Type: application/json" \
151
+ -d '{
152
+ "email": "user@example.com",
153
+ "password": "SecurePass123"
154
+ }'
155
+ ```
156
+
157
+ **Get Current User (requires JWT token):**
158
+ ```bash
159
+ curl http://localhost:8000/api/auth/me \
160
+ -H "Authorization: Bearer <your-jwt-token>"
161
+ ```
162
+
163
+ **Create Task (requires JWT token):**
164
+ ```bash
165
+ curl -X POST http://localhost:8000/api/tasks \
166
+ -H "Content-Type: application/json" \
167
+ -H "Authorization: Bearer <your-jwt-token>" \
168
+ -d '{"title": "Buy groceries", "description": "Milk, eggs, bread"}'
169
+ ```
170
+
171
+ **List Active Tasks:**
172
+ ```bash
173
+ curl "http://localhost:8000/api/tasks?completed=false&sort=created_at&order=desc" \
174
+ -H "Authorization: Bearer <your-jwt-token>"
175
+ ```
176
+
177
+ **Toggle Completion:**
178
+ ```bash
179
+ curl -X PATCH http://localhost:8000/api/tasks/1 \
180
+ -H "Content-Type: application/json" \
181
+ -H "Authorization: Bearer <your-jwt-token>" \
182
+ -d '{"completed": true}'
183
+ ```
184
+
185
+ ## API Documentation
186
+
187
+ Interactive API documentation available at:
188
+ - **Swagger UI**: http://localhost:8000/docs
189
+ - **ReDoc**: http://localhost:8000/redoc
190
+
191
+ ## Database Schema
192
+
193
+ ### Tasks Table
194
+
195
+ | Column | Type | Description |
196
+ |--------|------|-------------|
197
+ | id | INTEGER | Primary key |
198
+ | user_id | INTEGER | Foreign key to users |
199
+ | title | VARCHAR(200) | Task title (required) |
200
+ | description | VARCHAR(1000) | Task description (optional) |
201
+ | completed | BOOLEAN | Completion status |
202
+ | created_at | DATETIME | Creation timestamp |
203
+ | updated_at | DATETIME | Last update timestamp |
204
+
205
+ **Indexes:**
206
+ - `ix_tasks_user_id` - User lookup
207
+ - `ix_tasks_completed` - Status filtering
208
+ - `ix_tasks_user_id_completed` - Combined user + status
209
+ - `ix_tasks_created_at` - Date sorting
210
+
211
+ ## Authentication
212
+
213
+ **Status**: JWT-based authentication with Better Auth integration
214
+
215
+ ### Authentication Flow
216
+
217
+ 1. **User Registration** (`POST /api/auth/signup`):
218
+ - Validates email format (RFC 5322)
219
+ - Validates password strength (min 8 chars, uppercase, lowercase, number)
220
+ - Hashes password with bcrypt (cost factor 12)
221
+ - Creates user account in database
222
+ - Returns user profile (no token issued)
223
+
224
+ 2. **User Sign In** (`POST /api/auth/signin`):
225
+ - Verifies email and password
226
+ - Creates JWT token with 7-day expiration
227
+ - Token includes: user_id (sub), email, issued_at (iat), expiration (exp)
228
+ - Returns token and user profile
229
+
230
+ 3. **Protected Endpoints**:
231
+ - All `/api/tasks/*` endpoints require JWT authentication
232
+ - Client must include `Authorization: Bearer <token>` header
233
+ - Backend verifies token signature using `BETTER_AUTH_SECRET`
234
+ - Extracts user_id from token and filters all queries by authenticated user
235
+ - Returns 401 Unauthorized for missing, invalid, or expired tokens
236
+
237
+ ### Security Features
238
+
239
+ - **Stateless Authentication**: No server-side session storage
240
+ - **User Data Isolation**: All task queries automatically filtered by authenticated user_id
241
+ - **Password Security**: Bcrypt hashing with cost factor 12
242
+ - **Token Expiration**: 7-day JWT expiration (configurable via JWT_EXPIRATION_DAYS)
243
+ - **Shared Secret**: BETTER_AUTH_SECRET must match between frontend and backend
244
+ - **Error Handling**: Generic error messages for invalid credentials (prevents user enumeration)
245
+
246
+ ### Token Structure
247
+
248
+ ```json
249
+ {
250
+ "sub": "123", // User ID
251
+ "email": "user@example.com",
252
+ "iat": 1704067200, // Issued at timestamp
253
+ "exp": 1704672000, // Expiration timestamp (7 days)
254
+ "iss": "better-auth" // Issuer
255
+ }
256
+ ```
257
+
258
+ ### Error Responses
259
+
260
+ - **401 TOKEN_EXPIRED**: JWT token has expired
261
+ - **401 TOKEN_INVALID**: Invalid signature or malformed token
262
+ - **401 TOKEN_MISSING**: No Authorization header provided
263
+ - **401 INVALID_CREDENTIALS**: Email or password incorrect (generic message)
264
+ - **409 EMAIL_EXISTS**: Email already registered during signup
265
+
266
+ ## Development
267
+
268
+ ### Create New Migration
269
+
270
+ ```bash
271
+ python -m alembic revision --autogenerate -m "description"
272
+ ```
273
+
274
+ ### Rollback Migration
275
+
276
+ ```bash
277
+ python -m alembic downgrade -1
278
+ ```
279
+
280
+ ### Run Tests (when implemented)
281
+
282
+ ```bash
283
+ pytest
284
+ ```
285
+
286
+ ## Troubleshooting
287
+
288
+ ### Database Connection Issues
289
+
290
+ 1. **Neon**: Ensure connection string includes `?sslmode=require`
291
+ 2. **Local PostgreSQL**: Verify PostgreSQL is running and database exists
292
+ 3. Check `.env` file has correct `DATABASE_URL`
293
+
294
+ ### Migration Errors
295
+
296
+ ```bash
297
+ # Reset database (WARNING: deletes all data)
298
+ python -m alembic downgrade base
299
+ python -m alembic upgrade head
300
+ ```
301
+
302
+ ## Next Steps
303
+
304
+ 1. Implement JWT authentication (Spec 2)
305
+ 2. Add comprehensive test suite
306
+ 3. Add API rate limiting
307
+ 4. Implement pagination metadata
308
+ 5. Add task categories/tags
309
+ 6. Deploy to production (Vercel/Railway)
README_HF.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: TaskFlow API
3
+ emoji: ✅
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # TaskFlow Backend API
12
+
13
+ FastAPI backend for TaskFlow task management application.
14
+
15
+ ## Features
16
+
17
+ - User authentication with JWT
18
+ - Task CRUD operations
19
+ - PostgreSQL database with SQLModel ORM
20
+ - RESTful API design
21
+
22
+ ## Environment Variables
23
+
24
+ Configure these in your Space settings:
25
+
26
+ - `DATABASE_URL`: PostgreSQL connection string
27
+ - `SECRET_KEY`: JWT secret key (generate a secure random string)
28
+ - `ALGORITHM`: JWT algorithm (default: HS256)
29
+ - `ACCESS_TOKEN_EXPIRE_MINUTES`: Token expiration time (default: 30)
30
+
31
+ ## API Documentation
32
+
33
+ Once deployed, visit `/docs` for interactive API documentation.
alembic.ini ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = alembic
6
+
7
+ # template used to generate migration file names
8
+ file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s
9
+
10
+ # sys.path path, will be prepended to sys.path if present.
11
+ prepend_sys_path = .
12
+
13
+ # timezone to use when rendering the date within the migration file
14
+ # as well as the filename.
15
+ # If specified, requires the python-dateutil library that can be
16
+ # installed by adding `alembic[tz]` to the pip requirements
17
+ # string value is passed to dateutil.tz.gettz()
18
+ # leave blank for localtime
19
+ # timezone =
20
+
21
+ # max length of characters to apply to the
22
+ # "slug" field
23
+ # truncate_slug_length = 40
24
+
25
+ # set to 'true' to run the environment during
26
+ # the 'revision' command, regardless of autogenerate
27
+ # revision_environment = false
28
+
29
+ # set to 'true' to allow .pyc and .pyo files without
30
+ # a source .py file to be detected as revisions in the
31
+ # versions/ directory
32
+ # sourceless = false
33
+
34
+ # version location specification; This defaults
35
+ # to alembic/versions. When using multiple version
36
+ # directories, initial revisions must be specified with --version-path.
37
+ # The path separator used here should be the separator specified by "version_path_separator" below.
38
+ # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
39
+
40
+ # version path separator; As mentioned above, this is the character used to split
41
+ # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
42
+ # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
43
+ # Valid values for version_path_separator are:
44
+ #
45
+ # version_path_separator = :
46
+ # version_path_separator = ;
47
+ # version_path_separator = space
48
+ version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
49
+
50
+ # set to 'true' to search source files recursively
51
+ # in each "version_locations" directory
52
+ # new in Alembic version 1.10
53
+ # recursive_version_locations = false
54
+
55
+ # the output encoding used when revision files
56
+ # are written from script.py.mako
57
+ # output_encoding = utf-8
58
+
59
+ sqlalchemy.url = driver://user:pass@localhost/dbname
60
+
61
+
62
+ [post_write_hooks]
63
+ # post_write_hooks defines scripts or Python functions that are run
64
+ # on newly generated revision scripts. See the documentation for further
65
+ # detail and examples
66
+
67
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
68
+ # hooks = black
69
+ # black.type = console_scripts
70
+ # black.entrypoint = black
71
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
72
+
73
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
74
+ # hooks = ruff
75
+ # ruff.type = exec
76
+ # ruff.executable = %(here)s/.venv/bin/ruff
77
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
78
+
79
+ # Logging configuration
80
+ [loggers]
81
+ keys = root,sqlalchemy,alembic
82
+
83
+ [handlers]
84
+ keys = console
85
+
86
+ [formatters]
87
+ keys = generic
88
+
89
+ [logger_root]
90
+ level = WARN
91
+ handlers = console
92
+ qualname =
93
+
94
+ [logger_sqlalchemy]
95
+ level = WARN
96
+ handlers =
97
+ qualname = sqlalchemy.engine
98
+
99
+ [logger_alembic]
100
+ level = INFO
101
+ handlers =
102
+ qualname = alembic
103
+
104
+ [handler_console]
105
+ class = StreamHandler
106
+ args = (sys.stderr,)
107
+ level = NOTSET
108
+ formatter = generic
109
+
110
+ [formatter_generic]
111
+ format = %(levelname)-5.5s [%(name)s] %(message)s
112
+ datefmt = %H:%M:%S
alembic/__pycache__/env.cpython-313.pyc ADDED
Binary file (2.81 kB). View file
 
alembic/env.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+ from sqlalchemy import engine_from_config
3
+ from sqlalchemy import pool
4
+ from alembic import context
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ # Add the src directory to the path
9
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
10
+
11
+ from src.core.config import settings
12
+ from src.models import User, Task
13
+ from sqlmodel import SQLModel
14
+
15
+ # this is the Alembic Config object
16
+ config = context.config
17
+
18
+ # Set the database URL from settings
19
+ config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
20
+
21
+ # Interpret the config file for Python logging
22
+ if config.config_file_name is not None:
23
+ fileConfig(config.config_file_name)
24
+
25
+ # Add your model's MetaData object here for 'autogenerate' support
26
+ target_metadata = SQLModel.metadata
27
+
28
+
29
+ def run_migrations_offline() -> None:
30
+ """Run migrations in 'offline' mode."""
31
+ url = config.get_main_option("sqlalchemy.url")
32
+ context.configure(
33
+ url=url,
34
+ target_metadata=target_metadata,
35
+ literal_binds=True,
36
+ dialect_opts={"paramstyle": "named"},
37
+ )
38
+
39
+ with context.begin_transaction():
40
+ context.run_migrations()
41
+
42
+
43
+ def run_migrations_online() -> None:
44
+ """Run migrations in 'online' mode."""
45
+ connectable = engine_from_config(
46
+ config.get_section(config.config_ini_section, {}),
47
+ prefix="sqlalchemy.",
48
+ poolclass=pool.NullPool,
49
+ )
50
+
51
+ with connectable.connect() as connection:
52
+ context.configure(
53
+ connection=connection, target_metadata=target_metadata
54
+ )
55
+
56
+ with context.begin_transaction():
57
+ context.run_migrations()
58
+
59
+
60
+ if context.is_offline_mode():
61
+ run_migrations_offline()
62
+ else:
63
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade() -> None:
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade() -> None:
24
+ ${downgrades if downgrades else "pass"}
alembic/versions/001_initial.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Create users and tasks tables
2
+
3
+ Revision ID: 001_initial
4
+ Revises:
5
+ Create Date: 2026-01-08
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ from sqlalchemy.dialects import postgresql
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '001_initial'
14
+ down_revision = None
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # Create users table
21
+ op.create_table(
22
+ 'users',
23
+ sa.Column('id', sa.Integer(), nullable=False),
24
+ sa.Column('email', sa.String(length=255), nullable=False),
25
+ sa.Column('name', sa.String(length=100), nullable=False),
26
+ sa.Column('created_at', sa.DateTime(), nullable=False),
27
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
28
+ sa.PrimaryKeyConstraint('id')
29
+ )
30
+ op.create_index('ix_users_email', 'users', ['email'], unique=True)
31
+
32
+ # Create tasks table
33
+ op.create_table(
34
+ 'tasks',
35
+ sa.Column('id', sa.Integer(), nullable=False),
36
+ sa.Column('user_id', sa.Integer(), nullable=False),
37
+ sa.Column('title', sa.String(length=200), nullable=False),
38
+ sa.Column('description', sa.String(length=1000), nullable=True),
39
+ sa.Column('completed', sa.Boolean(), nullable=False, server_default='false'),
40
+ sa.Column('created_at', sa.DateTime(), nullable=False),
41
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
42
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
43
+ sa.PrimaryKeyConstraint('id')
44
+ )
45
+
46
+ # Create indexes for tasks table
47
+ op.create_index('ix_tasks_user_id', 'tasks', ['user_id'])
48
+ op.create_index('ix_tasks_completed', 'tasks', ['completed'])
49
+ op.create_index('ix_tasks_user_id_completed', 'tasks', ['user_id', 'completed'])
50
+ op.create_index('ix_tasks_created_at', 'tasks', ['created_at'])
51
+
52
+
53
+ def downgrade() -> None:
54
+ # Drop indexes
55
+ op.drop_index('ix_tasks_created_at', table_name='tasks')
56
+ op.drop_index('ix_tasks_user_id_completed', table_name='tasks')
57
+ op.drop_index('ix_tasks_completed', table_name='tasks')
58
+ op.drop_index('ix_tasks_user_id', table_name='tasks')
59
+
60
+ # Drop tables
61
+ op.drop_table('tasks')
62
+ op.drop_index('ix_users_email', table_name='users')
63
+ op.drop_table('users')
alembic/versions/002_add_user_password.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add password_hash to users table
2
+
3
+ Revision ID: 002_add_user_password
4
+ Revises: 001_initial
5
+ Create Date: 2026-01-09
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '002_add_user_password'
14
+ down_revision = '001_initial'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade():
20
+ """Add password_hash column to users table."""
21
+ op.add_column('users', sa.Column('password_hash', sa.String(length=255), nullable=False))
22
+
23
+
24
+ def downgrade():
25
+ """Remove password_hash column from users table."""
26
+ op.drop_column('users', 'password_hash')
alembic/versions/__pycache__/001_initial.cpython-313.pyc ADDED
Binary file (3.57 kB). View file
 
alembic/versions/__pycache__/002_add_user_password.cpython-313.pyc ADDED
Binary file (1.17 kB). View file
 
deploy-prepare.ps1 ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quick Deployment Script for Hugging Face Spaces (PowerShell)
2
+ # This script helps prepare your backend for deployment
3
+
4
+ Write-Host "🚀 TaskFlow Backend - Hugging Face Deployment Preparation" -ForegroundColor Cyan
5
+ Write-Host "==========================================================" -ForegroundColor Cyan
6
+ Write-Host ""
7
+
8
+ # Check if we're in the backend directory
9
+ if (-not (Test-Path "requirements.txt")) {
10
+ Write-Host "❌ Error: Please run this script from the backend directory" -ForegroundColor Red
11
+ exit 1
12
+ }
13
+
14
+ Write-Host "✅ Found backend directory" -ForegroundColor Green
15
+ Write-Host ""
16
+
17
+ # Check required files
18
+ Write-Host "📋 Checking required files..." -ForegroundColor Yellow
19
+ $files = @("Dockerfile", "requirements.txt", "alembic.ini", "src/main.py")
20
+ $missing_files = @()
21
+
22
+ foreach ($file in $files) {
23
+ if (Test-Path $file) {
24
+ Write-Host " ✅ $file" -ForegroundColor Green
25
+ } else {
26
+ Write-Host " ❌ $file (missing)" -ForegroundColor Red
27
+ $missing_files += $file
28
+ }
29
+ }
30
+
31
+ if ($missing_files.Count -gt 0) {
32
+ Write-Host ""
33
+ Write-Host "❌ Missing required files. Please ensure all files are present." -ForegroundColor Red
34
+ exit 1
35
+ }
36
+
37
+ Write-Host ""
38
+ Write-Host "🔐 Generating secrets..." -ForegroundColor Yellow
39
+
40
+ # Generate BETTER_AUTH_SECRET (using .NET crypto)
41
+ $bytes = New-Object byte[] 32
42
+ $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
43
+ $rng.GetBytes($bytes)
44
+ $SECRET = [System.BitConverter]::ToString($bytes).Replace("-", "").ToLower()
45
+
46
+ Write-Host ""
47
+ Write-Host "Your BETTER_AUTH_SECRET (save this!):" -ForegroundColor Cyan
48
+ Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
49
+ Write-Host $SECRET -ForegroundColor Yellow
50
+ Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
51
+ Write-Host ""
52
+
53
+ # Create deployment notes
54
+ $deploymentNotes = @"
55
+ TaskFlow Backend - Deployment Information
56
+ Generated: $(Get-Date)
57
+
58
+ BETTER_AUTH_SECRET: $SECRET
59
+
60
+ Required Environment Variables for Hugging Face:
61
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
62
+
63
+ 1. DATABASE_URL
64
+ Get from: Neon PostgreSQL Dashboard
65
+ Format: postgresql://user:password@host/database
66
+
67
+ 2. BETTER_AUTH_SECRET
68
+ Value: $SECRET
69
+
70
+ 3. CORS_ORIGINS
71
+ Initial: http://localhost:3000
72
+ After frontend deploy: https://your-app.vercel.app,https://your-app-*.vercel.app
73
+
74
+ 4. DEBUG
75
+ Value: False
76
+
77
+ 5. JWT_ALGORITHM (optional)
78
+ Value: HS256
79
+
80
+ 6. JWT_EXPIRATION_DAYS (optional)
81
+ Value: 7
82
+
83
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84
+
85
+ Next Steps:
86
+ 1. Create Hugging Face Space at https://huggingface.co/new-space
87
+ 2. Choose Docker SDK
88
+ 3. Clone the Space repository
89
+ 4. Copy all backend files to the Space directory
90
+ 5. Rename README_HF.md to README.md
91
+ 6. Commit and push
92
+ 7. Add environment variables in Space Settings
93
+ 8. Wait for build to complete
94
+
95
+ "@
96
+
97
+ $deploymentNotes | Out-File -FilePath "DEPLOYMENT_NOTES.txt" -Encoding UTF8
98
+
99
+ Write-Host "📝 Deployment notes saved to: DEPLOYMENT_NOTES.txt" -ForegroundColor Green
100
+ Write-Host ""
101
+
102
+ # Check if Docker is available
103
+ $dockerInstalled = Get-Command docker -ErrorAction SilentlyContinue
104
+ if ($dockerInstalled) {
105
+ Write-Host "🐳 Docker is installed" -ForegroundColor Green
106
+ Write-Host ""
107
+ $response = Read-Host "Would you like to test the Docker build locally? (y/n)"
108
+ if ($response -eq "y" -or $response -eq "Y") {
109
+ Write-Host "Building Docker image..." -ForegroundColor Yellow
110
+ docker build -t taskflow-backend-test .
111
+ if ($LASTEXITCODE -eq 0) {
112
+ Write-Host "✅ Docker build successful!" -ForegroundColor Green
113
+ } else {
114
+ Write-Host "❌ Docker build failed. Please check the errors above." -ForegroundColor Red
115
+ }
116
+ }
117
+ } else {
118
+ Write-Host "ℹ️ Docker not found. Skipping local build test." -ForegroundColor Yellow
119
+ }
120
+
121
+ Write-Host ""
122
+ Write-Host "✅ Preparation complete!" -ForegroundColor Green
123
+ Write-Host ""
124
+ Write-Host "📚 Next steps:" -ForegroundColor Cyan
125
+ Write-Host " 1. Review DEPLOYMENT_NOTES.txt"
126
+ Write-Host " 2. Follow the deployment guide in ../DEPLOYMENT_GUIDE.md"
127
+ Write-Host " 3. Create your Hugging Face Space"
128
+ Write-Host " 4. Push your code"
129
+ Write-Host ""
deploy-prepare.sh ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Quick Deployment Script for Hugging Face Spaces
4
+ # This script helps prepare your backend for deployment
5
+
6
+ echo "🚀 TaskFlow Backend - Hugging Face Deployment Preparation"
7
+ echo "=========================================================="
8
+ echo ""
9
+
10
+ # Check if we're in the backend directory
11
+ if [ ! -f "requirements.txt" ]; then
12
+ echo "❌ Error: Please run this script from the backend directory"
13
+ exit 1
14
+ fi
15
+
16
+ echo "✅ Found backend directory"
17
+ echo ""
18
+
19
+ # Check required files
20
+ echo "📋 Checking required files..."
21
+ files=("Dockerfile" "requirements.txt" "alembic.ini" "src/main.py")
22
+ missing_files=()
23
+
24
+ for file in "${files[@]}"; do
25
+ if [ -f "$file" ]; then
26
+ echo " ✅ $file"
27
+ else
28
+ echo " ❌ $file (missing)"
29
+ missing_files+=("$file")
30
+ fi
31
+ done
32
+
33
+ if [ ${#missing_files[@]} -ne 0 ]; then
34
+ echo ""
35
+ echo "❌ Missing required files. Please ensure all files are present."
36
+ exit 1
37
+ fi
38
+
39
+ echo ""
40
+ echo "🔐 Generating secrets..."
41
+
42
+ # Generate BETTER_AUTH_SECRET
43
+ SECRET=$(openssl rand -hex 32)
44
+ echo ""
45
+ echo "Your BETTER_AUTH_SECRET (save this!):"
46
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
47
+ echo "$SECRET"
48
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
49
+ echo ""
50
+
51
+ # Create deployment notes
52
+ cat > DEPLOYMENT_NOTES.txt << EOF
53
+ TaskFlow Backend - Deployment Information
54
+ Generated: $(date)
55
+
56
+ BETTER_AUTH_SECRET: $SECRET
57
+
58
+ Required Environment Variables for Hugging Face:
59
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
60
+
61
+ 1. DATABASE_URL
62
+ Get from: Neon PostgreSQL Dashboard
63
+ Format: postgresql://user:password@host/database
64
+
65
+ 2. BETTER_AUTH_SECRET
66
+ Value: $SECRET
67
+
68
+ 3. CORS_ORIGINS
69
+ Initial: http://localhost:3000
70
+ After frontend deploy: https://your-app.vercel.app,https://your-app-*.vercel.app
71
+
72
+ 4. DEBUG
73
+ Value: False
74
+
75
+ 5. JWT_ALGORITHM (optional)
76
+ Value: HS256
77
+
78
+ 6. JWT_EXPIRATION_DAYS (optional)
79
+ Value: 7
80
+
81
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
+
83
+ Next Steps:
84
+ 1. Create Hugging Face Space at https://huggingface.co/new-space
85
+ 2. Choose Docker SDK
86
+ 3. Clone the Space repository
87
+ 4. Copy all backend files to the Space directory
88
+ 5. Rename README_HF.md to README.md
89
+ 6. Commit and push
90
+ 7. Add environment variables in Space Settings
91
+ 8. Wait for build to complete
92
+
93
+ EOF
94
+
95
+ echo "📝 Deployment notes saved to: DEPLOYMENT_NOTES.txt"
96
+ echo ""
97
+
98
+ # Test if Docker is available
99
+ if command -v docker &> /dev/null; then
100
+ echo "🐳 Docker is installed"
101
+ echo ""
102
+ echo "Would you like to test the Docker build locally? (y/n)"
103
+ read -r response
104
+ if [[ "$response" =~ ^[Yy]$ ]]; then
105
+ echo "Building Docker image..."
106
+ docker build -t taskflow-backend-test .
107
+ if [ $? -eq 0 ]; then
108
+ echo "✅ Docker build successful!"
109
+ else
110
+ echo "❌ Docker build failed. Please check the errors above."
111
+ fi
112
+ fi
113
+ else
114
+ echo "ℹ️ Docker not found. Skipping local build test."
115
+ fi
116
+
117
+ echo ""
118
+ echo "✅ Preparation complete!"
119
+ echo ""
120
+ echo "📚 Next steps:"
121
+ echo " 1. Review DEPLOYMENT_NOTES.txt"
122
+ echo " 2. Follow the deployment guide in ../DEPLOYMENT_GUIDE.md"
123
+ echo " 3. Create your Hugging Face Space"
124
+ echo " 4. Push your code"
125
+ echo ""
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ sqlmodel==0.0.14
3
+ pydantic==2.5.0
4
+ uvicorn[standard]==0.24.0
5
+ alembic==1.13.0
6
+ psycopg2-binary==2.9.9
7
+ python-dotenv==1.0.0
8
+ pytest==7.4.3
9
+ httpx==0.25.2
10
+ PyJWT==2.8.0
11
+ passlib[bcrypt]==1.7.4
12
+ python-multipart==0.0.6
src/__pycache__/main.cpython-313.pyc ADDED
Binary file (1.46 kB). View file
 
src/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API module initialization."""
src/api/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (245 Bytes). View file
 
src/api/__pycache__/deps.cpython-313.pyc ADDED
Binary file (1.99 kB). View file
 
src/api/deps.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import Session
2
+ from typing import Generator
3
+ from fastapi import Depends, HTTPException, status
4
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
5
+ from src.core.database import get_session
6
+ from src.core.security import verify_jwt_token
7
+ from src.core.config import settings
8
+
9
+ security = HTTPBearer()
10
+
11
+
12
+ def get_db() -> Generator[Session, None, None]:
13
+ """Get database session dependency."""
14
+ yield from get_session()
15
+
16
+
17
+ def get_current_user(
18
+ credentials: HTTPAuthorizationCredentials = Depends(security)
19
+ ) -> int:
20
+ """
21
+ Get current user ID from JWT token.
22
+
23
+ Extracts and verifies JWT from Authorization header.
24
+
25
+ Args:
26
+ credentials: HTTP Bearer credentials from Authorization header
27
+
28
+ Returns:
29
+ User ID extracted from validated token
30
+
31
+ Raises:
32
+ HTTPException: 401 if token is missing, invalid, or expired
33
+ """
34
+ if not credentials:
35
+ raise HTTPException(
36
+ status_code=status.HTTP_401_UNAUTHORIZED,
37
+ detail="Not authenticated",
38
+ headers={"WWW-Authenticate": "Bearer"}
39
+ )
40
+
41
+ token = credentials.credentials
42
+
43
+ # Verify token and extract payload
44
+ payload = verify_jwt_token(token, settings.BETTER_AUTH_SECRET)
45
+
46
+ # Extract user ID from 'sub' claim
47
+ user_id = payload.get("sub")
48
+ if not user_id:
49
+ raise HTTPException(
50
+ status_code=status.HTTP_401_UNAUTHORIZED,
51
+ detail="Invalid token payload",
52
+ headers={"WWW-Authenticate": "Bearer"}
53
+ )
54
+
55
+ return int(user_id)
src/api/routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API routes module initialization."""
src/api/routes/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (259 Bytes). View file
 
src/api/routes/__pycache__/auth.cpython-313.pyc ADDED
Binary file (3.28 kB). View file
 
src/api/routes/__pycache__/tasks.cpython-313.pyc ADDED
Binary file (5.83 kB). View file
 
src/api/routes/auth.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Authentication API routes."""
2
+ from fastapi import APIRouter, Depends, HTTPException, status
3
+ from sqlmodel import Session, select
4
+ from src.api.deps import get_db, get_current_user
5
+ from src.schemas.auth import SignupRequest, SigninRequest, SignupResponse, TokenResponse, UserProfile
6
+ from src.services.auth_service import AuthService
7
+ from src.models.user import User
8
+
9
+ router = APIRouter(prefix="/api/auth", tags=["authentication"])
10
+
11
+
12
+ @router.post("/signup", response_model=SignupResponse, status_code=status.HTTP_201_CREATED)
13
+ def signup(
14
+ signup_data: SignupRequest,
15
+ db: Session = Depends(get_db)
16
+ ):
17
+ """
18
+ Register a new user account.
19
+
20
+ Args:
21
+ signup_data: User signup information (email, password, name)
22
+ db: Database session
23
+
24
+ Returns:
25
+ SignupResponse: Created user details
26
+
27
+ Raises:
28
+ HTTPException: 400 if validation fails
29
+ HTTPException: 409 if email already exists
30
+ """
31
+ service = AuthService(db)
32
+ return service.signup(signup_data)
33
+
34
+
35
+ @router.post("/signin", response_model=TokenResponse)
36
+ def signin(
37
+ signin_data: SigninRequest,
38
+ db: Session = Depends(get_db)
39
+ ):
40
+ """
41
+ Authenticate user and issue JWT token.
42
+
43
+ Args:
44
+ signin_data: User signin credentials (email, password)
45
+ db: Database session
46
+
47
+ Returns:
48
+ TokenResponse: JWT token and user profile
49
+
50
+ Raises:
51
+ HTTPException: 401 if credentials are invalid
52
+ """
53
+ service = AuthService(db)
54
+ return service.signin(signin_data)
55
+
56
+
57
+ @router.get("/me", response_model=UserProfile)
58
+ def get_current_user_profile(
59
+ current_user_id: int = Depends(get_current_user),
60
+ db: Session = Depends(get_db)
61
+ ):
62
+ """
63
+ Get current authenticated user's profile.
64
+
65
+ Args:
66
+ current_user_id: ID of authenticated user from JWT token
67
+ db: Database session
68
+
69
+ Returns:
70
+ UserProfile: Current user's profile information
71
+
72
+ Raises:
73
+ HTTPException: 404 if user not found
74
+ """
75
+ user = db.exec(select(User).where(User.id == current_user_id)).first()
76
+
77
+ if not user:
78
+ raise HTTPException(
79
+ status_code=status.HTTP_404_NOT_FOUND,
80
+ detail="User not found"
81
+ )
82
+
83
+ return UserProfile(
84
+ id=user.id,
85
+ email=user.email,
86
+ name=user.name,
87
+ created_at=user.created_at
88
+ )
src/api/routes/tasks.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task API routes."""
2
+ from typing import Optional
3
+ from fastapi import APIRouter, Depends, HTTPException, status, Query
4
+ from sqlmodel import Session
5
+ from src.api.deps import get_db, get_current_user
6
+ from src.schemas.task import TaskCreate, TaskUpdate, TaskPatch, TaskResponse, TaskListResponse
7
+ from src.services.task_service import TaskService
8
+
9
+ router = APIRouter(prefix="/api/tasks", tags=["tasks"])
10
+
11
+
12
+ @router.get("", response_model=TaskListResponse)
13
+ def get_tasks(
14
+ completed: Optional[bool] = Query(None, description="Filter by completion status"),
15
+ sort: str = Query("created_at", description="Sort field (created_at or updated_at)"),
16
+ order: str = Query("desc", description="Sort order (asc or desc)"),
17
+ limit: Optional[int] = Query(None, description="Maximum number of tasks to return"),
18
+ offset: int = Query(0, description="Number of tasks to skip"),
19
+ db: Session = Depends(get_db),
20
+ current_user_id: int = Depends(get_current_user)
21
+ ):
22
+ """
23
+ Get tasks for the current user with filtering and sorting.
24
+
25
+ Query Parameters:
26
+ - completed: Filter by completion status (true/false/null for all)
27
+ - sort: Sort field (created_at or updated_at)
28
+ - order: Sort order (asc or desc)
29
+ - limit: Maximum number of tasks to return
30
+ - offset: Number of tasks to skip
31
+
32
+ Returns:
33
+ TaskListResponse: List of tasks with total count
34
+ """
35
+ service = TaskService(db)
36
+ tasks = service.get_tasks(
37
+ user_id=current_user_id,
38
+ completed=completed,
39
+ sort=sort,
40
+ order=order,
41
+ limit=limit,
42
+ offset=offset
43
+ )
44
+ return TaskListResponse(tasks=tasks, total=len(tasks))
45
+
46
+
47
+ @router.post("", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
48
+ def create_task(
49
+ task_data: TaskCreate,
50
+ db: Session = Depends(get_db),
51
+ current_user_id: int = Depends(get_current_user)
52
+ ):
53
+ """
54
+ Create a new task for the current user.
55
+
56
+ Args:
57
+ task_data: Task creation data
58
+
59
+ Returns:
60
+ TaskResponse: Created task
61
+ """
62
+ service = TaskService(db)
63
+ task = service.create_task(user_id=current_user_id, task_data=task_data)
64
+ return task
65
+
66
+
67
+ @router.get("/{task_id}", response_model=TaskResponse)
68
+ def get_task(
69
+ task_id: int,
70
+ db: Session = Depends(get_db),
71
+ current_user_id: int = Depends(get_current_user)
72
+ ):
73
+ """
74
+ Get a single task by ID.
75
+
76
+ Args:
77
+ task_id: ID of the task
78
+
79
+ Returns:
80
+ TaskResponse: Task details
81
+
82
+ Raises:
83
+ HTTPException: 404 if task not found or doesn't belong to user
84
+ """
85
+ service = TaskService(db)
86
+ task = service.get_task(task_id=task_id, user_id=current_user_id)
87
+ if not task:
88
+ raise HTTPException(
89
+ status_code=status.HTTP_404_NOT_FOUND,
90
+ detail="Task not found"
91
+ )
92
+ return task
93
+
94
+
95
+ @router.put("/{task_id}", response_model=TaskResponse)
96
+ def update_task(
97
+ task_id: int,
98
+ task_data: TaskUpdate,
99
+ db: Session = Depends(get_db),
100
+ current_user_id: int = Depends(get_current_user)
101
+ ):
102
+ """
103
+ Update a task (PUT - replaces all fields).
104
+
105
+ Args:
106
+ task_id: ID of the task
107
+ task_data: Task update data
108
+
109
+ Returns:
110
+ TaskResponse: Updated task
111
+
112
+ Raises:
113
+ HTTPException: 404 if task not found or doesn't belong to user
114
+ """
115
+ service = TaskService(db)
116
+ task = service.update_task(task_id=task_id, user_id=current_user_id, task_data=task_data)
117
+ if not task:
118
+ raise HTTPException(
119
+ status_code=status.HTTP_404_NOT_FOUND,
120
+ detail="Task not found"
121
+ )
122
+ return task
123
+
124
+
125
+ @router.patch("/{task_id}", response_model=TaskResponse)
126
+ def patch_task(
127
+ task_id: int,
128
+ task_data: TaskPatch,
129
+ db: Session = Depends(get_db),
130
+ current_user_id: int = Depends(get_current_user)
131
+ ):
132
+ """
133
+ Partially update a task (PATCH - updates only provided fields).
134
+
135
+ Args:
136
+ task_id: ID of the task
137
+ task_data: Task patch data
138
+
139
+ Returns:
140
+ TaskResponse: Updated task
141
+
142
+ Raises:
143
+ HTTPException: 404 if task not found or doesn't belong to user
144
+ """
145
+ service = TaskService(db)
146
+ task = service.patch_task(task_id=task_id, user_id=current_user_id, task_data=task_data)
147
+ if not task:
148
+ raise HTTPException(
149
+ status_code=status.HTTP_404_NOT_FOUND,
150
+ detail="Task not found"
151
+ )
152
+ return task
153
+
154
+
155
+ @router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
156
+ def delete_task(
157
+ task_id: int,
158
+ db: Session = Depends(get_db),
159
+ current_user_id: int = Depends(get_current_user)
160
+ ):
161
+ """
162
+ Delete a task.
163
+
164
+ Args:
165
+ task_id: ID of the task
166
+
167
+ Returns:
168
+ No content (204)
169
+
170
+ Raises:
171
+ HTTPException: 404 if task not found or doesn't belong to user
172
+ """
173
+ service = TaskService(db)
174
+ deleted = service.delete_task(task_id=task_id, user_id=current_user_id)
175
+ if not deleted:
176
+ raise HTTPException(
177
+ status_code=status.HTTP_404_NOT_FOUND,
178
+ detail="Task not found"
179
+ )
src/core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Core module initialization."""
src/core/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (247 Bytes). View file
 
src/core/__pycache__/config.cpython-313.pyc ADDED
Binary file (1.18 kB). View file
 
src/core/__pycache__/database.cpython-313.pyc ADDED
Binary file (786 Bytes). View file
 
src/core/__pycache__/security.cpython-313.pyc ADDED
Binary file (3.5 kB). View file
 
src/core/config.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ """Application settings."""
6
+
7
+ # Database
8
+ DATABASE_URL: str
9
+
10
+ # Application
11
+ APP_NAME: str = "Task CRUD API"
12
+ DEBUG: bool = True
13
+
14
+ # CORS
15
+ CORS_ORIGINS: str = "http://localhost:3000"
16
+
17
+ # Authentication
18
+ BETTER_AUTH_SECRET: str # Required - must be set in .env
19
+ JWT_ALGORITHM: str = "HS256"
20
+ JWT_EXPIRATION_DAYS: int = 7
21
+
22
+ class Config:
23
+ env_file = ".env"
24
+ case_sensitive = True
25
+
26
+
27
+ settings = Settings()
src/core/database.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import create_engine, Session
2
+ from .config import settings
3
+
4
+ # Create database engine
5
+ engine = create_engine(
6
+ settings.DATABASE_URL,
7
+ echo=settings.DEBUG,
8
+ pool_pre_ping=True,
9
+ pool_size=5,
10
+ max_overflow=10
11
+ )
12
+
13
+
14
+ def get_session():
15
+ """Get database session."""
16
+ with Session(engine) as session:
17
+ yield session
src/core/security.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Security utilities for authentication and authorization."""
2
+ import jwt
3
+ from datetime import datetime, timedelta
4
+ from passlib.context import CryptContext
5
+ from fastapi import HTTPException, status
6
+ from typing import Optional
7
+
8
+ # Password hashing context
9
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
10
+
11
+
12
+ def hash_password(password: str) -> str:
13
+ """
14
+ Hash a password using bcrypt.
15
+
16
+ Args:
17
+ password: Plain text password
18
+
19
+ Returns:
20
+ Hashed password string
21
+ """
22
+ return pwd_context.hash(password)
23
+
24
+
25
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
26
+ """
27
+ Verify a password against its hash.
28
+
29
+ Args:
30
+ plain_password: Plain text password to verify
31
+ hashed_password: Hashed password to compare against
32
+
33
+ Returns:
34
+ True if password matches, False otherwise
35
+ """
36
+ return pwd_context.verify(plain_password, hashed_password)
37
+
38
+
39
+ def create_jwt_token(user_id: int, email: str, secret: str, expiration_days: int = 7) -> str:
40
+ """
41
+ Create a JWT token for a user.
42
+
43
+ Args:
44
+ user_id: User's unique identifier
45
+ email: User's email address
46
+ secret: Secret key for signing the token
47
+ expiration_days: Number of days until token expires (default: 7)
48
+
49
+ Returns:
50
+ Encoded JWT token string
51
+ """
52
+ now = datetime.utcnow()
53
+ payload = {
54
+ "sub": str(user_id),
55
+ "email": email,
56
+ "iat": now,
57
+ "exp": now + timedelta(days=expiration_days),
58
+ "iss": "better-auth"
59
+ }
60
+ return jwt.encode(payload, secret, algorithm="HS256")
61
+
62
+
63
+ def verify_jwt_token(token: str, secret: str) -> dict:
64
+ """
65
+ Verify and decode a JWT token.
66
+
67
+ Args:
68
+ token: JWT token string to verify
69
+ secret: Secret key used to sign the token
70
+
71
+ Returns:
72
+ Decoded token payload as dictionary
73
+
74
+ Raises:
75
+ HTTPException: 401 if token is expired or invalid
76
+ """
77
+ try:
78
+ payload = jwt.decode(
79
+ token,
80
+ secret,
81
+ algorithms=["HS256"],
82
+ options={
83
+ "verify_signature": True,
84
+ "verify_exp": True,
85
+ "require": ["sub", "email", "iat", "exp", "iss"]
86
+ }
87
+ )
88
+
89
+ # Validate issuer
90
+ if payload.get("iss") != "better-auth":
91
+ raise HTTPException(
92
+ status_code=status.HTTP_401_UNAUTHORIZED,
93
+ detail="Invalid token issuer",
94
+ headers={"WWW-Authenticate": "Bearer"}
95
+ )
96
+
97
+ return payload
98
+
99
+ except jwt.ExpiredSignatureError:
100
+ raise HTTPException(
101
+ status_code=status.HTTP_401_UNAUTHORIZED,
102
+ detail="Token has expired",
103
+ headers={"WWW-Authenticate": "Bearer"}
104
+ )
105
+ except jwt.InvalidTokenError:
106
+ raise HTTPException(
107
+ status_code=status.HTTP_401_UNAUTHORIZED,
108
+ detail="Invalid token",
109
+ headers={"WWW-Authenticate": "Bearer"}
110
+ )
src/main.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from .core.config import settings
4
+ from .api.routes import tasks, auth
5
+
6
+ app = FastAPI(
7
+ title=settings.APP_NAME,
8
+ debug=settings.DEBUG
9
+ )
10
+
11
+ # Configure CORS
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origins=settings.CORS_ORIGINS.split(","),
15
+ allow_credentials=True,
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ )
19
+
20
+ # Register routes
21
+ app.include_router(auth.router)
22
+ app.include_router(tasks.router)
23
+
24
+
25
+ @app.get("/")
26
+ async def root():
27
+ """Root endpoint."""
28
+ return {"message": "Task CRUD API", "status": "running"}
29
+
30
+
31
+ @app.get("/health")
32
+ async def health():
33
+ """Health check endpoint."""
34
+ return {"status": "healthy"}
src/models/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Models module initialization."""
2
+ from .user import User
3
+ from .task import Task
4
+
5
+ __all__ = ["User", "Task"]
src/models/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (361 Bytes). View file
 
src/models/__pycache__/task.cpython-313.pyc ADDED
Binary file (1.36 kB). View file
 
src/models/__pycache__/user.cpython-313.pyc ADDED
Binary file (1.23 kB). View file
 
src/models/task.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+
6
+ class Task(SQLModel, table=True):
7
+ """Task entity representing a to-do item."""
8
+
9
+ __tablename__ = "tasks"
10
+
11
+ id: Optional[int] = Field(default=None, primary_key=True)
12
+ user_id: int = Field(foreign_key="users.id", nullable=False, index=True)
13
+ title: str = Field(max_length=200, nullable=False)
14
+ description: Optional[str] = Field(default=None, max_length=1000)
15
+ completed: bool = Field(default=False, nullable=False, index=True)
16
+ created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, index=True)
17
+ updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
src/models/user.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+
6
+ class User(SQLModel, table=True):
7
+ """User entity with authentication support."""
8
+
9
+ __tablename__ = "users"
10
+
11
+ id: Optional[int] = Field(default=None, primary_key=True)
12
+ email: str = Field(max_length=255, unique=True, nullable=False, index=True)
13
+ name: str = Field(max_length=100, nullable=False)
14
+ password_hash: str = Field(max_length=255, nullable=False)
15
+ created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
16
+ updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
src/schemas/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Pydantic schemas for request/response validation."""
src/schemas/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (272 Bytes). View file
 
src/schemas/__pycache__/auth.cpython-313.pyc ADDED
Binary file (3.01 kB). View file
 
src/schemas/__pycache__/task.cpython-313.pyc ADDED
Binary file (3.91 kB). View file
 
src/schemas/auth.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Authentication schemas for request/response validation."""
2
+ from pydantic import BaseModel, EmailStr, Field
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+
7
+ class SignupRequest(BaseModel):
8
+ """Request schema for user signup."""
9
+ email: EmailStr = Field(..., description="User's email address")
10
+ password: str = Field(..., min_length=8, max_length=100, description="User's password")
11
+ name: str = Field(..., min_length=1, max_length=100, description="User's display name")
12
+
13
+
14
+ class SigninRequest(BaseModel):
15
+ """Request schema for user signin."""
16
+ email: EmailStr = Field(..., description="User's email address")
17
+ password: str = Field(..., description="User's password")
18
+
19
+
20
+ class UserProfile(BaseModel):
21
+ """User profile response schema."""
22
+ id: int
23
+ email: str
24
+ name: str
25
+ created_at: datetime
26
+
27
+ class Config:
28
+ from_attributes = True
29
+
30
+
31
+ class TokenResponse(BaseModel):
32
+ """Response schema for authentication with JWT token."""
33
+ access_token: str = Field(..., description="JWT access token")
34
+ token_type: str = Field(default="bearer", description="Token type")
35
+ expires_in: int = Field(..., description="Token expiration time in seconds")
36
+ user: UserProfile
37
+
38
+
39
+ class SignupResponse(BaseModel):
40
+ """Response schema for successful signup."""
41
+ id: int
42
+ email: str
43
+ name: str
44
+ created_at: datetime
45
+
46
+ class Config:
47
+ from_attributes = True
src/schemas/task.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pydantic schemas for Task CRUD operations."""
2
+ from datetime import datetime
3
+ from typing import Optional
4
+ from pydantic import BaseModel, Field, ConfigDict
5
+
6
+
7
+ class TaskCreate(BaseModel):
8
+ """Schema for creating a new task."""
9
+ title: str = Field(..., min_length=1, max_length=200, description="Task title")
10
+ description: Optional[str] = Field(None, max_length=1000, description="Task description")
11
+
12
+ model_config = ConfigDict(
13
+ json_schema_extra={
14
+ "example": {
15
+ "title": "Buy groceries",
16
+ "description": "Milk, eggs, bread"
17
+ }
18
+ }
19
+ )
20
+
21
+
22
+ class TaskResponse(BaseModel):
23
+ """Schema for task response."""
24
+ id: int = Field(..., description="Task ID")
25
+ user_id: int = Field(..., description="User ID who owns the task")
26
+ title: str = Field(..., description="Task title")
27
+ description: Optional[str] = Field(None, description="Task description")
28
+ completed: bool = Field(..., description="Task completion status")
29
+ created_at: datetime = Field(..., description="Task creation timestamp")
30
+ updated_at: datetime = Field(..., description="Task last update timestamp")
31
+
32
+ model_config = ConfigDict(
33
+ from_attributes=True,
34
+ json_schema_extra={
35
+ "example": {
36
+ "id": 1,
37
+ "user_id": 1,
38
+ "title": "Buy groceries",
39
+ "description": "Milk, eggs, bread",
40
+ "completed": False,
41
+ "created_at": "2026-01-08T10:00:00Z",
42
+ "updated_at": "2026-01-08T10:00:00Z"
43
+ }
44
+ }
45
+ )
46
+
47
+
48
+ class TaskUpdate(BaseModel):
49
+ """Schema for updating a task (PUT - replaces all fields)."""
50
+ title: str = Field(..., min_length=1, max_length=200, description="Task title")
51
+ description: Optional[str] = Field(None, max_length=1000, description="Task description")
52
+ completed: bool = Field(..., description="Task completion status")
53
+
54
+ model_config = ConfigDict(
55
+ json_schema_extra={
56
+ "example": {
57
+ "title": "Buy groceries",
58
+ "description": "Milk, eggs, bread, cheese",
59
+ "completed": False
60
+ }
61
+ }
62
+ )
63
+
64
+
65
+ class TaskPatch(BaseModel):
66
+ """Schema for partially updating a task (PATCH - updates only provided fields)."""
67
+ title: Optional[str] = Field(None, min_length=1, max_length=200, description="Task title")
68
+ description: Optional[str] = Field(None, max_length=1000, description="Task description")
69
+ completed: Optional[bool] = Field(None, description="Task completion status")
70
+
71
+ model_config = ConfigDict(
72
+ json_schema_extra={
73
+ "example": {
74
+ "completed": True
75
+ }
76
+ }
77
+ )
78
+
79
+
80
+ class TaskListResponse(BaseModel):
81
+ """Schema for list of tasks response."""
82
+ tasks: list[TaskResponse] = Field(..., description="List of tasks")
83
+ total: int = Field(..., description="Total number of tasks")
84
+
85
+ model_config = ConfigDict(
86
+ json_schema_extra={
87
+ "example": {
88
+ "tasks": [
89
+ {
90
+ "id": 1,
91
+ "user_id": 1,
92
+ "title": "Buy groceries",
93
+ "description": "Milk, eggs, bread",
94
+ "completed": False,
95
+ "created_at": "2026-01-08T10:00:00Z",
96
+ "updated_at": "2026-01-08T10:00:00Z"
97
+ }
98
+ ],
99
+ "total": 1
100
+ }
101
+ }
102
+ )
src/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Business logic services."""