Add user authentication system with JWT tokens

- Add user model with relationship to tasks
- Implement JWT token authentication
- Create user registration and login endpoints
- Update task endpoints to filter by current user
- Add Alembic migration for user table
- Update documentation with authentication details
This commit is contained in:
Automated Action 2025-05-16 12:40:03 +00:00
parent a4cc5dbcca
commit 0ceeef31a6
26 changed files with 1237 additions and 476 deletions

121
README.md
View File

@ -4,7 +4,9 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
## Features ## Features
- Task CRUD operations - User authentication with JWT tokens
- User registration and login
- Task CRUD operations with user-based access control
- Task status and priority management - Task status and priority management
- Task completion tracking - Task completion tracking
- API documentation with Swagger UI and ReDoc - API documentation with Swagger UI and ReDoc
@ -18,6 +20,9 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
- SQLite: Lightweight relational database - SQLite: Lightweight relational database
- Pydantic: Data validation and settings management - Pydantic: Data validation and settings management
- Uvicorn: ASGI server for FastAPI applications - Uvicorn: ASGI server for FastAPI applications
- JWT: JSON Web Tokens for authentication
- Passlib: Password hashing and verification
- Python-Jose: Python implementation of JWT
## API Endpoints ## API Endpoints
@ -25,14 +30,21 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
- `GET /`: Get API information and available endpoints - `GET /`: Get API information and available endpoints
### Task Management ### Authentication
- `GET /tasks`: Get all tasks - `POST /auth/register`: Register a new user
- `POST /tasks`: Create a new task - `POST /auth/login`: Login to get access token
- `GET /tasks/{task_id}`: Get a specific task - `GET /auth/me`: Get current user information
- `PUT /tasks/{task_id}`: Update a task - `POST /auth/test-token`: Test if the access token is valid
- `DELETE /tasks/{task_id}`: Delete a task
- `POST /tasks/{task_id}/complete`: Mark a task as completed ### Task Management (requires authentication)
- `GET /tasks`: Get all tasks for the current user
- `POST /tasks`: Create a new task for the current user
- `GET /tasks/{task_id}`: Get a specific task for the current user
- `PUT /tasks/{task_id}`: Update a task for the current user
- `DELETE /tasks/{task_id}`: Delete a task for the current user
- `POST /tasks/{task_id}/complete`: Mark a task as completed for the current user
### Health and Diagnostic Endpoints ### Health and Diagnostic Endpoints
@ -41,12 +53,37 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
## Example Curl Commands ## Example Curl Commands
### Create a new task ### Register a new user
```bash
curl -X 'POST' \
'https://taskmanagerapi-ttkjqk.backend.im/auth/register' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"username": "testuser",
"password": "password123"
}'
```
### Login to get access token
```bash
curl -X 'POST' \
'https://taskmanagerapi-ttkjqk.backend.im/auth/login' \
-H 'accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=user@example.com&password=password123'
```
### Create a new task (with authentication)
```bash ```bash
curl -X 'POST' \ curl -X 'POST' \
'https://taskmanagerapi-ttkjqk.backend.im/tasks/' \ 'https://taskmanagerapi-ttkjqk.backend.im/tasks/' \
-H 'accept: application/json' \ -H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{ -d '{
"title": "Complete project documentation", "title": "Complete project documentation",
@ -58,28 +95,31 @@ curl -X 'POST' \
}' }'
``` ```
### Get all tasks ### Get all tasks (with authentication)
```bash ```bash
curl -X 'GET' \ curl -X 'GET' \
'https://taskmanagerapi-ttkjqk.backend.im/tasks/' \ 'https://taskmanagerapi-ttkjqk.backend.im/tasks/' \
-H 'accept: application/json' -H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
``` ```
### Get a specific task ### Get a specific task (with authentication)
```bash ```bash
curl -X 'GET' \ curl -X 'GET' \
'https://taskmanagerapi-ttkjqk.backend.im/tasks/1' \ 'https://taskmanagerapi-ttkjqk.backend.im/tasks/1' \
-H 'accept: application/json' -H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
``` ```
### Update a task ### Update a task (with authentication)
```bash ```bash
curl -X 'PUT' \ curl -X 'PUT' \
'https://taskmanagerapi-ttkjqk.backend.im/tasks/1' \ 'https://taskmanagerapi-ttkjqk.backend.im/tasks/1' \
-H 'accept: application/json' \ -H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{ -d '{
"title": "Updated title", "title": "Updated title",
@ -88,20 +128,31 @@ curl -X 'PUT' \
}' }'
``` ```
### Delete a task ### Delete a task (with authentication)
```bash ```bash
curl -X 'DELETE' \ curl -X 'DELETE' \
'https://taskmanagerapi-ttkjqk.backend.im/tasks/1' \ 'https://taskmanagerapi-ttkjqk.backend.im/tasks/1' \
-H 'accept: application/json' -H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
``` ```
### Mark a task as completed ### Mark a task as completed (with authentication)
```bash ```bash
curl -X 'POST' \ curl -X 'POST' \
'https://taskmanagerapi-ttkjqk.backend.im/tasks/1/complete' \ 'https://taskmanagerapi-ttkjqk.backend.im/tasks/1/complete' \
-H 'accept: application/json' -H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```
### Get current user information (with authentication)
```bash
curl -X 'GET' \
'https://taskmanagerapi-ttkjqk.backend.im/auth/me' \
-H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
``` ```
## Project Structure ## Project Structure
@ -112,12 +163,28 @@ taskmanagerapi/
│ └── versions/ # Migration scripts │ └── versions/ # Migration scripts
├── app/ ├── app/
│ ├── api/ # API endpoints │ ├── api/ # API endpoints
│ │ ├── deps.py # Dependency injection for authentication
│ │ └── routers/ # API route definitions │ │ └── routers/ # API route definitions
│ │ ├── auth.py # Authentication endpoints
│ │ └── tasks.py # Task management endpoints
│ ├── core/ # Core application code │ ├── core/ # Core application code
│ │ ├── config.py # Application configuration
│ │ └── security.py # Security utilities (JWT, password hashing)
│ ├── crud/ # CRUD operations │ ├── crud/ # CRUD operations
│ ├── db/ # Database setup and models │ │ ├── base.py # Base CRUD operations
│ │ ├── task.py # Task CRUD operations
│ │ └── user.py # User CRUD operations
│ ├── db/ # Database setup
│ │ ├── base.py # Base imports for models
│ │ ├── base_class.py # Base class for SQLAlchemy models
│ │ ├── init_db.py # Database initialization
│ │ └── session.py # Database session management
│ ├── models/ # SQLAlchemy models │ ├── models/ # SQLAlchemy models
│ │ ├── task.py # Task model
│ │ └── user.py # User model
│ └── schemas/ # Pydantic schemas/models │ └── schemas/ # Pydantic schemas/models
│ ├── task.py # Task schemas
│ └── user.py # User and authentication schemas
├── main.py # Application entry point ├── main.py # Application entry point
└── requirements.txt # Project dependencies └── requirements.txt # Project dependencies
``` ```
@ -183,6 +250,22 @@ uvicorn main:app --reload
The API will be available at http://localhost:8000 The API will be available at http://localhost:8000
### Default Admin User
During initialization, the system creates a default admin user:
- Email: admin@example.com
- Username: admin
- Password: adminpassword
You can use these credentials to authenticate and get started right away.
### Authentication Flow
1. **Register a new user**: Send a POST request to `/auth/register` with email, username, and password
2. **Login**: Send a POST request to `/auth/login` with username/email and password to receive an access token
3. **Use token**: Include the token in your requests as a Bearer token in the Authorization header
4. **Access protected endpoints**: Use the token to access protected task management endpoints
### API Documentation ### API Documentation
- Swagger UI: http://localhost:8000/docs - Swagger UI: http://localhost:8000/docs

View File

