import os import time import json from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from flask import Flask, request, jsonify, session from flask_cors import CORS app = Flask(__name__) app.secret_key = "your-secret-key-here-change-in-production" # needed for sessions CORS(app, supports_credentials=True) # allow cookies # Directories DATA_DIR = "data" USERS_FILE = os.path.join(DATA_DIR, "users.json") os.makedirs(DATA_DIR, exist_ok=True) # ==================== User helpers ==================== def load_users(): try: 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 users: {e}") return [] def save_users(users): try: 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 for user {user_id}: {e}") 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 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(): if not require_login(): return jsonify({"error": "Authentication required"}), 401 data = request.get_json() if not data: return jsonify({"error": "No data provided"}), 400 title = data.get("title") 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": new_id, "title": title, "description": data.get("description", ""), "markers": data.get("markers", []), "created_at": datetime.now().isoformat(), } journeys.append(new_journey) save_user_journeys(user_id, journeys) return jsonify(new_journey), 201 @app.route("/api/journeys/", methods=["GET"]) def get_journey(journey_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 return jsonify(journey) @app.route("/api/journeys/", methods=["PUT"]) def update_journey(journey_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 data = request.get_json() if not data: return jsonify({"error": "No data provided"}), 400 if "title" in data: journey["title"] = data["title"] if "description" in data: journey["description"] = data["description"] if "markers" in data: journey["markers"] = data["markers"] save_user_journeys(get_current_user_id(), journeys) return jsonify(journey) @app.route("/api/journeys/", methods=["DELETE"]) def delete_journey(journey_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 journeys = [j for j in journeys if j["id"] != journey_id] save_user_journeys(get_current_user_id(), journeys) return jsonify({"message": "Journey deleted successfully", "journey": journey}) # ==================== 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) def get_next_post_id(posts): if not posts: return 1 return max(p["id"] for p in posts) + 1 @app.route("/api/blog-posts", methods=["GET"]) def get_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): 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": new_id, "title": title, "content": data.get("content", ""), "journeyId": data.get("journeyId"), "image": data.get("image"), "author_id": user_id, "created_at": datetime.now().isoformat(), } 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): 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 not get_current_user_id() == data["author_id"]: return jsonify({"error": "Wrong user"}) if "title" in data: post["title"] = data["title"] if "content" in data: post["content"] = data["content"] if "journeyId" in data: post["journeyId"] = data["journeyId"] if "image" in data: post["image"] = data["image"] save_user_posts(get_current_user_id(), posts) return jsonify(post) @app.route("/api/blog-posts/", methods=["DELETE"]) def delete_blog_post(post_id): 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 posts = [p for p in posts if p["id"] != post_id] save_user_posts(get_current_user_id(), posts) return jsonify({"message": "Post deleted"}) # ==================== Comments (stored inside posts) ==================== def get_post_by_id(user_id, post_id): posts = load_user_posts(user_id) return next((p for p in posts if p['id'] == post_id), None) def save_post(user_id, post): posts = load_user_posts(user_id) for i, p in enumerate(posts): if p['id'] == post['id']: posts[i] = post break save_user_posts(user_id, posts) @app.route('/api/posts//comments', methods=['GET']) def get_comments(post_id): user_id = session.get('user_id') if not user_id: return jsonify({'error': 'Authentication required'}), 401 post = get_post_by_id(user_id, post_id) if not post: return jsonify({'error': 'Post not found'}), 404 return jsonify(post.get('comments', [])) @app.route('/api/posts//comments', methods=['POST']) def add_comment(post_id): user_id = session.get('user_id') if not user_id: return jsonify({'error': 'Authentication required'}), 401 data = request.get_json() text = data.get('text') if not text: return jsonify({'error': 'Comment text required'}), 400 post = get_post_by_id(user_id, post_id) if not post: return jsonify({'error': 'Post not found'}), 404 comment = { 'id': int(time.time() * 1000), # simple unique id 'author_id': user_id, 'author_name': get_user_by_id(user_id)['username'], 'text': text, 'created_at': datetime.now().isoformat() } if 'comments' not in post: post['comments'] = [] post['comments'].append(comment) save_post(user_id, post) return jsonify(comment), 201 @app.route('/api/comments/', methods=['DELETE']) def delete_comment(comment_id): user_id = session.get('user_id') if not user_id: return jsonify({'error': 'Authentication required'}), 401 # Find which post contains this comment posts = load_user_posts(user_id) for post in posts: if 'comments' in post: for i, c in enumerate(post['comments']): if c['id'] == comment_id: # Allow deletion if current user is comment author or post author if c['author_id'] == user_id or post['id'] == post.get('author_id', user_id): del post['comments'][i] save_post(user_id, post) return jsonify({'message': 'Comment deleted'}) else: return jsonify({'error': 'Not authorized'}), 403 return jsonify({'error': 'Comment not found'}), 404 # ==================== 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)