Tahasaif3 commited on
Commit
bda4716
·
1 Parent(s): b675d80

'full-project'

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +19 -0
  2. alembic.ini +145 -0
  3. alembic/README +1 -0
  4. alembic/__pycache__/env.cpython-312.pyc +0 -0
  5. alembic/env.py +87 -0
  6. alembic/script.py.mako +28 -0
  7. alembic/versions/3b6c60669e48_add_project_model_and_relationship_to_.py +52 -0
  8. alembic/versions/4ac448e3f100_add_due_date_field_to_task_model.py +32 -0
  9. alembic/versions/__pycache__/3b6c60669e48_add_project_model_and_relationship_to_.cpython-312.pyc +0 -0
  10. alembic/versions/__pycache__/4ac448e3f100_add_due_date_field_to_task_model.cpython-312.pyc +0 -0
  11. alembic/versions/__pycache__/ec70eaafa7b6_initial_schema_with_users_and_tasks_.cpython-312.pyc +0 -0
  12. alembic/versions/ec70eaafa7b6_initial_schema_with_users_and_tasks_.py +54 -0
  13. main.py +6 -0
  14. requirements.txt +16 -0
  15. src/__init__.py +0 -0
  16. src/__pycache__/__init__.cpython-312.pyc +0 -0
  17. src/__pycache__/config.cpython-312.pyc +0 -0
  18. src/__pycache__/database.cpython-312.pyc +0 -0
  19. src/__pycache__/main.cpython-312.pyc +0 -0
  20. src/config.py +23 -0
  21. src/database.py +24 -0
  22. src/main.py +34 -0
  23. src/middleware/__pycache__/auth.cpython-312.pyc +0 -0
  24. src/middleware/auth.py +41 -0
  25. src/models/__pycache__/project.cpython-312.pyc +0 -0
  26. src/models/__pycache__/task.cpython-312.pyc +0 -0
  27. src/models/__pycache__/user.cpython-312.pyc +0 -0
  28. src/models/project.py +49 -0
  29. src/models/task.py +53 -0
  30. src/models/user.py +33 -0
  31. src/routers/__init__.py +3 -0
  32. src/routers/__pycache__/__init__.cpython-312.pyc +0 -0
  33. src/routers/__pycache__/auth.cpython-312.pyc +0 -0
  34. src/routers/__pycache__/projects.cpython-312.pyc +0 -0
  35. src/routers/__pycache__/tasks.cpython-312.pyc +0 -0
  36. src/routers/auth.py +189 -0
  37. src/routers/projects.py +259 -0
  38. src/routers/tasks.py +397 -0
  39. src/schemas/__pycache__/auth.cpython-312.pyc +0 -0
  40. src/schemas/__pycache__/task.cpython-312.pyc +0 -0
  41. src/schemas/auth.py +41 -0
  42. src/schemas/task.py +39 -0
  43. src/task_api.egg-info/PKG-INFO +16 -0
  44. src/task_api.egg-info/SOURCES.txt +21 -0
  45. src/task_api.egg-info/dependency_links.txt +1 -0
  46. src/task_api.egg-info/requires.txt +10 -0
  47. src/task_api.egg-info/top_level.txt +9 -0
  48. src/utils/__pycache__/deps.cpython-312.pyc +0 -0
  49. src/utils/__pycache__/security.cpython-312.pyc +0 -0
  50. src/utils/deps.py +66 -0
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,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # timezone to use when rendering the date within the migration file
22
+ # as well as the filename.
23
+ # If specified, requires the tzdata library which can be installed by adding
24
+ # `alembic[tz]` to the pip requirements.
25
+ # string value is passed to ZoneInfo()
26
+ # leave blank for localtime
27
+ # timezone =
28
+
29
+ # max length of characters to apply to the "slug" field
30
+ # truncate_slug_length = 40
31
+
32
+ # set to 'true' to run the environment during
33
+ # the 'revision' command, regardless of autogenerate
34
+ # revision_environment = false
35
+
36
+ # set to 'true' to allow .pyc and .pyo files without
37
+ # a source .py file to be detected as revisions in the
38
+ # versions/ directory
39
+ # sourceless = false
40
+
41
+ # version location specification; This defaults
42
+ # to <script_location>/versions. When using multiple version
43
+ # directories, initial revisions must be specified with --version-path.
44
+ # The path separator used here should be the separator specified by "path_separator"
45
+ # below.
46
+ # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
47
+
48
+ # path_separator; This indicates what character is used to split lists of file
49
+ # paths, including version_locations and prepend_sys_path within configparser
50
+ # files such as alembic.ini.
51
+ # The default rendered in new alembic.ini files is "os", which uses os.pathsep
52
+ # to provide os-dependent path splitting.
53
+ #
54
+ # Note that in order to support legacy alembic.ini files, this default does NOT
55
+ # take place if path_separator is not present in alembic.ini. If this
56
+ # option is omitted entirely, fallback logic is as follows:
57
+ #
58
+ # 1. Parsing of the version_locations option falls back to using the legacy
59
+ # "version_path_separator" key, which if absent then falls back to the legacy
60
+ # behavior of splitting on spaces and/or commas.
61
+ # 2. Parsing of the prepend_sys_path option falls back to the legacy
62
+ # behavior of splitting on spaces, commas, or colons.
63
+ #
64
+ # Valid values for path_separator are:
65
+ #
66
+ # path_separator = :
67
+ # path_separator = ;
68
+ # path_separator = space
69
+ # path_separator = newline
70
+ #
71
+ # Use os.pathsep. Default configuration used for new projects.
72
+ path_separator = os
73
+
74
+ # set to 'true' to search source files recursively
75
+ # in each "version_locations" directory
76
+ # new in Alembic version 1.10
77
+ # recursive_version_locations = false
78
+
79
+ # the output encoding used when revision files
80
+ # are written from script.py.mako
81
+ # output_encoding = utf-8
82
+
83
+ # database URL. This is consumed by the user-maintained env.py script only.
84
+ # other means of configuring database URLs may be customized within the env.py
85
+ # file.
86
+ sqlalchemy.url = postgresql://neondb_owner:npg_LsojKQF8bGn2@ep-mute-pine-a4g0wfsu-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require
87
+
88
+ [post_write_hooks]
89
+ # post_write_hooks defines scripts or Python functions that are run
90
+ # on newly generated revision scripts. See the documentation for further
91
+ # detail and examples
92
+
93
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
94
+ # hooks = black
95
+ # black.type = console_scripts
96
+ # black.entrypoint = black
97
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
98
+
99
+ # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
100
+ # hooks = ruff
101
+ # ruff.type = module
102
+ # ruff.module = ruff
103
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
104
+
105
+ # Alternatively, use the exec runner to execute a binary found on your PATH
106
+ # hooks = ruff
107
+ # ruff.type = exec
108
+ # ruff.executable = ruff
109
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
110
+
111
+ # Logging configuration. This is also consumed by the user-maintained
112
+ # env.py script only.
113
+ [loggers]
114
+ keys = root,sqlalchemy,alembic
115
+
116
+ [handlers]
117
+ keys = console
118
+
119
+ [formatters]
120
+ keys = generic
121
+
122
+ [logger_root]
123
+ level = WARNING
124
+ handlers = console
125
+ qualname =
126
+
127
+ [logger_sqlalchemy]
128
+ level = WARNING
129
+ handlers =
130
+ qualname = sqlalchemy.engine
131
+
132
+ [logger_alembic]
133
+ level = INFO
134
+ handlers =
135
+ qualname = alembic
136
+
137
+ [handler_console]
138
+ class = StreamHandler
139
+ args = (sys.stderr,)
140
+ level = NOTSET
141
+ formatter = generic
142
+
143
+ [formatter_generic]
144
+ format = %(levelname)-5.5s [%(name)s] %(message)s
145
+ datefmt = %H:%M:%S
alembic/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
alembic/__pycache__/env.cpython-312.pyc ADDED
Binary file (3.01 kB). View file
 
