Spaces:
Runtime error
Runtime error
Upload 10 files
Browse files- app.py +33 -0
- config/database.py +41 -0
- models/history.py +84 -0
- models/user.py +96 -0
- requirements.txt +7 -0
- routes/auth.py +109 -0
- routes/history.py +66 -0
- routes/recommend.py +222 -0
- routes/user.py +82 -0
- utils/jwt_helper.py +27 -0
app.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
from config.database import init_db
|
| 4 |
+
from routes.auth import auth_bp
|
| 5 |
+
from routes.recommend import recommend_bp
|
| 6 |
+
from routes.history import history_bp
|
| 7 |
+
from routes.user import user_bp
|
| 8 |
+
import os
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
app = Flask(__name__)
|
| 14 |
+
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
|
| 15 |
+
|
| 16 |
+
# Enable CORS
|
| 17 |
+
CORS(app)
|
| 18 |
+
|
| 19 |
+
# Initialize MongoDB
|
| 20 |
+
init_db()
|
| 21 |
+
|
| 22 |
+
# Register blueprints
|
| 23 |
+
app.register_blueprint(auth_bp, url_prefix='/auth')
|
| 24 |
+
app.register_blueprint(recommend_bp)
|
| 25 |
+
app.register_blueprint(history_bp)
|
| 26 |
+
app.register_blueprint(user_bp, url_prefix='/user')
|
| 27 |
+
|
| 28 |
+
@app.route('/health', methods=['GET'])
|
| 29 |
+
def health_check():
|
| 30 |
+
return {"status": "healthy", "message": "Flask backend is running"}, 200
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__":
|
| 33 |
+
app.run(port=5000, debug=True)
|
config/database.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pymongo import MongoClient
|
| 2 |
+
import os
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
client = None
|
| 8 |
+
db = None
|
| 9 |
+
|
| 10 |
+
def init_db():
|
| 11 |
+
global client, db
|
| 12 |
+
|
| 13 |
+
MONGO_URI = os.getenv('MONGO_URI')
|
| 14 |
+
DATABASE_NAME = os.getenv('DATABASE_NAME', 'recommendation_system')
|
| 15 |
+
|
| 16 |
+
if not MONGO_URI:
|
| 17 |
+
raise ValueError("MONGO_URI environment variable is required")
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
client = MongoClient(MONGO_URI)
|
| 21 |
+
db = client[DATABASE_NAME]
|
| 22 |
+
|
| 23 |
+
# Test connection
|
| 24 |
+
client.admin.command('ping')
|
| 25 |
+
print(f"Connected to MongoDB database: {DATABASE_NAME}")
|
| 26 |
+
|
| 27 |
+
# Create indexes for performance
|
| 28 |
+
db.users.create_index("email", unique=True)
|
| 29 |
+
db.users.create_index("username", unique=True)
|
| 30 |
+
db.history.create_index([("user_id", 1), ("timestamp", -1)])
|
| 31 |
+
|
| 32 |
+
return db
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"Failed to connect to MongoDB: {e}")
|
| 35 |
+
raise e
|
| 36 |
+
|
| 37 |
+
def get_db():
|
| 38 |
+
global db
|
| 39 |
+
if db is None:
|
| 40 |
+
init_db()
|
| 41 |
+
return db
|
models/history.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timezone
|
| 2 |
+
from config.database import get_db
|
| 3 |
+
from bson import ObjectId
|
| 4 |
+
|
| 5 |
+
class History:
|
| 6 |
+
def __init__(self, user_id, recommendation_type, genre, items, query_params=None):
|
| 7 |
+
self.user_id = ObjectId(user_id)
|
| 8 |
+
self.recommendation_type = recommendation_type
|
| 9 |
+
self.genre = genre
|
| 10 |
+
self.items = items
|
| 11 |
+
self.query_params = query_params or {}
|
| 12 |
+
self.timestamp = datetime.now(timezone.utc)
|
| 13 |
+
|
| 14 |
+
def save(self):
|
| 15 |
+
db = get_db()
|
| 16 |
+
history_data = {
|
| 17 |
+
"user_id": self.user_id,
|
| 18 |
+
"recommendation_type": self.recommendation_type,
|
| 19 |
+
"genre": self.genre,
|
| 20 |
+
"items": self.items,
|
| 21 |
+
"query_params": self.query_params,
|
| 22 |
+
"timestamp": self.timestamp
|
| 23 |
+
}
|
| 24 |
+
result = db.history.insert_one(history_data)
|
| 25 |
+
return str(result.inserted_id)
|
| 26 |
+
|
| 27 |
+
@staticmethod
|
| 28 |
+
def get_user_history(user_id, limit=50, offset=0):
|
| 29 |
+
db = get_db()
|
| 30 |
+
try:
|
| 31 |
+
history = list(db.history.find(
|
| 32 |
+
{"user_id": ObjectId(user_id)}
|
| 33 |
+
).sort("timestamp", -1).skip(offset).limit(limit))
|
| 34 |
+
|
| 35 |
+
# Convert ObjectId to string for JSON serialization
|
| 36 |
+
for item in history:
|
| 37 |
+
item["_id"] = str(item["_id"])
|
| 38 |
+
item["user_id"] = str(item["user_id"])
|
| 39 |
+
|
| 40 |
+
return history
|
| 41 |
+
except:
|
| 42 |
+
return []
|
| 43 |
+
|
| 44 |
+
@staticmethod
|
| 45 |
+
def get_user_stats(user_id):
|
| 46 |
+
db = get_db()
|
| 47 |
+
try:
|
| 48 |
+
# Type statistics
|
| 49 |
+
type_pipeline = [
|
| 50 |
+
{"$match": {"user_id": ObjectId(user_id)}},
|
| 51 |
+
{"$group": {
|
| 52 |
+
"_id": "$recommendation_type",
|
| 53 |
+
"count": {"$sum": 1},
|
| 54 |
+
"last_accessed": {"$max": "$timestamp"}
|
| 55 |
+
}}
|
| 56 |
+
]
|
| 57 |
+
type_stats = list(db.history.aggregate(type_pipeline))
|
| 58 |
+
|
| 59 |
+
# Genre preferences - handle both string and array genres
|
| 60 |
+
genre_pipeline = [
|
| 61 |
+
{"$match": {"user_id": ObjectId(user_id)}},
|
| 62 |
+
{"$unwind": "$genre"},
|
| 63 |
+
{"$group": {
|
| 64 |
+
"_id": "$genre",
|
| 65 |
+
"count": {"$sum": 1}
|
| 66 |
+
}},
|
| 67 |
+
{"$sort": {"count": -1}},
|
| 68 |
+
{"$limit": 10}
|
| 69 |
+
]
|
| 70 |
+
genre_stats = list(db.history.aggregate(genre_pipeline))
|
| 71 |
+
|
| 72 |
+
total_count = db.history.count_documents({"user_id": ObjectId(user_id)})
|
| 73 |
+
|
| 74 |
+
return {
|
| 75 |
+
"type_stats": type_stats,
|
| 76 |
+
"genre_preferences": genre_stats,
|
| 77 |
+
"total_recommendations": total_count
|
| 78 |
+
}
|
| 79 |
+
except:
|
| 80 |
+
return {
|
| 81 |
+
"type_stats": [],
|
| 82 |
+
"genre_preferences": [],
|
| 83 |
+
"total_recommendations": 0
|
| 84 |
+
}
|
models/user.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timezone
|
| 2 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 3 |
+
from config.database import get_db
|
| 4 |
+
from bson import ObjectId
|
| 5 |
+
|
| 6 |
+
class User:
|
| 7 |
+
def __init__(self, username=None, email=None, password=None, preferences=None):
|
| 8 |
+
self.username = username
|
| 9 |
+
self.email = email
|
| 10 |
+
self.password_hash = generate_password_hash(password) if password else None
|
| 11 |
+
self.preferences = preferences or {"genres": [], "types": []}
|
| 12 |
+
self.created_at = datetime.now(timezone.utc)
|
| 13 |
+
self.last_login = None
|
| 14 |
+
|
| 15 |
+
def save(self):
|
| 16 |
+
db = get_db()
|
| 17 |
+
user_data = {
|
| 18 |
+
"username": self.username,
|
| 19 |
+
"email": self.email,
|
| 20 |
+
"password_hash": self.password_hash,
|
| 21 |
+
"preferences": self.preferences,
|
| 22 |
+
"created_at": self.created_at,
|
| 23 |
+
"last_login": self.last_login
|
| 24 |
+
}
|
| 25 |
+
result = db.users.insert_one(user_data)
|
| 26 |
+
return str(result.inserted_id)
|
| 27 |
+
|
| 28 |
+
@staticmethod
|
| 29 |
+
def find_by_email(email):
|
| 30 |
+
db = get_db()
|
| 31 |
+
user_data = db.users.find_one({"email": email})
|
| 32 |
+
if user_data:
|
| 33 |
+
user = User()
|
| 34 |
+
user._id = user_data["_id"]
|
| 35 |
+
user.username = user_data["username"]
|
| 36 |
+
user.email = user_data["email"]
|
| 37 |
+
user.password_hash = user_data["password_hash"]
|
| 38 |
+
user.preferences = user_data.get("preferences", {"genres": [], "types": []})
|
| 39 |
+
user.created_at = user_data["created_at"]
|
| 40 |
+
user.last_login = user_data.get("last_login")
|
| 41 |
+
return user
|
| 42 |
+
return None
|
| 43 |
+
|
| 44 |
+
@staticmethod
|
| 45 |
+
def find_by_username(username):
|
| 46 |
+
db = get_db()
|
| 47 |
+
user_data = db.users.find_one({"username": username})
|
| 48 |
+
if user_data:
|
| 49 |
+
user = User()
|
| 50 |
+
user._id = user_data["_id"]
|
| 51 |
+
user.username = user_data["username"]
|
| 52 |
+
user.email = user_data["email"]
|
| 53 |
+
user.password_hash = user_data["password_hash"]
|
| 54 |
+
user.preferences = user_data.get("preferences", {"genres": [], "types": []})
|
| 55 |
+
user.created_at = user_data["created_at"]
|
| 56 |
+
user.last_login = user_data.get("last_login")
|
| 57 |
+
return user
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def find_by_id(user_id):
|
| 62 |
+
db = get_db()
|
| 63 |
+
try:
|
| 64 |
+
user_data = db.users.find_one({"_id": ObjectId(user_id)})
|
| 65 |
+
if user_data:
|
| 66 |
+
user = User()
|
| 67 |
+
user._id = user_data["_id"]
|
| 68 |
+
user.username = user_data["username"]
|
| 69 |
+
user.email = user_data["email"]
|
| 70 |
+
user.password_hash = user_data["password_hash"]
|
| 71 |
+
user.preferences = user_data.get("preferences", {"genres": [], "types": []})
|
| 72 |
+
user.created_at = user_data["created_at"]
|
| 73 |
+
user.last_login = user_data.get("last_login")
|
| 74 |
+
return user
|
| 75 |
+
except:
|
| 76 |
+
pass
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
def check_password(self, password):
|
| 80 |
+
return check_password_hash(self.password_hash, password)
|
| 81 |
+
|
| 82 |
+
def update_last_login(self):
|
| 83 |
+
db = get_db()
|
| 84 |
+
self.last_login = datetime.now(timezone.utc)
|
| 85 |
+
db.users.update_one(
|
| 86 |
+
{"_id": self._id},
|
| 87 |
+
{"$set": {"last_login": self.last_login}}
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
def update_preferences(self, preferences):
|
| 91 |
+
db = get_db()
|
| 92 |
+
self.preferences = preferences
|
| 93 |
+
db.users.update_one(
|
| 94 |
+
{"_id": self._id},
|
| 95 |
+
{"$set": {"preferences": preferences}}
|
| 96 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==2.3.3
|
| 2 |
+
pymongo==4.5.0
|
| 3 |
+
PyJWT==2.8.0
|
| 4 |
+
Werkzeug==2.3.7
|
| 5 |
+
python-dotenv==1.0.0
|
| 6 |
+
requests==2.31.0
|
| 7 |
+
flask-cors==4.0.0
|
routes/auth.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, request, jsonify
|
| 2 |
+
from models.user import User
|
| 3 |
+
from utils.jwt_helper import generate_jwt
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
auth_bp = Blueprint('auth', __name__)
|
| 7 |
+
|
| 8 |
+
def validate_email(email):
|
| 9 |
+
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
| 10 |
+
return re.match(pattern, email) is not None
|
| 11 |
+
|
| 12 |
+
def validate_password(password):
|
| 13 |
+
return len(password) >= 6
|
| 14 |
+
|
| 15 |
+
@auth_bp.route('/signup', methods=['POST'])
|
| 16 |
+
def signup():
|
| 17 |
+
try:
|
| 18 |
+
data = request.get_json()
|
| 19 |
+
|
| 20 |
+
if not data:
|
| 21 |
+
return jsonify({"error": "No data provided"}), 400
|
| 22 |
+
|
| 23 |
+
username = data.get('username', '').strip()
|
| 24 |
+
email = data.get('email', '').strip().lower()
|
| 25 |
+
password = data.get('password', '')
|
| 26 |
+
preferences = data.get('preferences', {"genres": [], "types": []})
|
| 27 |
+
|
| 28 |
+
# Validation
|
| 29 |
+
if not username or not email or not password:
|
| 30 |
+
return jsonify({"error": "Username, email, and password are required"}), 400
|
| 31 |
+
|
| 32 |
+
if len(username) < 3:
|
| 33 |
+
return jsonify({"error": "Username must be at least 3 characters long"}), 400
|
| 34 |
+
|
| 35 |
+
if not validate_email(email):
|
| 36 |
+
return jsonify({"error": "Invalid email format"}), 400
|
| 37 |
+
|
| 38 |
+
if not validate_password(password):
|
| 39 |
+
return jsonify({"error": "Password must be at least 6 characters long"}), 400
|
| 40 |
+
|
| 41 |
+
# Check if user already exists
|
| 42 |
+
if User.find_by_email(email):
|
| 43 |
+
return jsonify({"error": "Email already registered"}), 409
|
| 44 |
+
|
| 45 |
+
if User.find_by_username(username):
|
| 46 |
+
return jsonify({"error": "Username already taken"}), 409
|
| 47 |
+
|
| 48 |
+
# Create new user
|
| 49 |
+
user = User(username=username, email=email, password=password, preferences=preferences)
|
| 50 |
+
user_id = user.save()
|
| 51 |
+
|
| 52 |
+
# Generate JWT token
|
| 53 |
+
token = generate_jwt(user_id, username, email)
|
| 54 |
+
|
| 55 |
+
return jsonify({
|
| 56 |
+
"message": "User created successfully",
|
| 57 |
+
"token": token,
|
| 58 |
+
"user": {
|
| 59 |
+
"id": user_id,
|
| 60 |
+
"username": username,
|
| 61 |
+
"email": email,
|
| 62 |
+
"preferences": preferences
|
| 63 |
+
}
|
| 64 |
+
}), 201
|
| 65 |
+
|
| 66 |
+
except Exception as e:
|
| 67 |
+
print(f"Signup error: {e}")
|
| 68 |
+
return jsonify({"error": "Internal server error"}), 500
|
| 69 |
+
|
| 70 |
+
@auth_bp.route('/login', methods=['POST'])
|
| 71 |
+
def login():
|
| 72 |
+
try:
|
| 73 |
+
data = request.get_json()
|
| 74 |
+
|
| 75 |
+
if not data:
|
| 76 |
+
return jsonify({"error": "No data provided"}), 400
|
| 77 |
+
|
| 78 |
+
email = data.get('email', '').strip().lower()
|
| 79 |
+
password = data.get('password', '')
|
| 80 |
+
|
| 81 |
+
if not email or not password:
|
| 82 |
+
return jsonify({"error": "Email and password are required"}), 400
|
| 83 |
+
|
| 84 |
+
# Find user by email
|
| 85 |
+
user = User.find_by_email(email)
|
| 86 |
+
if not user or not user.check_password(password):
|
| 87 |
+
return jsonify({"error": "Invalid email or password"}), 401
|
| 88 |
+
|
| 89 |
+
# Update last login
|
| 90 |
+
user.update_last_login()
|
| 91 |
+
|
| 92 |
+
# Generate JWT token
|
| 93 |
+
token = generate_jwt(user._id, user.username, user.email)
|
| 94 |
+
|
| 95 |
+
return jsonify({
|
| 96 |
+
"message": "Login successful",
|
| 97 |
+
"token": token,
|
| 98 |
+
"user": {
|
| 99 |
+
"id": str(user._id),
|
| 100 |
+
"username": user.username,
|
| 101 |
+
"email": user.email,
|
| 102 |
+
"preferences": user.preferences,
|
| 103 |
+
"last_login": user.last_login.isoformat() if user.last_login else None
|
| 104 |
+
}
|
| 105 |
+
}), 200
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"Login error: {e}")
|
| 109 |
+
return jsonify({"error": "Internal server error"}), 500
|
routes/history.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, request, jsonify
|
| 2 |
+
from utils.jwt_helper import decode_jwt
|
| 3 |
+
from models.history import History
|
| 4 |
+
|
| 5 |
+
history_bp = Blueprint('history', __name__)
|
| 6 |
+
|
| 7 |
+
@history_bp.route('/history', methods=['GET'])
|
| 8 |
+
def get_history():
|
| 9 |
+
try:
|
| 10 |
+
# JWT Authentication
|
| 11 |
+
auth_header = request.headers.get('Authorization')
|
| 12 |
+
if not auth_header or not auth_header.startswith('Bearer '):
|
| 13 |
+
return jsonify({"error": "Missing or invalid Authorization header"}), 401
|
| 14 |
+
|
| 15 |
+
token = auth_header.split(" ")[1]
|
| 16 |
+
try:
|
| 17 |
+
user_data = decode_jwt(token)
|
| 18 |
+
user_id = user_data.get("user_id")
|
| 19 |
+
except Exception as e:
|
| 20 |
+
return jsonify({"error": "Invalid or expired token"}), 401
|
| 21 |
+
|
| 22 |
+
# Get query parameters
|
| 23 |
+
limit = min(int(request.args.get('limit', 20)), 100) # Max 100 items
|
| 24 |
+
offset = int(request.args.get('offset', 0))
|
| 25 |
+
|
| 26 |
+
# Get user history
|
| 27 |
+
history = History.get_user_history(user_id, limit=limit, offset=offset)
|
| 28 |
+
|
| 29 |
+
return jsonify({
|
| 30 |
+
"status": "success",
|
| 31 |
+
"history": history,
|
| 32 |
+
"count": len(history),
|
| 33 |
+
"limit": limit,
|
| 34 |
+
"offset": offset
|
| 35 |
+
}), 200
|
| 36 |
+
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"History error: {e}")
|
| 39 |
+
return jsonify({"error": "Internal server error"}), 500
|
| 40 |
+
|
| 41 |
+
@history_bp.route('/history/stats', methods=['GET'])
|
| 42 |
+
def get_history_stats():
|
| 43 |
+
try:
|
| 44 |
+
# JWT Authentication
|
| 45 |
+
auth_header = request.headers.get('Authorization')
|
| 46 |
+
if not auth_header or not auth_header.startswith('Bearer '):
|
| 47 |
+
return jsonify({"error": "Missing or invalid Authorization header"}), 401
|
| 48 |
+
|
| 49 |
+
token = auth_header.split(" ")[1]
|
| 50 |
+
try:
|
| 51 |
+
user_data = decode_jwt(token)
|
| 52 |
+
user_id = user_data.get("user_id")
|
| 53 |
+
except Exception as e:
|
| 54 |
+
return jsonify({"error": "Invalid or expired token"}), 401
|
| 55 |
+
|
| 56 |
+
# Get user stats
|
| 57 |
+
stats = History.get_user_stats(user_id)
|
| 58 |
+
|
| 59 |
+
return jsonify({
|
| 60 |
+
"status": "success",
|
| 61 |
+
"stats": stats
|
| 62 |
+
}), 200
|
| 63 |
+
|
| 64 |
+
except Exception as e:
|
| 65 |
+
print(f"Stats error: {e}")
|
| 66 |
+
return jsonify({"error": "Internal server error"}), 500
|
routes/recommend.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, request, jsonify
|
| 2 |
+
import requests
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from utils.jwt_helper import decode_jwt
|
| 6 |
+
from models.history import History
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
recommend_bp = Blueprint('recommend', __name__)
|
| 11 |
+
|
| 12 |
+
RECOMMENDER_ENDPOINTS = {
|
| 13 |
+
"movie": os.getenv("MOVIE_RECOMMENDER_URL"),
|
| 14 |
+
"book": os.getenv("BOOK_RECOMMENDER_URL"),
|
| 15 |
+
"tv": os.getenv("TV_RECOMMENDER_URL")
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
RESPONSE_KEYS = {
|
| 19 |
+
"movie": "movies",
|
| 20 |
+
"book": "books",
|
| 21 |
+
"tv": "shows"
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
@recommend_bp.route('/recommend/tvshowrec', methods=['POST'])
|
| 25 |
+
def recommend():
|
| 26 |
+
try:
|
| 27 |
+
data = request.get_json()
|
| 28 |
+
|
| 29 |
+
if not data:
|
| 30 |
+
return jsonify({"error": "No data provided"}), 400
|
| 31 |
+
|
| 32 |
+
rec_type = data.get('type')
|
| 33 |
+
genre = data.get('genre')
|
| 34 |
+
top_k = data.get('top_k', 10)
|
| 35 |
+
|
| 36 |
+
if not rec_type or not genre:
|
| 37 |
+
return jsonify({"error": "Missing 'type' or 'genre'"}), 400
|
| 38 |
+
|
| 39 |
+
if rec_type not in RECOMMENDER_ENDPOINTS or not RECOMMENDER_ENDPOINTS[rec_type]:
|
| 40 |
+
return jsonify({"error": "Invalid or missing recommender URL for type."}), 400
|
| 41 |
+
|
| 42 |
+
# JWT Authentication
|
| 43 |
+
auth_header = request.headers.get('Authorization')
|
| 44 |
+
if not auth_header or not auth_header.startswith('Bearer '):
|
| 45 |
+
return jsonify({"error": "Missing or invalid Authorization header"}), 401
|
| 46 |
+
|
| 47 |
+
token = auth_header.split(" ")[1]
|
| 48 |
+
try:
|
| 49 |
+
user_data = decode_jwt(token)
|
| 50 |
+
user_id = user_data.get("user_id")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
return jsonify({"error": "Invalid or expired token"}), 401
|
| 53 |
+
|
| 54 |
+
# Process genres
|
| 55 |
+
genres_list = [g.strip() for g in genre.split(",")] if isinstance(genre, str) else genre
|
| 56 |
+
|
| 57 |
+
# Call microservice
|
| 58 |
+
try:
|
| 59 |
+
recommender_url = RECOMMENDER_ENDPOINTS[rec_type]
|
| 60 |
+
response = requests.post(
|
| 61 |
+
recommender_url,
|
| 62 |
+
json={"genres": genres_list},
|
| 63 |
+
timeout=10
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
if not response.ok:
|
| 67 |
+
return jsonify({
|
| 68 |
+
"error": f"{rec_type} service error",
|
| 69 |
+
"details": response.text,
|
| 70 |
+
"status_code": response.status_code
|
| 71 |
+
}), 500
|
| 72 |
+
|
| 73 |
+
result = response.json()
|
| 74 |
+
if result.get("status") != "success":
|
| 75 |
+
return jsonify({"error": result.get("message", "Unknown error")}), 500
|
| 76 |
+
|
| 77 |
+
raw_items = result.get(RESPONSE_KEYS.get(rec_type, "items"), [])
|
| 78 |
+
|
| 79 |
+
# Normalize response format
|
| 80 |
+
normalized = []
|
| 81 |
+
for item in raw_items:
|
| 82 |
+
normalized.append({
|
| 83 |
+
"type": rec_type,
|
| 84 |
+
"name": item.get("name") or item.get("title"),
|
| 85 |
+
"creator": item.get("director") or item.get("author") or item.get("creator"),
|
| 86 |
+
"description": item.get("description", ""),
|
| 87 |
+
"genre": item.get("genre", []),
|
| 88 |
+
"rating": item.get("rating"),
|
| 89 |
+
"year": item.get("year"),
|
| 90 |
+
"image_url": item.get("image_url")
|
| 91 |
+
})
|
| 92 |
+
|
| 93 |
+
# Save to history
|
| 94 |
+
try:
|
| 95 |
+
history = History(
|
| 96 |
+
user_id=user_id,
|
| 97 |
+
recommendation_type=rec_type,
|
| 98 |
+
genre=genres_list,
|
| 99 |
+
items=normalized,
|
| 100 |
+
query_params={"top_k": top_k}
|
| 101 |
+
)
|
| 102 |
+
history.save()
|
| 103 |
+
print(f"Saved recommendation history for user {user_id}")
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"Failed to save history: {e}")
|
| 106 |
+
# Don't fail the request if history saving fails
|
| 107 |
+
|
| 108 |
+
return jsonify({
|
| 109 |
+
"status": "success",
|
| 110 |
+
"recommendations": normalized,
|
| 111 |
+
"count": len(normalized),
|
| 112 |
+
"type": rec_type,
|
| 113 |
+
"genres": genres_list
|
| 114 |
+
}), 200
|
| 115 |
+
|
| 116 |
+
except requests.exceptions.Timeout:
|
| 117 |
+
return jsonify({"error": f"{rec_type} service timeout"}), 504
|
| 118 |
+
except requests.exceptions.RequestException as e:
|
| 119 |
+
return jsonify({"error": f"Failed to connect to {rec_type} service", "details": str(e)}), 503
|
| 120 |
+
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"Recommend error: {e}")
|
| 123 |
+
return jsonify({"error": "Internal server error"}), 500
|
| 124 |
+
|
| 125 |
+
@recommend_bp.route('/recommend/movies', methods=['POST'])
|
| 126 |
+
def recommend():
|
| 127 |
+
try:
|
| 128 |
+
data = request.get_json()
|
| 129 |
+
|
| 130 |
+
if not data:
|
| 131 |
+
return jsonify({"error": "No data provided"}), 400
|
| 132 |
+
|
| 133 |
+
rec_type = data.get('type')
|
| 134 |
+
genre = data.get('genre')
|
| 135 |
+
top_k = data.get('top_k', 10)
|
| 136 |
+
|
| 137 |
+
if not rec_type or not genre:
|
| 138 |
+
return jsonify({"error": "Missing 'type' or 'genre'"}), 400
|
| 139 |
+
|
| 140 |
+
if rec_type not in RECOMMENDER_ENDPOINTS or not RECOMMENDER_ENDPOINTS[rec_type]:
|
| 141 |
+
return jsonify({"error": "Invalid or missing recommender URL for type."}), 400
|
| 142 |
+
|
| 143 |
+
# JWT Authentication
|
| 144 |
+
auth_header = request.headers.get('Authorization')
|
| 145 |
+
if not auth_header or not auth_header.startswith('Bearer '):
|
| 146 |
+
return jsonify({"error": "Missing or invalid Authorization header"}), 401
|
| 147 |
+
|
| 148 |
+
token = auth_header.split(" ")[1]
|
| 149 |
+
try:
|
| 150 |
+
user_data = decode_jwt(token)
|
| 151 |
+
user_id = user_data.get("user_id")
|
| 152 |
+
except Exception as e:
|
| 153 |
+
return jsonify({"error": "Invalid or expired token"}), 401
|
| 154 |
+
|
| 155 |
+
# Process genres
|
| 156 |
+
genres_list = [g.strip() for g in genre.split(",")] if isinstance(genre, str) else genre
|
| 157 |
+
|
| 158 |
+
# Call microservice
|
| 159 |
+
try:
|
| 160 |
+
recommender_url = RECOMMENDER_ENDPOINTS[rec_type]
|
| 161 |
+
response = requests.post(
|
| 162 |
+
recommender_url,
|
| 163 |
+
json={"genres": genres_list, "top_k": top_k},
|
| 164 |
+
timeout=10
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
if not response.ok:
|
| 168 |
+
return jsonify({
|
| 169 |
+
"error": f"{rec_type} service error",
|
| 170 |
+
"details": response.text,
|
| 171 |
+
"status_code": response.status_code
|
| 172 |
+
}), 500
|
| 173 |
+
|
| 174 |
+
result = response.json()
|
| 175 |
+
if result.get("status") != "success":
|
| 176 |
+
return jsonify({"error": result.get("message", "Unknown error")}), 500
|
| 177 |
+
|
| 178 |
+
raw_items = result.get(RESPONSE_KEYS.get(rec_type, "items"), [])
|
| 179 |
+
|
| 180 |
+
# Normalize response format
|
| 181 |
+
normalized = []
|
| 182 |
+
for item in raw_items:
|
| 183 |
+
normalized.append({
|
| 184 |
+
"type": rec_type,
|
| 185 |
+
"name": item.get("name") or item.get("title"),
|
| 186 |
+
"creator": item.get("director") or item.get("author") or item.get("creator"),
|
| 187 |
+
"description": item.get("description", ""),
|
| 188 |
+
"genre": item.get("genre", []),
|
| 189 |
+
"rating": item.get("rating")
|
| 190 |
+
})
|
| 191 |
+
|
| 192 |
+
# Save to history
|
| 193 |
+
try:
|
| 194 |
+
history = History(
|
| 195 |
+
user_id=user_id,
|
| 196 |
+
recommendation_type=rec_type,
|
| 197 |
+
genre=genres_list,
|
| 198 |
+
items=normalized,
|
| 199 |
+
query_params={"top_k": top_k}
|
| 200 |
+
)
|
| 201 |
+
history.save()
|
| 202 |
+
print(f"Saved recommendation history for user {user_id}")
|
| 203 |
+
except Exception as e:
|
| 204 |
+
print(f"Failed to save history: {e}")
|
| 205 |
+
# Don't fail the request if history saving fails
|
| 206 |
+
|
| 207 |
+
return jsonify({
|
| 208 |
+
"status": "success",
|
| 209 |
+
"recommendations": normalized,
|
| 210 |
+
"count": len(normalized),
|
| 211 |
+
"type": rec_type,
|
| 212 |
+
"genres": genres_list
|
| 213 |
+
}), 200
|
| 214 |
+
|
| 215 |
+
except requests.exceptions.Timeout:
|
| 216 |
+
return jsonify({"error": f"{rec_type} service timeout"}), 504
|
| 217 |
+
except requests.exceptions.RequestException as e:
|
| 218 |
+
return jsonify({"error": f"Failed to connect to {rec_type} service", "details": str(e)}), 503
|
| 219 |
+
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"Recommend error: {e}")
|
| 222 |
+
return jsonify({"error": "Internal server error"}), 500
|
routes/user.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, request, jsonify
|
| 2 |
+
from utils.jwt_helper import decode_jwt
|
| 3 |
+
from models.user import User
|
| 4 |
+
|
| 5 |
+
user_bp = Blueprint('user', __name__)
|
| 6 |
+
|
| 7 |
+
@user_bp.route('/profile', methods=['GET'])
|
| 8 |
+
def get_profile():
|
| 9 |
+
try:
|
| 10 |
+
# JWT Authentication
|
| 11 |
+
auth_header = request.headers.get('Authorization')
|
| 12 |
+
if not auth_header or not auth_header.startswith('Bearer '):
|
| 13 |
+
return jsonify({"error": "Missing or invalid Authorization header"}), 401
|
| 14 |
+
|
| 15 |
+
token = auth_header.split(" ")[1]
|
| 16 |
+
try:
|
| 17 |
+
user_data = decode_jwt(token)
|
| 18 |
+
user_id = user_data.get("user_id")
|
| 19 |
+
except Exception as e:
|
| 20 |
+
return jsonify({"error": "Invalid or expired token"}), 401
|
| 21 |
+
|
| 22 |
+
# Get user profile
|
| 23 |
+
user = User.find_by_id(user_id)
|
| 24 |
+
if not user:
|
| 25 |
+
return jsonify({"error": "User not found"}), 404
|
| 26 |
+
|
| 27 |
+
return jsonify({
|
| 28 |
+
"status": "success",
|
| 29 |
+
"user": {
|
| 30 |
+
"id": str(user._id),
|
| 31 |
+
"username": user.username,
|
| 32 |
+
"email": user.email,
|
| 33 |
+
"preferences": user.preferences,
|
| 34 |
+
"created_at": user.created_at.isoformat(),
|
| 35 |
+
"last_login": user.last_login.isoformat() if user.last_login else None
|
| 36 |
+
}
|
| 37 |
+
}), 200
|
| 38 |
+
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"Profile error: {e}")
|
| 41 |
+
return jsonify({"error": "Internal server error"}), 500
|
| 42 |
+
|
| 43 |
+
@user_bp.route('/preferences', methods=['PUT'])
|
| 44 |
+
def update_preferences():
|
| 45 |
+
try:
|
| 46 |
+
# JWT Authentication
|
| 47 |
+
auth_header = request.headers.get('Authorization')
|
| 48 |
+
if not auth_header or not auth_header.startswith('Bearer '):
|
| 49 |
+
return jsonify({"error": "Missing or invalid Authorization header"}), 401
|
| 50 |
+
|
| 51 |
+
token = auth_header.split(" ")[1]
|
| 52 |
+
try:
|
| 53 |
+
user_data = decode_jwt(token)
|
| 54 |
+
user_id = user_data.get("user_id")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
return jsonify({"error": "Invalid or expired token"}), 401
|
| 57 |
+
|
| 58 |
+
data = request.get_json()
|
| 59 |
+
if not data:
|
| 60 |
+
return jsonify({"error": "No data provided"}), 400
|
| 61 |
+
|
| 62 |
+
preferences = data.get('preferences')
|
| 63 |
+
|
| 64 |
+
if not preferences or not isinstance(preferences, dict):
|
| 65 |
+
return jsonify({"error": "Valid preferences object is required"}), 400
|
| 66 |
+
|
| 67 |
+
# Update user preferences
|
| 68 |
+
user = User.find_by_id(user_id)
|
| 69 |
+
if not user:
|
| 70 |
+
return jsonify({"error": "User not found"}), 404
|
| 71 |
+
|
| 72 |
+
user.update_preferences(preferences)
|
| 73 |
+
|
| 74 |
+
return jsonify({
|
| 75 |
+
"status": "success",
|
| 76 |
+
"message": "Preferences updated successfully",
|
| 77 |
+
"preferences": preferences
|
| 78 |
+
}), 200
|
| 79 |
+
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"Preferences error: {e}")
|
| 82 |
+
return jsonify({"error": "Internal server error"}), 500
|
utils/jwt_helper.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import jwt
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime, timedelta, timezone
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
SECRET_KEY = os.getenv("SECRET_KEY")
|
| 9 |
+
|
| 10 |
+
def generate_jwt(user_id, username, email):
|
| 11 |
+
payload = {
|
| 12 |
+
"user_id": str(user_id),
|
| 13 |
+
"username": username,
|
| 14 |
+
"email": email,
|
| 15 |
+
"exp": datetime.now(timezone.utc) + timedelta(days=7),
|
| 16 |
+
"iat": datetime.now(timezone.utc)
|
| 17 |
+
}
|
| 18 |
+
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
|
| 19 |
+
|
| 20 |
+
def decode_jwt(token):
|
| 21 |
+
try:
|
| 22 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
| 23 |
+
return payload
|
| 24 |
+
except jwt.ExpiredSignatureError:
|
| 25 |
+
raise Exception("Token has expired")
|
| 26 |
+
except jwt.InvalidTokenError:
|
| 27 |
+
raise Exception("Invalid token")
|