open-vtop 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Session Manager for VTOP
3
+ * Automatically initializes and maintains VTOP session cookies and CSRF tokens
4
+ */
5
+ import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.js";
6
+ import * as path from "path";
7
+ const BASE = "https://vtop.vit.ac.in";
8
+ const VTOP = `${BASE}/vtop/`;
9
+ const OPEN_PAGE = `${BASE}/vtop/openPage`;
10
+ const OPEN_PAGE_ALT = `${BASE}/vtop/open/page`;
11
+ const PRELOGIN_SETUP = `${BASE}/vtop/prelogin/setup`;
12
+ const LOGIN_PAGE = `${BASE}/vtop/login`;
13
+ // Post-login endpoints
14
+ const INIT_PAGE = `${BASE}/vtop/init/page`;
15
+ const MAIN_PAGE = `${BASE}/vtop/main/page`;
16
+ const VTOP_OPEN = `${BASE}/vtop/open`;
17
+ const CONTENT = `${BASE}/vtop/content`;
18
+ const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
19
+ const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
20
+ class VTOPSessionManager {
21
+ state = {
22
+ cookies: new Map(),
23
+ csrf: null,
24
+ initialized: false,
25
+ lastInitialized: null,
26
+ loggedIn: false,
27
+ username: null,
28
+ regNo: null,
29
+ };
30
+ /**
31
+ * Extract CSRF token from HTML response
32
+ */
33
+ extractCsrf(html) {
34
+ // Look for _csrf token in various forms
35
+ const patterns = [
36
+ /name="_csrf"\s+value="([^"]+)"/,
37
+ /name='_csrf'\s+value='([^']+)'/,
38
+ /<input[^>]*name="_csrf"[^>]*value="([^"]+)"/,
39
+ /<input[^>]*value="([^"]+)"[^>]*name="_csrf"/,
40
+ ];
41
+ for (const pattern of patterns) {
42
+ const match = html.match(pattern);
43
+ if (match && match[1]) {
44
+ return match[1];
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Parse Set-Cookie headers and store cookies
51
+ */
52
+ storeCookies(response) {
53
+ const setCookieHeaders = response.headers.getSetCookie?.() || [];
54
+ for (const cookieStr of setCookieHeaders) {
55
+ const [nameValue] = cookieStr.split(";");
56
+ const [name, value] = nameValue.split("=");
57
+ if (name && value) {
58
+ this.state.cookies.set(name.trim(), value.trim());
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Get cookie header string for requests
64
+ */
65
+ getCookieHeader() {
66
+ return Array.from(this.state.cookies.entries())
67
+ .map(([name, value]) => `${name}=${value}`)
68
+ .join("; ");
69
+ }
70
+ /**
71
+ * Make a fetch request with cookie handling
72
+ */
73
+ async fetchWithCookies(url, options = {}) {
74
+ const cookieHeader = this.getCookieHeader();
75
+ const headers = new Headers(options.headers);
76
+ if (cookieHeader) {
77
+ headers.set("Cookie", cookieHeader);
78
+ }
79
+ // Add common browser headers
80
+ headers.set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
81
+ headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
82
+ headers.set("Accept-Language", "en-US,en;q=0.9");
83
+ headers.set("Accept-Encoding", "gzip, deflate, br");
84
+ headers.set("Connection", "keep-alive");
85
+ headers.set("Upgrade-Insecure-Requests", "1");
86
+ const response = await fetch(url, {
87
+ ...options,
88
+ headers,
89
+ redirect: "manual", // Handle redirects manually to capture cookies
90
+ });
91
+ // Store any cookies from the response
92
+ this.storeCookies(response);
93
+ // Follow redirects manually if needed
94
+ if (response.status >= 300 && response.status < 400) {
95
+ const location = response.headers.get("Location");
96
+ if (location) {
97
+ const redirectUrl = location.startsWith("http")
98
+ ? location
99
+ : new URL(location, url).toString();
100
+ // Create new options without body for GET redirect
101
+ const redirectOptions = {
102
+ headers: options.headers,
103
+ redirect: options.redirect,
104
+ };
105
+ return this.fetchWithCookies(redirectUrl, redirectOptions);
106
+ }
107
+ }
108
+ return response;
109
+ }
110
+ /**
111
+ * Initialize VTOP session
112
+ */
113
+ async initialize() {
114
+ try {
115
+ console.log("🔄 Initializing VTOP session...");
116
+ // Step 1: GET / to get SERVERID cookie
117
+ console.log("Step 1: GET / to get SERVERID...");
118
+ await this.fetchWithCookies(BASE);
119
+ console.log(" ✓ OK");
120
+ // Step 2: GET /vtop/ to get JSESSIONID cookie
121
+ console.log("Step 2: GET /vtop/ to get JSESSIONID...");
122
+ await this.fetchWithCookies(VTOP);
123
+ console.log(" ✓ OK");
124
+ // Step 3: GET /vtop/openPage for CSRF token
125
+ console.log("Step 3: GET /vtop/openPage for CSRF...");
126
+ const openPageRes = await this.fetchWithCookies(OPEN_PAGE);
127
+ const openPageHtml = await openPageRes.text();
128
+ console.log(" ✓ OK");
129
+ const csrf = this.extractCsrf(openPageHtml);
130
+ if (!csrf) {
131
+ console.warn("⚠️ Warning: _csrf not found on /vtop/openPage. Continuing anyway...");
132
+ }
133
+ else {
134
+ this.state.csrf = csrf;
135
+ console.log(` ✓ Found _csrf: ${csrf.substring(0, 20)}...`);
136
+ }
137
+ // Optional: Visit alternate open page (used as Referer in browser)
138
+ await this.fetchWithCookies(OPEN_PAGE_ALT).catch(() => {
139
+ // Ignore errors on this optional step
140
+ });
141
+ // Step 4: POST /vtop/prelogin/setup
142
+ console.log("Step 4: POST /vtop/prelogin/setup (flag=VTOP)...");
143
+ const formData = new URLSearchParams();
144
+ if (csrf) {
145
+ formData.set("_csrf", csrf);
146
+ }
147
+ formData.set("flag", "VTOP");
148
+ const preloginRes = await this.fetchWithCookies(PRELOGIN_SETUP, {
149
+ method: "POST",
150
+ headers: {
151
+ "Content-Type": "application/x-www-form-urlencoded",
152
+ Referer: OPEN_PAGE,
153
+ Origin: BASE,
154
+ },
155
+ body: formData.toString(),
156
+ });
157
+ console.log(` ✓ Prelogin status: ${preloginRes.status}`);
158
+ // Mark as initialized
159
+ this.state.initialized = true;
160
+ this.state.lastInitialized = new Date();
161
+ console.log("✅ VTOP session initialized successfully!");
162
+ console.log(` Cookies stored: ${this.state.cookies.size}`);
163
+ console.log(` Cookie names: ${Array.from(this.state.cookies.keys()).join(", ")}`);
164
+ }
165
+ catch (error) {
166
+ console.error("❌ Failed to initialize VTOP session:", error);
167
+ throw error;
168
+ }
169
+ }
170
+ /**
171
+ * Get current session state
172
+ */
173
+ getState() {
174
+ return {
175
+ ...this.state,
176
+ cookies: new Map(this.state.cookies),
177
+ };
178
+ }
179
+ /**
180
+ * Get CSRF token
181
+ */
182
+ getCsrf() {
183
+ return this.state.csrf;
184
+ }
185
+ /**
186
+ * Get cookies as a header string
187
+ */
188
+ getCookies() {
189
+ return this.getCookieHeader();
190
+ }
191
+ /**
192
+ * Check if session is initialized
193
+ */
194
+ isInitialized() {
195
+ return this.state.initialized;
196
+ }
197
+ /**
198
+ * Re-initialize session (useful for periodic refresh)
199
+ */
200
+ async refresh() {
201
+ console.log("🔄 Refreshing VTOP session...");
202
+ this.state.cookies.clear();
203
+ this.state.csrf = null;
204
+ this.state.initialized = false;
205
+ this.state.loggedIn = false;
206
+ this.state.username = null;
207
+ await this.initialize();
208
+ }
209
+ /**
210
+ * Detect captcha type from login page HTML using regex (no cheerio)
211
+ */
212
+ detectCaptcha(html) {
213
+ // reCAPTCHA signals
214
+ const recaptchaDom = html.includes('id="recaptcha"') ||
215
+ html.includes('id="g-recaptcha"') ||
216
+ html.includes('class="g-recaptcha"');
217
+ const recaptchaJs = html.includes("var captchaType=2");
218
+ const isRecaptcha = recaptchaDom || recaptchaJs;
219
+ // Text CAPTCHA signals - check for captchaStr input
220
+ const hasCaptchaInput = html.includes('name="captchaStr"') || html.includes('id="captchaStr"');
221
+ // Look for data URI image in the HTML
222
+ // Support both single and double quotes
223
+ // VTOP sometimes sends "null" in base64, so we need to find the first valid one
224
+ const imgDataUriMatches = [
225
+ ...html.matchAll(/src=["'](data:image\/[^"']+)["']/gi),
226
+ ];
227
+ let imgDataUri = null;
228
+ for (const match of imgDataUriMatches) {
229
+ if (match[1] && !match[1].includes(";base64,null")) {
230
+ imgDataUri = match[1];
231
+ break;
232
+ }
233
+ }
234
+ // specific check for the warning if we found matches but none were valid
235
+ if (!imgDataUri && imgDataUriMatches.length > 0) {
236
+ console.warn("⚠️ Detected invalid captcha image (base64 is null)");
237
+ }
238
+ const isTextCaptcha = hasCaptchaInput && imgDataUri !== null;
239
+ // Extract CSRF token
240
+ const csrf = this.extractCsrf(html);
241
+ return { isTextCaptcha, isRecaptcha, csrf, imgDataUri };
242
+ }
243
+ /**
244
+ * Login to VTOP with username, password, and registration number
245
+ * Polls the login page until a text CAPTCHA appears, solves it, and submits
246
+ */
247
+ async login(username, password, regNo, maxAttempts = 10) {
248
+ if (!this.state.initialized) {
249
+ console.log("Session not initialized, initializing first...");
250
+ await this.initialize();
251
+ }
252
+ console.log("🔐 Starting VTOP login process...");
253
+ console.log("Polling /vtop/login until text CAPTCHA appears...");
254
+ let attempt = 0;
255
+ while (attempt < maxAttempts) {
256
+ attempt++;
257
+ try {
258
+ // Fetch login page
259
+ const res = await this.fetchWithCookies(LOGIN_PAGE);
260
+ const body = await res.text();
261
+ const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = this.detectCaptcha(body);
262
+ // Get current cookies for logging
263
+ const curJsession = this.state.cookies.get("JSESSIONID") || "(?)";
264
+ const curServerID = this.state.cookies.get("SERVERID") || "(?)";
265
+ console.log(`Attempt ${attempt}: status ${res.status} | text-captcha=${isTextCaptcha ? "YES" : "no"} | recaptcha=${isRecaptcha ? "YES" : "no"} | JSESSIONID=${curJsession}`);
266
+ if (isTextCaptcha) {
267
+ let solvedCaptcha = "";
268
+ if (imgDataUri) {
269
+ const parts = extractDataUriParts(imgDataUri);
270
+ try {
271
+ // Trim whitespace just in case
272
+ const cleanDataUri = imgDataUri.trim();
273
+ if (cleanDataUri !== imgDataUri) {
274
+ console.log("⚠️ Trimmed whitespace from captcha data URI");
275
+ }
276
+ solvedCaptcha = await solve(cleanDataUri);
277
+ console.log("✅ Solved CAPTCHA:", solvedCaptcha);
278
+ // Save captcha image for debugging
279
+ if (parts?.base64) {
280
+ const out = path.resolve(process.cwd(), "captcha.jpg");
281
+ await saveCaptchaImage(parts.base64, out);
282
+ // console.log(`📷 Saved CAPTCHA image to ${out}`);
283
+ }
284
+ }
285
+ catch (e) {
286
+ console.warn("⚠️ Failed to solve captcha:", e);
287
+ solvedCaptcha = "";
288
+ // Save the failed data URI for inspection
289
+ try {
290
+ const fs = await import("fs/promises");
291
+ const failPath = path.resolve(process.cwd(), "failed_captcha_data.txt");
292
+ await fs.writeFile(failPath, imgDataUri);
293
+ console.log(`📝 Saved failed captcha data URI to ${failPath}`);
294
+ }
295
+ catch (writeErr) {
296
+ console.error("Failed to save failed captcha data:", writeErr);
297
+ }
298
+ }
299
+ }
300
+ else {
301
+ console.log("Text CAPTCHA detected but no data URI image found.");
302
+ }
303
+ // Build and submit login form
304
+ console.log("\n📤 Submitting POST /vtop/login ...");
305
+ const loginForm = new URLSearchParams();
306
+ if (csrf)
307
+ loginForm.set("_csrf", csrf);
308
+ loginForm.set("username", username);
309
+ loginForm.set("password", password);
310
+ loginForm.set("captchaStr", solvedCaptcha);
311
+ const _cookies = `JSESSIONID=${curJsession}; SERVERID=${curServerID}`;
312
+ const postHeaders = {
313
+ "Cache-Control": "max-age=0",
314
+ Origin: BASE,
315
+ Referer: OPEN_PAGE_ALT,
316
+ "Content-Type": "application/x-www-form-urlencoded",
317
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
318
+ "Accept-Encoding": "gzip, deflate, br, zstd",
319
+ "Accept-Language": "en-US,en;q=0.9",
320
+ "Sec-Fetch-Dest": "document",
321
+ "Sec-Fetch-Mode": "navigate",
322
+ "Sec-Fetch-Site": "same-origin",
323
+ "Sec-Fetch-User": "?1",
324
+ "Upgrade-Insecure-Requests": "1",
325
+ Priority: "u=0, i",
326
+ Cookie: _cookies,
327
+ };
328
+ try {
329
+ const postRes = await fetch(LOGIN_PAGE, {
330
+ method: "POST",
331
+ headers: postHeaders,
332
+ body: loginForm.toString(),
333
+ redirect: "manual",
334
+ });
335
+ console.log(` -> Login POST status: ${postRes.status}`);
336
+ // Store any new cookies
337
+ this.storeCookies(postRes);
338
+ const setCookie = postRes.headers.getSetCookie?.() || [];
339
+ if (setCookie.length > 0) {
340
+ console.log(" -> New cookies received");
341
+ }
342
+ // Check for redirect
343
+ const location = postRes.headers.get("Location");
344
+ if (location) {
345
+ console.log(` -> Redirect to: ${location}`);
346
+ // Follow redirect to complete the login flow
347
+ const redirectUrl = location.startsWith("http")
348
+ ? location
349
+ : new URL(location, LOGIN_PAGE).toString();
350
+ await this.fetchWithCookies(redirectUrl);
351
+ }
352
+ // After successfully submitting the captcha, assume login worked
353
+ // The actual success will be determined when we try to fetch data
354
+ console.log("🎉 Login POST submitted successfully!");
355
+ this.state.loggedIn = true;
356
+ this.state.username = username;
357
+ this.state.regNo = regNo;
358
+ return true;
359
+ }
360
+ catch (e) {
361
+ console.error("Login POST failed:", e);
362
+ }
363
+ }
364
+ // If reCAPTCHA, we cannot solve it automatically
365
+ if (isRecaptcha) {
366
+ console.log("⚠️ reCAPTCHA detected - cannot solve automatically");
367
+ // Wait a bit and retry, hoping for text captcha
368
+ await new Promise((r) => setTimeout(r, 2000));
369
+ continue;
370
+ }
371
+ // Wait before next poll
372
+ await new Promise((r) => setTimeout(r, 500));
373
+ }
374
+ catch (e) {
375
+ console.error(`Attempt ${attempt} failed:`, e);
376
+ await new Promise((r) => setTimeout(r, 1000));
377
+ }
378
+ }
379
+ console.log(`❌ Login failed after ${maxAttempts} attempts`);
380
+ return false;
381
+ }
382
+ /**
383
+ * Check if logged in
384
+ */
385
+ isLoggedIn() {
386
+ return this.state.loggedIn;
387
+ }
388
+ /**
389
+ * Get current username
390
+ */
391
+ getUsername() {
392
+ return this.state.username;
393
+ }
394
+ /**
395
+ * Navigate through post-login pages to establish authenticated session
396
+ * This mimics what the browser does after successful login
397
+ */
398
+ async navigatePostLogin() {
399
+ if (!this.state.loggedIn) {
400
+ console.error("❌ Cannot navigate post-login pages: not logged in");
401
+ return false;
402
+ }
403
+ const cookies = this.getCookieHeader();
404
+ const headers = {
405
+ Referer: LOGIN_PAGE,
406
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
407
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
408
+ "Accept-Encoding": "gzip, deflate, br, zstd",
409
+ "Cache-Control": "max-age=0",
410
+ "Upgrade-Insecure-Requests": "1",
411
+ Origin: BASE,
412
+ "Content-Type": "application/x-www-form-urlencoded",
413
+ Cookie: cookies,
414
+ };
415
+ try {
416
+ // Navigate through required pages
417
+ console.log("📄 Navigating post-login pages...");
418
+ // 1. /vtop/init/page
419
+ const initRes = await this.fetchWithCookies(INIT_PAGE, { headers });
420
+ console.log(` -> /vtop/init/page: ${initRes.status}`);
421
+ // 2. /vtop/main/page
422
+ const mainRes = await this.fetchWithCookies(MAIN_PAGE, { headers });
423
+ console.log(` -> /vtop/main/page: ${mainRes.status}`);
424
+ // 3. /vtop/open
425
+ const openRes = await this.fetchWithCookies(VTOP_OPEN, { headers });
426
+ console.log(` -> /vtop/open: ${openRes.status}`);
427
+ // 4. /vtop/content - extract new CSRF from here
428
+ const contentRes = await this.fetchWithCookies(CONTENT, { headers });
429
+ const contentHtml = await contentRes.text();
430
+ console.log(` -> /vtop/content: ${contentRes.status}`);
431
+ const newCsrf = this.extractCsrf(contentHtml);
432
+ if (newCsrf) {
433
+ this.state.csrf = newCsrf;
434
+ console.log(` ✓ Updated CSRF token from content page`);
435
+ }
436
+ console.log("✅ Post-login navigation complete");
437
+ return true;
438
+ }
439
+ catch (e) {
440
+ console.error("❌ Post-login navigation failed:", e);
441
+ return false;
442
+ }
443
+ }
444
+ /**
445
+ * Parse upcoming assignments from VTOP HTML response
446
+ * Uses regex instead of cheerio
447
+ */
448
+ parseAssignmentsHtml(html) {
449
+ const assignments = [];
450
+ // VTOP returns assignments in a table or as JSON-like structure
451
+ // Try to parse as JSON first (some endpoints return JSON)
452
+ try {
453
+ const jsonMatch = html.match(/\[[\s\S]*?\]/);
454
+ if (jsonMatch) {
455
+ const parsed = JSON.parse(jsonMatch[0]);
456
+ if (Array.isArray(parsed)) {
457
+ return parsed.map((item) => ({
458
+ courseCode: String(item.courseCode || item.code || ""),
459
+ courseName: String(item.courseName || item.name || ""),
460
+ assignmentTitle: String(item.assignmentTitle || item.title || item.assignmentName || ""),
461
+ dueDate: String(item.dueDate || item.endDate || item.deadline || ""),
462
+ status: String(item.status || ""),
463
+ maxMarks: String(item.maxMarks || item.marks || ""),
464
+ }));
465
+ }
466
+ }
467
+ }
468
+ catch {
469
+ // Not JSON, try HTML parsing
470
+ }
471
+ // Parse HTML table rows using regex
472
+ // VTOP table structure: #(th), Course Name(td), Title(td), Last Date(td), Uploaded(td)
473
+ // Note: VTOP HTML sometimes has malformed tags like <<td
474
+ const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
475
+ // Match both <th> and <td> cells, and handle malformed <<td
476
+ const cellRegex = /<?<t[hd][^>]*>([\s\S]*?)(?:<\/t[hd]>|<\/tr|<tr|$)/gi;
477
+ let rowMatch;
478
+ while ((rowMatch = rowRegex.exec(html)) !== null) {
479
+ const cells = [];
480
+ let cellMatch;
481
+ const rowContent = rowMatch[1];
482
+ // Reset lastIndex for cell regex
483
+ cellRegex.lastIndex = 0;
484
+ while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
485
+ // Strip HTML tags from cell content
486
+ const cellContent = cellMatch[1].replace(/<[^>]+>/g, "").trim();
487
+ cells.push(cellContent);
488
+ }
489
+ // Skip header rows (first cell would be "#" or similar header text)
490
+ if (cells.length >= 4 && cells[0] !== "#" && !isNaN(Number(cells[0]))) {
491
+ // Table structure: # (row num), Course Name, Title, Last Date, Uploaded
492
+ // cells[0] = row number (skip)
493
+ // cells[1] = Course Name
494
+ // cells[2] = Title (assignment title)
495
+ // cells[3] = Last Date (due date)
496
+ // cells[4] = Uploaded (status)
497
+ assignments.push({
498
+ courseCode: "", // Not in this table format
499
+ courseName: cells[1] || "",
500
+ assignmentTitle: cells[2] || "",
501
+ dueDate: cells[3] || "",
502
+ status: cells[4] || "Pending",
503
+ maxMarks: "", // Not in this table format
504
+ });
505
+ }
506
+ }
507
+ return assignments;
508
+ }
509
+ /**
510
+ * Fetch upcoming assignments from VTOP
511
+ */
512
+ async fetchUpcomingAssignments() {
513
+ if (!this.state.loggedIn || !this.state.regNo) {
514
+ console.error("❌ Cannot fetch assignments: not logged in or no regNo");
515
+ return [];
516
+ }
517
+ // Ensure we've navigated post-login pages
518
+ await this.navigatePostLogin();
519
+ const cookies = this.getCookieHeader();
520
+ const now = new Date();
521
+ // First, do the academics default check
522
+ const accParams = new URLSearchParams();
523
+ accParams.set("authorizedID", this.state.regNo);
524
+ if (this.state.csrf)
525
+ accParams.set("_csrf", this.state.csrf);
526
+ accParams.set("x", now.toUTCString());
527
+ const apiHeaders = {
528
+ Accept: "*/*",
529
+ "Accept-Encoding": "gzip, deflate, br, zstd",
530
+ "Accept-Language": "en-US,en;q=0.7",
531
+ "Content-Type": "application/x-www-form-urlencoded",
532
+ Cookie: cookies,
533
+ Origin: BASE,
534
+ Priority: "u=1, i",
535
+ Referer: CONTENT,
536
+ "Sec-Fetch-Dest": "empty",
537
+ "Sec-Fetch-Mode": "cors",
538
+ "Sec-Fetch-Site": "same-origin",
539
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
540
+ };
541
+ try {
542
+ // AcademicsDefaultCheck
543
+ console.log("📚 Checking academics context...");
544
+ const accRes = await fetch(ACADEMICS_CHECK, {
545
+ method: "POST",
546
+ headers: apiHeaders,
547
+ body: accParams.toString(),
548
+ });
549
+ console.log(` -> AcademicsDefaultCheck: ${accRes.status}`);
550
+ }
551
+ catch (e) {
552
+ console.warn("⚠️ AcademicsDefaultCheck failed:", e);
553
+ }
554
+ // Now fetch the assignments
555
+ const assParams = new URLSearchParams();
556
+ assParams.set("authorizedID", this.state.regNo);
557
+ if (this.state.csrf)
558
+ assParams.set("_csrf", this.state.csrf);
559
+ assParams.set("x", now.toUTCString());
560
+ try {
561
+ console.log("📝 Fetching upcoming assignments...");
562
+ const assRes = await fetch(UPCOMING_ASSIGNMENTS, {
563
+ method: "POST",
564
+ headers: apiHeaders,
565
+ body: assParams.toString(),
566
+ });
567
+ console.log(` -> Upcoming assignments: ${assRes.status}`);
568
+ const assBody = await assRes.text();
569
+ console.log(` -> Response length: ${assBody.length} chars`);
570
+ console.log(` -> Raw response:\n${assBody}`);
571
+ if (assBody) {
572
+ const assignments = this.parseAssignmentsHtml(assBody);
573
+ console.log(`✅ Parsed ${assignments.length} assignments`);
574
+ return assignments;
575
+ }
576
+ }
577
+ catch (e) {
578
+ console.error("❌ Failed to fetch assignments:", e);
579
+ }
580
+ return [];
581
+ }
582
+ }
583
+ // Export singleton instance
584
+ export const sessionManager = new VTOPSessionManager();
585
+ // Auto-initialize on module load
586
+ sessionManager.initialize().catch((error) => {
587
+ console.error("Failed to auto-initialize session:", error);
588
+ });
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ import { BaseLayout } from "./layouts/Base.js";
3
+ export const Home = () => {
4
+ return (_jsxs(BaseLayout, { title: "Open-VTOP", children: [_jsx("h1", { children: "Open-VTOP" }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDD10 Login to VTOP" }), _jsxs("form", { id: "login-form", style: "display: flex; flex-direction: column; gap: 0.75rem; max-width: 400px;", children: [_jsxs("div", { children: [_jsx("label", { for: "username", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Username:" }), _jsx("input", { type: "text", id: "username", name: "username", placeholder: "Enter your VTOP username", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsxs("div", { children: [_jsx("label", { for: "password", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Password:" }), _jsx("input", { type: "password", id: "password", name: "password", placeholder: "Enter your password", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsxs("div", { children: [_jsx("label", { for: "regNo", style: "display: block; margin-bottom: 0.25rem; font-weight: bold;", children: "Registration Number:" }), _jsx("input", { type: "text", id: "regNo", name: "regNo", placeholder: "e.g. 24BCI0150", style: "width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem;", required: true })] }), _jsx("button", { type: "submit", "hx-post": "/api/login/form", "hx-target": "#login-result", "hx-swap": "innerHTML", "hx-include": "#login-form", style: "padding: 0.75rem; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: bold;", children: "\uD83D\uDE80 Login" })] }), _jsx("div", { id: "login-result", style: "margin-top: 1rem;" })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCCA Login Status" }), _jsx("button", { "hx-get": "/api/login/status/html", "hx-target": "#login-status", "hx-swap": "innerHTML", "hx-trigger": "load, click", children: "Refresh Login Status" }), _jsx("div", { id: "login-status", children: "Loading..." })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCDA Upcoming Assignments" }), _jsx("button", { "hx-get": "/api/assignments/html", "hx-target": "#assignments-list", "hx-swap": "innerHTML", "hx-indicator": "#assignments-loading", style: "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: bold; cursor: pointer;", children: "\uD83D\uDD04 Load Assignments" }), _jsx("span", { id: "assignments-loading", class: "htmx-indicator", style: "margin-left: 0.5rem; color: #aaa;", children: "Loading..." }), _jsx("div", { id: "assignments-list", style: "margin-top: 1rem;", children: _jsx("div", { style: "color: #888; padding: 1rem; background: #f5f5f5; border-radius: 8px;", children: "Click \"Load Assignments\" to fetch your upcoming assignments from VTOP." }) })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDD27 Session Status" }), _jsx("button", { "hx-get": "/api/session/status", "hx-target": "#session-status", "hx-swap": "innerHTML", "hx-trigger": "load, click", children: "Refresh Session Status" }), _jsx("div", { id: "session-status", children: "Loading session status..." })] }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "\uD83D\uDCDD Debug Logs" }), _jsx("button", { "hx-get": "/api/debug/logs", "hx-target": "#debug-logs", "hx-swap": "innerHTML", "hx-trigger": "click", children: "Load Debug Logs" }), _jsx("button", { "hx-post": "/api/debug/logs/clear", "hx-target": "#debug-logs", "hx-swap": "innerHTML", style: "margin-left: 0.5rem; background: #ff6b6b;", children: "Clear Logs" }), _jsx("div", { id: "debug-logs", style: "margin-top: 1rem; max-height: 400px; overflow-y: auto; background: #1e1e1e; color: #0f0; font-family: monospace; padding: 1rem; border-radius: 4px; font-size: 0.85rem;", children: "Click \"Load Debug Logs\" to see login attempts..." })] })] }));
5
+ };
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ export const Container = ({ title, children }) => {
3
+ return (_jsxs("div", { class: "container", children: [_jsx("h2", { children: title }), children] }));
4
+ };
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ export const Item = ({ id, name }) => {
3
+ return (_jsxs("div", { id: `item-${id}`, class: "item", children: [name, _jsx("button", { "hx-delete": `/api/items/${id}`, "hx-confirm": "Are you sure you want to delete this item?", "hx-target": `#item-${id}`, "hx-swap": "outerHTML", class: "danger", children: "Delete" })] }));
4
+ };