Nanny7 Claude Sonnet 4.5 commited on
Commit
e5fce98
Β·
1 Parent(s): 99a2380

feat: complete Phase 2 SaaS Full-Stack Todo Application

Browse files

Phase 2: SaaS Web Application with Authentication and Real Database

✨ Features Implemented:
- Complete FastAPI backend with PostgreSQL (Neon)
- Next.js 14 frontend with TypeScript and Tailwind
- JWT authentication with bcrypt password hashing
- Full CRUD operations for todos
- User isolation and security
- Modern responsive UI with dark mode
- API documentation with OpenAPI/Swagger

πŸ”§ Tech Stack:
Backend:
- FastAPI with SQLModel
- PostgreSQL (Neon cloud database)
- JWT authentication
- Alembic migrations
- CORS middleware

Frontend:
- Next.js 14 with App Router
- TypeScript
- Tailwind CSS
- Radix UI components
- Framer Motion animations
- Custom hooks for auth and todos

πŸ“¦ What's Included:
- Backend API with auth, todos, users, AI endpoints
- Frontend with login, register, dashboard, profile pages
- Docker Compose for local development
- Comprehensive documentation
- All test flows validated (signup, login, CRUD, logout)

πŸ”’ Security:
- Password hashing with bcrypt
- JWT token-based authentication
- User-specific data isolation
- Environment variable configuration

πŸ§ͺ Validated Flows:
βœ… User registration and login
βœ… Session management
βœ… Todo CRUD operations
βœ… Data persistence
βœ… User isolation

This completes Phase 2 of the Evolution of Todo project.
Next phase will merge to main branch for production deployment.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .claude/settings.local.json +9 -0
  2. .gitignore +236 -7
  3. .specify/memory/constitution.md +37 -350
  4. CLAUDE.md +1 -1
  5. backend/.env.example +44 -0
  6. backend/README.md +165 -0
  7. backend/alembic.ini +113 -0
  8. backend/alembic/env.py +81 -0
  9. backend/alembic/script.py.mako +26 -0
  10. backend/alembic/versions/001_initial_schema.py +185 -0
  11. backend/jest.config.js +28 -0
  12. backend/pyproject.toml +156 -0
  13. backend/src/__init__.py +0 -0
  14. backend/src/api/__init__.py +0 -0
  15. backend/src/api/ai.py +159 -0
  16. backend/src/api/auth.py +276 -0
  17. backend/src/api/deps.py +171 -0
  18. backend/src/api/todos.py +347 -0
  19. backend/src/api/users.py +173 -0
  20. backend/src/core/__init__.py +0 -0
  21. backend/src/core/config.py +132 -0
  22. backend/src/core/database.py +103 -0
  23. backend/src/core/security.py +154 -0
  24. backend/src/main.py +126 -0
  25. backend/src/models/__init__.py +0 -0
  26. backend/src/models/ai_request.py +93 -0
  27. backend/src/models/session.py +91 -0
  28. backend/src/models/todo.py +124 -0
  29. backend/src/models/user.py +65 -0
  30. backend/src/schemas/__init__.py +0 -0
  31. backend/src/schemas/auth.py +61 -0
  32. backend/src/schemas/todo.py +60 -0
  33. backend/src/schemas/user.py +66 -0
  34. backend/src/services/__init__.py +0 -0
  35. backend/src/services/ai_service.py +239 -0
  36. backend/src/services/auth_service.py +205 -0
  37. backend/src/tests/__init__.py +0 -0
  38. backend/src/utils/__init__.py +0 -0
  39. cookies.txt +5 -0
  40. docker-compose.override.yml.example +18 -0
  41. docker-compose.yml +46 -0
  42. frontend/.env.example +20 -0
  43. frontend/.eslintrc.json +13 -0
  44. frontend/.prettierrc +8 -0
  45. frontend/README.md +191 -0
  46. frontend/components.json +20 -0
  47. frontend/jest.setup.js +2 -0
  48. frontend/next-env.d.ts +5 -0
  49. frontend/next.config.js +67 -0
  50. frontend/package.json +63 -0
.claude/settings.local.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(set NODE_OPTIONS=--preserve-symlinks)",
5
+ "Bash(npm run dev:*)",
6
+ "Bash(curl:*)"
7
+ ]
8
+ }
9
+ }
.gitignore CHANGED
@@ -1,4 +1,45 @@
1
- # Python
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  __pycache__/
3
  *.py[cod]
4
  *$py.class
@@ -16,26 +57,214 @@ parts/
16
  sdist/
17
  var/
18
  wheels/
 
19
  *.egg-info/
20
  .installed.cfg
21
  *.egg
22
-
23
- # Virtual Environment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  venv/
25
  ENV/
26
  env/
27
- .venv
 
 
 
 
 
 
 
28
 
29
- # IDE
 
 
30
  .vscode/
31
  .idea/
32
  *.swp
33
  *.swo
34
  *~
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- # OS
 
 
37
  .DS_Store
 
 
 
 
 
38
  Thumbs.db
 
39
 
40
- # Project specific
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  *.log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 2 Premium Web SaaS - Comprehensive .gitignore
2
+ # Covers: Next.js 14, FastAPI, Python, Node.js, TypeScript, Tailwind, shadcn/ui
3
+
4
+ # ========================================
5
+ # Environment Variables & Secrets
6
+ # ========================================
7
+ .env
8
+ .env.local
9
+ .env.*.local
10
+ .env.development.local
11
+ .env.test.local
12
+ .env.production.local
13
+ .env.production
14
+ *.pem
15
+ *.key
16
+ *.crt
17
+ secrets/
18
+ credentials/
19
+
20
+ # ========================================
21
+ # Node.js / NPM / TypeScript
22
+ # ========================================
23
+ node_modules/
24
+ npm-debug.log*
25
+ yarn-debug.log*
26
+ yarn-error.log*
27
+ pnpm-debug.log*
28
+ dist/
29
+ dist-ssr/
30
+ build/
31
+ .next/
32
+ out/
33
+ .turbo/
34
+ .vercel/
35
+ *.tsbuildinfo
36
+ *.log
37
+ logs/
38
+ *.lnk
39
+
40
+ # ========================================
41
+ # Python / FastAPI / SQLModel
42
+ # ========================================
43
  __pycache__/
44
  *.py[cod]
45
  *$py.class
 
57
  sdist/
58
  var/
59
  wheels/
60
+ share/python-wheels/
61
  *.egg-info/
62
  .installed.cfg
63
  *.egg
64
+ MANIFEST
65
+ pip-log.txt
66
+ pip-delete-this-directory.txt
67
+ .pytest_cache/
68
+ .coverage
69
+ htmlcov/
70
+ .tox/
71
+ .nox/
72
+ .coverage.*
73
+ .cache
74
+ nosetests.xml
75
+ coverage.xml
76
+ *.cover
77
+ *.log
78
+ .pytest_cache/
79
+ .hypothesis/
80
+ .venv/
81
  venv/
82
  ENV/
83
  env/
