Source: wrapper.js

import { NotLoggedIn, SessionExpired, SessionError, AccountAPIError, LoginError, APIError } from "./exceptions.js";
import { RegisteredSubject, Registrations } from "./registration.js";
import { AttendanceMeta, AttendanceHeader, Semester } from "./attendance.js";
import { ExamEvent } from "./exam.js";
import { generate_local_name, serialize_payload } from "./encryption.js";

/**
 * @module Wrapper
 */
/**
 * Base API endpoint for the JIIT web portal
 * @constant {string}
 */
export const API = "https://webportal.jiit.ac.in:6011/StudentPortalAPI";

/**
 * Default CAPTCHA values used for login
 * @constant {{captcha: string, hidden: string}}
 */
export const DEFCAPTCHA = { captcha: "phw5n", hidden: "gmBctEffdSg=" };

/**
 * Class representing a session with the web portal
 */
export class WebPortalSession {
  /**
   * Creates a WebPortalSession instance from API response
   * @param {Object} resp - Response object from login API
   * @param {Object} resp.regdata - Registration data containing user details
   * @param {Array} resp.regdata.institutelist - List of institutes user has access to
   * @param {string} resp.regdata.memberid - Member ID of the user
   * @param {string} resp.regdata.userid - User ID
   * @param {string} resp.regdata.token - Token for authentication
   * @param {string} resp.regdata.clientid - Client ID
   * @param {string} resp.regdata.membertype - Type of member
   * @param {string} resp.regdata.name - Name of the user
   * @param {string} resp.regdata.enrollmentno - Enrollment number
   */
  constructor(resp) {
    this.raw_response = resp;
    this.regdata = resp["regdata"];

    let institute = this.regdata["institutelist"][0];
    this.institute = institute["label"];
    this.instituteid = institute["value"];
    this.memberid = this.regdata["memberid"];
    this.userid = this.regdata["userid"];

    this.token = this.regdata["token"];
    let expiry_timestamp = JSON.parse(atob(this.token.split(".")[1]))["exp"];
    this.expiry = new Date(expiry_timestamp * 1000); // In JavaScript, Date expects milliseconds

    this.clientid = this.regdata["clientid"];
    this.membertype = this.regdata["membertype"];
    this.name = this.regdata["name"];
    this.enrollmentno = this.regdata["enrollmentno"];
  }

  /**
   * Generates authentication headers for API requests
   * @returns {Promise<Object>} Headers object containing Authorization and LocalName
   */
  async get_headers() {
    const localname = await generate_local_name();
    return {
      Authorization: `Bearer ${this.token}`,
      LocalName: localname,
    };
  }
}

/**
 * Main class for interacting with the JIIT web portal API
 */
export class WebPortal {
  /**
   * Creates a WebPortal instance
   */
  constructor() {
    this.session = null;
  }

  /**
   * Internal method to make HTTP requests to the API
   * @private
   * @param {string} method - HTTP method (GET, POST etc)
   * @param {string} url - API endpoint URL
   * @param {Object} [options={}] - Request options
   * @param {Object} [options.headers] - Additional headers
   * @param {Object} [options.json] - JSON payload
   * @param {string} [options.body] - Raw body payload
   * @param {boolean} [options.authenticated] - Whether request needs authentication
   * @param {Error} [options.exception] - Custom error class to throw
   * @returns {Promise<Object>} API response
   * @throws {APIError} On API or network errors
   */
  async __hit(method, url, options = {}) {
    let exception = APIError; // Default exception
    if (options.exception) {
      exception = options.exception;
      delete options.exception;
    }

    console.log(options)
    let header;
    if (options.authenticated) {
      header = await this.session.get_headers(); // Assumes calling method is authenticated
      delete options.authenticated;
    } else {
      let localname = await generate_local_name();
      header = { LocalName: localname };
    }

    if (options.headers) {
      options.headers = { ...options.headers, ...header };
    } else {
      options.headers = header;
    }

    let fetchOptions = {
      method: method,
      headers: {
        "Content-Type": "application/json",
        ...options.headers,
      },
    };

    if (options.json) {
      fetchOptions.body = JSON.stringify(options.json);
    } else {
      fetchOptions.body = options.body;
    }

    try {
      console.log("fetching", url, "with options", fetchOptions);
      const response = await fetch(url, fetchOptions);

      if (response.status === 513) {
        throw new exception("JIIT Web Portal server is temporarily unavailable (HTTP 513). Please try again later.");
      }
      if (response.status === 401) {
          throw new SessionExpired(response.error);
      }

      const resp = await response.json();

      if (resp.status && resp.status.responseStatus !== "Success") {
        throw new exception(`status:\n${JSON.stringify(resp.status, null, 2)}`);
      }
      return resp;
    } catch (error) {
      // Handle CORS errors
      if (error instanceof TypeError && error.message.includes('CORS')) {
        throw new exception("JIIT Web Portal server is temporarily unavailable. Please try again later.");
      }
      throw new exception(error.message || "Unknown error");
    }
  }

