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
- 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 completion tracking
- 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
- Pydantic: Data validation and settings management
- 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
@ -25,14 +30,21 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
- `GET /`: Get API information and available endpoints
### Task Management
### Authentication
- `GET /tasks`: Get all tasks
- `POST /tasks`: Create a new task
- `GET /tasks/{task_id}`: Get a specific task
- `PUT /tasks/{task_id}`: Update a task
- `DELETE /tasks/{task_id}`: Delete a task
- `POST /tasks/{task_id}/complete`: Mark a task as completed
- `POST /auth/register`: Register a new user
- `POST /auth/login`: Login to get access token
- `GET /auth/me`: Get current user information
- `POST /auth/test-token`: Test if the access token is valid
### 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
@ -41,12 +53,37 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
## 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
curl -X 'POST' \
'https://taskmanagerapi-ttkjqk.backend.im/tasks/' \
-H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"title": "Complete project documentation",
@ -58,28 +95,31 @@ curl -X 'POST' \
}'
```
### Get all tasks
### Get all tasks (with authentication)
```bash
curl -X 'GET' \
'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
curl -X 'GET' \
'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
curl -X 'PUT' \
'https://taskmanagerapi-ttkjqk.backend.im/tasks/1' \
-H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"title": "Updated title",
@ -88,20 +128,31 @@ curl -X 'PUT' \
}'
```
### Delete a task
### Delete a task (with authentication)
```bash
curl -X 'DELETE' \
'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
curl -X 'POST' \
'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
@ -112,12 +163,28 @@ taskmanagerapi/
│ └── versions/ # Migration scripts
├── app/
│ ├── api/ # API endpoints
│ │ ├── deps.py # Dependency injection for authentication
│ │ └── routers/ # API route definitions
│ │ ├── auth.py # Authentication endpoints
│ │ └── tasks.py # Task management endpoints
│ ├── core/ # Core application code
│ │ ├── config.py # Application configuration
│ │ └── security.py # Security utilities (JWT, password hashing)
│ ├── 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
│ │ ├── task.py # Task model
│ │ └── user.py # User model
│ └── schemas/ # Pydantic schemas/models
│ ├── task.py # Task schemas
│ └── user.py # User and authentication schemas
├── main.py # Application entry point
└── requirements.txt # Project dependencies
```
@ -183,6 +250,22 @@ uvicorn main:app --reload
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
- Swagger UI: http://localhost:8000/docs

View File

@ -11,7 +11,7 @@ from sqlalchemy import pool
from alembic import context
# 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
# access to the values within the .ini file in use.
@ -66,6 +66,7 @@ if db_url.startswith("sqlite:///"):
# add your model's MetaData object here
# for 'autogenerate' support
from app.db.base import Base # noqa
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
@ -158,18 +159,22 @@ def run_migrations_online():
logger.error(f"Migration attempt {retry + 1} failed: {e}")
if retry < max_retries - 1:
import time
wait_time = (retry + 1) * 2 # Exponential backoff
logger.info(f"Retrying in {wait_time} seconds...")
time.sleep(wait_time)
# 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:
logger.error(f"Migration error: {e}")
# Print diagnostic information
from sqlalchemy import __version__ as sa_version
logger.error(f"SQLAlchemy version: {sa_version}")
# Get directory info

View File

@ -5,12 +5,12 @@ Revises:
Create Date: 2025-05-14
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic.
revision = '0001'
revision = "0001"
down_revision = None
branch_labels = None
depends_on = None
@ -18,21 +18,31 @@ depends_on = None
def upgrade():
op.create_table(
'task',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('priority', sa.Enum('low', 'medium', 'high', name='taskpriority'), default='medium'),
sa.Column('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')
"task",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sa.String(length=100), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column(
"priority",
sa.Enum("low", "medium", "high", name="taskpriority"),
default="medium",
),
sa.Column(
"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():
op.drop_index(op.f('ix_task_id'), table_name='task')
op.drop_table('task')
op.drop_index(op.f("ix_task_id"), table_name="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,4 +1,61 @@
from typing import Generator
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from pydantic import ValidationError
from sqlalchemy.orm import Session
# Use the improved get_db function from session.py
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 app.api.routers import tasks
from app.api.routers import tasks, auth
api_router = APIRouter()
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 fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
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.user import User
from app.schemas.task import Task, TaskCreate, TaskUpdate
router = APIRouter()
@ -17,14 +18,14 @@ def read_tasks(
skip: int = 0,
limit: int = 100,
status: Optional[TaskStatus] = None,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve tasks.
Retrieve tasks for the current user.
"""
try:
import traceback
import sqlite3
from sqlalchemy import text
from app.db.session import db_file
print(f"Getting tasks with status: {status}, skip: {skip}, limit: {limit}")
@ -32,9 +33,13 @@ def read_tasks(
# Try the normal SQLAlchemy approach first
try:
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:
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
except Exception as e:
print(f"Error getting tasks with SQLAlchemy: {e}")
@ -48,10 +53,15 @@ def read_tasks(
cursor = conn.cursor()
if status:
cursor.execute("SELECT * FROM task WHERE status = ? LIMIT ? OFFSET ?",
(status.value, limit, skip))
cursor.execute(
"SELECT * FROM task WHERE status = ? AND user_id = ? LIMIT ? OFFSET ?",
(status.value, current_user.id, limit, skip),
)
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()
@ -60,8 +70,8 @@ def read_tasks(
for row in rows:
task_dict = dict(row)
# Convert completed to boolean
if 'completed' in task_dict:
task_dict['completed'] = bool(task_dict['completed'])
if "completed" in task_dict:
task_dict["completed"] = bool(task_dict["completed"])
# Convert to object with attributes
class TaskResult:
@ -83,7 +93,7 @@ def read_tasks(
print(traceback.format_exc())
raise HTTPException(
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),
task_in: TaskCreate,
current_user: User = Depends(get_current_active_user),
) -> 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 time
@ -110,48 +121,48 @@ def create_task(
try:
# Extract task data regardless of Pydantic version
try:
if hasattr(task_in, 'model_dump'):
if hasattr(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()
else:
# Fallback for any case
task_data = {
'title': getattr(task_in, 'title', 'Untitled Task'),
'description': getattr(task_in, 'description', ''),
'priority': getattr(task_in, 'priority', 'medium'),
'status': getattr(task_in, 'status', 'todo'),
'due_date': getattr(task_in, 'due_date', None),
'completed': getattr(task_in, 'completed', False)
"title": getattr(task_in, "title", "Untitled Task"),
"description": getattr(task_in, "description", ""),
"priority": getattr(task_in, "priority", "medium"),
"status": getattr(task_in, "status", "todo"),
"due_date": getattr(task_in, "due_date", None),
"completed": getattr(task_in, "completed", False),
}
print(f"Task data: {task_data}")
except Exception as e:
print(f"Error extracting task data: {e}")
# Fallback to minimal data
task_data = {
'title': str(getattr(task_in, 'title', 'Unknown Title')),
'description': str(getattr(task_in, 'description', '')),
'priority': 'medium',
'status': 'todo',
'completed': False
"title": str(getattr(task_in, "title", "Unknown Title")),
"description": str(getattr(task_in, "description", "")),
"priority": "medium",
"status": "todo",
"completed": False,
}
# Format due_date if present
if task_data.get('due_date'):
if task_data.get("due_date"):
try:
if isinstance(task_data['due_date'], datetime):
task_data['due_date'] = task_data['due_date'].isoformat()
elif isinstance(task_data['due_date'], str):
if isinstance(task_data["due_date"], datetime):
task_data["due_date"] = task_data["due_date"].isoformat()
elif isinstance(task_data["due_date"], str):
# Standardize format by parsing and reformatting
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:
print(f"Warning: Could not parse due_date: {e}")
# Keep as-is or set to None if invalid
if not isinstance(task_data['due_date'], str):
task_data['due_date'] = None
if not isinstance(task_data["due_date"], str):
task_data["due_date"] = None
# Get current timestamp for created/updated fields
now = datetime.utcnow().isoformat()
@ -175,7 +186,8 @@ def create_task(
due_date TEXT,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER
)
""")
@ -184,19 +196,20 @@ def create_task(
"""
INSERT INTO task (
title, description, priority, status,
due_date, completed, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
due_date, completed, created_at, updated_at, user_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
task_data.get('title', 'Untitled'),
task_data.get('description', ''),
task_data.get('priority', 'medium'),
task_data.get('status', 'todo'),
task_data.get('due_date'),
1 if task_data.get('completed') else 0,
task_data.get("title", "Untitled"),
task_data.get("description", ""),
task_data.get("priority", "medium"),
task_data.get("status", "todo"),
task_data.get("due_date"),
1 if task_data.get("completed") else 0,
now,
now
)
now,
current_user.id,
),
)
# Get the ID of the inserted task
@ -207,7 +220,10 @@ def create_task(
conn.commit()
# 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()
if row:
@ -218,8 +234,8 @@ def create_task(
task_dict = dict(zip(column_names, row))
# Convert 'completed' to boolean
if 'completed' in task_dict:
task_dict['completed'] = bool(task_dict['completed'])
if "completed" in task_dict:
task_dict["completed"] = bool(task_dict["completed"])
# Create an object that mimics the Task model
class TaskResult:
@ -234,7 +250,9 @@ def create_task(
return TaskResult(**task_dict)
else:
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:
if conn:
@ -243,7 +261,9 @@ def create_task(
# Check if retry is appropriate
if "database is locked" in str(e) and retry < 2:
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)
else:
print(f"SQLite operational error: {e}")
@ -254,7 +274,7 @@ def create_task(
# Try to rollback if connection is still open
try:
conn.rollback()
except:
except Exception:
pass
conn.close()
@ -262,7 +282,9 @@ def create_task(
print(traceback.format_exc())
# 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)
continue
raise
@ -276,7 +298,9 @@ def create_task(
print(f"Direct SQLite approach failed: {sqlite_error}")
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}")
return task
@ -289,8 +313,7 @@ def create_task(
print(f"Final error: {error_detail}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_detail
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error_detail
)
@ -299,9 +322,10 @@ def read_task(
*,
db: Session = Depends(get_db),
task_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get task by ID.
Get task by ID for the current user.
"""
try:
import traceback
@ -312,10 +336,12 @@ def read_task(
# Try the normal SQLAlchemy approach first
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:
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:
print(f"Error getting task with SQLAlchemy: {e}")
print(traceback.format_exc())
@ -327,7 +353,10 @@ def read_task(
conn.row_factory = sqlite3.Row
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()
if not row:
@ -338,8 +367,8 @@ def read_task(
task_dict = dict(row)
# Convert completed to boolean
if 'completed' in task_dict:
task_dict['completed'] = bool(task_dict['completed'])
if "completed" in task_dict:
task_dict["completed"] = bool(task_dict["completed"])
# Convert to object with attributes
class TaskResult:
@ -356,7 +385,7 @@ def read_task(
print(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error retrieving task: {str(e)}"
detail=f"Error retrieving task: {str(e)}",
)
except HTTPException:
@ -366,7 +395,7 @@ def read_task(
print(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error retrieving task: {str(e)}"
detail=f"Error retrieving task: {str(e)}",
)
@ -376,14 +405,14 @@ def update_task(
db: Session = Depends(get_db),
task_id: int,
task_in: TaskUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a task.
Update a task for the current user.
"""
try:
import traceback
import sqlite3
import json
from datetime import datetime
from app.db.session import db_file
@ -393,13 +422,17 @@ def update_task(
if hasattr(task_in, "due_date") and task_in.due_date is not None:
if isinstance(task_in.due_date, str):
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:
print(f"Error parsing due_date: {e}")
# Try the normal SQLAlchemy approach first
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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -420,8 +453,11 @@ def update_task(
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# First check if task exists
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,))
# First check if task exists and belongs to current user
cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
row = cursor.fetchone()
if not row:
@ -432,7 +468,11 @@ def update_task(
# Convert Pydantic model to dict, excluding unset values
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
for key, value in model_data.items():
@ -443,8 +483,8 @@ def update_task(
# No updates provided
task_dict = dict(row)
# Convert completed to boolean
if 'completed' in task_dict:
task_dict['completed'] = bool(task_dict['completed'])
if "completed" in task_dict:
task_dict["completed"] = bool(task_dict["completed"])
# Return the unchanged task
class TaskResult:
@ -456,11 +496,11 @@ def update_task(
return TaskResult(**task_dict)
# Format datetime objects
if 'due_date' in updates and isinstance(updates['due_date'], datetime):
updates['due_date'] = updates['due_date'].isoformat()
if "due_date" in updates and isinstance(updates["due_date"], datetime):
updates["due_date"] = updates["due_date"].isoformat()
# Add updated_at timestamp
updates['updated_at'] = datetime.utcnow().isoformat()
updates["updated_at"] = datetime.utcnow().isoformat()
# Build the SQL update statement
set_clause = ", ".join([f"{key} = ?" for key in updates.keys()])
@ -471,15 +511,18 @@ def update_task(
conn.commit()
# 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()
conn.close()
if updated_row:
task_dict = dict(updated_row)
# Convert completed to boolean
if 'completed' in task_dict:
task_dict['completed'] = bool(task_dict['completed'])
if "completed" in task_dict:
task_dict["completed"] = bool(task_dict["completed"])
# Convert to object with attributes
class TaskResult:
@ -498,7 +541,7 @@ def update_task(
print(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating task: {str(e)}"
detail=f"Error updating task: {str(e)}",
)
except HTTPException:
@ -508,7 +551,7 @@ def update_task(
print(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating task: {str(e)}"
detail=f"Error updating task: {str(e)}",
)
@ -517,9 +560,10 @@ def delete_task(
*,
db: Session = Depends(get_db),
task_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a task.
Delete a task for the current user.
"""
try:
import traceback
@ -528,20 +572,18 @@ def delete_task(
print(f"Deleting task with ID: {task_id}")
# First, get the task to return it later
task_to_return = None
# Try the normal SQLAlchemy approach first
# Try the normal SQLAlchemy approach
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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
task_to_return = task
task = crud.task.remove(db, id=task_id)
return task
removed_task = crud.task.remove(db, id=task_id)
return removed_task
except HTTPException:
raise # Re-raise the 404 exception
except Exception as e:
@ -556,7 +598,10 @@ def delete_task(
cursor = conn.cursor()
# 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()
if not row:
@ -567,11 +612,14 @@ def delete_task(
task_dict = dict(row)
# Convert completed to boolean
if 'completed' in task_dict:
task_dict['completed'] = bool(task_dict['completed'])
if "completed" in task_dict:
task_dict["completed"] = bool(task_dict["completed"])
# 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.close()
@ -590,7 +638,7 @@ def delete_task(
print(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting task: {str(e)}"
detail=f"Error deleting task: {str(e)}",
)
except HTTPException:
@ -600,7 +648,7 @@ def delete_task(
print(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting task: {str(e)}"
detail=f"Error deleting task: {str(e)}",
)
@ -609,9 +657,10 @@ def complete_task(
*,
db: Session = Depends(get_db),
task_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Mark a task as completed.
Mark a task as completed for the current user.
"""
try:
import traceback
@ -623,7 +672,9 @@ def complete_task(
# Try the normal SQLAlchemy approach first
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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -641,8 +692,11 @@ def complete_task(
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# First check if task exists
cursor.execute("SELECT * FROM task WHERE id = ?", (task_id,))
# First check if task exists and belongs to current user
cursor.execute(
"SELECT * FROM task WHERE id = ? AND user_id = ?",
(task_id, current_user.id),
)
row = cursor.fetchone()
if not row:
@ -654,21 +708,24 @@ def complete_task(
# Update task to completed status
now = datetime.utcnow().isoformat()
cursor.execute(
"UPDATE task SET completed = ?, status = ?, updated_at = ? WHERE id = ?",
(1, "done", now, task_id)
"UPDATE task SET completed = ?, status = ?, updated_at = ? WHERE id = ? AND user_id = ?",
(1, "done", now, task_id, current_user.id),
)
conn.commit()
# 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()
conn.close()
if updated_row:
task_dict = dict(updated_row)
# Convert completed to boolean
if 'completed' in task_dict:
task_dict['completed'] = bool(task_dict['completed'])
if "completed" in task_dict:
task_dict["completed"] = bool(task_dict["completed"])
# Convert to object with attributes
class TaskResult:
@ -687,7 +744,7 @@ def complete_task(
print(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error completing task: {str(e)}"
detail=f"Error completing task: {str(e)}",
)
except HTTPException:
@ -697,5 +754,5 @@ def complete_task(
print(traceback.format_exc())
raise HTTPException(
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
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_settings import BaseSettings
@ -18,7 +18,7 @@ else:
paths_to_try = [
Path("/app/db"), # Production path
Path.cwd() / "db", # Local development path
Path("/tmp/taskmanager") # Fallback path
Path("/tmp/taskmanager"), # Fallback path
]
# Find the first writable path
@ -42,9 +42,10 @@ else:
print(f"Falling back to temporary directory: {DB_DIR}")
try:
Path("/tmp").mkdir(exist_ok=True)
except:
except Exception:
pass
class Settings(BaseSettings):
PROJECT_NAME: str = "Task Manager API"
# No API version prefix - use direct paths
@ -67,9 +68,7 @@ class Settings(BaseSettings):
# Database configuration
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
model_config = {
"case_sensitive": True
}
model_config = {"case_sensitive": True}
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

@ -43,6 +43,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
error_msg = f"Error creating {self.model.__name__}: {str(e)}"
print(error_msg)
import traceback
print(traceback.format_exc())
raise Exception(error_msg) from e
@ -51,7 +52,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
obj_in: Union[UpdateSchemaType, Dict[str, Any]],
) -> ModelType:
try:
# Log update operation
@ -92,6 +93,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
error_msg = f"Error updating {self.model.__name__}: {str(e)}"
print(error_msg)
import traceback
print(traceback.format_exc())
raise Exception(error_msg) from e
@ -117,5 +119,6 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
error_msg = f"Error deleting {self.model.__name__}: {str(e)}"
print(error_msg)
import traceback
print(traceback.format_exc())
raise Exception(error_msg) from e

View File

@ -7,20 +7,68 @@ from app.schemas.task import TaskCreate, TaskUpdate
class CRUDTask(CRUDBase[Task, TaskCreate, TaskUpdate]):
def get_by_status(self, db: Session, *, status: TaskStatus) -> List[Task]:
return db.query(self.model).filter(Task.status == status).all()
def get_by_status(
self, db: Session, *, status: TaskStatus, user_id: Optional[int] = None
) -> List[Task]:
query = db.query(self.model).filter(Task.status == status)
if user_id is not None:
query = query.filter(Task.user_id == user_id)
return query.all()
def get_completed(self, db: Session) -> List[Task]:
return db.query(self.model).filter(Task.completed == True).all()
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 mark_completed(self, db: Session, *, task_id: int) -> Optional[Task]:
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:
return None
task_in = TaskUpdate(
status=TaskStatus.DONE,
completed=True
)
task_in = TaskUpdate(status=TaskStatus.DONE, completed=True)
return self.update(db, db_obj=task, obj_in=task_in)

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

@ -2,3 +2,4 @@
# imported by Alembic
from app.db.base_class import Base # noqa
from app.models.task import Task # noqa
from app.models.user import User # noqa

View File

@ -1,12 +1,8 @@
import os
import sys
import time
import sqlite3
from pathlib import Path
from datetime import datetime, timedelta
from sqlalchemy import inspect, text
from sqlalchemy.exc import OperationalError
from sqlalchemy import text
from app.db.base import Base # Import all models
from app.db.session import engine, db_file
@ -25,7 +21,7 @@ def init_db() -> None:
# First try direct SQLite approach to ensure we have a basic database file
try:
# 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
print(f"Database file exists and is writable: {db_file}")
@ -48,32 +44,88 @@ def init_db() -> None:
due_date TEXT,
completed INTEGER DEFAULT 0,
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
)
""")
# 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_user_id ON task(user_id)")
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 sample task if the table is empty
# Add a default admin user if the user table is empty
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM task")
count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM user")
user_count = cursor.fetchone()[0]
if count == 0:
if user_count == 0:
now = datetime.utcnow().isoformat()
conn.execute("""
INSERT INTO task (title, description, priority, status, completed, created_at, updated_at)
# Import get_password_hash function here to avoid F823 error
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 (?, ?, ?, ?, ?, ?, ?)
""", (
""",
(
"admin@example.com",
"admin",
admin_password_hash,
1, # is_active
1, # is_superuser
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
))
now,
admin_id,
),
)
conn.commit()
cursor.close()
@ -82,6 +134,7 @@ def init_db() -> None:
except Exception as e:
print(f"Error during direct SQLite initialization: {e}")
import traceback
print(traceback.format_exc())
# Now try with SQLAlchemy as a backup approach
@ -95,45 +148,85 @@ def init_db() -> None:
# Verify tables exist
with engine.connect() as conn:
# Get list of 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]
print(f"Tables in database: {', '.join(tables)}")
# Verify user table exists
if "user" in tables:
# Check if user table is empty
result = conn.execute(text("SELECT COUNT(*) FROM user"))
user_count = result.scalar()
print(f"User table contains {user_count} records")
# If user table is empty, add default admin user
if user_count == 0:
print("Adding default admin user with SQLAlchemy")
from app.models.user import User
from app.core.security import get_password_hash
admin_user = User(
email="admin@example.com",
username="admin",
hashed_password=get_password_hash("adminpassword"),
is_active=True,
is_superuser=True,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
from app.db.session import SessionLocal
db = SessionLocal()
db.add(admin_user)
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:
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:
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()
updated_at=datetime.utcnow(),
)
from app.db.session import SessionLocal
db = SessionLocal()
db.add(sample_task)
db.commit()
db.close()
print("Added sample task with SQLAlchemy")
db.close()
print("Added default admin user with SQLAlchemy")
else:
print("WARNING: 'user' table not found!")
if "task" not in tables:
print("WARNING: 'task' table not found!")
print("SQLAlchemy database initialization completed")
except Exception as e:
print(f"Error during SQLAlchemy initialization: {e}")
import traceback
print(traceback.format_exc())
print("Continuing despite SQLAlchemy initialization error...")
@ -148,7 +241,9 @@ def create_test_task():
cursor = conn.cursor()
# 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():
print("Task table doesn't exist - cannot create test task")
return
@ -174,8 +269,8 @@ def create_test_task():
"todo",
0, # not completed
now,
now
)
now,
),
)
conn.commit()
task_id = cursor.lastrowid
@ -199,9 +294,13 @@ def create_test_task():
try:
# Check if there are any tasks
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:
print(f"Test task not needed, found {existing_tasks} existing tasks")
print(
f"Test task not needed, found {existing_tasks} existing tasks"
)
return
except Exception as e:
print(f"Error checking for existing tasks: {e}")
@ -214,7 +313,7 @@ def create_test_task():
priority="medium",
status="todo",
due_date=datetime.utcnow() + timedelta(days=7),
completed=False
completed=False,
)
created_task = task_crud.create(db, obj_in=test_task)
@ -228,6 +327,7 @@ def create_test_task():
except Exception as e:
print(f"Global error creating test task: {e}")
import traceback
print(traceback.format_exc())

View File

@ -1,12 +1,9 @@
import os
import time
import sqlite3
from typing import Generator
from pathlib import Path
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import OperationalError, SQLAlchemyError
from app.core.config import settings, DB_DIR
@ -35,6 +32,7 @@ engine = create_engine(
echo=True, # Log all SQL for debugging
)
# Add essential SQLite optimizations
@event.listens_for(engine, "connect")
def optimize_sqlite_connection(dbapi_connection, connection_record):
@ -46,9 +44,11 @@ def optimize_sqlite_connection(dbapi_connection, connection_record):
except Exception as e:
print(f"Warning: Could not configure SQLite connection: {e}")
# Simplified Session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# More robust database access with retry logic and error printing
def get_db() -> Generator:
"""
@ -82,6 +82,7 @@ def get_db() -> Generator:
# Log critical error details
import traceback
print(f"Error traceback: {traceback.format_exc()}")
# Check if we can directly access database

View File

@ -1,8 +1,17 @@
from datetime import datetime
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
@ -29,3 +38,7 @@ class Task(Base):
completed = Column(Boolean, default=False)
created_at = Column(DateTime, default=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

@ -45,10 +45,10 @@ class TaskUpdate(BaseModel):
"description": "Updated task description",
"priority": "high",
"status": "in_progress",
"completed": False
"completed": False,
}
]
}
},
}
@ -57,9 +57,7 @@ class TaskInDBBase(TaskBase):
created_at: datetime
updated_at: datetime
model_config = {
"from_attributes": True
}
model_config = {"from_attributes": True}
class Task(TaskInDBBase):

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

78
main.py
View File

@ -1,15 +1,14 @@
import sys
import os
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
project_root = Path(__file__).parent.absolute()
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.core.config import settings
from app.db import init_db
@ -19,10 +18,12 @@ print("Starting database initialization...")
try:
# Get absolute path of the database file from your config
from app.db.session import db_file
print(f"Database path: {db_file}")
# Check directory permissions for the configured DB_DIR
from app.core.config import DB_DIR
print(f"Database directory: {DB_DIR}")
print(f"Database directory exists: {DB_DIR.exists()}")
print(f"Database directory is writable: {os.access(DB_DIR, os.W_OK)}")
@ -39,6 +40,7 @@ try:
except Exception as e:
print(f"Error initializing database: {e}")
import traceback
print(f"Detailed error: {traceback.format_exc()}")
# Continue with app startup even if DB init fails, to allow debugging
@ -53,6 +55,7 @@ app.add_middleware(
allow_headers=["*"],
)
# Add comprehensive exception handlers for better error reporting
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
@ -68,16 +71,16 @@ async def global_exception_handler(request: Request, exc: Exception):
# Get request info for debugging
headers = dict(request.headers)
# Remove sensitive headers
if 'authorization' in headers:
headers['authorization'] = '[REDACTED]'
if 'cookie' in headers:
headers['cookie'] = '[REDACTED]'
if "authorization" in headers:
headers["authorization"] = "[REDACTED]"
if "cookie" in headers:
headers["cookie"] = "[REDACTED]"
# Include minimal traceback in response for debugging
tb_lines = error_tb.split('\n')
tb_lines = error_tb.split("\n")
simplified_tb = []
for line in tb_lines:
if line and not line.startswith(' '):
if line and not line.startswith(" "):
simplified_tb.append(line)
# Create detailed error response
@ -87,7 +90,9 @@ async def global_exception_handler(request: Request, exc: Exception):
"type": str(type(exc).__name__),
"path": request.url.path,
"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
@ -102,11 +107,14 @@ async def global_exception_handler(request: Request, exc: Exception):
integrity = cursor.fetchone()[0]
# 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
# Get file info
import os
file_exists = os.path.exists(db_file)
file_size = os.path.getsize(db_file) if file_exists else 0
@ -129,6 +137,7 @@ async def global_exception_handler(request: Request, exc: Exception):
content=error_detail,
)
# Include the API router directly (no version prefix)
app.include_router(api_router)
@ -141,16 +150,23 @@ def api_info():
return {
"name": settings.PROJECT_NAME,
"version": "1.0.0",
"description": "A RESTful API for managing tasks",
"description": "A RESTful API for managing tasks with user authentication",
"endpoints": {
"authentication": {
"register": "/auth/register",
"login": "/auth/login",
"me": "/auth/me",
"test-token": "/auth/test-token",
},
"tasks": "/tasks",
"docs": "/docs",
"redoc": "/redoc",
"health": "/health",
"db_test": "/db-test"
}
"db_test": "/db-test",
},
}
@app.get("/health", tags=["health"])
def health_check():
"""
@ -179,8 +195,12 @@ def test_db_connection():
"db_file": str(db_file),
"exists": os.path.exists(db_file),
"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,
"dir_writable": os.access(DB_DIR, os.W_OK) if os.path.exists(DB_DIR) else False
"writable": os.access(db_file, os.W_OK)
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
@ -196,7 +216,7 @@ def test_db_connection():
sqlite_test["tables"] = tables
# Check for task table specifically
if 'task' in tables:
if "task" in tables:
cursor.execute("SELECT COUNT(*) FROM task")
task_count = cursor.fetchone()[0]
sqlite_test["task_count"] = task_count
@ -204,7 +224,9 @@ def test_db_connection():
# Get a sample task if available
if task_count > 0:
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()
sample_task = dict(zip(column_names, row))
sqlite_test["sample_task"] = sample_task
@ -229,12 +251,14 @@ def test_db_connection():
sqlalchemy_test["basic_query"] = result
# 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]
sqlalchemy_test["tables"] = tables
# Check task table
if 'task' in tables:
if "task" in tables:
result = conn.execute(text("SELECT COUNT(*) FROM task"))
sqlalchemy_test["task_count"] = result.scalar()
except Exception as e:
@ -245,14 +269,18 @@ def test_db_connection():
# Check environment
env_info = {
"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
write_test = {}
try:
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")
write_test["success"] = True
write_test["path"] = str(test_path)
@ -268,7 +296,7 @@ def test_db_connection():
"sqlite_test": sqlite_test,
"sqlalchemy_test": sqlalchemy_test,
"environment": env_info,
"write_test": write_test
"write_test": write_test,
}
except Exception as e:
@ -276,5 +304,5 @@ def test_db_connection():
return {
"status": "error",
"message": str(e),
"traceback": traceback.format_exc()
"traceback": traceback.format_exc(),
}

View File

@ -6,3 +6,7 @@ pydantic>=2.4.2
pydantic-settings>=2.0.3
python-multipart>=0.0.6
ruff>=0.1.3
passlib>=1.7.4
bcrypt>=4.0.1
python-jose>=3.3.0
email-validator>=2.0.0