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)) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy