Claude Code - Backend Implementation Specialist Claude Sonnet 4.5 commited on
Commit
1941764
·
1 Parent(s): 3948ca6

Add complete FastAPI Todo application with Docker support

Browse files

- Add FastAPI backend with JWT authentication
- Add task and subtask management APIs
- Add password reset functionality with email support
- Add Docker and docker-compose configuration
- Add Alembic migrations for database schema
- Add comprehensive API documentation
- Configure for Hugging Face Spaces deployment on port 7860

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. .dockerignore +72 -0
  2. .env.example +44 -0
  3. .gitignore +12 -0
  4. DOCKER_SETUP.md +178 -0
  5. Dockerfile +25 -0
  6. README.md +48 -5
  7. alembic.ini +114 -0
  8. alembic/env.py +85 -0
  9. alembic/script.py.mako +24 -0
  10. alembic/versions/001_initial_schema.py +52 -0
  11. alembic/versions/002_add_password_reset_tokens.py +40 -0
  12. alembic/versions/a6878af5b66f_add_category_and_due_date_to_tasks.py +30 -0
  13. api/index.py +19 -0
  14. api/test.py +45 -0
  15. docker-compose.yml +55 -0
  16. init_db.py +9 -0
  17. migrate_db.py +36 -0
  18. requirements.txt +13 -0
  19. src/__pycache__/database.cpython-314.pyc +0 -0
  20. src/__pycache__/main.cpython-314.pyc +0 -0
  21. src/api/__init__.py +1 -0
  22. src/api/__pycache__/__init__.cpython-314.pyc +0 -0
  23. src/api/__pycache__/auth.cpython-314.pyc +0 -0
  24. src/api/__pycache__/password_reset.cpython-314.pyc +0 -0
  25. src/api/__pycache__/subtasks.cpython-314.pyc +0 -0
  26. src/api/__pycache__/tasks.cpython-314.pyc +0 -0
  27. src/api/ai.py +228 -0
  28. src/api/auth.py +155 -0
  29. src/api/password_reset.py +233 -0
  30. src/api/subtasks.py +230 -0
  31. src/api/tasks.py +278 -0
  32. src/database.py +59 -0
  33. src/main.py +68 -0
  34. src/main_minimal.py +21 -0
  35. src/middleware/__pycache__/jwt_auth.cpython-314.pyc +0 -0
  36. src/middleware/jwt_auth.py +86 -0
  37. src/models/__init__.py +6 -0
  38. src/models/__pycache__/__init__.cpython-314.pyc +0 -0
  39. src/models/__pycache__/password_reset.cpython-314.pyc +0 -0
  40. src/models/__pycache__/subtask.cpython-314.pyc +0 -0
  41. src/models/__pycache__/task.cpython-314.pyc +0 -0
  42. src/models/__pycache__/user.cpython-314.pyc +0 -0
  43. src/models/password_reset.py +30 -0
  44. src/models/subtask.py +32 -0
  45. src/models/task.py +49 -0
  46. src/models/user.py +27 -0
  47. src/services/__init__.py +1 -0
  48. src/services/__pycache__/__init__.cpython-314.pyc +0 -0
  49. src/services/__pycache__/auth.cpython-314.pyc +0 -0
  50. src/services/__pycache__/email.cpython-314.pyc +0 -0
