open-vtop 1.0.3 → 1.0.5

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.
package/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  An open-source VTOP client with automatic session management.
4
4
 
5
+ ## Todo
6
+ 1) save usn and password for future use
7
+ 2) grab regno from the responses it self
8
+ 3) attendance
9
+ 4) timetable
10
+ 5) cgpa
11
+ 6) course-page
12
+ 7) callendar
13
+ 8) many more...
14
+ 9) qol like automatic browser open, better logging, save usn password for faster logins, logout
5
15
  ## Features
6
16
 
7
17
  - 🚀 **Automatic Session Initialization**: VTOP session cookies and CSRF tokens are automatically established when the server starts
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * CAPTCHA Solver for VTOP
3
- * Uses canvas for image processing and bitmap matching / neural network for character recognition
3
+ * Uses pure JS for image processing and bitmap matching / neural network for character recognition
4
4
  */
5
- import { createCanvas, loadImage } from "canvas";
5
+ import * as jpeg from "jpeg-js";
6
+ import { PNG } from "pngjs";
6
7
  import { bitmaps } from "./bitmaps.js";
7
8
  // ============ Bitmap-based solver (original method) ============
8
9
  function captchaParse(imgarr) {
@@ -134,6 +135,53 @@ function maxSoft(a) {
134
135
  }
135
136
  return n;
136
137
  }
138
+ async function decodeImage(dataUri) {
139
+ const parts = extractDataUriParts(dataUri);
140
+ if (!parts)
141
+ throw new Error("Invalid Data URI");
142
+ const buffer = Buffer.from(parts.base64, "base64");
143
+ if (parts.mimeType === "image/png") {
144
+ return new Promise((resolve, reject) => {
145
+ const png = new PNG();
146
+ png.parse(buffer, (err, data) => {
147
+ if (err)
148
+ reject(err);
149
+ else
150
+ resolve({
151
+ width: data.width,
152
+ height: data.height,
153
+ data: data.data,
154
+ });
155
+ });
156
+ });
157
+ }
158
+ else if (parts.mimeType === "image/jpeg") {
159
+ const decoded = jpeg.decode(buffer, { useTArray: true });
160
+ return {
161
+ width: decoded.width,
162
+ height: decoded.height,
163
+ data: decoded.data,
164
+ };
165
+ }
166
+ throw new Error("Unsupported image type: " + parts.mimeType);
167
+ }
168
+ function resizeImage(src, targetW, targetH) {
169
+ // Nearest neighbor resize
170
+ const target = new Uint8ClampedArray(targetW * targetH * 4);
171
+ for (let y = 0; y < targetH; y++) {
172
+ for (let x = 0; x < targetW; x++) {
173
+ const srcX = Math.floor((x * src.width) / targetW);
174
+ const srcY = Math.floor((y * src.height) / targetH);
175
+ const srcIdx = (srcY * src.width + srcX) * 4;
176
+ const targetIdx = (y * targetW + x) * 4;
177
+ target[targetIdx] = src.data[srcIdx];
178
+ target[targetIdx + 1] = src.data[srcIdx + 1];
179
+ target[targetIdx + 2] = src.data[srcIdx + 2];
180
+ target[targetIdx + 3] = src.data[srcIdx + 3];
181
+ }
182
+ }
183
+ return target;
184
+ }
137
185
  /**
138
186
  * Solve a CAPTCHA image using the saturation-based neural network method
139
187
  * @param imgDataUri - Base64 data URI of the captcha image
@@ -147,12 +195,15 @@ export async function solve(imgDataUri) {
147
195
  return solveBitmap(imgDataUri);
148
196
  }
149
197
  const labelTxt = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
150
- const canvas = createCanvas(200, 40);
151
- const ctx = canvas.getContext("2d");
152
- const image = await loadImage(imgDataUri);
153
- ctx.drawImage(image, 0, 0, 200, 40);
154
- const pd = ctx.getImageData(0, 0, 200, 40);
155
- let bls = saturation(pd.data);
198
+ const img = await decodeImage(imgDataUri);
199
+ // Target size: 200x40
200
+ let dataToUse = img.data;
201
+ if (img.width !== 200 || img.height !== 40) {
202
+ dataToUse = resizeImage(img, 200, 40);
203
+ }
204
+ // dataToUse is now RGBA array of 200x40 image
205
+ // saturation expects Uint8ClampedArray or similar
206
+ let bls = saturation(dataToUse);
156
207
  let out = "";
157
208
  for (let i = 0; i < 6; i++) {
158
209
  let block = preImg(bls[i]);
@@ -171,16 +222,17 @@ export async function solve(imgDataUri) {
171
222
  * @returns Solved captcha string
172
223
  */