alembic/env.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+
3
+ from sqlalchemy import engine_from_config
4
+ from sqlalchemy import pool
5
+ from alembic import context
6
+
7
+ # Import SQLModel and models
8
+ from sqlmodel import SQLModel
9
+
10
+ import sys
11
+ import os
12
+ sys.path.append(os.path.dirname(os.path.dirname(__file__)))
13
+
14
+ from src.models.user import User # Import your models
15
+ from src.models.task import Task # Import your models
16
+ from src.models.project import Project # Import your models
17
+
18
+
19
+ # this is the Alembic Config object, which provides
20
+ # access to the values within the .ini file in use.
21
+ config = context.config
22
+
23
+ # Interpret the config file for Python logging.
24
+ # This line sets up loggers basically.
25
+ if config.config_file_name is not None:
26
+ fileConfig(config.config_file_name)
27
+
28
+ # add your model's MetaData object here
29
+ # for 'autogenerate' support
30
+ target_metadata = SQLModel.metadata
31
+
32
+ # other values from the config, defined by the needs of env.py,
33
+ # can be acquired:
34
+ # my_important_option = config.get_main_option("my_important_option")
35
+ # ... etc.
36
+
37
+
38
+ def run_migrations_offline() -> None:
39
+ """Run migrations in 'offline' mode.
40
+
41
+ This configures the context with just a URL
42
+ and not an Engine, though an Engine is acceptable
43
+ here as well. By skipping the Engine creation
44
+ we don't even need a DBAPI to be available.
45
+
46
+ Calls to context.execute() here emit the given string to the
47
+ script output.
48
+
49
+ """
50
+ url = config.get_main_option("sqlalchemy.url")
51
+ context.configure(
52
+ url=url,
53
+ target_metadata=target_metadata,
54
+ literal_binds=True,
55
+ dialect_opts={"paramstyle": "named"},
56
+ )
57
+
58
+ with context.begin_transaction():
59
+ context.run_migrations()
60
+
61
+
62
+ def run_migrations_online() -> None:
63
+ """Run migrations in 'online' mode.
64
+
65
+ In this scenario we need to create an Engine
66
+ and associate a connection with the context.
67
+
68
+ """
69
+ connectable = engine_from_config(
70
+ config.get_section(config.config_ini_section),
71
+ prefix="sqlalchemy.",
72
+ poolclass=pool.NullPool,
73
+ )
74
+
75
+ with connectable.connect() as connection:
76
+ context.configure(
77
+ connection=connection, target_metadata=target_metadata
78
+ )
79
+
80
+ with context.begin_transaction():
81
+ context.run_migrations()
82
+
83
+
84
+ if context.is_offline_mode():
85
+ run_migrations_offline()
86
+ else:
87
+ 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/3b6c60669e48_add_project_model_and_relationship_to_.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add Project model and relationship to Task
2
+
3
+ Revision ID: 3b6c60669e48
4
+ Revises: ec70eaafa7b6
5
+ Create Date: 2025-12-19 03:46:01.389687
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import sqlmodel
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = '3b6c60669e48'
17
+ down_revision: Union[str, Sequence[str], None] = 'ec70eaafa7b6'
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Upgrade schema."""
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.create_table('project',
26
+ sa.Column('id', sa.Uuid(), nullable=False),
27
+ sa.Column('user_id', sa.Uuid(), nullable=False),
28
+ sa.Column('name', sa.String(length=200), nullable=False),
29
+ sa.Column('description', sa.String(length=1000), nullable=True),
30
+ sa.Column('color', sa.String(length=7), nullable=True),
31
+ sa.Column('created_at', sa.DateTime(), nullable=True),
32
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
33
+ sa.Column('deadline', sa.DateTime(), nullable=True),
34
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
35
+ sa.PrimaryKeyConstraint('id')
36
+ )
37
+ op.create_index(op.f('ix_project_user_id'), 'project', ['user_id'], unique=False)
38
+ op.add_column('task', sa.Column('project_id', sa.Uuid(), nullable=True))
39
+ op.create_index(op.f('ix_task_project_id'), 'task', ['project_id'], unique=False)
40
+ op.create_foreign_key(None, 'task', 'project', ['project_id'], ['id'])
41
+ # ### end Alembic commands ###
42
+
43
+
44
+ def downgrade() -> None:
45
+ """Downgrade schema."""
46
+ # ### commands auto generated by Alembic - please adjust! ###
47
+ op.drop_constraint(None, 'task', type_='foreignkey')
48
+ op.drop_index(op.f('ix_task_project_id'), table_name='task')
49
+ op.drop_column('task', 'project_id')
50
+ op.drop_index(op.f('ix_project_user_id'), table_name='project')
51
+ op.drop_table('project')
52
+ # ### end Alembic commands ###
alembic/versions/4ac448e3f100_add_due_date_field_to_task_model.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add due_date field to Task model
2
+
3
+ Revision ID: 4ac448e3f100
4
+ Revises: 3b6c60669e48
5
+ Create Date: 2025-12-19 03:50:35.687835
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '4ac448e3f100'
16
+ down_revision: Union[str, Sequence[str], None] = '3b6c60669e48'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.add_column('task', sa.Column('due_date', sa.DateTime(), nullable=True))
25
+ # ### end Alembic commands ###
26
+
27
+
28
+ def downgrade() -> None:
29
+ """Downgrade schema."""
30
+ # ### commands auto generated by Alembic - please adjust! ###
31
+ op.drop_column('task', 'due_date')
32
+ # ### end Alembic commands ###
alembic/versions/__pycache__/3b6c60669e48_add_project_model_and_relationship_to_.cpython-312.pyc ADDED
Binary file (3.45 kB). View file
 
alembic/versions/__pycache__/4ac448e3f100_add_due_date_field_to_task_model.cpython-312.pyc ADDED
Binary file (1.35 kB). View file
 
alembic/versions/__pycache__/ec70eaafa7b6_initial_schema_with_users_and_tasks_.cpython-312.pyc ADDED
Binary file (3.82 kB). View file
 
alembic/versions/ec70eaafa7b6_initial_schema_with_users_and_tasks_.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Initial schema with users and tasks tables
2
+
3
+ Revision ID: ec70eaafa7b6
4
+ Revises:
5
+ Create Date: 2025-12-16 05:07:24.251683
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import sqlmodel
13
+ # revision identifiers, used by Alembic.
14
+ revision: str = 'ec70eaafa7b6'
15
+ down_revision: Union[str, Sequence[str], None] = None
16
+ branch_labels: Union[str, Sequence[str], None] = None
17
+ depends_on: Union[str, Sequence[str], None] = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ """Upgrade schema."""
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.create_table('user',
24
+ sa.Column('id', sa.Uuid(), nullable=False),
25
+ sa.Column('email', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
26
+ sa.Column('password_hash', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
27
+ sa.Column('created_at', sa.DateTime(), nullable=True),
28
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
29
+ sa.PrimaryKeyConstraint('id')
30
+ )
31
+ op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
32
+ op.create_table('task',
33
+ sa.Column('id', sa.Integer(), nullable=False),
34
+ sa.Column('user_id', sa.Uuid(), nullable=False),
35
+ sa.Column('title', sqlmodel.sql.sqltypes.AutoString(length=200), nullable=False),
36
+ sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=1000), nullable=True),
37
+ sa.Column('completed', sa.Boolean(), nullable=False),
38
+ sa.Column('created_at', sa.DateTime(), nullable=True),
39
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
40
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
41
+ sa.PrimaryKeyConstraint('id')
42
+ )
43
+ op.create_index(op.f('ix_task_user_id'), 'task', ['user_id'], unique=False)
44
+ # ### end Alembic commands ###
45
+
46
+
47
+ def downgrade() -> None:
48
+ """Downgrade schema."""
49
+ # ### commands auto generated by Alembic - please adjust! ###
50
+ op.drop_index(op.f('ix_task_user_id'), table_name='task')
51
+ op.drop_table('task')
52
+ op.drop_index(op.f('ix_user_email'), table_name='user')
53
+ op.drop_table('user')
54
+ # ### end Alembic commands ###
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from task-api!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ alembic>=1.17.2
2
+ fastapi>=0.124.4
3
+ passlib[bcrypt]>=1.7.4
4
+ psycopg2-binary>=2.9.11
5
+ pydantic-settings>=2.12.0
6
+ pydantic[email]>=2.12.5
7
+ python-jose[cryptography]>=3.5.0
8
+ python-multipart>=0.0.20
9
+ sqlmodel>=0.0.27
10
+ uvicorn>=0.38.0
11
+ httpx>=0.28.1
12
+ pytest>=9.0.2
13
+ pytest-asyncio>=1.3.0
14
+ python-dotenv>=1.0.1
15
+ bcrypt>=4.2.1
16
+ cryptography>=45.0.0
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (157 Bytes). View file
 
src/__pycache__/config.cpython-312.pyc ADDED
Binary file (1.27 kB). View file
 
src/__pycache__/database.cpython-312.pyc ADDED
Binary file (1.1 kB). View file
 
src/__pycache__/main.cpython-312.pyc ADDED
Binary file (1.36 kB). View file
 
src/config.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ from typing import Optional
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ # Database
7
+ DATABASE_URL: str = "postgresql://neondb_owner:npg_LsojKQF8bGn2@ep-mute-pine-a4g0wfsu-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require"
8
+
9
+ # Auth
10
+ BETTER_AUTH_SECRET: str = "your-secret-key-change-in-production"
11
+ JWT_SECRET_KEY: str = "your-jwt-secret-change-in-production"
12
+ JWT_ALGORITHM: str = "HS256"
13
+ ACCESS_TOKEN_EXPIRE_DAYS: int = 7
14
+ JWT_COOKIE_SECURE: bool = False # Set to True in production
15
+
16
+ # CORS
17
+ FRONTEND_URL: str = "http://localhost:3000"
18
+
19
+ class Config:
20
+ env_file = ".env"
21
+
22
+
23
+ settings = Settings()
src/database.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import create_engine, Session
2
+ from contextlib import contextmanager
3
+ from .config import settings
4
+ # Create the database engine
5
+ engine = create_engine(
6
+ settings.DATABASE_URL,
7
+ echo=False, # Set to True for SQL query logging
8
+ pool_pre_ping=True,
9
+ pool_size=5,
10
+ max_overflow=10
11
+ )
12
+
13
+
14
+ @contextmanager
15
+ def get_session():
16
+ """Context manager for database sessions."""
17
+ with Session(engine) as session:
18
+ yield session
19
+
20
+
21
+ def get_session_dep():
22
+ """Dependency for FastAPI to get database session."""
23
+ with get_session() as session:
24
+ yield session
src/main.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+
4
+ from .routers import auth, tasks, projects
5
+
6
+ app = FastAPI(
7
+ title="Task API",
8
+ description="Task management API with authentication",
9
+ version="1.0.0"
10
+ )
11
+
12
+ # Include routers
13
+ app.include_router(auth.router)
14
+ app.include_router(tasks.router)
15
+ app.include_router(projects.router)
16
+
17
+ # CORS configuration (development and production)
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"], # Allow all origins for development
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ # Expose headers for auth
25
+ expose_headers=["Access-Control-Allow-Origin", "Set-Cookie"]
26
+ )
27
+
28
+ @app.get("/api/health")
29
+ async def health_check():
30
+ return {"status": "healthy"}
31
+
32
+ if __name__ == "__main__":
33
+ import uvicorn
34
+ uvicorn.run(app, host="0.0.0.0", port=8000)
src/middleware/__pycache__/auth.cpython-312.pyc ADDED
Binary file (1.45 kB). View file
 
src/middleware/auth.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException, status
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from typing import Optional
4
+ from sqlmodel import Session
5
+ import uuid
6
+
7
+ from ..models.user import User
8
+ from ..utils.security import verify_user_id_from_token
9
+ from ..database import get_session_dep
10
+ from fastapi import Depends
11
+
12
+
13
+ # Security scheme for JWT
14
+ security = HTTPBearer()
15
+
16
+
17
+ async def verify_jwt_token(
18
+ credentials: HTTPAuthorizationCredentials = Depends(security),
19
+ session: Session = Depends(get_session_dep)
20
+ ):
21
+ """Verify JWT token and return user_id if valid."""
22
+ token = credentials.credentials
23
+ user_id = verify_user_id_from_token(token)
24
+
25
+ if not user_id:
26
+ raise HTTPException(
27
+ status_code=status.HTTP_401_UNAUTHORIZED,
28
+ detail="Invalid token or expired token.",
29
+ headers={"WWW-Authenticate": "Bearer"},
30
+ )
31
+
32
+ # Get user from database to ensure they still exist
33
+ user = session.get(User, user_id)
34
+ if not user:
35
+ raise HTTPException(
36
+ status_code=status.HTTP_401_UNAUTHORIZED,
37
+ detail="User no longer exists.",
38
+ headers={"WWW-Authenticate": "Bearer"},
39
+ )
40
+
41
+ return user_id
src/models/__pycache__/project.cpython-312.pyc ADDED
Binary file (3.28 kB). View file
 
src/models/__pycache__/task.cpython-312.pyc ADDED
Binary file (3.52 kB). View file
 
src/models/__pycache__/user.cpython-312.pyc ADDED
Binary file (2.16 kB). View file
 
src/models/project.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from typing import Optional, List
3
+ import uuid
4
+ from datetime import datetime
5
+ from sqlalchemy import Column, DateTime
6
+
7
+
8
+ class ProjectBase(SQLModel):
9
+ name: str = Field(min_length=1, max_length=200)
10
+ description: Optional[str] = Field(default=None, max_length=1000)
11
+ color: Optional[str] = Field(default="#3b82f6", max_length=7) # Hex color code
12
+
13
+
14
+ class Project(ProjectBase, table=True):
15
+ id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True)
16
+ user_id: uuid.UUID = Field(foreign_key="user.id", index=True)
17
+ name: str = Field(min_length=1, max_length=200)
18
+ description: Optional[str] = Field(default=None, max_length=1000)
19
+ color: Optional[str] = Field(default="#3b82f6", max_length=7)
20
+ created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.utcnow))
21
+ updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow))
22
+ deadline: Optional[datetime] = None
23
+
24
+ # Relationship to user
25
+ owner: Optional["User"] = Relationship(back_populates="projects")
26
+
27
+ # Relationship to tasks
28
+ tasks: List["Task"] = Relationship(back_populates="project")
29
+
30
+
31
+ class ProjectCreate(ProjectBase):
32
+ pass
33
+
34
+
35
+ class ProjectRead(ProjectBase):
36
+ id: uuid.UUID
37
+ user_id: uuid.UUID
38
+ created_at: datetime
39
+ updated_at: datetime
40
+
41
+ class Config:
42
+ from_attributes = True
43
+
44
+
45
+ class ProjectUpdate(SQLModel):
46
+ name: Optional[str] = Field(default=None, min_length=1, max_length=200)
47
+ description: Optional[str] = Field(default=None, max_length=1000)
48
+ color: Optional[str] = Field(default=None, max_length=7)
49
+ deadline: Optional[datetime] = None
src/models/task.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from typing import Optional
3
+ from datetime import datetime
4
+ import uuid
5
+ from sqlalchemy import Column, DateTime
6
+
7
+
8
+ class TaskBase(SQLModel):
9
+ title: str = Field(min_length=1, max_length=200)
10
+ description: Optional[str] = Field(default=None, max_length=1000)
11
+ completed: bool = Field(default=False)
12
+ due_date: Optional[datetime] = None
13
+
14
+
15
+ class Task(TaskBase, table=True):
16
+ id: Optional[int] = Field(default=None, primary_key=True)
17
+ user_id: uuid.UUID = Field(foreign_key="user.id", index=True)
18
+ project_id: Optional[uuid.UUID] = Field(default=None, foreign_key="project.id", index=True)
19
+ title: str = Field(min_length=1, max_length=200)
20
+ description: Optional[str] = Field(default=None, max_length=1000)
21
+ completed: bool = Field(default=False)
22
+ due_date: Optional[datetime] = None
23
+ created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.utcnow))
24
+ updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow))
25
+
26
+ # Relationship to user
27
+ owner: Optional["User"] = Relationship(back_populates="tasks")
28
+
29
+ # Relationship to project
30
+ project: Optional["Project"] = Relationship(back_populates="tasks")
31
+
32
+
33
+ class TaskCreate(TaskBase):
34
+ project_id: Optional[uuid.UUID] = None
35
+
36
+
37
+ class TaskRead(TaskBase):
38
+ id: int
39
+ user_id: uuid.UUID
40
+ project_id: Optional[uuid.UUID] = None
41
+ created_at: datetime
42
+ updated_at: datetime
43
+
44
+ class Config:
45
+ from_attributes = True
46
+
47
+
48
+ class TaskUpdate(SQLModel):
49
+ title: Optional[str] = Field(default=None, min_length=1, max_length=200)
50
+ description: Optional[str] = Field(default=None, max_length=1000)
51
+ completed: Optional[bool] = None
52
+ project_id: Optional[uuid.UUID] = None
53
+ due_date: Optional[datetime] = None
src/models/user.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field, Relationship
2
+ from typing import Optional, List
3
+ import uuid
4
+ from datetime import datetime
5
+ from sqlalchemy import Column, DateTime
6
+
7
+
8
+ class UserBase(SQLModel):
9
+ email: str = Field(unique=True, index=True, max_length=255)
10
+
11
+
12
+ class User(UserBase, table=True):
13
+ id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True)
14
+ email: str = Field(unique=True, index=True, max_length=255)
15
+ password_hash: str = Field(max_length=255)
16
+ created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.utcnow))
17
+ updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow))
18
+
19
+ # Relationship to tasks
20
+ tasks: List["Task"] = Relationship(back_populates="owner")
21
+
22
+ # Relationship to projects
23
+ projects: List["Project"] = Relationship(back_populates="owner")
24
+
25
+
26
+ class UserCreate(UserBase):
27
+ password: str
28
+
29
+
30
+ class UserRead(UserBase):
31
+ id: uuid.UUID
32
+ created_at: datetime
33
+ updated_at: datetime
src/routers/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from . import auth, tasks
2
+
3
+ __all__ = ["auth", "tasks"]
src/routers/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (246 Bytes). View file
 
src/routers/__pycache__/auth.cpython-312.pyc ADDED
Binary file (7.97 kB). View file
 
src/routers/__pycache__/projects.cpython-312.pyc ADDED
Binary file (8.38 kB). View file
 
src/routers/__pycache__/tasks.cpython-312.pyc ADDED
Binary file (12.2 kB). View file
 
src/routers/auth.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends, Response, Request
2
+ from sqlmodel import Session, select
3
+ from typing import Annotated
4
+ from datetime import datetime, timedelta
5
+ from uuid import uuid4
6
+ import secrets
7
+
8
+ from ..models.user import User, UserCreate, UserRead
9
+ from ..schemas.auth import RegisterRequest, RegisterResponse, LoginRequest, LoginResponse, ForgotPasswordRequest, ResetPasswordRequest
10
+ from ..utils.security import hash_password, create_access_token, verify_password
11
+ from ..utils.deps import get_current_user
12
+ from ..database import get_session_dep
13
+ from ..config import settings
14
+
15
+
16
+ router = APIRouter(prefix="/api/auth", tags=["auth"])
17
+
18
+
19
+ @router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED)
20
+ def register(user_data: RegisterRequest, response: Response, session: Session = Depends(get_session_dep)):
21
+ """Register a new user with email and password."""
22
+
23
+ # Check if user already exists
24
+ existing_user = session.exec(select(User).where(User.email == user_data.email)).first()
25
+ if existing_user:
26
+ raise HTTPException(
27
+ status_code=status.HTTP_409_CONFLICT,
28
+ detail="An account with this email already exists"
29
+ )
30
+
31
+ # Validate password length
32
+ if len(user_data.password) < 8:
33
+ raise HTTPException(
34
+ status_code=status.HTTP_400_BAD_REQUEST,
35
+ detail="Password must be at least 8 characters"
36
+ )
37
+
38
+ # Hash the password
39
+ password_hash = hash_password(user_data.password)
40
+
41
+ # Create new user
42
+ user = User(
43
+ email=user_data.email,
44
+ password_hash=password_hash
45
+ )
46
+
47
+ session.add(user)
48
+ session.commit()
49
+ session.refresh(user)
50
+
51
+ # Create access token
52
+ access_token = create_access_token(data={"sub": str(user.id)})
53
+
54
+ # Set the token as an httpOnly cookie
55
+ response.set_cookie(
56
+ key="access_token",
57
+ value=access_token,
58
+ httponly=True,
59
+ secure=settings.JWT_COOKIE_SECURE, # True in production, False in development
60
+ samesite="lax",
61
+ max_age=settings.ACCESS_TOKEN_EXPIRE_DAYS * 24 * 60 * 60, # Convert days to seconds
62
+ path="/"
63
+ )
64
+
65
+ # Return response
66
+ return RegisterResponse(
67
+ id=user.id,
68
+ email=user.email,
69
+ message="Account created successfully"
70
+ )
71
+
72
+
73
+ @router.post("/login", response_model=LoginResponse)
74
+ def login(login_data: LoginRequest, response: Response, session: Session = Depends(get_session_dep)):
75
+ """Authenticate user with email and password, return JWT token."""
76
+
77
+ # Find user by email
78
+ user = session.exec(select(User).where(User.email == login_data.email)).first()
79
+
80
+ if not user or not verify_password(login_data.password, user.password_hash):
81
+ raise HTTPException(
82
+ status_code=status.HTTP_401_UNAUTHORIZED,
83
+ detail="Invalid email or password",
84
+ headers={"WWW-Authenticate": "Bearer"},
85
+ )
86
+
87
+ # Create access token
88
+ access_token = create_access_token(data={"sub": str(user.id)})
89
+
90
+ # Set the token as an httpOnly cookie
91
+ response.set_cookie(
92
+ key="access_token",
93
+ value=access_token,
94
+ httponly=True,
95
+ secure=settings.JWT_COOKIE_SECURE, # True in production, False in development
96
+ samesite="lax",
97
+ max_age=settings.ACCESS_TOKEN_EXPIRE_DAYS * 24 * 60 * 60, # Convert days to seconds
98
+ path="/"
99
+ )
100
+
101
+ # Debug: Print the cookie being set
102
+ print(f"Setting cookie: access_token={access_token}")
103
+ print(f"Cookie attributes: httponly={True}, secure={settings.JWT_COOKIE_SECURE}, samesite=lax, max_age={settings.ACCESS_TOKEN_EXPIRE_DAYS * 24 * 60 * 60}")
104
+
105
+ # Return response
106
+ return LoginResponse(
107
+ access_token=access_token,
108
+ token_type="bearer",
109
+ user=RegisterResponse(
110
+ id=user.id,
111
+ email=user.email,
112
+ message="Login successful"
113
+ )
114
+ )
115
+
116
+
117
+ @router.post("/logout")
118
+ def logout(response: Response):
119
+ """Logout user by clearing the access token cookie."""
120
+ # Clear the access_token cookie
121
+ response.set_cookie(
122
+ key="access_token",
123
+ value="",
124
+ httponly=True,
125
+ secure=settings.JWT_COOKIE_SECURE,
126
+ samesite="lax",
127
+ max_age=0, # Expire immediately
128
+ path="/"
129
+ )
130
+
131
+ return {"message": "Logged out successfully"}
132
+
133
+
134
+ @router.get("/me", response_model=RegisterResponse)
135
+ def get_current_user_profile(request: Request, current_user: User = Depends(get_current_user)):
136
+ """Get the current authenticated user's profile."""
137
+ # Debug: Print the cookies received
138
+ print(f"Received cookies: {request.cookies}")
139
+ print(f"Access token cookie: {request.cookies.get('access_token')}")
140
+
141
+ return RegisterResponse(
142
+ id=current_user.id,
143
+ email=current_user.email,
144
+ message="User profile retrieved successfully"
145
+ )
146
+
147
+
148
+ @router.post("/forgot-password")
149
+ def forgot_password(forgot_data: ForgotPasswordRequest, session: Session = Depends(get_session_dep)):
150
+ """Initiate password reset process by verifying email exists."""
151
+ # Check if user exists
152
+ user = session.exec(select(User).where(User.email == forgot_data.email)).first()
153
+
154
+ if not user:
155
+ # For security reasons, we don't reveal if the email exists or not
156
+ return {"message": "If the email exists, a reset link would be sent"}
157
+
158
+ # In a real implementation, we would send an email here
159
+ # But as per requirements, we're just simulating the process
160
+ return {"message": "If the email exists, a reset link would be sent"}
161
+
162
+
163
+ @router.post("/reset-password")
164
+ def reset_password(reset_data: ResetPasswordRequest, session: Session = Depends(get_session_dep)):
165
+ """Reset user password after verification."""
166
+ # Check if user exists
167
+ user = session.exec(select(User).where(User.email == reset_data.email)).first()
168
+
169
+ if not user:
170
+ raise HTTPException(
171
+ status_code=status.HTTP_404_NOT_FOUND,
172
+ detail="User not found"
173
+ )
174
+
175
+ # Validate password length
176
+ if len(reset_data.new_password) < 8:
177
+ raise HTTPException(
178
+ status_code=status.HTTP_400_BAD_REQUEST,
179
+ detail="Password must be at least 8 characters"
180
+ )
181
+
182
+ # Hash the new password
183
+ user.password_hash = hash_password(reset_data.new_password)
184
+
185
+ # Update the user
186
+ session.add(user)
187
+ session.commit()
188
+
189
+ return {"message": "Password reset successfully"}
src/routers/projects.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends
2
+ from sqlmodel import Session, select, and_, func
3
+ from typing import List
4
+ from uuid import UUID
5
+ from datetime import datetime
6
+
7
+ from ..models.user import User
8
+ from ..models.project import Project, ProjectCreate, ProjectUpdate, ProjectRead
9
+ from ..models.task import Task
10
+ from ..database import get_session_dep
11
+ from ..utils.deps import get_current_user
12
+
13
+
14
+ router = APIRouter(prefix="/api/{user_id}/projects", tags=["projects"])
15
+
16
+
17
+ @router.get("/", response_model=List[ProjectRead])
18
+ def list_projects(
19
+ user_id: UUID,
20
+ current_user: User = Depends(get_current_user),
21
+ session: Session = Depends(get_session_dep)
22
+ ):
23
+ """List all projects for the authenticated user."""
24
+
25
+ # Verify that the user_id in the URL matches the authenticated user
26
+ if current_user.id != user_id:
27
+ raise HTTPException(
28
+ status_code=status.HTTP_404_NOT_FOUND,
29
+ detail="Project not found"
30
+ )
31
+
32
+ # Build the query with user_id filter
33
+ query = select(Project).where(Project.user_id == user_id)
34
+
35
+ # Apply ordering (newest first)
36
+ query = query.order_by(Project.created_at.desc())
37
+
38
+ projects = session.exec(query).all()
39
+ return projects
40
+
41
+
42
+ @router.post("/", response_model=ProjectRead, status_code=status.HTTP_201_CREATED)
43
+ def create_project(
44
+ *,
45
+ user_id: UUID,
46
+ project_data: ProjectCreate,
47
+ current_user: User = Depends(get_current_user),
48
+ session: Session = Depends(get_session_dep)
49
+ ):
50
+ """Create a new project for the authenticated user."""
51
+
52
+ # Verify that the user_id in the URL matches the authenticated user
53
+ if current_user.id != user_id:
54
+ raise HTTPException(
55
+ status_code=status.HTTP_404_NOT_FOUND,
56
+ detail="User not found"
57
+ )
58
+
59
+ # Create the project
60
+ project = Project(
61
+ name=project_data.name,
62
+ description=project_data.description,
63
+ color=project_data.color,
64
+ user_id=user_id
65
+ )
66
+
67
+ session.add(project)
68
+ session.commit()
69
+ session.refresh(project)
70
+
71
+ return project
72
+
73
+
74
+ @router.get("/{project_id}", response_model=ProjectRead)
75
+ def get_project(
76
+ *,
77
+ user_id: UUID,
78
+ project_id: UUID,
79
+ current_user: User = Depends(get_current_user),
80
+ session: Session = Depends(get_session_dep)
81
+ ):
82
+ """Get a specific project by ID for the authenticated user."""
83
+
84
+ # Verify that the user_id in the URL matches the authenticated user
85
+ if current_user.id != user_id:
86
+ raise HTTPException(
87
+ status_code=status.HTTP_404_NOT_FOUND,
88
+ detail="Project not found"
89
+ )
90
+
91
+ # Fetch the project
92
+ project = session.get(Project, project_id)
93
+
94
+ # Check if project exists and belongs to the user
95
+ if not project or project.user_id != user_id:
96
+ raise HTTPException(
97
+ status_code=status.HTTP_404_NOT_FOUND,
98
+ detail="Project not found"
99
+ )
100
+
101
+ return project
102
+
103
+
104
+ @router.put("/{project_id}", response_model=ProjectRead)
105
+ def update_project(
106
+ *,
107
+ user_id: UUID,
108
+ project_id: UUID,
109
+ project_data: ProjectUpdate,
110
+ current_user: User = Depends(get_current_user),
111
+ session: Session = Depends(get_session_dep)
112
+ ):
113
+ """Update an existing project for the authenticated user."""
114
+
115
+ # Verify that the user_id in the URL matches the authenticated user
116
+ if current_user.id != user_id:
117
+ raise HTTPException(
118
+ status_code=status.HTTP_404_NOT_FOUND,
119
+ detail="Project not found"
120
+ )
121
+
122
+ # Fetch the project
123
+ project = session.get(Project, project_id)
124
+
125
+ # Check if project exists and belongs to the user
126
+ if not project or project.user_id != user_id:
127
+ raise HTTPException(
128
+ status_code=status.HTTP_404_NOT_FOUND,
129
+ detail="Project not found"
130
+ )
131
+
132
+ # Update the project
133
+ project_data_dict = project_data.dict(exclude_unset=True)
134
+ for key, value in project_data_dict.items():
135
+ setattr(project, key, value)
136
+
137
+ session.add(project)
138
+ session.commit()
139
+ session.refresh(project)
140
+
141
+ return project
142
+
143
+
144
+ @router.delete("/{project_id}")
145
+ def delete_project(
146
+ *,
147
+ user_id: UUID,
148
+ project_id: UUID,
149
+ current_user: User = Depends(get_current_user),
150
+ session: Session = Depends(get_session_dep)
151
+ ):
152
+ """Delete a project for the authenticated user."""
153
+
154
+ # Verify that the user_id in the URL matches the authenticated user
155
+ if current_user.id != user_id:
156
+ raise HTTPException(
157
+ status_code=status.HTTP_404_NOT_FOUND,
158
+ detail="Project not found"
159
+ )
160
+
161
+ # Fetch the project
162
+ project = session.get(Project, project_id)
163
+
164
+ # Check if project exists and belongs to the user
165
+ if not project or project.user_id != user_id:
166
+ raise HTTPException(
167
+ status_code=status.HTTP_404_NOT_FOUND,
168
+ detail="Project not found"
169
+ )
170
+
171
+ # Delete the project
172
+ session.delete(project)
173
+ session.commit()
174
+
175
+ return {"message": "Project deleted successfully"}
176
+
177
+
178
+ @router.get("/{project_id}/tasks", response_model=List[Task])
179
+ def list_project_tasks(
180
+ *,
181
+ user_id: UUID,
182
+ project_id: UUID,
183
+ current_user: User = Depends(get_current_user),
184
+ session: Session = Depends(get_session_dep)
185
+ ):
186
+ """List all tasks for a specific project."""
187
+
188
+ # Verify that the user_id in the URL matches the authenticated user
189
+ if current_user.id != user_id:
190
+ raise HTTPException(
191
+ status_code=status.HTTP_404_NOT_FOUND,
192
+ detail="Project not found"
193
+ )
194
+
195
+ # Fetch the project
196
+ project = session.get(Project, project_id)
197
+
198
+ # Check if project exists and belongs to the user
199
+ if not project or project.user_id != user_id:
200
+ raise HTTPException(
201
+ status_code=status.HTTP_404_NOT_FOUND,
202
+ detail="Project not found"
203
+ )
204
+
205
+ # Build the query with project_id filter
206
+ query = select(Task).where(Task.project_id == project_id)
207
+
208
+ # Apply ordering (newest first)
209
+ query = query.order_by(Task.created_at.desc())
210
+
211
+ tasks = session.exec(query).all()
212
+ return tasks
213
+
214
+
215
+ @router.get("/{project_id}/progress")
216
+ def get_project_progress(
217
+ *,
218
+ user_id: UUID,
219
+ project_id: UUID,
220
+ current_user: User = Depends(get_current_user),
221
+ session: Session = Depends(get_session_dep)
222
+ ):
223
+ """Get progress statistics for a specific project."""
224
+
225
+ # Verify that the user_id in the URL matches the authenticated user
226
+ if current_user.id != user_id:
227
+ raise HTTPException(
228
+ status_code=status.HTTP_404_NOT_FOUND,
229
+ detail="Project not found"
230
+ )
231
+
232
+ # Fetch the project
233
+ project = session.get(Project, project_id)
234
+
235
+ # Check if project exists and belongs to the user
236
+ if not project or project.user_id != user_id:
237
+ raise HTTPException(
238
+ status_code=status.HTTP_404_NOT_FOUND,
239
+ detail="Project not found"
240
+ )
241
+
242
+ # Get task counts
243
+ total_tasks_query = select(func.count()).where(Task.project_id == project_id)
244
+ completed_tasks_query = select(func.count()).where(and_(Task.project_id == project_id, Task.completed == True))
245
+
246
+ total_tasks = session.exec(total_tasks_query).first()
247
+ completed_tasks = session.exec(completed_tasks_query).first()
248
+
249
+ # Calculate progress
250
+ progress = 0
251
+ if total_tasks > 0:
252
+ progress = round((completed_tasks / total_tasks) * 100, 2)
253
+
254
+ return {
255
+ "total_tasks": total_tasks,
256
+ "completed_tasks": completed_tasks,
257
+ "pending_tasks": total_tasks - completed_tasks,
258
+ "progress": progress
259
+ }
src/routers/tasks.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends
2
+ from sqlmodel import Session, select, and_, func
3
+ from typing import List
4
+ from uuid import UUID
5
+ from datetime import datetime
6
+
7
+ from ..models.user import User
8
+ from ..models.task import Task, TaskCreate, TaskUpdate, TaskRead
9
+ from ..schemas.task import TaskListResponse
10
+ from ..database import get_session_dep
11
+ from ..utils.deps import get_current_user
12
+
13
+
14
+ router = APIRouter(prefix="/api/{user_id}/tasks", tags=["tasks"])
15
+
16
+
17
+ @router.get("/", response_model=TaskListResponse)
18
+ def list_tasks(
19
+ user_id: UUID,
20
+ current_user: User = Depends(get_current_user),
21
+ session: Session = Depends(get_session_dep),
22
+ completed: bool = None,
23
+ offset: int = 0,
24
+ limit: int = 50
25
+ ):
26
+ """List all tasks for the authenticated user with optional filtering."""
27
+
28
+ # Verify that the user_id in the URL matches the authenticated user
29
+ if current_user.id != user_id:
30
+ raise HTTPException(
31
+ status_code=status.HTTP_404_NOT_FOUND,
32
+ detail="Task not found"
33
+ )
34
+
35
+ # Build the query with user_id filter
36
+ query = select(Task).where(Task.user_id == user_id)
37
+
38
+ # Apply completed filter if specified
39
+ if completed is not None:
40
+ query = query.where(Task.completed == completed)
41
+
42
+ # Apply ordering (newest first)
43
+ query = query.order_by(Task.created_at.desc())
44
+
45
+ # Apply pagination
46
+ query = query.offset(offset).limit(limit)
47
+
48
+ tasks = session.exec(query).all()
49
+
50
+ # Get total count for pagination info
51
+ total_query = select(func.count()).select_from(Task).where(Task.user_id == user_id)
52
+ if completed is not None:
53
+ total_query = total_query.where(Task.completed == completed)
54
+ total = session.exec(total_query).one()
55
+
56
+ # Convert to response format
57
+ task_responses = []
58
+ for task in tasks:
59
+ task_dict = {
60
+ "id": task.id,
61
+ "user_id": str(task.user_id),
62
+ "title": task.title,
63
+ "description": task.description,
64
+ "completed": task.completed,
65
+ "due_date": task.due_date.isoformat() if task.due_date else None,
66
+ "project_id": str(task.project_id) if task.project_id else None,
67
+ "created_at": task.created_at.isoformat(),
68
+ "updated_at": task.updated_at.isoformat()
69
+ }
70
+ task_responses.append(task_dict)
71
+
72
+ return TaskListResponse(
73
+ tasks=task_responses,
74
+ total=total,
75
+ offset=offset,
76
+ limit=limit
77
+ )
78
+
79
+
80
+ @router.post("/", response_model=TaskRead, status_code=status.HTTP_201_CREATED)
81
+ def create_task(
82
+ user_id: UUID,
83
+ task_data: TaskCreate,
84
+ current_user: User = Depends(get_current_user),
85
+ session: Session = Depends(get_session_dep)
86
+ ):
87
+ """Create a new task for the authenticated user."""
88
+
89
+ # Verify that the user_id in the URL matches the authenticated user
90
+ if current_user.id != user_id:
91
+ raise HTTPException(
92
+ status_code=status.HTTP_404_NOT_FOUND,
93
+ detail="User not found"
94
+ )
95
+
96
+ # Validate title length
97
+ if len(task_data.title) < 1 or len(task_data.title) > 200:
98
+ raise HTTPException(
99
+ status_code=status.HTTP_400_BAD_REQUEST,
100
+ detail="Title must be between 1 and 200 characters"
101
+ )
102
+
103
+ # Validate description length if provided
104
+ if task_data.description and len(task_data.description) > 1000:
105
+ raise HTTPException(
106
+ status_code=status.HTTP_400_BAD_REQUEST,
107
+ detail="Description must be 1000 characters or less"
108
+ )
109
+
110
+ # Create new task
111
+ task = Task(
112
+ title=task_data.title,
113
+ description=task_data.description,
114
+ completed=task_data.completed,
115
+ due_date=task_data.due_date,
116
+ project_id=task_data.project_id,
117
+ user_id=user_id
118
+ )
119
+
120
+ session.add(task)
121
+ session.commit()
122
+ session.refresh(task)
123
+
124
+ return TaskRead(
125
+ id=task.id,
126
+ user_id=task.user_id,
127
+ title=task.title,
128
+ description=task.description,
129
+ completed=task.completed,
130
+ due_date=task.due_date,
131
+ project_id=task.project_id,
132
+ created_at=task.created_at,
133
+ updated_at=task.updated_at
134
+ )
135
+
136
+
137
+ @router.get("/{task_id}", response_model=TaskRead)
138
+ def get_task(
139
+ user_id: UUID,
140
+ task_id: int,
141
+ current_user: User = Depends(get_current_user),
142
+ session: Session = Depends(get_session_dep)
143
+ ):
144
+ """Get a specific task by ID for the authenticated user."""
145
+
146
+ # Verify that the user_id in the URL matches the authenticated user
147
+ if current_user.id != user_id:
148
+ raise HTTPException(
149
+ status_code=status.HTTP_404_NOT_FOUND,
150
+ detail="Task not found"
151
+ )
152
+
153
+ # Get the task
154
+ task = session.get(Task, task_id)
155
+
156
+ # Verify the task exists and belongs to the user
157
+ if not task or task.user_id != user_id:
158
+ raise HTTPException(
159
+ status_code=status.HTTP_404_NOT_FOUND,
160
+ detail="Task not found"
161
+ )
162
+
163
+ return TaskRead(
164
+ id=task.id,
165
+ user_id=task.user_id,
166
+ title=task.title,
167
+ description=task.description,
168
+ completed=task.completed,
169
+ due_date=task.due_date,
170
+ project_id=task.project_id,
171
+ created_at=task.created_at,
172
+ updated_at=task.updated_at
173
+ )
174
+
175
+
176
+ @router.put("/{task_id}", response_model=TaskRead)
177
+ def update_task(
178
+ user_id: UUID,
179
+ task_id: int,
180
+ task_data: TaskUpdate,
181
+ current_user: User = Depends(get_current_user),
182
+ session: Session = Depends(get_session_dep)
183
+ ):
184
+ """Update an existing task for the authenticated user."""
185
+
186
+ # Verify that the user_id in the URL matches the authenticated user
187
+ if current_user.id != user_id:
188
+ raise HTTPException(
189
+ status_code=status.HTTP_404_NOT_FOUND,
190
+ detail="Task not found"
191
+ )
192
+
193
+ # Get the task
194
+ task = session.get(Task, task_id)
195
+
196
+ # Verify the task exists and belongs to the user
197
+ if not task or task.user_id != user_id:
198
+ raise HTTPException(
199
+ status_code=status.HTTP_404_NOT_FOUND,
200
+ detail="Task not found"
201
+ )
202
+
203
+ # Update fields if provided
204
+ if task_data.title is not None:
205
+ if len(task_data.title) < 1 or len(task_data.title) > 200:
206
+ raise HTTPException(
207
+ status_code=status.HTTP_400_BAD_REQUEST,
208
+ detail="Title must be between 1 and 200 characters"
209
+ )
210
+ task.title = task_data.title
211
+
212
+ if task_data.description is not None:
213
+ if len(task_data.description) > 1000:
214
+ raise HTTPException(
215
+ status_code=status.HTTP_400_BAD_REQUEST,
216
+ detail="Description must be 1000 characters or less"
217
+ )
218
+ task.description = task_data.description
219
+
220
+ if task_data.completed is not None:
221
+ task.completed = task_data.completed
222
+
223
+ if task_data.due_date is not None:
224
+ task.due_date = task_data.due_date
225
+
226
+ if task_data.project_id is not None:
227
+ task.project_id = task_data.project_id
228
+
229
+ # Update the timestamp
230
+ task.updated_at = datetime.utcnow()
231
+
232
+ session.add(task)
233
+ session.commit()
234
+ session.refresh(task)
235
+
236
+ return TaskRead(
237
+ id=task.id,
238
+ user_id=task.user_id,
239
+ title=task.title,
240
+ description=task.description,
241
+ completed=task.completed,
242
+ due_date=task.due_date,
243
+ project_id=task.project_id,
244
+ created_at=task.created_at,
245
+ updated_at=task.updated_at
246
+ )
247
+
248
+
249
+ @router.patch("/{task_id}", response_model=TaskRead)
250
+ def patch_task(
251
+ user_id: UUID,
252
+ task_id: int,
253
+ task_data: TaskUpdate,
254
+ current_user: User = Depends(get_current_user),
255
+ session: Session = Depends(get_session_dep)
256
+ ):
257
+ """Partially update an existing task for the authenticated user."""
258
+
259
+ # Verify that the user_id in the URL matches the authenticated user
260
+ if current_user.id != user_id:
261
+ raise HTTPException(
262
+ status_code=status.HTTP_404_NOT_FOUND,
263
+ detail="Task not found"
264
+ )
265
+
266
+ # Get the task
267
+ task = session.get(Task, task_id)
268
+
269
+ # Verify the task exists and belongs to the user
270
+ if not task or task.user_id != user_id:
271
+ raise HTTPException(
272
+ status_code=status.HTTP_404_NOT_FOUND,
273
+ detail="Task not found"
274
+ )
275
+
276
+ # Update fields if provided
277
+ if task_data.title is not None:
278
+ if len(task_data.title) < 1 or len(task_data.title) > 200:
279
+ raise HTTPException(
280
+ status_code=status.HTTP_400_BAD_REQUEST,
281
+ detail="Title must be between 1 and 200 characters"
282
+ )
283
+ task.title = task_data.title
284
+
285
+ if task_data.description is not None:
286
+ if len(task_data.description) > 1000:
287
+ raise HTTPException(
288
+ status_code=status.HTTP_400_BAD_REQUEST,
289
+ detail="Description must be 1000 characters or less"
290
+ )
291
+ task.description = task_data.description
292
+
293
+ if task_data.completed is not None:
294
+ task.completed = task_data.completed
295
+
296
+ if task_data.due_date is not None:
297
+ task.due_date = task_data.due_date
298
+
299
+ if task_data.project_id is not None:
300
+ task.project_id = task_data.project_id
301
+
302
+ # Update the timestamp
303
+ task.updated_at = datetime.utcnow()
304
+
305
+ session.add(task)
306
+ session.commit()
307
+ session.refresh(task)
308
+
309
+ return TaskRead(
310
+ id=task.id,
311
+ user_id=task.user_id,
312
+ title=task.title,
313
+ description=task.description,
314
+ completed=task.completed,
315
+ due_date=task.due_date,
316
+ project_id=task.project_id,
317
+ created_at=task.created_at,
318
+ updated_at=task.updated_at
319
+ )
320
+
321
+
322
+ @router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
323
+ def delete_task(
324
+ user_id: UUID,
325
+ task_id: int,
326
+ current_user: User = Depends(get_current_user),
327
+ session: Session = Depends(get_session_dep)
328
+ ):
329
+ """Delete a task for the authenticated user."""
330
+
331
+ # Verify that the user_id in the URL matches the authenticated user
332
+ if current_user.id != user_id:
333
+ raise HTTPException(
334
+ status_code=status.HTTP_404_NOT_FOUND,
335
+ detail="Task not found"
336
+ )
337
+
338
+ # Get the task
339
+ task = session.get(Task, task_id)
340
+
341
+ # Verify the task exists and belongs to the user
342
+ if not task or task.user_id != user_id:
343
+ raise HTTPException(
344
+ status_code=status.HTTP_404_NOT_FOUND,
345
+ detail="Task not found"
346
+ )
347
+
348
+ session.delete(task)
349
+ session.commit()
350
+
351
+ # Return 204 No Content
352
+ return
353
+
354
+
355
+ @router.patch("/{task_id}/toggle", response_model=TaskRead)
356
+ def toggle_task_completion(
357
+ user_id: UUID,
358
+ task_id: int,
359
+ current_user: User = Depends(get_current_user),
360
+ session: Session = Depends(get_session_dep)
361
+ ):
362
+ """Toggle the completion status of a task."""
363
+
364
+ # Verify that the user_id in the URL matches the authenticated user
365
+ if current_user.id != user_id:
366
+ raise HTTPException(
367
+ status_code=status.HTTP_404_NOT_FOUND,
368
+ detail="Task not found"
369
+ )
370
+
371
+ # Get the task
372
+ task = session.get(Task, task_id)
373
+
374
+ # Verify the task exists and belongs to the user
375
+ if not task or task.user_id != user_id:
376
+ raise HTTPException(
377
+ status_code=status.HTTP_404_NOT_FOUND,
378
+ detail="Task not found"
379
+ )
380
+
381
+ # Toggle the completion status
382
+ task.completed = not task.completed
383
+ task.updated_at = datetime.utcnow()
384
+
385
+ session.add(task)
386
+ session.commit()
387
+ session.refresh(task)
388
+
389
+ return TaskRead(
390
+ id=task.id,
391
+ user_id=task.user_id,
392
+ title=task.title,
393
+ description=task.description,
394
+ completed=task.completed,
395
+ created_at=task.created_at,
396
+ updated_at=task.updated_at
397
+ )
src/schemas/__pycache__/auth.cpython-312.pyc ADDED
Binary file (1.96 kB). View file
 
