NirmaQureshi commited on
Commit
e650b33
·
1 Parent(s): aad8d38
Files changed (47) hide show
  1. CLAUDE.md +194 -0
  2. Dockerfile +19 -0
  3. alembic.ini +147 -0
  4. alembic/README +1 -0
  5. alembic/env.py +83 -0
  6. alembic/script.py.mako +28 -0
  7. alembic/versions/20251221_164149_initial_migration_for_users_and_tasks_tables.py +65 -0
  8. run_migrations.py +42 -0
  9. src/__init__.py +0 -0
  10. src/__pycache__/__init__.cpython-312.pyc +0 -0
  11. src/__pycache__/config.cpython-312.pyc +0 -0
  12. src/__pycache__/database.cpython-312.pyc +0 -0
  13. src/__pycache__/main.cpython-312.pyc +0 -0
  14. src/config.py +45 -0
  15. src/database.py +50 -0
  16. src/main.py +45 -0
  17. src/middleware/__init__.py +0 -0
  18. src/middleware/auth.py +98 -0
  19. src/models/__init__.py +0 -0
  20. src/models/__pycache__/__init__.cpython-312.pyc +0 -0
  21. src/models/__pycache__/task.cpython-312.pyc +0 -0
  22. src/models/__pycache__/user.cpython-312.pyc +0 -0
  23. src/models/task.py +33 -0
  24. src/models/user.py +31 -0
  25. src/routers/__init__.py +0 -0
  26. src/routers/__pycache__/__init__.cpython-312.pyc +0 -0
  27. src/routers/__pycache__/auth.cpython-312.pyc +0 -0
  28. src/routers/__pycache__/tasks.cpython-312.pyc +0 -0
  29. src/routers/auth.py +161 -0
  30. src/routers/tasks.py +142 -0
  31. src/schemas/__init__.py +0 -0
  32. src/schemas/__pycache__/__init__.cpython-312.pyc +0 -0
  33. src/schemas/__pycache__/auth.cpython-312.pyc +0 -0
  34. src/schemas/__pycache__/task.cpython-312.pyc +0 -0
  35. src/schemas/auth.py +57 -0
  36. src/schemas/task.py +41 -0
  37. src/task_management_backend.egg-info/PKG-INFO +20 -0
  38. src/task_management_backend.egg-info/SOURCES.txt +24 -0
  39. src/task_management_backend.egg-info/dependency_links.txt +1 -0
  40. src/task_management_backend.egg-info/requires.txt +14 -0
  41. src/task_management_backend.egg-info/top_level.txt +9 -0
  42. src/utils/__init__.py +0 -0
  43. src/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  44. src/utils/__pycache__/deps.cpython-312.pyc +0 -0
  45. src/utils/__pycache__/security.cpython-312.pyc +0 -0
  46. src/utils/deps.py +63 -0
  47. src/utils/security.py +97 -0
