knai/school-system
0
1from fastapi import FastAPI2from fastapi.middleware.cors import CORSMiddleware3from sqlalchemy import text4from sqlalchemy.orm import Session5from routes import (6 auth_router, students_router, fees_router,7 payments_router, attendance_router, marks_router,8 messages_router, classes_router, teachers_router,9 users_router, admins_router, settings_router, homework_router,10 schools_router11)12from config import settings13import random14import string15 16 17def _random_link_code() -> str:18 """7-char uppercase alphanumeric code (no 0/O/1/I to avoid confusion)."""19 chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'20 return ''.join(random.choices(chars, k=3)) + '-' + ''.join(random.choices(chars, k=3))21 22app = FastAPI(23 title="School Management System API",24 description="Complete school management backend with FastAPI",25 version="1.0.0",26 docs_url="/docs",27 redoc_url="/redoc",28)29 30# CORS - allow Next.js frontend31_allowed_origins = [settings.FRONTEND_URL, "http://localhost:3000", "http://127.0.0.1:3000"]32if settings.EXTRA_ALLOWED_ORIGINS:33 _allowed_origins += [o.strip() for o in settings.EXTRA_ALLOWED_ORIGINS.split(",") if o.strip()]34 35app.add_middleware(36 CORSMiddleware,37 allow_origins=_allowed_origins,38 allow_credentials=True,39 allow_methods=["*"],40 allow_headers=["*"],41)42 43# Register all routers44app.include_router(auth_router)45app.include_router(students_router)46app.include_router(fees_router)47app.include_router(payments_router)48app.include_router(attendance_router)49app.include_router(marks_router)50app.include_router(messages_router)51app.include_router(classes_router)52app.include_router(teachers_router)53app.include_router(users_router)54app.include_router(admins_router)55app.include_router(settings_router)56app.include_router(homework_router)57app.include_router(schools_router)58 59 60def _safe_add_column(conn, table: str, col_name: str, col_type: str):61 """Add column if it doesn't exist, handling both PG (IF NOT EXISTS) and fallback."""62 try:63 conn.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col_name} {col_type}"))64 conn.commit()65 except Exception:66 try:67 conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {col_name} {col_type}"))68 conn.commit()69 except Exception:70 pass # Column already exists71 72 73@app.on_event("startup")74async def startup_migrate():75 """Add missing columns and backfill existing rows."""76 from database import engine77 from models import Student78 from models.models import Base, Homework as _HW, School as _School, TokenBlacklist as _TB79 80 with engine.connect() as conn:81 # ── Create schools table (new) ─────────────────────────────────82 try:83 Base.metadata.create_all(engine, tables=[_School.__table__], checkfirst=True)84 except Exception:85 pass86 87 # homeworks table — use SQLAlchemy ORM to handle dialect differences88 try:89 Base.metadata.create_all(engine, tables=[_HW.__table__], checkfirst=True)90 except Exception:91 pass92 93 # token_blacklist table94 try:95 Base.metadata.create_all(engine, tables=[_TB.__table__], checkfirst=True)96 except Exception:97 pass98 99 # Cleanup expired tokens on every startup100 try:101 conn.execute(text("DELETE FROM token_blacklist WHERE expires_at < NOW()"))102 conn.commit()103 except Exception:104 pass105 106 # link_code on students107 _safe_add_column(conn, "students", "link_code", "VARCHAR(10)")108 109 # school_id on all affected tables (nullable INTEGER, no FK constraint)110 school_id_tables = [111 "users",112 "classes",113 "fee_structures",114 "fee_invoices",115 "homeworks",116 "attendance",117 "marks",118 ]119 for table in school_id_tables:120 _safe_add_column(conn, table, "school_id", "INTEGER")121 122 # school_id on school_settings (unique)123 _safe_add_column(conn, "school_settings", "school_id", "INTEGER")124 125 # admin_permissions on school_settings126 _safe_add_column(conn, "school_settings", "admin_permissions", "TEXT")127 128 # JazzCash payment gateway columns on school_settings129 jazzcash_cols = [130 ("jazzcash_merchant_id", "VARCHAR(100)"),131 ("jazzcash_password", "VARCHAR(100)"),132 ("jazzcash_integrity_salt","VARCHAR(100)"),133 ("jazzcash_is_sandbox", "BOOLEAN DEFAULT TRUE"),134 ("jazzcash_enabled", "BOOLEAN DEFAULT FALSE"),135 ("jazzcash_number", "VARCHAR(20)"),136 ]137 for col_name, col_type in jazzcash_cols:138 _safe_add_column(conn, "school_settings", col_name, col_type)139 # Backfill any students missing a link_code140 db = Session(engine)141 try:142 students = db.query(Student).filter(Student.link_code == None).all()143 used = set(r[0] for r in db.query(Student.link_code).filter(Student.link_code != None).all())144 for s in students:145 code = _random_link_code()146 while code in used:147 code = _random_link_code()148 s.link_code = code149 used.add(code)150 if students:151 db.commit()152 finally:153 db.close()154 155 156@app.get("/")157async def root():158 return {159 "message": "School Management System API",160 "version": "1.0.0",161 "docs": "/docs",162 "status": "running"163 }164 165@app.get("/health")166async def health():167 return {"status": "healthy"}168 169 170if __name__ == "__main__":171 import uvicorn172 uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)173 