84
+ venv.bak/
85
+ venv.stamp/
86
+
87
+ # ========================================
88
+ # Alembic (Database Migrations)
89
+ # ========================================
90
+ alembic/versions/*.pyc
91
+ alembic/versions/__pycache__/
92
 
93
+ # ========================================
94
+ # IDE & Editor Files
95
+ # ========================================
96
  .vscode/
97
  .idea/
98
  *.swp
99
  *.swo
100
  *~
101
+ .DS_Store
102
+ .DS_Store?
103
+ ._*
104
+ .Spotlight-V100
105
+ .Trashes
106
+ ehthumbs.db
107
+ Thumbs.db
108
+ *.sublime-project
109
+ *.sublime-workspace
110
+ .history/
111
+ *.fdb_cdblock/
112
+ .project
113
+ .classpath
114
+ .settings/
115
+ *.launch
116
+ .factorypath
117
+ .metadata.gradle
118
+ .gradle/
119
+ *.iml
120
+ *.ipr
121
+ *.iws
122
+ .project.settings
123
+ .settings/
124
+ .loadpath
125
+ .recommenders
126
+ .springBeans
127
+ .sts4-cache
128
+ .idea_modules/
129
+ .vs/
130
+ *.code-workspace
131
+
132
+ # ========================================
133
+ # Testing & Coverage
134
+ # ========================================
135
+ coverage/
136
+ *.lcov
137
+ .nyc_output/
138
+ playwright-report/
139
+ test-results/
140
+ *.test.tsx.snap
141
+ *.mock.tsx
142
 
143
+ # ========================================
144
+ # OS Generated Files
145
+ # ========================================
146
  .DS_Store
147
+ .DS_Store?
148
+ ._*
149
+ .Spotlight-V100
150
+ .Trashes
151
+ ehthumbs.db
152
  Thumbs.db
153
+ desktop.ini
154
 
155
+ # ========================================
156
+ # Temporary Files
157
+ # ========================================
158
+ *.tmp
159
+ *.temp
160
+ *.cache
161
+ *.bak
162
+ *.backup
163
+ *.swp
164
+ *.swo
165
+ *~.nib
166
+ *.sql
167
+ *.sqlite
168
+ *.db
169
+
170
+ # ========================================
171
+ # Build & Distribution Artifacts
172
+ # ========================================
173
+ *.tgz
174
+ *.tar.gz
175
+ *.rar
176
+ *.zip
177
+ *.7z
178
+ *.exe
179
+ *.dll
180
+ *.dylib
181
+ *.bin
182
+ *.obj/
183
+ out/
184
+ target/
185
+ *.class
186
+ *.jar
187
+ *.war
188
+ *.ear
189
+
190
+ # ========================================
191
+ # Cloud / Deployment
192
+ # ========================================
193
+ .vercel
194
+ .netlify
195
+ .firebase/
196
+ amplify/
197
+ #debug.log
198
+ *.log
199
+
200
+ # ========================================
201
+ # Package Manager Lock Files (Optional)
202
+ # ========================================
203
+ # Uncomment if you want to ignore lock files
204
+ # package-lock.json
205
+ # yarn.lock
206
+ # pnpm-lock.yaml
207
+ # poetry.lock
208
+ # Pipfile.lock
209
+
210
+ # ========================================
211
+ # Docker
212
+ # ========================================
213
+ *.dockerfile
214
+ docker-compose.override.yml
215
+
216
+ # ========================================
217
+ # Database Files
218
+ # ========================================
219
+ *.db
220
+ *.sqlite
221
+ *.sqlite3
222
+ *.db3
223
+ *.psql
224
+ *.sql
225
+ *.sqlitedb
226
+
227
+ # ========================================
228
+ # Cloudinary / Image Uploads (if local storage)
229
+ # ========================================
230
+ uploads/
231
+ public/uploads/
232
+ static/uploads/
233
+ temp/
234
+
235
+ # ========================================
236
+ # AI / ML Model Files
237
+ # ========================================
238
+ *.pkl
239
+ *.h5
240
+ *.hdf5
241
+ *.pb
242
+ *.onnx
243
+ *.ckpt
244
+ *.pt
245
+ *.pth
246
+
247
+ # ========================================
248
+ # Logs & Debugging
249
+ # ========================================
250
  *.log
251
+ logs/
252
+ *.debug
253
+ debug.log
254
+ error.log
255
+ access.log
256
+ server.log
257
+ application.log
258
+
259
+ # ========================================
260
+ # Misc
261
+ # ========================================
262
+ .site/
263
+ .sass-cache/
264
+ .jekyll-cache/
265
+ .jekyll-metadata
266
+ .jekyll-server-cache
267
+ package-lock.json
268
+ yarn.lock
269
+ pnpm-lock.yaml
270
+ nul
.specify/memory/constitution.md CHANGED
@@ -1,368 +1,55 @@
1
- <!--
2
- Sync Impact Report:
3
- Version Change: Initial β†’ 1.0.0
4
- Modified Principles: N/A (initial creation)
5
- Added Sections:
6
- - Core Principles (12 sections)
7
- - Phase Evolution Contract
8
- - Phase-Wise Enforcement Rules
9
- - Architecture Principles
10
- - Security Rules
11
- - Technology Constraints
12
- - Error Handling Rules
13
- - Change Management
14
- - Enforcement Hierarchy
15
- - Definition of Success
16
- Removed Sections: N/A
17
- Templates Requiring Updates:
18
- - .specify/templates/plan-template.md (Constitution Check section needs phase-specific gates)
19
- - .specify/templates/spec-template.md (aligned with constitution requirements)
20
- - .specify/templates/tasks-template.md (aligned with phase-based enforcement)
21
- Follow-up TODOs: None
22
- -->
23
-
24
- # Evolution of Todo Constitution
25
 
26
  ## Core Principles
27
 
28
- ### I. Purpose
29
-
30
- This project exists to demonstrate **Spec-Driven Development (SDD)** for building a system that evolves from a **simple console application** into a **cloud-native, AI-driven, event-based distributed platform**.
31
-
32
- The primary objective is **architectural discipline**, not feature velocity.
33
-
34
- ---
35
-
36
- ### II. Spec-Driven Development Only
37
-
38
- All work MUST follow this strict order:
39
-
40
- Constitution β†’ Specify β†’ Plan β†’ Tasks β†’ Implement
41
-
42
- **Mandatory Rules:**
43
- - No skipping steps
44
- - No merging steps
45
- - No code without tasks
46
-
47
- **Rationale:** This ensures every implementation decision is traceable to requirements, prevents scope creep, and maintains architectural integrity across all evolution phases.
48
-
49
- ---
50
-
51
- ### III. No Manual Coding
52
-
53
- **Non-Negotiable Rules:**
54
- - Humans MUST NOT write application code
55
- - ALL code must be generated via `/sp.implement`
56
- - Humans MAY: edit specs, review output, request regeneration
57
-
58
- **Rationale:** Manual coding bypasses the spec-driven workflow and introduces untraceable behavior changes. Manual coding equals phase failure.
59
-
60
- ---
61
-
62
- ### IV. Single Source of Truth
63
-
64
- **Mandatory Rules:**
65
- - Specs are the only authority
66
- - If behavior is not written, it does not exist
67
- - Implementation may NEVER introduce new behavior
68
-
69
- **Rationale:** Prevents implementation drift and ensures all features are properly specified, reviewed, and approved before coding begins.
70
-
71
- ---
72
-
73
- ### V. Phase Evolution Contract
74
-
75
- The project MUST evolve strictly in this order:
76
-
77
- | Phase | Scope |
78
- |-----|-----|
79
- | Phase I | In-memory console app |
80
- | Phase II | Full-stack web app |
81
- | Phase III | AI agents via MCP |
82
- | Phase IV | Kubernetes deployment |
83
- | Phase V | Event-driven cloud system |
84
-
85
- **Non-Negotiable:** No phase may skip responsibilities.
86
-
87
- **Rationale:** Each phase builds upon previous foundations. Skipping phases breaks the evolutionary principle and introduces architectural debt.
88
-
89
- ---
90
-
91
- ### VI. Stateless Services
92
-
93
- **Mandatory Rules:**
94
- - Backend services MUST be stateless
95
- - State stored in: Database or Dapr state store
96
- - Restarting services must not break functionality
97
-
98
- **Rationale:** Enables horizontal scaling, fault tolerance, and cloud-native deployment patterns. Stateful services create scaling bottlenecks and operational complexity.
99
-
100
- ---
101
-
102
- ### VII. Agent-First Design
103
-
104
- **Mandatory Rules:**
105
- - Agents invoke tools, not functions
106
- - All agent behavior must be explicit
107
- - No autonomous free-form execution
108
-
109
- **Rationale:** Explicit tool invocations are auditable, testable, and可控. Free-form execution creates unpredictable behavior and security risks.
110
-
111
- ---
112
-
113
- ### VIII. Event-Driven by Default (Phase V)
114
-
115
- **Mandatory Rules:**
116
- - Events represent facts
117
- - Consumers react independently
118
- - No synchronous dependencies
119
-
120
- **Rationale:** Enables loose coupling, independent scaling, and resilience. Synchronous dependencies create cascading failures and tight coupling.
121
-
122
- ---
123
-
124
- ### IX. Security Rules
125
-
126
- **Mandatory Rules:**
127
- - Authentication mandatory once introduced
128
- - JWT verification at backend boundary
129
- - User data isolation enforced in backend
130
- - Secrets NEVER hard-coded
131
- - No trust in frontend
132
-
133
- **Rationale:** Defense-in-depth prevents unauthorized access and data leakage. Frontend is inherently untrustworthy; backend must enforce all security rules.
134
-
135
- ---
136
-
137
- ### X. Technology Constraints
138
-
139
- **Allowed Stack:**
140
- - Frontend: Next.js (App Router)
141
- - Backend: FastAPI (Python)
142
- - ORM: SQLModel
143
- - Database: PostgreSQL (Neon)
144
- - Auth: Better Auth
145
- - AI: OpenAI Agents SDK
146
- - MCP: Official MCP SDK
147
- - Orchestration: Kubernetes
148
- - Messaging: Kafka (via Dapr)
149
-
150
- **Non-Negotiable:** Changes require spec updates.
151
-
152
- **Rationale:** Standardized stack reduces complexity, improves maintainability, and ensures team expertise depth.
153
-
154
- ---
155
-
156
- ### XI. Error Handling
157
-
158
- **Mandatory Rules:**
159
- - Errors must be user-friendly
160
- - No crashes on invalid input
161
- - System must recover gracefully
162
- - Errors must not leak internals
163
-
164
- **Rationale:** User experience and security. Crashes and leaked internals create frustration and security vulnerabilities.
165
-
166
- ---
167
-
168
- ### XII. Change Management
169
-
170
- **Change Type Mapping:**
171
 
172
- | Change Type | Required Action |
173
- |-----------|----------------|
174
- | Behavior | Update `speckit.specify` |
175
- | Architecture | Update `speckit.plan` |
176
- | Tasks | Update `speckit.tasks` |
177
- | Principles | Update this constitution |
178
 
179
- **Rationale:** Ensures all changes are properly traced through the spec-driven workflow.
 
 
 
180
 
181
- ---
 
 
 
182
 
183
- ## Phase Enforcement Rules
 
 
 
184
 
185
- ### Phase I β€” Console (Foundation)
186
 
187
- **Scope Constraints:**
188
- - Single user
189
- - In-memory only
190
- - No database
191
- - No web
192
- - No auth
193
- - No AI
194
- - No agents
195
 
196
- **Rationale:** Establish core domain logic without infrastructure complexity.
197
 
198
- ---
 
199
 
200
- ### Phase II β€” Full Stack
 
201
 
202
- **Scope Requirements:**
203
- - Persistent database
204
- - REST APIs
205
- - Frontend + backend separation
206
- - Authentication mandatory
207
- - User-level data isolation
208
 
209
- **Rationale:** Transition from prototype to production-ready application.
210
-
211
- ---
212
-
213
- ### Phase III β€” AI & MCP
214
-
215
- **Scope Requirements:**
216
- - AI agents MUST operate via MCP tools
217
- - No direct DB access by agents
218
- - Chat must be stateless
219
- - Conversation state persisted externally
220
-
221
- **Rationale:** Enable AI capabilities while maintaining security and scalability.
222
-
223
- ---
224
-
225
- ### Phase IV β€” Kubernetes
226
-
227
- **Scope Requirements:**
228
- - All services containerized
229
- - Helm charts required
230
- - Minikube parity with production
231
- - No environment-specific logic
232
-
233
- **Rationale:** Enable cloud-native deployment and operational consistency.
234
-
235
- ---
236
-
237
- ### Phase V β€” Event-Driven Cloud
238
-
239
- **Scope Requirements:**
240
- - CRUD emits events
241
- - Asynchronous consumers
242
- - Kafka via Dapr only
243
- - No service-to-service tight coupling
244
-
245
- **Rationale:** Enable distributed system patterns and independent scaling.
246
-
247
- ---
248
-
249
- ## Architecture Principles
250
-
251
- ### 1. Stateless Services
252
-
253
- Backend services MUST be stateless. State stored in:
254
- - Database (PostgreSQL/Neon)
255
- - Dapr state store (Phase III+)
256
-
257
- Restarting services must not break functionality.
258
-
259
- ---
260
-
261
- ### 2. Agent-First Design
262
-
263
- - Agents invoke tools, not functions
264
- - All agent behavior must be explicit
265
- - No autonomous free-form execution
266
-
267
- ---
268
-
269
- ### 3. Event-Driven by Default (Phase V)
270
-
271
- - Events represent facts
272
- - Consumers react independently
273
- - No synchronous dependencies
274
-
275
- ---
276
-
277
- ## Security Rules
278
-
279
- - Authentication mandatory once introduced
280
- - JWT verification at backend boundary
281
- - User data isolation enforced in backend
282
- - Secrets NEVER hard-coded
283
- - No trust in frontend
284
-
285
- ---
286
-
287
- ## Technology Stack
288
-
289
- **Allowed Technologies:**
290
- - Frontend: Next.js (App Router)
291
- - Backend: FastAPI (Python)
292
- - ORM: SQLModel
293
- - Database: PostgreSQL (Neon)
294
- - Auth: Better Auth
295
- - AI: OpenAI Agents SDK
296
- - MCP: Official MCP SDK
297
- - Orchestration: Kubernetes
298
- - Messaging: Kafka (via Dapr)
299
-
300
- **Changes require spec updates.**
301
-
302
- ---
303
-
304
- ## Error Handling Standards
305
-
306
- - Errors must be user-friendly
307
- - No crashes on invalid input
308
- - System must recover gracefully
309
- - Errors must not leak internals
310
-
311
- ---
312
-
313
- ## Enforcement Hierarchy
314
-
315
- If conflicts occur, precedence is:
316
-
317
- Constitution > Specify > Plan > Tasks > Implementation
318
-
319
- Lower layers MUST obey higher layers.
320
-
321
- ---
322
-
323
- ## Definition of Success
324
-
325
- This project is successful when:
326
-
327
- - Every feature traces to a spec
328
- - No manual code exists
329
- - Agents operate only via tools
330
- - System scales from CLI to cloud
331
- - Architecture is explainable and auditable
332
-
333
- ---
334
-
335
- ## Final Rule
336
-
337
- If it is not specified,
338
- **it is forbidden.**
339
-
340
- ---
341
 
342
  ## Governance
 
343
 
344
- ### Amendment Procedure
345
-
346
- 1. Changes to this constitution require:
347
- - Documentation of rationale
348
- - Impact analysis on existing specs
349
- - Migration plan for affected features
350
- - Version bump following semantic versioning
351
-
352
- 2. Versioning Policy:
353
- - **MAJOR**: Backward incompatible governance/principle removals or redefinitions
354
- - **MINOR**: New principle/section added or materially expanded guidance
355
- - **PATCH**: Clarifications, wording, typo fixes, non-semantic refinements
356
-
357
- 3. Compliance Review:
358
- - All PRs MUST verify constitution compliance
359
- - Plan templates MUST include constitution check gates
360
- - Spec templates MUST enforce principle requirements
361
-
362
- ### Enforcement
363
-
364
- - Constitution supersedes all other practices
365
- - Complexity MUST be justified against principles
366
- - All agents and tools MUST follow constitution rules
367
 
368
- **Version**: 1.0.0 | **Ratified**: 2026-01-20 | **Last Amended**: 2026-01-20
 
 
1
+ # [PROJECT_NAME] Constitution
2
+ <!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  ## Core Principles
5
 
6
+ ### [PRINCIPLE_1_NAME]
7
+ <!-- Example: I. Library-First -->
8
+ [PRINCIPLE_1_DESCRIPTION]
9
+ <!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ ### [PRINCIPLE_2_NAME]
12
+ <!-- Example: II. CLI Interface -->
13
+ [PRINCIPLE_2_DESCRIPTION]
14
+ <!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args β†’ stdout, errors β†’ stderr; Support JSON + human-readable formats -->
 
 
15
 
16
+ ### [PRINCIPLE_3_NAME]
17
+ <!-- Example: III. Test-First (NON-NEGOTIABLE) -->
18
+ [PRINCIPLE_3_DESCRIPTION]
19
+ <!-- Example: TDD mandatory: Tests written β†’ User approved β†’ Tests fail β†’ Then implement; Red-Green-Refactor cycle strictly enforced -->
20
 
21
+ ### [PRINCIPLE_4_NAME]
22
+ <!-- Example: IV. Integration Testing -->
23
+ [PRINCIPLE_4_DESCRIPTION]
24
+ <!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
25
 
26
+ ### [PRINCIPLE_5_NAME]
27
+ <!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
28
+ [PRINCIPLE_5_DESCRIPTION]
29
+ <!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
30
 
31
+ ### [PRINCIPLE_6_NAME]
32
 
 
 
 
 
 
 
 
 
33
 
34
+ [PRINCIPLE__DESCRIPTION]
35
 
36
+ ## [SECTION_2_NAME]
37
+ <!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
38
 
39
+ [SECTION_2_CONTENT]
40
+ <!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
41
 
42
+ ## [SECTION_3_NAME]
43
+ <!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
 
 
 
 
44
 
45
+ [SECTION_3_CONTENT]
46
+ <!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  ## Governance
49
+ <!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
50
 
51
+ [GOVERNANCE_RULES]
52
+ <!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
+ **Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
55
+ <!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- ο»Ώ# Claude Code Rules
2
 
3
  This file is generated during init for the selected agent.
4
 
 
1
+ # Claude Code Rules
2
 
3
  This file is generated during init for the selected agent.
4
 
backend/.env.example ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ========================================
2
+ # Database Configuration
3
+ # ========================================
4
+ # PostgreSQL connection string (use Neon for production)
5
+ DATABASE_URL=postgresql+psycopg://user:password@localhost:5432/todoapp
6
+
7
+ # ========================================
8
+ # JWT Authentication
9
+ # ========================================
10
+ # Secret key for JWT token signing (must be at least 32 characters)
11
+ JWT_SECRET=your-super-secret-key-change-this-in-production-min-32-chars
12
+
13
+ # ========================================
14
+ # Cloudinary Configuration (Avatar Storage)
15
+ # ========================================
16
+ CLOUDINARY_CLOUD_NAME=your-cloud-name
17
+ CLOUDINARY_API_KEY=your-api-key
18
+ CLOUDINARY_API_SECRET=your-api-secret
19
+
20
+ # ========================================
21
+ # Hugging Face AI Configuration
22
+ # ========================================
23
+ HUGGINGFACE_API_KEY=your-huggingface-api-key
24
+
25
+ # ========================================
26
+ # Frontend URL
27
+ # ========================================
28
+ # Allowed CORS origin for frontend
29
+ FRONTEND_URL=http://localhost:3000
30
+
31
+ # ========================================
32
+ # Application Settings
33
+ # ========================================
34
+ # Environment: development, staging, production
35
+ ENV=development
36
+
37
+ # API Port
38
+ PORT=8000
39
+
40
+ # ========================================
41
+ # Optional: Log Level
42
+ # ========================================
43
+ # debug, info, warning, error, critical
44
+ LOG_LEVEL=info
backend/README.md ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Todo App Backend - Phase 2
2
+
3
+ FastAPI backend for the Todo SaaS application with authentication, database, and AI integration.
4
+
5
+ ## Tech Stack
6
+
7
+ - **FastAPI** - Modern, fast web framework for building APIs
8
+ - **SQLModel** - SQLModel for ORM with Pydantic validation
9
+ - **Alembic** - Database migration tool
10
+ - **PostgreSQL** - Primary database (Neon in production)
11
+ - **JWT + bcrypt** - Secure authentication
12
+ - **Hugging Face** - AI integration for todo features
13
+ - **Cloudinary** - Avatar image storage
14
+
15
+ ## Setup
16
+
17
+ ### 1. Create virtual environment
18
+
19
+ ```bash
20
+ python -m venv venv
21
+ source venv/bin/activate # Windows: venv\Scripts\activate
22
+ ```
23
+
24
+ ### 2. Install dependencies
25
+
26
+ ```bash
27
+ pip install -e .
28
+ ```
29
+
30
+ Or install with dev dependencies:
31
+
32
+ ```bash
33
+ pip install -e ".[dev]"
34
+ ```
35
+
36
+ ### 3. Setup environment
37
+
38
+ ```bash
39
+ cp .env.example .env
40
+ # Edit .env with your configuration
41
+ ```
42
+
43
+ ### 4. Run database migrations
44
+
45
+ ```bash
46
+ alembic upgrade head
47
+ ```
48
+
49
+ ### 5. Start development server
50
+
51
+ ```bash
52
+ uvicorn src.main:app --reload --port 8000
53
+ ```
54
+
55
+ API will be available at: http://localhost:8000
56
+ API docs at: http://localhost:8000/docs
57
+
58
+ ## Project Structure
59
+
60
+ ```
61
+ backend/
62
+ β”œβ”€β”€ src/
63
+ β”‚ β”œβ”€β”€ api/ # API route handlers
64
+ β”‚ β”œβ”€β”€ core/ # Core configuration and utilities
65
+ β”‚ β”œβ”€β”€ models/ # SQLModel database models
66
+ β”‚ β”œβ”€β”€ schemas/ # Pydantic schemas for request/response
67
+ β”‚ β”œβ”€β”€ services/ # Business logic services
68
+ β”‚ β”œβ”€β”€ tests/ # Test files
69
+ β”‚ └── utils/ # Utility functions
70
+ β”œβ”€β”€ alembic/ # Database migrations
71
+ └── pyproject.toml # Project configuration
72
+ ```
73
+
74
+ ## Available Scripts
75
+
76
+ ```bash
77
+ # Development
78
+ python -m uvicorn src.main:app --reload
79
+
80
+ # Testing
81
+ pytest # Run tests
82
+ pytest --cov=src # Run with coverage
83
+
84
+ # Database migrations
85
+ alembic revision --autogenerate -m "message" # Create migration
86
+ alembic upgrade head # Apply migrations
87
+ alembic downgrade -1 # Rollback one migration
88
+
89
+ # Code quality
90
+ black . # Format code
91
+ ruff check . # Lint code
92
+ ruff check . --fix # Fix linting issues
93
+ mypy . # Type checking
94
+ ```
95
+
96
+ ## API Endpoints
97
+
98
+ ### Authentication
99
+ - `POST /auth/signup` - User registration
100
+ - `POST /auth/login` - User login
101
+ - `POST /auth/logout` - User logout
102
+ - `GET /auth/me` - Get current user
103
+
104
+ ### Todos
105
+ - `GET /todos` - List todos with filtering
106
+ - `POST /todos` - Create todo
107
+ - `GET /todos/{id}` - Get single todo
108
+ - `PUT /todos/{id}` - Update todo
109
+ - `DELETE /todos/{id}` - Delete todo
110
+ - `PATCH /todos/{id}/complete` - Mark todo complete
111
+
112
+ ### User Profile
113
+ - `GET /users/me` - Get user profile
114
+ - `PUT /users/me` - Update user profile
115
+ - `POST /users/me/avatar` - Upload avatar
116
+
117
+ ### AI Features
118
+ - `POST /ai/generate-todo` - Generate todo from text
119
+ - `POST /ai/summarize` - Summarize todos
120
+ - `POST /ai/prioritize` - Prioritize todos
121
+
122
+ ## Environment Variables
123
+
124
+ See `.env.example` for required environment variables:
125
+
126
+ - `DATABASE_URL` - PostgreSQL connection string
127
+ - `JWT_SECRET` - Secret key for JWT (min 32 chars)
128
+ - `CLOUDINARY_CLOUD_NAME` - Cloudinary cloud name
129
+ - `CLOUDINARY_API_KEY` - Cloudinary API key
130
+ - `CLOUDINARY_API_SECRET` - Cloudinary API secret
131
+ - `HUGGINGFACE_API_KEY` - Hugging Face API key
132
+ - `FRONTEND_URL` - Frontend URL for CORS
133
+
134
+ ## Development with Docker
135
+
136
+ ```bash
137
+ # Start PostgreSQL
138
+ docker-compose up -d postgres
139
+
140
+ # Run migrations
141
+ alembic upgrade head
142
+
143
+ # Start server
144
+ uvicorn src.main:app --reload
145
+ ```
146
+
147
+ ## Testing
148
+
149
+ ```bash
150
+ # Run all tests
151
+ pytest
152
+
153
+ # Run with coverage
154
+ pytest --cov=src --cov-report=html
155
+
156
+ # Run specific test file
157
+ pytest tests/test_auth.py
158
+
159
+ # Run with verbose output
160
+ pytest -v
161
+ ```
162
+
163
+ ## License
164
+
165
+ MIT
backend/alembic.ini ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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; The default value is %%(rev)s_%%(slug)s
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
+ # defaults to the current working directory.
12
+ prepend_sys_path = .
13
+
14
+ # timezone to use when rendering the date within the migration file
15
+ # as well as the filename.
16
+ # If specified, requires the python-dateutil library that can be
17
+ # installed by adding `alembic[tz]` to the pip requirements
18
+ # string value is passed to dateutil.tz.gettz()
19
+ # leave blank for localtime
20
+ # timezone =
21
+
22
+ # max length of characters to apply to the
23
+ # "slug" field
24
+ # truncate_slug_length = 40
25
+
26
+ # set to 'true' to run the environment during
27
+ # the 'revision' command, regardless of autogenerate
28
+ # revision_environment = false
29
+
30
+ # set to 'true' to allow .pyc and .pyo files without
31
+ # a source .py file to be detected as revisions in the
32
+ # versions/ directory
33
+ # sourceless = false
34
+
35
+ # version location specification; This defaults
36
+ # to alembic/versions. When using multiple version
37
+ # directories, initial revisions must be specified with --version-path.
38
+ # The path separator used here should be the separator specified by "version_path_separator" below.
39
+ # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
40
+
41
+ # version path separator; As mentioned above, this is the character used to split
42
+ # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
43
+ # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
44
+ # Valid values for version_path_separator are:
45
+ #
46
+ # version_path_separator = :
47
+ # version_path_separator = ;
48
+ # version_path_separator = space
49
+ version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
50
+
51
+ # set to 'true' to search source files recursively
52
+ # in each "version_locations" directory
53
+ # new in Alembic version 1.10
54
+ # recursive_version_locations = false
55
+
56
+ # the output encoding used when revision files
57
+ # are written from script.py.mako
58
+ # output_encoding = utf-8
59
+
60
+ sqlalchemy.url = postgresql+psycopg://user:password@localhost:5432/todoapp
61
+
62
+
63
+ [post_write_hooks]
64
+ # post_write_hooks defines scripts or Python functions that are run
65
+ # on newly generated revision scripts. See the documentation for further
66
+ # detail and examples
67
+
68
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
69
+ # hooks = black
70
+ # black.type = console_scripts
71
+ # black.entrypoint = black
72
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
73
+
74
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
75
+ # hooks = ruff
76
+ # ruff.type = exec
77
+ # ruff.executable = %(here)s/.venv/bin/ruff
78
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
79
+
80
+ # Logging configuration
81
+ [loggers]
82
+ keys = root,sqlalchemy,alembic
83
+
84
+ [handlers]
85
+ keys = console
86
+
87
+ [formatters]
88
+ keys = generic
89
+
90
+ [logger_root]
91
+ level = WARN
92
+ handlers = console
93
+ qualname =
94
+
95
+ [logger_sqlalchemy]
96
+ level = WARN
97
+ handlers =
98
+ qualname = sqlalchemy.engine
99
+
100
+ [logger_alembic]
101
+ level = INFO
102
+ handlers =
103
+ qualname = alembic
104
+
105
+ [handler_console]
106
+ class = StreamHandler
107
+ args = (sys.stderr,)
108
+ level = NOTSET
109
+ formatter = generic
110
+
111
+ [formatter_generic]
112
+ format = %(levelname)-5.5s [%(name)s] %(message)s
113
+ datefmt = %H:%M:%S
backend/alembic/env.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+
3
+ from sqlalchemy import engine_from_config, pool
4
+
5
+ from alembic import context
6
+
7
+ # Import your models here for autogenerate support
8
+ from src.core.config import settings
9
+ from src.models.user import User
10
+ from src.models.todo import Todo
11
+ from src.models.session import Session
12
+ from src.models.ai_request import AIRequest
13
+ from sqlmodel import SQLModel
14
+
15
+ # this is the Alembic Config object, which provides
16
+ # access to the values within the .ini file in use.
17
+ config = context.config
18
+
19
+ # Interpret the config file for Python logging.
20
+ # This line sets up loggers basically.
21
+ if config.config_file_name is not None:
22
+ fileConfig(config.config_file_name)
23
+
24
+ # add your model's MetaData object here
25
+ # for 'autogenerate' support
26
+ target_metadata = SQLModel.metadata
27
+
28
+ # Set the database URL from settings
29
+ config.set_main_option('sqlalchemy.url', settings.database_url)
30
+
31
+
32
+ def run_migrations_offline() -> None:
33
+ """Run migrations in 'offline' mode.
34
+
35
+ This configures the context with just a URL
36
+ and not an Engine, though an Engine is acceptable
37
+ here as well. By skipping the Engine creation
38
+ we don't even need a DBAPI to be available.
39
+
40
+ Calls to context.execute() here emit the given string to the
41
+ script output.
42
+
43
+ """
44
+ url = config.get_main_option("sqlalchemy.url")
45
+ context.configure(
46
+ url=url,
47
+ target_metadata=target_metadata,
48
+ literal_binds=True,
49
+ dialect_opts={"paramstyle": "named"},
50
+ )
51
+
52
+ with context.begin_transaction():
53
+ context.run_migrations()
54
+
55
+
56
+ def run_migrations_online() -> None:
57
+ """Run migrations in 'online' mode.
58
+
59
+ In this scenario we need to create an Engine
60
+ and associate a connection with the context.
61
+
62
+ """
63
+ connectable = engine_from_config(
64
+ config.get_section(config.config_ini_section, {}),
65
+ prefix="sqlalchemy.",
66
+ poolclass=pool.NullPool,
67
+ )
68
+
69
+ with connectable.connect() as connection:
70
+ context.configure(
71
+ connection=connection, target_metadata=target_metadata
72
+ )
73
+
74
+ with context.begin_transaction():
75
+ context.run_migrations()
76
+
77
+
78
+ if context.is_offline_mode():
79
+ run_migrations_offline()
80
+ else:
81
+ run_migrations_online()
backend/alembic/script.py.mako ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}
backend/alembic/versions/001_initial_schema.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Initial schema: users, todos, sessions, ai_requests tables
2
+
3
+ Revision ID: 001
4
+ Revises:
5
+ Create Date: 2025-01-23
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ from sqlalchemy.dialects import postgresql
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '001'
16
+ down_revision: Union[str, None] = None
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # Create users table
23
+ op.create_table(
24
+ 'users',
25
+ sa.Column(
26
+ 'id',
27
+ postgresql.UUID(as_uuid=True),
28
+ server_default=sa.text('gen_random_uuid()'),
29
+ nullable=False,
30
+ ),
31
+ sa.Column('name', sa.String(length=255), nullable=False),
32
+ sa.Column('email', sa.String(length=255), nullable=False),
33
+ sa.Column('password_hash', sa.String(length=255), nullable=False),
34
+ sa.Column('avatar_url', sa.String(length=500), nullable=True),
35
+ sa.Column(
36
+ 'created_at',
37
+ sa.DateTime(),
38
+ server_default=sa.text('CURRENT_TIMESTAMP'),
39
+ nullable=False,
40
+ ),
41
+ sa.Column(
42
+ 'updated_at',
43
+ sa.DateTime(),
44
+ server_default=sa.text('CURRENT_TIMESTAMP'),
45
+ nullable=False,
46
+ ),
47
+ sa.PrimaryKeyConstraint('id'),
48
+ sa.UniqueConstraint('email'),
49
+ )
50
+ op.create_index(op.f('ix_users_id'), 'users', ['id'])
51
+ op.create_index(op.f('ix_users_email'), 'users', ['email'])
52
+
53
+ # Create todos table
54
+ op.create_table(
55
+ 'todos',
56
+ sa.Column(
57
+ 'id',
58
+ postgresql.UUID(as_uuid=True),
59
+ server_default=sa.text('gen_random_uuid()'),
60
+ nullable=False,
61
+ ),
62
+ sa.Column('title', sa.String(length=255), nullable=False),
63
+ sa.Column('description', sa.Text(), nullable=True),
64
+ sa.Column('status', sa.String(length=50), server_default='pending', nullable=False),
65
+ sa.Column('priority', sa.String(length=50), server_default='medium', nullable=False),
66
+ sa.Column('due_date', sa.DateTime(), nullable=True),
67
+ sa.Column('completed_at', sa.DateTime(), nullable=True),
68
+ sa.Column(
69
+ 'user_id',
70
+ postgresql.UUID(as_uuid=True),
71
+ nullable=False,
72
+ ),
73
+ sa.Column(
74
+ 'created_at',
75
+ sa.DateTime(),
76
+ server_default=sa.text('CURRENT_TIMESTAMP'),
77
+ nullable=False,
78
+ ),
79
+ sa.Column(
80
+ 'updated_at',
81
+ sa.DateTime(),
82
+ server_default=sa.text('CURRENT_TIMESTAMP'),
83
+ nullable=False,
84
+ ),
85
+ sa.PrimaryKeyConstraint('id'),
86
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
87
+ )
88
+ op.create_index(op.f('ix_todos_id'), 'todos', ['id'], unique=True)
89
+ op.create_index(op.f('ix_todos_user_id'), 'todos', ['user_id'])
90
+ op.create_index('idx_todos_user_status', 'todos', ['user_id', 'status'])
91
+ op.create_index('idx_todos_user_priority', 'todos', ['user_id', 'priority'])
92
+ op.create_index('idx_todos_due_date', 'todos', ['due_date'])
93
+
94
+ # Create sessions table
95
+ op.create_table(
96
+ 'sessions',
97
+ sa.Column(
98
+ 'id',
99
+ postgresql.UUID(as_uuid=True),
100
+ server_default=sa.text('gen_random_uuid()'),
101
+ nullable=False,
102
+ ),
103
+ sa.Column(
104
+ 'user_id',
105
+ postgresql.UUID(as_uuid=True),
106
+ nullable=False,
107
+ ),
108
+ sa.Column('token', sa.String(length=500), nullable=False),
109
+ sa.Column('expires_at', sa.DateTime(), nullable=False),
110
+ sa.Column(
111
+ 'created_at',
112
+ sa.DateTime(),
113
+ server_default=sa.text('CURRENT_TIMESTAMP'),
114
+ nullable=False,
115
+ ),
116
+ sa.Column('revoked_at', sa.DateTime(), nullable=True),
117
+ sa.Column('user_agent', sa.String(length=500), nullable=True),
118
+ sa.Column('ip_address', sa.String(length=45), nullable=True),
119
+ sa.PrimaryKeyConstraint('id'),
120
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
121
+ )
122
+ op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=True)
123
+ op.create_index(op.f('ix_sessions_user_id'), 'sessions', ['user_id'])
124
+ op.create_index(op.f('ix_sessions_token'), 'sessions', ['token'])
125
+ op.create_index('idx_sessions_user_expires', 'sessions', ['user_id', 'expires_at'])
126
+
127
+ # Create ai_requests table
128
+ op.create_table(
129
+ 'ai_requests',
130
+ sa.Column(
131
+ 'id',
132
+ postgresql.UUID(as_uuid=True),
133
+ server_default=sa.text('gen_random_uuid()'),
134
+ nullable=False,
135
+ ),
136
+ sa.Column(
137
+ 'user_id',
138
+ postgresql.UUID(as_uuid=True),
139
+ nullable=False,
140
+ ),
141
+ sa.Column('request_type', sa.String(length=50), nullable=False),
142
+ sa.Column('input_data', sa.Text(), nullable=False),
143
+ sa.Column('output_data', sa.Text(), nullable=True),
144
+ sa.Column('model_used', sa.String(length=100), nullable=False),
145
+ sa.Column('tokens_used', sa.Integer(), nullable=True),
146
+ sa.Column('processing_time_ms', sa.Integer(), nullable=True),
147
+ sa.Column(
148
+ 'created_at',
149
+ sa.DateTime(),
150
+ server_default=sa.text('CURRENT_TIMESTAMP'),
151
+ nullable=False,
152
+ ),
153
+ sa.PrimaryKeyConstraint('id'),
154
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
155
+ )
156
+ op.create_index(op.f('ix_ai_requests_id'), 'ai_requests', ['id'], unique=True)
157
+ op.create_index(op.f('ix_ai_requests_user_id'), 'ai_requests', ['user_id'])
158
+ op.create_index('idx_ai_requests_user_type', 'ai_requests', ['user_id', 'request_type'])
159
+ op.create_index('idx_ai_requests_created', 'ai_requests', ['created_at'])
160
+
161
+
162
+ def downgrade() -> None:
163
+ # Drop tables in reverse order
164
+ op.drop_index('idx_ai_requests_created', table_name='ai_requests')
165
+ op.drop_index('idx_ai_requests_user_type', table_name='ai_requests')
166
+ op.drop_index(op.f('ix_ai_requests_user_id'), table_name='ai_requests')
167
+ op.drop_index(op.f('ix_ai_requests_id'), table_name='ai_requests')
168
+ op.drop_table('ai_requests')
169
+
170
+ op.drop_index('idx_sessions_user_expires', table_name='sessions')
171
+ op.drop_index(op.f('ix_sessions_token'), table_name='sessions')
172
+ op.drop_index(op.f('ix_sessions_user_id'), table_name='sessions')
173
+ op.drop_index(op.f('ix_sessions_id'), table_name='sessions')
174
+ op.drop_table('sessions')
175
+
176
+ op.drop_index('idx_todos_due_date', table_name='todos')
177
+ op.drop_index('idx_todos_user_priority', table_name='todos')
178
+ op.drop_index('idx_todos_user_status', table_name='todos')
179
+ op.drop_index(op.f('ix_todos_user_id'), table_name='todos')
180
+ op.drop_index(op.f('ix_todos_id'), table_name='todos')
181
+ op.drop_table('todos')
182
+
183
+ op.drop_index(op.f('ix_users_email'), table_name='users')
184
+ op.drop_index(op.f('ix_users_id'), table_name='users')
185
+ op.drop_table('users')
backend/jest.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const nextJest = require('next/jest');
2
+
3
+ const createJestConfig = nextJest({
4
+ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
5
+ dir: './',
6
+ });
7
+
8
+ // Add any custom config to be passed to Jest
9
+ const customJestConfig = {
10
+ setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
11
+ testEnvironment: 'jest-environment-jsdom',
12
+ moduleNameMapper: {
13
+ '^@/(.*)$': '<rootDir>/src/$1',
14
+ },
15
+ collectCoverageFrom: [
16
+ 'src/**/*.{js,jsx,ts,tsx}',
17
+ '!src/**/*.d.ts',
18
+ '!src/**/*.stories.{js,jsx,ts,tsx}',
19
+ '!src/**/__tests__/**',
20
+ ],
21
+ testMatch: [
22
+ '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
23
+ '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
24
+ ],
25
+ };
26
+
27
+ // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
28
+ module.exports = createJestConfig(customJestConfig);
backend/pyproject.toml ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "todo-app-backend"
3
+ version = "0.1.0"
4
+ description = "Phase 2 Backend API for Todo SaaS Application"
5
+ authors = [
6
+ {name = "User", email = "user@example.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Framework :: FastAPI",
15
+ "Intended Audience :: Developers",
16
+ ]
17
+
18
+ dependencies = [
19
+ "fastapi>=0.109.0",
20
+ "uvicorn[standard]>=0.27.0",
21
+ "sqlmodel>=0.0.14",
22
+ "alembic>=1.13.0",
23
+ "psycopg[binary]>=3.1.0",
24
+ "python-jose[cryptography]>=3.3.0",
25
+ "passlib[bcrypt]>=1.7.4",
26
+ "python-multipart>=0.0.6",
27
+ "cloudinary>=1.40.0",
28
+ "huggingface-hub>=0.20.0",
29
+ "httpx>=0.26.0",
30
+ "pydantic>=2.5.0",
31
+ "pydantic-settings>=2.1.0",
32
+ "python-dotenv>=1.0.0",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=7.4.0",
38
+ "pytest-asyncio>=0.23.0",
39
+ "pytest-cov>=4.1.0",
40
+ "black>=23.12.0",
41
+ "ruff>=0.1.0",
42
+ "mypy>=1.8.0",
43
+ ]
44
+
45
+ [build-system]
46
+ requires = ["setuptools>=68.0"]
47
+ build-backend = "setuptools.build_meta"
48
+
49
+ [tool.black]
50
+ line-length = 100
51
+ target-version = ['py311']
52
+ include = '\.pyi?$'
53
+ extend-exclude = '''
54
+ /(
55
+ # directories
56
+ \.eggs
57
+ | \.git
58
+ | \.hg
59
+ | \.mypy_cache
60
+ | \.tox
61
+ | \.venv
62
+ | build
63
+ | dist
64
+ | alembic/versions
65
+ )/
66
+ '''
67
+
68
+ [tool.ruff]
69
+ line-length = 100
70
+ target-version = "py311"
71
+ select = [
72
+ "E", # pycodestyle errors
73
+ "W", # pycodestyle warnings
74
+ "F", # pyflakes
75
+ "I", # isort
76
+ "B", # flake8-bugbear
77
+ "C4", # flake8-comprehensions
78
+ "UP", # pyupgrade
79
+ ]
80
+ ignore = [
81
+ "E501", # line too long (handled by black)
82
+ "B008", # do not perform function calls in argument defaults
83
+ "C901", # too complex
84
+ ]
85
+
86
+ [tool.ruff.per-file-ignores]
87
+ "__init__.py" = ["F401"]
88
+
89
+ [tool.mypy]
90
+ python_version = "3.11"
91
+ warn_return_any = true
92
+ warn_unused_configs = true
93
+ disallow_untyped_defs = true
94
+ disallow_incomplete_defs = true
95
+ check_untyped_defs = true
96
+ no_implicit_optional = true
97
+ warn_redundant_casts = true
98
+ warn_unused_ignores = true
99
+ warn_no_return = true
100
+ follow_imports = "normal"
101
+ ignore_missing_imports = true
102
+
103
+ [[tool.mypy.overrides]]
104
+ module = "alembic.*"
105
+ ignore_missing_imports = true
106
+
107
+ [tool.pytest.ini_options]
108
+ minversion = "7.0"
109
+ asyncio_mode = "auto"
110
+ testpaths = ["src/tests"]
111
+ python_files = ["test_*.py"]
112
+ python_classes = ["Test*"]
113
+ python_functions = ["test_*"]
114
+ addopts = [
115
+ "-ra",
116
+ "--strict-markers",
117
+ "--strict-config",
118
+ "--cov=src",
119
+ "--cov-report=term-missing",
120
+ "--cov-report=html",
121
+ ]
122
+
123
+ [tool.coverage.run]
124
+ source = ["src"]
125
+ omit = [
126
+ "*/tests/*",
127
+ "*/alembic/versions/*",
128
+ ]
129
+
130
+ [tool.coverage.report]
131
+ exclude_lines = [
132
+ "pragma: no cover",
133
+ "def __repr__",
134
+ "raise AssertionError",
135
+ "raise NotImplementedError",
136
+ "if __name__ == .__main__.:",
137
+ "if TYPE_CHECKING:",
138
+ "@abstractmethod",
139
+ ]
140
+
141
+ [project.scripts]
142
+ # Development commands
143
+ dev = "uvicorn src.main:app --reload --port 8000 --host 0.0.0.0"
144
+ test = "pytest"
145
+ test-cov = "pytest --cov=src --cov-report=html"
146
+
147
+ # Database migration commands
148
+ db-upgrade = "alembic upgrade head"
149
+ db-downgrade = "alembic downgrade -1"
150
+ db-migration = "alembic revision --autogenerate -m"
151
+
152
+ # Code quality commands
153
+ format = "black ."
154
+ lint = "ruff check ."
155
+ lint-fix = "ruff check . --fix"
156
+ type-check = "mypy ."
backend/src/__init__.py ADDED
File without changes
backend/src/api/__init__.py ADDED
File without changes
backend/src/api/ai.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI API routes.
3
+
4
+ Provides endpoints for AI-powered todo features.
5
+ """
6
+ from typing import List
7
+ from fastapi import APIRouter, HTTPException, status, Depends
8
+ from pydantic import BaseModel
9
+
10
+ from src.api.deps import get_current_user_id, get_db
11
+ from src.services.ai_service import ai_service
12
+ from sqlmodel import Session, select
13
+ from src.models.todo import Todo
14
+ from uuid import UUID
15
+
16
+
17
+ class AIGenerateRequest(BaseModel):
18
+ """Request schema for AI todo generation."""
19
+
20
+ goal: str
21
+
22
+
23
+ class AIGenerateResponse(BaseModel):
24
+ """Response schema for AI todo generation."""
25
+
26
+ todos: List[dict]
27
+ message: str
28
+
29
+
30
+ class AISummarizeResponse(BaseModel):
31
+ """Response schema for AI todo summarization."""
32
+
33
+ summary: str
34
+ breakdown: dict
35
+ urgent_todos: List[str]
36
+
37
+
38
+ class AIPrioritizeResponse(BaseModel):
39
+ """Response schema for AI todo prioritization."""
40
+
41
+ prioritized_todos: List[dict]
42
+ message: str
43
+
44
+
45
+ router = APIRouter()
46
+
47
+
48
+ @router.post(
49
+ '/generate-todo',
50
+ response_model=AIGenerateResponse,
51
+ summary='Generate todos with AI',
52
+ description='Generate todo suggestions from a goal using AI',
53
+ )
54
+ async def generate_todos(
55
+ request: AIGenerateRequest,
56
+ current_user_id: str = Depends(get_current_user_id),
57
+ ):
58
+ """Generate todos from a goal using AI."""
59
+ try:
60
+ result = ai_service.generate_todos(request.goal)
61
+ return AIGenerateResponse(**result)
62
+ except ValueError as e:
63
+ raise HTTPException(
64
+ status_code=status.HTTP_400_BAD_REQUEST,
65
+ detail=str(e),
66
+ )
67
+ except Exception as e:
68
+ raise HTTPException(
69
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
70
+ detail=f"AI service error: {str(e)}",
71
+ )
72
+
73
+
74
+ @router.post(
75
+ '/summarize',
76
+ response_model=AISummarizeResponse,
77
+ summary='Summarize todos with AI',
78
+ description='Get an AI-powered summary of todos',
79
+ )
80
+ async def summarize_todos(
81
+ current_user_id: str = Depends(get_current_user_id),
82
+ db: Session = Depends(get_db),
83
+ ):
84
+ """Summarize todos using AI."""
85
+ try:
86
+ # Get user's todos
87
+ query = select(Todo).where(Todo.user_id == UUID(current_user_id))
88
+ todos = db.exec(query).all()
89
+
90
+ # Convert to dict format
91
+ todos_dict = [
92
+ {
93
+ "title": t.title,
94
+ "description": t.description,
95
+ "priority": t.priority.value,
96
+ "due_date": t.due_date.isoformat() if t.due_date else None,
97
+ }
98
+ for t in todos
99
+ ]
100
+
101
+ result = ai_service.summarize_todos(todos_dict)
102
+ return AISummarizeResponse(**result)
103
+
104
+ except ValueError as e:
105
+ raise HTTPException(
106
+ status_code=status.HTTP_400_BAD_REQUEST,
107
+ detail=str(e),
108
+ )
109
+ except Exception as e:
110
+ raise HTTPException(
111
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
112
+ detail=f"AI service error: {str(e)}",
113
+ )
114
+
115
+
116
+ @router.post(
117
+ '/prioritize',
118
+ response_model=AIPrioritizeResponse,
119
+ summary='Prioritize todos with AI',
120
+ description='Get AI-powered todo prioritization',
121
+ )
122
+ async def prioritize_todos(
123
+ current_user_id: str = Depends(get_current_user_id),
124
+ db: Session = Depends(get_db),
125
+ ):
126
+ """Prioritize todos using AI."""
127
+ try:
128
+ # Get user's todos
129
+ query = select(Todo).where(Todo.user_id == UUID(current_user_id))
130
+ todos = db.exec(query).all()
131
+
132
+ # Convert to dict format with IDs
133
+ todos_dict = [
134
+ {
135
+ "id": str(t.id),
136
+ "title": t.title,
137
+ "description": t.description,
138
+ "priority": t.priority.value,
139
+ "due_date": t.due_date.isoformat() if t.due_date else None,
140
+ }
141
+ for t in todos
142
+ ]
143
+
144
+ result = ai_service.prioritize_todos(todos_dict)
145
+ return AIPrioritizeResponse(**result)
146
+
147
+ except ValueError as e:
148
+ raise HTTPException(
149
+ status_code=status.HTTP_400_BAD_REQUEST,
150
+ detail=str(e),
151
+ )
152
+ except Exception as e:
153
+ raise HTTPException(
154
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
155
+ detail=f"AI service error: {str(e)}",
156
+ )
157
+
158
+
159
+ __all__ = ['router']
backend/src/api/auth.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication API routes.
3
+
4
+ Provides endpoints for user registration, login, logout, and token verification.
5
+ """
6
+ from fastapi import APIRouter, Depends, HTTPException, Response, status
7
+ from fastapi.security import OAuth2PasswordRequestForm
8
+ from sqlmodel import Session
9
+
10
+ from src.api.deps import get_current_user, get_db
11
+ from src.core.config import settings
12
+ from src.models.user import User
13
+ from src.schemas.auth import AuthResponse, LoginRequest, SignupRequest
14
+ from src.schemas.user import UserResponse
15
+ from src.services.auth_service import (
16
+ authenticate_user,
17
+ create_user,
18
+ create_user_token,
19
+ get_user_by_email,
20
+ )
21
+
22
+ router = APIRouter()
23
+
24
+
25
+ @router.post(
26
+ '/signup',
27
+ response_model=AuthResponse,
28
+ status_code=status.HTTP_201_CREATED,
29
+ summary='Register a new user',
30
+ description='Create a new user account with email and password',
31
+ )
32
+ async def signup(
33
+ user_data: SignupRequest,
34
+ db: Session = Depends(get_db),
35
+ ):
36
+ """
37
+ Register a new user.
38
+
39
+ Validates email format, checks for duplicate emails,
40
+ validates password strength, and creates a new user.
41
+
42
+ Args:
43
+ user_data: User registration data (name, email, password)
44
+ db: Database session
45
+
46
+ Returns:
47
+ AuthResponse: JWT token and user information
48
+
49
+ Raises:
50
+ HTTPException 400: If validation fails or email already exists
51
+ """
52
+ print(f"DEBUG: Signup request received: {user_data.dict()}")
53
+ try:
54
+ # Create user
55
+ user = create_user(db, user_data)
56
+
57
+ # Generate JWT token
58
+ access_token = create_user_token(user.id)
59
+
60
+ # Return response
61
+ return AuthResponse(
62
+ access_token=access_token,
63
+ token_type='bearer',
64
+ user={
65
+ 'id': str(user.id),
66
+ 'name': user.name,
67
+ 'email': user.email,
68
+ 'avatar_url': user.avatar_url,
69
+ 'created_at': user.created_at.isoformat(),
70
+ 'updated_at': user.updated_at.isoformat(),
71
+ },
72
+ )
73
+
74
+ except ValueError as e:
75
+ # Handle validation errors
76
+ error_msg = str(e)
77
+
78
+ if 'already registered' in error_msg:
79
+ raise HTTPException(
80
+ status_code=status.HTTP_400_BAD_REQUEST,
81
+ detail='Email is already registered. Please use a different email or login.',
82
+ )
83
+ elif 'Password' in error_msg:
84
+ raise HTTPException(
85
+ status_code=status.HTTP_400_BAD_REQUEST,
86
+ detail=error_msg,
87
+ )
88
+ else:
89
+ raise HTTPException(
90
+ status_code=status.HTTP_400_BAD_REQUEST,
91
+ detail='Validation failed: ' + error_msg,
92
+ )
93
+
94
+ except Exception as e:
95
+ # Handle unexpected errors
96
+ raise HTTPException(
97
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
98
+ detail='An error occurred while creating your account. Please try again.',
99
+ )
100
+
101
+
102
+ @router.post(
103
+ '/login',
104
+ response_model=AuthResponse,
105
+ summary='Login user',
106
+ description='Authenticate user with email and password',
107
+ )
108
+ async def login(
109
+ user_data: LoginRequest,
110
+ response: Response,
111
+ db: Session = Depends(get_db),
112
+ ):
113
+ """
114
+ Login a user.
115
+
116
+ Validates credentials and returns a JWT token.
117
+
118
+ Args:
119
+ user_data: Login credentials (email, password)
120
+ response: FastAPI response object
121
+ db: Database session
122
+
123
+ Returns:
124
+ AuthResponse: JWT token and user information
125
+
126
+ Raises:
127
+ HTTPException 401: If credentials are invalid
128
+ """
129
+ print(f"DEBUG: Login request received: email={user_data.email}")
130
+ # Authenticate user
131
+ user = authenticate_user(db, user_data.email, user_data.password)
132
+
133
+ if not user:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_401_UNAUTHORIZED,
136
+ detail='Invalid email or password',
137
+ headers={'WWW-Authenticate': 'Bearer'},
138
+ )
139
+
140
+ # Generate JWT token
141
+ access_token = create_user_token(user.id)
142
+
143
+ # Set httpOnly cookie (optional, for additional security)
144
+ response.set_cookie(
145
+ key='access_token',
146
+ value=access_token,
147
+ httponly=True,
148
+ secure=not settings.is_development, # HTTPS in production
149
+ samesite='lax',
150
+ max_age=settings.jwt_expiration_days * 24 * 60 * 60, # Convert days to seconds
151
+ )
152
+
153
+ # Return response
154
+ return AuthResponse(
155
+ access_token=access_token,
156
+ token_type='bearer',
157
+ user={
158
+ 'id': str(user.id),
159
+ 'name': user.name,
160
+ 'email': user.email,
161
+ 'avatar_url': user.avatar_url,
162
+ 'created_at': user.created_at.isoformat(),
163
+ 'updated_at': user.updated_at.isoformat(),
164
+ },
165
+ )
166
+
167
+
168
+ @router.post(
169
+ '/logout',
170
+ summary='Logout user',
171
+ description='Logout user and clear authentication token',
172
+ )
173
+ async def logout(response: Response):
174
+ """
175
+ Logout a user.
176
+
177
+ Clears the authentication cookie.
178
+
179
+ Args:
180
+ response: FastAPI response object
181
+
182
+ Returns:
183
+ dict: Logout confirmation message
184
+ """
185
+ # Clear authentication cookie
186
+ response.delete_cookie('access_token')
187
+
188
+ return {'message': 'Successfully logged out'}
189
+
190
+
191
+ @router.get(
192
+ '/me',
193
+ response_model=UserResponse,
194
+ summary='Get current user',
195
+ description='Get information about the currently authenticated user',
196
+ )
197
+ async def get_current_user_info(
198
+ current_user: User = Depends(get_current_user),
199
+ ):
200
+ """
201
+ Get current authenticated user.
202
+
203
+ Requires valid JWT token in Authorization header.
204
+
205
+ Args:
206
+ current_user: Current user from dependency
207
+
208
+ Returns:
209
+ UserResponse: Current user information
210
+ """
211
+ return current_user
212
+
213
+
214
+ # OAuth2 compatible endpoint for token generation
215
+ @router.post(
216
+ '/token',
217
+ response_model=AuthResponse,
218
+ summary='Get access token',
219
+ description='OAuth2 compatible endpoint to get access token',
220
+ )
221
+ async def get_access_token(
222
+ response: Response,
223
+ form_data: OAuth2PasswordRequestForm = Depends(),
224
+ db: Session = Depends(get_db),
225
+ ):
226
+ """
227
+ OAuth2 compatible token endpoint.
228
+
229
+ Used by OAuth2 clients to obtain access tokens.
230
+
231
+ Args:
232
+ form_data: OAuth2 password request form
233
+ response: FastAPI response object
234
+ db: Database session
235
+
236
+ Returns:
237
+ AuthResponse: JWT token and user information
238
+ """
239
+ # Use login logic
240
+ user = authenticate_user(db, form_data.username, form_data.password)
241
+
242
+ if not user:
243
+ raise HTTPException(
244
+ status_code=status.HTTP_401_UNAUTHORIZED,
245
+ detail='Incorrect email or password',
246
+ headers={'WWW-Authenticate': 'Bearer'},
247
+ )
248
+
249
+ access_token = create_user_token(user.id)
250
+
251
+ # Set cookie
252
+ response.set_cookie(
253
+ key='access_token',
254
+ value=access_token,
255
+ httponly=True,
256
+ secure=not settings.is_development,
257
+ samesite='lax',
258
+ max_age=settings.jwt_expiration_days * 24 * 60 * 60,
259
+ )
260
+
261
+ return AuthResponse(
262
+ access_token=access_token,
263
+ token_type='bearer',
264
+ user={
265
+ 'id': str(user.id),
266
+ 'name': user.name,
267
+ 'email': user.email,
268
+ 'avatar_url': user.avatar_url,
269
+ 'created_at': user.created_at.isoformat(),
270
+ 'updated_at': user.updated_at.isoformat(),
271
+ },
272
+ )
273
+
274
+
275
+ # Export router
276
+ __all__ = ['router']
backend/src/api/deps.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI dependencies for database sessions and authentication.
3
+
4
+ Provides reusable dependency functions for injecting database sessions
5
+ and authenticated users into route handlers.
6
+ """
7
+ from typing import Optional
8
+
9
+ from fastapi import Depends, HTTPException, status
10
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
11
+ from sqlmodel import Session, select
12
+
13
+ from src.core.database import get_session
14
+ from src.core.security import TokenData
15
+ from src.models.user import User
16
+
17
+ # HTTP Bearer token scheme for JWT extraction
18
+ security = HTTPBearer(auto_error=False)
19
+
20
+
21
+ async def get_db(
22
+ session: Session = Depends(get_session),
23
+ ) -> Session:
24
+ """
25
+ Dependency for getting database session.
26
+
27
+ This is a passthrough dependency that allows for future enhancements
28
+ like request-scoped sessions or transaction management.
29
+
30
+ Args:
31
+ session: Database session from get_session
32
+
33
+ Returns:
34
+ Session: Database session
35
+
36
+ Example:
37
+ @app.get("/users")
38
+ def get_users(db: Session = Depends(get_db)):
39
+ return db.exec(select(User)).all()
40
+ """
41
+ return session
42
+
43
+
44
+ async def get_current_user(
45
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
46
+ db: Session = Depends(get_db),
47
+ ) -> User:
48
+ """
49
+ Dependency for getting authenticated user from JWT token.
50
+
51
+ Extracts JWT token from Authorization header, validates it,
52
+ and returns the corresponding user.
53
+
54
+ Args:
55
+ credentials: HTTP Bearer credentials from Authorization header
56
+ db: Database session
57
+
58
+ Returns:
59
+ User: Authenticated user
60
+
61
+ Raises:
62
+ HTTPException: If token is missing, invalid, or user not found
63
+
64
+ Example:
65
+ @app.get("/me")
66
+ def get_me(current_user: User = Depends(get_current_user)):
67
+ return current_user
68
+ """
69
+ # Check if credentials are provided
70
+ if credentials is None:
71
+ raise HTTPException(
72
+ status_code=status.HTTP_401_UNAUTHORIZED,
73
+ detail='Not authenticated',
74
+ headers={'WWW-Authenticate': 'Bearer'},
75
+ )
76
+
77
+ # Extract and decode token
78
+ token = credentials.credentials
79
+ token_data = TokenData.from_token(token)
80
+
81
+ if token_data is None:
82
+ raise HTTPException(
83
+ status_code=status.HTTP_401_UNAUTHORIZED,
84
+ detail='Invalid authentication credentials',
85
+ headers={'WWW-Authenticate': 'Bearer'},
86
+ )
87
+
88
+ # Check if token is expired
89
+ if token_data.is_expired():
90
+ raise HTTPException(
91
+ status_code=status.HTTP_401_UNAUTHORIZED,
92
+ detail='Token has expired',
93
+ headers={'WWW-Authenticate': 'Bearer'},
94
+ )
95
+
96
+ # Get user from database
97
+ user = db.get(User, token_data.user_id)
98
+
99
+ if user is None:
100
+ raise HTTPException(
101
+ status_code=status.HTTP_401_UNAUTHORIZED,
102
+ detail='User not found',
103
+ headers={'WWW-Authenticate': 'Bearer'},
104
+ )
105
+
106
+ return user
107
+
108
+
109
+ async def get_current_user_optional(
110
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
111
+ db: Session = Depends(get_db),
112
+ ) -> Optional[User]:
113
+ """
114
+ Optional authentication dependency.
115
+
116
+ Returns the authenticated user if a valid token is provided,
117
+ otherwise returns None. Useful for routes that work for both
118
+ authenticated and anonymous users.
119
+
120
+ Args:
121
+ credentials: HTTP Bearer credentials from Authorization header
122
+ db: Database session
123
+
124
+ Returns:
125
+ Optional[User]: Authenticated user or None
126
+
127
+ Example:
128
+ @app.get("/public-data")
129
+ def get_public_data(user: Optional[User] = Depends(get_current_user_optional)):
130
+ if user:
131
+ return {'data': '...', 'user': user.email}
132
+ return {'data': '...'}
133
+ """
134
+ if credentials is None:
135
+ return None
136
+
137
+ token = credentials.credentials
138
+ token_data = TokenData.from_token(token)
139
+
140
+ if token_data is None or token_data.is_expired():
141
+ return None
142
+
143
+ user = db.get(User, token_data.user_id)
144
+ return user
145
+
146
+
147
+ async def get_current_user_id(
148
+ current_user: User = Depends(get_current_user),
149
+ ) -> str:
150
+ """
151
+ Dependency for getting authenticated user's ID as a string.
152
+
153
+ This is a convenience wrapper around get_current_user that extracts
154
+ just the user ID as a string, which is commonly needed in API routes.
155
+
156
+ Args:
157
+ current_user: Authenticated user from get_current_user
158
+
159
+ Returns:
160
+ str: User ID as a string
161
+
162
+ Example:
163
+ @app.get("/todos")
164
+ def list_todos(user_id: str = Depends(get_current_user_id)):
165
+ return {"user_id": user_id}
166
+ """
167
+ return str(current_user.id)
168
+
169
+
170
+ # Export for use in other modules
171
+ __all__ = ['get_db', 'get_current_user', 'get_current_user_optional', 'get_current_user_id']
backend/src/api/todos.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Todo API routes.
3
+
4
+ Provides endpoints for todo CRUD operations.
5
+ """
6
+ from typing import Optional
7
+ from uuid import UUID
8
+ from datetime import datetime
9
+
10
+ from fastapi import APIRouter, HTTPException, Query, status, Depends
11
+ from sqlmodel import Session, select
12
+
13
+ from src.api.deps import get_current_user_id, get_db
14
+ from src.models.todo import Priority, Status, Todo
15
+ from src.schemas.todo import TodoCreateRequest, TodoResponse, TodoUpdateRequest
16
+
17
+ router = APIRouter()
18
+
19
+
20
+ @router.get(
21
+ '/',
22
+ response_model=list[TodoResponse],
23
+ summary='List todos',
24
+ description='Get all todos for the current user with optional filtering',
25
+ )
26
+ async def list_todos(
27
+ skip: int = Query(0, ge=0, description='Number of todos to skip'),
28
+ limit: int = Query(20, ge=1, le=100, description='Number of todos to return'),
29
+ status_filter: Optional[str] = Query(None, alias='status', description='Filter by status'),
30
+ priority: Optional[str] = Query(None, description='Filter by priority'),
31
+ search: Optional[str] = Query(None, description='Search in title and description'),
32
+ sort_by: str = Query('created_at', description='Sort by field'),
33
+ current_user_id: str = Depends(get_current_user_id),
34
+ db: Session = Depends(get_db),
35
+ ):
36
+ """List todos for the current user with filtering and pagination."""
37
+ # Build base query with user isolation
38
+ query = select(Todo).where(Todo.user_id == UUID(current_user_id))
39
+
40
+ # Apply filters
41
+ if status_filter:
42
+ query = query.where(Todo.status == Status(status_filter))
43
+ if priority:
44
+ query = query.where(Todo.priority == Priority(priority))
45
+ if search:
46
+ search_pattern = f'%{search}%'
47
+ query = query.where(
48
+ (Todo.title.ilike(search_pattern)) | (Todo.description.ilike(search_pattern))
49
+ )
50
+
51
+ # Apply sorting
52
+ if sort_by == 'created_at':
53
+ query = query.order_by(Todo.created_at.desc())
54
+ elif sort_by == 'due_date':
55
+ query = query.order_by(Todo.due_date.asc().nulls_last())
56
+ elif sort_by == 'priority':
57
+ query = query.order_by(Todo.priority.desc())
58
+
59
+ # Apply pagination
60
+ query = query.offset(skip).limit(limit)
61
+
62
+ # Execute query
63
+ todos = db.exec(query).all()
64
+
65
+ return [
66
+ TodoResponse(
67
+ id=str(todo.id),
68
+ user_id=str(todo.user_id),
69
+ title=todo.title,
70
+ description=todo.description,
71
+ status=todo.status.value,
72
+ priority=todo.priority.value,
73
+ tags=todo.tags,
74
+ due_date=todo.due_date.isoformat() if todo.due_date else None,
75
+ created_at=todo.created_at.isoformat(),
76
+ updated_at=todo.updated_at.isoformat(),
77
+ )
78
+ for todo in todos
79
+ ]
80
+
81
+
82
+ @router.post(
83
+ '/',
84
+ response_model=TodoResponse,
85
+ status_code=status.HTTP_201_CREATED,
86
+ summary='Create todo',
87
+ description='Create a new todo',
88
+ )
89
+ async def create_todo(
90
+ todo_data: TodoCreateRequest,
91
+ current_user_id: str = Depends(get_current_user_id),
92
+ db: Session = Depends(get_db),
93
+ ):
94
+ """Create a new todo for the current user."""
95
+ todo = Todo(
96
+ title=todo_data.title,
97
+ description=todo_data.description,
98
+ priority=Priority(todo_data.priority) if todo_data.priority else Priority.MEDIUM,
99
+ due_date=todo_data.due_date,
100
+ tags=todo_data.tags,
101
+ user_id=UUID(current_user_id),
102
+ status=Status.PENDING,
103
+ )
104
+
105
+ db.add(todo)
106
+ db.commit()
107
+ db.refresh(todo)
108
+
109
+ return TodoResponse(
110
+ id=str(todo.id),
111
+ user_id=str(todo.user_id),
112
+ title=todo.title,
113
+ description=todo.description,
114
+ status=todo.status.value,
115
+ priority=todo.priority.value,
116
+ tags=todo.tags,
117
+ due_date=todo.due_date.isoformat() if todo.due_date else None,
118
+ created_at=todo.created_at.isoformat(),
119
+ updated_at=todo.updated_at.isoformat(),
120
+ )
121
+
122
+
123
+ # IMPORTANT: More specific routes must come BEFORE parameterized routes
124
+ @router.post(
125
+ '/{todo_id}/toggle',
126
+ response_model=TodoResponse,
127
+ summary='Toggle todo completion (POST)',
128
+ description='Toggle todo completion status - POST method for frontend compatibility',
129
+ )
130
+ async def toggle_todo_post(
131
+ todo_id: str,
132
+ current_user_id: str = Depends(get_current_user_id),
133
+ db: Session = Depends(get_db),
134
+ ):
135
+ """Toggle todo completion status using POST method."""
136
+ query = select(Todo).where(
137
+ Todo.id == UUID(todo_id),
138
+ Todo.user_id == UUID(current_user_id)
139
+ )
140
+ todo = db.exec(query).first()
141
+
142
+ if not todo:
143
+ raise HTTPException(
144
+ status_code=status.HTTP_404_NOT_FOUND,
145
+ detail='Todo not found',
146
+ )
147
+
148
+ # Toggle status - flip between completed and pending
149
+ if todo.status == Status.PENDING:
150
+ todo.status = Status.COMPLETED
151
+ if not todo.completed_at:
152
+ todo.completed_at = datetime.utcnow()
153
+ else:
154
+ todo.status = Status.PENDING
155
+ todo.completed_at = None
156
+
157
+ db.add(todo)
158
+ db.commit()
159
+ db.refresh(todo)
160
+
161
+ return TodoResponse(
162
+ id=str(todo.id),
163
+ user_id=str(todo.user_id),
164
+ title=todo.title,
165
+ description=todo.description,
166
+ status=todo.status.value,
167
+ priority=todo.priority.value,
168
+ tags=todo.tags,
169
+ due_date=todo.due_date.isoformat() if todo.due_date else None,
170
+ created_at=todo.created_at.isoformat(),
171
+ updated_at=todo.updated_at.isoformat(),
172
+ )
173
+
174
+
175
+ @router.patch(
176
+ '/{todo_id}/complete',
177
+ response_model=TodoResponse,
178
+ summary='Toggle todo completion',
179
+ description='Toggle todo completion status',
180
+ )
181
+ async def toggle_complete(
182
+ todo_id: str,
183
+ completed: bool = True,
184
+ current_user_id: str = Depends(get_current_user_id),
185
+ db: Session = Depends(get_db),
186
+ ):
187
+ """Toggle todo completion status."""
188
+ query = select(Todo).where(
189
+ Todo.id == UUID(todo_id),
190
+ Todo.user_id == UUID(current_user_id)
191
+ )
192
+ todo = db.exec(query).first()
193
+
194
+ if not todo:
195
+ raise HTTPException(
196
+ status_code=status.HTTP_404_NOT_FOUND,
197
+ detail='Todo not found',
198
+ )
199
+
200
+ # Toggle status
201
+ todo.status = Status.COMPLETED if completed else Status.PENDING
202
+ if completed and not todo.completed_at:
203
+ todo.completed_at = datetime.utcnow()
204
+ elif not completed:
205
+ todo.completed_at = None
206
+
207
+ db.add(todo)
208
+ db.commit()
209
+ db.refresh(todo)
210
+
211
+ return TodoResponse(
212
+ id=str(todo.id),
213
+ user_id=str(todo.user_id),
214
+ title=todo.title,
215
+ description=todo.description,
216
+ status=todo.status.value,
217
+ priority=todo.priority.value,
218
+ tags=todo.tags,
219
+ due_date=todo.due_date.isoformat() if todo.due_date else None,
220
+ created_at=todo.created_at.isoformat(),
221
+ updated_at=todo.updated_at.isoformat(),
222
+ )
223
+
224
+
225
+ @router.get(
226
+ '/{todo_id}',
227
+ response_model=TodoResponse,
228
+ summary='Get todo',
229
+ description='Get a specific todo by ID',
230
+ )
231
+ async def get_todo(
232
+ todo_id: str,
233
+ current_user_id: str = Depends(get_current_user_id),
234
+ db: Session = Depends(get_db),
235
+ ):
236
+ """Get a specific todo."""
237
+ query = select(Todo).where(
238
+ Todo.id == UUID(todo_id),
239
+ Todo.user_id == UUID(current_user_id)
240
+ )
241
+ todo = db.exec(query).first()
242
+
243
+ if not todo:
244
+ raise HTTPException(
245
+ status_code=status.HTTP_404_NOT_FOUND,
246
+ detail='Todo not found',
247
+ )
248
+
249
+ return TodoResponse(
250
+ id=str(todo.id),
251
+ user_id=str(todo.user_id),
252
+ title=todo.title,
253
+ description=todo.description,
254
+ status=todo.status.value,
255
+ priority=todo.priority.value,
256
+ tags=todo.tags,
257
+ due_date=todo.due_date.isoformat() if todo.due_date else None,
258
+ created_at=todo.created_at.isoformat(),
259
+ updated_at=todo.updated_at.isoformat(),
260
+ )
261
+
262
+
263
+ @router.put(
264
+ '/{todo_id}',
265
+ response_model=TodoResponse,
266
+ summary='Update todo',
267
+ description='Update a todo',
268
+ )
269
+ async def update_todo(
270
+ todo_id: str,
271
+ todo_data: TodoUpdateRequest,
272
+ current_user_id: str = Depends(get_current_user_id),
273
+ db: Session = Depends(get_db),
274
+ ):
275
+ """Update a todo."""
276
+ query = select(Todo).where(
277
+ Todo.id == UUID(todo_id),
278
+ Todo.user_id == UUID(current_user_id)
279
+ )
280
+ todo = db.exec(query).first()
281
+
282
+ if not todo:
283
+ raise HTTPException(
284
+ status_code=status.HTTP_404_NOT_FOUND,
285
+ detail='Todo not found',
286
+ )
287
+
288
+ # Update fields
289
+ if todo_data.title is not None:
290
+ todo.title = todo_data.title
291
+ if todo_data.description is not None:
292
+ todo.description = todo_data.description
293
+ if todo_data.priority is not None:
294
+ todo.priority = Priority(todo_data.priority)
295
+ if todo_data.due_date is not None:
296
+ todo.due_date = todo_data.due_date
297
+ if todo_data.tags is not None:
298
+ todo.tags = todo_data.tags
299
+
300
+ db.add(todo)
301
+ db.commit()
302
+ db.refresh(todo)
303
+
304
+ return TodoResponse(
305
+ id=str(todo.id),
306
+ user_id=str(todo.user_id),
307
+ title=todo.title,
308
+ description=todo.description,
309
+ status=todo.status.value,
310
+ priority=todo.priority.value,
311
+ tags=todo.tags,
312
+ due_date=todo.due_date.isoformat() if todo.due_date else None,
313
+ created_at=todo.created_at.isoformat(),
314
+ updated_at=todo.updated_at.isoformat(),
315
+ )
316
+
317
+
318
+ @router.delete(
319
+ '/{todo_id}',
320
+ status_code=status.HTTP_204_NO_CONTENT,
321
+ summary='Delete todo',
322
+ description='Delete a todo',
323
+ )
324
+ async def delete_todo(
325
+ todo_id: str,
326
+ current_user_id: str = Depends(get_current_user_id),
327
+ db: Session = Depends(get_db),
328
+ ):
329
+ """Delete a todo."""
330
+ query = select(Todo).where(
331
+ Todo.id == UUID(todo_id),
332
+ Todo.user_id == UUID(current_user_id)
333
+ )
334
+ todo = db.exec(query).first()
335
+
336
+ if not todo:
337
+ raise HTTPException(
338
+ status_code=status.HTTP_404_NOT_FOUND,
339
+ detail='Todo not found',
340
+ )
341
+
342
+ db.delete(todo)
343
+ db.commit()
344
+ return None
345
+
346
+
347
+ __all__ = ['router']
backend/src/api/users.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ User API routes.
3
+
4
+ Provides endpoints for user profile management.
5
+ """
6
+ from uuid import UUID
7
+ from typing import Optional
8
+
9
+ from fastapi import APIRouter, HTTPException, status, Depends, File, UploadFile
10
+ from sqlmodel import Session, select, func
11
+ from datetime import datetime
12
+
13
+ from src.api.deps import get_current_user_id, get_db
14
+ from src.models.user import User
15
+ from src.models.todo import Todo, Status
16
+ from src.schemas.user import UserResponse, UserProfileUpdateRequest
17
+
18
+ router = APIRouter()
19
+
20
+
21
+ @router.get(
22
+ '/me',
23
+ response_model=dict,
24
+ summary='Get current user profile',
25
+ description='Get current user profile with statistics',
26
+ )
27
+ async def get_profile(
28
+ current_user_id: str = Depends(get_current_user_id),
29
+ db: Session = Depends(get_db),
30
+ ):
31
+ """Get current user profile with todo statistics."""
32
+ # Get user
33
+ query = select(User).where(User.id == UUID(current_user_id))
34
+ user = db.exec(query).first()
35
+
36
+ if not user:
37
+ raise HTTPException(
38
+ status_code=status.HTTP_404_NOT_FOUND,
39
+ detail='User not found',
40
+ )
41
+
42
+ # Get todo statistics
43
+ total_todos = db.exec(
44
+ select(func.count()).select_from(Todo).where(Todo.user_id == user.id)
45
+ ).one()
46
+ pending_todos = db.exec(
47
+ select(func.count()).select_from(Todo).where(
48
+ Todo.user_id == user.id,
49
+ Todo.status == Status.PENDING
50
+ )
51
+ ).one()
52
+ completed_todos = db.exec(
53
+ select(func.count()).select_from(Todo).where(
54
+ Todo.user_id == user.id,
55
+ Todo.status == Status.COMPLETED
56
+ )
57
+ ).one()
58
+
59
+ return {
60
+ 'id': str(user.id),
61
+ 'name': user.name,
62
+ 'email': user.email,
63
+ 'avatar_url': user.avatar_url,
64
+ 'created_at': user.created_at.isoformat(),
65
+ 'updated_at': user.updated_at.isoformat(),
66
+ 'stats': {
67
+ 'total_todos': total_todos,
68
+ 'pending_todos': pending_todos,
69
+ 'completed_todos': completed_todos,
70
+ }
71
+ }
72
+
73
+
74
+ @router.put(
75
+ '/me',
76
+ response_model=dict,
77
+ summary='Update user profile',
78
+ description='Update current user profile',
79
+ )
80
+ async def update_profile(
81
+ profile_data: UserProfileUpdateRequest,
82
+ current_user_id: str = Depends(get_current_user_id),
83
+ db: Session = Depends(get_db),
84
+ ):
85
+ """Update current user profile."""
86
+ # Get user
87
+ query = select(User).where(User.id == UUID(current_user_id))
88
+ user = db.exec(query).first()
89
+
90
+ if not user:
91
+ raise HTTPException(
92
+ status_code=status.HTTP_404_NOT_FOUND,
93
+ detail='User not found',
94
+ )
95
+
96
+ # Update fields
97
+ if profile_data.name is not None:
98
+ user.name = profile_data.name
99
+ user.updated_at = datetime.utcnow()
100
+
101
+ db.add(user)
102
+ db.commit()
103
+ db.refresh(user)
104
+
105
+ return {
106
+ 'id': str(user.id),
107
+ 'name': user.name,
108
+ 'email': user.email,
109
+ 'avatar_url': user.avatar_url,
110
+ 'created_at': user.created_at.isoformat(),
111
+ 'updated_at': user.updated_at.isoformat(),
112
+ }
113
+
114
+
115
+ @router.post(
116
+ '/me/avatar',
117
+ response_model=dict,
118
+ summary='Upload avatar',
119
+ description='Upload user avatar image',
120
+ )
121
+ async def upload_avatar(
122
+ file: UploadFile = File(...),
123
+ current_user_id: str = Depends(get_current_user_id),
124
+ db: Session = Depends(get_db),
125
+ ):
126
+ """Upload user avatar."""
127
+ # Validate file type
128
+ allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
129
+ if file.content_type not in allowed_types:
130
+ raise HTTPException(
131
+ status_code=status.HTTP_400_BAD_REQUEST,
132
+ detail=f'Invalid file type. Allowed: {", ".join(allowed_types)}',
133
+ )
134
+
135
+ # Validate file size (5MB max)
136
+ MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
137
+ content = await file.read()
138
+ if len(content) > MAX_FILE_SIZE:
139
+ raise HTTPException(
140
+ status_code=status.HTTP_400_BAD_REQUEST,
141
+ detail='File too large. Maximum size: 5MB',
142
+ )
143
+
144
+ # For now, return a placeholder avatar URL
145
+ # In production, you would upload to Cloudinary here
146
+ from src.core.config import settings
147
+
148
+ if settings.cloudinary_cloud_name:
149
+ # TODO: Implement Cloudinary upload
150
+ avatar_url = f"https://ui-avatars.com/api/?name={file.filename}&background=random"
151
+ else:
152
+ # Use UI Avatars as fallback
153
+ query = select(User).where(User.id == UUID(current_user_id))
154
+ user = db.exec(query).first()
155
+ avatar_url = f"https://ui-avatars.com/api/?name={user.name}&background=random"
156
+
157
+ # Update user avatar
158
+ query = select(User).where(User.id == UUID(current_user_id))
159
+ user = db.exec(query).first()
160
+
161
+ if user:
162
+ user.avatar_url = avatar_url
163
+ user.updated_at = datetime.utcnow()
164
+ db.add(user)
165
+ db.commit()
166
+
167
+ return {
168
+ 'avatar_url': avatar_url,
169
+ 'message': 'Avatar uploaded successfully',
170
+ }
171
+
172
+
173
+ __all__ = ['router']
backend/src/core/__init__.py ADDED
File without changes
backend/src/core/config.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Application configuration using pydantic-settings.
3
+
4
+ Loads environment variables from .env file and provides type-safe access.
5
+ """
6
+ from functools import lru_cache
7
+ from typing import Optional
8
+
9
+ from pydantic import Field, field_validator
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+
13
+ class Settings(BaseSettings):
14
+ """Application settings loaded from environment variables."""
15
+
16
+ model_config = SettingsConfigDict(
17
+ env_file='.env',
18
+ env_file_encoding='utf-8',
19
+ case_sensitive=False,
20
+ extra='ignore',
21
+ )
22
+
23
+ # ========================================
24
+ # Database Configuration
25
+ # ========================================
26
+ database_url: str = Field(
27
+ default='postgresql+psycopg://todoapp:todoapp_password@localhost:5432/todoapp',
28
+ description='PostgreSQL connection string',
29
+ )
30
+
31
+ # ========================================
32
+ # JWT Authentication
33
+ # ========================================
34
+ jwt_secret: str = Field(
35
+ ...,
36
+ min_length=32,
37
+ description='Secret key for JWT token signing (min 32 characters)',
38
+ )
39
+
40
+ jwt_algorithm: str = Field(default='HS256', description='JWT algorithm')
41
+ jwt_expiration_days: int = Field(default=7, description='JWT token expiration in days')
42
+
43
+ # ========================================
44
+ # Cloudinary Configuration (Avatar Storage)
45
+ # ========================================
46
+ cloudinary_cloud_name: Optional[str] = Field(
47
+ default=None, description='Cloudinary cloud name'
48
+ )
49
+ cloudinary_api_key: Optional[str] = Field(default=None, description='Cloudinary API key')
50
+ cloudinary_api_secret: Optional[str] = Field(
51
+ default=None, description='Cloudinary API secret'
52
+ )
53
+
54
+ # ========================================
55
+ # Hugging Face AI Configuration
56
+ # ========================================
57
+ huggingface_api_key: Optional[str] = Field(
58
+ default=None, description='Hugging Face API key'
59
+ )
60
+
61
+ # ========================================
62
+ # Frontend URL
63
+ # ========================================
64
+ frontend_url: str = Field(
65
+ default='http://localhost:3000',
66
+ description='Allowed CORS origin for frontend',
67
+ )
68
+
69
+ # ========================================
70
+ # Application Settings
71
+ # ========================================
72
+ env: str = Field(default='development', description='Environment: development, staging, production')
73
+ port: int = Field(default=8000, description='API port')
74
+ log_level: str = Field(default='info', description='Log level: debug, info, warning, error, critical')
75
+
76
+ # ========================================
77
+ # Security Settings
78
+ # ========================================
79
+ bcrypt_rounds: int = Field(default=12, description='Bcrypt password hashing rounds')
80
+ cors_origins: list[str] = Field(
81
+ default=['http://localhost:3000', 'http://localhost:3001', 'http://localhost:3002', 'http://127.0.0.1:3000', 'http://127.0.0.1:3001', 'http://127.0.0.1:3002'], description='CORS allowed origins'
82
+ )
83
+
84
+ @field_validator('env')
85
+ @classmethod
86
+ def validate_environment(cls, v: str) -> str:
87
+ """Validate environment value."""
88
+ allowed = ['development', 'staging', 'production']
89
+ if v not in allowed:
90
+ raise ValueError(f'env must be one of {allowed}')
91
+ return v
92
+
93
+ @field_validator('log_level')
94
+ @classmethod
95
+ def validate_log_level(cls, v: str) -> str:
96
+ """Validate log level value."""
97
+ allowed = ['debug', 'info', 'warning', 'error', 'critical']
98
+ if v not in allowed:
99
+ raise ValueError(f'log_level must be one of {allowed}')
100
+ return v
101
+
102
+ @property
103
+ def is_development(self) -> bool:
104
+ """Check if running in development mode."""
105
+ return self.env == 'development'
106
+
107
+ @property
108
+ def is_production(self) -> bool:
109
+ """Check if running in production mode."""
110
+ return self.env == 'production'
111
+
112
+ @property
113
+ def database_url_sync(self) -> str:
114
+ """
115
+ Get synchronous database URL for Alembic migrations.
116
+ Replaces postgresql+psycopg with postgresql+psycopg2.
117
+ """
118
+ return self.database_url.replace('+psycopg', '+psycopg2')
119
+
120
+
121
+ @lru_cache()
122
+ def get_settings() -> Settings:
123
+ """
124
+ Get cached settings instance.
125
+
126
+ Uses lru_cache to ensure settings are loaded only once.
127
+ """
128
+ return Settings()
129
+
130
+
131
+ # Export settings instance
132
+ settings = get_settings()
backend/src/core/database.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database configuration and session management.
3
+
4
+ Provides SQLAlchemy engine with connection pooling and session dependency for FastAPI.
5
+ """
6
+ from typing import Generator
7
+
8
+ from sqlalchemy import create_engine, text
9
+ from sqlalchemy.exc import SQLAlchemyError
10
+ from sqlmodel import Session, SQLModel
11
+
12
+ from src.core.config import settings
13
+
14
+ # Create SQLAlchemy engine with connection pooling
15
+ engine = create_engine(
16
+ str(settings.database_url),
17
+ pool_size=10, # Number of connections to maintain
18
+ max_overflow=20, # Additional connections when pool is full
19
+ pool_recycle=3600, # Recycle connections after 1 hour
20
+ pool_pre_ping=True, # Verify connections before using
21
+ echo=settings.is_development, # Log SQL in development
22
+ )
23
+
24
+
25
+ def init_db() -> None:
26
+ """
27
+ Initialize database by creating all tables.
28
+
29
+ This should only be used for development/testing.
30
+ In production, use Alembic migrations instead.
31
+ """
32
+ SQLModel.metadata.create_all(engine)
33
+
34
+
35
+ def get_session() -> Generator[Session, None, None]:
36
+ """
37
+ FastAPI dependency for database session.
38
+
39
+ Yields a database session and ensures it's closed after use.
40
+ Automatically handles rollback on errors.
41
+
42
+ Yields:
43
+ Session: SQLAlchemy session
44
+
45
+ Example:
46
+ @app.get("/users")
47
+ def get_users(db: Session = Depends(get_session)):
48
+ return db.exec(select(User)).all()
49
+ """
50
+ session = Session(engine)
51
+ try:
52
+ yield session
53
+ session.commit()
54
+ except SQLAlchemyError:
55
+ session.rollback()
56
+ raise
57
+ finally:
58
+ session.close()
59
+
60
+
61
+ class DatabaseManager:
62
+ """
63
+ Database manager for advanced operations.
64
+
65
+ Provides methods for health checks, connection testing,
66
+ and administrative tasks.
67
+ """
68
+
69
+ @staticmethod
70
+ def check_connection() -> bool:
71
+ """
72
+ Check if database connection is alive.
73
+
74
+ Returns:
75
+ bool: True if connection is successful, False otherwise
76
+ """
77
+ try:
78
+ with engine.connect() as conn:
79
+ conn.execute(text("SELECT 1"))
80
+ return True
81
+ except Exception as e:
82
+ print(f"Database connection error: {e}")
83
+ return False
84
+
85
+ @staticmethod
86
+ def get_pool_status() -> dict:
87
+ """
88
+ Get connection pool status.
89
+
90
+ Returns:
91
+ dict: Pool statistics including size, checked out, and overflow
92
+ """
93
+ pool = engine.pool
94
+ return {
95
+ 'pool_size': pool.size(),
96
+ 'checked_out': pool.checkedout(),
97
+ 'overflow': pool.overflow(),
98
+ 'max_overflow': engine.pool.max_overflow,
99
+ }
100
+
101
+
102
+ # Export for use in other modules
103
+ __all__ = ['engine', 'get_session', 'init_db', 'DatabaseManager']
backend/src/core/security.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Security utilities for authentication and password management.
3
+
4
+ Provides password hashing with bcrypt and JWT token creation/verification.
5
+ """
6
+ from datetime import datetime, timedelta
7
+ from typing import Optional
8
+
9
+ from jose import JWTError, jwt
10
+ from passlib.context import CryptContext
11
+
12
+ from src.core.config import settings
13
+
14
+ # Password hashing context with bcrypt
15
+ pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
16
+
17
+
18
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
19
+ """
20
+ Verify a plain password against a hashed password.
21
+
22
+ Args:
23
+ plain_password: Plain text password to verify
24
+ hashed_password: Hashed password to compare against
25
+
26
+ Returns:
27
+ bool: True if passwords match, False otherwise
28
+ """
29
+ return pwd_context.verify(plain_password, hashed_password)
30
+
31
+
32
+ def get_password_hash(password: str) -> str:
33
+ """
34
+ Hash a password using bcrypt.
35
+
36
+ Args:
37
+ password: Plain text password to hash
38
+
39
+ Returns:
40
+ str: Hashed password
41
+ """
42
+ return pwd_context.hash(password)
43
+
44
+
45
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
46
+ """
47
+ Create a JWT access token.
48
+
49
+ Args:
50
+ data: Data to encode in the token (typically {'sub': user_id})
51
+ expires_delta: Optional custom expiration time
52
+
53
+ Returns:
54
+ str: Encoded JWT token
55
+
56
+ Example:
57
+ token = create_access_token(data={'sub': str(user.id)})
58
+ """
59
+ to_encode = data.copy()
60
+
61
+ # Set expiration time
62
+ if expires_delta:
63
+ expire = datetime.utcnow() + expires_delta
64
+ else:
65
+ expire = datetime.utcnow() + timedelta(days=settings.jwt_expiration_days)
66
+
67
+ to_encode.update({'exp': expire})
68
+
69
+ # Encode token
70
+ encoded_jwt = jwt.encode(
71
+ to_encode, settings.jwt_secret, algorithm=settings.jwt_algorithm
72
+ )
73
+
74
+ return encoded_jwt
75
+
76
+
77
+ def decode_access_token(token: str) -> Optional[dict]:
78
+ """
79
+ Decode and verify a JWT access token.
80
+
81
+ Args:
82
+ token: JWT token to decode
83
+
84
+ Returns:
85
+ dict: Decoded token payload if valid, None if invalid
86
+
87
+ Example:
88
+ payload = decode_access_token(token)
89
+ if payload:
90
+ user_id = payload.get('sub')
91
+ """
92
+ try:
93
+ payload = jwt.decode(
94
+ token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]
95
+ )
96
+ return payload
97
+ except JWTError:
98
+ return None
99
+
100
+
101
+ class TokenData:
102
+ """
103
+ Token data model for decoded JWT tokens.
104
+
105
+ Attributes:
106
+ user_id: User ID from token subject
107
+ exp: Token expiration timestamp
108
+ """
109
+
110
+ def __init__(self, user_id: Optional[str] = None, exp: Optional[int] = None):
111
+ self.user_id = user_id
112
+ self.exp = exp
113
+
114
+ @classmethod
115
+ def from_token(cls, token: str) -> Optional['TokenData']:
116
+ """
117
+ Create TokenData from JWT token.
118
+
119
+ Args:
120
+ token: JWT token to decode
121
+
122
+ Returns:
123
+ TokenData if token is valid, None otherwise
124
+ """
125
+ payload = decode_access_token(token)
126
+ if payload is None:
127
+ return None
128
+
129
+ user_id = payload.get('sub')
130
+ exp = payload.get('exp')
131
+
132
+ return cls(user_id=user_id, exp=exp)
133
+
134
+ def is_expired(self) -> bool:
135
+ """
136
+ Check if token is expired.
137
+
138
+ Returns:
139
+ bool: True if token is expired, False otherwise
140
+ """
141
+ if self.exp is None:
142
+ return False
143
+
144
+ return datetime.utcnow().timestamp() > self.exp
145
+
146
+
147
+ # Export for use in other modules
148
+ __all__ = [
149
+ 'verify_password',
150
+ 'get_password_hash',
151
+ 'create_access_token',
152
+ 'decode_access_token',
153
+ 'TokenData',
154
+ ]
backend/src/main.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI application main entry point.
3
+
4
+ Configures the FastAPI app with CORS middleware, routes, and middleware.
5
+ """
6
+ from contextlib import asynccontextmanager
7
+ from typing import AsyncGenerator
8
+
9
+ from fastapi import FastAPI, Request
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import JSONResponse
12
+
13
+ from src.core.config import settings
14
+ from src.core.database import DatabaseManager, init_db
15
+
16
+
17
+ @asynccontextmanager
18
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
19
+ """
20
+ Lifespan context manager for FastAPI app.
21
+
22
+ Handles startup and shutdown events.
23
+ """
24
+ # Startup
25
+ print(f"Starting Todo App API")
26
+ print(f"Environment: {settings.env}")
27
+ print(f"Database: {settings.database_url.split('@')[-1]}")
28
+
29
+ # Initialize database (create tables if not exists)
30
+ # In production, use Alembic migrations instead
31
+ if settings.is_development:
32
+ init_db()
33
+ print("Database initialized")
34
+
35
+ yield
36
+
37
+ # Shutdown
38
+ print("Shutting down Todo App API")
39
+
40
+
41
+ # Create FastAPI app
42
+ app = FastAPI(
43
+ title='Todo App API',
44
+ description='Premium Todo SaaS Application API',
45
+ version='0.1.0',
46
+ docs_url='/docs',
47
+ redoc_url='/redoc',
48
+ lifespan=lifespan,
49
+ )
50
+
51
+
52
+ # Configure CORS middleware
53
+ app.add_middleware(
54
+ CORSMiddleware,
55
+ allow_origins=settings.cors_origins,
56
+ allow_credentials=True,
57
+ allow_methods=['*'],
58
+ allow_headers=['*'],
59
+ )
60
+
61
+
62
+ # Global exception handler
63
+ @app.exception_handler(Exception)
64
+ async def global_exception_handler(request: Request, exc: Exception):
65
+ """Handle all unhandled exceptions."""
66
+ print(f"Unhandled exception: {exc}")
67
+ return JSONResponse(
68
+ status_code=500,
69
+ content={
70
+ 'detail': 'Internal server error',
71
+ 'message': str(exc) if settings.is_development else 'An error occurred',
72
+ },
73
+ )
74
+
75
+
76
+ # Health check endpoint
77
+ @app.get('/health', tags=['Health'])
78
+ async def health_check():
79
+ """
80
+ Health check endpoint.
81
+
82
+ Returns API status and database connection status.
83
+ """
84
+ db_connected = DatabaseManager.check_connection()
85
+
86
+ return {
87
+ 'status': 'healthy',
88
+ 'api': 'Todo App API',
89
+ 'version': '0.1.0',
90
+ 'environment': settings.env,
91
+ 'database': 'connected' if db_connected else 'disconnected',
92
+ }
93
+
94
+
95
+ # Root endpoint
96
+ @app.get('/', tags=['Root'])
97
+ async def root():
98
+ """
99
+ Root endpoint with API information.
100
+ """
101
+ return {
102
+ 'message': 'Welcome to Todo App API',
103
+ 'version': '0.1.0',
104
+ 'docs': '/docs',
105
+ 'health': '/health',
106
+ }
107
+
108
+
109
+ # Include routers
110
+ from src.api import auth, todos, users, ai
111
+
112
+ app.include_router(auth.router, prefix='/api/auth', tags=['Authentication'])
113
+ app.include_router(todos.router, prefix='/api/todos', tags=['Todos'])
114
+ app.include_router(users.router, prefix='/api/users', tags=['Users'])
115
+ app.include_router(ai.router, prefix='/api/ai', tags=['AI'])
116
+
117
+
118
+ if __name__ == '__main__':
119
+ import uvicorn
120
+
121
+ uvicorn.run(
122
+ 'src.main:app',
123
+ host='0.0.0.0',
124
+ port=settings.port,
125
+ reload=settings.is_development,
126
+ )
backend/src/models/__init__.py ADDED
File without changes
backend/src/models/ai_request.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AIRequest model for tracking AI feature usage.
3
+ """
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import Optional
7
+ from uuid import UUID, uuid4
8
+
9
+ from sqlmodel import Column, DateTime, Field, ForeignKey, SQLModel, Text
10
+ from sqlalchemy import text, Index
11
+
12
+
13
+ class AIRequestType(str, Enum):
14
+ """Types of AI requests."""
15
+
16
+ GENERATE_TODO = 'generate_todo'
17
+ SUMMARIZE = 'summarize'
18
+ PRIORITIZE = 'prioritize'
19
+
20
+
21
+ class AIRequest(SQLModel, table=True):
22
+ """
23
+ AIRequest model for tracking AI feature usage.
24
+
25
+ Attributes:
26
+ id: Unique request identifier (UUID)
27
+ user_id: User who made the request (foreign key)
28
+ request_type: Type of AI request
29
+ input_data: Input data sent to AI
30
+ output_data: Output data from AI
31
+ model_used: AI model used for processing
32
+ tokens_used: Optional number of tokens used
33
+ processing_time_ms: Processing time in milliseconds
34
+ created_at: Request timestamp
35
+ """
36
+
37
+ __tablename__ = 'ai_requests'
38
+
39
+ id: UUID = Field(
40
+ default_factory=uuid4,
41
+ primary_key=True,
42
+ index=True,
43
+ description='Unique request identifier',
44
+ )
45
+ user_id: UUID = Field(
46
+ default=None,
47
+ foreign_key='users.id',
48
+ nullable=False,
49
+ index=True,
50
+ description='User who made the request',
51
+ )
52
+ request_type: AIRequestType = Field(
53
+ description='Type of AI request',
54
+ )
55
+ input_data: str = Field(
56
+ sa_column=Column(Text),
57
+ description='Input data sent to AI',
58
+ )
59
+ output_data: Optional[str] = Field(
60
+ default=None,
61
+ sa_column=Column(Text),
62
+ description='Output data from AI',
63
+ )
64
+ model_used: str = Field(
65
+ max_length=100,
66
+ description='AI model used',
67
+ )
68
+ tokens_used: Optional[int] = Field(
69
+ default=None,
70
+ description='Number of tokens used',
71
+ )
72
+ processing_time_ms: Optional[int] = Field(
73
+ default=None,
74
+ description='Processing time in milliseconds',
75
+ )
76
+ created_at: datetime = Field(
77
+ default_factory=datetime.utcnow,
78
+ sa_column=Column(DateTime(), server_default=text('CURRENT_TIMESTAMP')),
79
+ description='Request timestamp',
80
+ )
81
+
82
+ # Define indexes
83
+ __table_args__ = (
84
+ Index('idx_ai_requests_user_type', 'user_id', 'request_type'),
85
+ Index('idx_ai_requests_created', 'created_at'),
86
+ )
87
+
88
+ def __repr__(self) -> str:
89
+ return f'<AIRequest {self.request_type}>'
90
+
91
+
92
+ # Export for use in other modules
93
+ __all__ = ['AIRequest', 'AIRequestType']
backend/src/models/session.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Session model for JWT token management.
3
+ """
4
+ from datetime import datetime
5
+ from typing import Optional
6
+ from uuid import UUID, uuid4
7
+
8
+ from sqlmodel import Column, DateTime, Field, ForeignKey, SQLModel, Text
9
+ from sqlalchemy import text, Index
10
+
11
+
12
+ class Session(SQLModel, table=True):
13
+ """
14
+ Session model for tracking active JWT tokens.
15
+
16
+ Attributes:
17
+ id: Unique session identifier (UUID)
18
+ user_id: Associated user ID (foreign key)
19
+ token: JWT token (hashed or partial)
20
+ expires_at: Token expiration timestamp
21
+ created_at: Session creation timestamp
22
+ revoked_at: Optional revocation timestamp
23
+ user_agent: Optional user agent string
24
+ ip_address: Optional IP address
25
+ """
26
+
27
+ __tablename__ = 'sessions'
28
+
29
+ id: UUID = Field(
30
+ default_factory=uuid4,
31
+ primary_key=True,
32
+ index=True,
33
+ description='Unique session identifier',
34
+ )
35
+ user_id: UUID = Field(
36
+ default=None,
37
+ foreign_key='users.id',
38
+ nullable=False,
39
+ index=True,
40
+ description='Associated user ID',
41
+ )
42
+ token: str = Field(
43
+ max_length=500,
44
+ index=True,
45
+ description='JWT token identifier',
46
+ )
47
+ expires_at: datetime = Field(
48
+ description='Token expiration timestamp',
49
+ )
50
+ created_at: datetime = Field(
51
+ default_factory=datetime.utcnow,
52
+ sa_column=Column(DateTime(), server_default=text('CURRENT_TIMESTAMP')),
53
+ description='Session creation timestamp',
54
+ )
55
+ revoked_at: Optional[datetime] = Field(
56
+ default=None,
57
+ description='Revocation timestamp',
58
+ )
59
+ user_agent: Optional[str] = Field(
60
+ default=None,
61
+ max_length=500,
62
+ description='User agent string',
63
+ )
64
+ ip_address: Optional[str] = Field(
65
+ default=None,
66
+ max_length=45,
67
+ description='IP address (IPv4 or IPv6)',
68
+ )
69
+
70
+ # Define indexes
71
+ __table_args__ = (
72
+ Index('idx_sessions_user_expires', 'user_id', 'expires_at'),
73
+ Index('idx_sessions_token', 'token'),
74
+ )
75
+
76
+ def __repr__(self) -> str:
77
+ return f'<Session {self.id}>'
78
+
79
+ def is_valid(self) -> bool:
80
+ """Check if session is valid (not expired and not revoked)."""
81
+ if self.revoked_at is not None:
82
+ return False
83
+ return datetime.utcnow() < self.expires_at
84
+
85
+ def revoke(self) -> None:
86
+ """Revoke the session."""
87
+ self.revoked_at = datetime.utcnow()
88
+
89
+
90
+ # Export for use in other modules
91
+ __all__ = ['Session']
backend/src/models/todo.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Todo model for task management.
3
+ """
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import Optional, List
7
+ from uuid import UUID, uuid4
8
+
9
+ from pydantic import Field as PydanticField
10
+ from sqlmodel import Column, DateTime, Field, ForeignKey, SQLModel, Text
11
+ from sqlalchemy import text, Index, ARRAY, String
12
+
13
+
14
+ class Priority(str, Enum):
15
+ """Todo priority levels."""
16
+
17
+ LOW = 'low'
18
+ MEDIUM = 'medium'
19
+ HIGH = 'high'
20
+
21
+
22
+ class Status(str, Enum):
23
+ """Todo status values."""
24
+
25
+ PENDING = 'pending'
26
+ COMPLETED = 'completed'
27
+
28
+
29
+ class Todo(SQLModel, table=True):
30
+ """
31
+ Todo model representing user tasks.
32
+
33
+ Attributes:
34
+ id: Unique todo identifier (UUID)
35
+ title: Todo title
36
+ description: Optional detailed description
37
+ status: Current status (pending, in_progress, completed, cancelled)
38
+ priority: Priority level (low, medium, high)
39
+ due_date: Optional due date
40
+ completed_at: Optional completion timestamp
41
+ user_id: Owner user ID (foreign key)
42
+ created_at: Creation timestamp
43
+ updated_at: Last update timestamp
44
+ """
45
+
46
+ __tablename__ = 'todos'
47
+
48
+ id: UUID = Field(
49
+ default_factory=uuid4,
50
+ primary_key=True,
51
+ index=True,
52
+ description='Unique todo identifier',
53
+ )
54
+ title: str = Field(max_length=255, description='Todo title')
55
+ description: Optional[str] = Field(
56
+ default=None, sa_column=Column(Text), description='Detailed description'
57
+ )
58
+ status: Status = Field(
59
+ default=Status.PENDING,
60
+ description='Current status',
61
+ )
62
+ priority: Priority = Field(
63
+ default=Priority.MEDIUM,
64
+ description='Priority level',
65
+ )
66
+ due_date: Optional[datetime] = Field(
67
+ default=None,
68
+ description='Due date',
69
+ )
70
+ tags: Optional[List[str]] = Field(
71
+ default=None,
72
+ sa_column=Column(ARRAY(String)), # PostgreSQL array type
73
+ description='Tags for categorization',
74
+ )
75
+ completed_at: Optional[datetime] = Field(
76
+ default=None,
77
+ description='Completion timestamp',
78
+ )
79
+ user_id: UUID = Field(
80
+ default=None,
81
+ foreign_key='users.id',
82
+ nullable=False,
83
+ index=True,
84
+ description='Owner user ID',
85
+ )
86
+ created_at: datetime = Field(
87
+ default_factory=datetime.utcnow,
88
+ sa_column=Column(DateTime(), server_default=text('CURRENT_TIMESTAMP')),
89
+ description='Creation timestamp',
90
+ )
91
+ updated_at: datetime = Field(
92
+ default_factory=datetime.utcnow,
93
+ sa_column=Column(
94
+ DateTime(),
95
+ server_default=text('CURRENT_TIMESTAMP'),
96
+ onupdate=text('CURRENT_TIMESTAMP'),
97
+ ),
98
+ description='Last update timestamp',
99
+ )
100
+
101
+ # Define indexes
102
+ __table_args__ = (
103
+ Index('idx_todos_user_status', 'user_id', 'status'),
104
+ Index('idx_todos_user_priority', 'user_id', 'priority'),
105
+ Index('idx_todos_due_date', 'due_date'),
106
+ )
107
+
108
+ def __repr__(self) -> str:
109
+ return f'<Todo {self.title}>'
110
+
111
+ def mark_completed(self) -> None:
112
+ """Mark todo as completed."""
113
+ self.status = Status.COMPLETED
114
+ self.completed_at = datetime.utcnow()
115
+
116
+ def is_overdue(self) -> bool:
117
+ """Check if todo is overdue."""
118
+ if self.due_date is None or self.status == Status.COMPLETED:
119
+ return False
120
+ return datetime.utcnow() > self.due_date
121
+
122
+
123
+ # Export for use in other modules
124
+ __all__ = ['Todo', 'Priority', 'Status']
backend/src/models/user.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ User model for authentication and profile.
3
+ """
4
+ from datetime import datetime
5
+ from typing import Optional
6
+ from uuid import UUID, uuid4
7
+
8
+ from sqlmodel import Column, DateTime, Field, SQLModel
9
+ from sqlalchemy import text
10
+
11
+
12
+ class User(SQLModel, table=True):
13
+ """
14
+ User model representing application users.
15
+
16
+ Attributes:
17
+ id: Unique user identifier (UUID)
18
+ name: User's full name
19
+ email: User's email address (unique)
20
+ password_hash: Bcrypt hashed password
21
+ avatar_url: Optional Cloudinary avatar URL
22
+ created_at: Account creation timestamp
23
+ updated_at: Last update timestamp
24
+ """
25
+
26
+ __tablename__ = 'users'
27
+
28
+ id: UUID = Field(
29
+ default_factory=uuid4,
30
+ primary_key=True,
31
+ index=True,
32
+ description='Unique user identifier',
33
+ )
34
+ name: str = Field(max_length=255, description="User's full name")
35
+ email: str = Field(
36
+ unique=True,
37
+ index=True,
38
+ max_length=255,
39
+ description="User's email address",
40
+ )
41
+ password_hash: str = Field(max_length=255, description='Bcrypt hashed password', exclude=True)
42
+ avatar_url: Optional[str] = Field(
43
+ default=None, max_length=500, description='Cloudinary avatar URL'
44
+ )
45
+ created_at: datetime = Field(
46
+ default_factory=datetime.utcnow,
47
+ sa_column=Column(DateTime(), server_default=text('CURRENT_TIMESTAMP')),
48
+ description='Account creation timestamp',
49
+ )
50
+ updated_at: datetime = Field(
51
+ default_factory=datetime.utcnow,
52
+ sa_column=Column(
53
+ DateTime(),
54
+ server_default=text('CURRENT_TIMESTAMP'),
55
+ onupdate=text('CURRENT_TIMESTAMP'),
56
+ ),
57
+ description='Last update timestamp',
58
+ )
59
+
60
+ def __repr__(self) -> str:
61
+ return f'<User {self.email}>'
62
+
63
+
64
+ # Export for use in other modules
65
+ __all__ = ['User']
backend/src/schemas/__init__.py ADDED
File without changes
backend/src/schemas/auth.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic schemas for authentication operations.
3
+
4
+ Used for request/response validation in auth endpoints.
5
+ """
6
+ from typing import Optional
7
+
8
+ from pydantic import BaseModel, EmailStr, Field
9
+
10
+
11
+ class SignupRequest(BaseModel):
12
+ """Schema for user registration request."""
13
+
14
+ name: str = Field(
15
+ ...,
16
+ min_length=1,
17
+ max_length=255,
18
+ description="User's full name",
19
+ )
20
+ email: EmailStr = Field(
21
+ ...,
22
+ description="User's email address",
23
+ )
24
+ password: str = Field(
25
+ ...,
26
+ min_length=8,
27
+ max_length=128,
28
+ description="Password (min 8 characters, must include letter and number)",
29
+ )
30
+
31
+
32
+ class LoginRequest(BaseModel):
33
+ """Schema for user login request."""
34
+
35
+ email: EmailStr = Field(..., description="User's email address")
36
+ password: str = Field(..., description="User's password")
37
+
38
+
39
+ class AuthResponse(BaseModel):
40
+ """Schema for authentication response."""
41
+
42
+ access_token: str = Field(..., description="JWT access token")
43
+ token_type: str = Field(default='bearer', description="Token type")
44
+ user: dict = Field(..., description="User information")
45
+
46
+
47
+ class LogoutResponse(BaseModel):
48
+ """Schema for logout response."""
49
+
50
+ message: str = Field(default='Successfully logged out', description="Logout message")
51
+
52
+
53
+ class ErrorResponse(BaseModel):
54
+ """Schema for error responses."""
55
+
56
+ detail: str = Field(..., description="Error message")
57
+ error_code: Optional[str] = Field(None, description="Error code for client handling")
58
+
59
+
60
+ # Export schemas
61
+ __all__ = ['SignupRequest', 'LoginRequest', 'AuthResponse', 'LogoutResponse', 'ErrorResponse']
backend/src/schemas/todo.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Todo schemas for request/response validation.
3
+ """
4
+ from datetime import datetime
5
+ from typing import Optional, List
6
+ from pydantic import BaseModel, Field
7
+ from uuid import UUID
8
+
9
+
10
+ class TodoCreateRequest(BaseModel):
11
+ """Request schema for creating a todo."""
12
+
13
+ title: str = Field(..., min_length=1, max_length=500, description="Todo title")
14
+ description: Optional[str] = Field(None, max_length=5000, description="Detailed description")
15
+ priority: Optional[str] = Field("medium", pattern="^(low|medium|high)$", description="Priority level")
16
+ due_date: Optional[datetime] = Field(None, description="Due date")
17
+ tags: Optional[List[str]] = Field(None, description="Tags for categorization")
18
+
19
+
20
+ class TodoUpdateRequest(BaseModel):
21
+ """Request schema for updating a todo."""
22
+
23
+ title: Optional[str] = Field(None, min_length=1, max_length=500, description="Todo title")
24
+ description: Optional[str] = Field(None, max_length=5000, description="Detailed description")
25
+ priority: Optional[str] = Field(None, pattern="^(low|medium|high)$", description="Priority level")
26
+ due_date: Optional[datetime] = Field(None, description="Due date")
27
+ tags: Optional[List[str]] = Field(None, description="Tags for categorization")
28
+
29
+
30
+ class TodoResponse(BaseModel):
31
+ """Response schema for a todo."""
32
+
33
+ id: str
34
+ user_id: str
35
+ title: str
36
+ description: Optional[str]
37
+ status: str
38
+ priority: str
39
+ tags: Optional[List[str]]
40
+ due_date: Optional[str]
41
+ created_at: str
42
+ updated_at: str
43
+
44
+
45
+ class TodoListResponse(BaseModel):
46
+ """Response schema for todo list with pagination."""
47
+
48
+ todos: List[TodoResponse]
49
+ total: int
50
+ skip: int
51
+ limit: int
52
+ has_more: bool
53
+
54
+
55
+ __all__ = [
56
+ "TodoCreateRequest",
57
+ "TodoUpdateRequest",
58
+ "TodoResponse",
59
+ "TodoListResponse",
60
+ ]
backend/src/schemas/user.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic schemas for User model.
3
+
4
+ Used for request/response validation and serialization.
5
+ """
6
+ from datetime import datetime
7
+ from typing import Optional
8
+ from uuid import UUID
9
+
10
+ from pydantic import BaseModel, EmailStr, Field
11
+
12
+
13
+ class UserBase(BaseModel):
14
+ """Base user schema with common fields."""
15
+
16
+ name: str = Field(..., min_length=1, max_length=255, description="User's full name")
17
+ email: EmailStr = Field(..., description="User's email address")
18
+
19
+
20
+ class UserCreate(UserBase):
21
+ """Schema for creating a new user."""
22
+
23
+ password: str = Field(
24
+ ...,
25
+ min_length=8,
26
+ max_length=128,
27
+ description="User's password (min 8 characters)",
28
+ )
29
+
30
+
31
+ class UserLogin(BaseModel):
32
+ """Schema for user login."""
33
+
34
+ email: EmailStr = Field(..., description="User's email address")
35
+ password: str = Field(..., description="User's password")
36
+
37
+
38
+ class UserUpdate(BaseModel):
39
+ """Schema for updating user profile."""
40
+
41
+ name: Optional[str] = Field(None, min_length=1, max_length=255)
42
+ avatar_url: Optional[str] = Field(None, max_length=500)
43
+
44
+
45
+ class UserProfileUpdateRequest(BaseModel):
46
+ """Schema for updating user profile (minimal)."""
47
+
48
+ name: Optional[str] = Field(None, min_length=1, max_length=255, description="User's full name")
49
+
50
+
51
+ class UserResponse(UserBase):
52
+ """Schema for user response (excluding sensitive data)."""
53
+
54
+ id: UUID = Field(..., description="User ID")
55
+ avatar_url: Optional[str] = Field(None, description="Avatar URL")
56
+ created_at: datetime = Field(..., description="Account creation timestamp")
57
+ updated_at: datetime = Field(..., description="Last update timestamp")
58
+
59
+ class Config:
60
+ """Pydantic config."""
61
+
62
+ from_attributes = True # Enable ORM mode
63
+
64
+
65
+ # Export schemas
66
+ __all__ = ['UserBase', 'UserCreate', 'UserLogin', 'UserUpdate', 'UserResponse', 'UserProfileUpdateRequest']
backend/src/services/__init__.py ADDED
File without changes
backend/src/services/ai_service.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI Service for Hugging Face integration.
3
+
4
+ Provides todo generation, summarization, and prioritization features.
5
+ """
6
+ import json
7
+ import os
8
+ from typing import List, Optional
9
+ from huggingface_hub import InferenceClient
10
+
11
+ from src.core.config import settings
12
+
13
+
14
+ class AIService:
15
+ """Service for AI-powered todo features."""
16
+
17
+ def __init__(self):
18
+ """Initialize AI service with Hugging Face client."""
19
+ self.client = None
20
+ if settings.huggingface_api_key:
21
+ self.client = InferenceClient(token=settings.huggingface_api_key)
22
+
23
+ def _generate_todos_prompt(self, goal: str) -> str:
24
+ """Generate prompt for todo creation."""
25
+ return f"""You are a task planning assistant. Generate 5-7 actionable, specific todo items for this goal: "{goal}"
26
+
27
+ Requirements:
28
+ - Each todo must be specific and actionable
29
+ - Include realistic due dates (relative: "tomorrow", "next week", "next month")
30
+ - Assign priority (low/medium/high)
31
+ - Return as JSON array with exact format below
32
+
33
+ Output format (JSON array):
34
+ {{
35
+ "todos": [
36
+ {{"title": "Research competitors", "description": "Analyze top 3 competitor features", "priority": "high", "due_date": "2025-01-25"}},
37
+ {{"title": "Create wireframes", "description": "Sketch main dashboard screens", "priority": "medium", "due_date": "2025-01-26"}}
38
+ ]
39
+ }}
40
+
41
+ Only return JSON, no other text."""
42
+
43
+ def _summarize_todos_prompt(self, todos: List) -> str:
44
+ """Generate prompt for todo summarization."""
45
+ todos_text = "\n".join([f"- {t['title']}: {t.get('description', '')}" for t in todos])
46
+ return f"""Summarize these {len(todos)} todo items into a concise overview:
47
+
48
+ {todos_text}
49
+
50
+ Provide:
51
+ - Total count breakdown by priority (high/medium/low)
52
+ - Top 3 most urgent items
53
+ - One sentence overall status
54
+
55
+ Keep under 100 words. Be concise and actionable."""
56
+
57
+ def _prioritize_todos_prompt(self, todos: List) -> str:
58
+ """Generate prompt for todo prioritization."""
59
+ todos_text = "\n".join([
60
+ f"{i+1}. {t['title']} (Priority: {t.get('priority', 'medium')}, Due: {t.get('due_date', 'none')})"
61
+ for i, t in enumerate(todos)
62
+ ])
63
+ return f"""You are a productivity expert. Reorder these todos by urgency and importance:
64
+
65
+ Current todos:
66
+ {todos_text}
67
+
68
+ Consider:
69
+ - Due dates (earlier = more urgent)
70
+ - Priority levels explicitly assigned
71
+ - Task dependencies
72
+
73
+ Return as ordered JSON array:
74
+ {{
75
+ "todos": [
76
+ {{"id": "1", "title": "...", "priority_score": 95, "reasoning": "Due tomorrow"}},
77
+ {{"id": "2", "title": "...", "priority_score": 80, "reasoning": "High priority, due in 3 days"}}
78
+ ]
79
+ }}
80
+
81
+ Only return JSON, no other text."""
82
+
83
+ def generate_todos(self, goal: str) -> dict:
84
+ """
85
+ Generate todos from a goal using AI.
86
+
87
+ Args:
88
+ goal: User's goal to break down into todos
89
+
90
+ Returns:
91
+ Dict with generated todos
92
+
93
+ Raises:
94
+ ValueError: If AI service is not configured or response is invalid
95
+ """
96
+ if not self.client:
97
+ raise ValueError("AI service not configured. Please set HUGGINGFACE_API_KEY.")
98
+
99
+ try:
100
+ prompt = self._generate_todos_prompt(goal)
101
+ response = self.client.text_generation(
102
+ prompt,
103
+ model="mistralai/Mistral-7B-Instruct-v0.2",
104
+ max_new_tokens=500,
105
+ temperature=0.7,
106
+ )
107
+
108
+ # Parse JSON response
109
+ response_text = response.strip()
110
+ if "```json" in response_text:
111
+ response_text = response_text.split("```json")[1].split("```")[0].strip()
112
+ elif "```" in response_text:
113
+ response_text = response_text.split("```")[1].split("```")[0].strip()
114
+
115
+ result = json.loads(response_text)
116
+
117
+ return {
118
+ "todos": result.get("todos", []),
119
+ "message": f"Generated {len(result.get('todos', []))} todos for your goal"
120
+ }
121
+
122
+ except json.JSONDecodeError as e:
123
+ raise ValueError(f"Invalid AI response format. Please try again.") from e
124
+ except Exception as e:
125
+ raise ValueError(f"AI service error: {str(e)}") from e
126
+
127
+ def summarize_todos(self, todos: List[dict]) -> dict:
128
+ """
129
+ Summarize todos using AI.
130
+
131
+ Args:
132
+ todos: List of todo dictionaries
133
+
134
+ Returns:
135
+ Dict with summary and breakdown
136
+ """
137
+ if not self.client:
138
+ raise ValueError("AI service not configured. Please set HUGGINGFACE_API_KEY.")
139
+
140
+ if not todos:
141
+ return {
142
+ "summary": "No todos to summarize.",
143
+ "breakdown": {"high_priority": 0, "medium_priority": 0, "low_priority": 0},
144
+ "urgent_todos": []
145
+ }
146
+
147
+ try:
148
+ # Calculate breakdown
149
+ breakdown = {
150
+ "high_priority": sum(1 for t in todos if t.get("priority") == "high"),
151
+ "medium_priority": sum(1 for t in todos if t.get("priority") == "medium"),
152
+ "low_priority": sum(1 for t in todos if t.get("priority") == "low"),
153
+ }
154
+
155
+ # Get urgent todos (high priority or due soon)
156
+ from datetime import datetime, timedelta
157
+ urgent = []
158
+ for t in todos:
159
+ if t.get("priority") == "high":
160
+ urgent.append(t.get("title", ""))
161
+ elif t.get("due_date"):
162
+ try:
163
+ due_date = datetime.fromisoformat(t["due_date"].replace("Z", "+00:00"))
164
+ if due_date <= datetime.now() + timedelta(days=2):
165
+ urgent.append(t.get("title", ""))
166
+ except:
167
+ pass
168
+
169
+ # Generate summary
170
+ prompt = self._summarize_todos_prompt(todos)
171
+ summary = self.client.text_generation(
172
+ prompt,
173
+ model="facebook/bart-large-cnn",
174
+ max_new_tokens=200,
175
+ temperature=0.5,
176
+ )
177
+
178
+ return {
179
+ "summary": summary.strip(),
180
+ "breakdown": breakdown,
181
+ "urgent_todos": urgent[:3] # Top 3 urgent
182
+ }
183
+
184
+ except Exception as e:
185
+ raise ValueError(f"AI service error: {str(e)}") from e
186
+
187
+ def prioritize_todos(self, todos: List[dict]) -> dict:
188
+ """
189
+ Prioritize todos using AI.
190
+
191
+ Args:
192
+ todos: List of todo dictionaries
193
+
194
+ Returns:
195
+ Dict with prioritized todos
196
+ """
197
+ if not self.client:
198
+ raise ValueError("AI service not configured. Please set HUGGINGFACE_API_KEY.")
199
+
200
+ if not todos:
201
+ return {
202
+ "prioritized_todos": [],
203
+ "message": "No todos to prioritize"
204
+ }
205
+
206
+ try:
207
+ prompt = self._prioritize_todos_prompt(todos)
208
+ response = self.client.text_generation(
209
+ prompt,
210
+ model="mistralai/Mistral-7B-Instruct-v0.2",
211
+ max_new_tokens=500,
212
+ temperature=0.7,
213
+ )
214
+
215
+ # Parse JSON response
216
+ response_text = response.strip()
217
+ if "```json" in response_text:
218
+ response_text = response_text.split("```json")[1].split("```")[0].strip()
219
+ elif "```" in response_text:
220
+ response_text = response_text.split("```")[1].split("```")[0].strip()
221
+
222
+ result = json.loads(response_text)
223
+
224
+ return {
225
+ "prioritized_todos": result.get("todos", []),
226
+ "message": f"Prioritized {len(result.get('todos', []))} todos"
227
+ }
228
+
229
+ except json.JSONDecodeError as e:
230
+ raise ValueError(f"Invalid AI response format. Please try again.") from e
231
+ except Exception as e:
232
+ raise ValueError(f"AI service error: {str(e)}") from e
233
+
234
+
235
+ # Global AI service instance
236
+ ai_service = AIService()
237
+
238
+
239
+ __all__ = ['ai_service', 'AIService']
backend/src/services/auth_service.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication service for user management.
3
+
4
+ Provides functions for user creation, authentication, and JWT token management.
5
+ """
6
+ import re
7
+ from typing import Optional
8
+ from uuid import UUID
9
+
10
+ from jose import JWTError
11
+ from sqlmodel import Session, select
12
+
13
+ from src.core.config import settings
14
+ from src.core.security import create_access_token, verify_password
15
+ from src.models.user import User
16
+ from src.schemas.user import UserCreate
17
+
18
+
19
+ def hash_password(password: str) -> str:
20
+ """
21
+ Hash a password using bcrypt.
22
+
23
+ Args:
24
+ password: Plain text password
25
+
26
+ Returns:
27
+ str: Hashed password
28
+
29
+ Raises:
30
+ ValueError: If password doesn't meet requirements
31
+ """
32
+ if not password or len(password) < 8:
33
+ raise ValueError('Password must be at least 8 characters long')
34
+
35
+ if not re.search(r'[A-Za-z]', password):
36
+ raise ValueError('Password must contain at least one letter')
37
+
38
+ if not re.search(r'\d', password):
39
+ raise ValueError('Password must contain at least one number')
40
+
41
+ from src.core.security import get_password_hash
42
+
43
+ return get_password_hash(password)
44
+
45
+
46
+ def check_email_exists(db: Session, email: str) -> bool:
47
+ """
48
+ Check if an email already exists in the database.
49
+
50
+ Args:
51
+ db: Database session
52
+ email: Email to check
53
+
54
+ Returns:
55
+ bool: True if email exists, False otherwise
56
+ """
57
+ user = db.exec(select(User).where(User.email.ilike(email))).first()
58
+ return user is not None
59
+
60
+
61
+ def create_user(db: Session, user_data: UserCreate) -> User:
62
+ """
63
+ Create a new user in the database.
64
+
65
+ Args:
66
+ db: Database session
67
+ user_data: User creation data
68
+
69
+ Returns:
70
+ User: Created user object
71
+
72
+ Raises:
73
+ ValueError: If email already exists or password is invalid
74
+ """
75
+ # Check if email already exists (case-insensitive)
76
+ if check_email_exists(db, user_data.email):
77
+ raise ValueError(f'Email {user_data.email} is already registered')
78
+
79
+ # Hash password
80
+ try:
81
+ password_hash = hash_password(user_data.password)
82
+ except ValueError as e:
83
+ raise ValueError(str(e))
84
+
85
+ # Create new user
86
+ user = User(
87
+ name=user_data.name.strip(),
88
+ email=user_data.email.lower().strip(),
89
+ password_hash=password_hash,
90
+ )
91
+
92
+ # Save to database
93
+ db.add(user)
94
+ db.commit()
95
+ db.refresh(user)
96
+
97
+ return user
98
+
99
+
100
+ def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
101
+ """
102
+ Authenticate a user with email and password.
103
+
104
+ Args:
105
+ db: Database session
106
+ email: User's email
107
+ password: Plain text password
108
+
109
+ Returns:
110
+ User: User object if authentication successful, None otherwise
111
+ """
112
+ # Find user by email (case-insensitive)
113
+ user = db.exec(select(User).where(User.email.ilike(email))).first()
114
+
115
+ if not user:
116
+ return None
117
+
118
+ # Verify password
119
+ if not verify_password(password, user.password_hash):
120
+ return None
121
+
122
+ return user
123
+
124
+
125
+ def create_user_token(user_id: UUID) -> str:
126
+ """
127
+ Create a JWT access token for a user.
128
+
129
+ Args:
130
+ user_id: User's UUID
131
+
132
+ Returns:
133
+ str: JWT access token
134
+
135
+ Raises:
136
+ ValueError: If token creation fails
137
+ """
138
+ try:
139
+ token = create_access_token(data={'sub': str(user_id)})
140
+ return token
141
+ except JWTError as e:
142
+ raise ValueError(f'Failed to create access token: {str(e)}')
143
+
144
+
145
+ def verify_user_token(token: str) -> Optional[UUID]:
146
+ """
147
+ Verify a JWT token and extract user ID.
148
+
149
+ Args:
150
+ token: JWT access token
151
+
152
+ Returns:
153
+ UUID: User ID if token is valid, None otherwise
154
+ """
155
+ from src.core.security import decode_access_token, TokenData
156
+
157
+ try:
158
+ token_data = TokenData.from_token(token)
159
+ if token_data and token_data.user_id and not token_data.is_expired():
160
+ return UUID(token_data.user_id)
161
+ except (JWTError, ValueError):
162
+ return None
163
+
164
+ return None
165
+
166
+
167
+ def get_user_by_id(db: Session, user_id: UUID) -> Optional[User]:
168
+ """
169
+ Get a user by ID.
170
+
171
+ Args:
172
+ db: Database session
173
+ user_id: User's UUID
174
+
175
+ Returns:
176
+ User: User object if found, None otherwise
177
+ """
178
+ return db.get(User, user_id)
179
+
180
+
181
+ def get_user_by_email(db: Session, email: str) -> Optional[User]:
182
+ """
183
+ Get a user by email.
184
+
185
+ Args:
186
+ db: Database session
187
+ email: User's email
188
+
189
+ Returns:
190
+ User: User object if found, None otherwise
191
+ """
192
+ return db.exec(select(User).where(User.email.ilike(email))).first()
193
+
194
+
195
+ # Export for use in other modules
196
+ __all__ = [
197
+ 'hash_password',
198
+ 'check_email_exists',
199
+ 'create_user',
200
+ 'authenticate_user',
201
+ 'create_user_token',
202
+ 'verify_user_token',
203
+ 'get_user_by_id',
204
+ 'get_user_by_email',
205
+ ]
backend/src/tests/__init__.py ADDED
File without changes
backend/src/utils/__init__.py ADDED
File without changes
cookies.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Netscape HTTP Cookie File
2
+ # https://curl.se/docs/http-cookies.html
3
+ # This file was generated by libcurl! Edit at your own risk.
4
+
5
+ #HttpOnly_localhost FALSE / FALSE 1769932286 access_token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NjQ3MDViNC1jYzNiLTRmYTktYWY3Yy00OGVkNTdkZGNlNDQiLCJleHAiOjE3Njk5MzIyODZ9.CWtt77dZvCXg9P8SQHI-pQ3nzemeFu8rZuaAmIv6H9c
docker-compose.override.yml.example ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Example docker-compose override for additional development tools
2
+ # Copy this file to docker-compose.override.yml and customize as needed
3
+
4
+ version: '3.8'
5
+
6
+ services:
7
+ # pgAdmin for PostgreSQL Management
8
+ pgadmin:
9
+ image: dpage/pgadmin4:latest
10
+ container_name: todo-app-pgadmin
11
+ restart: unless-stopped
12
+ environment:
13
+ PGADMIN_DEFAULT_EMAIL: admin@todoapp.local
14
+ PGADMIN_DEFAULT_PASSWORD: admin
15
+ ports:
16
+ - '5050:80'
17
+ depends_on:
18
+ - postgres
docker-compose.yml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ # PostgreSQL Database for Local Development
5
+ postgres:
6
+ image: postgres:16-alpine
7
+ container_name: todo-app-postgres
8
+ restart: unless-stopped
9
+ environment:
10
+ POSTGRES_USER: todoapp
11
+ POSTGRES_PASSWORD: todoapp_password
12
+ POSTGRES_DB: todoapp
13
+ ports:
14
+ - '5432:5432'
15
+ volumes:
16
+ - postgres_data:/var/lib/postgresql/data
17
+ healthcheck:
18
+ test: ['CMD-SHELL', 'pg_isready -U todoapp']
19
+ interval: 10s
20
+ timeout: 5s
21
+ retries: 5
22
+
23
+ # Redis for Caching (Optional - for future use)
24
+ redis:
25
+ image: redis:7-alpine
26
+ container_name: todo-app-redis
27
+ restart: unless-stopped
28
+ ports:
29
+ - '6379:6379'
30
+ volumes:
31
+ - redis_data:/data
32
+ healthcheck:
33
+ test: ['CMD', 'redis-cli', 'ping']
34
+ interval: 10s
35
+ timeout: 3s
36
+ retries: 5
37
+
38
+ volumes:
39
+ postgres_data:
40
+ driver: local
41
+ redis_data:
42
+ driver: local
43
+
44
+ networks:
45
+ default:
46
+ name: todo-app-network
frontend/.env.example ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ========================================
2
+ # API Configuration
3
+ # ========================================
4
+ # Backend API URL (change for production)
5
+ NEXT_PUBLIC_API_URL=http://localhost:8000
6
+
7
+ # ========================================
8
+ # Application Settings
9
+ # ========================================
10
+ # Environment: development, staging, production
11
+ NEXT_PUBLIC_APP_ENV=development
12
+
13
+ # ========================================
14
+ # Feature Flags
15
+ # ========================================
16
+ # Enable AI features (requires HUGGINGFACE_API_KEY in backend)
17
+ NEXT_PUBLIC_ENABLE_AI=true
18
+
19
+ # Enable analytics
20
+ NEXT_PUBLIC_ENABLE_ANALYTICS=false
frontend/.eslintrc.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": [
3
+ "next/core-web-vitals",
4
+ "prettier"
5
+ ],
6
+ "rules": {
7
+ "react/no-unescaped-entities": "off",
8
+ "@next/next/no-page-custom-font": "off",
9
+ "prefer-const": "error",
10
+ "no-unused-vars": "off",
11
+ "@typescript-eslint/no-unused-vars": "off"
12
+ }
13
+ }
frontend/.prettierrc ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5",
6
+ "printWidth": 100,
7
+ "plugins": ["prettier-plugin-tailwindcss"]
8
+ }
frontend/README.md ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Todo App Frontend - Phase 2
2
+
3
+ Next.js 14 frontend for the Todo SaaS application with premium UI and authentication.
4
+
5
+ ## Tech Stack
6
+
7
+ - **Next.js 14** - React framework with App Router
8
+ - **TypeScript** - Type-safe development
9
+ - **Tailwind CSS** - Utility-first CSS framework
10
+ - **shadcn/ui** - Premium UI components
11
+ - **Framer Motion** - Smooth animations
12
+ - **next-themes** - Dark/light theme support
13
+
14
+ ## Setup
15
+
16
+ ### 1. Install dependencies
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ ### 2. Setup environment
23
+
24
+ ```bash
25
+ cp .env.example .env.local
26
+ # Edit .env.local with your configuration
27
+ ```
28
+
29
+ ### 3. Start development server
30
+
31
+ ```bash
32
+ npm run dev
33
+ ```
34
+
35
+ App will be available at: http://localhost:3000
36
+
37
+ ## Project Structure
38
+
39
+ ```
40
+ frontend/
41
+ β”œβ”€β”€ src/
42
+ β”‚ β”œβ”€β”€ app/ # Next.js App Router pages
43
+ β”‚ β”œβ”€β”€ components/ # React components
44
+ β”‚ β”‚ └── ui/ # shadcn/ui components
45
+ β”‚ β”œβ”€β”€ hooks/ # Custom React hooks
46
+ β”‚ β”œβ”€β”€ lib/ # Utility functions
47
+ β”‚ β”œβ”€β”€ styles/ # Global styles
48
+ β”‚ └── types/ # TypeScript type definitions
49
+ β”œβ”€β”€ public/ # Static assets
50
+ └── package.json # Dependencies and scripts
51
+ ```
52
+
53
+ ## Available Scripts
54
+
55
+ ```bash
56
+ # Development
57
+ npm run dev # Start dev server
58
+
59
+ # Building
60
+ npm run build # Build for production
61
+ npm run start # Start production server
62
+
63
+ # Testing
64
+ npm test # Run unit tests
65
+ npm run test:watch # Watch mode
66
+ npm run test:e2e # Run E2E tests with Playwright
67
+
68
+ # Code Quality
69
+ npm run lint # Run ESLint
70
+ npm run lint:fix # Fix ESLint issues
71
+ npm run format # Format with Prettier
72
+ npm run type-check # TypeScript type checking
73
+ ```
74
+
75
+ ## Features
76
+
77
+ ### Authentication
78
+ - Login with email/password
79
+ - User registration with validation
80
+ - Secure JWT token storage
81
+ - Auto-redirect based on auth state
82
+
83
+ ### Todo Management
84
+ - Create, edit, delete todos
85
+ - Mark todos as complete
86
+ - Filter by status
87
+ - Search todos
88
+ - Sort by date, priority
89
+
90
+ ### User Profile
91
+ - View and edit profile
92
+ - Upload avatar (Cloudinary)
93
+ - Update name and email
94
+
95
+ ### AI Features
96
+ - Generate todos from text
97
+ - Summarize tasks
98
+ - Prioritize tasks
99
+
100
+ ### UI/UX
101
+ - Dark/light theme toggle
102
+ - Smooth animations
103
+ - Mobile responsive
104
+ - Loading states
105
+ - Error handling
106
+
107
+ ## Environment Variables
108
+
109
+ See `.env.example` for required environment variables:
110
+
111
+ ```env
112
+ NEXT_PUBLIC_API_URL=http://localhost:8000
113
+ NEXT_PUBLIC_APP_ENV=development
114
+ NEXT_PUBLIC_ENABLE_AI=true
115
+ ```
116
+
117
+ ## Component Library
118
+
119
+ This project uses [shadcn/ui](https://ui.shadcn.com/) for premium UI components.
120
+
121
+ ### Adding new components
122
+
123
+ ```bash
124
+ npx shadcn-ui@latest add [component-name]
125
+ ```
126
+
127
+ Available components:
128
+ - Button, Input, Label, Card
129
+ - Dialog, Dropdown Menu, Select
130
+ - Tabs, Switch, Avatar
131
+ - Toast, and more...
132
+
133
+ ## State Management
134
+
135
+ - React Context for auth state
136
+ - React hooks for local state
137
+ - Server Components for data fetching
138
+ - Client Components for interactivity
139
+
140
+ ## Styling
141
+
142
+ - **Tailwind CSS** - Utility classes
143
+ - **CSS Variables** - Theme customization
144
+ - **Framer Motion** - Animations
145
+ - **shadcn/ui** - Pre-built components
146
+
147
+ ## Testing
148
+
149
+ ```bash
150
+ # Unit tests
151
+ npm test
152
+
153
+ # E2E tests
154
+ npm run test:e2e
155
+
156
+ # E2E with UI
157
+ npm run e2e:ui
158
+
159
+ # E2E debug mode
160
+ npm run e2e:debug
161
+ ```
162
+
163
+ ## Building for Production
164
+
165
+ ```bash
166
+ # Build
167
+ npm run build
168
+
169
+ # Test production build locally
170
+ npm run start
171
+ ```
172
+
173
+ ## Deployment
174
+
175
+ This app is designed to be deployed on **Vercel**:
176
+
177
+ 1. Push code to GitHub
178
+ 2. Import project in Vercel
179
+ 3. Configure environment variables
180
+ 4. Deploy
181
+
182
+ ## Browser Support
183
+
184
+ - Chrome (last 2 versions)
185
+ - Firefox (last 2 versions)
186
+ - Safari (last 2 versions)
187
+ - Edge (last 2 versions)
188
+
189
+ ## License
190
+
191
+ MIT
frontend/components.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
frontend/jest.setup.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Learn more: https://github.com/testing-library/jest-dom
2
+ import '@testing-library/jest-dom';
frontend/next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
frontend/next.config.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ swcMinify: true,
5
+
6
+ env: {
7
+ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
8
+ },
9
+
10
+ // Enable experimental features for better performance
11
+ experimental: {
12
+ optimizePackageImports: ['lucide-react', '@radix-ui/react-icons'],
13
+ },
14
+
15
+ // Image optimization
16
+ images: {
17
+ remotePatterns: [
18
+ {
19
+ protocol: 'https',
20
+ hostname: 'res.cloudinary.com',
21
+ },
22
+ ],
23
+ },
24
+
25
+ // Webpack configuration
26
+ webpack: (config) => {
27
+ config.externals = [...(config.externals || []), { canvas: 'canvas' }];
28
+
29
+ // Handle Windows case sensitivity issues
30
+ config.resolve.symlinks = false;
31
+ config.snapshot = {
32
+ ...config.snapshot,
33
+ managedPaths: [/^(.+?[\\/]node_modules[\\/])/],
34
+ };
35
+
36
+ return config;
37
+ },
38
+
39
+ // Headers for security
40
+ async headers() {
41
+ return [
42
+ {
43
+ source: '/:path*',
44
+ headers: [
45
+ {
46
+ key: 'X-DNS-Prefetch-Control',
47
+ value: 'on'
48
+ },
49
+ {
50
+ key: 'X-Frame-Options',
51
+ value: 'SAMEORIGIN'
52
+ },
53
+ {
54
+ key: 'X-Content-Type-Options',
55
+ value: 'nosniff'
56
+ },
57
+ {
58
+ key: 'Referrer-Policy',
59
+ value: 'origin-when-cross-origin'
60
+ },
61
+ ],
62
+ },
63
+ ];
64
+ },
65
+ };
66
+
67
+ module.exports = nextConfig;
frontend/package.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "todo-app-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "lint:fix": "next lint --fix",
11
+ "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
12
+ "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
13
+ "type-check": "tsc --noEmit",
14
+ "test": "jest",
15
+ "test:watch": "jest --watch",
16
+ "test:coverage": "jest --coverage",
17
+ "test:e2e": "playwright test",
18
+ "e2e:ui": "playwright test --ui",
19
+ "e2e:debug": "playwright test --debug"
20
+ },
21
+ "dependencies": {
22
+ "next": "14.1.0",
23
+ "react": "^18.2.0",
24
+ "react-dom": "^18.2.0",
25
+ "@radix-ui/react-avatar": "^1.0.4",
26
+ "@radix-ui/react-dialog": "^1.0.5",
27
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
28
+ "@radix-ui/react-label": "^2.0.2",
29
+ "@radix-ui/react-select": "^2.0.0",
30
+ "@radix-ui/react-slot": "^1.0.2",
31
+ "@radix-ui/react-switch": "^1.0.3",
32
+ "@radix-ui/react-tabs": "^1.0.4",
33
+ "@radix-ui/react-toast": "^1.1.5",
34
+ "framer-motion": "^11.0.0",
35
+ "next-themes": "^0.2.1",
36
+ "class-variance-authority": "^0.7.0",
37
+ "clsx": "^2.1.0",
38
+ "tailwind-merge": "^2.2.0",
39
+ "tailwindcss-animate": "^1.0.7",
40
+ "lucide-react": "^0.323.0",
41
+ "zod": "^3.22.0",
42
+ "date-fns": "^3.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20",
46
+ "@types/react": "^18",
47
+ "@types/react-dom": "^18",
48
+ "typescript": "^5",
49
+ "tailwindcss": "^3.4.0",
50
+ "postcss": "^8",
51
+ "autoprefixer": "^10.0.1",
52
+ "eslint": "^8",
53
+ "eslint-config-next": "14.1.0",
54
+ "eslint-config-prettier": "^9.1.0",
55
+ "prettier": "^3.2.0",
56
+ "prettier-plugin-tailwindcss": "^0.5.0",
57
+ "@testing-library/react": "^14.0.0",
58
+ "@testing-library/jest-dom": "^6.0.0",
59
+ "@playwright/test": "^1.40.0",
60
+ "jest": "^29.0.0",
61
+ "jest-environment-jsdom": "^29.0.0"
62
+ }
63
+ }