173
224
  export async function solveBitmap(imgDataUri) {
174
- const canvas = createCanvas(180, 45);
175
- const ctx = canvas.getContext("2d");
176
- const image = await loadImage(imgDataUri);
177
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
178
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
179
- const data = imageData.data;
225
+ const img = await decodeImage(imgDataUri);
226
+ // For bitmap solver, we need 180x45
227
+ // Original code: canvas 180x45, drawImage(image, 0, 0, 180, 45)
228
+ // So we definitely need to resize
229
+ const resizedData = resizeImage(img, 180, 45);
180
230
  // Convert to grayscale 2D array
181
231
  const arr = [];
182
- for (let i = 0; i < data.length; i += 4) {
183
- const gval = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
232
+ for (let i = 0; i < resizedData.length; i += 4) {
233
+ const gval = resizedData[i] * 0.299 +
234
+ resizedData[i + 1] * 0.587 +
235
+ resizedData[i + 2] * 0.114;
184
236
  arr.push(Math.round(gval));
185
237
  }
186
238
  const newArr = [];
package/dist/index.js CHANGED
@@ -3,124 +3,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
3
3
  import { serve } from "@hono/node-server";
4
4
  import { Hono } from "hono";
5
5
  import { serveStatic } from "@hono/node-server/serve-static";
6
- import { Home } from "./views/Home.js";
7
- import { SuccessMessage } from "./views/partials.js";
6
+ import { Login } from "./views/Login.js";
7
+ import { Dashboard } from "./views/Dashboard.js";
8
8
  import { sessionManager } from "./session-manager.js";
9
- import { readFile } from "fs/promises";
10
- import { dirname, join } from "path";
11
- import { fileURLToPath } from "url";
9
+ import { createRequire } from "module";
12
10
  const app = new Hono();
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- // Serve htmx from node_modules - resolve absolute path
15
- const htmxPath = join(__dirname, "../node_modules/htmx.org/dist/htmx.min.js");
16
- app.get("/static/htmx.js", async (c) => {
17
- try {
18
- const content = await readFile(htmxPath);
19
- return c.body(content, 200, {
20
- "Content-Type": "application/javascript",
21
- });
22
- }
23
- catch (e) {
24
- console.error("Failed to serve HTMX:", e);
25
- return c.text("Failed to load HTMX", 500);
26
- }
27
- });
28
- // Main page
11
+ const require = createRequire(import.meta.url);
12
+ const htmxPath = require.resolve("htmx.org/dist/htmx.min.js");
13
+ app.use("/static/htmx.js", serveStatic({ path: htmxPath }));
29
14
  app.get("/", (c) => {
30
- return c.html(_jsx(Home, {}));
31
- });
32
- // API Endpoints
33
- app.get("/api/hello", (c) => {
34
- return c.html(_jsx(SuccessMessage, { message: "Hello from HTMX!" }));
35
- });
36
- // Session status endpoint
37
- app.get("/api/session/status", (c) => {
38
- const state = sessionManager.getState();
39
- const status = state.initialized ? "✅ Initialized" : "❌ Not Initialized";
40
- const lastInit = state.lastInitialized
41
- ? new Date(state.lastInitialized).toLocaleString()
42
- : "Never";
43
- // Redact sensitive values for security
44
- const hasCsrf = !!state.csrf;
45
- const hasJsession = state.cookies.has("JSESSIONID");
46
- const hasServerId = state.cookies.has("SERVERID");
47
- return c.html(_jsxs("div", { style: "font-family: monospace; padding: 1rem; background: #f5f5f5; border-radius: 4px; color: #000;", children: [_jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Status:" }), " ", status] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Last Initialized:" }), " ", lastInit] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "Total Cookies:" }), " ", state.cookies.size] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "CSRF Token:" }), " ", hasCsrf ? "✅ Present" : "❌ Missing"] }), _jsxs("div", { style: "margin-bottom: 0.5rem;", children: [_jsx("strong", { children: "JSESSIONID:" }), " ", hasJsession ? "✅ Present" : "❌ Missing"] }), _jsxs("div", { children: [_jsx("strong", { children: "SERVERID:" }), " ", hasServerId ? "✅ Present" : "❌ Missing"] })] }));
48
- });
49
- // Manual session refresh endpoint
50
- app.post("/api/session/refresh", async (c) => {
51
- try {
52
- await sessionManager.refresh();
53
- return c.json({ success: true, message: "Session refreshed successfully" });
54
- }
55
- catch (error) {
56
- return c.json({ success: false, error: String(error) }, 500);
57
- }
58
- });
59
- // Login endpoint
60
- app.post("/api/login", async (c) => {
61
- try {
62
- const body = await c.req.json();
63
- const { username, password, regNo } = body;
64
- if (!username || !password || !regNo) {
65
- return c.json({
66
- success: false,
67
- error: "Username, password, and registration number are required",
68
- }, 400);
69
- }
70
- console.log(`🔐 Login requested for user: ${username} (${regNo})`);
71
- const success = await sessionManager.login(username, password, regNo);
72
- if (success) {
73
- return c.json({
74
- success: true,
75
- message: "Login successful",
76
- username: sessionManager.getUsername(),
77
- });
78
- }
79
- else {
80
- return c.json({
81
- success: false,
82
- error: "Login failed - check credentials or try again",
83
- }, 401);
84
- }
85
- }
86
- catch (error) {
87
- console.error("Login error:", error);
88
- return c.json({ success: false, error: String(error) }, 500);
89
- }
90
- });
91
- // Login status endpoint
92
- app.get("/api/login/status", (c) => {
93
- return c.json({
94
- loggedIn: sessionManager.isLoggedIn(),
95
- username: sessionManager.getUsername(),
96
- });
97
- });
98
- // Login status HTML endpoint
99
- app.get("/api/login/status/html", (c) => {
100
- const isLoggedIn = sessionManager.isLoggedIn();
101
- const username = sessionManager.getUsername();
102
- if (isLoggedIn) {
103
- return c.html(_jsxs("div", { style: "padding: 1rem; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; color: #155724;", children: [_jsx("strong", { children: "\u2705 Logged In" }), _jsxs("div", { style: "margin-top: 0.5rem;", children: ["Username: ", _jsx("code", { children: username })] })] }));
104
- }
105
- else {
106
- return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Not Logged In" }), _jsx("div", { style: "margin-top: 0.5rem;", children: "Please use the login form above." })] }));
15
+ if (sessionManager.isLoggedIn()) {
16
+ return c.html(_jsx(Dashboard, { username: sessionManager.getUsername() }));
107
17
  }
18
+ return c.html(_jsx(Login, {}));
108
19
  });
109
- // Debug logs storage
110
- const debugLogs = [];
111
- function addDebugLog(level, message) {
112
- // Only log if specifically enabled
113
- if (process.env.DEBUG_LOGS === "true") {
114
- const timestamp = new Date().toLocaleTimeString();
115
- debugLogs.push({ timestamp, level, message });
116
- // Keep only last 100 logs
117
- if (debugLogs.length > 100) {
118
- debugLogs.shift();
119
- }
120
- console.log(`[${level}] ${message}`);
121
- }
122
- }
123
- // Form-based login endpoint (for HTMX)
124
20
  app.post("/api/login/form", async (c) => {
125
21
  try {
126
22
  const formData = await c.req.parseBody();
@@ -128,103 +24,42 @@ app.post("/api/login/form", async (c) => {
128
24
  const password = formData["password"];
129
25
  const regNo = formData["regNo"];
130
26
  if (!username || !password || !regNo) {
131
- addDebugLog("ERROR", "Login attempt with missing credentials");
132
- return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Error:" }), " Username, password, and registration number are required."] }));
27
+ return c.html(_jsx("div", { id: "error-message", class: "mt-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm", children: "Missing credentials. Please try again." }));
133
28
  }
134
- // Don't log credentials even in debug
135
- addDebugLog("INFO", "Login requested");
136
- // Show that login is in progress
137
- const startTime = Date.now();
138
- addDebugLog("INFO", "Starting login process - polling for text CAPTCHA...");
139
29
  const success = await sessionManager.login(username, password, regNo);
140
- const duration = ((Date.now() - startTime) / 1000).toFixed(1);
141
30
  if (success) {
142
- addDebugLog("SUCCESS", `Login successful (took ${duration}s)`);
143
- return c.html(_jsxs("div", { style: "padding: 1rem; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; color: #155724;", children: [_jsx("strong", { children: "\uD83C\uDF89 Login Successful!" }), _jsxs("div", { style: "margin-top: 0.5rem;", children: ["Welcome, ", _jsx("strong", { children: username })] }), _jsxs("div", { style: "margin-top: 0.25rem; font-size: 0.85rem;", children: ["Login took ", duration, " seconds"] })] }));
31
+ return c.html(_jsx(Dashboard, { username: username }));
144
32
  }
145
33
  else {
146
- addDebugLog("ERROR", `Login failed (took ${duration}s)`);
147
- return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Login Failed" }), _jsx("div", { style: "margin-top: 0.5rem;", children: "Check your credentials or try again. VTOP might be showing reCAPTCHA." }), _jsxs("div", { style: "margin-top: 0.25rem; font-size: 0.85rem;", children: ["Attempt took ", duration, " seconds"] })] }));
34
+ return c.html(_jsx("div", { id: "error-message", class: "mt-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm", children: "Login failed. Invalid credentials or captcha error." }));
148
35
  }
149
36
  }
150
37
  catch (error) {
151
- addDebugLog("ERROR", `Login exception: ${String(error)}`);
152
- return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Error:" }), " ", String(error)] }));
153
- }
154
- });
155
- // Debug logs endpoint
156
- app.get("/api/debug/logs", (c) => {
157
- if (process.env.DEBUG_LOGS !== "true") {
158
- return c.html(_jsx("div", { style: "color: #888;", children: "Debug logs are disabled. Set DEBUG_LOGS=true to enable." }));
159
- }
160
- if (debugLogs.length === 0) {
161
- return c.html(_jsx("div", { style: "color: #888;", children: "No logs yet. Try logging in to see debug output." }));
162
- }
163
- return c.html(_jsx("div", { children: debugLogs.map((log, i) => (_jsxs("div", { style: {
164
- color: log.level === "ERROR"
165
- ? "#ff6b6b"
166
- : log.level === "SUCCESS"
167
- ? "#51cf66"
168
- : log.level === "WARN"
169
- ? "#fcc419"
170
- : "#0f0",
171
- marginBottom: "0.25rem",
172
- }, children: [_jsxs("span", { style: "color: #888;", children: ["[", log.timestamp, "]"] }), " ", _jsxs("span", { style: "font-weight: bold;", children: ["[", log.level, "]"] }), " ", log.message] }, i))) }));
173
- });
174
- // Clear debug logs endpoint
175
- app.post("/api/debug/logs/clear", (c) => {
176
- debugLogs.length = 0;
177
- return c.html(_jsx("div", { style: "color: #888;", children: "Logs cleared." }));
178
- });
179
- // Assignments JSON endpoint
180
- app.get("/api/assignments", async (c) => {
181
- if (!sessionManager.isLoggedIn()) {
182
- return c.json({ success: false, error: "Not logged in" }, 401);
183
- }
184
- try {
185
- const assignments = await sessionManager.fetchUpcomingAssignments();
186
- return c.json({ success: true, assignments });
187
- }
188
- catch (error) {
189
- return c.json({ success: false, error: String(error) }, 500);
38
+ return c.html(_jsxs("div", { id: "error-message", class: "mt-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm", children: ["Server error: ", String(error)] }));
190
39
  }
191
40
  });
192
- // Assignments HTML endpoint (for HTMX)
193
41
  app.get("/api/assignments/html", async (c) => {
194
42
  if (!sessionManager.isLoggedIn()) {
195
- return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Not Logged In" }), _jsx("div", { style: "margin-top: 0.5rem;", children: "Please log in first to see your assignments." })] }));
43
+ // If session expired, redirect/render login
44
+ return c.html(_jsx("div", { class: "text-red-500", children: "Session expired. Please refresh to log in again." }));
196
45
  }
197
46
  try {
198
47
  const assignments = await sessionManager.fetchUpcomingAssignments();
199
48
  if (assignments.length === 0) {
200
- return c.html(_jsxs("div", { style: "padding: 1.5rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; color: white; text-align: center;", children: [_jsx("div", { style: "font-size: 3rem; margin-bottom: 0.5rem;", children: "\uD83C\uDF89" }), _jsx("strong", { style: "font-size: 1.25rem;", children: "No Upcoming Assignments!" }), _jsx("div", { style: "margin-top: 0.5rem; opacity: 0.9;", children: "You're all caught up. Time to relax!" })] }));
49
+ return c.html(_jsxs("div", { class: "p-12 text-center bg-surface border border-border rounded-lg", children: [_jsx("h3", { class: "text-lg font-medium mb-2", children: "No Upcoming Assignments" }), _jsx("p", { class: "text-muted", children: "You are all caught up!" })] }));
201
50
  }
202
- return c.html(_jsxs("div", { style: "display: flex; flex-direction: column; gap: 1rem;", children: [_jsxs("div", { style: "padding: 0.75rem 1rem; background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); border-radius: 8px; color: white; font-weight: bold;", children: ["\uD83D\uDCDA ", assignments.length, " Upcoming Assignment", assignments.length > 1 ? "s" : ""] }), assignments.map((ass, i) => (_jsxs("div", { style: {
203
- padding: "1rem",
204
- background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)",
205
- borderRadius: "12px",
206
- border: "1px solid rgba(255,255,255,0.1)",
207
- color: "#fff",
208
- boxShadow: "0 4px 15px rgba(0,0,0,0.2)",
209
- }, children: [_jsxs("div", { style: "display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem;", children: [_jsxs("div", { children: [_jsx("div", { style: "font-size: 0.85rem; color: #64ffda; font-weight: 600;", children: ass.courseCode }), _jsx("div", { style: "font-size: 0.8rem; color: #aaa;", children: ass.courseName })] }), _jsx("div", { style: {
210
- padding: "0.25rem 0.75rem",
211
- background: ass.status?.toLowerCase().includes("pending")
212
- ? "#ff6b6b"
213
- : "#4CAF50",
214
- borderRadius: "20px",
215
- fontSize: "0.75rem",
216
- fontWeight: "bold",
217
- }, children: ass.status || "Pending" })] }), _jsx("div", { style: "font-weight: bold; font-size: 1rem; margin-bottom: 0.5rem;", children: ass.assignmentTitle }), _jsxs("div", { style: "display: flex; justify-content: space-between; font-size: 0.85rem; color: #aaa;", children: [_jsxs("span", { children: ["\uD83D\uDCC5 Due: ", ass.dueDate || "N/A"] }), _jsxs("span", { children: ["\uD83D\uDCCA Max: ", ass.maxMarks || "N/A", " marks"] })] })] }, i)))] }));
51
+ //ass
52
+ return c.html(_jsx("div", { class: "flex flex-col gap-3", children: assignments.map((ass, i) => (_jsxs("div", { class: "flex items-center justify-between p-4 bg-surface border border-border rounded-lg transition-colors hover:border-muted group", children: [_jsxs("div", { class: "flex-1 min-w-0 pr-4", children: [_jsxs("div", { class: "flex items-baseline gap-3 overflow-hidden whitespace-nowrap text-ellipsis", children: [_jsx("span", { class: "text-xs font-bold text-muted uppercase tracking-wider min-w-fit", children: ass.courseCode }), _jsx("span", { class: "font-semibold text-sm truncate", children: ass.assignmentTitle }), _jsxs("span", { class: "text-xs text-muted", children: ["\u2014 ", ass.courseName] })] }), _jsxs("div", { class: "flex gap-4 mt-1 text-xs text-muted", children: [_jsxs("span", { children: ["Due:", " ", _jsx("span", { class: "text-foreground", children: ass.dueDate || "N/A" })] }), _jsxs("span", { children: ["Max:", " ", _jsx("span", { class: "text-foreground", children: ass.maxMarks || "N/A" })] })] })] }), _jsx("span", { class: `text-xs px-2.5 py-1 rounded-full font-medium whitespace-nowrap ${ass.status?.toLowerCase().includes("pending")
53
+ ? "bg-red-500/10 text-red-500 border border-red-500/20"
54
+ : "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] }, i))) }));
218
55
  }
219
56
  catch (error) {
220
- return c.html(_jsxs("div", { style: "padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;", children: [_jsx("strong", { children: "\u274C Error:" }), " ", String(error)] }));
57
+ return c.html(_jsxs("div", { class: "p-4 border border-red-500 rounded-md text-red-500", children: ["Failed to load assignments: ", String(error)] }));
221
58
  }
222
59
  });
223
- const port = Number(process.env.PORT) || 6767;
224
60
  serve({
225
61
  fetch: app.fetch,
226
- port,
62
+ port: 6767,
227
63
  }, (info) => {
228
- console.log(`🚀 Server is running on http://localhost:${info.port}`);
229
- console.log(`📊 Session status: http://localhost:${info.port}/api/session/status`);
64
+ console.log(`Server is running on http://localhost:${info.port}`);
230
65
  });
@@ -10,7 +10,6 @@ const OPEN_PAGE = `${BASE}/vtop/openPage`;
10
10
  const OPEN_PAGE_ALT = `${BASE}/vtop/open/page`;
11
11
  const PRELOGIN_SETUP = `${BASE}/vtop/prelogin/setup`;
12
12
  const LOGIN_PAGE = `${BASE}/vtop/login`;
13
- // Post-login endpoints
14
13
  const INIT_PAGE = `${BASE}/vtop/init/page`;
15
14
  const MAIN_PAGE = `${BASE}/vtop/main/page`;
16
15
  const VTOP_OPEN = `${BASE}/vtop/open`;
@@ -27,9 +26,7 @@ class VTOPSessionManager {
27
26
  username: null,
28
27
  regNo: null,
29
28
  };
30
- /**
31
- * Extract CSRF token from HTML response
32
- */
29
+ //cheerio at home
33
30
  extractCsrf(html) {
34
31
  // Look for _csrf token in various forms
35
32
  const patterns = [
@@ -46,9 +43,6 @@ class VTOPSessionManager {
46
43
  }
47
44
  return null;
48
45
  }
49
- /**
50
- * Parse Set-Cookie headers and store cookies
51
- */
52
46
  storeCookies(response) {
53
47
  const setCookieHeaders = response.headers.getSetCookie?.() || [];
54
48
  for (const cookieStr of setCookieHeaders) {
@@ -59,9 +53,6 @@ class VTOPSessionManager {
59
53
  }
60
54
  }
61
55
  }
62
- /**
63
- * Get cookie header string for requests
64
- */
65
56
  getCookieHeader() {
66
57
  return Array.from(this.state.cookies.entries())
67
58
  .map(([name, value]) => `${name}=${value}`)
@@ -276,7 +267,7 @@ class VTOPSessionManager {
276
267
  solvedCaptcha = await solve(cleanDataUri);
277
268
  console.log("✅ Solved CAPTCHA:", solvedCaptcha);
278
269
  // Save captcha image for debugging
279
- if (parts?.base64 && process.env.DEBUG_CAPTCHA === "true") {
270
+ if (parts?.base64) {
280
271
  const out = path.resolve(process.cwd(), "captcha.jpg");
281
272
  await saveCaptchaImage(parts.base64, out);
282
273
  // console.log(`📷 Saved CAPTCHA image to ${out}`);
@@ -349,25 +340,13 @@ class VTOPSessionManager {
349
340
  : new URL(location, LOGIN_PAGE).toString();
350
341
  await this.fetchWithCookies(redirectUrl);
351
342
  }
352
- // After successfully submitting the captcha, we MUST verify login success
353
- console.log("POST successful, verifying login status...");
354
- // Try to navigate to post-login pages to confirm we are actually logged in
355
- // VTOP is tricky: a 200 OK on login POST doesn't guarantee success (could be wrong password)
356
- const navSuccess = await this.navigatePostLogin();
357
- if (navSuccess) {
358
- console.log("🎉 Login verified successfully!");
359
- this.state.loggedIn = true;
360
- this.state.username = username;
361
- this.state.regNo = regNo;
362
- return true;
363
- }
364
- else {
365
- console.error("❌ Login verification failed - cleaning up session");
366
- this.state.loggedIn = false;
367
- this.state.username = null;
368
- this.state.regNo = null;
369
- return false;
370
- }
343
+ // After successfully submitting the captcha, assume login worked
344
+ // The actual success will be determined when we try to fetch data
345
+ console.log("🎉 Login POST submitted successfully!");
346
+ this.state.loggedIn = true;
347
+ this.state.username = username;
348
+ this.state.regNo = regNo;
349
+ return true;
371
350
  }
372
351
  catch (e) {
373
352
  console.error("Login POST failed:", e);
@@ -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 Dashboard = ({ username }) => {
4
+ return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-8", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-2xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-sm", children: ["Welcome back, ", username] })] }), _jsx("div", {})] }), _jsx("div", { "hx-get": "/api/assignments/html", "hx-trigger": "load", "hx-target": "#assignments-container", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { id: "assignments-container", class: "space-y-4", children: _jsxs("div", { id: "loading-state", class: "text-center py-16 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-6 w-6 border-2 border-muted border-t-white mb-4" }), _jsx("p", { class: "text-sm", children: "Syncing assignments..." })] }) }) })] }));
5
+ };
@@ -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 Login = () => {
4
+ return (_jsx(BaseLayout, { title: "Login - Open-VTOP", children: _jsxs("div", { "x-data": "{ \n showPassword: false, \n loading: false \n }", class: "w-full", children: [_jsxs("div", { class: "text-center mb-8", children: [_jsx("h1", { class: "text-3xl font-bold tracking-tight mb-2", children: "Log In" }), _jsx("p", { class: "text-muted text-sm", children: "Enter your VTOP credentials to continue." })] }), _jsx("div", { class: "bg-surface border border-border rounded-lg p-6 shadow-sm", children: _jsxs("form", { id: "login-form", "hx-post": "/api/login/form", "hx-target": "body", "hx-swap": "outerHTML", "x-on:submit": "loading = true", "x-on:htmx:after-request": "loading = false", children: [_jsxs("div", { class: "mb-4", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "username", children: "Username" }), _jsx("input", { type: "text", id: "username", name: "username", required: true, autofocus: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted" })] }), _jsxs("div", { class: "mb-4", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "password", children: "Password" }), _jsxs("div", { class: "relative", children: [_jsx("input", { "x-bind:type": "showPassword ? 'text' : 'password'", id: "password", name: "password", required: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted pr-10" }), _jsxs("button", { type: "button", "x-on:click": "showPassword = !showPassword", class: "absolute inset-y-0 right-0 px-3 flex items-center text-muted hover:text-foreground transition-colors", children: [_jsx("span", { "x-show": "!showPassword", class: "text-xs font-medium", children: "SHOW" }), _jsx("span", { "x-show": "showPassword", style: "display: none;", class: "text-xs font-medium", children: "HIDE" })] })] })] }), _jsxs("div", { class: "mb-6", children: [_jsx("label", { class: "block text-sm text-muted mb-2", for: "regNo", children: "Registration Number" }), _jsx("input", { type: "text", id: "regNo", name: "regNo", required: true, class: "w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:border-white transition-colors placeholder-muted" })] }), _jsxs("button", { type: "submit", class: "w-full bg-white text-black font-semibold py-2.5 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2", "x-bind:disabled": "loading", children: [_jsx("span", { "x-show": "!loading", children: "Log In" }), _jsxs("span", { "x-show": "loading", style: "display: none;", class: "flex items-center gap-2", children: [_jsxs("svg", { class: "animate-spin h-4 w-4 text-black", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [_jsx("circle", { class: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", "stroke-width": "4" }), _jsx("path", { class: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Authenticating..."] })] }), _jsx("div", { id: "error-message" })] }) }), _jsx("div", { class: "text-center mt-8 text-muted text-xs", children: _jsx("p", { children: "Open-VTOP \u00A9 2026" }) })] }) }));
5
+ };
@@ -1,121 +1,43 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
- export const BaseLayout = ({ title = "Open-VTOP", children, }) => {
3
- return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("script", { src: "/static/htmx.js" }), _jsx("style", { children: `
4
- * {
5
- margin: 0;
6
- padding: 0;
7
- box-sizing: border-box;
8
- }
9
-
2
+ export const BaseLayout = ({ title, children, }) => {
3
+ return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("script", { src: "/static/htmx.js" }), _jsx("script", { src: "https://cdn.tailwindcss.com" }), _jsx("script", { dangerouslySetInnerHTML: {
4
+ __html: `
5
+ tailwind.config = {
6
+ theme: {
7
+ extend: {
8
+ colors: {
9
+ background: '#000000',
10
+ surface: '#111111',
11
+ border: '#333333',
12
+ foreground: '#ffffff',
13
+ muted: '#888888',
14
+ primary: '#ffffff',
15
+ 'primary-fg': '#000000',
16
+ },
17
+ fontFamily: {
18
+ sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
19
+ }
20
+ }
21
+ }
22
+ }
23
+ `,
24
+ } }), _jsx("script", { defer: true, src: "https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js" }), _jsx("style", { dangerouslySetInnerHTML: {
25
+ __html: `
10
26
  body {
11
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Dank Mono', 'Source Code Pro', monospace;
12
- background: #000;
13
- color: #fff;
14
- min-height: 100vh;
15
- padding: 2rem;
16
- line-height: 1.6;
17
- }
18
-
19
- h1 {
20
- font-size: 2rem;
21
- font-weight: 600;
22
- margin-bottom: 2rem;
23
- letter-spacing: -0.02em;
24
- }
25
-
26
- h2 {
27
- font-size: 1.25rem;
28
- font-weight: 500;
29
- margin-bottom: 1rem;
30
- letter-spacing: -0.01em;
31
- }
32
-
33
- .container {
34
- max-width: 800px;
35
- margin: 0 auto;
36
- }
37
-
38
- .section {
39
- background: #111;
40
- border: 1px solid #333;
41
- border-radius: 8px;
42
- padding: 2rem;
43
- margin-bottom: 2rem;
44
- transition: border-color 0.2s ease;
45
- }
46
-
47
- .section:hover {
48
- border-color: #555;
49
- }
50
-
51
- button {
52
- background: #fff;
53
- color: #000;
54
- border: none;
55
- padding: 0.75rem 1.5rem;
56
- border-radius: 6px;
57
- cursor: pointer;
58
- font-size: 0.875rem;
59
- font-family: inherit;
60
- font-weight: 500;
61
- transition: all 0.2s ease;
62
- }
63
-
64
- button:hover {
65
- background: #e6e6e6;
66
- transform: translateY(-1px);
67
- }
68
-
69
- button:active {
70
- transform: translateY(0);
71
- }
72
-
73
- input {
74
- background: #111;
75
- color: #fff;
76
- border: 1px solid #333;
77
- padding: 0.75rem 1rem;
78
- border-radius: 6px;
79
- font-size: 0.875rem;
80
- font-family: inherit;
81
- width: 100%;
82
- transition: border-color 0.2s ease;
83
- }
84
-
85
- input:focus {
86
- outline: none;
87
- border-color: #fff;
88
- }
89
-
90
- input::placeholder {
91
- color: #666;
92
- }
93
-
94
- #result {
95
- margin-top: 1rem;
96
- padding: 1rem;
97
- background: #0a0a0a;
98
- border: 1px solid #222;
99
- border-radius: 6px;
100
- min-height: 2rem;
101
- font-size: 0.875rem;
102
- }
103
-
104
- .success {
105
- color: #0f0;
106
- }
107
-
108
- .error {
109
- color: #f00;
110
- }
111
-
112
- .htmx-swapping {
113
- opacity: 0.5;
114
- transition: opacity 0.2s;
115
- }
116
-
117
- .htmx-request {
118
- opacity: 0.8;
119
- }
120
- ` })] }), _jsx("body", { children: _jsx("div", { class: "container", children: children }) })] }));
27
+ background-color: theme('colors.background');
28
+ color: theme('colors.foreground');
29
+ }
30
+ /* Custom spinner animation since Tailwind's animate-spin is utility based */
31
+ .htmx-indicator {
32
+ display: none;
33
+ opacity: 0;
34
+ transition: opacity 200ms ease-in;
35
+ }
36
+ .htmx-request .htmx-indicator,
37
+ .htmx-request.htmx-indicator {
38
+ display: inline-block;
39
+ opacity: 1;
40
+ }
41
+ `,
42
+ } })] }), _jsx("body", { class: "bg-background text-foreground antialiased min-h-screen flex flex-col items-center justify-center p-4", children: _jsx("main", { class: "w-full max-w-md flex flex-col gap-6", children: children }) })] }));
121
43
  };