  /**
   * Logs in a student user
   * @param {string} username - Student username
   * @param {string} password - Student password
   * @param {{captcha: string, hidden: string}} [captcha=DEFCAPTCHA] - CAPTCHA
   * @returns {Promise<WebPortalSession>} New session instance
   * @throws {LoginError} On login failure
   */
  async student_login(username, password, captcha = DEFCAPTCHA) {
    let pretoken_endpoint = "/token/pretoken-check";
    let token_endpoint = "/token/generate-token1";

    let payload = { username: username, usertype: "S", captcha: captcha };
    payload = await serialize_payload(payload);

    let resp = await this.__hit("POST", API + pretoken_endpoint, { body: payload, exception: LoginError });

    let payload2 = resp["response"];
    delete payload2["rejectedData"];
    payload2["Modulename"] = "STUDENTMODULE";
    payload2["passwordotpvalue"] = password;
    payload2 = await serialize_payload(payload2);

    const resp2 = await this.__hit("POST", API + token_endpoint, { body: payload2, exception: LoginError });
    this.session = new WebPortalSession(resp2["response"]);
    return this.session;
  }

  /**
   * Gets personal information of logged in student
   * @returns {Promise<Object>} Student personal information
   */
  async get_personal_info() {
    const ENDPOINT = "/studentpersinfo/getstudent-personalinformation";
    const payload = {
      clinetid: "SOAU",
      instituteid: this.session.instituteid,
    };
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"];
  }

  /**
   * Gets bank account information of logged in student
   * @returns {Promise<Object>} Student bank information
   */
  async get_student_bank_info() {
    const ENDPOINT = "/studentbankdetails/getstudentbankinfo";
    const payload = {
      instituteid: this.session.instituteid,
      studentid: this.session.memberid,
    };
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"];
  }

  /**
   * Changes password for logged in student
   * @param {string} old_password - Current password
   * @param {string} new_password - New password
   * @returns {Promise<Object>} Response indicating success/failure
   * @throws {AccountAPIError} On password change failure
   */
  async change_password(old_password, new_password) {
    const ENDPOINT = "/clxuser/changepassword";
    const payload = {
      membertype: this.session.membertype,
      oldpassword: old_password,
      newpassword: new_password,
      confirmpassword: new_password,
    };
    const resp = await this.__hit("POST", API + ENDPOINT, {
      json: payload,
      authenticated: true,
      exception: AccountAPIError,
    });
    return resp["response"];
  }

  /**
   * Gets attendance metadata including headers and semesters
   * @returns {Promise<AttendanceMeta>} Attendance metadata
   */
  async get_attendance_meta() {
    const ENDPOINT = "/StudentClassAttendance/getstudentInforegistrationforattendence";

    const payload = {
      clientid: this.session.clientid,
      instituteid: this.session.instituteid,
      membertype: this.session.membertype,
    };

    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return new AttendanceMeta(resp["response"]);
  }