@ -11,7 +11,7 @@ from sqlalchemy import pool
from alembic import context from alembic import context
# Add the parent directory to the Python path # Add the parent directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
@ -29,27 +29,27 @@ db_url = config.get_main_option("sqlalchemy.url")
if db_url.startswith("sqlite:///"): if db_url.startswith("sqlite:///"):
# Extract the path part after sqlite:/// # Extract the path part after sqlite:///
if db_url.startswith("sqlite:////"): # Absolute path (4 slashes) if db_url.startswith("sqlite:////"): # Absolute path (4 slashes)
db_path = db_url[len("sqlite:///"):] db_path = db_url[len("sqlite:///") :]
else: # Relative path (3 slashes) else: # Relative path (3 slashes)
db_path = db_url[len("sqlite:///"):] db_path = db_url[len("sqlite:///") :]
# Get the directory path # Get the directory path
db_dir = os.path.dirname(db_path) db_dir = os.path.dirname(db_path)
logger.info(f"Database URL: {db_url}") logger.info(f"Database URL: {db_url}")
logger.info(f"Database path: {db_path}") logger.info(f"Database path: {db_path}")
logger.info(f"Database directory: {db_dir}") logger.info(f"Database directory: {db_dir}")
# Create directory if it doesn't exist # Create directory if it doesn't exist
try: try:
os.makedirs(db_dir, exist_ok=True) os.makedirs(db_dir, exist_ok=True)
logger.info(f"Ensured database directory exists: {db_dir}") logger.info(f"Ensured database directory exists: {db_dir}")
# Test if we can create the database file # Test if we can create the database file
try: try:
# Try to touch the database file # Try to touch the database file
Path(db_path).touch(exist_ok=True) Path(db_path).touch(exist_ok=True)
logger.info(f"Database file is accessible: {db_path}") logger.info(f"Database file is accessible: {db_path}")
# Test direct SQLite connection # Test direct SQLite connection
try: try:
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
@ -66,6 +66,7 @@ if db_url.startswith("sqlite:///"):
# add your model's MetaData object here # add your model's MetaData object here
# for 'autogenerate' support # for 'autogenerate' support
from app.db.base import Base # noqa from app.db.base import Base # noqa
target_metadata = Base.metadata target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py, # other values from the config, defined by the needs of env.py,
@ -89,7 +90,7 @@ def run_migrations_offline():
try: try:
url = config.get_main_option("sqlalchemy.url") url = config.get_main_option("sqlalchemy.url")
logger.info(f"Running offline migrations using URL: {url}") logger.info(f"Running offline migrations using URL: {url}")
context.configure( context.configure(
url=url, url=url,
target_metadata=target_metadata, target_metadata=target_metadata,
@ -101,7 +102,7 @@ def run_migrations_offline():
logger.info("Running offline migrations...") logger.info("Running offline migrations...")
context.run_migrations() context.run_migrations()
logger.info("Offline migrations completed successfully") logger.info("Offline migrations completed successfully")
except Exception as e: except Exception as e:
logger.error(f"Offline migration error: {e}") logger.error(f"Offline migration error: {e}")
# Re-raise the error # Re-raise the error
@ -120,11 +121,11 @@ def run_migrations_online():
cfg = config.get_section(config.config_ini_section) cfg = config.get_section(config.config_ini_section)
url = cfg.get("sqlalchemy.url") url = cfg.get("sqlalchemy.url")
logger.info(f"Running online migrations using URL: {url}") logger.info(f"Running online migrations using URL: {url}")
# Create engine with retry logic # Create engine with retry logic
max_retries = 3 max_retries = 3
last_error = None last_error = None
for retry in range(max_retries): for retry in range(max_retries):
try: try:
logger.info(f"Connection attempt {retry + 1}/{max_retries}") logger.info(f"Connection attempt {retry + 1}/{max_retries}")
@ -133,13 +134,13 @@ def run_migrations_online():
prefix="sqlalchemy.", prefix="sqlalchemy.",
poolclass=pool.NullPool, poolclass=pool.NullPool,
) )
# Configure SQLite for better reliability # Configure SQLite for better reliability
@event.listens_for(connectable, "connect") @event.listens_for(connectable, "connect")
def setup_sqlite_connection(dbapi_connection, connection_record): def setup_sqlite_connection(dbapi_connection, connection_record):
dbapi_connection.execute("PRAGMA journal_mode=WAL") dbapi_connection.execute("PRAGMA journal_mode=WAL")
dbapi_connection.execute("PRAGMA synchronous=NORMAL") dbapi_connection.execute("PRAGMA synchronous=NORMAL")
# Connect and run migrations # Connect and run migrations
with connectable.connect() as connection: with connectable.connect() as connection:
logger.info("Connection successful") logger.info("Connection successful")
@ -152,35 +153,39 @@ def run_migrations_online():
context.run_migrations() context.run_migrations()
logger.info("Migrations completed successfully") logger.info("Migrations completed successfully")
return # Success, exit the function return # Success, exit the function
except Exception as e: except Exception as e:
last_error = e last_error = e
logger.error(f"Migration attempt {retry + 1} failed: {e}") logger.error(f"Migration attempt {retry + 1} failed: {e}")
if retry < max_retries - 1: if retry < max_retries - 1:
import time import time
wait_time = (retry + 1) * 2 # Exponential backoff wait_time = (retry + 1) * 2 # Exponential backoff
logger.info(f"Retrying in {wait_time} seconds...") logger.info(f"Retrying in {wait_time} seconds...")
time.sleep(wait_time) time.sleep(wait_time)
# If we get here, all retries failed # If we get here, all retries failed
raise Exception(f"Failed to run migrations after {max_retries} attempts: {last_error}") raise Exception(
f"Failed to run migrations after {max_retries} attempts: {last_error}"
)
except Exception as e: except Exception as e:
logger.error(f"Migration error: {e}") logger.error(f"Migration error: {e}")
# Print diagnostic information # Print diagnostic information
from sqlalchemy import __version__ as sa_version from sqlalchemy import __version__ as sa_version
logger.error(f"SQLAlchemy version: {sa_version}") logger.error(f"SQLAlchemy version: {sa_version}")
# Get directory info # Get directory info
if url and url.startswith("sqlite:///"): if url and url.startswith("sqlite:///"):
if url.startswith("sqlite:////"): # Absolute path if url.startswith("sqlite:////"): # Absolute path
db_path = url[len("sqlite:///"):] db_path = url[len("sqlite:///") :]
else: # Relative path else: # Relative path
db_path = url[len("sqlite:///"):] db_path = url[len("sqlite:///") :]
db_dir = os.path.dirname(db_path) db_dir = os.path.dirname(db_path)
# Directory permissions # Directory permissions
if os.path.exists(db_dir): if os.path.exists(db_dir):
stats = os.stat(db_dir) stats = os.stat(db_dir)
@ -189,7 +194,7 @@ def run_migrations_online():
logger.error(f"DB directory is writable: {os.access(db_dir, os.W_OK)}") logger.error(f"DB directory is writable: {os.access(db_dir, os.W_OK)}")
else: else:
logger.error("DB directory exists: No") logger.error("DB directory exists: No")
# File permissions if file exists # File permissions if file exists
if os.path.exists(db_path): if os.path.exists(db_path):
stats = os.stat(db_path) stats = os.stat(db_path)
@ -198,7 +203,7 @@ def run_migrations_online():
logger.error(f"DB file is writable: {os.access(db_path, os.W_OK)}") logger.error(f"DB file is writable: {os.access(db_path, os.W_OK)}")
else: else:
logger.error("DB file exists: No") logger.error("DB file exists: No")
# Re-raise the error # Re-raise the error
raise raise
@ -206,4 +211,4 @@ def run_migrations_online():
if context.is_offline_mode(): if context.is_offline_mode():
run_migrations_offline() run_migrations_offline()
else: else:
run_migrations_online() run_migrations_online()

View File

@ -1,16 +1,16 @@
"""create tasks table """create tasks table
Revision ID: 0001 Revision ID: 0001
Revises: Revises:
Create Date: 2025-05-14 Create Date: 2025-05-14
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '0001' revision = "0001"
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -18,21 +18,31 @@ depends_on = None
def upgrade(): def upgrade():
op.create_table( op.create_table(
'task', "task",
sa.Column('id', sa.Integer(), nullable=False), sa.Column("id", sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False), sa.Column("title", sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True), sa.Column("description", sa.Text(), nullable=True),
sa.Column('priority', sa.Enum('low', 'medium', 'high', name='taskpriority'), default='medium'), sa.Column(
sa.Column('status', sa.Enum('todo', 'in_progress', 'done', name='taskstatus'), default='todo'), "priority",
sa.Column('due_date', sa.DateTime(), nullable=True), sa.Enum("low", "medium", "high", name="taskpriority"),
sa.Column('completed', sa.Boolean(), default=False), default="medium",
sa.Column('created_at', sa.DateTime(), default=sa.func.now()), ),
sa.Column('updated_at', sa.DateTime(), default=sa.func.now(), onupdate=sa.func.now()), sa.Column(
sa.PrimaryKeyConstraint('id') "status",
sa.Enum("todo", "in_progress", "done", name="taskstatus"),
default="todo",
),
sa.Column("due_date", sa.DateTime(), nullable=True),
sa.Column("completed", sa.Boolean(), default=False),
sa.Column("created_at", sa.DateTime(), default=sa.func.now()),
sa.Column(
"updated_at", sa.DateTime(), default=sa.func.now(), onupdate=sa.func.now()
),
sa.PrimaryKeyConstraint("id"),
) )
op.create_index(op.f('ix_task_id'), 'task', ['id'], unique=False) op.create_index(op.f("ix_task_id"), "task", ["id"], unique=False)
def downgrade(): def downgrade():
op.drop_index(op.f('ix_task_id'), table_name='task') op.drop_index(op.f("ix_task_id"), table_name="task")
op.drop_table('task') op.drop_table("task")

View File