src/schemas/__pycache__/task.cpython-312.pyc ADDED
Binary file (1.95 kB). View file
 
src/schemas/auth.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr
2
+ from typing import Optional
3
+ from datetime import datetime
4
+ from uuid import UUID
5
+
6
+
7
+ class RegisterRequest(BaseModel):
8
+ email: EmailStr
9
+ password: str
10
+
11
+
12
+ class RegisterResponse(BaseModel):
13
+ id: UUID
14
+ email: EmailStr
15
+ message: str
16
+
17
+
18
+ class LoginRequest(BaseModel):
19
+ email: EmailStr
20
+ password: str
21
+
22
+
23
+ class LoginResponse(BaseModel):
24
+ access_token: str
25
+ token_type: str
26
+ user: RegisterResponse
27
+
28
+
29
+ class ErrorResponse(BaseModel):
30
+ detail: str
31
+ status_code: Optional[int] = None
32
+ errors: Optional[list] = None
33
+
34
+
35
+ class ForgotPasswordRequest(BaseModel):
36
+ email: EmailStr
37
+
38
+
39
+ class ResetPasswordRequest(BaseModel):
40
+ email: EmailStr
41
+ new_password: str
src/schemas/task.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
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
9
+ description: Optional[str] = None
10
+ completed: bool = False
11
+ due_date: Optional[datetime] = None
12
+ project_id: Optional[UUID] = None
13
+
14
+
15
+ class TaskCreate(TaskBase):
16
+ title: str
17
+ description: Optional[str] = None
18
+
19
+
20
+ class TaskUpdate(BaseModel):
21
+ title: Optional[str] = None
22
+ description: Optional[str] = None
23
+ completed: Optional[bool] = None
24
+
25
+
26
+ class TaskRead(TaskBase):
27
+ id: int
28
+ user_id: UUID
29
+ due_date: Optional[datetime] = None
30
+ project_id: Optional[UUID] = None
31
+ created_at: datetime
32
+ updated_at: datetime
33
+
34
+
35
+ class TaskListResponse(BaseModel):
36
+ tasks: List[TaskRead]
37
+ total: int
38
+ offset: int
39
+ limit: int
src/task_api.egg-info/PKG-INFO ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: task-api
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: alembic>=1.17.2
8
+ Requires-Dist: fastapi>=0.124.4
9
+ Requires-Dist: passlib[bcrypt]>=1.7.4
10
+ Requires-Dist: psycopg2-binary>=2.9.11
11
+ Requires-Dist: pydantic-settings>=2.12.0
12
+ Requires-Dist: pydantic[email]>=2.12.5
13
+ Requires-Dist: python-jose[cryptography]>=3.5.0
14
+ Requires-Dist: python-multipart>=0.0.20
15
+ Requires-Dist: sqlmodel>=0.0.27
16
+ Requires-Dist: uvicorn>=0.38.0
src/task_api.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ pyproject.toml
3
+ src/__init__.py
4
+ src/config.py
5
+ src/database.py
6
+ src/main.py
7
+ src/middleware/auth.py
8
+ src/models/task.py
9
+ src/models/user.py
10
+ src/routers/__init__.py
11
+ src/routers/auth.py
12
+ src/routers/tasks.py
13
+ src/schemas/auth.py
14
+ src/schemas/task.py
15
+ src/task_api.egg-info/PKG-INFO
16
+ src/task_api.egg-info/SOURCES.txt
17
+ src/task_api.egg-info/dependency_links.txt
18
+ src/task_api.egg-info/requires.txt
19
+ src/task_api.egg-info/top_level.txt
20
+ src/utils/deps.py
21
+ src/utils/security.py
src/task_api.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
src/task_api.egg-info/requires.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ alembic>=1.17.2
2
+ fastapi>=0.124.4
3
+ passlib[bcrypt]>=1.7.4
4
+ psycopg2-binary>=2.9.11
5
+ pydantic-settings>=2.12.0
6
+ pydantic[email]>=2.12.5
7
+ python-jose[cryptography]>=3.5.0
8
+ python-multipart>=0.0.20
9
+ sqlmodel>=0.0.27
10
+ uvicorn>=0.38.0
src/task_api.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/__pycache__/deps.cpython-312.pyc ADDED
Binary file (2.46 kB). View file
 
src/utils/__pycache__/security.cpython-312.pyc ADDED
Binary file (2.93 kB). View file
 
src/utils/deps.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Depends, HTTPException, status, Request
2
+ from sqlmodel import Session
3
+ from typing import Generator
4
+ from ..database import get_session_dep
5
+ from ..models.user import User
6
+ from .security import verify_user_id_from_token
7
+ from uuid import UUID
8
+
9
+
10
+ def get_current_user(
11
+ request: Request,
12
+ session: Session = Depends(get_session_dep)
13
+ ) -> User:
14
+ """Dependency to get the current authenticated user from JWT token in cookie."""
15
+ # Debug: Print all cookies
16
+ print(f"All cookies received: {request.cookies}")
17
+
18
+ # Get the token from the cookie
19
+ token = request.cookies.get("access_token")
20
+ print(f"Access token from cookie: {token}")
21
+
22
+ if not token:
23
+ print("No access token found in cookies")
24
+ raise HTTPException(
25
+ status_code=status.HTTP_401_UNAUTHORIZED,
26
+ detail="Not authenticated",
27
+ headers={"WWW-Authenticate": "Bearer"},
28
+ )
29
+
30
+ user_id = verify_user_id_from_token(token)
31
+ print(f"User ID from token: {user_id}")
32
+
33
+ if not user_id:
34
+ print("Invalid user ID from token")
35
+ raise HTTPException(
36
+ status_code=status.HTTP_401_UNAUTHORIZED,
37
+ detail="Invalid authentication credentials",
38
+ headers={"WWW-Authenticate": "Bearer"},
39
+ )
40
+
41
+ user = session.get(User, user_id)
42
+ print(f"User from database: {user}")
43
+
44
+ if not user:
45
+ print("User not found in database")
46
+ raise HTTPException(
47
+ status_code=status.HTTP_401_UNAUTHORIZED,
48
+ detail="Invalid authentication credentials",
49
+ headers={"WWW-Authenticate": "Bearer"},
50
+ )
51
+
52
+ return user
53
+
54
+
55
+ def get_user_by_id(
56
+ user_id: UUID,
57
+ session: Session = Depends(get_session_dep)
58
+ ) -> User:
59
+ """Dependency to get a user by ID from the database."""
60
+ user = session.get(User, user_id)
61
+ if not user:
62
+ raise HTTPException(
63
+ status_code=status.HTTP_404_NOT_FOUND,
64
+ detail="User not found"
65
+ )
66
+ return user