  /**
   * Gets attendance details for a semester
   * @param {AttendanceHeader} header - Attendance header
   * @param {Semester} semester - Semester object
   * @returns {Promise<Object>} Attendance details
   */
  async get_attendance(header, semester) {
    const ENDPOINT = "/StudentClassAttendance/getstudentattendancedetail";

    const payload = await serialize_payload({
      clientid: this.session.clientid,
      instituteid: this.session.instituteid,
      registrationcode: semester.registration_code,
      registrationid: semester.registration_id,
      stynumber: header.stynumber,
    });

    // console.log(payload)

    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"];
  }

  /**
   * Gets attendance for every class of the subject for the semester.
   * @param {Semester} semester - Semester object
   * @param {string} subjectid - Subject ID
   * @param {string} individualsubjectcode - Individual subject code
   * @param {Array<string>} subjectcomponentids - Array of subject component IDs
   * @returns {Promise<Object>} Subject attendance details
   */
  async get_subject_daily_attendance(semester, subjectid, individualsubjectcode, subjectcomponentids) {
    const ENDPOINT = "/StudentClassAttendance/getstudentsubjectpersentage";
    const payload = await serialize_payload({
      cmpidkey: subjectcomponentids.map((id) => ({ subjectcomponentid: id })),
      clientid: this.session.clientid,
      instituteid: this.session.instituteid,
      registrationcode: semester.registration_code,
      registrationid: semester.registration_id,
      subjectcode: individualsubjectcode,
      subjectid: subjectid,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"];
  }

  /**
   * Gets list of registered semesters
   * @returns {Promise<Array<Semester>>} Array of semester objects
   */
  async get_registered_semesters() {
    const ENDPOINT = "/reqsubfaculty/getregistrationList";

    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
      studentid: this.session.memberid,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"]["registrations"].map((i) => Semester.from_json(i));
  }

  /**
   * Gets registered subjects and faculty details for a semester
   * @param {Semester} semester - Semester object
   * @returns {Promise<Registrations>} Registration details
   */
  async get_registered_subjects_and_faculties(semester) {
    const ENDPOINT = "/reqsubfaculty/getfaculties";
    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
      studentid: this.session.memberid,
      registrationid: semester.registration_id,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return new Registrations(resp["response"]);
  }

  /**
   * Gets semesters that have exam events
   * @returns {Promise<Array<Semester>>} Array of semester objects
   */
  async get_semesters_for_exam_events() {
    const ENDPOINT = "/studentcommonsontroller/getsemestercode-withstudentexamevents";
    const payload = await serialize_payload({
      clientid: this.session.clientid,
      instituteid: this.session.instituteid,
      memberid: this.session.memberid,
    });

    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"]["semesterCodeinfo"]["semestercode"].map((i) => Semester.from_json(i));
  }

  /**
   * Gets exam events for a semester
   * @param {Semester} semester - Semester object
   * @returns {Promise<Array<ExamEvent>>} Array of exam event objects
   */
  async get_exam_events(semester) {
    const ENDPOINT = "/studentcommonsontroller/getstudentexamevents";
    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
      registationid: semester.registration_id, // not a typo
    });

    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"]["eventcode"]["examevent"].map((i) => ExamEvent.from_json(i));
  }

  /**
   * Gets exam schedule for an exam event
   * @param {ExamEvent} exam_event - Exam event object
   * @returns {Promise<Object>} Exam schedule details
   */
  async get_exam_schedule(exam_event) {
    const ENDPOINT = "/studentsttattview/getstudent-examschedule";
    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
      registrationid: exam_event.registration_id,
      exameventid: exam_event.exam_event_id,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"];
  }

  /**
   * Gets semesters that have marks available
   * @returns {Promise<Array<Semester>>} Array of semester objects
   */
  async get_semesters_for_marks() {
    const ENDPOINT = "/studentcommonsontroller/getsemestercode-exammarks";
    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
      studentid: this.session.memberid,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"]["semestercode"].map((i) => Semester.from_json(i));
  }

