479 lines
14 KiB
Python
479 lines
14 KiB
Python
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/<int:journey_id>", 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/<int:journey_id>", 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/<int:journey_id>", 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/<int:post_id>", 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/<int:post_id>", 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/<int:post_id>", 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/<int:post_id>/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/<int:post_id>/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/<int:comment_id>', 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 """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><title>Journey Mapper API</title></head>
|
||
<body>
|
||
<h1>Journey Mapper API</h1>
|
||
<p>Backend is running. Use <code>/api/journeys</code> endpoints.</p>
|
||
<p>Authentication required: register, login, then use session cookies.</p>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app.run(debug=True, port=5000)
|