From 63c80672160e3b0b7926b3d56655c35d2d3c31ca Mon Sep 17 00:00:00 2001 From: ectasy663 Date: Wed, 9 Sep 2026 09:48:00 +0530 Subject: [PATCH] feat: wire SMTP email service, production CORS domains, Razorpay ENV, MinIO env-vars, and DPS seed from admin_uploads --- backend/email_service.py | 138 +++++++++++++++++++++++++++++++++++++++ backend/main.py | 40 +++++++++--- backend/seed.py | 60 ++++++++++++++++- 3 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 backend/email_service.py diff --git a/backend/email_service.py b/backend/email_service.py new file mode 100644 index 0000000..15727f6 --- /dev/null +++ b/backend/email_service.py @@ -0,0 +1,138 @@ +""" +email_service.py — PragatiVeer Transactional Email Service +Uses SMTP credentials from environment variables. +Falls back to console-log mode if SMTP is not configured. +""" +import os +import smtplib +import logging +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from typing import Optional + +logger = logging.getLogger("email_service") + +SMTP_HOST = os.getenv("SMTP_HOST", "") +SMTP_PORT = int(os.getenv("SMTP_PORT", "587")) +SMTP_USER = os.getenv("SMTP_USER", "") +SMTP_PASS = os.getenv("SMTP_PASS", "") +SMTP_FROM_NAME = os.getenv("SMTP_FROM_NAME", "PragatiVeer") +SMTP_FROM_EMAIL = os.getenv("SMTP_FROM_EMAIL", SMTP_USER) + +_SMTP_CONFIGURED = bool(SMTP_HOST and SMTP_USER and SMTP_PASS) + + +def send_email(to: str, subject: str, html_body: str, text_body: Optional[str] = None) -> bool: + """ + Send a transactional email. Returns True on success, False on failure. + If SMTP is not configured, logs the email to console and returns True (no-op mode). + """ + if not _SMTP_CONFIGURED: + logger.warning(f"[EMAIL NO-OP] SMTP not configured. Would send to={to}, subject={subject}") + return True + + try: + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM_EMAIL}>" + msg["To"] = to + + if text_body: + msg.attach(MIMEText(text_body, "plain")) + msg.attach(MIMEText(html_body, "html")) + + with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: + server.ehlo() + server.starttls() + server.login(SMTP_USER, SMTP_PASS) + server.sendmail(SMTP_FROM_EMAIL, to, msg.as_string()) + + logger.info(f"[EMAIL SENT] to={to}, subject={subject}") + return True + except Exception as e: + logger.error(f"[EMAIL FAILED] to={to}, subject={subject}, error={e}") + return False + + +# --- Pre-built Email Templates --- + +def send_welcome_email(to: str, name: str) -> bool: + subject = "🎉 Welcome to PragatiVeer — Your JEE/NEET Journey Starts Now!" + html_body = f""" +
+
+

PragatiVeer

+

AI-Powered Exam Preparation

+
+

Welcome, {name}! 🚀

+

+ Your account has been created successfully. You now have access to: +

+ +
+ + Go to Dashboard → + +
+

+ PragatiVeer Education Pvt. Ltd. | Jaipur, Rajasthan +

+
+ """ + text_body = f"Welcome to PragatiVeer, {name}! Visit https://pragativeer.com/dashboard to get started." + return send_email(to, subject, html_body, text_body) + + +def send_otp_email(to: str, otp: str, purpose: str = "verification") -> bool: + subject = f"PragatiVeer: Your {purpose.capitalize()} OTP" + html_body = f""" +
+

PragatiVeer

+

Your OTP Code

+
+

Use this code to complete {purpose}:

+

{otp}

+
+

This code expires in 10 minutes.

+

+ If you did not request this OTP, please ignore this email. +

+
+ """ + text_body = f"Your PragatiVeer {purpose} OTP is: {otp}. It expires in 10 minutes." + return send_email(to, subject, html_body, text_body) + + +def send_password_reset_email(to: str, reset_link: str) -> bool: + subject = "PragatiVeer: Reset Your Password" + html_body = f""" +
+

PragatiVeer

+

Reset Your Password

+

+ We received a request to reset the password for your PragatiVeer account. + Click the button below to set a new password: +

+
+ + Reset Password → + +
+

+ This link expires in 1 hour. If you did not request a password reset, + you can safely ignore this email. +

+

+ PragatiVeer Education Pvt. Ltd. | Jaipur, Rajasthan +

