Spaces:
Sleeping
Sleeping
| import pymssql | |
| import random | |
| import uuid | |
| import requests | |
| from fastapi import FastAPI, HTTPException, Header | |
| from pydantic import BaseModel | |
| from datetime import datetime, timedelta | |
| from typing import Optional | |
| import os | |
| import firebase_admin | |
| from firebase_admin import credentials, messaging | |
| import json | |
| # 🔥 Firebase Setup | |
| firebase_key = json.loads(os.environ["FIREBASE_KEY"]) | |
| cred = credentials.Certificate(firebase_key) | |
| firebase_admin.initialize_app(cred) | |
| # ===================================================== | |
| # CONFIG | |
| # ===================================================== | |
| # --- Brevo Email Config --- | |
| BREVO_API_KEY = os.getenv("KEY") | |
| BREVO_SENDER_EMAIL = "patelkanostudent@gmail.com" | |
| BREVO_SENDER_NAME = "College ERP System" | |
| BREVO_API_URL = "https://api.brevo.com/v3/smtp/email" | |
| # --- Database Config --- | |
| DB_SERVER = "kano2026.mssql.somee.com:1433" # ✅ fixed | |
| DB_USER = "Dhvanit_SQLLogin_1" | |
| DB_PASSWORD = os.getenv("PASS") | |
| DB_NAME = "kano2026" | |
| # ===================================================== | |
| # ===================================================== | |
| # FASTAPI APP | |
| # ===================================================== | |
| app = FastAPI(title="College ERP Backend") | |
| # ===================================================== | |
| # DATABASE CONNECTION | |
| # ===================================================== | |
| def get_connection(): | |
| return pymssql.connect( | |
| server=DB_SERVER, | |
| user=DB_USER, | |
| password=DB_PASSWORD, | |
| database=DB_NAME | |
| ) | |
| # ===================================================== | |
| # BREVO EMAIL — CORE FUNCTION | |
| # ===================================================== | |
| def send_email_brevo(to_email: str, subject: str, body: str) -> bool: | |
| """ | |
| Send email using Brevo transactional API. | |
| Returns True on success, False on failure. | |
| """ | |
| headers = { | |
| "accept": "application/json", | |
| "content-type": "application/json", | |
| "api-key": BREVO_API_KEY | |
| } | |
| payload = { | |
| "sender": { | |
| "name": BREVO_SENDER_NAME, | |
| "email": BREVO_SENDER_EMAIL | |
| }, | |
| "to": [ | |
| { | |
| "email": to_email, | |
| "name": to_email | |
| } | |
| ], | |
| "subject": subject, | |
| "textContent": body | |
| } | |
| try: | |
| response = requests.post( | |
| BREVO_API_URL, | |
| json=payload, | |
| headers=headers, | |
| timeout=30 | |
| ) | |
| print(f"Brevo Status Code: {response.status_code}") | |
| print(f"Brevo Response: {response.text}") | |
| if response.status_code in (200, 201): | |
| print(f"Email sent successfully to: {to_email}") | |
| return True | |
| else: | |
| print(f"Brevo FAILED: {response.status_code} - {response.text}") | |
| return False | |
| except requests.exceptions.Timeout: | |
| print("Brevo Error: Request timed out") | |
| return False | |
| except requests.exceptions.ConnectionError: | |
| print("Brevo Error: Cannot connect to Brevo API") | |
| return False | |
| except Exception as e: | |
| print(f"Brevo Error: {e}") | |
| return False | |
| # ===================================================== | |
| # OTP EMAIL | |
| # ===================================================== | |
| def generate_otp() -> str: | |
| return str(random.randint(100000, 999999)) | |
| def send_otp_email(email: str, otp: str) -> bool: | |
| subject = "College Leave Management System — Login OTP" | |
| body = f"Your OTP is {otp}. Valid for 5 minutes." | |
| return send_email_brevo(email, subject, body) | |
| # ===================================================== | |
| # PARENT NOTIFICATION EMAIL | |
| # ===================================================== | |
| def send_parent_email( | |
| parent_email: str, | |
| student_name: str, | |
| from_date: str, | |
| to_date: str, | |
| reason: str, | |
| status: str | |
| ) -> bool: | |
| subject = f"Leave {status} Notification" | |
| body = ( | |
| f"Dear Parent,\n\n" | |
| f"Your ward {student_name} applied for leave.\n\n" | |
| f"From: {from_date}\n" | |
| f"To: {to_date}\n" | |
| f"Reason: {reason}\n\n" | |
| f"Status: {status}\n\n" | |
| f"Thank you.\n" | |
| f"College ERP System" | |
| ) | |
| return send_email_brevo(parent_email, subject, body) | |
| # ===================================================== | |
| # PYDANTIC MODELS | |
| # ===================================================== | |
| class StudentRegister(BaseModel): | |
| fullname: str | |
| roll_no: int | |
| registration_no: str | |
| semester: int | |
| student_email: str | |
| parent_email: str | |
| class SendOTP(BaseModel): | |
| email: str | |
| class OTPVerify(BaseModel): | |
| email: str | |
| otp: str | |
| class LeaveApply(BaseModel): | |
| student_id: int | |
| from_date: str | |
| to_date: str | |
| reason: str | |
| class EmergencyLeave(BaseModel): | |
| student_id: int | |
| from_date: str | |
| to_date: str | |
| reason: str | |
| class Action(BaseModel): | |
| leave_id: int | |
| action: str | |
| # ===================================================== | |
| # TOKEN VALIDATION | |
| # ===================================================== | |
| def get_user_from_token(authorization: str) -> int: | |
| if not authorization: | |
| raise HTTPException( | |
| status_code=401, | |
| detail="Authorization header missing" | |
| ) | |
| token = authorization.replace("Bearer ", "") | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "SELECT UserId FROM LoginSessions " | |
| "WHERE Token=%s AND IsActive=1", | |
| (token,) | |
| ) | |
| row = cursor.fetchone() | |
| if not row: | |
| raise HTTPException( | |
| status_code=401, | |
| detail="Invalid session" | |
| ) | |
| return row[0] | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # TEST BREVO EMAIL ENDPOINT | |
| # ===================================================== | |
| def test_email(to_email: str): | |
| """ | |
| Test endpoint to check if Brevo email works. | |
| Usage: GET /test/email/someone@gmail.com | |
| """ | |
| success = send_email_brevo( | |
| to_email=to_email, | |
| subject="Test Email from College ERP", | |
| body="If you received this, Brevo is working correctly!" | |
| ) | |
| if success: | |
| return { | |
| "status": "success", | |
| "message": f"Test email sent to {to_email}" | |
| } | |
| else: | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Brevo email failed. Check console logs." | |
| ) | |
| # ===================================================== | |
| # DASHBOARD | |
| # ===================================================== | |
| def dashboard(): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "SELECT COUNT(*) FROM Users " | |
| "WHERE Role='STUDENT' AND IsActive=1" | |
| ) | |
| count = cursor.fetchone()[0] | |
| return {"total_students": count} | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # STUDENT REGISTER | |
| # ===================================================== | |
| def student_register(data: StudentRegister): | |
| nms = 0 | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| # Check duplicate email | |
| cursor.execute( | |
| "SELECT 1 FROM Users WHERE Email=%s", | |
| (data.student_email,) | |
| ) | |
| if cursor.fetchone(): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Email already registered" | |
| ) | |
| # Check duplicate registration | |
| cursor.execute( | |
| "SELECT 1 FROM StudentProfile WHERE RegistrationNo=%s", | |
| (data.registration_no,) | |
| ) | |
| if cursor.fetchone(): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Student already registered" | |
| ) | |
| # Insert user | |
| cursor.execute( | |
| "INSERT INTO Users (FullName, Email, Role) " | |
| "VALUES (%s, %s, 'STUDENT')", | |
| (data.fullname, data.student_email) | |
| ) | |
| # Get new user ID | |
| cursor.execute("SELECT SCOPE_IDENTITY()") | |
| student_id = cursor.fetchone()[0] | |
| nms = nms + 1 | |
| # Insert student profile | |
| cursor.execute( | |
| """ | |
| INSERT INTO StudentProfile | |
| (StudentId, RollNo, RegistrationNo, | |
| Semester, StudentEmail, ParentEmail) | |
| VALUES (%s, %s, %s, %s, %s, %s) | |
| """, | |
| ( | |
| student_id, | |
| data.roll_no, | |
| data.registration_no, | |
| data.semester, | |
| data.student_email, | |
| data.parent_email | |
| ) | |
| ) | |
| # Find available professor | |
| cursor.execute( | |
| """ | |
| SELECT p.ProfessorId | |
| FROM ProfessorProfile p | |
| LEFT JOIN StudentProfessorMapping sp | |
| ON p.ProfessorId = sp.ProfessorId | |
| AND sp.Semester = %s | |
| GROUP BY p.ProfessorId | |
| HAVING COUNT(sp.StudentId) < 7 | |
| ORDER BY COUNT(sp.StudentId) ASC | |
| """, | |
| (data.semester,) | |
| ) | |
| prof = cursor.fetchone() | |
| if not prof: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="No professor available" | |
| ) | |
| # Map student to professor | |
| cursor.execute( | |
| """ | |
| INSERT INTO StudentProfessorMapping | |
| (StudentId, ProfessorId, Semester) | |
| VALUES (%s, %s, %s) | |
| """, | |
| (student_id, prof[0], data.semester) | |
| ) | |
| conn.commit() | |
| return { | |
| "message": "Student registered", | |
| "professor": prof[0], | |
| "number_of_student": nms | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| conn.rollback() | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # AUTH — SEND OTP (via Brevo) | |
| # ===================================================== | |
| def send_otp(data: SendOTP): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| # Check user exists | |
| cursor.execute( | |
| "SELECT UserId FROM Users " | |
| "WHERE Email=%s AND IsActive=1", | |
| (data.email,) | |
| ) | |
| user = cursor.fetchone() | |
| if not user: | |
| raise HTTPException( | |
| status_code=404, | |
| detail="User not found" | |
| ) | |
| # Generate OTP | |
| otp = generate_otp() | |
| expiry = datetime.now() + timedelta(minutes=5) | |
| # Save OTP to database | |
| cursor.execute( | |
| "INSERT INTO EmailOTP (Email, OTPCode, ExpiryTime) " | |
| "VALUES (%s, %s, %s)", | |
| (data.email, otp, expiry) | |
| ) | |
| conn.commit() | |
| # Send OTP via Brevo | |
| email_sent = send_otp_email(data.email, otp) | |
| if email_sent: | |
| return {"message": "OTP sent successfully via Brevo"} | |
| else: | |
| return { | |
| "message": "OTP saved in database but email failed", | |
| "otp_for_debug": otp, | |
| "warning": "Check Brevo API key and sender verification" | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| conn.rollback() | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # AUTH — VERIFY OTP | |
| # ===================================================== | |
| def verify_otp(data: OTPVerify): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| # Validate OTP | |
| cursor.execute( | |
| """ | |
| SELECT OTPId FROM EmailOTP | |
| WHERE Email=%s AND OTPCode=%s | |
| AND IsUsed=0 AND ExpiryTime>=GETDATE() | |
| """, | |
| (data.email, data.otp) | |
| ) | |
| otp_row = cursor.fetchone() | |
| if not otp_row: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Invalid OTP" | |
| ) | |
| # Mark OTP used | |
| cursor.execute( | |
| "UPDATE EmailOTP SET IsUsed=1 WHERE OTPId=%s", | |
| (otp_row[0],) | |
| ) | |
| # Get user | |
| cursor.execute( | |
| "SELECT UserId, Role FROM Users " | |
| "WHERE Email=%s AND IsActive=1", | |
| (data.email,) | |
| ) | |
| user = cursor.fetchone() | |
| if not user: | |
| raise HTTPException( | |
| status_code=404, | |
| detail="User not found" | |
| ) | |
| # Create session | |
| token = str(uuid.uuid4()) | |
| cursor.execute( | |
| "UPDATE LoginSessions SET IsActive=0 " | |
| "WHERE UserId=%s", | |
| (user[0],) | |
| ) | |
| cursor.execute( | |
| "INSERT INTO LoginSessions (UserId, Token) " | |
| "VALUES (%s, %s)", | |
| (user[0], token) | |
| ) | |
| conn.commit() | |
| return { | |
| "login": "success", | |
| "token": token, | |
| "user_id": user[0], | |
| "role": user[1] | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| conn.rollback() | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # APPLY LEAVE | |
| # ===================================================== | |
| def apply_leave( | |
| data: LeaveApply, | |
| authorization: str = Header(None) | |
| ): | |
| data.student_id = get_user_from_token(authorization) | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "SELECT ProfessorId FROM StudentProfessorMapping WHERE StudentId=%s", | |
| (data.student_id,) | |
| ) | |
| prof = cursor.fetchone() | |
| cursor.execute("SELECT DeanId FROM DeanProfile") | |
| dean = cursor.fetchone() | |
| if not prof or not dean: | |
| raise HTTPException(status_code=400, detail="Config error") | |
| # INSERT | |
| cursor.execute( | |
| """ | |
| INSERT INTO LeaveApplications | |
| (StudentId, ProfessorId, DeanId, | |
| ProfessorStatus, DeanStatus, | |
| FromDate, ToDate, Reason) | |
| VALUES (%s,%s,%s,'PENDING','PENDING',%s,%s,%s) | |
| """, | |
| ( | |
| data.student_id, | |
| prof[0], | |
| dean[0], | |
| data.from_date, | |
| data.to_date, | |
| data.reason | |
| ) | |
| ) | |
| # Inside @app.post("/leave/apply") | |
| cursor.execute( | |
| "SELECT p.FcmToken FROM Users p " | |
| "JOIN StudentProfessorMapping sp ON p.UserId = sp.ProfessorId " | |
| "WHERE sp.StudentId = %s", | |
| (data.student_id,) | |
| ) | |
| prof_token = cursor.fetchone() | |
| if prof_token and prof_token[0]: | |
| send_fcm(prof_token[0], "New Leave Request", f"Student {data.student_id} has applied for leave.") | |
| except Exception as e: | |
| conn.rollback() | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| conn.close()# ===================================================== | |
| # STUDENT LEAVE VIEWS | |
| # ===================================================== | |
| def student_rejected(student_id: int): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT LeaveId, FromDate, ToDate, Reason | |
| FROM LeaveApplications | |
| WHERE StudentId=%s | |
| AND FinalStatus LIKE 'REJECTED%%' | |
| """, | |
| (student_id,) | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| def student_approved(student_id: int): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT LeaveId, FromDate, ToDate, Reason | |
| FROM LeaveApplications | |
| WHERE StudentId=%s | |
| AND FinalStatus='FINAL_APPROVED' | |
| """, | |
| (student_id,) | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| # ✅ Added {student_id} to match Flutter | |
| def student_leaves(student_id: int): # ✅ Accepting ID as a parameter | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| # Fetching TOP 5 to keep the dashboard clean | |
| cursor.execute( | |
| """ | |
| SELECT TOP 5 | |
| LeaveId, ProfessorStatus, DeanStatus, FinalStatus, FromDate, ToDate, Reason | |
| FROM LeaveApplications | |
| WHERE StudentId=%s | |
| ORDER BY LeaveId DESC | |
| """, | |
| (student_id,) | |
| ) | |
| rows = cursor.fetchall() | |
| results = [] | |
| for r in rows: | |
| # --- STATUS MINING LOGIC --- | |
| # This tells the student exactly who needs to take action next | |
| if r[3] == 'PENDING' and r[1] == 'PENDING': | |
| mining_status = "Waiting for Professor" | |
| elif r[1] == 'APPROVED' and r[2] == 'PENDING': | |
| mining_status = "Waiting for Dean" | |
| elif r[3] == 'FINAL_APPROVED': | |
| mining_status = "Approved ✅" | |
| elif 'REJECTED' in r[3]: | |
| mining_status = "Rejected ❌" | |
| else: | |
| mining_status = r[3] # Fallback to FinalStatus | |
| results.append({ | |
| "leave_id": r[0], | |
| "professor_status": r[1], | |
| "dean_status": r[2], | |
| "final_status": r[3], | |
| "display_status": mining_status, # ✅ NEW: Send this to Flutter | |
| "from_date": str(r[4]), | |
| "to_date": str(r[5]), | |
| "reason": r[6] | |
| }) | |
| return results | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # EMERGENCY LEAVE | |
| # ===================================================== | |
| def emergency_leave( | |
| data: EmergencyLeave, | |
| authorization: str = Header(None) | |
| ): | |
| data.student_id= get_user_from_token(authorization) | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "SELECT ProfessorId FROM StudentProfessorMapping " | |
| "WHERE StudentId=%s", | |
| (data.student_id,) | |
| ) | |
| prof = cursor.fetchone() | |
| cursor.execute("SELECT DeanId FROM DeanProfile") | |
| dean = cursor.fetchone() | |
| if not prof or not dean: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Configuration error" | |
| ) | |
| cursor.execute( | |
| """ | |
| INSERT INTO LeaveApplications | |
| (StudentId, ProfessorId, DeanId, | |
| ProfessorStatus, DeanStatus, | |
| FromDate, ToDate, Reason) | |
| VALUES (%s, %s, %s, 'SKIPPED', 'PENDING', %s, %s, %s) | |
| """, | |
| ( | |
| data.student_id, | |
| prof[0], | |
| dean[0], | |
| data.from_date, | |
| data.to_date, | |
| data.reason | |
| ) | |
| ) | |
| # Inside @app.post("/leave/emergency") | |
| cursor.execute("SELECT FcmToken FROM Users WHERE Role='DEAN'") | |
| dean_token = cursor.fetchone() | |
| if dean_token and dean_token[0]: | |
| send_fcm(dean_token[0], "🚨 EMERGENCY LEAVE", f"Student {data.student_id} requested emergency leave.") | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| conn.rollback() | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # DEAN APIs | |
| # ===================================================== | |
| def dean_pending(): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT l.LeaveId, l.StudentId, u.FullName, | |
| s.Semester, l.FromDate, l.ToDate, l.Reason | |
| FROM LeaveApplications l | |
| JOIN Users u ON l.StudentId = u.UserId | |
| JOIN StudentProfile s ON s.StudentId = l.StudentId | |
| WHERE l.DeanStatus='PENDING' | |
| """ | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| def dean_approved(): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT l.LeaveId, l.StudentId, u.FullName, | |
| s.Semester, l.FromDate, l.ToDate, l.Reason | |
| FROM LeaveApplications l | |
| JOIN Users u ON l.StudentId = u.UserId | |
| JOIN StudentProfile s ON s.StudentId = l.StudentId | |
| WHERE l.DeanStatus='APPROVED' | |
| """ | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| def dean_emergency(): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT l.LeaveId, l.StudentId, u.FullName, | |
| s.Semester, l.FromDate, l.ToDate, l.Reason | |
| FROM LeaveApplications l | |
| JOIN Users u ON l.StudentId = u.UserId | |
| JOIN StudentProfile s ON s.StudentId = l.StudentId | |
| WHERE l.ProfessorStatus='SKIPPED' | |
| AND l.DeanStatus='PENDING' | |
| """ | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| def dean_students(): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT u.UserId, u.FullName, s.RollNo, | |
| s.RegistrationNo, s.Semester, | |
| s.StudentEmail, s.ParentEmail | |
| FROM Users u | |
| JOIN StudentProfile s ON u.UserId = s.StudentId | |
| WHERE u.Role='STUDENT' AND u.IsActive=1 | |
| """ | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| def dean_action(data: Action): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| status = "APPROVED" if data.action == "APPROVED" else "REJECTED" | |
| final_status = ( | |
| "FINAL_APPROVED" if status == "APPROVED" | |
| else "REJECTED_BY_DEAN" | |
| ) | |
| cursor.execute( | |
| """ | |
| UPDATE LeaveApplications | |
| SET DeanStatus=%s, FinalStatus=%s | |
| WHERE LeaveId=%s | |
| """, | |
| (status, final_status, data.leave_id) | |
| ) | |
| # Get student + parent info | |
| cursor.execute( | |
| """ | |
| SELECT u.FullName, s.ParentEmail, | |
| l.FromDate, l.ToDate, l.Reason | |
| FROM LeaveApplications l | |
| JOIN Users u ON l.StudentId = u.UserId | |
| JOIN StudentProfile s ON s.StudentId = l.StudentId | |
| WHERE l.LeaveId=%s | |
| """, | |
| (data.leave_id,) | |
| ) | |
| row = cursor.fetchone() | |
| # Send parent email via Brevo | |
| email_sent = False | |
| if row: | |
| try: | |
| email_sent = send_parent_email( | |
| parent_email=row[1], | |
| student_name=row[0], | |
| from_date=str(row[2]), | |
| to_date=str(row[3]), | |
| reason=row[4], | |
| status=status | |
| ) | |
| except Exception as mail_err: | |
| print(f"Parent Email Error: {mail_err}") | |
| # ✅ GET STUDENT TOKEN | |
| cursor.execute( | |
| "SELECT u.FcmToken FROM LeaveApplications l " | |
| "JOIN Users u ON l.StudentId=u.UserId " | |
| "WHERE l.LeaveId=%s", | |
| (data.leave_id,) | |
| ) | |
| student = cursor.fetchone() | |
| conn.commit() | |
| # ✅ SEND FCM | |
| if student and student[0]: | |
| send_fcm( | |
| student[0], | |
| "Leave Status", | |
| f"Your leave is {status}" | |
| ) | |
| # Inside @app.post("/leave/dean-action") | |
| cursor.execute( | |
| "SELECT FcmToken FROM Users WHERE UserId = " | |
| "(SELECT StudentId FROM LeaveApplications WHERE LeaveId = %s)", | |
| (data.leave_id,) | |
| ) | |
| student_token = cursor.fetchone() | |
| if student_token and student_token[0]: | |
| status_msg = "Approved" if data.action == "APPROVED" else "Rejected" | |
| send_fcm(student_token[0], "Leave Decision", f"The Dean has {status_msg} your leave request.") | |
| return { | |
| "message": f"Dean action done ({status})", | |
| "parent_email_sent": email_sent | |
| } | |
| except Exception as e: | |
| conn.rollback() | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| conn.close() | |
| def semester_wise(): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT s.Semester, | |
| u.FullName AS StudentName, | |
| s.RollNo, | |
| pUser.FullName AS ProfessorName | |
| FROM StudentProfessorMapping sp | |
| JOIN Users u ON sp.StudentId = u.UserId | |
| JOIN StudentProfile s ON s.StudentId = u.UserId | |
| JOIN Users pUser ON pUser.UserId = sp.ProfessorId | |
| ORDER BY s.Semester, s.RollNo | |
| """ | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # PROFESSOR APIs | |
| # ===================================================== | |
| def professor_pending(professor_id: int): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT l.LeaveId, l.StudentId, u.FullName, | |
| s.Semester, l.FromDate, l.ToDate, l.Reason | |
| FROM LeaveApplications l | |
| JOIN Users u ON l.StudentId = u.UserId | |
| JOIN StudentProfile s ON s.StudentId = l.StudentId | |
| WHERE l.ProfessorId=%s | |
| AND l.ProfessorStatus='PENDING' | |
| """, | |
| (professor_id,) | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| def professor_approved(professor_id: int): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT l.LeaveId, u.FullName, s.Semester, | |
| l.FromDate, l.ToDate | |
| FROM LeaveApplications l | |
| JOIN Users u ON l.StudentId = u.UserId | |
| JOIN StudentProfile s ON s.StudentId = l.StudentId | |
| WHERE l.ProfessorId=%s | |
| AND l.ProfessorStatus='APPROVED' | |
| """, | |
| (professor_id,) | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| def professor_rejected(professor_id: int): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT l.LeaveId, u.FullName, s.Semester, | |
| l.FromDate, l.ToDate | |
| FROM LeaveApplications l | |
| JOIN Users u ON l.StudentId = u.UserId | |
| JOIN StudentProfile s ON s.StudentId = l.StudentId | |
| WHERE l.ProfessorId=%s | |
| AND l.ProfessorStatus='REJECTED' | |
| """, | |
| (professor_id,) | |
| ) | |
| rows = cursor.fetchall() | |
| return [list(r) for r in rows] | |
| finally: | |
| conn.close() | |
| def professor_students(professor_id: int): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| """ | |
| SELECT sp.Semester, | |
| COUNT(*) AS total_students | |
| FROM StudentProfessorMapping sp | |
| WHERE sp.ProfessorId=%s | |
| GROUP BY sp.Semester | |
| """, | |
| (professor_id,) | |
| ) | |
| rows = cursor.fetchall() | |
| return [ | |
| { | |
| "semester": r[0], | |
| "total_students": r[1] | |
| } | |
| for r in rows | |
| ] | |
| finally: | |
| conn.close() | |
| def professor_action(data: Action): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| status = "APPROVED" if data.action == "APPROVED" else "REJECTED" | |
| final_status = ( | |
| "FORWARDED_TO_DEAN" if status == "APPROVED" | |
| else "REJECTED_BY_PROFESSOR" | |
| ) | |
| cursor.execute( | |
| """ | |
| UPDATE LeaveApplications | |
| SET ProfessorStatus=%s, FinalStatus=%s | |
| WHERE LeaveId=%s | |
| """, | |
| (status, final_status, data.leave_id) | |
| ) | |
| # Inside @app.post("/leave/professor-action") | |
| if data.action == "APPROVED": | |
| # Get the Dean's token (assuming there is one Dean or a specific DeanId) | |
| cursor.execute("SELECT FcmToken FROM Users WHERE Role='DEAN'") | |
| dean_token = cursor.fetchone() | |
| if dean_token and dean_token[0]: | |
| send_fcm(dean_token[0], "Leave Forwarded", f"Professor approved Leave ID {data.leave_id}. Action required.") | |
| except Exception as e: | |
| conn.rollback() | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| conn.close() | |
| # ===================================================== | |
| # USER PROFILE | |
| # ===================================================== | |
| def get_profile(authorization: str = Header(None)): | |
| user_id = get_user_from_token(authorization) | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "SELECT FullName, Email, Role " | |
| "FROM Users WHERE UserId=%s", | |
| (user_id,) | |
| ) | |
| user = cursor.fetchone() | |
| if not user: | |
| raise HTTPException( | |
| status_code=404, | |
| detail="User not found" | |
| ) | |
| role = user[2] | |
| profile_data = { | |
| "user_id": user_id, | |
| "name": user[0], | |
| "email": user[1], | |
| "role": role | |
| } | |
| # STUDENT | |
| if role == "STUDENT": | |
| cursor.execute( | |
| """ | |
| SELECT RollNo, RegistrationNo, Semester, | |
| StudentEmail, ParentEmail | |
| FROM StudentProfile | |
| WHERE StudentId=%s | |
| """, | |
| (user_id,) | |
| ) | |
| data = cursor.fetchone() | |
| if data: | |
| profile_data.update({ | |
| "roll_no": data[0], | |
| "registration_no": data[1], | |
| "semester": data[2], | |
| "student_email": data[3], | |
| "parent_email": data[4], | |
| }) | |
| # PROFESSOR | |
| elif role == "PROFESSOR": | |
| cursor.execute( | |
| """ | |
| SELECT ProfessorCode, Email | |
| FROM ProfessorProfile | |
| WHERE ProfessorId=%s | |
| """, | |
| (user_id,) | |
| ) | |
| data = cursor.fetchone() | |
| if data: | |
| profile_data.update({ | |
| "professor_code": data[0], | |
| "email": data[1], | |
| }) | |
| # DEAN | |
| elif role == "DEAN": | |
| cursor.execute( | |
| """ | |
| SELECT DeanCode, Email | |
| FROM DeanProfile | |
| WHERE DeanId=%s | |
| """, | |
| (user_id,) | |
| ) | |
| data = cursor.fetchone() | |
| if data: | |
| profile_data.update({ | |
| "dean_code": data[0], | |
| "email": data[1], | |
| }) | |
| return profile_data | |
| finally: | |
| conn.close() | |
| #========================================================== | |
| # =============================== | |
| # ADD PROFESSOR (DEAN) | |
| # ========================================================== | |
| # ADD PROFESSOR (DEAN) | |
| # ========================================================== | |
| class ProfessorCreate(BaseModel): | |
| full_name: str | |
| email: str | |
| def add_professor(prof: ProfessorCreate): | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| # check email already exists | |
| cursor.execute( | |
| "SELECT UserId FROM Users WHERE Email=%s", | |
| (prof.email,) | |
| ) | |
| if cursor.fetchone(): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Email already exists" | |
| ) | |
| # insert into Users | |
| cursor.execute( | |
| """ | |
| INSERT INTO Users (FullName, Email, Role, IsActive, CreatedAt) | |
| VALUES (%s,%s,'PROFESSOR',1,GETDATE()) | |
| """, | |
| (prof.full_name, prof.email) | |
| ) | |
| # get new professor id | |
| cursor.execute("SELECT SCOPE_IDENTITY()") | |
| professor_id = cursor.fetchone()[0] | |
| # generate professor code | |
| professor_code = f"PROF{int(professor_id):04d}" | |
| # insert into ProfessorProfile | |
| cursor.execute( | |
| """ | |
| INSERT INTO ProfessorProfile | |
| (ProfessorId, ProfessorCode, Email) | |
| VALUES (%s,%s,%s) | |
| """, | |
| ( | |
| professor_id, | |
| professor_code, | |
| prof.email | |
| ) | |
| ) | |
| conn.commit() | |
| return { | |
| "message": "Professor added successfully", | |
| "professor_id": professor_id, | |
| "professor_code": professor_code | |
| } | |
| except Exception as e: | |
| conn.rollback() | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| conn.close() | |
| #========================================================== | |
| #============================fcm tocken============================== | |
| class SaveToken(BaseModel): | |
| user_id: int | |
| fcm_token: str | |
| def save_fcm_token( | |
| data: SaveToken, | |
| authorization: str = Header(None) # FastAPI looks for 'Authorization' header | |
| ): | |
| # This line triggers the 401 if 'authorization' is missing or token is invalid | |
| user_id_from_db = get_user_from_token(authorization) | |
| conn = get_connection() | |
| try: | |
| cursor = conn.cursor() | |
| # Use the ID from the token for better security | |
| cursor.execute( | |
| "UPDATE Users SET FcmToken=%s WHERE UserId=%s", | |
| (data.fcm_token, user_id_from_db) | |
| ) | |
| conn.commit() | |
| return {"message": "Token saved successfully"} | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| raise HTTPException(status_code=500, detail="Database error") | |
| finally: | |
| conn.close() | |
| #======================================================= | |
| def send_fcm(token, title, body): | |
| try: | |
| message = messaging.Message( | |
| notification=messaging.Notification( | |
| title=title, | |
| body=body, | |
| ), | |
| token=token, | |
| ) | |
| response = messaging.send(message) | |
| print(f"✅ FCM sent successfully: {response}") | |
| except Exception as e: | |
| # This is where your 'Permission denied' error is coming from | |
| print(f"❌ FCM FATAL ERROR: {str(e)}") | |
| # ===================================================== | |
| # RUN | |
| # ===================================================== | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("app:app", host="0.0.0.0", port=7860) |