@ -0,0 +1,58 @@
"""Add user table and task relation
Revision ID: 0002
Revises: 0001_create_tasks_table
Create Date: 2023-10-31
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic
revision = "0002"
down_revision = "0001_create_tasks_table"
branch_labels = None
depends_on = None
def upgrade():
# Create user table
op.create_table(
"user",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("email", sa.String(length=255), nullable=False),
sa.Column("username", sa.String(length=100), nullable=False),
sa.Column("hashed_password", sa.String(length=255), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=True, default=True),
sa.Column("is_superuser", sa.Boolean(), nullable=True, default=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
# Create indexes on user table
op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False)
op.create_index(op.f("ix_user_username"), "user", ["username"], unique=True)
# Add user_id column to task table
with op.batch_alter_table("task", schema=None) as batch_op:
batch_op.add_column(sa.Column("user_id", sa.Integer(), nullable=True))
batch_op.create_foreign_key("fk_task_user_id", "user", ["user_id"], ["id"])
def downgrade():
# Remove foreign key and user_id column from task table
with op.batch_alter_table("task", schema=None) as batch_op:
batch_op.drop_constraint("fk_task_user_id", type_="foreignkey")
batch_op.drop_column("user_id")
# Drop indexes on user table
op.drop_index(op.f("ix_user_username"), table_name="user")
op.drop_index(op.f("ix_user_id"), table_name="user")
op.drop_index(op.f("ix_user_email"), table_name="user")
# Drop user table
op.drop_table("user")

View File

@ -1 +1 @@
# App package # App package

View File

@ -1 +1 @@
# API package # API package

View File

@ -1,4 +1,61 @@
from typing import Generator from typing import Optional
# Use the improved get_db function from session.py from fastapi import Depends, HTTPException, status
from app.db.session import get_db from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.core.config import settings
from app.core.security import ALGORITHM
from app.models.user import User
from app.schemas.user import TokenPayload
from app import crud
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login", auto_error=False)
def get_current_user(
db: Session = Depends(get_db), token: Optional[str] = Depends(oauth2_scheme)
) -> Optional[User]:
"""
Get the current user from the provided JWT token.
Returns None if no token is provided or the token is invalid.
"""
if not token:
return None
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
token_data = TokenPayload(**payload)
except (JWTError, ValidationError):
return None
user = crud.user.get(db, id=token_data.sub)
if not user:
return None
return user
def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
"""
Get the current active user and raise an exception if the user is not
authenticated or inactive.
"""
if not current_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
if not crud.user.is_active(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
)
return current_user

View File

@ -1,6 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.routers import tasks from app.api.routers import tasks, auth
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"]) api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])

95
app/api/routers/auth.py Normal file
View File

@ -0,0 +1,95 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app import crud
from app.api.deps import get_db, get_current_active_user
from app.core.config import settings
from app.core.security import create_access_token
from app.models.user import User
from app.schemas.user import User as UserSchema
from app.schemas.user import UserCreate, Token
router = APIRouter()
@router.post("/register", response_model=UserSchema)
def register(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
) -> Any:
"""
Register a new user.
"""
# Check if user with this email already exists
user = crud.user.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A user with this email already exists.",
)
# Check if user with this username already exists
user = crud.user.get_by_username(db, username=user_in.username)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A user with this username already exists.",
)
# Create new user
user = crud.user.create(db, obj_in=user_in)
return user
@router.post("/login", response_model=Token)
def login(
db: Session = Depends(get_db),
form_data: OAuth2PasswordRequestForm = Depends(),
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests.
"""
user = crud.user.authenticate(
db, email_or_username=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email/username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not crud.user.is_active(user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": create_access_token(
subject=user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.post("/test-token", response_model=UserSchema)
def test_token(current_user: User = Depends(get_current_active_user)) -> Any:
"""
Test access token.
"""
return current_user
@router.get("/me", response_model=UserSchema)
def read_users_me(current_user: User = Depends(get_current_active_user)) -> Any:
"""
Get current user.
"""
return current_user

View File

@ -1,11 +1,12 @@
from typing import Any, List, Optional from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app import crud from app import crud
from app.api.deps import get_db from app.api.deps import get_db, get_current_active_user
from app.models.task import TaskStatus from app.models.task import TaskStatus
from app.models.user import User
from app.schemas.task import Task, TaskCreate, TaskUpdate from app.schemas.task import Task, TaskCreate, TaskUpdate
router = APIRouter() router = APIRouter()
@ -17,73 +18,82 @@ def read_tasks(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
status: Optional[TaskStatus] = None, status: Optional[TaskStatus] = None,
current_user: User = Depends(get_current_active_user),
) -> Any: ) -> Any:
""" """
Retrieve tasks. Retrieve tasks for the current user.
""" """
try: try:
import traceback import traceback
import sqlite3 import sqlite3
from sqlalchemy import text
from app.db.session import db_file from app.db.session import db_file
print(f"Getting tasks with status: {status}, skip: {skip}, limit: {limit}") print(f"Getting tasks with status: {status}, skip: {skip}, limit: {limit}")
# Try the normal SQLAlchemy approach first # Try the normal SQLAlchemy approach first
try: try:
if status: if status:
tasks = crud.task.get_by_status(db, status=status) tasks = crud.task.get_by_status(
db, status=status, user_id=current_user.id
)
else: else:
tasks = crud.task.get_multi(db, skip=skip, limit=limit) tasks = crud.task.get_multi(
db, skip=skip, limit=limit, user_id=current_user.id
)
return tasks return tasks
except Exception as e: except Exception as e:
print(f"Error getting tasks with SQLAlchemy: {e}") print(f"Error getting tasks with SQLAlchemy: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
# Continue to fallback # Continue to fallback
# Fallback to direct SQLite approach # Fallback to direct SQLite approach
try: try:
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
cursor = conn.cursor() cursor = conn.cursor()
if status: if status:
cursor.execute("SELECT * FROM task WHERE status = ? LIMIT ? OFFSET ?", cursor.execute(
(status.value, limit, skip)) "SELECT * FROM task WHERE status = ? AND user_id = ? LIMIT ? OFFSET ?",
(status.value, current_user.id, limit, skip),
)
else: else:
cursor.execute("SELECT * FROM task LIMIT ? OFFSET ?", (limit, skip)) cursor.execute(
"SELECT * FROM task WHERE user_id = ? LIMIT ? OFFSET ?",
(current_user.id, limit, skip),
)
rows = cursor.fetchall() rows = cursor.fetchall()
# Convert to Task objects # Convert to Task objects
tasks = [] tasks = []
for row in rows: for row in rows:
task_dict = dict(row) task_dict = dict(row)
# Convert completed to boolean # Convert completed to boolean
if 'completed' in task_dict: if "completed" in task_dict:
task_dict['completed'] = bool(task_dict['completed']) task_dict["completed"] = bool(task_dict["completed"])
# Convert to object with attributes # Convert to object with attributes
class TaskResult: class TaskResult:
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
tasks.append(TaskResult(**task_dict)) tasks.append(TaskResult(**task_dict))
conn.close() conn.close()
return tasks return tasks
except Exception as e: except Exception as e:
print(f"Error getting tasks with direct SQLite: {e}") print(f"Error getting tasks with direct SQLite: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
raise raise
except Exception as e: except Exception as e:
print(f"Global error in read_tasks: {e}") print(f"Global error in read_tasks: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error retrieving tasks: {str(e)}" detail=f"Error retrieving tasks: {str(e)}",
) )
@ -92,9 +102,10 @@ def create_task(
*, *,
db: Session = Depends(get_db), db: Session = Depends(get_db),
task_in: TaskCreate, task_in: TaskCreate,
current_user: User = Depends(get_current_active_user),
) -> Any: ) -> Any:
""" """
Create new task - using direct SQLite approach for reliability. Create new task for the current user - using direct SQLite approach for reliability.
""" """
import sqlite3 import sqlite3
import time import time
@ -102,60 +113,60 @@ def create_task(
import traceback import traceback
from datetime import datetime from datetime import datetime
from app.db.session import db_file from app.db.session import db_file
# Log creation attempt # Log creation attempt
print(f"[{datetime.now().isoformat()}] Task creation requested", file=sys.stdout) print(f"[{datetime.now().isoformat()}] Task creation requested", file=sys.stdout)
# Use direct SQLite for maximum reliability # Use direct SQLite for maximum reliability
try: try:
# Extract task data regardless of Pydantic version # Extract task data regardless of Pydantic version
try: try:
if hasattr(task_in, 'model_dump'): if hasattr(task_in, "model_dump"):
task_data = task_in.model_dump() task_data = task_in.model_dump()
elif hasattr(task_in, 'dict'): elif hasattr(task_in, "dict"):
task_data = task_in.dict() task_data = task_in.dict()
else: else:
# Fallback for any case # Fallback for any case
task_data = { task_data = {
'title': getattr(task_in, 'title', 'Untitled Task'), "title": getattr(task_in, "title", "Untitled Task"),
'description': getattr(task_in, 'description', ''), "description": getattr(task_in, "description", ""),
'priority': getattr(task_in, 'priority', 'medium'), "priority": getattr(task_in, "priority", "medium"),
'status': getattr(task_in, 'status', 'todo'), "status": getattr(task_in, "status", "todo"),
'due_date': getattr(task_in, 'due_date', None), "due_date": getattr(task_in, "due_date", None),
'completed': getattr(task_in, 'completed', False) "completed": getattr(task_in, "completed", False),
} }
print(f"Task data: {task_data}") print(f"Task data: {task_data}")
except Exception as e: except Exception as e:
print(f"Error extracting task data: {e}") print(f"Error extracting task data: {e}")
# Fallback to minimal data # Fallback to minimal data
task_data = { task_data = {
'title': str(getattr(task_in, 'title', 'Unknown Title')), "title": str(getattr(task_in, "title", "Unknown Title")),
'description': str(getattr(task_in, 'description', '')), "description": str(getattr(task_in, "description", "")),
'priority': 'medium', "priority": "medium",
'status': 'todo', "status": "todo",
'completed': False "completed": False,
} }
# Format due_date if present # Format due_date if present
if task_data.get('due_date'): if task_data.get("due_date"):
try: try:
if isinstance(task_data['due_date'], datetime): if isinstance(task_data["due_date"], datetime):
task_data['due_date'] = task_data['due_date'].isoformat() task_data["due_date"] = task_data["due_date"].isoformat()
elif isinstance(task_data['due_date'], str): elif isinstance(task_data["due_date"], str):
# Standardize format by parsing and reformatting # Standardize format by parsing and reformatting
parsed_date = datetime.fromisoformat( parsed_date = datetime.fromisoformat(
task_data['due_date'].replace('Z', '+00:00') task_data["due_date"].replace("Z", "+00:00")
) )
task_data['due_date'] = parsed_date.isoformat() task_data["due_date"] = parsed_date.isoformat()
except Exception as e: except Exception as e:
print(f"Warning: Could not parse due_date: {e}") print(f"Warning: Could not parse due_date: {e}")
# Keep as-is or set to None if invalid # Keep as-is or set to None if invalid
if not isinstance(task_data['due_date'], str): if not isinstance(task_data["due_date"], str):
task_data['due_date'] = None task_data["due_date"] = None
# Get current timestamp for created/updated fields # Get current timestamp for created/updated fields
now = datetime.utcnow().isoformat() now = datetime.utcnow().isoformat()
# Connect to SQLite with retry logic # Connect to SQLite with retry logic
for retry in range(3): for retry in range(3):
conn = None conn = None
@ -163,7 +174,7 @@ def create_task(
# Try to connect to the database with a timeout # Try to connect to the database with a timeout
conn = sqlite3.connect(str(db_file), timeout=30) conn = sqlite3.connect(str(db_file), timeout=30)
cursor = conn.cursor() cursor = conn.cursor()
# Create the task table if it doesn't exist - using minimal schema # Create the task table if it doesn't exist - using minimal schema
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS task ( CREATE TABLE IF NOT EXISTS task (
@ -175,122 +186,134 @@ def create_task(
due_date TEXT, due_date TEXT,
completed INTEGER DEFAULT 0, completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER
) )
""") """)
# Insert the task - provide defaults for all fields # Insert the task - provide defaults for all fields
cursor.execute( cursor.execute(
""" """
INSERT INTO task ( INSERT INTO task (
title, description, priority, status, title, description, priority, status,
due_date, completed, created_at, updated_at due_date, completed, created_at, updated_at, user_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
task_data.get('title', 'Untitled'), task_data.get("title", "Untitled"),
task_data.get('description', ''), task_data.get("description", ""),
task_data.get('priority', 'medium'), task_data.get("priority", "medium"),
task_data.get('status', 'todo'), task_data.get("status", "todo"),
task_data.get('due_date'), task_data.get("due_date"),
1 if task_data.get('completed') else 0, 1 if task_data.get("completed") else 0,
now, now,
now now,
) current_user.id,
),
) )
# Get the ID of the inserted task # Get the ID of the inserted task
task_id = cursor.lastrowid task_id = cursor.lastrowid
print(f"Task inserted with ID: {task_id}") print(f"Task inserted with ID: {task_id}")
# Commit the transaction # Commit the transaction
conn.commit() conn.commit()
# Retrieve the created task to return it # Retrieve the created task to return it
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
# Get column names from cursor description # Get column names from cursor description
column_names = [desc[0] for desc in cursor.description] column_names = [desc[0] for desc in cursor.description]
# Create a dictionary from row values # Create a dictionary from row values
task_dict = dict(zip(column_names, row)) task_dict = dict(zip(column_names, row))
# Convert 'completed' to boolean # Convert 'completed' to boolean
if 'completed' in task_dict: if "completed" in task_dict:
task_dict['completed'] = bool(task_dict['completed']) task_dict["completed"] = bool(task_dict["completed"])
# Create an object that mimics the Task model # Create an object that mimics the Task model
class TaskResult: class TaskResult:
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
print(f"Task created successfully: ID={task_id}") print(f"Task created successfully: ID={task_id}")
# Close the connection and return the task # Close the connection and return the task
conn.close() conn.close()
return TaskResult(**task_dict) return TaskResult(**task_dict)
else: else:
conn.close() conn.close()
raise Exception(f"Task creation succeeded but retrieval failed for ID: {task_id}") raise Exception(
f"Task creation succeeded but retrieval failed for ID: {task_id}"
)
except sqlite3.OperationalError as e: except sqlite3.OperationalError as e:
if conn: if conn:
conn.close() conn.close()
# Check if retry is appropriate # Check if retry is appropriate
if "database is locked" in str(e) and retry < 2: if "database is locked" in str(e) and retry < 2:
wait_time = (retry + 1) * 1.5 # Exponential backoff wait_time = (retry + 1) * 1.5 # Exponential backoff
print(f"Database locked, retrying in {wait_time}s (attempt {retry+1}/3)") print(
f"Database locked, retrying in {wait_time}s (attempt {retry + 1}/3)"
)
time.sleep(wait_time) time.sleep(wait_time)
else: else:
print(f"SQLite operational error: {e}") print(f"SQLite operational error: {e}")
raise raise
except Exception as e: except Exception as e:
if conn: if conn:
# Try to rollback if connection is still open # Try to rollback if connection is still open
try: try:
conn.rollback() conn.rollback()
except: except Exception:
pass pass
conn.close() conn.close()
print(f"Error in SQLite task creation: {e}") print(f"Error in SQLite task creation: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
# Only retry on specific transient errors # Only retry on specific transient errors
if retry < 2 and ("locked" in str(e).lower() or "busy" in str(e).lower()): if retry < 2 and (
"locked" in str(e).lower() or "busy" in str(e).lower()
):
time.sleep(1) time.sleep(1)
continue continue
raise raise
# If we reach here, the retry loop failed # If we reach here, the retry loop failed
raise Exception("Failed to create task after multiple attempts") raise Exception("Failed to create task after multiple attempts")
except Exception as sqlite_error: except Exception as sqlite_error:
# Final fallback: try SQLAlchemy approach # Final fallback: try SQLAlchemy approach
try: try:
print(f"Direct SQLite approach failed: {sqlite_error}") print(f"Direct SQLite approach failed: {sqlite_error}")
print("Trying SQLAlchemy as fallback...") print("Trying SQLAlchemy as fallback...")
task = crud.task.create(db, obj_in=task_in) task = crud.task.create_with_owner(
db, obj_in=task_in, user_id=current_user.id
)
print(f"Task created with SQLAlchemy fallback: ID={task.id}") print(f"Task created with SQLAlchemy fallback: ID={task.id}")
return task return task
except Exception as alch_error: except Exception as alch_error:
print(f"SQLAlchemy fallback also failed: {alch_error}") print(f"SQLAlchemy fallback also failed: {alch_error}")
print(traceback.format_exc()) print(traceback.format_exc())
# Provide detailed error information # Provide detailed error information
error_detail = f"Task creation failed. Primary error: {str(sqlite_error)}. Fallback error: {str(alch_error)}" error_detail = f"Task creation failed. Primary error: {str(sqlite_error)}. Fallback error: {str(alch_error)}"
print(f"Final error: {error_detail}") print(f"Final error: {error_detail}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error_detail
detail=error_detail
) )
@ -299,54 +322,60 @@ def read_task(
*, *,
db: Session = Depends(get_db), db: Session = Depends(get_db),
task_id: int, task_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any: ) -> Any:
""" """
Get task by ID. Get task by ID for the current user.
""" """
try: try:
import traceback import traceback
import sqlite3 import sqlite3
from app.db.session import db_file from app.db.session import db_file
print(f"Getting task with ID: {task_id}") print(f"Getting task with ID: {task_id}")
# Try the normal SQLAlchemy approach first # Try the normal SQLAlchemy approach first
try: try:
task = crud.task.get(db, id=task_id) task = crud.task.get_by_id_and_user(
db, task_id=task_id, user_id=current_user.id
)
if task: if task:
return task return task
# Fall through to direct SQLite if task not found # Task not found or doesn't belong to user - check further with direct SQLite
except Exception as e: except Exception as e:
print(f"Error getting task with SQLAlchemy: {e}") print(f"Error getting task with SQLAlchemy: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
# Continue to fallback # Continue to fallback
# Fallback to direct SQLite approach # Fallback to direct SQLite approach
try: try:
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
row = cursor.fetchone() row = cursor.fetchone()
if not row: if not row:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found", detail="Task not found",
) )
task_dict = dict(row) task_dict = dict(row)
# Convert completed to boolean # Convert completed to boolean
if 'completed' in task_dict: if "completed" in task_dict:
task_dict['completed'] = bool(task_dict['completed']) task_dict["completed"] = bool(task_dict["completed"])
# Convert to object with attributes # Convert to object with attributes
class TaskResult: class TaskResult:
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
conn.close() conn.close()
return TaskResult(**task_dict) return TaskResult(**task_dict)
except HTTPException: except HTTPException:
@ -356,9 +385,9 @@ def read_task(
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error retrieving task: {str(e)}" detail=f"Error retrieving task: {str(e)}",
) )
except HTTPException: except HTTPException:
raise # Re-raise any HTTP exceptions raise # Re-raise any HTTP exceptions
except Exception as e: except Exception as e:
@ -366,7 +395,7 @@ def read_task(
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error retrieving task: {str(e)}" detail=f"Error retrieving task: {str(e)}",
) )
@ -376,30 +405,34 @@ def update_task(
db: Session = Depends(get_db), db: Session = Depends(get_db),
task_id: int, task_id: int,
task_in: TaskUpdate, task_in: TaskUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any: ) -> Any:
""" """
Update a task. Update a task for the current user.
""" """
try: try:
import traceback import traceback
import sqlite3 import sqlite3
import json
from datetime import datetime from datetime import datetime
from app.db.session import db_file from app.db.session import db_file
print(f"Updating task with ID: {task_id}, data: {task_in}") print(f"Updating task with ID: {task_id}, data: {task_in}")
# Handle datetime conversion for due_date if present # Handle datetime conversion for due_date if present
if hasattr(task_in, "due_date") and task_in.due_date is not None: if hasattr(task_in, "due_date") and task_in.due_date is not None:
if isinstance(task_in.due_date, str): if isinstance(task_in.due_date, str):
try: try:
task_in.due_date = datetime.fromisoformat(task_in.due_date.replace('Z', '+00:00')) task_in.due_date = datetime.fromisoformat(
task_in.due_date.replace("Z", "+00:00")
)
except Exception as e: except Exception as e:
print(f"Error parsing due_date: {e}") print(f"Error parsing due_date: {e}")
# Try the normal SQLAlchemy approach first # Try the normal SQLAlchemy approach first
try: try:
task = crud.task.get(db, id=task_id) task = crud.task.get_by_id_and_user(
db, task_id=task_id, user_id=current_user.id
)
if not task: if not task:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -413,84 +446,94 @@ def update_task(
print(f"Error updating task with SQLAlchemy: {e}") print(f"Error updating task with SQLAlchemy: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
# Continue to fallback # Continue to fallback
# Fallback to direct SQLite approach # Fallback to direct SQLite approach
try: try:
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
cursor = conn.cursor() cursor = conn.cursor()
# First check if task exists # First check if task exists and belongs to current user
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
row = cursor.fetchone() row = cursor.fetchone()
if not row: if not row:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found", detail="Task not found",
) )
# Convert Pydantic model to dict, excluding unset values # Convert Pydantic model to dict, excluding unset values
updates = {} updates = {}
model_data = task_in.model_dump(exclude_unset=True) if hasattr(task_in, "model_dump") else task_in.dict(exclude_unset=True) model_data = (
task_in.model_dump(exclude_unset=True)
if hasattr(task_in, "model_dump")
else task_in.dict(exclude_unset=True)
)
# Only include fields that were provided in the update # Only include fields that were provided in the update
for key, value in model_data.items(): for key, value in model_data.items():
if value is not None: # Skip None values if value is not None: # Skip None values
updates[key] = value updates[key] = value
if not updates: if not updates:
# No updates provided # No updates provided
task_dict = dict(row) task_dict = dict(row)
# Convert completed to boolean # Convert completed to boolean
if 'completed' in task_dict: if "completed" in task_dict:
task_dict['completed'] = bool(task_dict['completed']) task_dict["completed"] = bool(task_dict["completed"])
# Return the unchanged task # Return the unchanged task
class TaskResult: class TaskResult:
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
conn.close() conn.close()
return TaskResult(**task_dict) return TaskResult(**task_dict)
# Format datetime objects # Format datetime objects
if 'due_date' in updates and isinstance(updates['due_date'], datetime): if "due_date" in updates and isinstance(updates["due_date"], datetime):
updates['due_date'] = updates['due_date'].isoformat() updates["due_date"] = updates["due_date"].isoformat()
# Add updated_at timestamp # Add updated_at timestamp
updates['updated_at'] = datetime.utcnow().isoformat() updates["updated_at"] = datetime.utcnow().isoformat()
# Build the SQL update statement # Build the SQL update statement
set_clause = ", ".join([f"{key} = ?" for key in updates.keys()]) set_clause = ", ".join([f"{key} = ?" for key in updates.keys()])
params = list(updates.values()) params = list(updates.values())
params.append(task_id) # For the WHERE clause params.append(task_id) # For the WHERE clause
cursor.execute(f"UPDATE task SET {set_clause} WHERE id = ?", params) cursor.execute(f"UPDATE task SET {set_clause} WHERE id = ?", params)
conn.commit() conn.commit()
# Return the updated task # Return the updated task
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
updated_row = cursor.fetchone() updated_row = cursor.fetchone()
conn.close() conn.close()
if updated_row: if updated_row:
task_dict = dict(updated_row) task_dict = dict(updated_row)
# Convert completed to boolean # Convert completed to boolean
if 'completed' in task_dict: if "completed" in task_dict:
task_dict['completed'] = bool(task_dict['completed']) task_dict["completed"] = bool(task_dict["completed"])
# Convert to object with attributes # Convert to object with attributes
class TaskResult: class TaskResult:
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
return TaskResult(**task_dict) return TaskResult(**task_dict)
else: else:
raise Exception("Task was updated but could not be retrieved") raise Exception("Task was updated but could not be retrieved")
except HTTPException: except HTTPException:
raise # Re-raise the 404 exception raise # Re-raise the 404 exception
except Exception as e: except Exception as e:
@ -498,9 +541,9 @@ def update_task(
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating task: {str(e)}" detail=f"Error updating task: {str(e)}",
) )
except HTTPException: except HTTPException:
raise # Re-raise any HTTP exceptions raise # Re-raise any HTTP exceptions
except Exception as e: except Exception as e:
@ -508,7 +551,7 @@ def update_task(
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating task: {str(e)}" detail=f"Error updating task: {str(e)}",
) )
@ -517,72 +560,77 @@ def delete_task(
*, *,
db: Session = Depends(get_db), db: Session = Depends(get_db),
task_id: int, task_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any: ) -> Any:
""" """
Delete a task. Delete a task for the current user.
""" """
try: try:
import traceback import traceback
import sqlite3 import sqlite3
from app.db.session import db_file from app.db.session import db_file
print(f"Deleting task with ID: {task_id}") print(f"Deleting task with ID: {task_id}")
# First, get the task to return it later # Try the normal SQLAlchemy approach
task_to_return = None
# Try the normal SQLAlchemy approach first
try: try:
task = crud.task.get(db, id=task_id) task = crud.task.get_by_id_and_user(
db, task_id=task_id, user_id=current_user.id
)
if not task: if not task:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found", detail="Task not found",
) )
task_to_return = task removed_task = crud.task.remove(db, id=task_id)
task = crud.task.remove(db, id=task_id) return removed_task
return task
except HTTPException: except HTTPException:
raise # Re-raise the 404 exception raise # Re-raise the 404 exception
except Exception as e: except Exception as e:
print(f"Error deleting task with SQLAlchemy: {e}") print(f"Error deleting task with SQLAlchemy: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
# Continue to fallback # Continue to fallback
# Fallback to direct SQLite approach # Fallback to direct SQLite approach
try: try:
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
cursor = conn.cursor() cursor = conn.cursor()
# First save the task data for the return value # First save the task data for the return value
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
row = cursor.fetchone() row = cursor.fetchone()
if not row: if not row:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found", detail="Task not found",
) )
task_dict = dict(row) task_dict = dict(row)
# Convert completed to boolean # Convert completed to boolean
if 'completed' in task_dict: if "completed" in task_dict:
task_dict['completed'] = bool(task_dict['completed']) task_dict["completed"] = bool(task_dict["completed"])
# Delete the task # Delete the task
cursor.execute("DELETE FROM task WHERE id = ?", (task_id,)) cursor.execute(
"DELETE FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
conn.commit() conn.commit()
conn.close() conn.close()
# Convert to object with attributes # Convert to object with attributes
class TaskResult: class TaskResult:
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
return TaskResult(**task_dict) return TaskResult(**task_dict)
except HTTPException: except HTTPException:
raise # Re-raise the 404 exception raise # Re-raise the 404 exception
except Exception as e: except Exception as e:
@ -590,9 +638,9 @@ def delete_task(
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting task: {str(e)}" detail=f"Error deleting task: {str(e)}",
) )
except HTTPException: except HTTPException:
raise # Re-raise any HTTP exceptions raise # Re-raise any HTTP exceptions
except Exception as e: except Exception as e:
@ -600,7 +648,7 @@ def delete_task(
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting task: {str(e)}" detail=f"Error deleting task: {str(e)}",
) )
@ -609,21 +657,24 @@ def complete_task(
*, *,
db: Session = Depends(get_db), db: Session = Depends(get_db),
task_id: int, task_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any: ) -> Any:
""" """
Mark a task as completed. Mark a task as completed for the current user.
""" """
try: try:
import traceback import traceback
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from app.db.session import db_file from app.db.session import db_file
print(f"Marking task {task_id} as completed") print(f"Marking task {task_id} as completed")
# Try the normal SQLAlchemy approach first # Try the normal SQLAlchemy approach first
try: try:
task = crud.task.mark_completed(db, task_id=task_id) task = crud.task.mark_completed(
db, task_id=task_id, user_id=current_user.id
)
if not task: if not task:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -634,52 +685,58 @@ def complete_task(
print(f"Error completing task with SQLAlchemy: {e}") print(f"Error completing task with SQLAlchemy: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
# Continue to fallback # Continue to fallback
# Fallback to direct SQLite approach # Fallback to direct SQLite approach
try: try:
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
cursor = conn.cursor() cursor = conn.cursor()
# First check if task exists # First check if task exists and belongs to current user
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
row = cursor.fetchone() row = cursor.fetchone()
if not row: if not row:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found", detail="Task not found",
) )
# Update task to completed status # Update task to completed status
now = datetime.utcnow().isoformat() now = datetime.utcnow().isoformat()
cursor.execute( cursor.execute(
"UPDATE task SET completed = ?, status = ?, updated_at = ? WHERE id = ?", "UPDATE task SET completed = ?, status = ?, updated_at = ? WHERE id = ? AND user_id = ?",
(1, "done", now, task_id) (1, "done", now, task_id, current_user.id),
) )
conn.commit() conn.commit()
# Get the updated task # Get the updated task
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,)) cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
updated_row = cursor.fetchone() updated_row = cursor.fetchone()
conn.close() conn.close()
if updated_row: if updated_row:
task_dict = dict(updated_row) task_dict = dict(updated_row)
# Convert completed to boolean # Convert completed to boolean
if 'completed' in task_dict: if "completed" in task_dict:
task_dict['completed'] = bool(task_dict['completed']) task_dict["completed"] = bool(task_dict["completed"])
# Convert to object with attributes # Convert to object with attributes
class TaskResult: class TaskResult:
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
return TaskResult(**task_dict) return TaskResult(**task_dict)
else: else:
raise Exception("Task was completed but could not be retrieved") raise Exception("Task was completed but could not be retrieved")
except HTTPException: except HTTPException:
raise # Re-raise the 404 exception raise # Re-raise the 404 exception
except Exception as e: except Exception as e:
@ -687,9 +744,9 @@ def complete_task(
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error completing task: {str(e)}" detail=f"Error completing task: {str(e)}",
) )
except HTTPException: except HTTPException:
raise # Re-raise any HTTP exceptions raise # Re-raise any HTTP exceptions
except Exception as e: except Exception as e:
@ -697,5 +754,5 @@ def complete_task(
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error completing task: {str(e)}" detail=f"Error completing task: {str(e)}",
) )

