diff --git a/README.md b/README.md index 35a6277..f230289 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,7 @@ -# Build with Me + GitHub Copilot πŸš€ - -You can build along with me in this [Youtube video]() or read this [blog post](). +# Planventure API 🚁 -![Build with Me + GitHub Copilot on Youtube](link-to-image) -![Build with Me + GitHub Copilot on the Blog](link-to-image) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/github-samples/planventure) -# Planventure API 🚁 A Flask-based REST API backend for the Planventure application. ## Prerequisites @@ -19,10 +15,44 @@ Before you begin, ensure you have the following: ## πŸš€ Getting Started -1. Fork this repository to your GitHub account. -2. Switch to the `api-start` branch. -3. Clone the repository to your local machine. +## Build along in a Codespace + +1. Click the "Open in GitHub Codespaces" button above to start developing in a GitHub Codespace. + +### Local Development Setup + +If you prefer to develop locally, follow the steps below: + +1.Fork and clone the repository and navigate to the [planventue-api](/planventure-api/) directory: +```sh +cd planventure-api +``` + +2. Create a virtual environment and activate it: +```sh +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +3. Install the required dependencies: +```sh +pip install -r requirements.txt +``` + +4. Create an `.env` file based on [.sample.env](/planventure-api/.sample.env): +```sh +cp .sample.env .env +``` + +5. Start the Flask development server: +```sh +flask run +``` + +## πŸ“š API Endpoints +- GET / - Welcome message +- GET /health - Health check endpoint -You can find next steps in the README on the `api-start` branch. +## πŸ“ License -Happy Coding! πŸŽ‰ \ No newline at end of file +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/index.html b/index.html deleted file mode 100644 index e69de29..0000000 diff --git a/planventure-api/.env.example b/planventure-api/.env.example new file mode 100644 index 0000000..38ba6c2 --- /dev/null +++ b/planventure-api/.env.example @@ -0,0 +1,8 @@ +# CORS Settings +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 + +# JWT Settings +JWT_SECRET_KEY=your-secret-key-here + +# Database Settings +DATABASE_URL=sqlite:///planventure.db diff --git a/planventure-api/.sample.env b/planventure-api/.sample.env new file mode 100644 index 0000000..53df029 --- /dev/null +++ b/planventure-api/.sample.env @@ -0,0 +1,4 @@ +SECRET_KEY=your-secret-key-here +JWT_SECRET_KEY=your-jwt-secret-key-here +DATABASE_URL=your-sqldatabase-url-here +CORS_ORIGINS=your-cors-origins-here-host-hopefully-localhost:3000 \ No newline at end of file diff --git a/planventure-api/PROMPTS.md b/planventure-api/PROMPTS.md new file mode 100644 index 0000000..60b4ee9 --- /dev/null +++ b/planventure-api/PROMPTS.md @@ -0,0 +1,252 @@ +# Building the Planventure API with GitHub Copilot + +This guide will walk you through creating a Flask-based REST API with SQLAlchemy and JWT authentication using GitHub Copilot to accelerate development. + +## Prerequisites + +- Python 3.8 or higher +- VS Code with GitHub Copilot extension +- Bruno API Client (for testing API endpoints) +- Git installed + +## Project Structure + +We'll be working in the `api-start` branch and creating a structured API with: +- Authentication system +- Database models +- CRUD operations for trips +- JWT token protection + +## Step 1: Project Setup +### Prompts to Configure Flask with SQLAlchemy + +Open Copilot Chat and type: +``` +@workspace Update the Flask app with SQLAlchemy and basic configurations +``` + +When the code is generated, click "Apply in editor" to update your `app.py` file. + +### Update Dependencies + +In Copilot Chat, type: +``` +update requirements.txt with necessary packages for Flask API with SQLAlchemy and JWT +``` + +Install the updated dependencies: +```bash +pip install -r requirements.txt +``` + +### Create .env File + +Create a `.env` file for environment variables and add it to `.gitignore`. + +## Step 2: Database Models + +### User Model + +In Copilot Edits, type: +``` +Create SQLAlchemy User model with email, password_hash, and timestamps. add code in new files +``` + +Review and accept the generated code. + +### Initialize Database Tables + +Ask Copilot to create a database initialization script: +``` +update code to be able to create the db tables with a python shell script +``` + +Run the initialization script: +```bash +python init_db.py +``` + +### Install SQLite Viewer Extension + +1. Go to VS Code extensions +2. Search for "SQLite viewer" +3. Install the extension +4. Click on `init_db.py` to view the created tables + +### Trip Model + +In Copilot Edits, type: +``` +Create SQLAlchemy Trip model with user relationship, destination, start date, end date, coordinates and itinerary +``` + +Accept changes and run the initialization script again: +```bash +python3 init_db.py +``` + +### Commit Your Changes + +Use Source Control in VS Code: +1. Stage all changes +2. Click the sparkle icon to generate a commit message with Copilot +3. Click commit + +## Step 3: Authentication System + +### Password Hashing Utilities + +In Copilot Edits, type: +``` +Create password hashing and salt utility functions for the User model +``` + +Review, accept changes, and install required packages: +```bash +pip install bcrypt +``` + +### JWT Token Functions + +In Copilot Edits, type: +``` +Setup JWT token generation and validation functions +``` + +Review, accept changes, and install the JWT package: +```bash +pip install flask-jwt-extended +``` + +### Registration Route + +In Copilot Edits, type: +``` +Create auth routes for user registration with email validation +``` + +Review and accept the changes. + +### Test Registration Route + +Use Bruno API Client: +1. Create a new POST request +2. Set URL to `http://localhost:5000/auth/register` +3. Add header: `Content-Type: application/json` +4. Add JSON body: +```json +{ + "email": "user@example.com", + "password": "test1234" +} +``` +5. Send the request and verify the response + +### Login Route + +In Copilot Edits, type: +``` +Create login route with JWT token generation +``` + +Review, accept changes, and restart the Flask server. + +### Enable Development Mode + +To have Flask automatically reload on code changes: + +```bash +export FLASK_DEBUG=1 +flask run +``` + +### Authentication Middleware + +In Copilot Edits, type: +``` +Create auth middleware to protect routes +``` + +Review and accept the changes. + +### Commit Your Changes + +Use Source Control and Copilot to create a commit message. + +## Step 4: Trip Routes + +### Create Trip Routes Blueprint + +In Copilot Edits, type: +``` +Create Trip routes blueprint with CRUD operations +``` + +Review and accept the changes. + +> **Note**: Ensure that `verify_jwt_in_request` is set to `verify_jwt_in_request(optional=True)` if needed + +### Test Trip Routes + +Use Bruno API Client to test: +1. CREATE a new trip +2. GET a trip by ID + +### Add Itinerary Template Generator + +In Copilot Edits, type: +``` +Create function to generate default itinerary template +``` + +Review, accept changes, and test the updated route. + +## Step 5: Finalize API + +### Configure CORS for Frontend Access + +In Copilot Edits, type: +``` +Setup CORS configuration for React frontend +``` + +Review and accept the changes. + +### Add Health Check Endpoint + +In Copilot Edits, type: +``` +Create basic health check endpoint +``` + +Review and accept the changes. + +### Commit Final Changes + +Use Source Control with Copilot to create your final commit. + +### Create README + +Ask Copilot to write a comprehensive README for your API project. + +## Common Issues and Solutions + +### GOTCHAS: + +- Ensure there are no trailing slashes in any of the routes - especially the base `/trip` route +- Make sure all required packages are installed +- Check that JWT token validation is configured correctly +- Verify database tables are created properly using the SQLite viewer + +## Next Steps + +Consider these enhancements for your API: +- Add more comprehensive input validation +- Create custom error handlers for HTTP exceptions +- Setup logging configuration +- Add validation error handlers for form data +- Configure database migrations + +## Conclusion + +You now have a fully functional API with authentication, database models, and protected routes. This can serve as the backend for your Planventure application! \ No newline at end of file diff --git a/planventure-api/README.md b/planventure-api/README.md new file mode 100644 index 0000000..a9abbf4 --- /dev/null +++ b/planventure-api/README.md @@ -0,0 +1,62 @@ +# Planventure API 🌍✈️ + +A Flask-based REST API for managing travel itineraries and trip planning. + +## Features + +- πŸ” User Authentication (JWT-based) +- πŸ—ΊοΈ Trip Management +- πŸ“… Itinerary Planning +- πŸ”’ Secure Password Hashing +- ⚑ CORS Support + +## Tech Stack + +- Python 3.x +- Flask +- SQLAlchemy +- Flask-JWT-Extended +- SQLite Database +- BCrypt for password hashing + +## API Endpoints + +### Authentication + +- `POST /auth/register` - Register a new user + ```json + { + "email": "user@example.com", + "password": "secure_password" + } + ``` + +- `POST /auth/login` - Login and get JWT token + ```json + \{ + "email": "user@example.com", + "password": "secure_password" +} + ``` + +### Trips + +- `GET /trips` - Get all trips +- `POST /trips` - Create a new trip + ```json + { + "destination": "Paris, France", + "start_date": "2024-06-15T00:00:00Z", + "end_date": "2024-06-22T00:00:00Z", + "latitude": 48.8566, + "longitude": 2.3522, + "itinerary": {} + } + ``` +- `GET /trips/` - Get a single trip +- `PUT /trips/` - Update a trip +- `DELETE /trips/` - Delete a trip + + + + diff --git a/planventure-api/app.py b/planventure-api/app.py new file mode 100644 index 0000000..9cea4d3 --- /dev/null +++ b/planventure-api/app.py @@ -0,0 +1,80 @@ +from flask import Flask, jsonify +from flask_cors import CORS +from flask_sqlalchemy import SQLAlchemy +from flask_jwt_extended import JWTManager +from os import environ +from dotenv import load_dotenv +from datetime import timedelta +from config import Config + +# Load environment variables +load_dotenv() + +# Initialize SQLAlchemy +db = SQLAlchemy() + +def create_app(): + app = Flask(__name__) + + # Configure CORS + CORS(app, + resources={r"/*": { + "origins": Config.CORS_ORIGINS, + "methods": Config.CORS_METHODS, + "allow_headers": Config.CORS_HEADERS, + "supports_credentials": Config.CORS_SUPPORTS_CREDENTIALS + }}) + + # JWT Configuration + app.config['JWT_SECRET_KEY'] = environ.get('JWT_SECRET_KEY', 'your-secret-key') + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1) + jwt = JWTManager(app) + + @jwt.expired_token_loader + def expired_token_callback(jwt_header, jwt_data): + return jsonify({ + 'error': 'Token has expired', + 'code': 'token_expired' + }), 401 + + @jwt.invalid_token_loader + def invalid_token_callback(error): + return jsonify({ + 'error': 'Invalid token', + 'code': 'invalid_token' + }), 401 + + @jwt.unauthorized_loader + def missing_token_callback(error): + return jsonify({ + 'error': 'Authorization token is missing', + 'code': 'authorization_required' + }), 401 + + # Database configuration + app.config['SQLALCHEMY_DATABASE_URI'] = environ.get('DATABASE_URL', 'sqlite:///planventure.db') + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + # Initialize extensions + db.init_app(app) + + # Register blueprints + from routes.auth import auth_bp + from routes.trips import trips_bp + app.register_blueprint(auth_bp, url_prefix='/auth') + app.register_blueprint(trips_bp, url_prefix='/api') + + # Register routes + @app.route('/') + def home(): + return jsonify({"message": "Welcome to PlanVenture API"}) + + @app.route('/health') + def health_check(): + return jsonify({"status": "healthy"}) + + return app + +if __name__ == '__main__': + app = create_app() + app.run(debug=True) \ No newline at end of file diff --git a/planventure-api/config.py b/planventure-api/config.py new file mode 100644 index 0000000..6596ffc --- /dev/null +++ b/planventure-api/config.py @@ -0,0 +1,28 @@ +from os import environ +from dotenv import load_dotenv + +load_dotenv() + +class Config: + # CORS Settings + CORS_ORIGINS = environ.get( + 'CORS_ORIGINS', + 'http://localhost:3000,http://localhost:5173' + ).split(',') + + CORS_HEADERS = [ + 'Content-Type', + 'Authorization', + 'Access-Control-Allow-Credentials' + ] + + CORS_METHODS = [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'OPTIONS' + ] + + # Cookie Settings + CORS_SUPPORTS_CREDENTIALS = True diff --git a/planventure-api/init_db.py b/planventure-api/init_db.py new file mode 100644 index 0000000..1aa9518 --- /dev/null +++ b/planventure-api/init_db.py @@ -0,0 +1,12 @@ +from app import create_app, db +from models import User + +def init_db(): + app = create_app() + with app.app_context(): + # Create all database tables + db.create_all() + print("Database tables created successfully!") + +if __name__ == '__main__': + init_db() diff --git a/planventure-api/instance/planventure.db b/planventure-api/instance/planventure.db new file mode 100644 index 0000000..b0955cd Binary files /dev/null and b/planventure-api/instance/planventure.db differ diff --git a/planventure-api/middleware/auth.py b/planventure-api/middleware/auth.py new file mode 100644 index 0000000..6633c0b --- /dev/null +++ b/planventure-api/middleware/auth.py @@ -0,0 +1,21 @@ +from functools import wraps +from flask import jsonify +from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity +from models import User + +def auth_middleware(f): + @wraps(f) + def decorated(*args, **kwargs): + try: + verify_jwt_in_request() + current_user_id = get_jwt_identity() + + # Check if user still exists in database + user = User.query.get(current_user_id) + if not user: + return jsonify({"error": "User not found"}), 401 + + return f(*args, **kwargs) + except Exception as e: + return jsonify({"error": "Invalid or expired token"}), 401 + return decorated diff --git a/planventure-api/models/__init__.py b/planventure-api/models/__init__.py new file mode 100644 index 0000000..34a38f3 --- /dev/null +++ b/planventure-api/models/__init__.py @@ -0,0 +1,4 @@ +from .user import User +from .trip import Trip + +__all__ = ['User', 'Trip'] \ No newline at end of file diff --git a/planventure-api/models/trip.py b/planventure-api/models/trip.py new file mode 100644 index 0000000..d82b832 --- /dev/null +++ b/planventure-api/models/trip.py @@ -0,0 +1,22 @@ +from datetime import datetime, timezone +from app import db + +class Trip(db.Model): + __tablename__ = 'trips' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + destination = db.Column(db.String(200), nullable=False) + start_date = db.Column(db.DateTime, nullable=False) + end_date = db.Column(db.DateTime, nullable=False) + latitude = db.Column(db.Float) + longitude = db.Column(db.Float) + itinerary = db.Column(db.JSON) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) + + # Relationship + user = db.relationship('User', back_populates='trips') + + def __repr__(self): + return f'' diff --git a/planventure-api/models/user.py b/planventure-api/models/user.py new file mode 100644 index 0000000..3aacbdf --- /dev/null +++ b/planventure-api/models/user.py @@ -0,0 +1,39 @@ +from datetime import datetime, timezone +from flask_jwt_extended import create_access_token +from app import db +from utils.password import hash_password, check_password + +class User(db.Model): + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) + + # Add relationship + trips = db.relationship('Trip', back_populates='user', cascade='all, delete-orphan') + + @property + def password(self): + raise AttributeError('password is not a readable attribute') + + @password.setter + def password(self, password): + self.password_hash = hash_password(password) + + def verify_password(self, password): + return check_password(password, self.password_hash) + + def generate_auth_token(self): + """Generate JWT token for the user""" + return create_access_token(identity=(str(self.id))) + + @staticmethod + def verify_auth_token(token): + """Verify the auth token - handled by @auth_required decorator""" + pass + + def __repr__(self): + return f'' diff --git a/planventure-api/requirements.txt b/planventure-api/requirements.txt new file mode 100644 index 0000000..20cf188 --- /dev/null +++ b/planventure-api/requirements.txt @@ -0,0 +1,15 @@ +# Core dependencies +flask==2.3.3 +flask-sqlalchemy==3.1.1 +flask-cors==4.0.0 +python-dotenv==1.0.0 + +# Authentication +flask-jwt-extended==4.5.3 +werkzeug==2.3.7 + +# Password hashing +bcrypt==4.0.1 + +# Database migrations +Flask-Migrate==4.0.5 \ No newline at end of file diff --git a/planventure-api/routes/__init__.py b/planventure-api/routes/__init__.py new file mode 100644 index 0000000..d437beb --- /dev/null +++ b/planventure-api/routes/__init__.py @@ -0,0 +1,4 @@ +from .auth import auth_bp +from .trips import trips_bp + +__all__ = ['auth_bp', 'trips_bp'] diff --git a/planventure-api/routes/auth.py b/planventure-api/routes/auth.py new file mode 100644 index 0000000..025c446 --- /dev/null +++ b/planventure-api/routes/auth.py @@ -0,0 +1,70 @@ +from flask import Blueprint, request, jsonify +from app import db +from models import User +from utils.validators import validate_email + +auth_bp = Blueprint('auth', __name__) + +# Error responses +INVALID_CREDENTIALS = {"error": "Invalid email or password"}, 401 +MISSING_FIELDS = {"error": "Missing required fields"}, 400 +INVALID_EMAIL = {"error": "Invalid email format"}, 400 +EMAIL_EXISTS = {"error": "Email already registered"}, 409 + +@auth_bp.route('/register', methods=['POST']) +def register(): + data = request.get_json() + + # Validate required fields + if not all(k in data for k in ['email', 'password']): + return jsonify(MISSING_FIELDS) + + # Validate email format + if not validate_email(data['email']): + return jsonify(INVALID_EMAIL) + + # Check if user already exists + if User.query.filter_by(email=data['email']).first(): + return jsonify(EMAIL_EXISTS) + + # Create new user + try: + user = User(email=data['email']) + user.password = data['password'] # This will hash the password + db.session.add(user) + db.session.commit() + + # Generate auth token + token = user.generate_auth_token() + return jsonify({ + 'message': 'User registered successfully', + 'token': token + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': 'Registration failed'}), 500 + +@auth_bp.route('/login', methods=['POST']) +def login(): + data = request.get_json() + + # Validate required fields + if not all(k in data for k in ['email', 'password']): + return jsonify(MISSING_FIELDS) + + # Find user by email + user = User.query.filter_by(email=data['email']).first() + + # Verify user exists and password is correct + if user and user.verify_password(data['password']): + token = user.generate_auth_token() + return jsonify({ + 'message': 'Login successful', + 'token': token, + 'user': { + 'id': user.id, + 'email': user.email + } + }), 200 + + return jsonify(INVALID_CREDENTIALS) diff --git a/planventure-api/routes/trips.py b/planventure-api/routes/trips.py new file mode 100644 index 0000000..78efa57 --- /dev/null +++ b/planventure-api/routes/trips.py @@ -0,0 +1,203 @@ +from flask import Blueprint, request, jsonify, current_app, make_response +from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request +from flask_cors import cross_origin +from config import Config +from app import db +from models import Trip +from middleware.auth import auth_middleware +from datetime import datetime +import logging +from utils.itinerary import generate_default_itinerary + +trips_bp = Blueprint('trips', __name__) + +def validate_auth_header(): + auth_header = request.headers.get('Authorization') + if not auth_header: + return False, 'Authorization header is missing' + if not auth_header.startswith('Bearer '): + return False, 'Invalid authorization format. Use Bearer token' + return True, None + +@trips_bp.route('/trips', methods=['GET', 'POST', 'OPTIONS']) +@cross_origin( + origins=Config.CORS_ORIGINS, + methods=Config.CORS_METHODS, + allow_headers=Config.CORS_HEADERS, + supports_credentials=Config.CORS_SUPPORTS_CREDENTIALS +) +@auth_middleware +def handle_trips(): + # Handle preflight OPTIONS request + if request.method == 'OPTIONS': + response = make_response() + response.headers.add('Access-Control-Allow-Methods', ','.join(Config.CORS_METHODS)) + response.headers.add('Access-Control-Allow-Headers', ','.join(Config.CORS_HEADERS)) + return response + + try: + # Log incoming request details + token = request.headers.get('Authorization', '').replace('Bearer ', '') + current_app.logger.debug(f"Received token: {token[:10]}...") # Log first 10 chars for safety + + # Verify JWT token explicitly + verify_jwt_in_request() + user_id = get_jwt_identity() + current_app.logger.debug(f"Authenticated user_id: {user_id}") + + if request.method == 'POST': + return create_trip() + return get_trips() + except Exception as e: + current_app.logger.error(f"Authentication error: {str(e)}") + return jsonify({'error': str(e)}), 401 + +def create_trip(): + try: + data = request.get_json() + user_id = get_jwt_identity() + + if not user_id: + return jsonify({'error': 'Invalid user token'}), 401 + + # Rest of the create_trip function remains the same + required_fields = ['destination', 'start_date', 'end_date'] + if not all(field in data for field in required_fields): + return jsonify({'error': 'Missing required fields'}), 400 + + start_date = datetime.fromisoformat(data['start_date'].replace('Z', '+00:00')) + end_date = datetime.fromisoformat(data['end_date'].replace('Z', '+00:00')) + + # Generate default itinerary if none provided + itinerary = data.get('itinerary', generate_default_itinerary(start_date, end_date)) + + trip = Trip( + user_id=user_id, + destination=data['destination'], + start_date=start_date, + end_date=end_date, + latitude=data.get('latitude'), + longitude=data.get('longitude'), + itinerary=itinerary + ) + + db.session.add(trip) + db.session.commit() + + return jsonify({ + 'message': 'Trip created successfully', + 'trip_id': trip.id + }), 201 + except ValueError as ve: + return jsonify({'error': 'Invalid date format'}), 400 + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Create trip error: {str(e)}") + return jsonify({'error': 'Failed to create trip'}), 500 + +def get_trips(): + user_id = get_jwt_identity() + trips = Trip.query.filter_by(user_id=user_id).all() + + return jsonify({ + 'trips': [{ + 'id': trip.id, + 'destination': trip.destination, + 'start_date': trip.start_date.isoformat(), + 'end_date': trip.end_date.isoformat(), + 'latitude': trip.latitude, + 'longitude': trip.longitude, + 'itinerary': trip.itinerary + } for trip in trips] + }), 200 + +@trips_bp.route('/trips/', methods=['GET', 'PUT', 'DELETE', 'OPTIONS']) +@cross_origin( + origins=Config.CORS_ORIGINS, + methods=Config.CORS_METHODS, + allow_headers=Config.CORS_HEADERS, + supports_credentials=Config.CORS_SUPPORTS_CREDENTIALS +) +@auth_middleware +def handle_trip(trip_id): + if request.method == 'OPTIONS': + response = make_response() + response.headers.add('Access-Control-Allow-Methods', ','.join(Config.CORS_METHODS)) + response.headers.add('Access-Control-Allow-Headers', ','.join(Config.CORS_HEADERS)) + return response + + if request.method == 'GET': + return get_trip(trip_id) + elif request.method == 'PUT': + return update_trip(trip_id) + elif request.method == 'DELETE': + return delete_trip(trip_id) + +def get_trip(trip_id): + user_id = get_jwt_identity() + trip = Trip.query.filter_by(id=trip_id, user_id=user_id).first() + + if not trip: + return jsonify({'error': 'Trip not found'}), 404 + + return jsonify({ + 'id': trip.id, + 'destination': trip.destination, + 'start_date': trip.start_date.isoformat(), + 'end_date': trip.end_date.isoformat(), + 'latitude': trip.latitude, + 'longitude': trip.longitude, + 'itinerary': trip.itinerary + }), 200 + +def update_trip(trip_id): + user_id = get_jwt_identity() + trip = Trip.query.filter_by(id=trip_id, user_id=user_id).first() + + if not trip: + return jsonify({'error': 'Trip not found'}), 404 + + data = request.get_json() + + try: + if 'destination' in data: + trip.destination = data['destination'] + if 'start_date' in data or 'end_date' in data: + start_date = datetime.fromisoformat(data.get('start_date', trip.start_date.isoformat()).replace('Z', '+00:00')) + end_date = datetime.fromisoformat(data.get('end_date', trip.end_date.isoformat()).replace('Z', '+00:00')) + # Generate new itinerary template for new dates if itinerary is not provided + if 'itinerary' not in data: + data['itinerary'] = generate_default_itinerary(start_date, end_date) + if 'latitude' in data: + trip.latitude = data['latitude'] + if 'longitude' in data: + trip.longitude = data['longitude'] + if 'itinerary' in data: + trip.itinerary = data['itinerary'] + + db.session.commit() + return jsonify({'message': 'Trip updated successfully'}), 200 + except ValueError: + return jsonify({'error': 'Invalid date format'}), 400 + except Exception as e: + db.session.rollback() + return jsonify({'error': 'Failed to update trip'}), 500 + +def delete_trip(trip_id): + user_id = get_jwt_identity() + trip = Trip.query.filter_by(id=trip_id, user_id=user_id).first() + + if not trip: + return jsonify({'error': 'Trip not found'}), 404 + + try: + db.session.delete(trip) + db.session.commit() + return jsonify({'message': 'Trip deleted successfully'}), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': 'Failed to delete trip'}), 500 + +@trips_bp.errorhandler(405) +def method_not_allowed(e): + return jsonify({'error': 'Method not allowed'}), 405 diff --git a/planventure-api/utils/__init__.py b/planventure-api/utils/__init__.py new file mode 100644 index 0000000..98b5427 --- /dev/null +++ b/planventure-api/utils/__init__.py @@ -0,0 +1,11 @@ +from .password import hash_password, check_password +from .auth import auth_required, get_current_user_id +from .itinerary import generate_default_itinerary + +__all__ = [ + 'hash_password', + 'check_password', + 'auth_required', + 'get_current_user_id', + 'generate_default_itinerary' +] diff --git a/planventure-api/utils/auth.py b/planventure-api/utils/auth.py new file mode 100644 index 0000000..6a16a80 --- /dev/null +++ b/planventure-api/utils/auth.py @@ -0,0 +1,16 @@ +from functools import wraps +from flask import jsonify +from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity + +def auth_required(f): + @wraps(f) + def decorated(*args, **kwargs): + try: + verify_jwt_in_request() + return f(*args, **kwargs) + except Exception as e: + return jsonify({"msg": "Invalid token"}), 401 + return decorated + +def get_current_user_id(): + return get_jwt_identity() diff --git a/planventure-api/utils/itinerary.py b/planventure-api/utils/itinerary.py new file mode 100644 index 0000000..44be7d9 --- /dev/null +++ b/planventure-api/utils/itinerary.py @@ -0,0 +1,29 @@ +from datetime import datetime, timedelta + +def generate_default_itinerary(start_date: datetime, end_date: datetime) -> dict: + """Generate a default itinerary template for the trip duration""" + itinerary = {} + current_date = start_date + + while current_date <= end_date: + date_str = current_date.strftime('%Y-%m-%d') + itinerary[date_str] = { + 'activities': [], + 'meals': { + 'breakfast': {'time': '08:00', 'place': '', 'notes': ''}, + 'lunch': {'time': '13:00', 'place': '', 'notes': ''}, + 'dinner': {'time': '19:00', 'place': '', 'notes': ''} + }, + 'accommodation': { + 'name': '', + 'address': '', + 'check_in': '', + 'check_out': '', + 'confirmation': '' + }, + 'transportation': [], + 'notes': '' + } + current_date += timedelta(days=1) + + return itinerary diff --git a/planventure-api/utils/password.py b/planventure-api/utils/password.py new file mode 100644 index 0000000..133f9e2 --- /dev/null +++ b/planventure-api/utils/password.py @@ -0,0 +1,13 @@ +import bcrypt + +def hash_password(password: str) -> str: + """Hash a password using bcrypt""" + salt = bcrypt.gensalt() + return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8') + +def check_password(password: str, password_hash: str) -> bool: + """Verify a password against its hash""" + return bcrypt.checkpw( + password.encode('utf-8'), + password_hash.encode('utf-8') + ) diff --git a/planventure-api/utils/validators.py b/planventure-api/utils/validators.py new file mode 100644 index 0000000..887f770 --- /dev/null +++ b/planventure-api/utils/validators.py @@ -0,0 +1,6 @@ +import re + +def validate_email(email: str) -> bool: + """Validate email format using regex pattern""" + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return bool(re.match(pattern, email))