2026-03-29 11:00:12 +02:00

380 lines
12 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")
JOURNEYS_FILE = os.path.join(DATA_DIR, 'journeys.json')
os.makedirs(DATA_DIR, exist_ok=True)
# ==================== User helpers ====================
def require_login():
if "user_id" not in session:
return False
return True
def get_current_user_id():
return session.get("user_id")
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)
# ==================== 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)
# 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 helper functions ====================
def load_all_journeys():
try:
if os.path.exists(JOURNEYS_FILE):
with open(JOURNEYS_FILE, 'r') as f:
return json.load(f)
except Exception as e:
print(f"Error loading journeys: {e}")
return []
def save_all_journeys(journeys):
try:
with open(JOURNEYS_FILE, 'w') as f:
json.dump(journeys, f, indent=2)
except Exception as e:
print(f"Error saving journeys: {e}")
def get_next_journey_id(journeys):
if not journeys:
return 1
return max(j['id'] for j in journeys) + 1
def get_journey_by_id(journey_id):
journeys = load_all_journeys()
return next((j for j in journeys if j['id'] == journey_id), None)
def user_can_view_journey(journey, user_id):
if journey['owner_id'] == user_id:
return True
if journey.get('visibility') == 'public':
return True
if journey.get('visibility') == 'shared' and user_id in journey.get('shared_read', []):
return True
return False
def user_can_edit_journey(journey, user_id):
if journey['owner_id'] == user_id:
return True
if journey.get('visibility') == 'shared' and user_id in journey.get('shared_edit', []):
return True
return False
# ==================== Journey endpoints ====================
@app.route('/api/journeys', methods=['GET'])
def get_journeys():
if not require_login():
return jsonify({'error': 'Authentication required'}), 401
user_id = get_current_user_id()
all_journeys = load_all_journeys()
result = []
for j in all_journeys:
if user_can_view_journey(j, user_id):
# Add extra flags for the frontend
j_copy = j.copy()
j_copy['can_edit'] = user_can_edit_journey(j, user_id)
result.append(j_copy)
return jsonify(result)
@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 = load_all_journeys()
new_id = get_next_journey_id(journeys)
new_journey = {
'id': new_id,
'owner_id': user_id,
'title': title,
'description': data.get('description', ''),
'markers': data.get('markers', []),
'created_at': datetime.now().isoformat(),
'visibility': data.get('visibility', 'private'),
'shared_read': data.get('shared_read', []),
'shared_edit': data.get('shared_edit', []),
'comments': data.get('comments', [])
}
journeys.append(new_journey)
save_all_journeys(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
user_id = get_current_user_id()
journey = get_journey_by_id(journey_id)
if journey is None:
return jsonify({'error': 'Journey not found'}), 404
if not user_can_view_journey(journey, user_id):
return jsonify({'error': 'Access denied'}), 403
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
user_id = get_current_user_id()
journeys = load_all_journeys()
journey = next((j for j in journeys if j['id'] == journey_id), None)
if journey is None:
return jsonify({'error': 'Journey not found'}), 404
if not user_can_edit_journey(journey, user_id):
return jsonify({'error': 'Not authorized to edit this journey'}), 403
data = request.get_json()
if 'title' in data:
journey['title'] = data['title']
if 'description' in data:
journey['description'] = data['description']
if 'markers' in data:
journey['markers'] = data['markers']
if 'visibility' in data:
journey['visibility'] = data['visibility']
if 'shared_read' in data:
journey['shared_read'] = data['shared_read']
if 'shared_edit' in data:
journey['shared_edit'] = data['shared_edit']
if 'comments' in data:
journey['comments'] = data ['comments']
save_all_journeys(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
user_id = get_current_user_id()
journeys = load_all_journeys()
journey = next((j for j in journeys if j['id'] == journey_id), None)
if journey is None:
return jsonify({'error': 'Journey not found'}), 404
if journey['owner_id'] != user_id:
return jsonify({'error': 'Only the owner can delete this journey'}), 403
journeys = [j for j in journeys if j['id'] != journey_id]
save_all_journeys(journeys)
return jsonify({'message': 'Journey deleted successfully', 'journey': journey})
# ==================== Comments (stored inside journeys) ====================
def save_journey(journey):
journeys = load_all_journeys()
for i, j in enumerate(journeys):
if j['id'] == journey['id']:
journeys[i] = journey
break
save_all_journeys(journeys)
@app.route('/api/journeys/<int:journey_id>/comments', methods=['GET'])
def get_journey_comments(journey_id):
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Authentication required'}), 401
journey = get_journey_by_id(journey_id)
if not journey:
return jsonify({'error': 'Journey not found'}), 404
if not user_can_view_journey(journey, user_id):
return jsonify({'error': 'Access denied'}), 403
return jsonify(journey.get('comments', []))
@app.route('/api/journeys/<int:journey_id>/comments', methods=['POST'])
def add_journey_comment(journey_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
journey = get_journey_by_id(journey_id)
if not journey:
return jsonify({'error': 'Journey not found'}), 404
if not user_can_view_journey(journey, user_id):
return jsonify({'error': 'Access denied'}), 403
comment = {
'id': int(time.time() * 1000),
'author_id': user_id,
'author_name': get_user_by_id(user_id)['username'],
'text': text,
'created_at': datetime.now().isoformat(),
}
if 'comments' not in journey:
journey['comments'] = []
journey['comments'].append(comment)
save_journey(journey)
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
journeys = load_all_journeys()
for journey in journeys:
if 'comments' in journey:
for i, c in enumerate(journey['comments']):
if c['id'] == comment_id:
# Check permissions: comment author or journey owner can delete
if c['author_id'] == user_id or journey['owner_id'] == user_id:
del journey['comments'][i]
save_journey(journey)
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)