View File

@ -1,6 +1,6 @@
import secrets import secrets
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import List, Union
from pydantic import AnyHttpUrl, field_validator from pydantic import AnyHttpUrl, field_validator
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@ -16,11 +16,11 @@ if DB_PATH:
else: else:
# Try production path first, then local directory # Try production path first, then local directory
paths_to_try = [ paths_to_try = [
Path("/app/db"), # Production path Path("/app/db"), # Production path
Path.cwd() / "db", # Local development path Path.cwd() / "db", # Local development path
Path("/tmp/taskmanager") # Fallback path Path("/tmp/taskmanager"), # Fallback path
] ]
# Find the first writable path # Find the first writable path
DB_DIR = None DB_DIR = None
for path in paths_to_try: for path in paths_to_try:
@ -35,16 +35,17 @@ else:
break break
except Exception as e: except Exception as e:
print(f"Cannot use path {path}: {e}") print(f"Cannot use path {path}: {e}")
# Last resort fallback # Last resort fallback
if DB_DIR is None: if DB_DIR is None:
DB_DIR = Path("/tmp") DB_DIR = Path("/tmp")
print(f"Falling back to temporary directory: {DB_DIR}") print(f"Falling back to temporary directory: {DB_DIR}")
try: try:
Path("/tmp").mkdir(exist_ok=True) Path("/tmp").mkdir(exist_ok=True)
except: except Exception:
pass pass
class Settings(BaseSettings): class Settings(BaseSettings):
PROJECT_NAME: str = "Task Manager API" PROJECT_NAME: str = "Task Manager API"
# No API version prefix - use direct paths # No API version prefix - use direct paths
@ -52,7 +53,7 @@ class Settings(BaseSettings):
SECRET_KEY: str = secrets.token_urlsafe(32) SECRET_KEY: str = secrets.token_urlsafe(32)
# 60 minutes * 24 hours * 8 days = 8 days # 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
# CORS Configuration # CORS Configuration
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
@ -66,10 +67,8 @@ class Settings(BaseSettings):
# Database configuration # Database configuration
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite" SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
model_config = { model_config = {"case_sensitive": True}
"case_sensitive": True
}
settings = Settings() settings = Settings()

