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 +10 -0
- package/dist/captcha-solver.js +68 -16
- package/dist/index.js +23 -188
- package/dist/session-manager.js +9 -30
- package/dist/views/Dashboard.js +5 -0
- package/dist/views/Login.js +5 -0
- package/dist/views/layouts/Base.js +40 -118
- package/package.json +9 -7
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
|
package/dist/captcha-solver.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CAPTCHA Solver for VTOP
|
|
3
|
-
* Uses
|
|
3
|
+
* Uses pure JS for image processing and bitmap matching / neural network for character recognition
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
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 <
|
|
183
|
-
const gval =
|
|
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 {
|
|
7
|
-
import {
|
|
6
|
+
import { Login } from "./views/Login.js";
|
|
7
|
+
import { Dashboard } from "./views/Dashboard.js";
|
|
8
8
|
import { sessionManager } from "./session-manager.js";
|
|
9
|
-
import {
|
|
10
|
-
import { dirname, join } from "path";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
9
|
+
import { createRequire } from "module";
|
|
12
10
|
const app = new Hono();
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", {
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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", {
|
|
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(
|
|
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
|
});
|
package/dist/session-manager.js
CHANGED
|
@@ -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
|
|
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,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
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("
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
30
|
+
"version": "1.0.5"
|
|
29
31
|
}
|