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:
parent
a4cc5dbcca
commit
0ceeef31a6
121
README.md
121
README.md
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
58
alembic/versions/0002_add_user_table.py
Normal file
58
alembic/versions/0002_add_user_table.py
Normal 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")
|
@ -1 +1 @@
|
|||||||
# App package
|
# App package
|
||||||
|
@ -1 +1 @@
|
|||||||
# API package
|
# API package
|
||||||
|
@ -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
|
||||||
|
@ -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
95
app/api/routers/auth.py
Normal 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
|
@ -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)}",
|
||||||
)
|
)
|
||||||
|
@ -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
42
app/core/security.py
Normal 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)
|
@ -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"]
|
||||||
|
@ -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
|
||||||
|
@ -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
74
app/crud/user.py
Normal 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)
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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}")
|
||||||
|
@ -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
20
app/models/user.py
Normal 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")
|
@ -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
61
app/schemas/user.py
Normal 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
136
main.py
@ -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(),
|
||||||
}
|
}
|
||||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user