Complete rebuild: Node.js/Express/TypeScript WhatsApp AI task scheduling service
- Replaced Python/FastAPI implementation with Node.js/Express/TypeScript
- Added Prisma ORM with SQLite database
- Implemented JWT authentication with bcrypt password hashing
- Created comprehensive task management API with CRUD operations
- Integrated Twilio WhatsApp Business API for notifications
- Added OpenAI integration for intelligent task scheduling
- Implemented automated background jobs with node-cron
- Added comprehensive error handling and validation
- Structured logging with Winston
- Complete API documentation and setup instructions
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7446415eec
commit
a8270cc5cb
25
.eslintrc.js
Normal file
25
.eslintrc.js
Normal file
@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'@typescript-eslint/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js', 'dist/**/*'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
},
|
||||
};
|
206
README.md
206
README.md
@ -1,3 +1,205 @@
|
||||
# FastAPI Application
|
||||
# WhatsApp AI Task Scheduling Service
|
||||
|
||||
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
|
||||
A Node.js/Express/TypeScript application that provides intelligent task scheduling with WhatsApp notifications and AI-powered optimization.
|
||||
|
||||
## Features
|
||||
|
||||
- **Task Management**: Create, update, delete, and track tasks with priorities and due dates
|
||||
- **AI-Powered Scheduling**: Uses OpenAI to suggest optimal times for task completion
|
||||
- **WhatsApp Integration**: Sends task reminders and notifications via WhatsApp using Twilio
|
||||
- **User Authentication**: JWT-based authentication system
|
||||
- **Automated Scheduling**: Background jobs for daily reminders and overdue task notifications
|
||||
- **Health Monitoring**: Built-in health check endpoints
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Runtime**: Node.js with TypeScript
|
||||
- **Framework**: Express.js
|
||||
- **Database**: SQLite with Prisma ORM
|
||||
- **Authentication**: JWT tokens with bcrypt password hashing
|
||||
- **WhatsApp**: Twilio WhatsApp Business API
|
||||
- **AI**: OpenAI GPT-3.5 for intelligent scheduling
|
||||
- **Scheduling**: node-cron for background jobs
|
||||
- **Logging**: Winston for structured logging
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ and npm
|
||||
- Environment variables (see below)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Copy environment configuration:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. Configure environment variables in `.env` file
|
||||
|
||||
5. Generate Prisma client and run migrations:
|
||||
```bash
|
||||
npm run prisma:generate
|
||||
npm run prisma:migrate
|
||||
```
|
||||
|
||||
6. Build the application:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
7. Start the server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
For development:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Required Environment Variables
|
||||
|
||||
```env
|
||||
# Database
|
||||
DATABASE_URL="file:./storage/db/database.db"
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET="your-super-secret-jwt-key-here"
|
||||
JWT_EXPIRES_IN="24h"
|
||||
|
||||
# Twilio WhatsApp Integration
|
||||
TWILIO_ACCOUNT_SID="your-twilio-account-sid"
|
||||
TWILIO_AUTH_TOKEN="your-twilio-auth-token"
|
||||
TWILIO_WHATSAPP_FROM="whatsapp:+14155238886"
|
||||
|
||||
# OpenAI for AI Features
|
||||
OPENAI_API_KEY="your-openai-api-key"
|
||||
|
||||
# Server Configuration
|
||||
PORT=3000
|
||||
NODE_ENV="development"
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Base URL
|
||||
- **GET** `/` - Service information and documentation links
|
||||
- **GET** `/docs` - API documentation
|
||||
- **GET** `/api/health` - Health check endpoint
|
||||
|
||||
### Authentication
|
||||
- **POST** `/api/auth/register` - Register new user
|
||||
- **POST** `/api/auth/login` - User login
|
||||
|
||||
### Tasks (Protected Routes)
|
||||
- **POST** `/api/tasks` - Create new task
|
||||
- **GET** `/api/tasks` - Get user's tasks (with pagination and filtering)
|
||||
- **GET** `/api/tasks/:id` - Get specific task
|
||||
- **PUT** `/api/tasks/:id` - Update task
|
||||
- **DELETE** `/api/tasks/:id` - Delete task
|
||||
- **POST** `/api/tasks/:id/remind` - Send WhatsApp reminder for task
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Register User
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword",
|
||||
"fullName": "John Doe",
|
||||
"whatsappNumber": "+1234567890"
|
||||
}'
|
||||
```
|
||||
|
||||
### Create Task
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/tasks \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"title": "Complete project proposal",
|
||||
"description": "Write and review the Q1 project proposal",
|
||||
"priority": "HIGH",
|
||||
"dueDate": "2024-02-15T17:00:00.000Z"
|
||||
}'
|
||||
```
|
||||
|
||||
## Features Details
|
||||
|
||||
### AI-Powered Scheduling
|
||||
The service uses OpenAI to analyze task priority, due dates, and optimal work hours to suggest the best time to work on tasks.
|
||||
|
||||
### WhatsApp Notifications
|
||||
- Daily reminders for scheduled tasks (9 AM UTC)
|
||||
- Overdue task notifications (6 PM UTC)
|
||||
- Manual reminder triggers via API
|
||||
|
||||
### Background Jobs
|
||||
- **Daily Reminders**: Sends WhatsApp reminders for tasks scheduled for today
|
||||
- **Overdue Checks**: Notifies users about overdue tasks
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Users Table
|
||||
- `id`, `email`, `password` (hashed)
|
||||
- `fullName`, `phoneNumber`, `whatsappNumber`
|
||||
- `isActive`, `createdAt`, `updatedAt`
|
||||
|
||||
### Tasks Table
|
||||
- `id`, `title`, `description`
|
||||
- `status` (PENDING, IN_PROGRESS, COMPLETED, CANCELLED)
|
||||
- `priority` (LOW, MEDIUM, HIGH, URGENT)
|
||||
- `dueDate`, `scheduledAt`, `aiSuggestedTime`
|
||||
- `whatsappReminderSent`, `whatsappCompletionSent`
|
||||
- `ownerId`, timestamps
|
||||
|
||||
## Development
|
||||
|
||||
### Available Scripts
|
||||
- `npm run dev` - Start development server with hot reload
|
||||
- `npm run build` - Build TypeScript to JavaScript
|
||||
- `npm start` - Start production server
|
||||
- `npm run lint` - Run ESLint
|
||||
- `npm run lint:fix` - Fix ESLint issues
|
||||
- `npm run prisma:generate` - Generate Prisma client
|
||||
- `npm run prisma:migrate` - Run database migrations
|
||||
- `npm run prisma:studio` - Open Prisma Studio
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
src/
|
||||
├── controllers/ # Request handlers
|
||||
├── middleware/ # Express middleware
|
||||
├── models/ # Database models (Prisma)
|
||||
├── routes/ # API route definitions
|
||||
├── services/ # Business logic services
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The application includes comprehensive error handling with:
|
||||
- Structured error responses
|
||||
- Request validation using Joi
|
||||
- Database error handling
|
||||
- Authentication error handling
|
||||
- Rate limiting protection
|
||||
|
||||
## Logging
|
||||
|
||||
Winston logger provides structured logging with different levels:
|
||||
- Development: Console output with colors
|
||||
- Production: File-based logging (error.log, combined.log)
|
||||
|
||||
## Contributing
|
||||
|
||||
This project was generated by BackendIM, an AI-powered backend generation platform.
|
||||
|
41
alembic.ini
41
alembic.ini
@ -1,41 +0,0 @@
|
||||
[alembic]
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
version_path_separator = os
|
||||
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
@ -1,55 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
sys.path = ['', '..'] + sys.path[1:]
|
||||
|
||||
from app.db.base import Base
|
||||
from app.models.user import User
|
||||
from app.models.task import Task
|
||||
|
||||
config = context.config
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
@ -1,24 +0,0 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
@ -1,63 +0,0 @@
|
||||
"""Initial migration
|
||||
|
||||
Revision ID: 001
|
||||
Revises:
|
||||
Create Date: 2024-01-01 00:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '001'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('email', sa.String(), nullable=False),
|
||||
sa.Column('hashed_password', sa.String(), nullable=False),
|
||||
sa.Column('full_name', sa.String(), nullable=True),
|
||||
sa.Column('phone_number', sa.String(), nullable=True),
|
||||
sa.Column('whatsapp_number', sa.String(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
||||
op.create_table('tasks',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sa.String(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('status', sa.Enum('PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', name='taskstatus'), nullable=True),
|
||||
sa.Column('priority', sa.Enum('LOW', 'MEDIUM', 'HIGH', 'URGENT', name='taskpriority'), nullable=True),
|
||||
sa.Column('due_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('scheduled_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('ai_suggested_time', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('whatsapp_reminder_sent', sa.Boolean(), nullable=True),
|
||||
sa.Column('whatsapp_completion_sent', sa.Boolean(), nullable=True),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tasks_id'), 'tasks', ['id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_tasks_id'), table_name='tasks')
|
||||
op.drop_table('tasks')
|
||||
op.drop_index(op.f('ix_users_id'), table_name='users')
|
||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||
op.drop_table('users')
|
||||
# ### end Alembic commands ###
|
@ -1,53 +0,0 @@
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.api.schemas import UserCreate, UserResponse, UserLogin, Token
|
||||
from app.core.security import verify_password, get_password_hash, create_access_token
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
def register_user(user: UserCreate, db: Session = Depends(get_db)):
|
||||
db_user = db.query(User).filter(User.email == user.email).first()
|
||||
if db_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already registered"
|
||||
)
|
||||
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = User(
|
||||
email=user.email,
|
||||
hashed_password=hashed_password,
|
||||
full_name=user.full_name,
|
||||
phone_number=user.phone_number,
|
||||
whatsapp_number=user.whatsapp_number
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
|
||||
return db_user
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
|
||||
user = db.query(User).filter(User.email == user_credentials.email).first()
|
||||
|
||||
if not user or not verify_password(user_credentials.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
@ -1,76 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from app.models.task import TaskStatus, TaskPriority
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: EmailStr
|
||||
full_name: Optional[str] = None
|
||||
phone_number: Optional[str] = None
|
||||
whatsapp_number: Optional[str] = None
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
|
||||
class UserResponse(UserBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TaskBase(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
priority: TaskPriority = TaskPriority.MEDIUM
|
||||
due_date: Optional[datetime] = None
|
||||
scheduled_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class TaskCreate(TaskBase):
|
||||
pass
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
priority: Optional[TaskPriority] = None
|
||||
status: Optional[TaskStatus] = None
|
||||
due_date: Optional[datetime] = None
|
||||
scheduled_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class TaskResponse(TaskBase):
|
||||
id: int
|
||||
status: TaskStatus
|
||||
ai_suggested_time: Optional[datetime] = None
|
||||
whatsapp_reminder_sent: bool
|
||||
whatsapp_completion_sent: bool
|
||||
owner_id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
timestamp: datetime
|
||||
version: str
|
165
app/api/tasks.py
165
app/api/tasks.py
@ -1,165 +0,0 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.models.task import Task, TaskStatus
|
||||
from app.api.schemas import TaskCreate, TaskUpdate, TaskResponse
|
||||
from app.core.auth import get_current_user
|
||||
from app.services.ai_service import suggest_optimal_time
|
||||
from app.services.whatsapp_service import send_task_reminder
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=TaskResponse)
|
||||
def create_task(
|
||||
task: TaskCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
ai_suggested_time = None
|
||||
if task.due_date and not task.scheduled_at:
|
||||
ai_suggested_time = suggest_optimal_time(task.due_date, task.priority)
|
||||
|
||||
db_task = Task(
|
||||
title=task.title,
|
||||
description=task.description,
|
||||
priority=task.priority,
|
||||
due_date=task.due_date,
|
||||
scheduled_at=task.scheduled_at,
|
||||
ai_suggested_time=ai_suggested_time,
|
||||
owner_id=current_user.id
|
||||
)
|
||||
db.add(db_task)
|
||||
db.commit()
|
||||
db.refresh(db_task)
|
||||
|
||||
return db_task
|
||||
|
||||
|
||||
@router.get("/", response_model=List[TaskResponse])
|
||||
def get_tasks(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: TaskStatus = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
query = db.query(Task).filter(Task.owner_id == current_user.id)
|
||||
if status:
|
||||
query = query.filter(Task.status == status)
|
||||
|
||||
tasks = query.offset(skip).limit(limit).all()
|
||||
return tasks
|
||||
|
||||
|
||||
@router.get("/{task_id}", response_model=TaskResponse)
|
||||
def get_task(
|
||||
task_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.owner_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Task not found"
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
@router.put("/{task_id}", response_model=TaskResponse)
|
||||
def update_task(
|
||||
task_id: int,
|
||||
task_update: TaskUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.owner_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Task not found"
|
||||
)
|
||||
|
||||
update_data = task_update.dict(exclude_unset=True)
|
||||
|
||||
if task_update.status == TaskStatus.COMPLETED and task.status != TaskStatus.COMPLETED:
|
||||
update_data["completed_at"] = datetime.utcnow()
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(task, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
@router.delete("/{task_id}")
|
||||
def delete_task(
|
||||
task_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.owner_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Task not found"
|
||||
)
|
||||
|
||||
db.delete(task)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Task deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/{task_id}/send-reminder")
|
||||
def send_reminder(
|
||||
task_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.owner_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Task not found"
|
||||
)
|
||||
|
||||
if not current_user.whatsapp_number:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="WhatsApp number not configured"
|
||||
)
|
||||
|
||||
success = send_task_reminder(current_user.whatsapp_number, task)
|
||||
if success:
|
||||
task.whatsapp_reminder_sent = True
|
||||
db.commit()
|
||||
return {"message": "Reminder sent successfully"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to send reminder"
|
||||
)
|
@ -1,31 +0,0 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.core.security import verify_token
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
) -> User:
|
||||
payload = verify_token(credentials.credentials)
|
||||
email = payload.get("sub")
|
||||
|
||||
user = db.query(User).filter(User.email == email).first()
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Inactive user"
|
||||
)
|
||||
|
||||
return user
|
@ -1,23 +0,0 @@
|
||||
import os
|
||||
from pydantic import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-here")
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
TWILIO_ACCOUNT_SID: str = os.getenv("TWILIO_ACCOUNT_SID", "")
|
||||
TWILIO_AUTH_TOKEN: str = os.getenv("TWILIO_AUTH_TOKEN", "")
|
||||
TWILIO_WHATSAPP_FROM: str = os.getenv("TWILIO_WHATSAPP_FROM", "")
|
||||
|
||||
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
|
||||
|
||||
PROJECT_NAME: str = "WhatsApp AI Task Scheduling Service"
|
||||
PROJECT_VERSION: str = "1.0.0"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
@ -1,46 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import HTTPException, status
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_token(token: str) -> dict:
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
email: str = payload.get("sub")
|
||||
if email is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return payload
|
||||
except JWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
@ -1,3 +0,0 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
@ -1,22 +0,0 @@
|
||||
from pathlib import Path
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
DB_DIR = Path("/app") / "storage" / "db"
|
||||
DB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False}
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
@ -1,40 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey, Enum
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
|
||||
class TaskStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class TaskPriority(str, enum.Enum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
URGENT = "urgent"
|
||||
|
||||
|
||||
class Task(Base):
|
||||
__tablename__ = "tasks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
status = Column(Enum(TaskStatus), default=TaskStatus.PENDING)
|
||||
priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM)
|
||||
due_date = Column(DateTime(timezone=True), nullable=True)
|
||||
scheduled_at = Column(DateTime(timezone=True), nullable=True)
|
||||
ai_suggested_time = Column(DateTime(timezone=True), nullable=True)
|
||||
whatsapp_reminder_sent = Column(Boolean, default=False)
|
||||
whatsapp_completion_sent = Column(Boolean, default=False)
|
||||
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
owner = relationship("User", back_populates="tasks")
|
@ -1,20 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
full_name = Column(String, nullable=True)
|
||||
phone_number = Column(String, nullable=True)
|
||||
whatsapp_number = Column(String, nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
tasks = relationship("Task", back_populates="owner")
|
@ -1,133 +0,0 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List
|
||||
import openai
|
||||
from app.core.config import settings
|
||||
from app.models.task import TaskPriority
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AIService:
|
||||
def __init__(self):
|
||||
if settings.OPENAI_API_KEY:
|
||||
openai.api_key = settings.OPENAI_API_KEY
|
||||
self.client = openai.OpenAI(api_key=settings.OPENAI_API_KEY)
|
||||
else:
|
||||
self.client = None
|
||||
logger.warning("OpenAI API key not configured. AI functionality disabled.")
|
||||
|
||||
def suggest_optimal_time(self, due_date: datetime, priority: TaskPriority) -> Optional[datetime]:
|
||||
if not self.client:
|
||||
return self._fallback_scheduling(due_date, priority)
|
||||
|
||||
try:
|
||||
current_time = datetime.utcnow()
|
||||
time_until_due = due_date - current_time
|
||||
|
||||
if time_until_due.total_seconds() <= 0:
|
||||
return current_time + timedelta(hours=1)
|
||||
|
||||
prompt = f"""
|
||||
Based on task management best practices, suggest an optimal time to work on a task with the following details:
|
||||
|
||||
Current time: {current_time.strftime('%Y-%m-%d %H:%M:%S')}
|
||||
Due date: {due_date.strftime('%Y-%m-%d %H:%M:%S')}
|
||||
Priority: {priority.value}
|
||||
Time available: {time_until_due.days} days, {time_until_due.seconds // 3600} hours
|
||||
|
||||
Consider:
|
||||
- Task priority (urgent tasks should be scheduled sooner)
|
||||
- Optimal work hours (9 AM - 5 PM on weekdays)
|
||||
- Buffer time before due date
|
||||
- Work-life balance
|
||||
|
||||
Respond with only the suggested datetime in format: YYYY-MM-DD HH:MM:SS
|
||||
"""
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a task scheduling AI assistant."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
max_tokens=50,
|
||||
temperature=0.3
|
||||
)
|
||||
|
||||
suggested_time_str = response.choices[0].message.content.strip()
|
||||
suggested_time = datetime.strptime(suggested_time_str, '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if suggested_time < current_time:
|
||||
suggested_time = current_time + timedelta(hours=1)
|
||||
elif suggested_time > due_date:
|
||||
suggested_time = due_date - timedelta(hours=2)
|
||||
|
||||
return suggested_time
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AI scheduling failed: {str(e)}")
|
||||
return self._fallback_scheduling(due_date, priority)
|
||||
|
||||
def _fallback_scheduling(self, due_date: datetime, priority: TaskPriority) -> datetime:
|
||||
current_time = datetime.utcnow()
|
||||
time_until_due = due_date - current_time
|
||||
|
||||
if priority == TaskPriority.URGENT:
|
||||
return current_time + timedelta(hours=1)
|
||||
elif priority == TaskPriority.HIGH:
|
||||
return current_time + timedelta(hours=min(6, time_until_due.total_seconds() // 3600 // 4))
|
||||
elif priority == TaskPriority.MEDIUM:
|
||||
return current_time + timedelta(hours=min(24, time_until_due.total_seconds() // 3600 // 2))
|
||||
else: # LOW priority
|
||||
return current_time + timedelta(hours=min(48, time_until_due.total_seconds() // 3600 * 0.75))
|
||||
|
||||
def generate_task_insights(self, tasks: List) -> str:
|
||||
if not self.client:
|
||||
return "AI insights not available. Please configure OpenAI API key."
|
||||
|
||||
try:
|
||||
task_summary = []
|
||||
for task in tasks:
|
||||
task_summary.append(f"- {task.title} (Priority: {task.priority.value}, Status: {task.status.value})")
|
||||
|
||||
prompt = f"""
|
||||
Analyze the following task list and provide productivity insights:
|
||||
|
||||
{chr(10).join(task_summary)}
|
||||
|
||||
Provide insights on:
|
||||
1. Task distribution by priority
|
||||
2. Potential bottlenecks
|
||||
3. Productivity recommendations
|
||||
4. Time management suggestions
|
||||
|
||||
Keep the response concise and actionable (max 200 words).
|
||||
"""
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a productivity analysis AI assistant."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
max_tokens=250,
|
||||
temperature=0.7
|
||||
)
|
||||
|
||||
return response.choices[0].message.content.strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AI insights generation failed: {str(e)}")
|
||||
return "Unable to generate insights at this time."
|
||||
|
||||
|
||||
ai_service = AIService()
|
||||
|
||||
|
||||
def suggest_optimal_time(due_date: datetime, priority: TaskPriority) -> Optional[datetime]:
|
||||
return ai_service.suggest_optimal_time(due_date, priority)
|
||||
|
||||
|
||||
def generate_task_insights(tasks: List) -> str:
|
||||
return ai_service.generate_task_insights(tasks)
|
@ -1,129 +0,0 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.task import Task, TaskStatus
|
||||
from app.models.user import User
|
||||
from app.services.whatsapp_service import send_task_reminder, send_task_overdue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SchedulerService:
|
||||
def __init__(self):
|
||||
self.scheduler = BackgroundScheduler()
|
||||
self.scheduler.start()
|
||||
self._setup_jobs()
|
||||
|
||||
def _setup_jobs(self):
|
||||
self.scheduler.add_job(
|
||||
func=self.send_reminders,
|
||||
trigger=CronTrigger(hour=9, minute=0), # Daily at 9 AM
|
||||
id='daily_reminders',
|
||||
name='Send daily task reminders',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
self.scheduler.add_job(
|
||||
func=self.check_overdue_tasks,
|
||||
trigger=CronTrigger(hour=18, minute=0), # Daily at 6 PM
|
||||
id='overdue_check',
|
||||
name='Check for overdue tasks',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
def send_reminders(self):
|
||||
logger.info("Starting daily reminder job")
|
||||
db: Session = SessionLocal()
|
||||
|
||||
try:
|
||||
# Get tasks scheduled for today that haven't been reminded
|
||||
today = datetime.utcnow().date()
|
||||
tomorrow = today + timedelta(days=1)
|
||||
|
||||
tasks = db.query(Task).join(User).filter(
|
||||
Task.status.in_([TaskStatus.PENDING, TaskStatus.IN_PROGRESS]),
|
||||
Task.scheduled_at >= datetime.combine(today, datetime.min.time()),
|
||||
Task.scheduled_at < datetime.combine(tomorrow, datetime.min.time()),
|
||||
Task.whatsapp_reminder_sent == False,
|
||||
User.whatsapp_number.isnot(None)
|
||||
).all()
|
||||
|
||||
sent_count = 0
|
||||
for task in tasks:
|
||||
if send_task_reminder(task.owner.whatsapp_number, task):
|
||||
task.whatsapp_reminder_sent = True
|
||||
sent_count += 1
|
||||
|
||||
db.commit()
|
||||
logger.info(f"Sent {sent_count} reminders")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in reminder job: {str(e)}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def check_overdue_tasks(self):
|
||||
logger.info("Starting overdue task check")
|
||||
db: Session = SessionLocal()
|
||||
|
||||
try:
|
||||
now = datetime.utcnow()
|
||||
overdue_tasks = db.query(Task).join(User).filter(
|
||||
Task.status.in_([TaskStatus.PENDING, TaskStatus.IN_PROGRESS]),
|
||||
Task.due_date < now,
|
||||
User.whatsapp_number.isnot(None)
|
||||
).all()
|
||||
|
||||
sent_count = 0
|
||||
for task in overdue_tasks:
|
||||
if send_task_overdue(task.owner.whatsapp_number, task):
|
||||
sent_count += 1
|
||||
|
||||
logger.info(f"Sent {sent_count} overdue notifications")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in overdue check job: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def schedule_task_reminder(self, task_id: int, remind_at: datetime):
|
||||
job_id = f"task_reminder_{task_id}"
|
||||
|
||||
self.scheduler.add_job(
|
||||
func=self._send_individual_reminder,
|
||||
trigger='date',
|
||||
run_date=remind_at,
|
||||
args=[task_id],
|
||||
id=job_id,
|
||||
name=f'Reminder for task {task_id}',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
def _send_individual_reminder(self, task_id: int):
|
||||
db: Session = SessionLocal()
|
||||
|
||||
try:
|
||||
task = db.query(Task).join(User).filter(
|
||||
Task.id == task_id,
|
||||
User.whatsapp_number.isnot(None)
|
||||
).first()
|
||||
|
||||
if task and task.status in [TaskStatus.PENDING, TaskStatus.IN_PROGRESS]:
|
||||
send_task_reminder(task.owner.whatsapp_number, task)
|
||||
task.whatsapp_reminder_sent = True
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending individual reminder for task {task_id}: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def shutdown(self):
|
||||
self.scheduler.shutdown()
|
||||
|
||||
|
||||
scheduler_service = SchedulerService()
|
@ -1,64 +0,0 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from twilio.rest import Client
|
||||
from twilio.base.exceptions import TwilioException
|
||||
from app.core.config import settings
|
||||
from app.models.task import Task
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WhatsAppService:
|
||||
def __init__(self):
|
||||
if settings.TWILIO_ACCOUNT_SID and settings.TWILIO_AUTH_TOKEN:
|
||||
self.client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
|
||||
else:
|
||||
self.client = None
|
||||
logger.warning("Twilio credentials not configured. WhatsApp functionality disabled.")
|
||||
|
||||
def send_message(self, to_number: str, message: str) -> bool:
|
||||
if not self.client:
|
||||
logger.error("WhatsApp service not configured")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Ensure phone number is in WhatsApp format
|
||||
if not to_number.startswith("whatsapp:"):
|
||||
to_number = f"whatsapp:{to_number}"
|
||||
|
||||
message = self.client.messages.create(
|
||||
body=message,
|
||||
from_=settings.TWILIO_WHATSAPP_FROM,
|
||||
to=to_number
|
||||
)
|
||||
logger.info(f"WhatsApp message sent successfully. SID: {message.sid}")
|
||||
return True
|
||||
except TwilioException as e:
|
||||
logger.error(f"Failed to send WhatsApp message: {str(e)}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error sending WhatsApp message: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
whatsapp_service = WhatsAppService()
|
||||
|
||||
|
||||
def send_task_reminder(phone_number: str, task: Task) -> bool:
|
||||
due_info = f" (Due: {task.due_date.strftime('%Y-%m-%d %H:%M')})" if task.due_date else ""
|
||||
message = f"🔔 Task Reminder: {task.title}\n\n{task.description or 'No description provided'}{due_info}\n\nPriority: {task.priority.value.upper()}"
|
||||
|
||||
return whatsapp_service.send_message(phone_number, message)
|
||||
|
||||
|
||||
def send_task_completion(phone_number: str, task: Task) -> bool:
|
||||
message = f"✅ Task Completed: {task.title}\n\nCongratulations! You've successfully completed this task."
|
||||
|
||||
return whatsapp_service.send_message(phone_number, message)
|
||||
|
||||
|
||||
def send_task_overdue(phone_number: str, task: Task) -> bool:
|
||||
overdue_days = (task.due_date - task.created_at).days if task.due_date else 0
|
||||
message = f"⚠️ Task Overdue: {task.title}\n\nThis task is overdue by {overdue_days} days. Please complete it as soon as possible.\n\nPriority: {task.priority.value.upper()}"
|
||||
|
||||
return whatsapp_service.send_message(phone_number, message)
|
48
package.json
Normal file
48
package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "whatsapp-ai-task-scheduling-service",
|
||||
"version": "1.0.0",
|
||||
"description": "WhatsApp AI Task Scheduling Service built with Node.js, Express, and TypeScript",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"lint:fix": "eslint src/**/*.ts --fix",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:studio": "prisma studio"
|
||||
},
|
||||
"keywords": ["whatsapp", "ai", "task-scheduling", "nodejs", "express", "typescript"],
|
||||
"author": "BackendIM",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"joi": "^17.11.0",
|
||||
"prisma": "^5.7.1",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"twilio": "^4.19.0",
|
||||
"openai": "^4.20.1",
|
||||
"node-cron": "^3.0.3",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"eslint": "^8.55.0",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
59
prisma/schema.prisma
Normal file
59
prisma/schema.prisma
Normal file
@ -0,0 +1,59 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
fullName String? @map("full_name")
|
||||
phoneNumber String? @map("phone_number")
|
||||
whatsappNumber String? @map("whatsapp_number")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
tasks Task[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Task {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String?
|
||||
status TaskStatus @default(PENDING)
|
||||
priority TaskPriority @default(MEDIUM)
|
||||
dueDate DateTime? @map("due_date")
|
||||
scheduledAt DateTime? @map("scheduled_at")
|
||||
aiSuggestedTime DateTime? @map("ai_suggested_time")
|
||||
whatsappReminderSent Boolean @default(false) @map("whatsapp_reminder_sent")
|
||||
whatsappCompletionSent Boolean @default(false) @map("whatsapp_completion_sent")
|
||||
ownerId Int @map("owner_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("tasks")
|
||||
}
|
||||
|
||||
enum TaskStatus {
|
||||
PENDING
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum TaskPriority {
|
||||
LOW
|
||||
MEDIUM
|
||||
HIGH
|
||||
URGENT
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
alembic==1.13.0
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
passlib==1.7.4
|
||||
python-jose==3.3.0
|
||||
bcrypt==4.1.2
|
||||
httpx==0.25.2
|
||||
python-dotenv==1.0.0
|
||||
apscheduler==3.10.4
|
||||
openai==1.3.7
|
||||
twilio==8.10.3
|
||||
ruff==0.1.6
|
100
src/app.ts
Normal file
100
src/app.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { config } from '@/utils/config';
|
||||
import logger from '@/utils/logger';
|
||||
import { errorHandler } from '@/middleware/errorHandler';
|
||||
import routes from '@/routes';
|
||||
import schedulerService from '@/services/schedulerService';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
|
||||
app.use(cors({
|
||||
origin: '*',
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
},
|
||||
});
|
||||
|
||||
app.use(limiter);
|
||||
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'WhatsApp AI Task Scheduling Service',
|
||||
data: {
|
||||
title: 'WhatsApp AI Task Scheduling Service',
|
||||
version: '1.0.0',
|
||||
documentation: '/docs',
|
||||
health: '/health',
|
||||
endpoints: {
|
||||
auth: '/api/auth',
|
||||
tasks: '/api/tasks',
|
||||
health: '/api/health'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api', routes);
|
||||
|
||||
app.get('/docs', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'API Documentation',
|
||||
data: {
|
||||
title: 'WhatsApp AI Task Scheduling Service API',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
auth: {
|
||||
'POST /api/auth/register': 'Register a new user',
|
||||
'POST /api/auth/login': 'Login user'
|
||||
},
|
||||
tasks: {
|
||||
'POST /api/tasks': 'Create a new task',
|
||||
'GET /api/tasks': 'Get all tasks for user',
|
||||
'GET /api/tasks/:id': 'Get specific task',
|
||||
'PUT /api/tasks/:id': 'Update task',
|
||||
'DELETE /api/tasks/:id': 'Delete task',
|
||||
'POST /api/tasks/:id/remind': 'Send WhatsApp reminder'
|
||||
},
|
||||
health: {
|
||||
'GET /api/health': 'Health check endpoint'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: 'Route not found',
|
||||
});
|
||||
});
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
const gracefulShutdown = () => {
|
||||
logger.info('Received shutdown signal, shutting down gracefully...');
|
||||
schedulerService.shutdown();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
|
||||
export default app;
|
118
src/controllers/authController.ts
Normal file
118
src/controllers/authController.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { hashPassword, comparePassword, generateToken } from '@/utils/auth';
|
||||
import { validate, userRegistrationSchema, userLoginSchema } from '@/utils/validation';
|
||||
import prisma from '@/utils/database';
|
||||
import logger from '@/utils/logger';
|
||||
import { CreateUserData, LoginData, ApiResponse } from '@/types';
|
||||
|
||||
export const register = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userData: CreateUserData = validate(userRegistrationSchema, req.body);
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: userData.email }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'User with this email already exists'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(userData.password);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: userData.email,
|
||||
password: hashedPassword,
|
||||
fullName: userData.fullName,
|
||||
phoneNumber: userData.phoneNumber,
|
||||
whatsappNumber: userData.whatsappNumber,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
fullName: true,
|
||||
phoneNumber: true,
|
||||
whatsappNumber: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
const token = generateToken({ id: user.id, email: user.email });
|
||||
|
||||
logger.info(`New user registered: ${user.email}`);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: 'User registered successfully',
|
||||
data: {
|
||||
user,
|
||||
token
|
||||
}
|
||||
} as ApiResponse);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Registration error:', error);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Registration failed'
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
||||
|
||||
export const login = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const loginData: LoginData = validate(userLoginSchema, req.body);
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: loginData.email }
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
const isPasswordValid = await comparePassword(loginData.password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
const token = generateToken({ id: user.id, email: user.email });
|
||||
|
||||
logger.info(`User logged in: ${user.email}`);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
fullName: user.fullName,
|
||||
phoneNumber: user.phoneNumber,
|
||||
whatsappNumber: user.whatsappNumber,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
},
|
||||
token
|
||||
}
|
||||
} as ApiResponse);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Login error:', error);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Login failed'
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
42
src/controllers/healthController.ts
Normal file
42
src/controllers/healthController.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Request, Response } from 'express';
|
||||
import prisma from '@/utils/database';
|
||||
import { ApiResponse } from '@/types';
|
||||
|
||||
export const healthCheck = async (req: Request, res: Response) => {
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
|
||||
const healthData = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
services: {
|
||||
database: 'connected',
|
||||
api: 'running'
|
||||
}
|
||||
};
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Service is healthy',
|
||||
data: healthData
|
||||
} as ApiResponse);
|
||||
|
||||
} catch (error) {
|
||||
const healthData = {
|
||||
status: 'unhealthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
services: {
|
||||
database: 'disconnected',
|
||||
api: 'running'
|
||||
}
|
||||
};
|
||||
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: 'Service is unhealthy',
|
||||
data: healthData
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
306
src/controllers/taskController.ts
Normal file
306
src/controllers/taskController.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import { Response } from 'express';
|
||||
import { validate, taskCreationSchema, taskUpdateSchema } from '@/utils/validation';
|
||||
import prisma from '@/utils/database';
|
||||
import logger from '@/utils/logger';
|
||||
import { AuthenticatedRequest, CreateTaskData, UpdateTaskData, ApiResponse } from '@/types';
|
||||
import { suggestOptimalTime } from '@/services/aiService';
|
||||
import { sendTaskReminder } from '@/services/whatsappService';
|
||||
|
||||
export const createTask = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const taskData: CreateTaskData = validate(taskCreationSchema, req.body);
|
||||
const userId = req.user!.id;
|
||||
|
||||
let aiSuggestedTime = null;
|
||||
if (taskData.dueDate && !taskData.scheduledAt) {
|
||||
aiSuggestedTime = await suggestOptimalTime(taskData.dueDate, taskData.priority || 'MEDIUM');
|
||||
}
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
title: taskData.title,
|
||||
description: taskData.description,
|
||||
priority: taskData.priority || 'MEDIUM',
|
||||
dueDate: taskData.dueDate,
|
||||
scheduledAt: taskData.scheduledAt,
|
||||
aiSuggestedTime,
|
||||
ownerId: userId,
|
||||
},
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
fullName: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Task created: ${task.title} by user ${userId}`);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: 'Task created successfully',
|
||||
data: task
|
||||
} as ApiResponse);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Task creation error:', error);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Task creation failed'
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTasks = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
const { status, priority, page = '1', limit = '20' } = req.query;
|
||||
|
||||
const pageNum = parseInt(page as string);
|
||||
const limitNum = parseInt(limit as string);
|
||||
const skip = (pageNum - 1) * limitNum;
|
||||
|
||||
const where: any = { ownerId: userId };
|
||||
if (status) where.status = status;
|
||||
if (priority) where.priority = priority;
|
||||
|
||||
const [tasks, total] = await Promise.all([
|
||||
prisma.task.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: limitNum,
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
fullName: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.task.count({ where })
|
||||
]);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Tasks retrieved successfully',
|
||||
data: {
|
||||
tasks,
|
||||
pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
pages: Math.ceil(total / limitNum)
|
||||
}
|
||||
}
|
||||
} as ApiResponse);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Get tasks error:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to retrieve tasks'
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTask = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const taskId = parseInt(req.params.id);
|
||||
const userId = req.user!.id;
|
||||
|
||||
const task = await prisma.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
ownerId: userId
|
||||
},
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
fullName: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Task not found'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Task retrieved successfully',
|
||||
data: task
|
||||
} as ApiResponse);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Get task error:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to retrieve task'
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTask = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const taskId = parseInt(req.params.id);
|
||||
const userId = req.user!.id;
|
||||
const updateData: UpdateTaskData = validate(taskUpdateSchema, req.body);
|
||||
|
||||
const existingTask = await prisma.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
ownerId: userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingTask) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Task not found'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
const updatePayload: any = { ...updateData };
|
||||
|
||||
if (updateData.status === 'COMPLETED' && existingTask.status !== 'COMPLETED') {
|
||||
updatePayload.completedAt = new Date();
|
||||
}
|
||||
|
||||
const task = await prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: updatePayload,
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
fullName: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Task updated: ${task.title} by user ${userId}`);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Task updated successfully',
|
||||
data: task
|
||||
} as ApiResponse);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Task update error:', error);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Task update failed'
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTask = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const taskId = parseInt(req.params.id);
|
||||
const userId = req.user!.id;
|
||||
|
||||
const task = await prisma.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
ownerId: userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Task not found'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
await prisma.task.delete({
|
||||
where: { id: taskId }
|
||||
});
|
||||
|
||||
logger.info(`Task deleted: ${task.title} by user ${userId}`);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Task deleted successfully'
|
||||
} as ApiResponse);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Task deletion error:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to delete task'
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendReminder = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const taskId = parseInt(req.params.id);
|
||||
const userId = req.user!.id;
|
||||
|
||||
const task = await prisma.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
ownerId: userId
|
||||
},
|
||||
include: {
|
||||
owner: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Task not found'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
if (!task.owner.whatsappNumber) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'WhatsApp number not configured'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
const success = await sendTaskReminder(task.owner.whatsappNumber, task);
|
||||
|
||||
if (success) {
|
||||
await prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: { whatsappReminderSent: true }
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Reminder sent successfully'
|
||||
} as ApiResponse);
|
||||
} else {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to send reminder'
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Send reminder error:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to send reminder'
|
||||
} as ApiResponse);
|
||||
}
|
||||
};
|
43
src/index.ts
Normal file
43
src/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import app from './app';
|
||||
import { config } from '@/utils/config';
|
||||
import logger from '@/utils/logger';
|
||||
import prisma from '@/utils/database';
|
||||
|
||||
async function startServer() {
|
||||
try {
|
||||
await prisma.$connect();
|
||||
logger.info('Database connected successfully');
|
||||
|
||||
const server = app.listen(config.port, () => {
|
||||
logger.info(`Server is running on port ${config.port}`);
|
||||
logger.info(`Environment: ${config.nodeEnv}`);
|
||||
logger.info(`Health check: http://localhost:${config.port}/api/health`);
|
||||
logger.info(`Documentation: http://localhost:${config.port}/docs`);
|
||||
});
|
||||
|
||||
const gracefulShutdown = () => {
|
||||
logger.info('Received shutdown signal, shutting down gracefully...');
|
||||
server.close(() => {
|
||||
logger.info('HTTP server closed');
|
||||
prisma.$disconnect()
|
||||
.then(() => {
|
||||
logger.info('Database connection closed');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error closing database connection:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
50
src/middleware/auth.ts
Normal file
50
src/middleware/auth.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { verifyToken } from '@/utils/auth';
|
||||
import prisma from '@/utils/database';
|
||||
import logger from '@/utils/logger';
|
||||
import { AuthenticatedRequest } from '@/types';
|
||||
|
||||
export const authenticate = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Access token is required'
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.id },
|
||||
select: { id: true, email: true, isActive: true }
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid or inactive user'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = {
|
||||
id: user.id,
|
||||
email: user.email
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('Authentication error:', error);
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid or expired token'
|
||||
});
|
||||
}
|
||||
};
|
26
src/middleware/errorHandler.ts
Normal file
26
src/middleware/errorHandler.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export const errorHandler = (
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
logger.error('Unhandled error:', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
});
|
||||
|
||||
if (res.headersSent) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error',
|
||||
...(process.env.NODE_ENV === 'development' && { error: error.message }),
|
||||
});
|
||||
};
|
9
src/routes/auth.ts
Normal file
9
src/routes/auth.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Router } from 'express';
|
||||
import { register, login } from '@/controllers/authController';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/register', register);
|
||||
router.post('/login', login);
|
||||
|
||||
export default router;
|
8
src/routes/health.ts
Normal file
8
src/routes/health.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Router } from 'express';
|
||||
import { healthCheck } from '@/controllers/healthController';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', healthCheck);
|
||||
|
||||
export default router;
|
12
src/routes/index.ts
Normal file
12
src/routes/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import authRoutes from './auth';
|
||||
import taskRoutes from './tasks';
|
||||
import healthRoutes from './health';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/auth', authRoutes);
|
||||
router.use('/tasks', taskRoutes);
|
||||
router.use('/health', healthRoutes);
|
||||
|
||||
export default router;
|
23
src/routes/tasks.ts
Normal file
23
src/routes/tasks.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '@/middleware/auth';
|
||||
import {
|
||||
createTask,
|
||||
getTasks,
|
||||
getTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
sendReminder
|
||||
} from '@/controllers/taskController';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.post('/', createTask);
|
||||
router.get('/', getTasks);
|
||||
router.get('/:id', getTask);
|
||||
router.put('/:id', updateTask);
|
||||
router.delete('/:id', deleteTask);
|
||||
router.post('/:id/remind', sendReminder);
|
||||
|
||||
export default router;
|
162
src/services/aiService.ts
Normal file
162
src/services/aiService.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import OpenAI from 'openai';
|
||||
import { config } from '@/utils/config';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
class AIService {
|
||||
private client: OpenAI | null = null;
|
||||
|
||||
constructor() {
|
||||
if (config.openai.apiKey) {
|
||||
this.client = new OpenAI({
|
||||
apiKey: config.openai.apiKey,
|
||||
});
|
||||
} else {
|
||||
logger.warn('OpenAI API key not configured. AI functionality disabled.');
|
||||
}
|
||||
}
|
||||
|
||||
async suggestOptimalTime(dueDate: Date, priority: string): Promise<Date | null> {
|
||||
if (!this.client) {
|
||||
return this.fallbackScheduling(dueDate, priority);
|
||||
}
|
||||
|
||||
try {
|
||||
const currentTime = new Date();
|
||||
const timeUntilDue = dueDate.getTime() - currentTime.getTime();
|
||||
|
||||
if (timeUntilDue <= 0) {
|
||||
return new Date(currentTime.getTime() + 60 * 60 * 1000); // 1 hour from now
|
||||
}
|
||||
|
||||
const daysDiff = Math.floor(timeUntilDue / (1000 * 60 * 60 * 24));
|
||||
const hoursDiff = Math.floor(timeUntilDue / (1000 * 60 * 60));
|
||||
|
||||
const prompt = `
|
||||
Based on task management best practices, suggest an optimal time to work on a task with the following details:
|
||||
|
||||
Current time: ${currentTime.toISOString()}
|
||||
Due date: ${dueDate.toISOString()}
|
||||
Priority: ${priority}
|
||||
Time available: ${daysDiff} days, ${hoursDiff % 24} hours
|
||||
|
||||
Consider:
|
||||
- Task priority (urgent tasks should be scheduled sooner)
|
||||
- Optimal work hours (9 AM - 5 PM on weekdays)
|
||||
- Buffer time before due date
|
||||
- Work-life balance
|
||||
|
||||
Respond with only the suggested datetime in ISO format: YYYY-MM-DDTHH:MM:SS.000Z
|
||||
`;
|
||||
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a task scheduling AI assistant.' },
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
max_tokens: 50,
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const suggestedTimeStr = response.choices[0]?.message?.content?.trim();
|
||||
if (!suggestedTimeStr) {
|
||||
return this.fallbackScheduling(dueDate, priority);
|
||||
}
|
||||
|
||||
const suggestedTime = new Date(suggestedTimeStr);
|
||||
|
||||
if (isNaN(suggestedTime.getTime()) || suggestedTime < currentTime) {
|
||||
return this.fallbackScheduling(dueDate, priority);
|
||||
}
|
||||
|
||||
if (suggestedTime > dueDate) {
|
||||
return new Date(dueDate.getTime() - 2 * 60 * 60 * 1000); // 2 hours before due
|
||||
}
|
||||
|
||||
return suggestedTime;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('AI scheduling failed:', error);
|
||||
return this.fallbackScheduling(dueDate, priority);
|
||||
}
|
||||
}
|
||||
|
||||
private fallbackScheduling(dueDate: Date, priority: string): Date {
|
||||
const currentTime = new Date();
|
||||
const timeUntilDue = dueDate.getTime() - currentTime.getTime();
|
||||
|
||||
let hoursToAdd: number;
|
||||
|
||||
switch (priority) {
|
||||
case 'URGENT':
|
||||
hoursToAdd = 1;
|
||||
break;
|
||||
case 'HIGH':
|
||||
hoursToAdd = Math.min(6, timeUntilDue / (1000 * 60 * 60) / 4);
|
||||
break;
|
||||
case 'MEDIUM':
|
||||
hoursToAdd = Math.min(24, timeUntilDue / (1000 * 60 * 60) / 2);
|
||||
break;
|
||||
case 'LOW':
|
||||
default:
|
||||
hoursToAdd = Math.min(48, (timeUntilDue / (1000 * 60 * 60)) * 0.75);
|
||||
break;
|
||||
}
|
||||
|
||||
return new Date(currentTime.getTime() + hoursToAdd * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async generateTaskInsights(tasks: any[]): Promise<string> {
|
||||
if (!this.client) {
|
||||
return 'AI insights not available. Please configure OpenAI API key.';
|
||||
}
|
||||
|
||||
try {
|
||||
const taskSummary = tasks.map(task =>
|
||||
`- ${task.title} (Priority: ${task.priority}, Status: ${task.status})`
|
||||
).join('\n');
|
||||
|
||||
const prompt = `
|
||||
Analyze the following task list and provide productivity insights:
|
||||
|
||||
${taskSummary}
|
||||
|
||||
Provide insights on:
|
||||
1. Task distribution by priority
|
||||
2. Potential bottlenecks
|
||||
3. Productivity recommendations
|
||||
4. Time management suggestions
|
||||
|
||||
Keep the response concise and actionable (max 200 words).
|
||||
`;
|
||||
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a productivity analysis AI assistant.' },
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
max_tokens: 250,
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content?.trim() || 'Unable to generate insights at this time.';
|
||||
|
||||
} catch (error) {
|
||||
logger.error('AI insights generation failed:', error);
|
||||
return 'Unable to generate insights at this time.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const aiService = new AIService();
|
||||
|
||||
export const suggestOptimalTime = async (dueDate: Date, priority: string): Promise<Date | null> => {
|
||||
return aiService.suggestOptimalTime(dueDate, priority);
|
||||
};
|
||||
|
||||
export const generateTaskInsights = async (tasks: any[]): Promise<string> => {
|
||||
return aiService.generateTaskInsights(tasks);
|
||||
};
|
||||
|
||||
export default aiService;
|
190
src/services/schedulerService.ts
Normal file
190
src/services/schedulerService.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import cron from 'node-cron';
|
||||
import prisma from '@/utils/database';
|
||||
import logger from '@/utils/logger';
|
||||
import { sendTaskReminder, sendTaskOverdue } from './whatsappService';
|
||||
|
||||
class SchedulerService {
|
||||
private jobs: Map<string, cron.ScheduledTask> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.setupRecurringJobs();
|
||||
}
|
||||
|
||||
private setupRecurringJobs() {
|
||||
const dailyReminderJob = cron.schedule('0 9 * * *', async () => {
|
||||
await this.sendDailyReminders();
|
||||
}, {
|
||||
scheduled: false,
|
||||
timezone: 'UTC'
|
||||
});
|
||||
|
||||
const overdueCheckJob = cron.schedule('0 18 * * *', async () => {
|
||||
await this.checkOverdueTasks();
|
||||
}, {
|
||||
scheduled: false,
|
||||
timezone: 'UTC'
|
||||
});
|
||||
|
||||
this.jobs.set('daily_reminders', dailyReminderJob);
|
||||
this.jobs.set('overdue_check', overdueCheckJob);
|
||||
|
||||
dailyReminderJob.start();
|
||||
overdueCheckJob.start();
|
||||
|
||||
logger.info('Scheduler service initialized with recurring jobs');
|
||||
}
|
||||
|
||||
private async sendDailyReminders() {
|
||||
logger.info('Starting daily reminder job');
|
||||
|
||||
try {
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
|
||||
today.setHours(0, 0, 0, 0);
|
||||
tomorrow.setHours(0, 0, 0, 0);
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
where: {
|
||||
status: {
|
||||
in: ['PENDING', 'IN_PROGRESS']
|
||||
},
|
||||
scheduledAt: {
|
||||
gte: today,
|
||||
lt: tomorrow
|
||||
},
|
||||
whatsappReminderSent: false,
|
||||
owner: {
|
||||
whatsappNumber: {
|
||||
not: null
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
owner: true
|
||||
}
|
||||
});
|
||||
|
||||
let sentCount = 0;
|
||||
for (const task of tasks) {
|
||||
if (task.owner.whatsappNumber && await sendTaskReminder(task.owner.whatsappNumber, task)) {
|
||||
await prisma.task.update({
|
||||
where: { id: task.id },
|
||||
data: { whatsappReminderSent: true }
|
||||
});
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Sent ${sentCount} daily reminders`);
|
||||
} catch (error) {
|
||||
logger.error('Error in daily reminder job:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkOverdueTasks() {
|
||||
logger.info('Starting overdue task check');
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
const overdueTasks = await prisma.task.findMany({
|
||||
where: {
|
||||
status: {
|
||||
in: ['PENDING', 'IN_PROGRESS']
|
||||
},
|
||||
dueDate: {
|
||||
lt: now
|
||||
},
|
||||
owner: {
|
||||
whatsappNumber: {
|
||||
not: null
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
owner: true
|
||||
}
|
||||
});
|
||||
|
||||
let sentCount = 0;
|
||||
for (const task of overdueTasks) {
|
||||
if (task.owner.whatsappNumber && await sendTaskOverdue(task.owner.whatsappNumber, task)) {
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Sent ${sentCount} overdue notifications`);
|
||||
} catch (error) {
|
||||
logger.error('Error in overdue check job:', error);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleTaskReminder(taskId: number, remindAt: Date) {
|
||||
const jobId = `task_reminder_${taskId}`;
|
||||
|
||||
if (this.jobs.has(jobId)) {
|
||||
this.jobs.get(jobId)?.stop();
|
||||
this.jobs.delete(jobId);
|
||||
}
|
||||
|
||||
const job = cron.schedule(this.dateToCron(remindAt), async () => {
|
||||
await this.sendIndividualReminder(taskId);
|
||||
this.jobs.delete(jobId);
|
||||
}, {
|
||||
scheduled: false,
|
||||
timezone: 'UTC'
|
||||
});
|
||||
|
||||
this.jobs.set(jobId, job);
|
||||
job.start();
|
||||
|
||||
logger.info(`Scheduled reminder for task ${taskId} at ${remindAt.toISOString()}`);
|
||||
}
|
||||
|
||||
private async sendIndividualReminder(taskId: number) {
|
||||
try {
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: taskId },
|
||||
include: { owner: true }
|
||||
});
|
||||
|
||||
if (task &&
|
||||
task.status !== 'COMPLETED' &&
|
||||
task.status !== 'CANCELLED' &&
|
||||
task.owner.whatsappNumber) {
|
||||
|
||||
const success = await sendTaskReminder(task.owner.whatsappNumber, task);
|
||||
if (success) {
|
||||
await prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: { whatsappReminderSent: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error sending individual reminder for task ${taskId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private dateToCron(date: Date): string {
|
||||
const minute = date.getUTCMinutes();
|
||||
const hour = date.getUTCHours();
|
||||
const day = date.getUTCDate();
|
||||
const month = date.getUTCMonth() + 1;
|
||||
|
||||
return `${minute} ${hour} ${day} ${month} *`;
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
for (const [jobId, job] of this.jobs) {
|
||||
job.stop();
|
||||
logger.info(`Stopped job: ${jobId}`);
|
||||
}
|
||||
this.jobs.clear();
|
||||
logger.info('Scheduler service shut down');
|
||||
}
|
||||
}
|
||||
|
||||
export default new SchedulerService();
|
61
src/services/whatsappService.ts
Normal file
61
src/services/whatsappService.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import twilio from 'twilio';
|
||||
import { config } from '@/utils/config';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
class WhatsAppService {
|
||||
private client: twilio.Twilio | null = null;
|
||||
|
||||
constructor() {
|
||||
if (config.twilio.accountSid && config.twilio.authToken) {
|
||||
this.client = twilio(config.twilio.accountSid, config.twilio.authToken);
|
||||
} else {
|
||||
logger.warn('Twilio credentials not configured. WhatsApp functionality disabled.');
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(toNumber: string, message: string): Promise<boolean> {
|
||||
if (!this.client) {
|
||||
logger.error('WhatsApp service not configured');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const formattedNumber = toNumber.startsWith('whatsapp:') ? toNumber : `whatsapp:${toNumber}`;
|
||||
|
||||
const messageResponse = await this.client.messages.create({
|
||||
body: message,
|
||||
from: config.twilio.whatsappFrom,
|
||||
to: formattedNumber,
|
||||
});
|
||||
|
||||
logger.info(`WhatsApp message sent successfully. SID: ${messageResponse.sid}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Failed to send WhatsApp message:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const whatsappService = new WhatsAppService();
|
||||
|
||||
export const sendTaskReminder = async (phoneNumber: string, task: any): Promise<boolean> => {
|
||||
const dueInfo = task.dueDate ? ` (Due: ${new Date(task.dueDate).toLocaleDateString()})` : '';
|
||||
const message = `🔔 Task Reminder: ${task.title}\n\n${task.description || 'No description provided'}${dueInfo}\n\nPriority: ${task.priority}`;
|
||||
|
||||
return whatsappService.sendMessage(phoneNumber, message);
|
||||
};
|
||||
|
||||
export const sendTaskCompletion = async (phoneNumber: string, task: any): Promise<boolean> => {
|
||||
const message = `✅ Task Completed: ${task.title}\n\nCongratulations! You've successfully completed this task.`;
|
||||
|
||||
return whatsappService.sendMessage(phoneNumber, message);
|
||||
};
|
||||
|
||||
export const sendTaskOverdue = async (phoneNumber: string, task: any): Promise<boolean> => {
|
||||
const message = `⚠️ Task Overdue: ${task.title}\n\nThis task is overdue. Please complete it as soon as possible.\n\nPriority: ${task.priority}`;
|
||||
|
||||
return whatsappService.sendMessage(phoneNumber, message);
|
||||
};
|
||||
|
||||
export default whatsappService;
|
42
src/types/index.ts
Normal file
42
src/types/index.ts
Normal file
@ -0,0 +1,42 @@
|
||||
export interface CreateUserData {
|
||||
email: string;
|
||||
password: string;
|
||||
fullName?: string;
|
||||
phoneNumber?: string;
|
||||
whatsappNumber?: string;
|
||||
}
|
||||
|
||||
export interface LoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface CreateTaskData {
|
||||
title: string;
|
||||
description?: string;
|
||||
priority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
||||
dueDate?: Date;
|
||||
scheduledAt?: Date;
|
||||
}
|
||||
|
||||
export interface UpdateTaskData {
|
||||
title?: string;
|
||||
description?: string;
|
||||
priority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
||||
status?: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED';
|
||||
dueDate?: Date;
|
||||
scheduledAt?: Date;
|
||||
}
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
id: number;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: T;
|
||||
}
|
25
src/utils/auth.ts
Normal file
25
src/utils/auth.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { config } from './config';
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
const salt = await bcrypt.genSalt(12);
|
||||
return bcrypt.hash(password, salt);
|
||||
};
|
||||
|
||||
export const comparePassword = async (
|
||||
password: string,
|
||||
hashedPassword: string
|
||||
): Promise<boolean> => {
|
||||
return bcrypt.compare(password, hashedPassword);
|
||||
};
|
||||
|
||||
export const generateToken = (payload: { id: number; email: string }): string => {
|
||||
return jwt.sign(payload, config.jwt.secret, {
|
||||
expiresIn: config.jwt.expiresIn,
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyToken = (token: string): { id: number; email: string } => {
|
||||
return jwt.verify(token, config.jwt.secret) as { id: number; email: string };
|
||||
};
|
27
src/utils/config.ts
Normal file
27
src/utils/config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const config = {
|
||||
port: parseInt(process.env.PORT || '3000'),
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key-here',
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
|
||||
},
|
||||
|
||||
twilio: {
|
||||
accountSid: process.env.TWILIO_ACCOUNT_SID || '',
|
||||
authToken: process.env.TWILIO_AUTH_TOKEN || '',
|
||||
whatsappFrom: process.env.TWILIO_WHATSAPP_FROM || '',
|
||||
},
|
||||
|
||||
openai: {
|
||||
apiKey: process.env.OPENAI_API_KEY || '',
|
||||
},
|
||||
|
||||
database: {
|
||||
url: process.env.DATABASE_URL || 'file:./storage/db/database.db',
|
||||
},
|
||||
};
|
45
src/utils/database.ts
Normal file
45
src/utils/database.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import logger from './logger';
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
log: [
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'query',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'error',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'info',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
prisma.$on('error', (e) => {
|
||||
logger.error('Database error:', e);
|
||||
});
|
||||
|
||||
prisma.$on('warn', (e) => {
|
||||
logger.warn('Database warning:', e);
|
||||
});
|
||||
|
||||
prisma.$on('info', (e) => {
|
||||
logger.info('Database info:', e);
|
||||
});
|
||||
|
||||
prisma.$on('query', (e) => {
|
||||
logger.debug('Database query:', {
|
||||
query: e.query,
|
||||
params: e.params,
|
||||
duration: e.duration,
|
||||
});
|
||||
});
|
||||
|
||||
export default prisma;
|
27
src/utils/logger.ts
Normal file
27
src/utils/logger.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import winston from 'winston';
|
||||
import { config } from './config';
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: config.nodeEnv === 'production' ? 'info' : 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: { service: 'whatsapp-ai-task-scheduler' },
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'logs/combined.log' }),
|
||||
],
|
||||
});
|
||||
|
||||
if (config.nodeEnv !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
export default logger;
|
39
src/utils/validation.ts
Normal file
39
src/utils/validation.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
export const userRegistrationSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).required(),
|
||||
fullName: Joi.string().optional(),
|
||||
phoneNumber: Joi.string().optional(),
|
||||
whatsappNumber: Joi.string().optional(),
|
||||
});
|
||||
|
||||
export const userLoginSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().required(),
|
||||
});
|
||||
|
||||
export const taskCreationSchema = Joi.object({
|
||||
title: Joi.string().required(),
|
||||
description: Joi.string().optional(),
|
||||
priority: Joi.string().valid('LOW', 'MEDIUM', 'HIGH', 'URGENT').optional(),
|
||||
dueDate: Joi.date().optional(),
|
||||
scheduledAt: Joi.date().optional(),
|
||||
});
|
||||
|
||||
export const taskUpdateSchema = Joi.object({
|
||||
title: Joi.string().optional(),
|
||||
description: Joi.string().optional(),
|
||||
priority: Joi.string().valid('LOW', 'MEDIUM', 'HIGH', 'URGENT').optional(),
|
||||
status: Joi.string().valid('PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED').optional(),
|
||||
dueDate: Joi.date().optional(),
|
||||
scheduledAt: Joi.date().optional(),
|
||||
});
|
||||
|
||||
export const validate = (schema: Joi.Schema, data: any) => {
|
||||
const { error, value } = schema.validate(data);
|
||||
if (error) {
|
||||
throw new Error(error.details[0].message);
|
||||
}
|
||||
return value;
|
||||
};
|
39
tsconfig.json
Normal file
39
tsconfig.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["es2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user