diff --git a/backend/app.py b/backend/app.py index 99270b4..4823853 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,53 +1,199 @@ -from flask import Flask, request, jsonify +from flask import Flask, request, jsonify, session from flask_cors import CORS +from werkzeug.security import generate_password_hash, check_password_hash import json import os from datetime import datetime app = Flask(__name__) -CORS(app) # Enable CORS for all routes +app.secret_key = 'your-secret-key-here-change-in-production' # needed for sessions +CORS(app, supports_credentials=True) # allow cookies -# Data directory and file +# Directories DATA_DIR = 'data' -DATA_FILE = os.path.join(DATA_DIR, 'journeys.json') +USERS_FILE = os.path.join(DATA_DIR, 'users.json') os.makedirs(DATA_DIR, exist_ok=True) -# In-memory store (loaded from file on startup) -journeys = [] - -def load_journeys(): - """Load journeys from the JSON file.""" - global journeys +# ==================== User helpers ==================== +def load_users(): try: - if os.path.exists(DATA_FILE): - with open(DATA_FILE, 'r') as f: - journeys = json.load(f) - else: - journeys = [] + if os.path.exists(USERS_FILE): + with open(USERS_FILE, 'r') as f: + return json.load(f) except Exception as e: - print(f"Error loading journeys: {e}") - journeys = [] + print(f"Error loading users: {e}") + return [] -def save_journeys(): - """Save journeys to the JSON file.""" +def save_users(users): try: - with open(DATA_FILE, 'w') as f: + with open(USERS_FILE, 'w') as f: + json.dump(users, f, indent=2) + except Exception as e: + print(f"Error saving users: {e}") + +def get_next_user_id(users): + if not users: + return 1 + return max(u['id'] for u in users) + 1 + +def get_user_by_username(username): + users = load_users() + return next((u for u in users if u['username'] == username), None) + +def get_user_by_id(user_id): + users = load_users() + return next((u for u in users if u['id'] == user_id), None) + +# ==================== Per‑user data helpers ==================== +def get_user_data_dir(user_id): + user_dir = os.path.join(DATA_DIR, 'users', str(user_id)) + os.makedirs(user_dir, exist_ok=True) + return user_dir + +def load_user_journeys(user_id): + file_path = os.path.join(get_user_data_dir(user_id), 'journeys.json') + try: + if os.path.exists(file_path): + with open(file_path, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Error loading journeys for user {user_id}: {e}") + return [] + +def save_user_journeys(user_id, journeys): + file_path = os.path.join(get_user_data_dir(user_id), 'journeys.json') + try: + with open(file_path, 'w') as f: json.dump(journeys, f, indent=2) except Exception as e: - print(f"Error saving journeys: {e}") + print(f"Error saving journeys for user {user_id}: {e}") -# Load existing journeys on startup -load_journeys() +def load_user_posts(user_id): + file_path = os.path.join(get_user_data_dir(user_id), 'posts.json') + try: + if os.path.exists(file_path): + with open(file_path, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Error loading posts for user {user_id}: {e}") + return [] -def get_next_id(): - """Return the next available ID (simple integer increment).""" +def save_user_posts(user_id, posts): + file_path = os.path.join(get_user_data_dir(user_id), 'posts.json') + try: + with open(file_path, 'w') as f: + json.dump(posts, f, indent=2) + except Exception as e: + print(f"Error saving posts for user {user_id}: {e}") + +# ==================== Authentication endpoints ==================== +@app.route('/api/register', methods=['POST']) +def register(): + data = request.get_json() + username = data.get('username') + password = data.get('password') + + if not username or not password: + return jsonify({'error': 'Username and password required'}), 400 + + # Check if username already exists + if get_user_by_username(username): + return jsonify({'error': 'Username already taken'}), 409 + + users = load_users() + new_id = get_next_user_id(users) + + hashed = generate_password_hash(password) + new_user = { + 'id': new_id, + 'username': username, + 'password_hash': hashed, + 'created_at': datetime.now().isoformat() + } + users.append(new_user) + save_users(users) + + # Create empty data files for the new user + save_user_journeys(new_id, []) + save_user_posts(new_id, []) + + # Log the user in automatically + session['user_id'] = new_id + + return jsonify({ + 'id': new_id, + 'username': username, + 'message': 'Registration successful' + }), 201 + +@app.route('/api/login', methods=['POST']) +def login(): + data = request.get_json() + username = data.get('username') + password = data.get('password') + + user = get_user_by_username(username) + if not user or not check_password_hash(user['password_hash'], password): + return jsonify({'error': 'Invalid username or password'}), 401 + + session['user_id'] = user['id'] + return jsonify({ + 'id': user['id'], + 'username': user['username'], + 'message': 'Login successful' + }) + +@app.route('/api/logout', methods=['POST']) +def logout(): + session.pop('user_id', None) + return jsonify({'message': 'Logged out'}) + +@app.route('/api/me', methods=['GET']) +def me(): + user_id = session.get('user_id') + if not user_id: + return jsonify({'error': 'Not logged in'}), 401 + user = get_user_by_id(user_id) + if not user: + # Should not happen, but clean session + session.pop('user_id', None) + return jsonify({'error': 'User not found'}), 401 + return jsonify({ + 'id': user['id'], + 'username': user['username'] + }) + +# ==================== Journey endpoints (protected, user‑specific) ==================== +def require_login(): + if 'user_id' not in session: + return False + return True + +def get_current_user_id(): + return session.get('user_id') + +def get_journeys_for_current_user(): + user_id = get_current_user_id() + if user_id is None: + return None + return load_user_journeys(user_id) + +@app.route('/api/journeys', methods=['GET']) +def get_journeys(): + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 + journeys = get_journeys_for_current_user() + return jsonify(journeys) + +def get_next_journey_id(journeys): if not journeys: return 1 return max(j['id'] for j in journeys) + 1 @app.route('/api/journeys', methods=['POST']) def create_journey(): - """Create a new journey.""" + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 data = request.get_json() if not data: return jsonify({'error': 'No data provided'}), 400 @@ -56,26 +202,27 @@ def create_journey(): if not title: return jsonify({'error': 'Journey title is required'}), 400 + user_id = get_current_user_id() + journeys = get_journeys_for_current_user() + new_id = get_next_journey_id(journeys) + new_journey = { - 'id': get_next_id(), + 'id': new_id, 'title': title, 'description': data.get('description', ''), - 'markers': data.get('markers', []), # list of marker objects + 'markers': data.get('markers', []), 'created_at': datetime.now().isoformat() } journeys.append(new_journey) - save_journeys() + save_user_journeys(user_id, journeys) return jsonify(new_journey), 201 -@app.route('/api/journeys', methods=['GET']) -def get_journeys(): - """Return all journeys.""" - return jsonify(journeys) - @app.route('/api/journeys/', methods=['GET']) def get_journey(journey_id): - """Return a specific journey by ID.""" + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 + journeys = get_journeys_for_current_user() journey = next((j for j in journeys if j['id'] == journey_id), None) if journey is None: return jsonify({'error': 'Journey not found'}), 404 @@ -83,7 +230,9 @@ def get_journey(journey_id): @app.route('/api/journeys/', methods=['PUT']) def update_journey(journey_id): - """Update an existing journey.""" + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 + journeys = get_journeys_for_current_user() journey = next((j for j in journeys if j['id'] == journey_id), None) if journey is None: return jsonify({'error': 'Journey not found'}), 404 @@ -92,7 +241,6 @@ def update_journey(journey_id): if not data: return jsonify({'error': 'No data provided'}), 400 - # Update allowed fields if 'title' in data: journey['title'] = data['title'] if 'description' in data: @@ -100,98 +248,86 @@ def update_journey(journey_id): if 'markers' in data: journey['markers'] = data['markers'] - save_journeys() + save_user_journeys(get_current_user_id(), journeys) return jsonify(journey) @app.route('/api/journeys/', methods=['DELETE']) def delete_journey(journey_id): - """Delete a journey.""" - global journeys + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 + journeys = get_journeys_for_current_user() journey = next((j for j in journeys if j['id'] == journey_id), None) if journey is None: return jsonify({'error': 'Journey not found'}), 404 journeys = [j for j in journeys if j['id'] != journey_id] - save_journeys() + save_user_journeys(get_current_user_id(), journeys) return jsonify({'message': 'Journey deleted successfully', 'journey': journey}) -@app.route('/api/journeys/health', methods=['GET']) -def health_check(): - """Simple health check endpoint.""" - return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()}) +# ==================== Blog Post endpoints (protected, user‑specific) ==================== +def get_posts_for_current_user(): + user_id = get_current_user_id() + if user_id is None: + return None + return load_user_posts(user_id) -@app.route('/') -def index(): - """Root endpoint – just a welcome message.""" - return ''' - - - Journey Mapper Backend - -

