Add backend user authentification

This commit is contained in:
Josh-Dev-Quest 2026-03-27 20:31:14 +01:00
parent bcc86be6c4
commit f88fed7c89
No known key found for this signature in database

View File

@ -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)
# ==================== Peruser 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, userspecific) ====================
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/<int:journey_id>', 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/<int:journey_id>', 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/<int:journey_id>', 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, userspecific) ====================
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 '''
<!DOCTYPE html>
<html>
<head><title>Journey Mapper Backend</title></head>
<body>
<h1>Journey Mapper API</h1>
<p>Backend is running. Use <code>/api/journeys</code> endpoints.</p>
</body>
</html>
'''
# 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/<int:post_id>', 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/<int:post_id>', 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/<int:post_id>', 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 '''
<!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)