.dockerignore ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ ENV/
10
+ build/
11
+ develop-eggs/
12
+ dist/
13
+ downloads/
14
+ eggs/
15
+ .eggs/
16
+ lib/
17
+ lib64/
18
+ parts/
19
+ sdist/
20
+ var/
21
+ wheels/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+
26
+ # Environment variables
27
+ .env
28
+ .env.local
29
+
30
+ # Database
31
+ *.db
32
+ *.sqlite
33
+ *.sqlite3
34
+
35
+ # IDE
36
+ .vscode/
37
+ .idea/
38
+ *.swp
39
+ *.swo
40
+ *~
41
+
42
+ # Git
43
+ .git/
44
+ .gitignore
45
+
46
+ # Testing
47
+ .pytest_cache/
48
+ .coverage
49
+ htmlcov/
50
+ .tox/
51
+
52
+ # Documentation
53
+ *.md
54
+ docs/
55
+
56
+ # Logs
57
+ *.log
58
+
59
+ # OS
60
+ .DS_Store
61
+ Thumbs.db
62
+
63
+ # Alembic
64
+ alembic/versions/*.pyc
65
+
66
+ # Vercel
67
+ .vercel/
68
+ vercel.json
69
+
70
+ # Test files
71
+ test_*.py
72
+ *_test.py
.env.example ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Backend Environment Variables
2
+ # Copy this file to .env and fill in your values
3
+
4
+ # Database Configuration
5
+ DATABASE_URL=sqlite:///./todo.db
6
+ # For production, use PostgreSQL:
7
+ # DATABASE_URL=postgresql://user:password@host:5432/database
8
+
9
+ # JWT Configuration
10
+ JWT_SECRET_KEY=your-super-secret-key-change-this-min-32-characters-long
11
+ JWT_ALGORITHM=HS256
12
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
13
+
14
+ # CORS Configuration
15
+ CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002
16
+ # For production, add your frontend URL:
17
+ # CORS_ORIGINS=https://your-frontend-url.com,http://localhost:3000
18
+
19
+ # Gmail SMTP Configuration (for password reset emails)
20
+ # To get app-specific password:
21
+ # 1. Enable 2-Factor Authentication on your Gmail account
22
+ # 2. Go to Google Account → Security → 2-Step Verification → App passwords
23
+ # 3. Select "Mail" and "Other (Custom name)"
24
+ # 4. Copy the 16-character password
25
+ SMTP_HOST=smtp.gmail.com
26
+ SMTP_PORT=587
27
+ SMTP_USERNAME=your_email@gmail.com
28
+ SMTP_PASSWORD=your_app_specific_password_here
29
+ SMTP_USE_TLS=true
30
+ EMAIL_FROM=your_email@gmail.com
31
+ EMAIL_FROM_NAME=Todo Application
32
+
33
+ # Frontend URL (for password reset links)
34
+ FRONTEND_URL=http://localhost:3000
35
+ # For production:
36
+ # FRONTEND_URL=https://your-frontend-url.com
37
+
38
+ # Password Reset Configuration
39
+ PASSWORD_RESET_TOKEN_EXPIRY_MINUTES=15
40
+ PASSWORD_RESET_MAX_REQUESTS_PER_HOUR=3
41
+
42
+ # Cohere AI API Configuration
43
+ # Get your API key from: https://dashboard.cohere.com/api-keys
44
+ COHERE_API_KEY=your-cohere-api-key-here
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .vercel
2
+ .env*.local
3
+
4
+ # Test data and tokens
5
+ *.token.json
6
+ *_token.json
7
+ token.txt
8
+ *_test.json
9
+ *_response.json
10
+ signin_*.json
11
+ signup_*.json
12
+ fresh_token.json
DOCKER_SETUP.md ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docker Setup Guide
2
+
3
+ ## Quick Start
4
+
5
+ ### 1. Build and Run with Docker Compose (Recommended)
6
+
7
+ ```bash
8
+ # Start all services (API + PostgreSQL)
9
+ docker-compose up -d
10
+
11
+ # View logs
12
+ docker-compose logs -f
13
+
14
+ # Stop services
15
+ docker-compose down
16
+
17
+ # Stop and remove volumes (clears database)
18
+ docker-compose down -v
19
+ ```
20
+
21
+ ### 2. Build and Run Dockerfile Only
22
+
23
+ ```bash
24
+ # Build the image
25
+ docker build -t todo-api .
26
+
27
+ # Run the container
28
+ docker run -p 8000:8000 --env-file .env todo-api
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ ### Environment Variables
34
+
35
+ Create a `.env` file in the project root with your configuration:
36
+
37
+ ```env
38
+ # Required for docker-compose
39
+ JWT_SECRET_KEY=your-super-secret-key-min-32-chars
40
+ SMTP_USERNAME=your_email@gmail.com
41
+ SMTP_PASSWORD=your_app_password
42
+ EMAIL_FROM=your_email@gmail.com
43
+ COHERE_API_KEY=your-cohere-key
44
+
45
+ # Optional overrides
46
+ FRONTEND_URL=http://localhost:3000
47
+ EMAIL_FROM_NAME=Todo Application
48
+ ```
49
+
50
+ ## Accessing the Application
51
+
52
+ Once running:
53
+ - API: http://localhost:8000
54
+ - API Docs: http://localhost:8000/docs
55
+ - Health Check: http://localhost:8000/health
56
+ - PostgreSQL: localhost:5432
57
+
58
+ ## Database Management
59
+
60
+ ### Run Migrations
61
+
62
+ ```bash
63
+ # Access the API container
64
+ docker-compose exec api bash
65
+
66
+ # Run migrations (if using Alembic)
67
+ alembic upgrade head
68
+ ```
69
+
70
+ ### Access PostgreSQL
71
+
72
+ ```bash
73
+ # Connect to database
74
+ docker-compose exec db psql -U todouser -d tododb
75
+
76
+ # Backup database
77
+ docker-compose exec db pg_dump -U todouser tododb > backup.sql
78
+
79
+ # Restore database
80
+ docker-compose exec -T db psql -U todouser tododb < backup.sql
81
+ ```
82
+
83
+ ## Development Workflow
84
+
85
+ ### Hot Reload (Development)
86
+
87
+ The docker-compose.yml mounts `./src` as a volume, so code changes are reflected immediately.
88
+
89
+ ### View Logs
90
+
91
+ ```bash
92
+ # All services
93
+ docker-compose logs -f
94
+
95
+ # Specific service
96
+ docker-compose logs -f api
97
+ docker-compose logs -f db
98
+ ```
99
+
100
+ ### Rebuild After Dependency Changes
101
+
102
+ ```bash
103
+ # Rebuild and restart
104
+ docker-compose up -d --build
105
+ ```
106
+
107
+ ## Production Deployment
108
+
109
+ ### Build Production Image
110
+
111
+ ```bash
112
+ docker build -t todo-api:production .
113
+ ```
114
+
115
+ ### Security Considerations
116
+
117
+ 1. Change default PostgreSQL credentials in docker-compose.yml
118
+ 2. Use strong JWT_SECRET_KEY
119
+ 3. Enable HTTPS in production
120
+ 4. Use secrets management (Docker Secrets, Kubernetes Secrets)
121
+ 5. Regular security updates: `docker-compose pull && docker-compose up -d`
122
+
123
+ ### Deploy to Cloud
124
+
125
+ **Docker Hub:**
126
+ ```bash
127
+ docker tag todo-api:production yourusername/todo-api:latest
128
+ docker push yourusername/todo-api:latest
129
+ ```
130
+
131
+ **AWS ECS, Google Cloud Run, Azure Container Instances:**
132
+ - Use the Dockerfile to build and deploy
133
+ - Set environment variables in the cloud platform
134
+ - Use managed PostgreSQL services (RDS, Cloud SQL, Azure Database)
135
+
136
+ ## Troubleshooting
137
+
138
+ ### Container won't start
139
+ ```bash
140
+ # Check logs
141
+ docker-compose logs api
142
+
143
+ # Check if port is already in use
144
+ netstat -ano | findstr :8000 # Windows
145
+ lsof -i :8000 # Linux/Mac
146
+ ```
147
+
148
+ ### Database connection issues
149
+ ```bash
150
+ # Verify database is healthy
151
+ docker-compose ps
152
+
153
+ # Test connection
154
+ docker-compose exec api python -c "from src.database import engine; print(engine.url)"
155
+ ```
156
+
157
+ ### Reset everything
158
+ ```bash
159
+ # Stop and remove all containers, networks, and volumes
160
+ docker-compose down -v
161
+ docker-compose up -d --build
162
+ ```
163
+
164
+ ## Image Size Optimization
165
+
166
+ Current Dockerfile uses multi-stage builds for smaller image size:
167
+ - Builder stage: ~1GB (includes build tools)
168
+ - Final stage: ~200-300MB (runtime only)
169
+
170
+ ## Health Checks
171
+
172
+ The Dockerfile includes a health check that pings `/health` endpoint every 30 seconds.
173
+
174
+ Check container health:
175
+ ```bash
176
+ docker ps
177
+ # Look for "healthy" status
178
+ ```
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 slim image
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy requirements first for better caching
8
+ COPY requirements.txt .
9
+
10
+ # Install Python dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy application code
14
+ COPY src ./src
15
+ COPY api ./api
16
+ COPY alembic ./alembic
17
+ COPY alembic.ini .
18
+ COPY init_db.py .
19
+ COPY .env.example .env
20
+
21
+ # Expose port
22
+ EXPOSE 7860
23
+
24
+ # Run the application
25
+ CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,53 @@
1
  ---
2
- title: Todo Web
3
- emoji: 👀
4
- colorFrom: pink
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Todo Web Application
3
+ emoji:
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
+ app_port: 7860
10
  ---
11
 
12
+ # Todo Web Application
13
+
14
+ A full-featured Todo application with JWT authentication, task management, and AI-powered features.
15
+
16
+ ## Features
17
+
18
+ - 🔐 User authentication with JWT
19
+ - ✅ Create, read, update, delete tasks
20
+ - 📝 Subtasks support
21
+ - 🔄 Task status management
22
+ - 🎨 Modern REST API
23
+ - 📧 Password reset functionality
24
+
25
+ ## API Documentation
26
+
27
+ Once deployed, visit `/docs` for interactive API documentation.
28
+
29
+ ## Tech Stack
30
+
31
+ - FastAPI
32
+ - SQLModel
33
+ - SQLite
34
+ - JWT Authentication
35
+ - Python 3.11
36
+
37
+ ## Endpoints
38
+
39
+ - `GET /` - API information
40
+ - `GET /health` - Health check
41
+ - `GET /docs` - API documentation
42
+ - `POST /api/auth/signup` - User registration
43
+ - `POST /api/auth/login` - User login
44
+ - `GET /api/tasks` - Get all tasks
45
+ - `POST /api/tasks` - Create new task
46
+ - And more...
47
+
48
+ ## Local Development
49
+
50
+ ```bash
51
+ pip install -r requirements.txt
52
+ uvicorn src.main:app --reload
53
+ ```
alembic.ini ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
10
+
11
+ # sys.path path, will be prepended to sys.path if present.
12
+ # defaults to the current working directory.
13
+ prepend_sys_path = .
14
+
15
+ # timezone to use when rendering the date within the migration file
16
+ # as well as the filename.
17
+ # If specified, requires the python>=3.9 or backports.zoneinfo library.
18
+ # Any required deps can installed by adding `alembic[tz]` to the pip requirements
19
+ # string value is passed to ZoneInfo()
20
+ # leave blank for localtime
21
+ # timezone =
22
+
23
+ # max length of characters to apply to the
24
+ # "slug" field
25
+ # truncate_slug_length = 40
26
+
27
+ # set to 'true' to run the environment during
28
+ # the 'revision' command, regardless of autogenerate
29
+ # revision_environment = false
30
+
31
+ # set to 'true' to allow .pyc and .pyo files without
32
+ # a source .py file to be detected as revisions in the
33
+ # versions/ directory
34
+ # sourceless = false
35
+
36
+ # version location specification; This defaults
37
+ # to alembic/versions. When using multiple version
38
+ # directories, initial revisions must be specified with --version-path.
39
+ # The path separator used here should be the separator specified by "version_path_separator" below.
40
+ # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
41
+
42
+ # version path separator; As mentioned above, this is the character used to split
43
+ # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
44
+ # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
45
+ # Valid values for version_path_separator are:
46
+ #
47
+ # version_path_separator = :
48
+ # version_path_separator = ;
49
+ # version_path_separator = space
50
+ version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
51
+
52
+ # set to 'true' to search source files recursively
53
+ # in each "version_locations" directory
54
+ # new in Alembic version 1.10
55
+ # recursive_version_locations = false
56
+
57
+ # the output encoding used when revision files
58
+ # are written from script.py.mako
59
+ # output_encoding = utf-8
60
+
61
+ sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/todo_db
62
+
63
+
64
+ [post_write_hooks]
65
+ # post_write_hooks defines scripts or Python functions that are run
66
+ # on newly generated revision scripts. See the documentation for further
67
+ # detail and examples
68
+
69
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
70
+ # hooks = black
71
+ # black.type = console_scripts
72
+ # black.entrypoint = black
73
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
74
+
75
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
76
+ # hooks = ruff
77
+ # ruff.type = exec
78
+ # ruff.executable = %(here)s/.venv/bin/ruff
79
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
80
+
81
+ # Logging configuration
82
+ [loggers]
83
+ keys = root,sqlalchemy,alembic
84
+
85
+ [handlers]
86
+ keys = console
87
+
88
+ [formatters]
89
+ keys = generic
90
+
91
+ [logger_root]
92
+ level = WARN
93
+ handlers = console
94
+ qualname =
95
+
96
+ [logger_sqlalchemy]
97
+ level = WARN
98
+ handlers =
99
+ qualname = sqlalchemy.engine
100
+
101
+ [logger_alembic]
102
+ level = INFO
103
+ handlers =
104
+ qualname = alembic
105
+
106
+ [handler_console]
107
+ class = StreamHandler
108
+ args = (sys.stderr,)
109
+ level = NOTSET
110
+ formatter = generic
111
+
112
+ [formatter_generic]
113
+ format = %(levelname)-5.5s [%(name)s] %(message)s
114
+ datefmt = %H:%M:%S
alembic/env.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+
3
+ from sqlalchemy import engine_from_config
4
+ from sqlalchemy import pool
5
+
6
+ from alembic import context
7
+
8
+ # Import your models here
9
+ import sys
10
+ import os
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
12
+
13
+ from src.models.user import User
14
+ from src.models.task import Task
15
+ from sqlmodel import SQLModel
16
+
17
+ # this is the Alembic Config object, which provides
18
+ # access to the values within the .ini file in use.
19
+ config = context.config
20
+
21
+ # Interpret the config file for Python logging.
22
+ # This line sets up loggers basically.
23
+ if config.config_file_name is not None:
24
+ fileConfig(config.config_file_name)
25
+
26
+ # add your model's MetaData object here
27
+ # for 'autogenerate' support
28
+ target_metadata = SQLModel.metadata
29
+
30
+ # other values from the config, defined by the needs of env.py,
31
+ # can be acquired:
32
+ # my_important_option = config.get_main_option("my_important_option")
33
+ # ... etc.
34
+
35
+
36
+ def run_migrations_offline() -> None:
37
+ """Run migrations in 'offline' mode.
38
+
39
+ This configures the context with just a URL
40
+ and not an Engine, though an Engine is acceptable
41
+ here as well. By skipping the Engine creation
42
+ we don't even need a DBAPI to be available.
43
+
44
+ Calls to context.execute() here emit the given string to the
45
+ script output.
46
+
47
+ """
48
+ url = config.get_main_option("sqlalchemy.url")
49
+ context.configure(
50
+ url=url,
51
+ target_metadata=target_metadata,
52
+ literal_binds=True,
53
+ dialect_opts={"paramstyle": "named"},
54
+ )
55
+
56
+ with context.begin_transaction():
57
+ context.run_migrations()
58
+
59
+
60
+ def run_migrations_online() -> None:
61
+ """Run migrations in 'online' mode.
62
+
63
+ In this scenario we need to create an Engine
64
+ and associate a connection with the context.
65
+
66
+ """
67
+ connectable = engine_from_config(
68
+ config.get_section(config.config_ini_section, {}),
69
+ prefix="sqlalchemy.",
70
+ poolclass=pool.NullPool,
71
+ )
72
+
73
+ with connectable.connect() as connection:
74
+ context.configure(
75
+ connection=connection, target_metadata=target_metadata
76
+ )
77
+
78
+ with context.begin_transaction():
79
+ context.run_migrations()
80
+
81
+
82
+ if context.is_offline_mode():
83
+ run_migrations_offline()
84
+ else:
85
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade() -> None:
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade() -> None:
24
+ ${downgrades if downgrades else "pass"}
alembic/versions/001_initial_schema.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Initial schema
2
+
3
+ Revision ID: 001
4
+ Revises:
5
+ Create Date: 2026-02-05
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ from sqlalchemy.dialects import postgresql
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '001'
14
+ down_revision = None
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # Create users table
21
+ op.create_table(
22
+ 'users',
23
+ sa.Column('id', sa.Integer(), nullable=False),
24
+ sa.Column('email', sa.String(length=255), nullable=False),
25
+ sa.Column('hashed_password', sa.String(length=255), nullable=False),
26
+ sa.Column('created_at', sa.DateTime(), nullable=False),
27
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
28
+ sa.PrimaryKeyConstraint('id')
29
+ )
30
+ op.create_index('idx_users_email', 'users', ['email'], unique=True)
31
+
32
+ # Create tasks table
33
+ op.create_table(
34
+ 'tasks',
35
+ sa.Column('id', sa.Integer(), nullable=False),
36
+ sa.Column('user_id', sa.Integer(), nullable=False),
37
+ sa.Column('title', sa.String(length=500), nullable=False),
38
+ sa.Column('description', sa.Text(), nullable=True),
39
+ sa.Column('completed', sa.Boolean(), nullable=False, server_default='false'),
40
+ sa.Column('created_at', sa.DateTime(), nullable=False),
41
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
42
+ sa.PrimaryKeyConstraint('id'),
43
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE')
44
+ )
45
+ op.create_index('idx_tasks_user_id', 'tasks', ['user_id'], unique=False)
46
+
47
+
48
+ def downgrade() -> None:
49
+ op.drop_index('idx_tasks_user_id', table_name='tasks')
50
+ op.drop_table('tasks')
51
+ op.drop_index('idx_users_email', table_name='users')
52
+ op.drop_table('users')
alembic/versions/002_add_password_reset_tokens.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add password reset tokens table
2
+
3
+ Revision ID: 002
4
+ Revises: a6878af5b66f
5
+ Create Date: 2026-02-07
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+ # revision identifiers, used by Alembic.
12
+ revision = '002'
13
+ down_revision = 'a6878af5b66f'
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ # Create password_reset_tokens table
20
+ op.create_table(
21
+ 'password_reset_tokens',
22
+ sa.Column('id', sa.Integer(), nullable=False),
23
+ sa.Column('user_id', sa.Integer(), nullable=False),
24
+ sa.Column('token', sa.String(length=255), nullable=False),
25
+ sa.Column('expires_at', sa.DateTime(), nullable=False),
26
+ sa.Column('used', sa.Boolean(), nullable=False, server_default='false'),
27
+ sa.Column('created_at', sa.DateTime(), nullable=False),
28
+ sa.PrimaryKeyConstraint('id'),
29
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE')
30
+ )
31
+ op.create_index('idx_password_reset_tokens_user_id', 'password_reset_tokens', ['user_id'], unique=False)
32
+ op.create_index('idx_password_reset_tokens_token', 'password_reset_tokens', ['token'], unique=True)
33
+ op.create_index('idx_password_reset_tokens_expires_at', 'password_reset_tokens', ['expires_at'], unique=False)
34
+
35
+
36
+ def downgrade() -> None:
37
+ op.drop_index('idx_password_reset_tokens_expires_at', table_name='password_reset_tokens')
38
+ op.drop_index('idx_password_reset_tokens_token', table_name='password_reset_tokens')
39
+ op.drop_index('idx_password_reset_tokens_user_id', table_name='password_reset_tokens')
40
+ op.drop_table('password_reset_tokens')
alembic/versions/a6878af5b66f_add_category_and_due_date_to_tasks.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """add_category_and_due_date_to_tasks
2
+
3
+ Revision ID: a6878af5b66f
4
+ Revises: 001
5
+ Create Date: 2026-02-05 14:23:11.577860
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = 'a6878af5b66f'
14
+ down_revision = '001'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # Add category column
21
+ op.add_column('tasks', sa.Column('category', sa.String(length=50), nullable=True))
22
+
23
+ # Add due_date column
24
+ op.add_column('tasks', sa.Column('due_date', sa.DateTime(), nullable=True))
25
+
26
+
27
+ def downgrade() -> None:
28
+ # Remove columns in reverse order
29
+ op.drop_column('tasks', 'due_date')
30
+ op.drop_column('tasks', 'category')
api/index.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Vercel Serverless Function for FastAPI
3
+ Vercel natively supports ASGI apps - just export the app directly
4
+ """
5
+ import sys
6
+ import os
7
+
8
+ # Add parent directory to path for imports
9
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10
+
11
+ from src.main import app
12
+
13
+ # Vercel will automatically detect and handle the ASGI app
14
+ # No need for Mangum or any wrapper
15
+
16
+ # For local testing
17
+ if __name__ == "__main__":
18
+ import uvicorn
19
+ uvicorn.run(app, host="0.0.0.0", port=8000)
api/test.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Minimal test endpoint for Vercel deployment debugging
3
+ """
4
+ from fastapi import FastAPI
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ import os
7
+
8
+ app = FastAPI(title="Todo API - Minimal Test")
9
+
10
+ # CORS
11
+ app.add_middleware(
12
+ CORSMiddleware,
13
+ allow_origins=["*"],
14
+ allow_credentials=True,
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ )
18
+
19
+ @app.get("/")
20
+ async def root():
21
+ return {
22
+ "status": "ok",
23
+ "message": "Minimal FastAPI working on Vercel",
24
+ "environment": {
25
+ "VERCEL": os.getenv("VERCEL", "not set"),
26
+ "VERCEL_ENV": os.getenv("VERCEL_ENV", "not set"),
27
+ }
28
+ }
29
+
30
+ @app.get("/health")
31
+ async def health():
32
+ return {"status": "healthy"}
33
+
34
+ @app.get("/test-db")
35
+ async def test_db():
36
+ """Test database connection"""
37
+ try:
38
+ from src.database import engine
39
+ from sqlmodel import text
40
+
41
+ with engine.connect() as conn:
42
+ result = conn.execute(text("SELECT 1"))
43
+ return {"status": "ok", "database": "connected"}
44
+ except Exception as e:
45
+ return {"status": "error", "message": str(e)}
docker-compose.yml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ # PostgreSQL Database
5
+ db:
6
+ image: postgres:15-alpine
7
+ container_name: todo-db
8
+ environment:
9
+ POSTGRES_USER: todouser
10
+ POSTGRES_PASSWORD: todopassword
11
+ POSTGRES_DB: tododb
12
+ volumes:
13
+ - postgres_data:/var/lib/postgresql/data
14
+ ports:
15
+ - "5432:5432"
16
+ healthcheck:
17
+ test: ["CMD-SHELL", "pg_isready -U todouser"]
18
+ interval: 10s
19
+ timeout: 5s
20
+ retries: 5
21
+
22
+ # FastAPI Backend
23
+ api:
24
+ build:
25
+ context: .
26
+ dockerfile: Dockerfile
27
+ container_name: todo-api
28
+ environment:
29
+ DATABASE_URL: postgresql://todouser:todopassword@db:5432/tododb
30
+ JWT_SECRET_KEY: ${JWT_SECRET_KEY:-your-super-secret-key-change-this-min-32-characters-long}
31
+ JWT_ALGORITHM: HS256
32
+ ACCESS_TOKEN_EXPIRE_MINUTES: 30
33
+ CORS_ORIGINS: http://localhost:3000,http://localhost:3001
34
+ SMTP_HOST: ${SMTP_HOST:-smtp.gmail.com}
35
+ SMTP_PORT: ${SMTP_PORT:-587}
36
+ SMTP_USERNAME: ${SMTP_USERNAME}
37
+ SMTP_PASSWORD: ${SMTP_PASSWORD}
38
+ SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
39
+ EMAIL_FROM: ${EMAIL_FROM}
40
+ EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-Todo Application}
41
+ FRONTEND_URL: ${FRONTEND_URL:-http://localhost:3000}
42
+ PASSWORD_RESET_TOKEN_EXPIRY_MINUTES: 15
43
+ PASSWORD_RESET_MAX_REQUESTS_PER_HOUR: 3
44
+ COHERE_API_KEY: ${COHERE_API_KEY}
45
+ ports:
46
+ - "8000:8000"
47
+ depends_on:
48
+ db:
49
+ condition: service_healthy
50
+ volumes:
51
+ - ./src:/app/src
52
+ restart: unless-stopped
53
+
54
+ volumes:
55
+ postgres_data:
init_db.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Initialize database tables for the Todo application.
3
+ """
4
+ from src.database import create_db_and_tables
5
+
6
+ if __name__ == "__main__":
7
+ print("Creating database tables...")
8
+ create_db_and_tables()
9
+ print("Database tables created successfully!")
migrate_db.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple migration script to add category and due_date columns to tasks table.
3
+ """
4
+ import sqlite3
5
+
6
+ # Connect to database
7
+ conn = sqlite3.connect('todo.db')
8
+ cursor = conn.cursor()
9
+
10
+ try:
11
+ # Check if columns exist
12
+ cursor.execute("PRAGMA table_info(tasks)")
13
+ columns = [col[1] for col in cursor.fetchall()]
14
+
15
+ # Add category column if it doesn't exist
16
+ if 'category' not in columns:
17
+ cursor.execute("ALTER TABLE tasks ADD COLUMN category VARCHAR(50)")
18
+ print("Added 'category' column")
19
+ else:
20
+ print("'category' column already exists")
21
+
22
+ # Add due_date column if it doesn't exist
23
+ if 'due_date' not in columns:
24
+ cursor.execute("ALTER TABLE tasks ADD COLUMN due_date DATETIME")
25
+ print("Added 'due_date' column")
26
+ else:
27
+ print("'due_date' column already exists")
28
+
29
+ conn.commit()
30
+ print("\nDatabase migration completed successfully!")
31
+
32
+ except Exception as e:
33
+ print(f"Error: {e}")
34
+ conn.rollback()
35
+ finally:
36
+ conn.close()
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ sqlmodel
3
+ python-jose
4
+ passlib
5
+ bcrypt
6
+ python-multipart
7
+ uvicorn
8
+ psycopg2-binary
9
+ pydantic
10
+ pydantic-settings
11
+ python-dotenv
12
+ mangum
13
+ email-validator
src/__pycache__/database.cpython-314.pyc ADDED
Binary file (2.03 kB). View file
 
src/__pycache__/main.cpython-314.pyc ADDED
Binary file (2.81 kB). View file
 
src/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # API module
src/api/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (206 Bytes). View file
 
src/api/__pycache__/auth.cpython-314.pyc ADDED
Binary file (6.72 kB). View file
 
src/api/__pycache__/password_reset.cpython-314.pyc ADDED
Binary file (8.97 kB). View file
 
src/api/__pycache__/subtasks.cpython-314.pyc ADDED
Binary file (9.82 kB). View file
 
src/api/__pycache__/tasks.cpython-314.pyc ADDED
Binary file (13.3 kB). View file
 
src/api/ai.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI-powered task management endpoints using Cohere.
3
+
4
+ This module provides REST API endpoints for AI features:
5
+ - Task suggestions
6
+ - Smart auto-completion
7
+ - Task categorization
8
+ - Description enhancement
9
+ - Complexity analysis
10
+ """
11
+
12
+ from fastapi import APIRouter, HTTPException, Depends
13
+ from pydantic import BaseModel, Field
14
+ from typing import List, Dict, Optional
15
+ from src.services.cohere_ai import cohere_service
16
+ from src.middleware.jwt_auth import get_current_user
17
+
18
+ router = APIRouter()
19
+
20
+
21
+ # Request/Response Models
22
+ class TaskSuggestionRequest(BaseModel):
23
+ context: str = Field(..., description="Context to generate suggestions from")
24
+ count: int = Field(default=5, ge=1, le=10, description="Number of suggestions")
25
+
26
+
27
+ class TaskSuggestionResponse(BaseModel):
28
+ suggestions: List[str]
29
+
30
+
31
+ class EnhanceDescriptionRequest(BaseModel):
32
+ title: str = Field(..., description="Task title")
33
+ description: str = Field(default="", description="Current description")
34
+
35
+
36
+ class EnhanceDescriptionResponse(BaseModel):
37
+ enhanced_description: str
38
+
39
+
40
+ class CategorizeTaskRequest(BaseModel):
41
+ title: str = Field(..., description="Task title")
42
+ description: str = Field(default="", description="Task description")
43
+
44
+
45
+ class CategorizeTaskResponse(BaseModel):
46
+ category: str
47
+ priority: str
48
+ tags: List[str]
49
+
50
+
51
+ class AutoCompleteRequest(BaseModel):
52
+ partial_title: str = Field(..., description="Partial task title")
53
+
54
+
55
+ class AutoCompleteResponse(BaseModel):
56
+ completions: List[str]
57
+
58
+
59
+ class AnalyzeComplexityRequest(BaseModel):
60
+ title: str = Field(..., description="Task title")
61
+ description: str = Field(default="", description="Task description")
62
+
63
+
64
+ class AnalyzeComplexityResponse(BaseModel):
65
+ complexity: str
66
+ estimated_time: str
67
+ needs_subtasks: bool
68
+
69
+
70
+ # Endpoints
71
+ @router.post("/suggestions", response_model=TaskSuggestionResponse)
72
+ async def generate_task_suggestions(
73
+ request: TaskSuggestionRequest,
74
+ current_user: dict = Depends(get_current_user)
75
+ ):
76
+ """
77
+ Generate AI-powered task suggestions based on context.
78
+
79
+ Requires authentication.
80
+ """
81
+ try:
82
+ suggestions = cohere_service.generate_task_suggestions(
83
+ context=request.context,
84
+ count=request.count
85
+ )
86
+
87
+ if not suggestions:
88
+ raise HTTPException(
89
+ status_code=500,
90
+ detail="Failed to generate suggestions. Please try again."
91
+ )
92
+
93
+ return TaskSuggestionResponse(suggestions=suggestions)
94
+
95
+ except Exception as e:
96
+ raise HTTPException(
97
+ status_code=500,
98
+ detail=f"Error generating suggestions: {str(e)}"
99
+ )
100
+
101
+
102
+ @router.post("/enhance-description", response_model=EnhanceDescriptionResponse)
103
+ async def enhance_task_description(
104
+ request: EnhanceDescriptionRequest,
105
+ current_user: dict = Depends(get_current_user)
106
+ ):
107
+ """
108
+ Enhance a task description with AI to make it more clear and actionable.
109
+
110
+ Requires authentication.
111
+ """
112
+ try:
113
+ enhanced = cohere_service.enhance_task_description(
114
+ title=request.title,
115
+ description=request.description
116
+ )
117
+
118
+ return EnhanceDescriptionResponse(enhanced_description=enhanced)
119
+
120
+ except Exception as e:
121
+ raise HTTPException(
122
+ status_code=500,
123
+ detail=f"Error enhancing description: {str(e)}"
124
+ )
125
+
126
+
127
+ @router.post("/categorize", response_model=CategorizeTaskResponse)
128
+ async def categorize_task(
129
+ request: CategorizeTaskRequest,
130
+ current_user: dict = Depends(get_current_user)
131
+ ):
132
+ """
133
+ Categorize a task and suggest priority level using AI.
134
+
135
+ Requires authentication.
136
+ """
137
+ try:
138
+ result = cohere_service.categorize_task(
139
+ title=request.title,
140
+ description=request.description
141
+ )
142
+
143
+ return CategorizeTaskResponse(**result)
144
+
145
+ except Exception as e:
146
+ raise HTTPException(
147
+ status_code=500,
148
+ detail=f"Error categorizing task: {str(e)}"
149
+ )
150
+
151
+
152
+ @router.post("/autocomplete", response_model=AutoCompleteResponse)
153
+ async def autocomplete_task(
154
+ request: AutoCompleteRequest,
155
+ current_user: dict = Depends(get_current_user)
156
+ ):
157
+ """
158
+ Provide smart auto-completion suggestions for task titles.
159
+
160
+ Requires authentication.
161
+ """
162
+ try:
163
+ completions = cohere_service.smart_complete_task(
164
+ partial_title=request.partial_title
165
+ )
166
+
167
+ return AutoCompleteResponse(completions=completions)
168
+
169
+ except Exception as e:
170
+ raise HTTPException(
171
+ status_code=500,
172
+ detail=f"Error generating completions: {str(e)}"
173
+ )
174
+
175
+
176
+ @router.post("/analyze-complexity", response_model=AnalyzeComplexityResponse)
177
+ async def analyze_task_complexity(
178
+ request: AnalyzeComplexityRequest,
179
+ current_user: dict = Depends(get_current_user)
180
+ ):
181
+ """
182
+ Analyze task complexity and provide time estimates using AI.
183
+
184
+ Requires authentication.
185
+ """
186
+ try:
187
+ result = cohere_service.analyze_task_complexity(
188
+ title=request.title,
189
+ description=request.description
190
+ )
191
+
192
+ return AnalyzeComplexityResponse(**result)
193
+
194
+ except Exception as e:
195
+ raise HTTPException(
196
+ status_code=500,
197
+ detail=f"Error analyzing complexity: {str(e)}"
198
+ )
199
+
200
+
201
+ @router.get("/health")
202
+ async def ai_health_check():
203
+ """
204
+ Check if AI service is properly configured.
205
+
206
+ Does not require authentication.
207
+ """
208
+ try:
209
+ import os
210
+ api_key = os.getenv("COHERE_API_KEY")
211
+
212
+ if not api_key:
213
+ return {
214
+ "status": "error",
215
+ "message": "COHERE_API_KEY not configured"
216
+ }
217
+
218
+ return {
219
+ "status": "healthy",
220
+ "message": "AI service is configured and ready",
221
+ "provider": "Cohere"
222
+ }
223
+
224
+ except Exception as e:
225
+ return {
226
+ "status": "error",
227
+ "message": str(e)
228
+ }
src/api/auth.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication API endpoints for user signup and signin.
3
+
4
+ This module provides:
5
+ - POST /api/auth/signup - Create new user account
6
+ - POST /api/auth/signin - Authenticate existing user
7
+ """
8
+
9
+ from fastapi import APIRouter, HTTPException, Depends
10
+ from sqlmodel import Session, select
11
+ from pydantic import BaseModel, EmailStr, Field
12
+
13
+ from ..models.user import User
14
+ from ..services.auth import hash_password, verify_password, create_access_token
15
+ from ..database import get_session
16
+
17
+ router = APIRouter()
18
+
19
+
20
+ # Request/Response Models
21
+ class SignUpRequest(BaseModel):
22
+ """Request model for user signup."""
23
+ email: EmailStr = Field(..., description="User email address")
24
+ password: str = Field(..., min_length=8, description="User password (minimum 8 characters)")
25
+
26
+
27
+ class SignInRequest(BaseModel):
28
+ """Request model for user signin."""
29
+ email: EmailStr = Field(..., description="User email address")
30
+ password: str = Field(..., description="User password")
31
+
32
+
33
+ class UserResponse(BaseModel):
34
+ """User data response model."""
35
+ id: int
36
+ email: str
37
+ created_at: str
38
+ updated_at: str
39
+
40
+
41
+ class AuthResponse(BaseModel):
42
+ """Authentication response with token and user data."""
43
+ token: str
44
+ user: UserResponse
45
+
46
+
47
+ @router.post("/signup", response_model=AuthResponse, status_code=201)
48
+ async def signup(
49
+ request: SignUpRequest,
50
+ session: Session = Depends(get_session)
51
+ ) -> AuthResponse:
52
+ """
53
+ Create a new user account.
54
+
55
+ Args:
56
+ request: Signup request with email and password
57
+ session: Database session
58
+
59
+ Returns:
60
+ AuthResponse with JWT token and user data
61
+
62
+ Raises:
63
+ HTTPException 400: If email already exists
64
+ HTTPException 422: If validation fails
65
+ """
66
+ # Check if email already exists
67
+ statement = select(User).where(User.email == request.email)
68
+ existing_user = session.exec(statement).first()
69
+
70
+ if existing_user:
71
+ raise HTTPException(
72
+ status_code=400,
73
+ detail="Email already registered"
74
+ )
75
+
76
+ # Hash password
77
+ hashed_password = hash_password(request.password)
78
+
79
+ # Create new user
80
+ new_user = User(
81
+ email=request.email,
82
+ hashed_password=hashed_password
83
+ )
84
+
85
+ session.add(new_user)
86
+ session.commit()
87
+ session.refresh(new_user)
88
+
89
+ # Create JWT token
90
+ token = create_access_token(
91
+ data={
92
+ "user_id": new_user.id,
93
+ "email": new_user.email
94
+ }
95
+ )
96
+
97
+ # Return response
98
+ return AuthResponse(
99
+ token=token,
100
+ user=UserResponse(
101
+ id=new_user.id,
102
+ email=new_user.email,
103
+ created_at=new_user.created_at.isoformat(),
104
+ updated_at=new_user.updated_at.isoformat()
105
+ )
106
+ )
107
+
108
+
109
+ @router.post("/signin", response_model=AuthResponse)
110
+ async def signin(
111
+ request: SignInRequest,
112
+ session: Session = Depends(get_session)
113
+ ) -> AuthResponse:
114
+ """
115
+ Authenticate an existing user.
116
+
117
+ Args:
118
+ request: Signin request with email and password
119
+ session: Database session
120
+
121
+ Returns:
122
+ AuthResponse with JWT token and user data
123
+
124
+ Raises:
125
+ HTTPException 401: If credentials are invalid
126
+ """
127
+ # Find user by email
128
+ statement = select(User).where(User.email == request.email)
129
+ user = session.exec(statement).first()
130
+
131
+ # Verify user exists and password is correct
132
+ if not user or not verify_password(request.password, user.hashed_password):
133
+ raise HTTPException(
134
+ status_code=401,
135
+ detail="Invalid email or password"
136
+ )
137
+
138
+ # Create JWT token
139
+ token = create_access_token(
140
+ data={
141
+ "user_id": user.id,
142
+ "email": user.email
143
+ }
144
+ )
145
+
146
+ # Return response
147
+ return AuthResponse(
148
+ token=token,
149
+ user=UserResponse(
150
+ id=user.id,
151
+ email=user.email,
152
+ created_at=user.created_at.isoformat(),
153
+ updated_at=user.updated_at.isoformat()
154
+ )
155
+ )
src/api/password_reset.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Password Reset API endpoints for secure password recovery.
3
+
4
+ This module provides:
5
+ - POST /api/auth/forgot-password - Request password reset email
6
+ - GET /api/auth/reset-password/{token} - Verify reset token validity
7
+ - POST /api/auth/reset-password - Reset password with token
8
+ """
9
+
10
+ from fastapi import APIRouter, HTTPException, Depends
11
+ from sqlmodel import Session
12
+ from pydantic import BaseModel, EmailStr, Field
13
+ from typing import Optional
14
+
15
+ from ..models.user import User
16
+ from ..services.auth import hash_password
17
+ from ..services.password_reset import (
18
+ create_reset_token,
19
+ validate_reset_token,
20
+ invalidate_token,
21
+ check_rate_limit,
22
+ validate_password_strength,
23
+ get_user_by_email
24
+ )
25
+ from ..services.email import send_password_reset_email
26
+ from ..database import get_session
27
+
28
+ router = APIRouter()
29
+
30
+
31
+ # Request/Response Models
32
+ class ForgotPasswordRequest(BaseModel):
33
+ """Request model for forgot password."""
34
+ email: EmailStr = Field(..., description="User email address")
35
+
36
+
37
+ class ForgotPasswordResponse(BaseModel):
38
+ """Response model for forgot password request."""
39
+ message: str
40
+
41
+
42
+ class TokenValidationResponse(BaseModel):
43
+ """Response model for token validation."""
44
+ valid: bool
45
+ email: Optional[str] = None
46
+ error: Optional[str] = None
47
+
48
+
49
+ class ResetPasswordRequest(BaseModel):
50
+ """Request model for password reset."""
51
+ token: str = Field(..., description="Password reset token")
52
+ new_password: str = Field(..., min_length=8, description="New password (minimum 8 characters)")
53
+
54
+
55
+ class ResetPasswordResponse(BaseModel):
56
+ """Response model for password reset."""
57
+ message: str
58
+
59
+
60
+ @router.post("/forgot-password", response_model=ForgotPasswordResponse)
61
+ async def forgot_password(
62
+ request: ForgotPasswordRequest,
63
+ session: Session = Depends(get_session)
64
+ ) -> ForgotPasswordResponse:
65
+ """
66
+ Request a password reset email.
67
+
68
+ Security features:
69
+ - No user enumeration (same response for existing/non-existing emails)
70
+ - Rate limiting (3 requests per hour per user)
71
+ - Cryptographically secure tokens
72
+ - 15-minute token expiry
73
+
74
+ Args:
75
+ request: Forgot password request with email
76
+ session: Database session
77
+
78
+ Returns:
79
+ Generic success message (no user enumeration)
80
+
81
+ Raises:
82
+ HTTPException 400: If email format is invalid
83
+ HTTPException 429: If rate limit exceeded
84
+ """
85
+ # Find user by email
86
+ user = get_user_by_email(session, request.email)
87
+
88
+ # Always return same message to prevent user enumeration
89
+ generic_message = "If an account exists with this email, you will receive a password reset link shortly."
90
+
91
+ # If user doesn't exist, return generic message (no enumeration)
92
+ if not user:
93
+ return ForgotPasswordResponse(message=generic_message)
94
+
95
+ # Check rate limit
96
+ if not check_rate_limit(session, user.id):
97
+ raise HTTPException(
98
+ status_code=429,
99
+ detail="Too many password reset requests. Please try again later."
100
+ )
101
+
102
+ # Create reset token
103
+ token = create_reset_token(session, user.id)
104
+
105
+ # Send reset email
106
+ email_sent = send_password_reset_email(user.email, token)
107
+
108
+ if not email_sent:
109
+ # Log error but don't expose to user
110
+ print(f"Failed to send password reset email to {user.email}")
111
+
112
+ # Always return generic message
113
+ return ForgotPasswordResponse(message=generic_message)
114
+
115
+
116
+ @router.get("/reset-password/{token}", response_model=TokenValidationResponse)
117
+ async def verify_reset_token(
118
+ token: str,
119
+ session: Session = Depends(get_session)
120
+ ) -> TokenValidationResponse:
121
+ """
122
+ Verify if a password reset token is valid.
123
+
124
+ Checks:
125
+ - Token exists
126
+ - Token has not expired (15 minutes)
127
+ - Token has not been used
128
+
129
+ Args:
130
+ token: Password reset token to verify
131
+ session: Database session
132
+
133
+ Returns:
134
+ TokenValidationResponse with validity status and user email
135
+
136
+ Example:
137
+ GET /api/auth/reset-password/abc123def456
138
+ """
139
+ # Validate token
140
+ token_record = validate_reset_token(session, token)
141
+
142
+ if not token_record:
143
+ return TokenValidationResponse(
144
+ valid=False,
145
+ error="Invalid or expired reset token"
146
+ )
147
+
148
+ # Get user email
149
+ user = session.get(User, token_record.user_id)
150
+
151
+ if not user:
152
+ return TokenValidationResponse(
153
+ valid=False,
154
+ error="User not found"
155
+ )
156
+
157
+ return TokenValidationResponse(
158
+ valid=True,
159
+ email=user.email
160
+ )
161
+
162
+
163
+ @router.post("/reset-password", response_model=ResetPasswordResponse)
164
+ async def reset_password(
165
+ request: ResetPasswordRequest,
166
+ session: Session = Depends(get_session)
167
+ ) -> ResetPasswordResponse:
168
+ """
169
+ Reset user password with a valid token.
170
+
171
+ Security features:
172
+ - Token validation (expiry, usage)
173
+ - Password strength validation
174
+ - One-time use tokens
175
+ - Automatic token invalidation
176
+
177
+ Args:
178
+ request: Reset password request with token and new password
179
+ session: Database session
180
+
181
+ Returns:
182
+ Success message
183
+
184
+ Raises:
185
+ HTTPException 400: If token is invalid or password is weak
186
+ HTTPException 422: If validation fails
187
+ """
188
+ # Validate token
189
+ token_record = validate_reset_token(session, request.token)
190
+
191
+ if not token_record:
192
+ raise HTTPException(
193
+ status_code=400,
194
+ detail="Invalid or expired reset token"
195
+ )
196
+
197
+ # Validate password strength
198
+ password_validation = validate_password_strength(request.new_password)
199
+
200
+ if not password_validation["valid"]:
201
+ raise HTTPException(
202
+ status_code=400,
203
+ detail={
204
+ "message": "Password does not meet strength requirements",
205
+ "errors": password_validation["errors"]
206
+ }
207
+ )
208
+
209
+ # Get user
210
+ user = session.get(User, token_record.user_id)
211
+
212
+ if not user:
213
+ raise HTTPException(
214
+ status_code=400,
215
+ detail="User not found"
216
+ )
217
+
218
+ # Hash new password
219
+ hashed_password = hash_password(request.new_password)
220
+
221
+ # Update user password
222
+ user.hashed_password = hashed_password
223
+ session.add(user)
224
+
225
+ # Invalidate token (mark as used)
226
+ invalidate_token(session, request.token)
227
+
228
+ # Commit changes
229
+ session.commit()
230
+
231
+ return ResetPasswordResponse(
232
+ message="Password successfully reset. You can now sign in with your new password."
233
+ )
src/api/subtasks.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Subtasks API endpoints for CRUD operations on subtasks.
3
+
4
+ This module provides:
5
+ - GET /api/tasks/{task_id}/subtasks - List all subtasks for a task
6
+ - POST /api/tasks/{task_id}/subtasks - Create new subtask
7
+ - PUT /api/subtasks/{subtask_id} - Update existing subtask
8
+ - DELETE /api/subtasks/{subtask_id} - Delete subtask
9
+
10
+ All endpoints require JWT authentication and enforce user isolation.
11
+ """
12
+
13
+ from fastapi import APIRouter, HTTPException, Depends, status
14
+ from sqlmodel import Session
15
+ from pydantic import BaseModel, Field
16
+ from typing import Optional, List
17
+
18
+ from ..models.subtask import Subtask
19
+ from ..services import subtasks as subtask_service
20
+ from ..middleware.jwt_auth import get_current_user_id
21
+ from ..database import get_session
22
+
23
+ router = APIRouter()
24
+
25
+
26
+ # Request/Response Models
27
+ class CreateSubtaskRequest(BaseModel):
28
+ """Request model for creating a subtask."""
29
+ title: str = Field(..., min_length=1, max_length=500, description="Subtask title")
30
+ order: Optional[int] = Field(0, description="Order position")
31
+
32
+
33
+ class UpdateSubtaskRequest(BaseModel):
34
+ """Request model for updating a subtask."""
35
+ title: Optional[str] = Field(None, min_length=1, max_length=500, description="Subtask title")
36
+ completed: Optional[bool] = Field(None, description="Subtask completion status")
37
+ order: Optional[int] = Field(None, description="Order position")
38
+
39
+
40
+ class SubtaskResponse(BaseModel):
41
+ """Subtask data response model."""
42
+ id: int
43
+ task_id: int
44
+ title: str
45
+ completed: bool
46
+ order: int
47
+ created_at: str
48
+ updated_at: str
49
+
50
+
51
+ class SubtaskListResponse(BaseModel):
52
+ """Response model for subtask list."""
53
+ subtasks: List[SubtaskResponse]
54
+
55
+
56
+ @router.get("/tasks/{task_id}/subtasks", response_model=SubtaskListResponse)
57
+ async def list_subtasks(
58
+ task_id: int,
59
+ user_id: int = Depends(get_current_user_id),
60
+ session: Session = Depends(get_session)
61
+ ) -> SubtaskListResponse:
62
+ """
63
+ Get all subtasks for a task.
64
+
65
+ Args:
66
+ task_id: Task ID
67
+ user_id: Current user ID from JWT token
68
+ session: Database session
69
+
70
+ Returns:
71
+ SubtaskListResponse with array of subtasks
72
+
73
+ Raises:
74
+ HTTPException 401: If JWT token is invalid
75
+ HTTPException 404: If task not found or doesn't belong to user
76
+ """
77
+ # Get subtasks
78
+ subtasks = subtask_service.get_task_subtasks(session, task_id, user_id)
79
+
80
+ # Convert to response format
81
+ subtask_responses = [
82
+ SubtaskResponse(
83
+ id=subtask.id,
84
+ task_id=subtask.task_id,
85
+ title=subtask.title,
86
+ completed=subtask.completed,
87
+ order=subtask.order,
88
+ created_at=subtask.created_at.isoformat(),
89
+ updated_at=subtask.updated_at.isoformat()
90
+ )
91
+ for subtask in subtasks
92
+ ]
93
+
94
+ return SubtaskListResponse(subtasks=subtask_responses)
95
+
96
+
97
+ @router.post("/tasks/{task_id}/subtasks", response_model=SubtaskResponse, status_code=status.HTTP_201_CREATED)
98
+ async def create_subtask(
99
+ task_id: int,
100
+ request: CreateSubtaskRequest,
101
+ user_id: int = Depends(get_current_user_id),
102
+ session: Session = Depends(get_session)
103
+ ) -> SubtaskResponse:
104
+ """
105
+ Create a new subtask for a task.
106
+
107
+ Args:
108
+ task_id: Task ID
109
+ request: Subtask creation request
110
+ user_id: Current user ID from JWT token
111
+ session: Database session
112
+
113
+ Returns:
114
+ SubtaskResponse with created subtask data
115
+
116
+ Raises:
117
+ HTTPException 401: If JWT token is invalid
118
+ HTTPException 404: If task not found or doesn't belong to user
119
+ """
120
+ # Create subtask
121
+ subtask = subtask_service.create_subtask(
122
+ session=session,
123
+ task_id=task_id,
124
+ user_id=user_id,
125
+ title=request.title,
126
+ order=request.order or 0
127
+ )
128
+
129
+ if not subtask:
130
+ raise HTTPException(
131
+ status_code=status.HTTP_404_NOT_FOUND,
132
+ detail="Task not found"
133
+ )
134
+
135
+ # Return response
136
+ return SubtaskResponse(
137
+ id=subtask.id,
138
+ task_id=subtask.task_id,
139
+ title=subtask.title,
140
+ completed=subtask.completed,
141
+ order=subtask.order,
142
+ created_at=subtask.created_at.isoformat(),
143
+ updated_at=subtask.updated_at.isoformat()
144
+ )
145
+
146
+
147
+ @router.put("/subtasks/{subtask_id}", response_model=SubtaskResponse)
148
+ async def update_subtask(
149
+ subtask_id: int,
150
+ request: UpdateSubtaskRequest,
151
+ user_id: int = Depends(get_current_user_id),
152
+ session: Session = Depends(get_session)
153
+ ) -> SubtaskResponse:
154
+ """
155
+ Update an existing subtask.
156
+
157
+ Args:
158
+ subtask_id: ID of the subtask to update
159
+ request: Subtask update request
160
+ user_id: Current user ID from JWT token
161
+ session: Database session
162
+
163
+ Returns:
164
+ SubtaskResponse with updated subtask data
165
+
166
+ Raises:
167
+ HTTPException 401: If JWT token is invalid
168
+ HTTPException 404: If subtask not found or doesn't belong to user
169
+ """
170
+ # Update subtask
171
+ subtask = subtask_service.update_subtask(
172
+ session=session,
173
+ subtask_id=subtask_id,
174
+ user_id=user_id,
175
+ title=request.title,
176
+ completed=request.completed,
177
+ order=request.order
178
+ )
179
+
180
+ if not subtask:
181
+ raise HTTPException(
182
+ status_code=status.HTTP_404_NOT_FOUND,
183
+ detail="Subtask not found"
184
+ )
185
+
186
+ # Return response
187
+ return SubtaskResponse(
188
+ id=subtask.id,
189
+ task_id=subtask.task_id,
190
+ title=subtask.title,
191
+ completed=subtask.completed,
192
+ order=subtask.order,
193
+ created_at=subtask.created_at.isoformat(),
194
+ updated_at=subtask.updated_at.isoformat()
195
+ )
196
+
197
+
198
+ @router.delete("/subtasks/{subtask_id}", status_code=status.HTTP_204_NO_CONTENT)
199
+ async def delete_subtask(
200
+ subtask_id: int,
201
+ user_id: int = Depends(get_current_user_id),
202
+ session: Session = Depends(get_session)
203
+ ) -> None:
204
+ """
205
+ Delete a subtask.
206
+
207
+ Args:
208
+ subtask_id: ID of the subtask to delete
209
+ user_id: Current user ID from JWT token
210
+ session: Database session
211
+
212
+ Returns:
213
+ None (204 No Content)
214
+
215
+ Raises:
216
+ HTTPException 401: If JWT token is invalid
217
+ HTTPException 404: If subtask not found or doesn't belong to user
218
+ """
219
+ # Delete subtask
220
+ success = subtask_service.delete_subtask(
221
+ session=session,
222
+ subtask_id=subtask_id,
223
+ user_id=user_id
224
+ )
225
+
226
+ if not success:
227
+ raise HTTPException(
228
+ status_code=status.HTTP_404_NOT_FOUND,
229
+ detail="Subtask not found"
230
+ )
src/api/tasks.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tasks API endpoints for CRUD operations on tasks.
3
+
4
+ This module provides:
5
+ - GET /api/tasks - List all user tasks
6
+ - POST /api/tasks - Create new task
7
+ - PUT /api/tasks/{id} - Update existing task
8
+ - DELETE /api/tasks/{id} - Delete task
9
+
10
+ All endpoints require JWT authentication and enforce user isolation.
11
+ """
12
+
13
+ from fastapi import APIRouter, HTTPException, Depends, status
14
+ from sqlmodel import Session
15
+ from pydantic import BaseModel, Field
16
+ from typing import Optional, List
17
+
18
+ from ..models.task import Task
19
+ from ..services import tasks as task_service
20
+ from ..middleware.jwt_auth import get_current_user_id
21
+ from ..database import get_session
22
+
23
+ router = APIRouter()
24
+
25
+
26
+ # Request/Response Models
27
+ class CreateTaskRequest(BaseModel):
28
+ """Request model for creating a task."""
29
+ title: str = Field(..., min_length=1, max_length=500, description="Task title")
30
+ description: Optional[str] = Field(None, description="Optional task description")
31
+ category: Optional[str] = Field(None, max_length=50, description="Task category/tag")
32
+ due_date: Optional[str] = Field(None, description="Due date in ISO format")
33
+ priority: Optional[str] = Field("medium", description="Task priority: low, medium, high")
34
+ is_recurring: Optional[bool] = Field(False, description="Whether task is recurring")
35
+ recurrence_type: Optional[str] = Field(None, description="Recurrence type: daily, weekly, monthly, yearly")
36
+ recurrence_interval: Optional[int] = Field(1, description="Recurrence interval (e.g., every 2 days)")
37
+ recurrence_end_date: Optional[str] = Field(None, description="Recurrence end date in ISO format")
38
+
39
+
40
+ class UpdateTaskRequest(BaseModel):
41
+ """Request model for updating a task."""
42
+ title: Optional[str] = Field(None, min_length=1, max_length=500, description="Task title")
43
+ description: Optional[str] = Field(None, description="Task description")
44
+ completed: Optional[bool] = Field(None, description="Task completion status")
45
+ category: Optional[str] = Field(None, max_length=50, description="Task category/tag")
46
+ due_date: Optional[str] = Field(None, description="Due date in ISO format")
47
+ priority: Optional[str] = Field(None, description="Task priority: low, medium, high")
48
+ is_recurring: Optional[bool] = Field(None, description="Whether task is recurring")
49
+ recurrence_type: Optional[str] = Field(None, description="Recurrence type: daily, weekly, monthly, yearly")
50
+ recurrence_interval: Optional[int] = Field(None, description="Recurrence interval")
51
+ recurrence_end_date: Optional[str] = Field(None, description="Recurrence end date in ISO format")
52
+
53
+
54
+ class TaskResponse(BaseModel):
55
+ """Task data response model."""
56
+ id: int
57
+ user_id: int
58
+ title: str
59
+ description: Optional[str]
60
+ completed: bool
61
+ category: Optional[str]
62
+ due_date: Optional[str]
63
+ priority: Optional[str]
64
+ is_recurring: bool
65
+ recurrence_type: Optional[str]
66
+ recurrence_interval: Optional[int]
67
+ recurrence_end_date: Optional[str]
68
+ parent_task_id: Optional[int]
69
+ created_at: str
70
+ updated_at: str
71
+
72
+
73
+ class TaskListResponse(BaseModel):
74
+ """Response model for task list."""
75
+ tasks: List[TaskResponse]
76
+
77
+
78
+ @router.get("", response_model=TaskListResponse)
79
+ async def list_tasks(
80
+ user_id: int = Depends(get_current_user_id),
81
+ session: Session = Depends(get_session)
82
+ ) -> TaskListResponse:
83
+ """
84
+ Get all tasks for the authenticated user.
85
+
86
+ Args:
87
+ user_id: Current user ID from JWT token
88
+ session: Database session
89
+
90
+ Returns:
91
+ TaskListResponse with array of user's tasks
92
+
93
+ Raises:
94
+ HTTPException 401: If JWT token is invalid
95
+ """
96
+ # Get user tasks
97
+ tasks = task_service.get_user_tasks(session, user_id)
98
+
99
+ # Convert to response format
100
+ task_responses = [
101
+ TaskResponse(
102
+ id=task.id,
103
+ user_id=task.user_id,
104
+ title=task.title,
105
+ description=task.description,
106
+ completed=task.completed,
107
+ category=task.category,
108
+ due_date=task.due_date.isoformat() if task.due_date else None,
109
+ priority=task.priority,
110
+ is_recurring=task.is_recurring,
111
+ recurrence_type=task.recurrence_type,
112
+ recurrence_interval=task.recurrence_interval,
113
+ recurrence_end_date=task.recurrence_end_date.isoformat() if task.recurrence_end_date else None,
114
+ parent_task_id=task.parent_task_id,
115
+ created_at=task.created_at.isoformat(),
116
+ updated_at=task.updated_at.isoformat()
117
+ )
118
+ for task in tasks
119
+ ]
120
+
121
+ return TaskListResponse(tasks=task_responses)
122
+
123
+
124
+ @router.post("", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
125
+ async def create_task(
126
+ request: CreateTaskRequest,
127
+ user_id: int = Depends(get_current_user_id),
128
+ session: Session = Depends(get_session)
129
+ ) -> TaskResponse:
130
+ """
131
+ Create a new task for the authenticated user.
132
+
133
+ Args:
134
+ request: Task creation request with title and optional description
135
+ user_id: Current user ID from JWT token
136
+ session: Database session
137
+
138
+ Returns:
139
+ TaskResponse with created task data
140
+
141
+ Raises:
142
+ HTTPException 401: If JWT token is invalid
143
+ HTTPException 422: If validation fails
144
+ """
145
+ # Create task
146
+ task = task_service.create_task(
147
+ session=session,
148
+ user_id=user_id,
149
+ title=request.title,
150
+ description=request.description,
151
+ category=request.category,
152
+ due_date=request.due_date,
153
+ priority=request.priority,
154
+ is_recurring=request.is_recurring or False,
155
+ recurrence_type=request.recurrence_type,
156
+ recurrence_interval=request.recurrence_interval or 1,
157
+ recurrence_end_date=request.recurrence_end_date
158
+ )
159
+
160
+ # Return response
161
+ return TaskResponse(
162
+ id=task.id,
163
+ user_id=task.user_id,
164
+ title=task.title,
165
+ description=task.description,
166
+ completed=task.completed,
167
+ category=task.category,
168
+ due_date=task.due_date.isoformat() if task.due_date else None,
169
+ priority=task.priority,
170
+ is_recurring=task.is_recurring,
171
+ recurrence_type=task.recurrence_type,
172
+ recurrence_interval=task.recurrence_interval,
173
+ recurrence_end_date=task.recurrence_end_date.isoformat() if task.recurrence_end_date else None,
174
+ parent_task_id=task.parent_task_id,
175
+ created_at=task.created_at.isoformat(),
176
+ updated_at=task.updated_at.isoformat()
177
+ )
178
+
179
+
180
+ @router.put("/{task_id}", response_model=TaskResponse)
181
+ async def update_task(
182
+ task_id: int,
183
+ request: UpdateTaskRequest,
184
+ user_id: int = Depends(get_current_user_id),
185
+ session: Session = Depends(get_session)
186
+ ) -> TaskResponse:
187
+ """
188
+ Update an existing task.
189
+
190
+ Args:
191
+ task_id: ID of the task to update
192
+ request: Task update request with optional fields
193
+ user_id: Current user ID from JWT token
194
+ session: Database session
195
+
196
+ Returns:
197
+ TaskResponse with updated task data
198
+
199
+ Raises:
200
+ HTTPException 401: If JWT token is invalid
201
+ HTTPException 404: If task not found or doesn't belong to user
202
+ """
203
+ # Update task
204
+ task = task_service.update_task(
205
+ session=session,
206
+ task_id=task_id,
207
+ user_id=user_id,
208
+ title=request.title,
209
+ description=request.description,
210
+ completed=request.completed,
211
+ category=request.category,
212
+ due_date=request.due_date,
213
+ priority=request.priority,
214
+ is_recurring=request.is_recurring,
215
+ recurrence_type=request.recurrence_type,
216
+ recurrence_interval=request.recurrence_interval,
217
+ recurrence_end_date=request.recurrence_end_date
218
+ )
219
+
220
+ if not task:
221
+ raise HTTPException(
222
+ status_code=status.HTTP_404_NOT_FOUND,
223
+ detail="Task not found"
224
+ )
225
+
226
+ # Return response
227
+ return TaskResponse(
228
+ id=task.id,
229
+ user_id=task.user_id,
230
+ title=task.title,
231
+ description=task.description,
232
+ completed=task.completed,
233
+ category=task.category,
234
+ due_date=task.due_date.isoformat() if task.due_date else None,
235
+ priority=task.priority,
236
+ is_recurring=task.is_recurring,
237
+ recurrence_type=task.recurrence_type,
238
+ recurrence_interval=task.recurrence_interval,
239
+ recurrence_end_date=task.recurrence_end_date.isoformat() if task.recurrence_end_date else None,
240
+ parent_task_id=task.parent_task_id,
241
+ created_at=task.created_at.isoformat(),
242
+ updated_at=task.updated_at.isoformat()
243
+ )
244
+
245
+
246
+ @router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
247
+ async def delete_task(
248
+ task_id: int,
249
+ user_id: int = Depends(get_current_user_id),
250
+ session: Session = Depends(get_session)
251
+ ) -> None:
252
+ """
253
+ Delete a task.
254
+
255
+ Args:
256
+ task_id: ID of the task to delete
257
+ user_id: Current user ID from JWT token
258
+ session: Database session
259
+
260
+ Returns:
261
+ None (204 No Content)
262
+
263
+ Raises:
264
+ HTTPException 401: If JWT token is invalid
265
+ HTTPException 404: If task not found or doesn't belong to user
266
+ """
267
+ # Delete task
268
+ success = task_service.delete_task(
269
+ session=session,
270
+ task_id=task_id,
271
+ user_id=user_id
272
+ )
273
+
274
+ if not success:
275
+ raise HTTPException(
276
+ status_code=status.HTTP_404_NOT_FOUND,
277
+ detail="Task not found"
278
+ )
src/database.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database configuration and session management.
3
+
4
+ This module provides:
5
+ - Database engine creation
6
+ - Session management
7
+ - Dependency injection for FastAPI routes
8
+ """
9
+
10
+ import os
11
+ from typing import Generator
12
+
13
+ from sqlmodel import Session, create_engine, SQLModel
14
+
15
+ # Get database URL from environment variable
16
+ # For Vercel serverless, use /tmp directory for SQLite
17
+ DATABASE_URL = os.getenv("DATABASE_URL")
18
+
19
+ if DATABASE_URL is None:
20
+ # Check if running on Vercel (serverless environment)
21
+ if os.getenv("VERCEL"):
22
+ # Use /tmp directory which is writable in Vercel serverless
23
+ DATABASE_URL = "sqlite:////tmp/todo.db"
24
+ else:
25
+ # Local development
26
+ DATABASE_URL = "sqlite:///./todo.db"
27
+
28
+ # Create database engine
29
+ # connect_args only needed for SQLite
30
+ connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
31
+
32
+ engine = create_engine(
33
+ DATABASE_URL,
34
+ echo=False, # Disable SQL query logging for serverless
35
+ connect_args=connect_args,
36
+ pool_pre_ping=True, # Verify connections before using
37
+ )
38
+
39
+
40
+ def create_db_and_tables():
41
+ """Create all database tables."""
42
+ SQLModel.metadata.create_all(engine)
43
+
44
+
45
+ def get_session() -> Generator[Session, None, None]:
46
+ """
47
+ Dependency function to provide database session to FastAPI routes.
48
+
49
+ Yields:
50
+ Session: SQLModel database session
51
+
52
+ Example:
53
+ @app.get("/items")
54
+ def get_items(session: Session = Depends(get_session)):
55
+ items = session.exec(select(Item)).all()
56
+ return items
57
+ """
58
+ with Session(engine) as session:
59
+ yield session
src/main.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from dotenv import load_dotenv
4
+ import os
5
+
6
+ # Load environment variables
7
+ load_dotenv()
8
+
9
+ # Create FastAPI application
10
+ app = FastAPI(
11
+ title="Todo Application API",
12
+ description="Backend API for Todo application with JWT authentication",
13
+ version="1.0.0",
14
+ )
15
+
16
+ # CORS Configuration
17
+ CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:3005").split(",")
18
+
19
+ # Configure CORS middleware
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=CORS_ORIGINS,
23
+ allow_credentials=True,
24
+ allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
25
+ allow_headers=["*"],
26
+ expose_headers=["*"],
27
+ max_age=3600,
28
+ )
29
+
30
+ # Initialize database tables on startup
31
+ from src.database import create_db_and_tables
32
+
33
+ @app.on_event("startup")
34
+ def on_startup():
35
+ """Initialize database tables on application startup."""
36
+ try:
37
+ create_db_and_tables()
38
+ except Exception as e:
39
+ print(f"Warning: Could not initialize database tables: {e}")
40
+ # Continue anyway - tables might already exist
41
+
42
+ # Health check endpoint
43
+ @app.get("/health")
44
+ async def health_check():
45
+ """Health check endpoint to verify API is running."""
46
+ return {"status": "healthy"}
47
+
48
+ # Root endpoint
49
+ @app.get("/")
50
+ async def root():
51
+ """Root endpoint with API information."""
52
+ return {
53
+ "message": "Todo Application API",
54
+ "version": "1.0.0",
55
+ "docs": "/docs",
56
+ "health": "/health"
57
+ }
58
+
59
+ # Router registration
60
+ from src.api import auth, tasks, subtasks, password_reset
61
+ # AI router temporarily disabled due to Vercel size constraints
62
+ # from src.api import ai
63
+
64
+ app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
65
+ app.include_router(password_reset.router, prefix="/api/auth", tags=["Password Reset"])
66
+ app.include_router(tasks.router, prefix="/api/tasks", tags=["Tasks"])
67
+ app.include_router(subtasks.router, prefix="/api", tags=["Subtasks"])
68
+ # app.include_router(ai.router, prefix="/api/ai", tags=["AI Features"])
src/main_minimal.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ import os
3
+
4
+ app = FastAPI(title="Todo API - Minimal Test")
5
+
6
+ @app.get("/")
7
+ def root():
8
+ return {
9
+ "status": "ok",
10
+ "message": "Railway FastAPI is working!",
11
+ "port": os.getenv("PORT", "not set"),
12
+ "database": "connected" if os.getenv("DATABASE_URL") else "not configured"
13
+ }
14
+
15
+ @app.get("/health")
16
+ def health():
17
+ return {"status": "healthy", "service": "railway-test"}
18
+
19
+ @app.get("/api/health")
20
+ def api_health():
21
+ return {"status": "healthy", "api": "working"}
src/middleware/__pycache__/jwt_auth.cpython-314.pyc ADDED
Binary file (3.55 kB). View file
 
src/middleware/jwt_auth.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, HTTPException, status, Depends
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from jose import JWTError, jwt
4
+ from typing import Optional
5
+ import os
6
+
7
+ # JWT Configuration
8
+ SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-here")
9
+ ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
10
+
11
+ security = HTTPBearer()
12
+
13
+
14
+ async def verify_jwt_token(credentials: HTTPAuthorizationCredentials) -> dict:
15
+ """
16
+ Verify JWT token and return payload.
17
+
18
+ Args:
19
+ credentials: HTTP Authorization credentials with Bearer token
20
+
21
+ Returns:
22
+ dict: JWT payload containing user_id and other claims
23
+
24
+ Raises:
25
+ HTTPException: If token is invalid or expired
26
+ """
27
+ try:
28
+ token = credentials.credentials
29
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
30
+ user_id: Optional[int] = payload.get("user_id")
31
+
32
+ if user_id is None:
33
+ raise HTTPException(
34
+ status_code=status.HTTP_401_UNAUTHORIZED,
35
+ detail="Invalid authentication credentials",
36
+ headers={"WWW-Authenticate": "Bearer"},
37
+ )
38
+
39
+ return payload
40
+
41
+ except JWTError:
42
+ raise HTTPException(
43
+ status_code=status.HTTP_401_UNAUTHORIZED,
44
+ detail="Could not validate credentials",
45
+ headers={"WWW-Authenticate": "Bearer"},
46
+ )
47
+
48
+
49
+ async def get_current_user_id(credentials: HTTPAuthorizationCredentials = Depends(security)) -> int:
50
+ """
51
+ Extract user_id from JWT token.
52
+
53
+ This function is used as a dependency in FastAPI routes to get the
54
+ authenticated user's ID from the JWT token.
55
+
56
+ Args:
57
+ credentials: HTTP Authorization credentials (injected by FastAPI)
58
+
59
+ Returns:
60
+ int: The authenticated user's ID
61
+
62
+ Raises:
63
+ HTTPException: If token is invalid or user_id is missing
64
+ """
65
+ payload = await verify_jwt_token(credentials)
66
+ return payload["user_id"]
67
+
68
+
69
+ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
70
+ """
71
+ Extract full user payload from JWT token.
72
+
73
+ This function is used as a dependency in FastAPI routes to get the
74
+ authenticated user's full information from the JWT token.
75
+
76
+ Args:
77
+ credentials: HTTP Authorization credentials (injected by FastAPI)
78
+
79
+ Returns:
80
+ dict: The JWT payload containing user information
81
+
82
+ Raises:
83
+ HTTPException: If token is invalid
84
+ """
85
+ payload = await verify_jwt_token(credentials)
86
+ return payload
src/models/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .user import User
2
+ from .task import Task
3
+ from .subtask import Subtask
4
+ from .password_reset import PasswordResetToken
5
+
6
+ __all__ = ["User", "Task", "Subtask", "PasswordResetToken"]
src/models/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (421 Bytes). View file
 
src/models/__pycache__/password_reset.cpython-314.pyc ADDED
Binary file (2.09 kB). View file
 
src/models/__pycache__/subtask.cpython-314.pyc ADDED
Binary file (2.11 kB). View file
 
src/models/__pycache__/task.cpython-314.pyc ADDED
Binary file (3.18 kB). View file
 
src/models/__pycache__/user.cpython-314.pyc ADDED
Binary file (1.95 kB). View file
 
src/models/password_reset.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+ class PasswordResetToken(SQLModel, table=True):
6
+ """Password reset token model for secure password recovery."""
7
+
8
+ __tablename__ = "password_reset_tokens"
9
+
10
+ id: Optional[int] = Field(default=None, primary_key=True)
11
+ user_id: int = Field(foreign_key="users.id", index=True)
12
+ token: str = Field(unique=True, index=True, max_length=255)
13
+ expires_at: datetime = Field(index=True)
14
+ used: bool = Field(default=False)
15
+ created_at: datetime = Field(default_factory=datetime.utcnow)
16
+
17
+ # Relationships
18
+ user: Optional["User"] = Relationship()
19
+
20
+ class Config:
21
+ json_schema_extra = {
22
+ "example": {
23
+ "id": 1,
24
+ "user_id": 1,
25
+ "token": "abc123def456...",
26
+ "expires_at": "2026-02-07T12:15:00Z",
27
+ "used": False,
28
+ "created_at": "2026-02-07T12:00:00Z"
29
+ }
30
+ }
src/models/subtask.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+ class Subtask(SQLModel, table=True):
6
+ """Subtask model representing a checklist item within a task."""
7
+
8
+ __tablename__ = "subtasks"
9
+
10
+ id: Optional[int] = Field(default=None, primary_key=True)
11
+ task_id: int = Field(foreign_key="tasks.id", index=True)
12
+ title: str = Field(max_length=500)
13
+ completed: bool = Field(default=False)
14
+ order: int = Field(default=0) # For ordering subtasks
15
+ created_at: datetime = Field(default_factory=datetime.utcnow)
16
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
17
+
18
+ # Relationships
19
+ task: "Task" = Relationship(back_populates="subtasks")
20
+
21
+ class Config:
22
+ json_schema_extra = {
23
+ "example": {
24
+ "id": 1,
25
+ "task_id": 42,
26
+ "title": "Review documentation",
27
+ "completed": False,
28
+ "order": 0,
29
+ "created_at": "2026-02-05T10:00:00Z",
30
+ "updated_at": "2026-02-05T10:00:00Z"
31
+ }
32
+ }
src/models/task.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from datetime import datetime
3
+ from typing import Optional, List, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .subtask import Subtask
7
+
8
+ class Task(SQLModel, table=True):
9
+ """Task model representing a work item belonging to a user."""
10
+
11
+ __tablename__ = "tasks"
12
+
13
+ id: Optional[int] = Field(default=None, primary_key=True)
14
+ user_id: int = Field(foreign_key="users.id", index=True)
15
+ title: str = Field(max_length=500)
16
+ description: Optional[str] = Field(default=None)
17
+ completed: bool = Field(default=False)
18
+ category: Optional[str] = Field(default=None, max_length=50)
19
+ due_date: Optional[datetime] = Field(default=None)
20
+ priority: Optional[str] = Field(default="medium", max_length=20) # low, medium, high
21
+
22
+ # Recurring task fields
23
+ is_recurring: bool = Field(default=False)
24
+ recurrence_type: Optional[str] = Field(default=None, max_length=20) # daily, weekly, monthly, yearly
25
+ recurrence_interval: Optional[int] = Field(default=1) # e.g., every 2 days, every 3 weeks
26
+ recurrence_end_date: Optional[datetime] = Field(default=None)
27
+ parent_task_id: Optional[int] = Field(default=None, foreign_key="tasks.id") # For recurring instances
28
+
29
+ created_at: datetime = Field(default_factory=datetime.utcnow)
30
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
31
+
32
+ # Relationships
33
+ user: "User" = Relationship(back_populates="tasks")
34
+ subtasks: List["Subtask"] = Relationship(back_populates="task")
35
+
36
+ class Config:
37
+ json_schema_extra = {
38
+ "example": {
39
+ "id": 1,
40
+ "user_id": 42,
41
+ "title": "Buy groceries",
42
+ "description": "Milk, eggs, bread",
43
+ "completed": False,
44
+ "category": "Personal",
45
+ "due_date": "2026-02-10T10:00:00Z",
46
+ "created_at": "2026-02-05T10:00:00Z",
47
+ "updated_at": "2026-02-05T10:00:00Z"
48
+ }
49
+ }
src/models/user.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from datetime import datetime
3
+ from typing import Optional, List
4
+
5
+ class User(SQLModel, table=True):
6
+ """User model representing an authenticated user of the application."""
7
+
8
+ __tablename__ = "users"
9
+
10
+ id: Optional[int] = Field(default=None, primary_key=True)
11
+ email: str = Field(unique=True, index=True, max_length=255)
12
+ hashed_password: str = Field(max_length=255)
13
+ created_at: datetime = Field(default_factory=datetime.utcnow)
14
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
15
+
16
+ # Relationships
17
+ tasks: List["Task"] = Relationship(back_populates="user")
18
+
19
+ class Config:
20
+ json_schema_extra = {
21
+ "example": {
22
+ "id": 1,
23
+ "email": "user@example.com",
24
+ "created_at": "2026-02-05T10:00:00Z",
25
+ "updated_at": "2026-02-05T10:00:00Z"
26
+ }
27
+ }
src/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Services module
src/services/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (211 Bytes). View file
 
src/services/__pycache__/auth.cpython-314.pyc ADDED
Binary file (4.72 kB). View file
 
src/services/__pycache__/email.cpython-314.pyc ADDED
Binary file (9.1 kB). View file