  /**
   * Downloads marks PDF for a semester
   * @param {Semester} semester - Semester object
   * @throws {APIError} On download failure
   */
  async download_marks(semester) {
    const ENDPOINT =
      "/studentsexamview/printstudent-exammarks/" +
      // this.session.memberid +
      // "/" +
      this.session.instituteid +
      "/" +
      semester.registration_id +
      "/" +
      semester.registration_code;
    const localname = await generate_local_name();
    let _headers = await this.session.get_headers(localname);
    const fetchOptions = {
      method: "GET",
      headers: _headers,
    };

    try {
      const resp = await fetch(API + ENDPOINT, fetchOptions);
      const blob = await resp.blob();
      const url = window.URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `marks_${semester.registration_code}.pdf`;
      document.body.appendChild(a);
      a.click();
      window.URL.revokeObjectURL(url);
      a.remove();
    } catch (error) {
      throw new APIError(error);
    }
  }

  /**
   * Gets semesters that have grade cards available
   * @returns {Promise<Array<Semester>>} Array of semester objects
   */
  async get_semesters_for_grade_card() {
    const ENDPOINT = "/studentgradecard/getregistrationList";
    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"]["registrations"].map((i) => Semester.from_json(i));
  }

  /**
   * Gets program ID for grade card
   * @private
   * @returns {Promise<string>} Program ID
   */
  async __get_program_id() {
    const ENDPOINT = "/studentgradecard/getstudentinfo";
    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"]["programid"];
  }

  /**
   * Gets grade card for a semester
   * @param {Semester} semester - Semester object
   * @returns {Promise<Object>} Grade card details
   */
  async get_grade_card(semester) {
    const programid = await this.__get_program_id();
    const ENDPOINT = "/studentgradecard/showstudentgradecard";
    const payload = await serialize_payload({
      branchid: this.session.branch_id,
      instituteid: this.session.instituteid,
      programid: programid,
      registrationid: semester.registration_id,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"];
  }

  /**
   * Gets current semester number
   * @private
   * @returns {Promise<number>} Current semester number
   */
  async __get_semester_number() {
    const ENDPOINT = "/studentsgpacgpa/checkIfstudentmasterexist";
    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
      studentid: this.session.memberid,
      name: this.session.name,
      enrollmentno: this.session.enrollmentno,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"]["studentlov"]["currentsemester"];
  }

  /**
   * Gets SGPA and CGPA details
   * @returns {Promise<Object>} SGPA and CGPA details
   */
  async get_sgpa_cgpa() {
    const ENDPOINT = "/studentsgpacgpa/getallsemesterdata";
    const stynumber = await this.__get_semester_number();
    const payload = await serialize_payload({
      instituteid: this.session.instituteid,
      studentid: this.session.memberid,
      stynumber: stynumber,
    });
    const resp = await this.__hit("POST", API + ENDPOINT, { json: payload, authenticated: true });
    return resp["response"];
  }
}

/**
 * Decorator that checks if user is authenticated before executing method
 * @param {Function} method - Method to decorate
 * @returns {Function} Decorated method that checks authentication
 * @throws {NotLoggedIn} If user is not logged in
 */
function authenticated(method) {
  return function (...args) {
    if (this.session == null) {
      throw new NotLoggedIn();
    }
    return method.apply(this, args);
  };
}

/**
 * List of methods that require authentication
 * @constant {Array<string>}
 */
const authenticatedMethods = [
  "get_personal_info",
  "get_student_bank_info",
  "change_password",
  "get_attendance_meta",
  "get_attendance",
  "get_subject_daily_attendance",
  "get_registered_semesters",
  "get_registered_subjects_and_faculties",
  "get_semesters_for_exam_events",
  "get_exam_events",
  "get_exam_schedule",
  "get_semesters_for_marks",
  "download_marks",
  "get_semesters_for_grade_card",
  "__get_program_id",
  "get_grade_card",
  "__get_semester_number",
  "get_sgpa_cgpa",
];

authenticatedMethods.forEach((methodName) => {
  WebPortal.prototype[methodName] = authenticated(WebPortal.prototype[methodName]);
});