package/package.json CHANGED
@@ -4,26 +4,28 @@
4
4
  "scripts": {
5
5
  "dev": "tsx watch src/index.tsx",
6
6
  "build": "tsc",
7
- "start": "node dist/index.js"
7
+ "start": "node dist/index.js",
8
+ "prepare": "npm run build"
8
9
  },
9
10
  "dependencies": {
10
11
  "@hono/node-server": "^1.19.9",
11
- "canvas": "^3.2.1",
12
12
  "hono": "^4.11.4",
13
- "htmx.org": "^2.0.8"
13
+ "htmx.org": "^2.0.8",
14
+ "jpeg-js": "^0.4.4",
15
+ "pngjs": "^7.0.0"
14
16
  },
15
17
  "bin": {
16
18
  "open-vtop": "dist/index.js"
17
19
  },
18
20
  "files": [
19
- "dist",
20
- "README.md",
21
- "LICENSE"
21
+ "dist"
22
22
  ],
23
23
  "devDependencies": {
24
+ "@types/jpeg-js": "^0.3.0",
24
25
  "@types/node": "^20.11.17",
26
+ "@types/pngjs": "^6.0.5",
25
27
  "tsx": "^4.7.1",
26
28
  "typescript": "^5.8.3"
27
29
  },
28
- "version": "1.0.3"
30
+ "version": "1.0.5"
29
31
  }