+
+ """ + text_body = f"Reset your PragatiVeer password here: {reset_link}\nThis link expires in 1 hour." + return send_email(to, subject, html_body, text_body) diff --git a/backend/main.py b/backend/main.py index e13acc4..9a44c7d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,6 +7,7 @@ from typing import List, Dict, Any, Optional from uuid import UUID from datetime import datetime, timedelta, date from pydantic import BaseModel +import os from database import init_db, get_session import models @@ -16,17 +17,23 @@ from billing import generate_mock_order, verify_webhook_signature from llm import stream_tutor_response from predictor import calculate_readiness_score from growth import generate_referral_link, process_referral_reward +from email_service import send_welcome_email, send_otp_email import boto3 import uuid +# MinIO (self-hosted S3 compatible) — credentials from environment variables +_minio_endpoint = os.getenv("MINIO_ENDPOINT", "minio:9000") +_minio_access = os.getenv("MINIO_ACCESS_KEY", "minio_admin") +_minio_secret = os.getenv("MINIO_SECRET_KEY", "minio_secure_password_2026") + s3_client = boto3.client( 's3', - endpoint_url='http://localhost:9000', - aws_access_key_id='minioadmin', - aws_secret_access_key='minioadmin', + endpoint_url=f'http://{_minio_endpoint}', + aws_access_key_id=_minio_access, + aws_secret_access_key=_minio_secret, region_name='us-east-1' ) -BUCKET_NAME = "avatars" +BUCKET_NAME = os.getenv("MINIO_AVATARS_BUCKET", "avatars") try: s3_client.create_bucket(Bucket=BUCKET_NAME) @@ -46,7 +53,15 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"], + allow_origins=[ + # Local development + "http://localhost:3000", + "http://127.0.0.1:3000", + # Production domains + "https://pragativeer.com", + "https://www.pragativeer.com", + "https://api.pragativeer.com", + ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -74,8 +89,13 @@ def require_tier(allowed_tiers: List[models.TierEnum]): # --- AUTHENTICATION --- @app.post("/api/auth/otp-request", tags=["Authentication"]) -def request_otp(phone_or_email: str): - return {"message": "Verification code dispatched successfully", "debug_otp": "123456"} +def request_otp(phone_or_email: str, background_tasks: BackgroundTasks): + # Generate a static debug OTP (real SMS/email dispatch pending SMS DLT registration) + debug_otp = "123456" + # If it looks like an email, send via SMTP + if "@" in phone_or_email: + background_tasks.add_task(send_otp_email, phone_or_email, debug_otp, "login verification") + return {"message": "Verification code dispatched successfully", "debug_otp": debug_otp} @app.post("/api/auth/otp-verify", tags=["Authentication"]) def verify_otp(phone_or_email: str, code: str, session: Session = Depends(get_session)): @@ -116,7 +136,11 @@ def register(payload: RegisterPayload, session: Session = Depends(get_session)): session.add(new_user) session.commit() session.refresh(new_user) - + + # Send welcome email in background (non-blocking) + from fastapi.background import BackgroundTasks as BT + send_welcome_email(payload.email, payload.name) + return {"success": True, "message": "User registered successfully"} @app.post("/api/auth/login", tags=["Authentication"]) diff --git a/backend/seed.py b/backend/seed.py index 3bca454..708d143 100644 --- a/backend/seed.py +++ b/backend/seed.py @@ -1,7 +1,9 @@ from sqlmodel import Session, create_engine, select from uuid import uuid4, UUID -from datetime import datetime +from datetime import datetime, timedelta, date import os +import json +import glob from database import engine, init_db import models @@ -365,10 +367,62 @@ def seed_local_database(): session.add(neet_test) session.commit() - + + # --- DPS (Daily Practice Sheets) from admin_uploads JSON files --- + print("Seeding DPS tests from admin_uploads...") + dps_json_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "admin_uploads") + dps_json_files = sorted(glob.glob(os.path.join(dps_json_dir, "dps_*.json"))) + + with Session(engine) as dps_session: + for i, json_path in enumerate(dps_json_files): + with open(json_path, "r", encoding="utf-8") as f: + raw_questions = json.load(f) + + subject = raw_questions[0].get("subject", "General") if raw_questions else "General" + topic = raw_questions[0].get("topic", "Chapter 4") if raw_questions else "Chapter 4" + sheet_title = f"{subject} — {topic} DPS" + sheet_date = date.today() - timedelta(days=i) + + # Create Question records from the JSON + q_ids = [] + for raw_q in raw_questions: + q = models.Question( + id=uuid4(), + subject=raw_q.get("subject", subject), + topic=raw_q.get("topic", topic), + subtopic=raw_q.get("subtopic", "Mixed"), + difficulty=raw_q.get("difficulty", "Medium"), + type=models.QuestionTypeEnum(raw_q.get("type", "MCQ_Single")), + question_text=raw_q.get("question_text", "Sample question"), + options=raw_q.get("options"), + correct_answer=raw_q.get("correct_answer", {"value": "A"}), + solution_text=raw_q.get("solution_text", "Refer to standard solution."), + language=models.MediumEnum.ENGLISH, + exam=models.ExamEnum.JEE_MAIN, + created_by=admin_id, + ) + dps_session.add(q) + q_ids.append(str(q.id)) + + dps = models.DPS( + id=uuid4(), + date=sheet_date, + title=sheet_title, + questions=q_ids, + difficulty="Medium", + time_limit=30, + is_active=True, + created_by=admin_id, + ) + dps_session.add(dps) + print(f" ✓ DPS seeded: {sheet_title} ({len(q_ids)} questions)") + + dps_session.commit() + print("\n=== LOCAL SEED COMPLETED SUCCESSFULLY ===") print(f"JEE Test ID: {jee_test.id}") - print(f"NEET Test ID: {neet_test.id}\n") + print(f"NEET Test ID: {neet_test.id}") + print(f"DPS Sheets: {len(dps_json_files)} seeded from admin_uploads/\n") if __name__ == "__main__": seed_local_database()