|
1 | 1 | # Flask backend application with GoodReads mood analysis integration |
2 | 2 | # Initialize Flask app, configure CORS, and setup mood analysis endpoints |
3 | 3 |
|
| 4 | +import sys |
| 5 | +import os |
| 6 | +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| 7 | +sys.path.append(os.path.dirname(os.path.abspath(__file__))) |
| 8 | + |
4 | 9 | from flask import Flask, request, jsonify, redirect, url_for |
5 | 10 | from flask_cors import CORS |
6 | 11 | from flask_jwt_extended import ( |
|
25 | 30 |
|
26 | 31 | import logging |
27 | 32 | from datetime import datetime, timedelta, timezone |
28 | | -import sys |
29 | | -import os |
30 | | -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
31 | | -from backend.sanitizer import sanitize_payload |
32 | | -from backend.reader_identity.routes import reader_identity_bp |
| 33 | +from sanitizer import sanitize_payload |
| 34 | +from reader_identity.routes import reader_identity_bp |
33 | 35 |
|
34 | 36 | # Load environment variables from config directory based on APP_ENV |
35 | 37 | env = os.getenv('APP_ENV', 'development') |
|
79 | 81 | validate_jwt_secret, |
80 | 82 | is_production_mode |
81 | 83 | ) |
82 | | -<<<<<<< HEAD |
83 | 84 |
|
84 | | -======= |
85 | | -from middleware import require_strong_password # Issue #790 |
| 85 | + |
| 86 | + |
| 87 | + |
86 | 88 | from password_reset_service import ( |
87 | 89 | FORGOT_PASSWORD_MESSAGE, |
88 | 90 | request_password_reset, |
89 | 91 | reset_password_with_token, |
90 | 92 | ) |
91 | | ->>>>>>> 040197406bf60becce17486e4b203a59814834f0 |
| 93 | + |
| 94 | + |
| 95 | + |
| 96 | +from email_service import ( |
| 97 | + build_password_reset_url, |
| 98 | + is_email_configured, |
| 99 | + send_password_reset_email, |
| 100 | +) |
| 101 | + |
92 | 102 | from collections import defaultdict, deque |
93 | 103 | from math import ceil |
94 | 104 | from time import time |
95 | | -from urllib.parse import quote |
96 | 105 | from error_responses import ( |
97 | 106 | ErrorCodes, error_response, success_response, |
98 | 107 | validation_error, missing_fields_error, invalid_json_error, |
@@ -778,13 +787,55 @@ def _validate_jwt_secret_startup(): |
778 | 787 |
|
779 | 788 | _validate_jwt_secret_startup() |
780 | 789 |
|
781 | | -@app.route('/api/v1/config', methods=['GET']) |
782 | | -def get_config(): |
783 | | - """Serve public configuration values like Google Books API Key.""" |
784 | | - return jsonify({ |
785 | | - "google_books_key": os.getenv('GOOGLE_BOOKS_API_KEY', ''), |
786 | | - "google_books_key_secondary": os.getenv('GOOGLE_BOOKS_API_KEY_SECONDARY', '') |
787 | | - }) |
| 790 | + |
| 791 | +@app.route('/api/v1/content/live-shelves', methods=['GET']) |
| 792 | +def get_live_shelves(): |
| 793 | + """Return the configuration for live discovery shelves on the frontend.""" |
| 794 | + discovery_shelves = [ |
| 795 | + { |
| 796 | + "type": "query", |
| 797 | + "query": "subject:mystery atmosphere", |
| 798 | + "elementId": "row-rainy", |
| 799 | + "title": "Rainy Evening Reads", |
| 800 | + "subtitle": "Mystery & Melancholy", |
| 801 | + "icon": "fa-cloud-rain" |
| 802 | + }, |
| 803 | + { |
| 804 | + "type": "query", |
| 805 | + "query": "authors:arundhati roy|subject:india", |
| 806 | + "elementId": "row-indian", |
| 807 | + "title": "Indian Authors", |
| 808 | + "subtitle": "Subcontinent Voices", |
| 809 | + "icon": "fa-feather" |
| 810 | + }, |
| 811 | + { |
| 812 | + "type": "query", |
| 813 | + "query": "subject:classic fiction", |
| 814 | + "elementId": "row-classics", |
| 815 | + "title": "Forgotten Classics", |
| 816 | + "subtitle": "Timeless & Dust-free", |
| 817 | + "icon": "fa-hourglass" |
| 818 | + }, |
| 819 | + { |
| 820 | + "type": "query", |
| 821 | + "query": "subject:gothic fiction subject:dark academia subject:campus", |
| 822 | + "elementId": "row-dark-academia", |
| 823 | + "title": "Dark Academia", |
| 824 | + "subtitle": "Gothic, cerebral, candlelit", |
| 825 | + "icon": "fa-feather-pointed", |
| 826 | + "vibeDescription": "gothic, intellectual, melancholic, and candlelit", |
| 827 | + "fallbackQuery": "subject:gothic fiction subject:campus" |
| 828 | + }, |
| 829 | + { |
| 830 | + "type": "query", |
| 831 | + "query": "subject:fiction", |
| 832 | + "elementId": "row-fiction", |
| 833 | + "title": "General Fiction", |
| 834 | + "subtitle": "Stories for everyone", |
| 835 | + "icon": "fa-book-open" |
| 836 | + } |
| 837 | + ] |
| 838 | + return success_response(data={"shelves": discovery_shelves}) |
788 | 839 |
|
789 | 840 | # ===================================================================== |
790 | 841 | # ENDPOINT: CSRF Token Retrieval |
@@ -1261,26 +1312,6 @@ def handle_chat(validated_data): |
1261 | 1312 | logger.error(f"Unexpected error in handle_chat: {type(e).__name__}: {e}", exc_info=True) |
1262 | 1313 | return handle_exception(e, "handle_chat") |
1263 | 1314 |
|
1264 | | -@app.route('/api/v1/health', methods=['GET']) |
1265 | | -def health_check(): |
1266 | | - """Health check endpoint with cache statistics.""" |
1267 | | - cache_stats = cache_service.get_stats() |
1268 | | - |
1269 | | - return jsonify({ |
1270 | | - "status": "healthy", |
1271 | | - "service": "BiblioDrift AI Service", |
1272 | | - "version": "2.0.0", |
1273 | | - "features": { |
1274 | | - "mood_analysis_available": MOOD_ANALYSIS_AVAILABLE, |
1275 | | - "llm_service_available": llm_service.is_available(), |
1276 | | - "openai_configured": llm_service.openai_client is not None, |
1277 | | - "groq_configured": llm_service.groq_client is not None, |
1278 | | - "gemini_configured": llm_service.gemini_client is not None, |
1279 | | - "preferred_llm": llm_service.preferred_llm, |
1280 | | - "caching_enabled": cache_stats.get('cache_type') != 'null' |
1281 | | - }, |
1282 | | - "cache": cache_stats |
1283 | | - }) |
1284 | 1315 |
|
1285 | 1316 |
|
1286 | 1317 | # ========================================================================= |
@@ -1688,15 +1719,15 @@ def sync_library(validated_data): |
1688 | 1719 | # immediately responds with an active session ready to go. |
1689 | 1720 | # ========================================================================= |
1690 | 1721 | @app.route('/api/v1/register', methods=['POST']) |
1691 | | -<<<<<<< HEAD |
1692 | | -@limiter.limit("5 per minute") |
| 1722 | + |
| 1723 | + |
1693 | 1724 |
|
1694 | 1725 | def register(): |
1695 | | -======= |
| 1726 | + |
1696 | 1727 | @limiter.limit("5 per 10 seconds") |
1697 | 1728 | @validate_schema(RegisterRequest) |
1698 | 1729 | def register(validated_data): |
1699 | | ->>>>>>> 040197406bf60becce17486e4b203a59814834f0 |
| 1730 | + |
1700 | 1731 | """Register a new user and return JWT token.""" |
1701 | 1732 | try: |
1702 | 1733 |
|
@@ -2006,20 +2037,36 @@ def forgot_password(validated_data): |
2006 | 2037 | logger.error("forgot-password database error: %s", e, exc_info=True) |
2007 | 2038 |
|
2008 | 2039 | response_data = {"message": FORGOT_PASSWORD_MESSAGE} |
2009 | | - |
2010 | | - if plain_token and app_config.is_development(): |
2011 | | - frontend_base = os.getenv( |
2012 | | - 'FRONTEND_ORIGIN', |
2013 | | - 'http://127.0.0.1:5500', |
2014 | | - ).rstrip('/') |
2015 | | - response_data["reset_url"] = ( |
2016 | | - f"{frontend_base}/pages/auth.html?token={quote(plain_token)}" |
2017 | | - ) |
2018 | | - logger.info( |
2019 | | - "Dev password reset link for %s: %s", |
2020 | | - validated_data.email, |
2021 | | - response_data["reset_url"], |
2022 | | - ) |
| 2040 | + frontend_base = os.getenv('FRONTEND_ORIGIN', 'http://127.0.0.1:5500').rstrip('/') |
| 2041 | + email_config = app_config.email |
| 2042 | + |
| 2043 | + if plain_token: |
| 2044 | + reset_url = build_password_reset_url(plain_token, frontend_base) |
| 2045 | + if is_email_configured(email_config): |
| 2046 | + send_result = send_password_reset_email( |
| 2047 | + validated_data.email, |
| 2048 | + reset_url, |
| 2049 | + email_config, |
| 2050 | + ) |
| 2051 | + if not send_result.ok: |
| 2052 | + logger.error( |
| 2053 | + "Password reset email not sent for %s: %s", |
| 2054 | + validated_data.email, |
| 2055 | + send_result.detail, |
| 2056 | + ) |
| 2057 | + elif app_config.is_development(): |
| 2058 | + response_data["reset_url"] = reset_url |
| 2059 | + logger.info( |
| 2060 | + "Dev password reset link for %s (email not configured): %s", |
| 2061 | + validated_data.email, |
| 2062 | + reset_url, |
| 2063 | + ) |
| 2064 | + elif app_config.is_production(): |
| 2065 | + logger.warning( |
| 2066 | + "Password reset token created but EMAIL_* is not configured; " |
| 2067 | + "user %s will not receive mail.", |
| 2068 | + validated_data.email, |
| 2069 | + ) |
2023 | 2070 |
|
2024 | 2071 | return success_response(data=response_data) |
2025 | 2072 | except Exception as e: |
|
0 commit comments