Journey Mapper API

-

Backend is running. Use /api/journeys endpoints.

- - - ''' - - -# Blog posts data file -BLOG_DATA_FILE = os.path.join(DATA_DIR, 'blog_posts.json') - -def load_blog_posts(): - try: - if os.path.exists(BLOG_DATA_FILE): - with open(BLOG_DATA_FILE, 'r') as f: - return json.load(f) - except: - pass - return [] - -def save_blog_posts(posts): - with open(BLOG_DATA_FILE, 'w') as f: - json.dump(posts, f, indent=2) - -blog_posts = load_blog_posts() - -def get_next_blog_id(): - if not blog_posts: +def get_next_post_id(posts): + if not posts: return 1 - return max(p['id'] for p in blog_posts) + 1 + return max(p['id'] for p in posts) + 1 @app.route('/api/blog-posts', methods=['GET']) def get_blog_posts(): - return jsonify(blog_posts) + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 + posts = get_posts_for_current_user() + return jsonify(posts) @app.route('/api/blog-posts/', methods=['GET']) def get_blog_post(post_id): - post = next((p for p in blog_posts if p['id'] == post_id), None) + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 + posts = get_posts_for_current_user() + post = next((p for p in posts if p['id'] == post_id), None) if not post: return jsonify({'error': 'Post not found'}), 404 return jsonify(post) @app.route('/api/blog-posts', methods=['POST']) def create_blog_post(): + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 data = request.get_json() title = data.get('title') if not title: return jsonify({'error': 'Title required'}), 400 + + user_id = get_current_user_id() + posts = get_posts_for_current_user() + new_id = get_next_post_id(posts) + new_post = { - 'id': get_next_blog_id(), + 'id': new_id, 'title': title, 'content': data.get('content', ''), 'journeyId': data.get('journeyId'), 'image': data.get('image'), 'created_at': datetime.now().isoformat() } - blog_posts.append(new_post) - save_blog_posts(blog_posts) + + posts.append(new_post) + save_user_posts(user_id, posts) return jsonify(new_post), 201 @app.route('/api/blog-posts/', methods=['PUT']) def update_blog_post(post_id): - post = next((p for p in blog_posts if p['id'] == post_id), None) + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 + posts = get_posts_for_current_user() + post = next((p for p in posts if p['id'] == post_id), None) if not post: return jsonify({'error': 'Post not found'}), 404 + data = request.get_json() if 'title' in data: post['title'] = data['title'] @@ -201,19 +337,41 @@ def update_blog_post(post_id): post['journeyId'] = data['journeyId'] if 'image' in data: post['image'] = data['image'] - save_blog_posts(blog_posts) + + save_user_posts(get_current_user_id(), posts) return jsonify(post) @app.route('/api/blog-posts/', methods=['DELETE']) def delete_blog_post(post_id): - global blog_posts - post = next((p for p in blog_posts if p['id'] == post_id), None) + if not require_login(): + return jsonify({'error': 'Authentication required'}), 401 + posts = get_posts_for_current_user() + post = next((p for p in posts if p['id'] == post_id), None) if not post: return jsonify({'error': 'Post not found'}), 404 - blog_posts = [p for p in blog_posts if p['id'] != post_id] - save_blog_posts(blog_posts) + + posts = [p for p in posts if p['id'] != post_id] + save_user_posts(get_current_user_id(), posts) return jsonify({'message': 'Post deleted'}) +# ==================== Health and root ==================== +@app.route('/api/journeys/health', methods=['GET']) +def health_check(): + return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()}) + +@app.route('/') +def index(): + return ''' + + + Journey Mapper API + +

Journey Mapper API

+

Backend is running. Use /api/journeys endpoints.

+

Authentication required: register, login, then use session cookies.

+ + + ''' if __name__ == '__main__': app.run(debug=True, port=5000) \ No newline at end of file