42
app/core/security.py Normal file
View File

@ -0,0 +1,42 @@
from datetime import datetime, timedelta
from typing import Any, Optional, Union
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def create_access_token(
subject: Union[str, Any], expires_delta: Optional[timedelta] = None
) -> str:
"""
Create a JWT access token for a user
"""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify a password against a hash
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""
Hash a password
"""
return pwd_context.hash(password)

View File

@ -1 +1,4 @@
from app.crud.task import task from app.crud.task import task as task
from app.crud.user import user as user
__all__ = ["task", "user"]

View File

@ -30,12 +30,12 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
try: try:
obj_in_data = jsonable_encoder(obj_in) obj_in_data = jsonable_encoder(obj_in)
print(f"Creating {self.model.__name__} with data: {obj_in_data}") print(f"Creating {self.model.__name__} with data: {obj_in_data}")
db_obj = self.model(**obj_in_data) db_obj = self.model(**obj_in_data)
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()
db.refresh(db_obj) db.refresh(db_obj)
print(f"Successfully created {self.model.__name__} with id: {db_obj.id}") print(f"Successfully created {self.model.__name__} with id: {db_obj.id}")
return db_obj return db_obj
except Exception as e: except Exception as e:
@ -43,6 +43,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
error_msg = f"Error creating {self.model.__name__}: {str(e)}" error_msg = f"Error creating {self.model.__name__}: {str(e)}"
print(error_msg) print(error_msg)
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
raise Exception(error_msg) from e raise Exception(error_msg) from e
@ -51,15 +52,15 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
db: Session, db: Session,
*, *,
db_obj: ModelType, db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]] obj_in: Union[UpdateSchemaType, Dict[str, Any]],
) -> ModelType: ) -> ModelType:
try: try:
# Log update operation # Log update operation
print(f"Updating {self.model.__name__} with id: {db_obj.id}") print(f"Updating {self.model.__name__} with id: {db_obj.id}")
# Get the existing data # Get the existing data
obj_data = jsonable_encoder(db_obj) obj_data = jsonable_encoder(db_obj)
# Process the update data # Process the update data
if isinstance(obj_in, dict): if isinstance(obj_in, dict):
update_data = obj_in update_data = obj_in
@ -69,29 +70,30 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
update_data = obj_in.model_dump(exclude_unset=True) update_data = obj_in.model_dump(exclude_unset=True)
else: else:
update_data = obj_in.dict(exclude_unset=True) update_data = obj_in.dict(exclude_unset=True)
# Log the changes being made # Log the changes being made
changes = {k: v for k, v in update_data.items() if k in obj_data} changes = {k: v for k, v in update_data.items() if k in obj_data}
print(f"Fields to update: {changes}") print(f"Fields to update: {changes}")
# Apply the updates # Apply the updates
for field in obj_data: for field in obj_data:
if field in update_data: if field in update_data:
setattr(db_obj, field, update_data[field]) setattr(db_obj, field, update_data[field])
# Save changes # Save changes
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()
db.refresh(db_obj) db.refresh(db_obj)
print(f"Successfully updated {self.model.__name__} with id: {db_obj.id}") print(f"Successfully updated {self.model.__name__} with id: {db_obj.id}")
return db_obj return db_obj
except Exception as e: except Exception as e:
db.rollback() db.rollback()
error_msg = f"Error updating {self.model.__name__}: {str(e)}" error_msg = f"Error updating {self.model.__name__}: {str(e)}"
print(error_msg) print(error_msg)
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
raise Exception(error_msg) from e raise Exception(error_msg) from e
@ -102,20 +104,21 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
if not obj: if not obj:
print(f"{self.model.__name__} with id {id} not found for deletion") print(f"{self.model.__name__} with id {id} not found for deletion")
return None return None
print(f"Deleting {self.model.__name__} with id: {id}") print(f"Deleting {self.model.__name__} with id: {id}")
# Delete the object # Delete the object
db.delete(obj) db.delete(obj)
db.commit() db.commit()
print(f"Successfully deleted {self.model.__name__} with id: {id}") print(f"Successfully deleted {self.model.__name__} with id: {id}")
return obj return obj
except Exception as e: except Exception as e:
db.rollback() db.rollback()
error_msg = f"Error deleting {self.model.__name__}: {str(e)}" error_msg = f"Error deleting {self.model.__name__}: {str(e)}"
print(error_msg) print(error_msg)
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
raise Exception(error_msg) from e raise Exception(error_msg) from e

View File

@ -7,21 +7,69 @@ from app.schemas.task import TaskCreate, TaskUpdate
class CRUDTask(CRUDBase[Task, TaskCreate, TaskUpdate]): class CRUDTask(CRUDBase[Task, TaskCreate, TaskUpdate]):
def get_by_status(self, db: Session, *, status: TaskStatus) -> List[Task]: def get_by_status(
return db.query(self.model).filter(Task.status == status).all() self, db: Session, *, status: TaskStatus, user_id: Optional[int] = None
) -> List[Task]:
def get_completed(self, db: Session) -> List[Task]: query = db.query(self.model).filter(Task.status == status)
return db.query(self.model).filter(Task.completed == True).all() if user_id is not None:
query = query.filter(Task.user_id == user_id)
def mark_completed(self, db: Session, *, task_id: int) -> Optional[Task]: return query.all()
task = self.get(db, id=task_id)
def get_completed(
self, db: Session, *, user_id: Optional[int] = None
) -> List[Task]:
query = db.query(self.model).filter(Task.completed.is_(True))
if user_id is not None:
query = query.filter(Task.user_id == user_id)
return query.all()
def get_multi(
self,
db: Session,
*,
skip: int = 0,
limit: int = 100,
user_id: Optional[int] = None,
) -> List[Task]:
query = db.query(self.model)
if user_id is not None:
query = query.filter(Task.user_id == user_id)
return query.offset(skip).limit(limit).all()
def create_with_owner(
self, db: Session, *, obj_in: TaskCreate, user_id: int
) -> Task:
obj_in_data = (
obj_in.model_dump() if hasattr(obj_in, "model_dump") else obj_in.dict()
)
db_obj = self.model(**obj_in_data, user_id=user_id)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def get_by_id_and_user(
self, db: Session, *, task_id: int, user_id: int
) -> Optional[Task]:
return (
db.query(self.model)
.filter(Task.id == task_id, Task.user_id == user_id)
.first()
)
def mark_completed(
self, db: Session, *, task_id: int, user_id: Optional[int] = None
) -> Optional[Task]:
if user_id:
task = self.get_by_id_and_user(db, task_id=task_id, user_id=user_id)
else:
task = self.get(db, id=task_id)
if not task: if not task:
return None return None
task_in = TaskUpdate(
status=TaskStatus.DONE, task_in = TaskUpdate(status=TaskStatus.DONE, completed=True)
completed=True
)
return self.update(db, db_obj=task, obj_in=task_in) return self.update(db, db_obj=task, obj_in=task_in)
task = CRUDTask(Task) task = CRUDTask(Task)

74
app/crud/user.py Normal file
View File

@ -0,0 +1,74 @@
from typing import Any, Dict, Optional, Union
from sqlalchemy import or_
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password
from app.crud.base import CRUDBase
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def get_by_username(self, db: Session, *, username: str) -> Optional[User]:
return db.query(User).filter(User.username == username).first()
def get_by_email_or_username(
self, db: Session, *, email_or_username: str
) -> Optional[User]:
return (
db.query(User)
.filter(
or_(User.email == email_or_username, User.username == email_or_username)
)
.first()
)
def create(self, db: Session, *, obj_in: UserCreate) -> User:
db_obj = User(
email=obj_in.email,
username=obj_in.username,
hashed_password=get_password_hash(obj_in.password),
is_active=obj_in.is_active,
is_superuser=obj_in.is_superuser,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
) -> User:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.model_dump(exclude_unset=True)
if "password" in update_data and update_data["password"]:
hashed_password = get_password_hash(update_data["password"])
del update_data["password"]
update_data["hashed_password"] = hashed_password
return super().update(db, db_obj=db_obj, obj_in=update_data)
def authenticate(
self, db: Session, *, email_or_username: str, password: str
) -> Optional[User]:
user = self.get_by_email_or_username(db, email_or_username=email_or_username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def is_active(self, user: User) -> bool:
return user.is_active
def is_superuser(self, user: User) -> bool:
return user.is_superuser
# Create instance for use throughout the app
user = CRUDUser(User)

View File

@ -1,4 +1,5 @@
# Import all the models, so that Base has them before being # Import all the models, so that Base has them before being
# imported by Alembic # imported by Alembic
from app.db.base_class import Base # noqa from app.db.base_class import Base # noqa
from app.models.task import Task # noqa from app.models.task import Task # noqa
from app.models.user import User # noqa

View File

@ -7,8 +7,8 @@ from sqlalchemy.ext.declarative import as_declarative, declared_attr
class Base: class Base:
id: Any id: Any
__name__: str __name__: str
# Generate __tablename__ automatically based on class name # Generate __tablename__ automatically based on class name
@declared_attr @declared_attr
def __tablename__(cls) -> str: def __tablename__(cls) -> str:
return cls.__name__.lower() return cls.__name__.lower()

View File

@ -1,12 +1,8 @@
import os import os
import sys
import time
import sqlite3 import sqlite3
from pathlib import Path
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import inspect, text from sqlalchemy import text
from sqlalchemy.exc import OperationalError
from app.db.base import Base # Import all models from app.db.base import Base # Import all models
from app.db.session import engine, db_file from app.db.session import engine, db_file
@ -21,22 +17,22 @@ def init_db() -> None:
print(f"Initializing database at {db_file}") print(f"Initializing database at {db_file}")
print(f"Using SQLAlchemy URL: {settings.SQLALCHEMY_DATABASE_URL}") print(f"Using SQLAlchemy URL: {settings.SQLALCHEMY_DATABASE_URL}")
print(f"DB_DIR is set to: {DB_DIR} (this should be /app/db in production)") print(f"DB_DIR is set to: {DB_DIR} (this should be /app/db in production)")
# First try direct SQLite approach to ensure we have a basic database file # First try direct SQLite approach to ensure we have a basic database file
try: try:
# Ensure database file exists and is writable # Ensure database file exists and is writable
with open(db_file, 'a'): # Try opening for append (creates if doesn't exist) with open(db_file, "a"): # Try opening for append (creates if doesn't exist)
os.utime(db_file, None) # Update access/modify time os.utime(db_file, None) # Update access/modify time
print(f"Database file exists and is writable: {db_file}") print(f"Database file exists and is writable: {db_file}")
# Try direct SQLite connection to create task table # Try direct SQLite connection to create task table
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
# Enable foreign keys and WAL journal mode # Enable foreign keys and WAL journal mode
conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA journal_mode = WAL") conn.execute("PRAGMA journal_mode = WAL")
# Create task table if it doesn't exist # Create task table if it doesn't exist
conn.execute(""" conn.execute("""
CREATE TABLE IF NOT EXISTS task ( CREATE TABLE IF NOT EXISTS task (
@ -48,33 +44,89 @@ def init_db() -> None:
due_date TEXT, due_date TEXT,
completed INTEGER DEFAULT 0, completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER
)
""")
# Create user table if it doesn't exist
conn.execute("""
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
hashed_password TEXT NOT NULL,
is_active INTEGER DEFAULT 1,
is_superuser INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP updated_at TEXT DEFAULT CURRENT_TIMESTAMP
) )
""") """)
# Create an index on the id column # Create indexes
conn.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON task(id)") conn.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON task(id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_task_user_id ON task(user_id)")
# Add a sample task if the table is empty conn.execute("CREATE INDEX IF NOT EXISTS idx_user_id ON user(id)")
conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_email ON user(email)")
conn.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_user_username ON user(username)"
)
# Add a default admin user if the user table is empty
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM task") cursor.execute("SELECT COUNT(*) FROM user")
count = cursor.fetchone()[0] user_count = cursor.fetchone()[0]
if count == 0: if user_count == 0:
now = datetime.utcnow().isoformat() now = datetime.utcnow().isoformat()
conn.execute(""" # Import get_password_hash function here to avoid F823 error
INSERT INTO task (title, description, priority, status, completed, created_at, updated_at) from app.core.security import get_password_hash
admin_password_hash = get_password_hash("adminpassword")
conn.execute(
"""
INSERT INTO user (email, username, hashed_password, is_active, is_superuser, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", ( """,
"Example Task", (
"This is an example task created during initialization", "admin@example.com",
"medium", "admin",
"todo", admin_password_hash,
0, 1, # is_active
now, 1, # is_superuser
now now,
)) now,
),
)
print("Created default admin user: admin@example.com / adminpassword")
# Add a sample task if the task table is empty
cursor.execute("SELECT COUNT(*) FROM task")
task_count = cursor.fetchone()[0]
if task_count == 0:
# Get the admin user ID if it exists
cursor.execute("SELECT id FROM user WHERE username = ?", ("admin",))
admin_row = cursor.fetchone()
admin_id = admin_row[0] if admin_row else None
now = datetime.utcnow().isoformat()
conn.execute(
"""
INSERT INTO task (title, description, priority, status, completed, created_at, updated_at, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"Example Task",
"This is an example task created during initialization",
"medium",
"todo",
0,
now,
now,
admin_id,
),
)
conn.commit() conn.commit()
cursor.close() cursor.close()
conn.close() conn.close()
@ -82,58 +134,99 @@ def init_db() -> None:
except Exception as e: except Exception as e:
print(f"Error during direct SQLite initialization: {e}") print(f"Error during direct SQLite initialization: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
# Now try with SQLAlchemy as a backup approach # Now try with SQLAlchemy as a backup approach
try: try:
print("Attempting SQLAlchemy database initialization...") print("Attempting SQLAlchemy database initialization...")
# Try to create all tables from models # Try to create all tables from models
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
print("Successfully created tables with SQLAlchemy") print("Successfully created tables with SQLAlchemy")
# Verify tables exist # Verify tables exist
with engine.connect() as conn: with engine.connect() as conn:
# Get list of tables # Get list of tables
result = conn.execute(text( result = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'" text("SELECT name FROM sqlite_master WHERE type='table'")
)) )
tables = [row[0] for row in result] tables = [row[0] for row in result]
print(f"Tables in database: {', '.join(tables)}") print(f"Tables in database: {', '.join(tables)}")
# Verify task table exists # Verify user table exists
if 'task' in tables: if "user" in tables:
# Check if task table is empty # Check if user table is empty
result = conn.execute(text("SELECT COUNT(*) FROM task")) result = conn.execute(text("SELECT COUNT(*) FROM user"))
task_count = result.scalar() user_count = result.scalar()
print(f"Task table contains {task_count} records") print(f"User table contains {user_count} records")
# If table exists but is empty, add a sample task # If user table is empty, add default admin user
if task_count == 0: if user_count == 0:
print("Adding sample task with SQLAlchemy") print("Adding default admin user with SQLAlchemy")
from app.models.task import Task, TaskPriority, TaskStatus from app.models.user import User
sample_task = Task( from app.core.security import get_password_hash
title="Sample SQLAlchemy Task",
description="This is a sample task created with SQLAlchemy", admin_user = User(
priority=TaskPriority.MEDIUM, email="admin@example.com",
status=TaskStatus.TODO, username="admin",
completed=False, hashed_password=get_password_hash("adminpassword"),
is_active=True,
is_superuser=True,
created_at=datetime.utcnow(), created_at=datetime.utcnow(),
updated_at=datetime.utcnow() updated_at=datetime.utcnow(),
) )
from app.db.session import SessionLocal from app.db.session import SessionLocal
db = SessionLocal() db = SessionLocal()
db.add(sample_task) db.add(admin_user)
db.commit() db.commit()
# Get the admin user ID for the sample task
admin_id = None
admin = db.query(User).filter_by(username="admin").first()
if admin:
admin_id = admin.id
# Verify task table exists
if "task" in tables:
# Check if task table is empty
result = conn.execute(text("SELECT COUNT(*) FROM task"))
task_count = result.scalar()
print(f"Task table contains {task_count} records")
# If table exists but is empty, add a sample task
if task_count == 0 and admin_id:
print("Adding sample task with SQLAlchemy")
from app.models.task import Task, TaskPriority, TaskStatus
sample_task = Task(
title="Sample SQLAlchemy Task",
description="This is a sample task created with SQLAlchemy",
priority=TaskPriority.MEDIUM,
status=TaskStatus.TODO,
completed=False,
user_id=admin_id,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
db.add(sample_task)
db.commit()
print("Added sample task with SQLAlchemy")
db.close() db.close()
print("Added sample task with SQLAlchemy") print("Added default admin user with SQLAlchemy")
else: else:
print("WARNING: 'user' table not found!")
if "task" not in tables:
print("WARNING: 'task' table not found!") print("WARNING: 'task' table not found!")
print("SQLAlchemy database initialization completed") print("SQLAlchemy database initialization completed")
except Exception as e: except Exception as e:
print(f"Error during SQLAlchemy initialization: {e}") print(f"Error during SQLAlchemy initialization: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
print("Continuing despite SQLAlchemy initialization error...") print("Continuing despite SQLAlchemy initialization error...")
@ -146,17 +239,19 @@ def create_test_task():
try: try:
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
cursor = conn.cursor() cursor = conn.cursor()
# Check if task table exists # Check if task table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='task'") cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='task'"
)
if not cursor.fetchone(): if not cursor.fetchone():
print("Task table doesn't exist - cannot create test task") print("Task table doesn't exist - cannot create test task")
return return
# Check if any tasks exist # Check if any tasks exist
cursor.execute("SELECT COUNT(*) FROM task") cursor.execute("SELECT COUNT(*) FROM task")
count = cursor.fetchone()[0] count = cursor.fetchone()[0]
if count == 0: if count == 0:
# Create a task directly with SQLite # Create a task directly with SQLite
now = datetime.utcnow().isoformat() now = datetime.utcnow().isoformat()
@ -166,47 +261,51 @@ def create_test_task():
title, description, priority, status, title, description, priority, status,
completed, created_at, updated_at completed, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
( (
"Test Task (Direct SQL)", "Test Task (Direct SQL)",
"This is a test task created directly with SQLite", "This is a test task created directly with SQLite",
"medium", "medium",
"todo", "todo",
0, # not completed 0, # not completed
now, now,
now now,
) ),
) )
conn.commit() conn.commit()
task_id = cursor.lastrowid task_id = cursor.lastrowid
print(f"Created test task with direct SQLite, ID: {task_id}") print(f"Created test task with direct SQLite, ID: {task_id}")
else: else:
print(f"Found {count} existing tasks, no need to create test task") print(f"Found {count} existing tasks, no need to create test task")
conn.close() conn.close()
except Exception as e: except Exception as e:
print(f"Error with direct SQLite test task creation: {e}") print(f"Error with direct SQLite test task creation: {e}")
# Continue with SQLAlchemy approach # Continue with SQLAlchemy approach
# Now try with SQLAlchemy # Now try with SQLAlchemy
try: try:
from app.crud.task import task as task_crud from app.crud.task import task as task_crud
from app.schemas.task import TaskCreate from app.schemas.task import TaskCreate
from app.db.session import SessionLocal from app.db.session import SessionLocal
db = SessionLocal() db = SessionLocal()
try: try:
# Check if there are any tasks # Check if there are any tasks
try: try:
existing_tasks = db.execute(text("SELECT COUNT(*) FROM task")).scalar() existing_tasks = db.execute(
text("SELECT COUNT(*) FROM task")
).scalar()
if existing_tasks > 0: if existing_tasks > 0:
print(f"Test task not needed, found {existing_tasks} existing tasks") print(
f"Test task not needed, found {existing_tasks} existing tasks"
)
return return
except Exception as e: except Exception as e:
print(f"Error checking for existing tasks: {e}") print(f"Error checking for existing tasks: {e}")
# Continue anyway to try creating a task # Continue anyway to try creating a task
# Create a test task # Create a test task
test_task = TaskCreate( test_task = TaskCreate(
title="Test Task (SQLAlchemy)", title="Test Task (SQLAlchemy)",
@ -214,23 +313,24 @@ def create_test_task():
priority="medium", priority="medium",
status="todo", status="todo",
due_date=datetime.utcnow() + timedelta(days=7), due_date=datetime.utcnow() + timedelta(days=7),
completed=False completed=False,
) )
created_task = task_crud.create(db, obj_in=test_task) created_task = task_crud.create(db, obj_in=test_task)
print(f"Created test task with SQLAlchemy, ID: {created_task.id}") print(f"Created test task with SQLAlchemy, ID: {created_task.id}")
finally: finally:
db.close() db.close()
except Exception as e: except Exception as e:
print(f"Error with SQLAlchemy test task creation: {e}") print(f"Error with SQLAlchemy test task creation: {e}")
except Exception as e: except Exception as e:
print(f"Global error creating test task: {e}") print(f"Global error creating test task: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
if __name__ == "__main__": if __name__ == "__main__":
init_db() init_db()
create_test_task() create_test_task()

View File

@ -1,12 +1,9 @@
import os
import time import time
import sqlite3 import sqlite3
from typing import Generator from typing import Generator
from pathlib import Path
from sqlalchemy import create_engine, event from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import OperationalError, SQLAlchemyError
from app.core.config import settings, DB_DIR from app.core.config import settings, DB_DIR
@ -22,19 +19,20 @@ try:
print(f"Database file created or verified: {db_file}") print(f"Database file created or verified: {db_file}")
except Exception as e: except Exception as e:
print(f"Warning: Could not create database file: {e}") print(f"Warning: Could not create database file: {e}")
# Configure SQLite connection with simplified, robust settings # Configure SQLite connection with simplified, robust settings
engine = create_engine( engine = create_engine(
settings.SQLALCHEMY_DATABASE_URL, settings.SQLALCHEMY_DATABASE_URL,
connect_args={ connect_args={
"check_same_thread": False, "check_same_thread": False,
"timeout": 30, # Wait up to 30 seconds for the lock "timeout": 30, # Wait up to 30 seconds for the lock
}, },
# Minimal pool settings for stability # Minimal pool settings for stability
pool_pre_ping=True, # Verify connections before usage pool_pre_ping=True, # Verify connections before usage
echo=True, # Log all SQL for debugging echo=True, # Log all SQL for debugging
) )
# Add essential SQLite optimizations # Add essential SQLite optimizations
@event.listens_for(engine, "connect") @event.listens_for(engine, "connect")
def optimize_sqlite_connection(dbapi_connection, connection_record): def optimize_sqlite_connection(dbapi_connection, connection_record):
@ -46,9 +44,11 @@ def optimize_sqlite_connection(dbapi_connection, connection_record):
except Exception as e: except Exception as e:
print(f"Warning: Could not configure SQLite connection: {e}") print(f"Warning: Could not configure SQLite connection: {e}")
# Simplified Session factory # Simplified Session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# More robust database access with retry logic and error printing # More robust database access with retry logic and error printing
def get_db() -> Generator: def get_db() -> Generator:
""" """
@ -56,34 +56,35 @@ def get_db() -> Generator:
""" """
db = None db = None
retries = 3 retries = 3
for attempt in range(retries): for attempt in range(retries):
try: try:
db = SessionLocal() db = SessionLocal()
# Log connection attempt # Log connection attempt
print(f"Database connection attempt {attempt+1}") print(f"Database connection attempt {attempt + 1}")
# Test connection with a simple query # Test connection with a simple query
db.execute("SELECT 1") db.execute("SELECT 1")
# Connection succeeded # Connection succeeded
print("Database connection successful") print("Database connection successful")
yield db yield db
break break
except Exception as e: except Exception as e:
# Close failed connection # Close failed connection
if db: if db:
db.close() db.close()
db = None db = None
error_msg = f"Database connection attempt {attempt+1} failed: {e}" error_msg = f"Database connection attempt {attempt + 1} failed: {e}"
print(error_msg) print(error_msg)
# Log critical error details # Log critical error details
import traceback import traceback
print(f"Error traceback: {traceback.format_exc()}") print(f"Error traceback: {traceback.format_exc()}")
# Check if we can directly access database # Check if we can directly access database
try: try:
# Try direct sqlite3 connection as a test # Try direct sqlite3 connection as a test
@ -93,19 +94,19 @@ def get_db() -> Generator:
print("Direct SQLite connection succeeded but SQLAlchemy failed") print("Direct SQLite connection succeeded but SQLAlchemy failed")
except Exception as direct_e: except Exception as direct_e:
print(f"Direct SQLite connection also failed: {direct_e}") print(f"Direct SQLite connection also failed: {direct_e}")
# Last attempt - raise the error to return 500 status # Last attempt - raise the error to return 500 status
if attempt == retries - 1: if attempt == retries - 1:
print("All database connection attempts failed") print("All database connection attempts failed")
raise raise
# Otherwise sleep and retry # Otherwise sleep and retry
time.sleep(1) time.sleep(1)
# Always ensure db is closed # Always ensure db is closed
try: try:
if db: if db:
print("Closing database connection") print("Closing database connection")
db.close() db.close()
except Exception as e: except Exception as e:
print(f"Error closing database: {e}") print(f"Error closing database: {e}")

View File

@ -1,8 +1,17 @@
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Optional
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, Enum as SQLEnum from sqlalchemy import (
Column,
Integer,
String,
Text,
DateTime,
Boolean,
Enum as SQLEnum,
ForeignKey,
)
from sqlalchemy.orm import relationship
from app.db.base_class import Base from app.db.base_class import Base
@ -28,4 +37,8 @@ class Task(Base):
due_date = Column(DateTime, nullable=True) due_date = Column(DateTime, nullable=True)
completed = Column(Boolean, default=False) completed = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Add relationship to User model
user_id = Column(Integer, ForeignKey("user.id"), nullable=True)
user = relationship("User", back_populates="tasks")

20
app/models/user.py Normal file
View File

@ -0,0 +1,20 @@
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Boolean
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class User(Base):
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, index=True, nullable=False)
username = Column(String(100), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationship with Task model
tasks = relationship("Task", back_populates="user", cascade="all, delete-orphan")

View File

@ -13,7 +13,7 @@ class TaskBase(BaseModel):
status: TaskStatus = TaskStatus.TODO status: TaskStatus = TaskStatus.TODO
due_date: Optional[datetime] = None due_date: Optional[datetime] = None
completed: bool = False completed: bool = False
model_config = { model_config = {
"json_encoders": { "json_encoders": {
datetime: lambda dt: dt.isoformat(), datetime: lambda dt: dt.isoformat(),
@ -32,7 +32,7 @@ class TaskUpdate(BaseModel):
status: Optional[TaskStatus] = None status: Optional[TaskStatus] = None
due_date: Optional[datetime] = None due_date: Optional[datetime] = None
completed: Optional[bool] = None completed: Optional[bool] = None
model_config = { model_config = {
"json_encoders": { "json_encoders": {
datetime: lambda dt: dt.isoformat(), datetime: lambda dt: dt.isoformat(),
@ -45,10 +45,10 @@ class TaskUpdate(BaseModel):
"description": "Updated task description", "description": "Updated task description",
"priority": "high", "priority": "high",
"status": "in_progress", "status": "in_progress",
"completed": False "completed": False,
} }
] ]
} },
} }
@ -57,10 +57,8 @@ class TaskInDBBase(TaskBase):
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
model_config = { model_config = {"from_attributes": True}
"from_attributes": True
}
class Task(TaskInDBBase): class Task(TaskInDBBase):
pass pass

61
app/schemas/user.py Normal file
View File

@ -0,0 +1,61 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field, validator
class UserBase(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = None
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
class UserCreate(UserBase):
email: EmailStr = Field(..., description="User email")
username: str = Field(..., min_length=3, max_length=50, description="Username")
password: str = Field(..., min_length=8, description="Password")
@validator("username")
def username_valid(cls, v):
# Additional validation for username
if not v.isalnum():
raise ValueError("Username must be alphanumeric")
return v
class UserUpdate(UserBase):
password: Optional[str] = Field(None, min_length=8, description="Password")
class UserInDBBase(UserBase):
id: int
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class User(UserInDBBase):
"""Return user information without sensitive data"""
pass
class UserInDB(UserInDBBase):
"""User model stored in DB, with hashed password"""
hashed_password: str
class Token(BaseModel):
"""OAuth2 compatible token"""
access_token: str
token_type: str
class TokenPayload(BaseModel):
"""JWT token payload"""
sub: Optional[int] = None

136
main.py
View File

@ -1,15 +1,14 @@
import sys import sys
import os import os
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
# Add project root to Python path for imports in alembic migrations # Add project root to Python path for imports in alembic migrations
project_root = Path(__file__).parent.absolute() project_root = Path(__file__).parent.absolute()
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.api.routers import api_router from app.api.routers import api_router
from app.core.config import settings from app.core.config import settings
from app.db import init_db from app.db import init_db
@ -19,17 +18,19 @@ print("Starting database initialization...")
try: try:
# Get absolute path of the database file from your config # Get absolute path of the database file from your config
from app.db.session import db_file from app.db.session import db_file
print(f"Database path: {db_file}") print(f"Database path: {db_file}")
# Check directory permissions for the configured DB_DIR # Check directory permissions for the configured DB_DIR
from app.core.config import DB_DIR from app.core.config import DB_DIR
print(f"Database directory: {DB_DIR}") print(f"Database directory: {DB_DIR}")
print(f"Database directory exists: {DB_DIR.exists()}") print(f"Database directory exists: {DB_DIR.exists()}")
print(f"Database directory is writable: {os.access(DB_DIR, os.W_OK)}") print(f"Database directory is writable: {os.access(DB_DIR, os.W_OK)}")
# Initialize the database and create test task # Initialize the database and create test task
init_db.init_db() init_db.init_db()
# Try to create a test task # Try to create a test task
try: try:
init_db.create_test_task() init_db.create_test_task()
@ -39,6 +40,7 @@ try:
except Exception as e: except Exception as e:
print(f"Error initializing database: {e}") print(f"Error initializing database: {e}")
import traceback import traceback
print(f"Detailed error: {traceback.format_exc()}") print(f"Detailed error: {traceback.format_exc()}")
# Continue with app startup even if DB init fails, to allow debugging # Continue with app startup even if DB init fails, to allow debugging
@ -53,33 +55,34 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Add comprehensive exception handlers for better error reporting # Add comprehensive exception handlers for better error reporting
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception): async def global_exception_handler(request: Request, exc: Exception):
import traceback import traceback
import sys import sys
# Log the full error with traceback to stdout/stderr # Log the full error with traceback to stdout/stderr
error_tb = traceback.format_exc() error_tb = traceback.format_exc()
print(f"CRITICAL ERROR: {str(exc)}", file=sys.stderr) print(f"CRITICAL ERROR: {str(exc)}", file=sys.stderr)
print(f"Request path: {request.url.path}", file=sys.stderr) print(f"Request path: {request.url.path}", file=sys.stderr)
print(f"Traceback:\n{error_tb}", file=sys.stderr) print(f"Traceback:\n{error_tb}", file=sys.stderr)
# Get request info for debugging # Get request info for debugging
headers = dict(request.headers) headers = dict(request.headers)
# Remove sensitive headers # Remove sensitive headers
if 'authorization' in headers: if "authorization" in headers:
headers['authorization'] = '[REDACTED]' headers["authorization"] = "[REDACTED]"
if 'cookie' in headers: if "cookie" in headers:
headers['cookie'] = '[REDACTED]' headers["cookie"] = "[REDACTED]"
# Include minimal traceback in response for debugging # Include minimal traceback in response for debugging
tb_lines = error_tb.split('\n') tb_lines = error_tb.split("\n")
simplified_tb = [] simplified_tb = []
for line in tb_lines: for line in tb_lines:
if line and not line.startswith(' '): if line and not line.startswith(" "):
simplified_tb.append(line) simplified_tb.append(line)
# Create detailed error response # Create detailed error response
error_detail = { error_detail = {
"status": "error", "status": "error",
@ -87,29 +90,34 @@ async def global_exception_handler(request: Request, exc: Exception):
"type": str(type(exc).__name__), "type": str(type(exc).__name__),
"path": request.url.path, "path": request.url.path,
"method": request.method, "method": request.method,
"traceback_summary": simplified_tb[-10:] if len(simplified_tb) > 10 else simplified_tb, "traceback_summary": simplified_tb[-10:]
if len(simplified_tb) > 10
else simplified_tb,
} }
# Add SQLite diagnostic check # Add SQLite diagnostic check
try: try:
import sqlite3 import sqlite3
from app.db.session import db_file from app.db.session import db_file
# Try basic SQLite operations # Try basic SQLite operations
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("PRAGMA integrity_check") cursor.execute("PRAGMA integrity_check")
integrity = cursor.fetchone()[0] integrity = cursor.fetchone()[0]
# Check if task table exists # Check if task table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='task'") cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='task'"
)
task_table_exists = cursor.fetchone() is not None task_table_exists = cursor.fetchone() is not None
# Get file info # Get file info
import os import os
file_exists = os.path.exists(db_file) file_exists = os.path.exists(db_file)
file_size = os.path.getsize(db_file) if file_exists else 0 file_size = os.path.getsize(db_file) if file_exists else 0
# Add SQLite diagnostics to response # Add SQLite diagnostics to response
error_detail["db_diagnostics"] = { error_detail["db_diagnostics"] = {
"file_exists": file_exists, "file_exists": file_exists,
@ -117,11 +125,11 @@ async def global_exception_handler(request: Request, exc: Exception):
"integrity": integrity, "integrity": integrity,
"task_table_exists": task_table_exists, "task_table_exists": task_table_exists,
} }
conn.close() conn.close()
except Exception as db_error: except Exception as db_error:
error_detail["db_diagnostics"] = {"error": str(db_error)} error_detail["db_diagnostics"] = {"error": str(db_error)}
# Return the error response # Return the error response
print(f"Returning error response: {error_detail}") print(f"Returning error response: {error_detail}")
return JSONResponse( return JSONResponse(
@ -129,6 +137,7 @@ async def global_exception_handler(request: Request, exc: Exception):
content=error_detail, content=error_detail,
) )
# Include the API router directly (no version prefix) # Include the API router directly (no version prefix)
app.include_router(api_router) app.include_router(api_router)
@ -141,16 +150,23 @@ def api_info():
return { return {
"name": settings.PROJECT_NAME, "name": settings.PROJECT_NAME,
"version": "1.0.0", "version": "1.0.0",
"description": "A RESTful API for managing tasks", "description": "A RESTful API for managing tasks with user authentication",
"endpoints": { "endpoints": {
"authentication": {
"register": "/auth/register",
"login": "/auth/login",
"me": "/auth/me",
"test-token": "/auth/test-token",
},
"tasks": "/tasks", "tasks": "/tasks",
"docs": "/docs", "docs": "/docs",
"redoc": "/redoc", "redoc": "/redoc",
"health": "/health", "health": "/health",
"db_test": "/db-test" "db_test": "/db-test",
} },
} }
@app.get("/health", tags=["health"]) @app.get("/health", tags=["health"])
def health_check(): def health_check():
""" """
@ -172,54 +188,60 @@ def test_db_connection():
from sqlalchemy import text from sqlalchemy import text
from app.db.session import engine, db_file from app.db.session import engine, db_file
from app.core.config import DB_DIR from app.core.config import DB_DIR
# First check direct file access # First check direct file access
file_info = { file_info = {
"db_dir": str(DB_DIR), "db_dir": str(DB_DIR),
"db_file": str(db_file), "db_file": str(db_file),
"exists": os.path.exists(db_file), "exists": os.path.exists(db_file),
"size": os.path.getsize(db_file) if os.path.exists(db_file) else 0, "size": os.path.getsize(db_file) if os.path.exists(db_file) else 0,
"writable": os.access(db_file, os.W_OK) if os.path.exists(db_file) else False, "writable": os.access(db_file, os.W_OK)
"dir_writable": os.access(DB_DIR, os.W_OK) if os.path.exists(DB_DIR) else False if os.path.exists(db_file)
else False,
"dir_writable": os.access(DB_DIR, os.W_OK)
if os.path.exists(DB_DIR)
else False,
} }
# Try direct SQLite connection # Try direct SQLite connection
sqlite_test = {} sqlite_test = {}
try: try:
conn = sqlite3.connect(str(db_file)) conn = sqlite3.connect(str(db_file))
sqlite_test["connection"] = "successful" sqlite_test["connection"] = "successful"
# Check if task table exists # Check if task table exists
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = [row[0] for row in cursor.fetchall()] tables = [row[0] for row in cursor.fetchall()]
sqlite_test["tables"] = tables sqlite_test["tables"] = tables
# Check for task table specifically # Check for task table specifically
if 'task' in tables: if "task" in tables:
cursor.execute("SELECT COUNT(*) FROM task") cursor.execute("SELECT COUNT(*) FROM task")
task_count = cursor.fetchone()[0] task_count = cursor.fetchone()[0]
sqlite_test["task_count"] = task_count sqlite_test["task_count"] = task_count
# Get a sample task if available # Get a sample task if available
if task_count > 0: if task_count > 0:
cursor.execute("SELECT * FROM task LIMIT 1") cursor.execute("SELECT * FROM task LIMIT 1")
column_names = [description[0] for description in cursor.description] column_names = [
description[0] for description in cursor.description
]
row = cursor.fetchone() row = cursor.fetchone()
sample_task = dict(zip(column_names, row)) sample_task = dict(zip(column_names, row))
sqlite_test["sample_task"] = sample_task sqlite_test["sample_task"] = sample_task
# Check database integrity # Check database integrity
cursor.execute("PRAGMA integrity_check") cursor.execute("PRAGMA integrity_check")
integrity = cursor.fetchone()[0] integrity = cursor.fetchone()[0]
sqlite_test["integrity"] = integrity sqlite_test["integrity"] = integrity
conn.close() conn.close()
except Exception as e: except Exception as e:
sqlite_test["connection"] = "failed" sqlite_test["connection"] = "failed"
sqlite_test["error"] = str(e) sqlite_test["error"] = str(e)
sqlite_test["traceback"] = traceback.format_exc() sqlite_test["traceback"] = traceback.format_exc()
# Try SQLAlchemy connection # Try SQLAlchemy connection
sqlalchemy_test = {} sqlalchemy_test = {}
try: try:
@ -227,32 +249,38 @@ def test_db_connection():
# Basic connectivity test # Basic connectivity test
result = conn.execute(text("SELECT 1")).scalar() result = conn.execute(text("SELECT 1")).scalar()
sqlalchemy_test["basic_query"] = result sqlalchemy_test["basic_query"] = result
# Check tables # Check tables
result = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table'")) result = conn.execute(
text("SELECT name FROM sqlite_master WHERE type='table'")
)
tables = [row[0] for row in result] tables = [row[0] for row in result]
sqlalchemy_test["tables"] = tables sqlalchemy_test["tables"] = tables
# Check task table # Check task table
if 'task' in tables: if "task" in tables:
result = conn.execute(text("SELECT COUNT(*) FROM task")) result = conn.execute(text("SELECT COUNT(*) FROM task"))
sqlalchemy_test["task_count"] = result.scalar() sqlalchemy_test["task_count"] = result.scalar()
except Exception as e: except Exception as e:
sqlalchemy_test["connection"] = "failed" sqlalchemy_test["connection"] = "failed"
sqlalchemy_test["error"] = str(e) sqlalchemy_test["error"] = str(e)
sqlalchemy_test["traceback"] = traceback.format_exc() sqlalchemy_test["traceback"] = traceback.format_exc()
# Check environment # Check environment
env_info = { env_info = {
"cwd": os.getcwd(), "cwd": os.getcwd(),
"env_variables": {k: v for k, v in os.environ.items() if k.startswith(('DB_', 'SQL', 'PATH'))} "env_variables": {
k: v
for k, v in os.environ.items()
if k.startswith(("DB_", "SQL", "PATH"))
},
} }
# Try to create a test file # Try to create a test file
write_test = {} write_test = {}
try: try:
test_path = DB_DIR / "write_test.txt" test_path = DB_DIR / "write_test.txt"
with open(test_path, 'w') as f: with open(test_path, "w") as f:
f.write("Test content") f.write("Test content")
write_test["success"] = True write_test["success"] = True
write_test["path"] = str(test_path) write_test["path"] = str(test_path)
@ -260,7 +288,7 @@ def test_db_connection():
except Exception as e: except Exception as e:
write_test["success"] = False write_test["success"] = False
write_test["error"] = str(e) write_test["error"] = str(e)
return { return {
"status": "ok", "status": "ok",
"timestamp": datetime.datetime.utcnow().isoformat(), "timestamp": datetime.datetime.utcnow().isoformat(),
@ -268,13 +296,13 @@ def test_db_connection():
"sqlite_test": sqlite_test, "sqlite_test": sqlite_test,
"sqlalchemy_test": sqlalchemy_test, "sqlalchemy_test": sqlalchemy_test,
"environment": env_info, "environment": env_info,
"write_test": write_test "write_test": write_test,
} }
except Exception as e: except Exception as e:
# Catch-all error handler # Catch-all error handler
return { return {
"status": "error", "status": "error",
"message": str(e), "message": str(e),
"traceback": traceback.format_exc() "traceback": traceback.format_exc(),
} }

View File

@ -5,4 +5,8 @@ alembic>=1.12.0
pydantic>=2.4.2 pydantic>=2.4.2
pydantic-settings>=2.0.3 pydantic-settings>=2.0.3
python-multipart>=0.0.6 python-multipart>=0.0.6
ruff>=0.1.3 ruff>=0.1.3
passlib>=1.7.4
bcrypt>=4.0.1
python-jose>=3.3.0
email-validator>=2.0.0