CLAUDE.md ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Claude Code Instructions: Backend - Task Management API
2
+
3
+ ## Project Context
4
+
5
+ This is the backend component of "The Evolution of Todo" - Phase II, a full-stack web application for managing personal tasks with user authentication. Built with FastAPI, SQLModel, Python 3.13+, and PostgreSQL, following RESTful API design principles with automatic OpenAPI documentation.
6
+
7
+ ## Project Constitution (MANDATORY - Read Carefully)
8
+
9
+ You MUST follow the constitutional requirements defined in the root `.specify/memory/constitution.md`:
10
+
11
+ ### Core Principles (MANDATORY)
12
+ 1. **Spec-Driven Development Only**: All features must be implemented from approved specifications in `specs/`
13
+ 2. **AI as Primary Developer**: Humans write specs, Claude Code implements all code
14
+ 3. **Mandatory Traceability**: Complete audit trail (ADR → Spec → Plan → Tasks → Implementation → Tests)
15
+ 4. **Test-First Mandate**: Minimum 80% coverage target (pytest backend, Jest frontend, Playwright E2E)
16
+ 5. **Evolutionary Consistency**: Phase II extends Phase I without breaking changes
17
+
18
+ ### Phase II Backend Technology Stack Requirements (MANDATORY)
19
+
20
+ #### FastAPI Framework + SQLModel ORM (NOT raw SQLAlchemy)
21
+ - Python 3.13+ with UV package manager
22
+ - SQLModel ORM for type-safe database operations (NOT raw SQLAlchemy)
23
+ - Pydantic v2 for request/response validation and serialization
24
+ - Automatic OpenAPI/Swagger documentation generation
25
+ - Async/await patterns for non-blocking I/O
26
+ - Dependency injection for testability
27
+
28
+ #### Security Requirements (NON-NEGOTIABLE)
29
+ - User Data Isolation: ALL database queries MUST filter by user_id
30
+ - Authorization: verify user_id in URL matches authenticated user
31
+ - Return 404 (NOT 403) for unauthorized access attempts
32
+ - SQL injection prevention via SQLModel parameterized queries
33
+ - JWT validation on all protected endpoints
34
+ - Password hashing with bcrypt (NEVER plaintext passwords)
35
+
36
+ #### API Design Standards
37
+ - RESTful resource naming (`/api/tasks`, not `/api/getTasks`)
38
+ - Standard HTTP methods (GET, POST, PUT, PATCH, DELETE)
39
+ - Consistent error response format (JSON with status, message, details)
40
+ - Proper HTTP status codes (200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Internal Server Error)
41
+ - Type-safe request/response schemas using Pydantic
42
+
43
+ ## Current Feature Context
44
+
45
+ **Feature**: Task CRUD Operations with Authentication (001-task-crud-auth)
46
+ **Spec**: `../specs/001-task-crud-auth/spec.md`
47
+ **Plan**: `../specs/001-task-crud-auth/plan.md`
48
+ **Tasks**: `../specs/001-task-crud-auth/tasks.md`
49
+
50
+ ### User Stories (Priority Order):
51
+ 1. **US1 (P1)**: User Registration - New users can create accounts
52
+ 2. **US2 (P1)**: User Login - Registered users can authenticate
53
+ 3. **US3 (P2)**: View All My Tasks - Display user's tasks with data isolation
54
+ 4. **US4 (P2)**: Create New Task - Add tasks with title/description
55
+ 5. **US5 (P3)**: Mark Task Complete/Incomplete - Toggle completion status
56
+ 6. **US6 (P3)**: Update Task - Edit task title/description
57
+ 7. **US7 (P4)**: Delete Task - Remove tasks permanently
58
+
59
+ ### Key Entities
60
+ - **User**: id (UUID), email (unique, indexed), password_hash (bcrypt), account timestamps
61
+ - **Task**: id (int), user_id (FK, indexed), title (1-200 chars), description (0-1000 chars), completed (bool, indexed), task timestamps
62
+
63
+ ## Implementation Guidelines
64
+
65
+ ### Database Layer (SQLModel)
66
+ - Use SQLModel models with proper relationships and constraints
67
+ - Add indexes on critical fields (user_id for performance, completed for filtering)
68
+ - Implement proper foreign key relationships
69
+ - Use automatic timestamp fields (created_at, updated_at)
70
+ - Follow SQLModel best practices for type safety
71
+
72
+ ### Authentication Layer
73
+ - Implement JWT-based authentication with 7-day expiration
74
+ - Store secrets in environment variables (BETTER_AUTH_SECRET)
75
+ - Validate JWT tokens on all protected endpoints
76
+ - Return httpOnly cookies for secure token storage
77
+ - Hash passwords with bcrypt before storing
78
+
79
+ ### Authorization Layer (CRITICAL - Security)
80
+ - ALL database queries must filter by user_id (100% data isolation)
81
+ - Verify user_id in URL paths matches authenticated user (return 404 if mismatch)
82
+ - Use dependency injection for authentication (get_current_user)
83
+ - Return 404 (not 403) for unauthorized access to prevent information leakage
84
+ - Implement proper role-based access if needed (though user isolation is primary)
85
+
86
+ ### API Design
87
+ - Use FastAPI routers for endpoint organization
88
+ - Implement proper request/response validation with Pydantic schemas
89
+ - Follow RESTful conventions for resource naming and HTTP methods
90
+ - Include comprehensive API documentation via FastAPI auto-generation
91
+ - Handle errors consistently with proper HTTP status codes
92
+
93
+ ### Error Handling
94
+ - Use HTTPException for API errors with appropriate status codes
95
+ - Implement custom exception handlers for consistent error responses
96
+ - Log errors for debugging while protecting sensitive information from users
97
+ - Provide user-friendly error messages without exposing system details
98
+
99
+ ## Quality Standards
100
+
101
+ ### Code Quality
102
+ - Clean, readable, well-documented code
103
+ - Follow Python PEP 8 standards
104
+ - Use type hints for all public interfaces
105
+ - Implement separation of concerns (models, schemas, routers, middleware, utils)
106
+ - No circular dependencies between modules
107
+
108
+ ### Security
109
+ - Parameterized queries via SQLModel to prevent SQL injection
110
+ - Input validation and sanitization to prevent XSS
111
+ - Proper authentication and authorization on all endpoints
112
+ - Secure password handling with bcrypt
113
+ - Environment variable configuration for secrets
114
+
115
+ ### Performance
116
+ - Proper indexing on database queries (especially user_id)
117
+ - Efficient database queries with proper joins when needed
118
+ - Async/await patterns for non-blocking operations
119
+ - Connection pooling for database operations
120
+
121
+ ### Testing Requirements
122
+ - Unit tests for models and utility functions
123
+ - API integration tests for all endpoints
124
+ - Authentication and authorization tests (especially user isolation)
125
+ - Database transaction tests
126
+ - Error handling tests
127
+
128
+ ### Documentation
129
+ - Type hints for all public interfaces
130
+ - Docstrings for complex functions
131
+ - API documentation via FastAPI auto-generation
132
+ - Inline comments for complex business logic only
133
+
134
+ ## File Structure
135
+
136
+ ```
137
+ backend/
138
+ ├── src/
139
+ │ ├── main.py # FastAPI app entry point and configuration
140
+ │ ├── config.py # Configuration management (env vars, settings)
141
+ │ ├── database.py # Database connection and session management
142
+ │ ├── models/ # SQLModel database models
143
+ │ │ ├── __init__.py
144
+ │ │ ├── user.py # User model with authentication fields
145
+ │ │ └── task.py # Task model with user relationship
146
+ │ ├── schemas/ # Pydantic request/response schemas
147
+ │ │ ├── __init__.py
148
+ │ │ ├── auth.py # Auth request/response schemas
149
+ │ │ └── task.py # Task request/response schemas
150
+ │ ├── routers/ # API route handlers
151
+ │ │ ├── __init__.py
152
+ │ │ ├── auth.py # Authentication endpoints (register, login)
153
+ │ │ └── tasks.py # Task CRUD endpoints
154
+ │ ├── middleware/ # FastAPI middleware
155
+ │ │ ├── __init__.py
156
+ │ │ └── auth.py # JWT validation middleware
157
+ │ └── utils/ # Utility functions
158
+ │ ├── __init__.py
159
+ │ ├── security.py # Password hashing, JWT utilities
160
+ │ └── deps.py # Dependency injection functions
161
+ ├── tests/ # Backend tests
162
+ │ ├── conftest.py # pytest fixtures (test database, client)
163
+ │ ├── unit/ # Unit tests
164
+ │ │ ├── test_models.py # Model validation tests
165
+ │ │ └── test_security.py # Security utility tests
166
+ │ └── integration/ # API integration tests
167
+ │ ├── test_auth.py # Authentication flow tests
168
+ │ ├── test_tasks.py # Task CRUD operation tests
169
+ │ └── test_user_isolation.py # User data isolation tests (CRITICAL)
170
+ ├── alembic/ # Database migrations
171
+ │ ├── versions/ # Migration files
172
+ │ ├── env.py # Alembic environment
173
+ │ └── alembic.ini # Alembic configuration
174
+ ├── .env # Backend environment variables
175
+ ├── pyproject.toml # UV project configuration
176
+ ├── uv.lock # UV lock file
177
+ └── CLAUDE.md # Backend-specific agent instructions
178
+ ```
179
+
180
+ ## Working with Claude Code
181
+
182
+ 1. **Follow Specifications**: Implement exactly what's in the spec, nothing more/less
183
+ 2. **Maintain Security**: Never compromise user data isolation requirements
184
+ 3. **Keep User Focus**: Remember this is for individual task management
185
+ 4. **Test Thoroughly**: Implement tests for all acceptance scenarios, especially security
186
+ 5. **Stay Organized**: Follow the defined project structure
187
+
188
+ ## Success Metrics
189
+
190
+ - API endpoints respond in under 200ms (p95)
191
+ - 100% user data isolation (no cross-user data access)
192
+ - 95%+ of API requests succeed
193
+ - 80%+ test coverage across all layers
194
+ - Proper error handling with appropriate HTTP status codes
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base Image
2
+ FROM python:3.11-slim
3
+
4
+ # Set work directory
5
+ WORKDIR /app
6
+
7
+ # Install Dependencies
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+
12
+ # Copy application code
13
+ COPY . .
14
+
15
+ # Expose the port Hugging Face expects
16
+ EXPOSE 7860
17
+
18
+ # Command to run FastAPI with uvicorn
19
+ CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "7860"]
alembic.ini ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts.
5
+ # this is typically a path given in POSIX (e.g. forward slashes)
6
+ # format, relative to the token %(here)s which refers to the location of this
7
+ # ini file
8
+ script_location = %(here)s/alembic
9
+
10
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
11
+ # Uncomment the line below if you want the files to be prepended with date and time
12
+ # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
13
+ # for all available tokens
14
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
15
+
16
+ # sys.path path, will be prepended to sys.path if present.
17
+ # defaults to the current working directory. for multiple paths, the path separator
18
+ # is defined by "path_separator" below.
19
+ prepend_sys_path = .
20
+
21
+
22
+ # timezone to use when rendering the date within the migration file
23
+ # as well as the filename.
24
+ # If specified, requires the tzdata library which can be installed by adding
25
+ # `alembic[tz]` to the pip requirements.
26
+ # string value is passed to ZoneInfo()
27
+ # leave blank for localtime
28
+ # timezone =
29
+
30
+ # max length of characters to apply to the "slug" field
31
+ # truncate_slug_length = 40
32
+
33
+ # set to 'true' to run the environment during
34
+ # the 'revision' command, regardless of autogenerate
35
+ # revision_environment = false
36
+
37
+ # set to 'true' to allow .pyc and .pyo files without
38
+ # a source .py file to be detected as revisions in the
39
+ # versions/ directory
40
+ # sourceless = false
41
+
42
+ # version location specification; This defaults
43
+ # to <script_location>/versions. When using multiple version
44
+ # directories, initial revisions must be specified with --version-path.
45
+ # The path separator used here should be the separator specified by "path_separator"
46
+ # below.
47
+ # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
48
+
49
+ # path_separator; This indicates what character is used to split lists of file
50
+ # paths, including version_locations and prepend_sys_path within configparser
51
+ # files such as alembic.ini.
52
+ # The default rendered in new alembic.ini files is "os", which uses os.pathsep
53
+ # to provide os-dependent path splitting.
54
+ #
55
+ # Note that in order to support legacy alembic.ini files, this default does NOT
56
+ # take place if path_separator is not present in alembic.ini. If this
57
+ # option is omitted entirely, fallback logic is as follows:
58
+ #
59
+ # 1. Parsing of the version_locations option falls back to using the legacy
60
+ # "version_path_separator" key, which if absent then falls back to the legacy
61
+ # behavior of splitting on spaces and/or commas.
62
+ # 2. Parsing of the prepend_sys_path option falls back to the legacy
63
+ # behavior of splitting on spaces, commas, or colons.
64
+ #
65
+ # Valid values for path_separator are:
66
+ #
67
+ # path_separator = :
68
+ # path_separator = ;
69
+ # path_separator = space
70
+ # path_separator = newline
71
+ #
72
+ # Use os.pathsep. Default configuration used for new projects.
73
+ path_separator = os
74
+
75
+ # set to 'true' to search source files recursively
76
+ # in each "version_locations" directory
77
+ # new in Alembic version 1.10
78
+ # recursive_version_locations = false
79
+
80
+ # the output encoding used when revision files
81
+ # are written from script.py.mako
82
+ # output_encoding = utf-8
83
+
84
+ # database URL. This is consumed by the user-maintained env.py script only.
85
+ # other means of configuring database URLs may be customized within the env.py
86
+ # file.
87
+ sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/todoapp
88
+
89
+
90
+ [post_write_hooks]
91
+ # post_write_hooks defines scripts or Python functions that are run
92
+ # on newly generated revision scripts. See the documentation for further
93
+ # detail and examples
94
+
95
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
96
+ # hooks = black
97
+ # black.type = console_scripts
98
+ # black.entrypoint = black
99
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
100
+
101
+ # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
102
+ # hooks = ruff
103
+ # ruff.type = module
104
+ # ruff.module = ruff
105
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
106
+
107
+ # Alternatively, use the exec runner to execute a binary found on your PATH
108
+ # hooks = ruff
109
+ # ruff.type = exec
110
+ # ruff.executable = ruff
111
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
112
+
113
+ # Logging configuration. This is also consumed by the user-maintained
114
+ # env.py script only.
115
+ [loggers]
116
+ keys = root,sqlalchemy,alembic
117
+
118
+ [handlers]
119
+ keys = console
120
+
121
+ [formatters]
122
+ keys = generic
123
+
124
+ [logger_root]
125
+ level = WARNING
126
+ handlers = console
127
+ qualname =
128
+
129
+ [logger_sqlalchemy]
130
+ level = WARNING
131
+ handlers =
132
+ qualname = sqlalchemy.engine
133
+
134
+ [logger_alembic]
135
+ level = INFO
136
+ handlers =
137
+ qualname = alembic
138
+
139
+ [handler_console]
140
+ class = StreamHandler
141
+ args = (sys.stderr,)
142
+ level = NOTSET
143
+ formatter = generic
144
+
145
+ [formatter_generic]
146
+ format = %(levelname)-5.5s [%(name)s] %(message)s
147
+ datefmt = %H:%M:%S
alembic/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
alembic/env.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+ import sys
3
+ import os
4
+
5
+ # Add the backend directory to the path so we can import our models
6
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
+
8
+ from sqlalchemy import engine_from_config
9
+ from sqlalchemy import pool
10
+
11
+ from alembic import context
12
+
13
+ # Import our models to make them available for Alembic
14
+ from src.models.user import User
15
+ from src.models.task import Task
16
+ from src.database import SQLModel
17
+
18
+ # this is the Alembic Config object, which provides
19
+ # access to the values within the .ini file in use.
20
+ config = context.config
21
+
22
+ # Interpret the config file for Python logging.
23
+ # This line sets up loggers basically.
24
+ if config.config_file_name is not None:
25
+ fileConfig(config.config_file_name)
26
+
27
+ # Set the target_metadata to our SQLModel's metadata
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
+ dialect_opts={"paramstyle": "named"},
53
+ )
54
+
55
+ context.run_migrations()
56
+
57
+
58
+ def run_migrations_online() -> None:
59
+ """Run migrations in 'online' mode.
60
+
61
+ In this scenario we need to create an Engine
62
+ and associate a connection with the context.
63
+
64
+ """
65
+ connectable = engine_from_config(
66
+ config.get_section(config.config_ini_section, {}),
67
+ prefix="sqlalchemy.",
68
+ poolclass=pool.NullPool,
69
+ )
70
+
71
+ with connectable.connect() as connection:
72
+ context.configure(
73
+ connection=connection, target_metadata=target_metadata
74
+ )
75
+
76
+ with context.begin_transaction():
77
+ context.run_migrations()
78
+
79
+
80
+ if context.is_offline_mode():
81
+ run_migrations_offline()
82
+ else:
83
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, Sequence[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
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}
alembic/versions/20251221_164149_initial_migration_for_users_and_tasks_tables.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Initial migration for users and tasks tables
2
+
3
+ Revision ID: 20251221_164149
4
+ Revises:
5
+ Create Date: 2025-12-21 16:41:49.000000
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+ from datetime import datetime
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+ import sqlmodel.sql.sqltypes
14
+ from sqlalchemy.dialects import postgresql
15
+
16
+
17
+ # revision identifiers, used by Alembic.
18
+ revision: str = '20251221_164149'
19
+ down_revision: Union[str, None] = None
20
+ branch_labels: Union[str, Sequence[str], None] = None
21
+ depends_on: Union[str, Sequence[str], None] = None
22
+
23
+
24
+ def upgrade() -> None:
25
+ # Create users table
26
+ op.create_table('user',
27
+ sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
28
+ sa.Column('email', sa.String(), nullable=False),
29
+ sa.Column('password_hash', sa.String(), nullable=False),
30
+ sa.Column('created_at', sa.DateTime(), nullable=False),
31
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
32
+ sa.PrimaryKeyConstraint('id'),
33
+ sa.UniqueConstraint('email')
34
+ )
35
+
36
+ # Create index for email field
37
+ op.create_index(op.f('ix_user_email'), 'user', ['email'])
38
+
39
+ # Create tasks table
40
+ op.create_table('task',
41
+ sa.Column('id', sa.Integer(), nullable=False),
42
+ sa.Column('title', sa.String(), nullable=False),
43
+ sa.Column('description', sa.String(), nullable=True),
44
+ sa.Column('completed', sa.Boolean(), nullable=False),
45
+ sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
46
+ sa.Column('created_at', sa.DateTime(), nullable=False),
47
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
48
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
49
+ sa.PrimaryKeyConstraint('id')
50
+ )
51
+
52
+ # Create indexes for user_id and completed fields
53
+ op.create_index(op.f('ix_task_user_id'), 'task', ['user_id'])
54
+ op.create_index(op.f('ix_task_completed'), 'task', ['completed'])
55
+
56
+
57
+ def downgrade() -> None:
58
+ # Drop indexes
59
+ op.drop_index(op.f('ix_task_completed'), table_name='task')
60
+ op.drop_index(op.f('ix_task_user_id'), table_name='task')
61
+ op.drop_index(op.f('ix_user_email'), table_name='user')
62
+
63
+ # Drop tables
64
+ op.drop_table('task')
65
+ op.drop_table('user')
run_migrations.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Script to run Alembic migrations for the task management application.
4
+
5
+ This script demonstrates how to run the database migrations when PostgreSQL is available.
6
+
7
+ To run migrations in a real environment:
8
+
9
+ 1. Make sure PostgreSQL is running:
10
+ docker-compose up -d db
11
+
12
+ 2. Run this script:
13
+ python run_migrations.py
14
+
15
+ Or run directly with Alembic:
16
+ cd backend
17
+ alembic upgrade head
18
+ """
19
+
20
+ import subprocess
21
+ import sys
22
+ import os
23
+
24
+ def run_migrations():
25
+ """Run alembic migrations to create database tables."""
26
+ print("Attempting to run Alembic migrations...")
27
+ print("Migration file created: alembic/versions/20251221_164149_initial_migration_for_users_and_tasks_tables.py")
28
+ print()
29
+ print("To execute the migration, please ensure PostgreSQL is running and run:")
30
+ print(" cd backend")
31
+ print(" alembic upgrade head")
32
+ print()
33
+ print("Or using the environment-specific command:")
34
+ print(" DATABASE_URL=postgresql://postgres:postgres@localhost:5432/todoapp alembic upgrade head")
35
+ print()
36
+ print("The migration will create:")
37
+ print("- users table with id, email, password_hash, created_at, updated_at")
38
+ print("- tasks table with id, title, description, completed, user_id, created_at, updated_at")
39
+ print("- proper indexes and foreign key relationships")
40
+
41
+ if __name__ == "__main__":
42
+ run_migrations()
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (162 Bytes). View file
 
src/__pycache__/config.cpython-312.pyc ADDED
Binary file (2.12 kB). View file
 
src/__pycache__/database.cpython-312.pyc ADDED
Binary file (1.8 kB). View file
 
src/__pycache__/main.cpython-312.pyc ADDED
Binary file (2.02 kB). View file
 
src/config.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ from pydantic import Field
3
+ from typing import Optional
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ """
8
+ Application settings loaded from environment variables.
9
+ """
10
+ # Database settings
11
+ database_url: str = Field(default="postgresql://postgres:postgres@localhost:5432/todoapp", alias="DATABASE_URL")
12
+
13
+ # Authentication settings
14
+ auth_secret: str = Field(default="dev_secret_for_development_do_not_use_in_production", alias="SECRET_KEY")
15
+ jwt_algorithm: str = Field(default="HS256", alias="ALGORITHM")
16
+ jwt_expiration_minutes: int = Field(default=10080, alias="ACCESS_TOKEN_EXPIRE_MINUTES") # 7 days
17
+
18
+ # Server settings
19
+ server_host: str = Field(default="0.0.0.0", alias="SERVER_HOST")
20
+ server_port: int = Field(default=8000, alias="SERVER_PORT")
21
+ debug: bool = Field(default=True, alias="DEBUG")
22
+
23
+ # CORS settings
24
+ frontend_url: str = Field(default="http://localhost:3000", alias="FRONTEND_URL")
25
+ DATABASE_URL: str = "postgresql://neondb_owner:npg_LE9V4bojIRQD@ep-blue-block-ahgb84lf-pooler.c-3.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require"
26
+
27
+ # Authentication settings
28
+ AUTH_SECRET: str = "dev_secret_for_development_do_not_use_in_production"
29
+ JWT_ALGORITHM: str = "HS256"
30
+ JWT_EXPIRATION_MINUTES: int = 10080 # 7 days
31
+
32
+ # Server settings
33
+ SERVER_HOST: str = "0.0.0.0"
34
+ SERVER_PORT: int = 8000
35
+ DEBUG: bool = True
36
+
37
+ # CORS settings
38
+ FRONTEND_URL: str = "http://localhost:3000"
39
+
40
+ class Config:
41
+ env_file = ".env"
42
+ case_sensitive = True
43
+
44
+
45
+ settings = Settings()
src/database.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.pool import QueuePool
3
+ from sqlalchemy.orm import sessionmaker, Session
4
+ from contextlib import contextmanager
5
+ from typing import Generator
6
+ from .config import settings
7
+ from sqlmodel import SQLModel
8
+
9
+
10
+ # Create the database engine with connection pooling
11
+ engine = create_engine(
12
+ settings.DATABASE_URL,
13
+ poolclass=QueuePool,
14
+ pool_size=5,
15
+ max_overflow=10,
16
+ pool_pre_ping=True, # Verify connections before use
17
+ pool_recycle=300, # Recycle connections every 5 minutes
18
+ echo=settings.DEBUG # Log SQL queries in debug mode
19
+ )
20
+
21
+ # Create a SessionLocal class for database sessions
22
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
23
+
24
+
25
+ def get_db_session() -> Generator[Session, None, None]:
26
+ """
27
+ Generator function for FastAPI dependency injection.
28
+ Provides database sessions and ensures they're properly closed.
29
+
30
+ Yields:
31
+ Session: Database session that will be automatically closed
32
+ """
33
+ db = SessionLocal()
34
+ try:
35
+ yield db
36
+ finally:
37
+ db.close()
38
+
39
+
40
+ @contextmanager
41
+ def get_db() -> Generator[Session, None, None]:
42
+ """
43
+ Context manager for database sessions.
44
+ Ensures the database session is properly closed after use.
45
+ """
46
+ db = SessionLocal()
47
+ try:
48
+ yield db
49
+ finally:
50
+ db.close()
src/main.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from .database import engine
4
+ from .models import user, task # Import models to register them with SQLModel
5
+
6
+ def create_app():
7
+ app = FastAPI(
8
+ title="Task Management API",
9
+ description="API for managing tasks with user authentication",
10
+ version="1.0.0"
11
+ )
12
+
13
+ # Add CORS middleware
14
+ # Note: When allow_credentials=True, you cannot use wildcard '*' for origins
15
+ # Must specify exact origins
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["http://localhost:3000"], # Frontend origin
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ # Import and include routers
25
+ from .routers import auth, tasks
26
+ app.include_router(auth.router, prefix="/api", tags=["authentication"])
27
+ app.include_router(tasks.router, prefix="/api", tags=["tasks"])
28
+
29
+ @app.get("/")
30
+ def read_root():
31
+ return {"message": "Task Management API"}
32
+
33
+ @app.get("/health")
34
+ def health_check():
35
+ return {"status": "healthy"}
36
+
37
+ return app
38
+
39
+ app = create_app()
40
+
41
+ # Create database tables on startup (for development)
42
+ @app.on_event("startup")
43
+ def on_startup():
44
+ from sqlmodel import SQLModel
45
+ SQLModel.metadata.create_all(bind=engine)
src/middleware/__init__.py ADDED
File without changes
src/middleware/auth.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, HTTPException, status
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from typing import Optional
4
+ from src.utils.security import verify_token
5
+ from src.models.user import User
6
+ from sqlalchemy.orm import Session
7
+ from src.database import get_db_session
8
+
9
+
10
+ class JWTBearer(HTTPBearer):
11
+ """
12
+ JWT Bearer token authentication middleware.
13
+ """
14
+ def __init__(self, auto_error: bool = True):
15
+ super(JWTBearer, self).__init__(auto_error=auto_error)
16
+
17
+ async def __call__(self, request: Request) -> Optional[str]:
18
+ """
19
+ Validate JWT token from request.
20
+
21
+ Args:
22
+ request: FastAPI request object
23
+
24
+ Returns:
25
+ str: The validated token if valid, None if invalid and auto_error is False
26
+
27
+ Raises:
28
+ HTTPException: If token is invalid and auto_error is True
29
+ """
30
+ credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
31
+
32
+ if credentials:
33
+ if not credentials.scheme == "Bearer":
34
+ raise HTTPException(
35
+ status_code=status.HTTP_403_FORBIDDEN,
36
+ detail="Invalid authentication scheme."
37
+ )
38
+ token = credentials.credentials
39
+ else:
40
+ # Try to get token from cookie as fallback
41
+ token = request.cookies.get("access_token")
42
+ if not token:
43
+ raise HTTPException(
44
+ status_code=status.HTTP_403_FORBIDDEN,
45
+ detail="No token provided."
46
+ )
47
+
48
+ # Verify the token
49
+ payload = verify_token(token)
50
+ if payload is None:
51
+ raise HTTPException(
52
+ status_code=status.HTTP_403_FORBIDDEN,
53
+ detail="Invalid or expired token."
54
+ )
55
+
56
+ # Add user ID to request state for later use
57
+ user_id = payload.get("sub")
58
+ if not user_id:
59
+ raise HTTPException(
60
+ status_code=status.HTTP_403_FORBIDDEN,
61
+ detail="Invalid token payload."
62
+ )
63
+
64
+ request.state.user_id = user_id
65
+ return token
66
+
67
+
68
+ def verify_user_access(user_id: str, authenticated_user_id: str) -> bool:
69
+ """
70
+ Verify that the requested user ID matches the authenticated user ID.
71
+
72
+ Args:
73
+ user_id: The user ID from the request path/params
74
+ authenticated_user_id: The user ID from the JWT token
75
+
76
+ Returns:
77
+ bool: True if the IDs match, False otherwise
78
+ """
79
+ return user_id == authenticated_user_id
80
+
81
+
82
+ def get_current_user_from_request(request: Request) -> str:
83
+ """
84
+ Get the authenticated user ID from the request state.
85
+
86
+ Args:
87
+ request: FastAPI request object
88
+
89
+ Returns:
90
+ str: The authenticated user ID
91
+ """
92
+ if hasattr(request.state, 'user_id'):
93
+ return request.state.user_id
94
+ else:
95
+ raise HTTPException(
96
+ status_code=status.HTTP_401_UNAUTHORIZED,
97
+ detail="User not authenticated"
98
+ )
src/models/__init__.py ADDED
File without changes
src/models/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (169 Bytes). View file
 
src/models/__pycache__/task.cpython-312.pyc ADDED
Binary file (2.06 kB). View file
 
src/models/__pycache__/user.cpython-312.pyc ADDED
Binary file (1.88 kB). View file
 
src/models/task.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING, Optional
3
+ from uuid import UUID
4
+
5
+ from sqlalchemy import event
6
+ from sqlmodel import Field, SQLModel, Relationship
7
+
8
+ if TYPE_CHECKING:
9
+ from .user import User
10
+
11
+
12
+ class TaskBase(SQLModel):
13
+ """Base model for Task with common fields."""
14
+ title: str = Field(min_length=1, max_length=200, nullable=False)
15
+ description: Optional[str] = Field(default=None, max_length=1000)
16
+ completed: bool = Field(default=False)
17
+
18
+
19
+ class Task(TaskBase, table=True):
20
+ """Task model for the task management application."""
21
+ id: int = Field(default=None, primary_key=True)
22
+ user_id: UUID = Field(foreign_key="user.id", index=True, nullable=False)
23
+ created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
24
+ updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
25
+
26
+ # Relationship to User
27
+ user: "User" = Relationship(back_populates="tasks")
28
+
29
+
30
+ # SQLAlchemy event listener to update the updated_at field before each update
31
+ @event.listens_for(Task, "before_update")
32
+ def update_updated_at(mapper, connection, target):
33
+ target.updated_at = datetime.utcnow()
src/models/user.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING, Optional
3
+ from uuid import UUID, uuid4
4
+
5
+ from sqlalchemy import event
6
+ from sqlmodel import Field, SQLModel, Relationship
7
+
8
+ if TYPE_CHECKING:
9
+ from .task import Task
10
+
11
+
12
+ class UserBase(SQLModel):
13
+ """Base model for User with common fields."""
14
+ email: str = Field(unique=True, index=True, nullable=False)
15
+
16
+
17
+ class User(UserBase, table=True):
18
+ """User model for the task management application."""
19
+ id: UUID = Field(default_factory=uuid4, primary_key=True)
20
+ password_hash: str = Field(nullable=False)
21
+ created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
22
+ updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
23
+
24
+ # Relationship to tasks
25
+ tasks: list["Task"] = Relationship(back_populates="user")
26
+
27
+
28
+ # SQLAlchemy event listener to update the updated_at field before each update
29
+ @event.listens_for(User, "before_update")
30
+ def update_updated_at(target, value, oldvalue, initiator):
31
+ target.updated_at = datetime.utcnow()
src/routers/__init__.py ADDED
File without changes
src/routers/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (170 Bytes). View file
 
src/routers/__pycache__/auth.cpython-312.pyc ADDED
Binary file (5.82 kB). View file
 
src/routers/__pycache__/tasks.cpython-312.pyc ADDED
Binary file (5.66 kB). View file
 
src/routers/auth.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Response, Request
2
+ from sqlalchemy.orm import Session
3
+ from typing import Any
4
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
5
+
6
+ from src.schemas.auth import RegisterRequest, RegisterResponse, LoginRequest, LoginResponse
7
+ from src.models.user import User
8
+ from src.utils.security import get_password_hash, verify_password, create_access_token, verify_token
9
+ from src.utils.deps import get_db
10
+
11
+ router = APIRouter(tags=["Authentication"])
12
+
13
+
14
+ @router.post("/register", response_model=RegisterResponse)
15
+ def register(user_data: RegisterRequest, db: Session = Depends(get_db)) -> RegisterResponse:
16
+ """
17
+ Register a new user with email and password.
18
+
19
+ Args:
20
+ user_data: Registration request containing email and password
21
+ db: Database session dependency
22
+
23
+ Returns:
24
+ RegisterResponse: Created user information
25
+
26
+ Raises:
27
+ HTTPException: If email is invalid format (handled by Pydantic) or email already exists
28
+ """
29
+ # Check if user with this email already exists
30
+ existing_user = db.query(User).filter(User.email == user_data.email).first()
31
+ if existing_user:
32
+ raise HTTPException(
33
+ status_code=status.HTTP_409_CONFLICT,
34
+ detail="An account with this email already exists"
35
+ )
36
+
37
+ # Validate password length (minimum 8 characters)
38
+ if len(user_data.password) < 8:
39
+ raise HTTPException(
40
+ status_code=status.HTTP_400_BAD_REQUEST,
41
+ detail="Password must be at least 8 characters"
42
+ )
43
+
44
+ # Create password hash (password truncation to 72 bytes is handled in get_password_hash)
45
+ password_hash = get_password_hash(user_data.password)
46
+
47
+ # Create new user
48
+ user = User(
49
+ email=user_data.email,
50
+ password_hash=password_hash
51
+ )
52
+
53
+ # Add user to database
54
+ db.add(user)
55
+ db.commit()
56
+ db.refresh(user)
57
+
58
+ # Return response
59
+ return RegisterResponse(
60
+ id=str(user.id),
61
+ email=user.email,
62
+ created_at=user.created_at
63
+ )
64
+
65
+
66
+ @router.post("/login", response_model=LoginResponse)
67
+ def login(login_data: LoginRequest, response: Response, db: Session = Depends(get_db)) -> LoginResponse:
68
+ """
69
+ Authenticate user and return JWT token.
70
+
71
+ Args:
72
+ login_data: Login request containing email and password
73
+ response: FastAPI response object to set cookies
74
+ db: Database session dependency
75
+
76
+ Returns:
77
+ LoginResponse: JWT token and user information
78
+
79
+ Raises:
80
+ HTTPException: If credentials are invalid
81
+ """
82
+ # Find user by email
83
+ user = db.query(User).filter(User.email == login_data.email).first()
84
+
85
+ # Check if user exists and password is correct (password truncation to 72 bytes is handled in verify_password)
86
+ if not user or not verify_password(login_data.password, user.password_hash):
87
+ raise HTTPException(
88
+ status_code=status.HTTP_401_UNAUTHORIZED,
89
+ detail="Invalid email or password",
90
+ headers={"WWW-Authenticate": "Bearer"},
91
+ )
92
+
93
+ # Create access token
94
+ access_token = create_access_token(data={"sub": str(user.id)})
95
+
96
+ # Set the token in an httpOnly cookie for security
97
+ response.set_cookie(
98
+ key="access_token",
99
+ value=access_token,
100
+ httponly=True,
101
+ secure=False, # Set to True in production with HTTPS
102
+ samesite="lax", # Protects against CSRF
103
+ max_age=604800 # 7 days in seconds
104
+ )
105
+
106
+ # Return response
107
+ return LoginResponse(
108
+ access_token=access_token,
109
+ token_type="bearer",
110
+ user_id=str(user.id),
111
+ email=user.email
112
+ )
113
+
114
+
115
+ @router.get("/me")
116
+ def get_current_user(request: Request, db: Session = Depends(get_db)):
117
+ """
118
+ Get current authenticated user information.
119
+ This endpoint is used to check if a user is authenticated and get their info.
120
+ """
121
+ # Get the token from cookies
122
+ token = request.cookies.get("access_token")
123
+
124
+ if not token:
125
+ raise HTTPException(
126
+ status_code=status.HTTP_401_UNAUTHORIZED,
127
+ detail="Not authenticated",
128
+ headers={"WWW-Authenticate": "Bearer"},
129
+ )
130
+
131
+ # Verify the token
132
+ payload = verify_token(token)
133
+ if not payload:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_401_UNAUTHORIZED,
136
+ detail="Invalid token",
137
+ headers={"WWW-Authenticate": "Bearer"},
138
+ )
139
+
140
+ # Get user ID from token
141
+ user_id = payload.get("sub")
142
+ if not user_id:
143
+ raise HTTPException(
144
+ status_code=status.HTTP_401_UNAUTHORIZED,
145
+ detail="Invalid token payload",
146
+ headers={"WWW-Authenticate": "Bearer"},
147
+ )
148
+
149
+ # Get user from database
150
+ user = db.query(User).filter(User.id == user_id).first()
151
+ if not user:
152
+ raise HTTPException(
153
+ status_code=status.HTTP_401_UNAUTHORIZED,
154
+ detail="User not found",
155
+ headers={"WWW-Authenticate": "Bearer"},
156
+ )
157
+
158
+ return {
159
+ "user_id": str(user.id),
160
+ "email": user.email
161
+ }
src/routers/tasks.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+ from typing import List
4
+ from uuid import UUID
5
+ from datetime import datetime
6
+
7
+ from src.schemas.task import TaskResponse, TaskListResponse, TaskCreateRequest, TaskPatchRequest, TaskUpdateRequest
8
+ from src.models.task import Task
9
+ from src.models.user import User
10
+ from src.utils.deps import get_db, get_current_user
11
+ from src.utils.security import verify_token
12
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
13
+
14
+ router = APIRouter(tags=["Tasks"])
15
+
16
+ security = HTTPBearer()
17
+
18
+
19
+ @router.get("/{user_id}/tasks", response_model=TaskListResponse)
20
+ def get_user_tasks(
21
+ user_id: UUID,
22
+ db: Session = Depends(get_db),
23
+ current_user: User = Depends(get_current_user),
24
+ ):
25
+ if user_id != current_user.id:
26
+ raise HTTPException(status_code=404, detail="Task not found")
27
+
28
+ tasks = (
29
+ db.query(Task)
30
+ .filter(Task.user_id == current_user.id)
31
+ .order_by(Task.created_at.desc())
32
+ .all()
33
+ )
34
+
35
+ return TaskListResponse(
36
+ tasks=tasks,
37
+ total=len(tasks),
38
+ )
39
+
40
+ @router.post("/{user_id}/tasks", response_model=TaskResponse, status_code=201)
41
+ def create_task(
42
+ user_id: UUID,
43
+ task_data: TaskCreateRequest,
44
+ db: Session = Depends(get_db),
45
+ current_user: User = Depends(get_current_user),
46
+ ):
47
+ if user_id != current_user.id:
48
+ raise HTTPException(status_code=404, detail="User not found")
49
+
50
+ task = Task(
51
+ title=task_data.title,
52
+ description=task_data.description,
53
+ completed=task_data.completed,
54
+ user_id=current_user.id,
55
+ )
56
+
57
+ db.add(task)
58
+ db.commit()
59
+ db.refresh(task)
60
+
61
+ return task
62
+
63
+
64
+ @router.patch("/{user_id}/tasks/{task_id}", response_model=TaskResponse)
65
+ def update_task_partial(
66
+ user_id: UUID,
67
+ task_id: int,
68
+ task_update: TaskPatchRequest,
69
+ db: Session = Depends(get_db),
70
+ current_user: User = Depends(get_current_user),
71
+ ):
72
+ if user_id != current_user.id:
73
+ raise HTTPException(status_code=404, detail="Task not found")
74
+
75
+ task = (
76
+ db.query(Task)
77
+ .filter(Task.id == task_id, Task.user_id == current_user.id)
78
+ .first()
79
+ )
80
+
81
+ if not task:
82
+ raise HTTPException(status_code=404, detail="Task not found")
83
+
84
+ if task_update.completed is not None:
85
+ task.completed = task_update.completed
86
+
87
+ db.commit()
88
+ db.refresh(task)
89
+
90
+ return task
91
+
92
+ @router.put("/{user_id}/tasks/{task_id}", response_model=TaskResponse)
93
+ def update_task_full(
94
+ user_id: UUID,
95
+ task_id: int,
96
+ task_update: TaskUpdateRequest,
97
+ db: Session = Depends(get_db),
98
+ current_user: User = Depends(get_current_user),
99
+ ):
100
+ if user_id != current_user.id:
101
+ raise HTTPException(status_code=404, detail="Task not found")
102
+
103
+ task = (
104
+ db.query(Task)
105
+ .filter(Task.id == task_id, Task.user_id == current_user.id)
106
+ .first()
107
+ )
108
+
109
+ if not task:
110
+ raise HTTPException(status_code=404, detail="Task not found")
111
+
112
+ task.title = task_update.title
113
+ task.description = task_update.description
114
+ task.completed = task_update.completed
115
+
116
+ db.commit()
117
+ db.refresh(task)
118
+
119
+ return task
120
+
121
+
122
+ @router.delete("/{user_id}/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
123
+ def delete_task(
124
+ user_id: UUID,
125
+ task_id: int,
126
+ db: Session = Depends(get_db),
127
+ current_user: User = Depends(get_current_user),
128
+ ):
129
+ if user_id != current_user.id:
130
+ raise HTTPException(status_code=404, detail="Task not found")
131
+
132
+ task = (
133
+ db.query(Task)
134
+ .filter(Task.id == task_id, Task.user_id == current_user.id)
135
+ .first()
136
+ )
137
+
138
+ if not task:
139
+ raise HTTPException(status_code=404, detail="Task not found")
140
+
141
+ db.delete(task)
142
+ db.commit()
src/schemas/__init__.py ADDED
File without changes
src/schemas/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (170 Bytes). View file
 
src/schemas/__pycache__/auth.cpython-312.pyc ADDED
Binary file (2.17 kB). View file
 
src/schemas/__pycache__/task.cpython-312.pyc ADDED
Binary file (2.14 kB). View file
 
src/schemas/auth.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+
6
+ class RegisterRequest(BaseModel):
7
+ """
8
+ Schema for user registration request.
9
+
10
+ Attributes:
11
+ email: User's email address (must be valid email format)
12
+ password: User's password (minimum 8 characters will be validated in endpoint)
13
+ """
14
+ email: EmailStr
15
+ password: str
16
+
17
+
18
+ class RegisterResponse(BaseModel):
19
+ """
20
+ Schema for user registration response.
21
+
22
+ Attributes:
23
+ id: Unique identifier of the created user
24
+ email: Email address of the created user
25
+ created_at: Timestamp when the user was created
26
+ """
27
+ id: str # UUID as string
28
+ email: EmailStr
29
+ created_at: datetime
30
+
31
+
32
+ class LoginRequest(BaseModel):
33
+ """
34
+ Schema for user login request.
35
+
36
+ Attributes:
37
+ email: User's email address
38
+ password: User's password
39
+ """
40
+ email: EmailStr
41
+ password: str
42
+
43
+
44
+ class LoginResponse(BaseModel):
45
+ """
46
+ Schema for user login response.
47
+
48
+ Attributes:
49
+ access_token: JWT token for authentication
50
+ token_type: Type of token (usually "bearer")
51
+ user_id: Unique identifier of the authenticated user
52
+ email: Email address of the authenticated user
53
+ """
54
+ access_token: str
55
+ token_type: str
56
+ user_id: str # UUID as string
57
+ email: EmailStr
src/schemas/task.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional
3
+ from datetime import datetime
4
+ from uuid import UUID
5
+
6
+
7
+ class TaskBase(BaseModel):
8
+ title: str = Field(..., min_length=1, max_length=200)
9
+ description: Optional[str] = Field(None, max_length=1000)
10
+ completed: bool = False
11
+
12
+
13
+ class TaskCreateRequest(TaskBase):
14
+ pass
15
+
16
+
17
+ class TaskUpdateRequest(BaseModel):
18
+ title: str = Field(..., min_length=1, max_length=200)
19
+ description: Optional[str] = Field(default="", max_length=1000)
20
+ completed: bool
21
+
22
+
23
+ class TaskPatchRequest(BaseModel):
24
+ completed: Optional[bool] = None
25
+
26
+
27
+ class TaskResponse(TaskBase):
28
+ id: int
29
+ user_id: UUID
30
+ created_at: datetime
31
+ updated_at: datetime
32
+
33
+ # ✅ REQUIRED FOR ORM (Pydantic v2)
34
+ model_config = {
35
+ "from_attributes": True
36
+ }
37
+
38
+
39
+ class TaskListResponse(BaseModel):
40
+ tasks: List[TaskResponse]
41
+ total: int = 0
src/task_management_backend.egg-info/PKG-INFO ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: task-management-backend
3
+ Version: 0.1.0
4
+ Summary: Backend for task management application with authentication
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastapi>=0.115.0
8
+ Requires-Dist: sqlmodel>=0.0.24
9
+ Requires-Dist: uvicorn>=0.32.0
10
+ Requires-Dist: pydantic>=2.10.0
11
+ Requires-Dist: pydantic-settings>=2.6.0
12
+ Requires-Dist: python-jose>=3.3.0
13
+ Requires-Dist: passlib[bcrypt]>=1.7.0
14
+ Requires-Dist: python-multipart>=0.0.20
15
+ Requires-Dist: alembic>=1.14.0
16
+ Requires-Dist: psycopg2-binary>=2.9.10
17
+ Requires-Dist: pytest>=8.3.0
18
+ Requires-Dist: pytest-asyncio>=0.25.0
19
+ Requires-Dist: httpx>=0.28.0
20
+ Requires-Dist: bcrypt>=5.0.0
src/task_management_backend.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pyproject.toml
2
+ src/__init__.py
3
+ src/config.py
4
+ src/database.py
5
+ src/main.py
6
+ src/middleware/__init__.py
7
+ src/middleware/auth.py
8
+ src/models/__init__.py
9
+ src/models/task.py
10
+ src/models/user.py
11
+ src/routers/__init__.py
12
+ src/routers/auth.py
13
+ src/routers/tasks.py
14
+ src/schemas/__init__.py
15
+ src/schemas/auth.py
16
+ src/schemas/task.py
17
+ src/task_management_backend.egg-info/PKG-INFO
18
+ src/task_management_backend.egg-info/SOURCES.txt
19
+ src/task_management_backend.egg-info/dependency_links.txt
20
+ src/task_management_backend.egg-info/requires.txt
21
+ src/task_management_backend.egg-info/top_level.txt
22
+ src/utils/__init__.py
23
+ src/utils/deps.py
24
+ src/utils/security.py
src/task_management_backend.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
src/task_management_backend.egg-info/requires.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.115.0
2
+ sqlmodel>=0.0.24
3
+ uvicorn>=0.32.0
4
+ pydantic>=2.10.0
5
+ pydantic-settings>=2.6.0
6
+ python-jose>=3.3.0
7
+ passlib[bcrypt]>=1.7.0
8
+ python-multipart>=0.0.20
9
+ alembic>=1.14.0
10
+ psycopg2-binary>=2.9.10
11
+ pytest>=8.3.0
12
+ pytest-asyncio>=0.25.0
13
+ httpx>=0.28.0
14
+ bcrypt>=5.0.0
src/task_management_backend.egg-info/top_level.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __init__
2
+ config
3
+ database
4
+ main
5
+ middleware
6
+ models
7
+ routers
8
+ schemas
9
+ utils
src/utils/__init__.py ADDED
File without changes
src/utils/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (168 Bytes). View file
 
src/utils/__pycache__/deps.cpython-312.pyc ADDED
Binary file (2.36 kB). View file
 
src/utils/__pycache__/security.cpython-312.pyc ADDED
Binary file (3.55 kB). View file
 
src/utils/deps.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Generator
2
+ from fastapi import Depends, HTTPException, status, Request
3
+ from sqlalchemy.orm import Session
4
+ from uuid import UUID
5
+
6
+ from src.database import get_db_session
7
+ from src.models.user import User
8
+ from src.utils.security import verify_token
9
+
10
+
11
+ def get_db() -> Generator[Session, None, None]:
12
+ db = next(get_db_session())
13
+ try:
14
+ yield db
15
+ finally:
16
+ db.close()
17
+
18
+
19
+ def get_current_user(
20
+ request: Request,
21
+ db: Session = Depends(get_db)
22
+ ) -> User:
23
+ """
24
+ Get the currently authenticated user.
25
+ Supports BOTH:
26
+ - HTTP-only cookies (preferred)
27
+ - Authorization: Bearer header (fallback)
28
+ """
29
+
30
+ token = None
31
+
32
+ # 1️⃣ Try cookie first
33
+ token = request.cookies.get("access_token")
34
+
35
+ # 2️⃣ Fallback to Authorization header
36
+ if not token:
37
+ auth_header = request.headers.get("Authorization")
38
+ if auth_header and auth_header.startswith("Bearer "):
39
+ token = auth_header.split(" ")[1]
40
+
41
+ if not token:
42
+ raise HTTPException(
43
+ status_code=status.HTTP_401_UNAUTHORIZED,
44
+ detail="Not authenticated",
45
+ )
46
+
47
+ payload = verify_token(token)
48
+ if not payload or "sub" not in payload:
49
+ raise HTTPException(
50
+ status_code=status.HTTP_401_UNAUTHORIZED,
51
+ detail="Invalid or expired token",
52
+ )
53
+
54
+ user_id = payload["sub"]
55
+
56
+ user = db.query(User).filter(User.id == UUID(user_id)).first()
57
+ if not user:
58
+ raise HTTPException(
59
+ status_code=status.HTTP_401_UNAUTHORIZED,
60
+ detail="User not found",
61
+ )
62
+
63
+ return user
src/utils/security.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from typing import Optional
3
+
4
+ import bcrypt
5
+ from jose import JWTError, jwt
6
+
7
+ from src.config import settings
8
+
9
+ # JWT settings
10
+ SECRET_KEY = settings.AUTH_SECRET
11
+ ALGORITHM = settings.JWT_ALGORITHM
12
+ ACCESS_TOKEN_EXPIRE_MINUTES = settings.JWT_EXPIRATION_MINUTES
13
+
14
+
15
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
16
+ """
17
+ Verify a plain password against a hashed password.
18
+
19
+ Args:
20
+ plain_password: The plain text password to verify
21
+ hashed_password: The hashed password to compare against
22
+
23
+ Returns:
24
+ bool: True if password matches, False otherwise
25
+ """
26
+ # Ensure password is encoded as bytes (bcrypt limitation: max 72 bytes)
27
+ password_bytes = plain_password.encode('utf-8')
28
+ if len(password_bytes) > 72:
29
+ password_bytes = password_bytes[:72]
30
+
31
+ # Verify the password
32
+ try:
33
+ return bcrypt.checkpw(password_bytes, hashed_password.encode('utf-8'))
34
+ except (ValueError, TypeError):
35
+ return False
36
+
37
+
38
+ def get_password_hash(password: str) -> str:
39
+ """
40
+ Generate a hash for the given password.
41
+
42
+ Args:
43
+ password: The plain text password to hash
44
+
45
+ Returns:
46
+ str: The hashed password
47
+ """
48
+ # Ensure password is encoded as bytes (bcrypt limitation: max 72 bytes)
49
+ password_bytes = password.encode('utf-8')
50
+ if len(password_bytes) > 72:
51
+ password_bytes = password_bytes[:72]
52
+
53
+ # Generate salt and hash
54
+ salt = bcrypt.gensalt()
55
+ hashed = bcrypt.hashpw(password_bytes, salt)
56
+ return hashed.decode('utf-8')
57
+
58
+
59
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
60
+ """
61
+ Create a JWT access token.
62
+
63
+ Args:
64
+ data: The data to include in the token (typically user info)
65
+ expires_delta: Optional expiration time delta (defaults to 7 days)
66
+
67
+ Returns:
68
+ str: The encoded JWT token
69
+ """
70
+ to_encode = data.copy()
71
+
72
+ if expires_delta:
73
+ expire = datetime.utcnow() + expires_delta
74
+ else:
75
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
76
+
77
+ to_encode.update({"exp": expire})
78
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
79
+
80
+ return encoded_jwt
81
+
82
+
83
+ def verify_token(token: str) -> Optional[dict]:
84
+ """
85
+ Verify a JWT token and return the payload if valid.
86
+
87
+ Args:
88
+ token: The JWT token to verify
89
+
90
+ Returns:
91
+ dict: The token payload if valid, None if invalid
92
+ """
93
+ try:
94
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
95
+ return payload
96
+ except JWTError:
97
+ return None