open-vtop 1.0.6 → 1.0.7
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 +13 -52
- package/dist/cli.cjs +150 -0
- package/dist/components/AssignmentCard.js +6 -0
- package/dist/components/AssignmentsList.js +5 -0
- package/dist/components/CourseCard.js +8 -0
- package/dist/components/CoursesList.js +5 -0
- package/dist/components/EmptyState.js +4 -0
- package/dist/components/ErrorMessage.js +4 -0
- package/dist/components/SessionExpired.js +4 -0
- package/dist/constants.js +82 -0
- package/dist/index.js +69 -25
- package/dist/parsers.js +138 -0
- package/dist/session-manager.js +52 -225
- package/dist/views/Dashboard.js +12 -2
- package/dist/views/Home.js +1 -1
- package/dist/views/Login.js +1 -1
- package/dist/views/layouts/Base.js +2 -2
- package/package.json +7 -4
- package/dist/views/components/Container.js +0 -4
- package/dist/views/components/Item.js +0 -4
package/README.md
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# Open-VTOP
|
|
2
2
|
|
|
3
3
|
An open-source VTOP client with automatic session management.
|
|
4
|
+
```bash
|
|
5
|
+
npx open-vtop
|
|
6
|
+
bunx open-vtop
|
|
4
7
|
|
|
8
|
+
npx open-vtop logs
|
|
9
|
+
bunx open-vtop logs
|
|
10
|
+
```
|
|
5
11
|
## Todo
|
|
6
12
|
1) save usn and password for future use
|
|
7
13
|
2) grab regno from the responses it self
|
|
@@ -10,73 +16,28 @@ An open-source VTOP client with automatic session management.
|
|
|
10
16
|
5) cgpa
|
|
11
17
|
6) course-page
|
|
12
18
|
7) callendar
|
|
13
|
-
|
|
14
|
-
9) qol like automatic browser open, better logging, save usn password for faster logins, logout
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- **Automatic Session Initialization**: VTOP session cookies and CSRF tokens are automatically established when the server starts
|
|
18
|
-
- **Background Processing**: Session setup happens in the background, no need to hit any endpoints first
|
|
19
|
-
- **Session Monitoring**: Check session status in real-time via the web interface
|
|
19
|
+
9) qol like automatic browser open, better logging
|
|
20
20
|
|
|
21
21
|
## Getting Started
|
|
22
22
|
|
|
23
23
|
### Installation
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
|
|
26
|
+
bun i
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### Development
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
|
|
32
|
+
bun start
|
|
33
|
+
bun run logs #for logs
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
Then open http://localhost:
|
|
36
|
+
Then open http://localhost:6767
|
|
36
37
|
|
|
37
38
|
### Production
|
|
38
39
|
|
|
39
40
|
```bash
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## How It Works
|
|
45
|
-
|
|
46
|
-
When the server starts, it automatically:
|
|
47
|
-
|
|
48
|
-
1. **Establishes cookies** - Gets `SERVERID` and `JSESSIONID` cookies from VTOP
|
|
49
|
-
2. **Retrieves CSRF token** - Extracts the `_csrf` token from `/vtop/openPage`
|
|
50
|
-
3. **Completes prelogin setup** - POSTs to `/vtop/prelogin/setup` with flag=VTOP
|
|
51
|
-
4. **Maintains session state** - Stores cookies and tokens for subsequent requests
|
|
52
|
-
|
|
53
|
-
All of this happens in the background using Node.js's native fetch API.
|
|
54
|
-
|
|
55
|
-
## API Endpoints
|
|
56
|
-
|
|
57
|
-
- `GET /api/session/status` - View current session status (cookies, CSRF, etc.)
|
|
58
|
-
- `POST /api/session/refresh` - Manually refresh the VTOP session
|
|
59
|
-
|
|
60
|
-
## Session Manager
|
|
61
|
-
|
|
62
|
-
The session manager (`src/session-manager.ts`) is a singleton that:
|
|
63
|
-
- Initializes automatically on server startup
|
|
64
|
-
- Stores cookies and CSRF tokens
|
|
65
|
-
- Can be imported and used by other modules
|
|
66
|
-
- Supports manual refresh when needed
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { sessionManager } from "./session-manager.js";
|
|
70
|
-
|
|
71
|
-
// Check if initialized
|
|
72
|
-
const isReady = sessionManager.isInitialized();
|
|
73
|
-
|
|
74
|
-
// Get CSRF token
|
|
75
|
-
const csrf = sessionManager.getCsrf();
|
|
76
|
-
|
|
77
|
-
// Get cookies as header string
|
|
78
|
-
const cookies = sessionManager.getCookies();
|
|
79
|
-
|
|
80
|
-
// Manually refresh
|
|
81
|
-
await sessionManager.refresh();
|
|
41
|
+
bun run build
|
|
42
|
+
bun start
|
|
82
43
|
```
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn } = require("child_process");
|
|
3
|
+
const { exec } = require("child_process");
|
|
4
|
+
const { platform } = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const showLogs = args.includes("logs");
|
|
9
|
+
|
|
10
|
+
const colors = {
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
bold: "\x1b[1m",
|
|
13
|
+
dim: "\x1b[2m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
green: "\x1b[32m",
|
|
16
|
+
yellow: "\x1b[33m",
|
|
17
|
+
magenta: "\x1b[35m",
|
|
18
|
+
blue: "\x1b[34m",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PORT = 6767;
|
|
22
|
+
const URL = `http://localhost:${PORT}`;
|
|
23
|
+
|
|
24
|
+
function openBrowser(url) {
|
|
25
|
+
const plat = platform();
|
|
26
|
+
let command;
|
|
27
|
+
|
|
28
|
+
switch (plat) {
|
|
29
|
+
case "darwin":
|
|
30
|
+
command = `open "${url}"`;
|
|
31
|
+
break;
|
|
32
|
+
case "win32":
|
|
33
|
+
command = `start "" "${url}"`;
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
command = `xdg-open "${url}"`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
exec(command, (error) => {
|
|
40
|
+
if (error) {
|
|
41
|
+
console.log(
|
|
42
|
+
`${colors.yellow}Could not open browser automatically. Please visit: ${url}${colors.reset}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printBanner() {
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(
|
|
51
|
+
`${colors.cyan}${colors.bold} ┌───────────────────────────────────────┐${colors.reset}`,
|
|
52
|
+
);
|
|
53
|
+
console.log(
|
|
54
|
+
`${colors.cyan}${colors.bold} │ ${colors.magenta}open-vtop${colors.cyan} │${colors.reset}`,
|
|
55
|
+
);
|
|
56
|
+
console.log(
|
|
57
|
+
`${colors.cyan}${colors.bold} └───────────────────────────────────────┘${colors.reset}`,
|
|
58
|
+
);
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(
|
|
61
|
+
` ${colors.green}✓${colors.reset} Server running at ${colors.bold}${URL}${colors.reset}`,
|
|
62
|
+
);
|
|
63
|
+
console.log();
|
|
64
|
+
console.log(` ${colors.dim}Keyboard shortcuts:${colors.reset}`);
|
|
65
|
+
console.log(` ${colors.bold}q${colors.reset} → Quit server`);
|
|
66
|
+
console.log(` ${colors.bold}o${colors.reset} → Open browser`);
|
|
67
|
+
console.log(` ${colors.bold}c${colors.reset} → Clear console`);
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let serverProcess = null;
|
|
72
|
+
|
|
73
|
+
function shutdown() {
|
|
74
|
+
console.log(`\n${colors.yellow}Shutting down server...${colors.reset}`);
|
|
75
|
+
if (serverProcess) {
|
|
76
|
+
serverProcess.kill("SIGTERM");
|
|
77
|
+
}
|
|
78
|
+
// Restore terminal to normal mode before exiting
|
|
79
|
+
if (process.stdin.isTTY) {
|
|
80
|
+
process.stdin.setRawMode(false);
|
|
81
|
+
}
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setupKeyboardShortcuts() {
|
|
86
|
+
if (process.stdin.isTTY) {
|
|
87
|
+
process.stdin.setRawMode(true);
|
|
88
|
+
process.stdin.resume();
|
|
89
|
+
process.stdin.setEncoding("utf8");
|
|
90
|
+
|
|
91
|
+
process.stdin.on("data", (key) => {
|
|
92
|
+
switch (key.toLowerCase()) {
|
|
93
|
+
case "q":
|
|
94
|
+
case "\u0003": // Ctrl+C
|
|
95
|
+
shutdown();
|
|
96
|
+
break;
|
|
97
|
+
case "o":
|
|
98
|
+
console.log(`${colors.dim}Opening browser...${colors.reset}`);
|
|
99
|
+
openBrowser(URL);
|
|
100
|
+
break;
|
|
101
|
+
case "c":
|
|
102
|
+
console.clear();
|
|
103
|
+
printBanner();
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
process.on("SIGINT", shutdown);
|
|
111
|
+
process.on("SIGTERM", shutdown);
|
|
112
|
+
|
|
113
|
+
const serverPath = path.join(__dirname, "index.js");
|
|
114
|
+
serverProcess = spawn(process.execPath, [serverPath], {
|
|
115
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
+
env: { ...process.env, OPEN_VTOP_CLI: "true" },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
serverProcess.stdout.on("data", (data) => {
|
|
120
|
+
const text = data.toString();
|
|
121
|
+
if (text.includes("Server is running")) {
|
|
122
|
+
printBanner();
|
|
123
|
+
setupKeyboardShortcuts();
|
|
124
|
+
openBrowser(URL);
|
|
125
|
+
} else if (showLogs) {
|
|
126
|
+
process.stdout.write(data);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
serverProcess.stderr.on("data", (data) => {
|
|
131
|
+
if (showLogs) {
|
|
132
|
+
process.stderr.write(data);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
serverProcess.on("error", (err) => {
|
|
137
|
+
console.error(
|
|
138
|
+
`${colors.yellow}Failed to start server: ${err.message}${colors.reset}`,
|
|
139
|
+
);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
serverProcess.on("exit", (code) => {
|
|
144
|
+
if (code !== 0 && code !== null) {
|
|
145
|
+
console.log(
|
|
146
|
+
`${colors.yellow}Server exited with code ${code}${colors.reset}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
process.exit(code || 0);
|
|
150
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
export function AssignmentCard({ assignment }) {
|
|
3
|
+
return (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg hover:border-muted transition-colors group flex flex-col gap-1", children: [_jsxs("div", { class: "flex justify-between items-start gap-2", children: [_jsx("span", { class: "font-bold text-xs text-foreground line-clamp-1", title: assignment.courseName, children: assignment.courseName }), _jsxs("span", { class: "text-[0.65rem] font-medium text-red-400 whitespace-nowrap shrink-0", children: ["Due: ", assignment.dueDate || "N/A"] })] }), _jsxs("div", { class: "flex justify-between items-end gap-2", children: [_jsxs("div", { class: "flex flex-col min-w-0", children: [_jsx("span", { class: "text-[0.7rem] text-muted truncate", title: assignment.assignmentTitle, children: assignment.assignmentTitle }), _jsx("span", { class: "text-[0.6rem] text-muted/60 font-mono", children: assignment.courseCode })] }), _jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded font-medium whitespace-nowrap shrink-0 ${assignment.status?.toLowerCase().includes("pending")
|
|
4
|
+
? "bg-red-500/10 text-red-500 border border-red-500/20"
|
|
5
|
+
: "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: assignment.status || "Pending" })] })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { AssignmentCard } from "./AssignmentCard.js";
|
|
3
|
+
export function AssignmentsList({ assignments, }) {
|
|
4
|
+
return (_jsx("div", { class: "flex flex-col gap-2", children: assignments.map((assignment, i) => (_jsx(AssignmentCard, { assignment: assignment }, i))) }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
export function CourseCard({ course }) {
|
|
3
|
+
return (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg flex flex-col justify-between h-full hover:border-muted transition-colors", children: [_jsxs("div", { children: [_jsxs("div", { class: "flex justify-between items-start mb-1.5", children: [_jsx("span", { class: "text-[0.65rem] font-bold text-muted uppercase tracking-wider", children: course.code }), _jsx("span", { class: "text-[0.6rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type })] }), _jsx("h4", { class: "font-semibold text-xs mb-2 leading-relaxed line-clamp-2", children: course.name })] }), _jsxs("div", { class: "flex items-end justify-between mt-2 pt-2 border-t border-border/50", children: [_jsxs("div", { class: "flex flex-col", children: [_jsx("span", { class: "text-[0.6rem] text-muted uppercase", children: "Attendance" }), _jsxs("span", { class: `text-base font-bold ${course.attendanceColor === "danger"
|
|
4
|
+
? "text-red-500"
|
|
5
|
+
: course.attendanceColor === "warning"
|
|
6
|
+
? "text-yellow-500"
|
|
7
|
+
: "text-green-500"}`, children: [course.attendance, "%"] })] }), course.remarks && (_jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded bg-${course.attendanceColor === "danger" ? "red" : "green"}-500/10 text-${course.attendanceColor === "danger" ? "red" : "green"}-500`, children: course.remarks }))] })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { CourseCard } from "./CourseCard.js";
|
|
3
|
+
export function CoursesList({ courses }) {
|
|
4
|
+
return (_jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3", children: courses.map((course, i) => (_jsx(CourseCard, { course: course }, i))) }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// VTOP API endpoints and constants
|
|
2
|
+
export const BASE = "https://vtop.vit.ac.in";
|
|
3
|
+
export const VTOP = `${BASE}/vtop/`;
|
|
4
|
+
export const OPEN_PAGE = `${BASE}/vtop/openPage`;
|
|
5
|
+
export const OPEN_PAGE_ALT = `${BASE}/vtop/open/page`;
|
|
6
|
+
export const PRELOGIN_SETUP = `${BASE}/vtop/prelogin/setup`;
|
|
7
|
+
export const LOGIN_PAGE = `${BASE}/vtop/login`;
|
|
8
|
+
export const INIT_PAGE = `${BASE}/vtop/init/page`;
|
|
9
|
+
export const MAIN_PAGE = `${BASE}/vtop/main/page`;
|
|
10
|
+
export const VTOP_OPEN = `${BASE}/vtop/open`;
|
|
11
|
+
export const CONTENT = `${BASE}/vtop/content`;
|
|
12
|
+
export const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
|
|
13
|
+
export const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
|
|
14
|
+
export const COURSE_DETAILS = `${BASE}/vtop/get/dashboard/current/semester/course/details`;
|
|
15
|
+
// HTTP Headers
|
|
16
|
+
// Common browser headers
|
|
17
|
+
export const USER_AGENT_CHROME = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
18
|
+
export const USER_AGENT_CHROME_139 = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36";
|
|
19
|
+
export const USER_AGENT_CHROME_140 = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
20
|
+
export const ACCEPT_HTML = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
|
|
21
|
+
export const ACCEPT_HTML_EXTENDED = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8";
|
|
22
|
+
export const ACCEPT_ALL = "*/*";
|
|
23
|
+
export const ACCEPT_LANGUAGE = "en-US,en;q=0.9";
|
|
24
|
+
export const ACCEPT_LANGUAGE_ALT = "en-US,en;q=0.7";
|
|
25
|
+
export const ACCEPT_ENCODING = "gzip, deflate, br";
|
|
26
|
+
export const ACCEPT_ENCODING_ZSTD = "gzip, deflate, br, zstd";
|
|
27
|
+
// Standard headers for fetch requests
|
|
28
|
+
export const BROWSER_HEADERS = {
|
|
29
|
+
"User-Agent": USER_AGENT_CHROME,
|
|
30
|
+
Accept: ACCEPT_HTML,
|
|
31
|
+
"Accept-Language": ACCEPT_LANGUAGE,
|
|
32
|
+
"Accept-Encoding": ACCEPT_ENCODING,
|
|
33
|
+
Connection: "keep-alive",
|
|
34
|
+
"Upgrade-Insecure-Requests": "1",
|
|
35
|
+
};
|
|
36
|
+
// Headers for login POST request
|
|
37
|
+
export const LOGIN_POST_HEADERS = {
|
|
38
|
+
"Cache-Control": "max-age=0",
|
|
39
|
+
Origin: BASE,
|
|
40
|
+
Referer: OPEN_PAGE_ALT,
|
|
41
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
42
|
+
Accept: ACCEPT_HTML_EXTENDED,
|
|
43
|
+
"Accept-Encoding": ACCEPT_ENCODING_ZSTD,
|
|
44
|
+
"Accept-Language": ACCEPT_LANGUAGE,
|
|
45
|
+
"Sec-Fetch-Dest": "document",
|
|
46
|
+
"Sec-Fetch-Mode": "navigate",
|
|
47
|
+
"Sec-Fetch-Site": "same-origin",
|
|
48
|
+
"Sec-Fetch-User": "?1",
|
|
49
|
+
"Upgrade-Insecure-Requests": "1",
|
|
50
|
+
Priority: "u=0, i",
|
|
51
|
+
};
|
|
52
|
+
// Headers for post-login navigation
|
|
53
|
+
export const POST_LOGIN_HEADERS = {
|
|
54
|
+
Referer: LOGIN_PAGE,
|
|
55
|
+
"User-Agent": USER_AGENT_CHROME_140,
|
|
56
|
+
Accept: ACCEPT_HTML_EXTENDED,
|
|
57
|
+
"Accept-Encoding": ACCEPT_ENCODING_ZSTD,
|
|
58
|
+
"Cache-Control": "max-age=0",
|
|
59
|
+
"Upgrade-Insecure-Requests": "1",
|
|
60
|
+
Origin: BASE,
|
|
61
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
62
|
+
};
|
|
63
|
+
// Headers for API requests (assignments, courses)
|
|
64
|
+
export const API_REQUEST_HEADERS = {
|
|
65
|
+
Accept: ACCEPT_ALL,
|
|
66
|
+
"Accept-Encoding": ACCEPT_ENCODING_ZSTD,
|
|
67
|
+
"Accept-Language": ACCEPT_LANGUAGE_ALT,
|
|
68
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
69
|
+
Origin: BASE,
|
|
70
|
+
Priority: "u=1, i",
|
|
71
|
+
Referer: CONTENT,
|
|
72
|
+
"Sec-Fetch-Dest": "empty",
|
|
73
|
+
"Sec-Fetch-Mode": "cors",
|
|
74
|
+
"Sec-Fetch-Site": "same-origin",
|
|
75
|
+
"User-Agent": USER_AGENT_CHROME_139,
|
|
76
|
+
};
|
|
77
|
+
// Headers for academics check
|
|
78
|
+
export const ACADEMICS_CHECK_HEADERS = {
|
|
79
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
80
|
+
Referer: CONTENT,
|
|
81
|
+
"User-Agent": USER_AGENT_CHROME_139,
|
|
82
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
3
2
|
import { serve } from "@hono/node-server";
|
|
4
3
|
import { Hono } from "hono";
|
|
4
|
+
import { streamSSE } from "hono/streaming";
|
|
5
5
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
6
6
|
import { Login } from "./views/Login.js";
|
|
7
7
|
import { Dashboard } from "./views/Dashboard.js";
|
|
8
8
|
import { sessionManager } from "./session-manager.js";
|
|
9
9
|
import { createRequire } from "module";
|
|
10
|
+
import { ErrorMessage } from "./components/ErrorMessage.js";
|
|
11
|
+
import { CoursesList } from "./components/CoursesList.js";
|
|
12
|
+
import { AssignmentsList } from "./components/AssignmentsList.js";
|
|
13
|
+
import { EmptyState } from "./components/EmptyState.js";
|
|
14
|
+
import { SessionExpired } from "./components/SessionExpired.js";
|
|
10
15
|
const app = new Hono();
|
|
11
16
|
const require = createRequire(import.meta.url);
|
|
12
17
|
const htmxPath = require.resolve("htmx.org/dist/htmx.min.js");
|
|
18
|
+
const ssePath = require.resolve("htmx-ext-sse/sse.js");
|
|
13
19
|
app.use("/static/htmx.js", serveStatic({ path: htmxPath }));
|
|
20
|
+
app.use("/static/sse.js", serveStatic({ path: ssePath }));
|
|
14
21
|
app.get("/", (c) => {
|
|
15
22
|
if (sessionManager.isLoggedIn()) {
|
|
16
23
|
return c.html(_jsx(Dashboard, { username: sessionManager.getUsername() }));
|
|
17
24
|
}
|
|
25
|
+
return c.redirect("/login");
|
|
26
|
+
});
|
|
27
|
+
app.get("/login", (c) => {
|
|
28
|
+
if (sessionManager.isLoggedIn()) {
|
|
29
|
+
return c.redirect("/");
|
|
30
|
+
}
|
|
18
31
|
return c.html(_jsx(Login, {}));
|
|
19
32
|
});
|
|
20
33
|
app.post("/api/login/form", async (c) => {
|
|
@@ -22,58 +35,89 @@ app.post("/api/login/form", async (c) => {
|
|
|
22
35
|
const formData = await c.req.parseBody();
|
|
23
36
|
const username = formData["username"];
|
|
24
37
|
const password = formData["password"];
|
|
25
|
-
|
|
26
|
-
|
|
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." }));
|
|
38
|
+
if (!username || !password) {
|
|
39
|
+
return c.html(_jsx(ErrorMessage, { message: "Missing credentials. Please try again." }));
|
|
28
40
|
}
|
|
29
|
-
const success = await sessionManager.login(username, password
|
|
41
|
+
const success = await sessionManager.login(username, password);
|
|
30
42
|
if (success) {
|
|
31
|
-
|
|
43
|
+
await sessionManager.navigatePostLogin();
|
|
44
|
+
await sessionManager.performAcademicsCheck();
|
|
45
|
+
const courses = await sessionManager.fetchCourseDetails();
|
|
46
|
+
const assignments = await sessionManager.fetchUpcomingAssignments();
|
|
47
|
+
c.header("HX-Push-Url", "/");
|
|
48
|
+
return c.html(_jsx(Dashboard, { username: username, courses: courses, assignments: assignments }));
|
|
32
49
|
}
|
|
33
50
|
else {
|
|
34
|
-
return c.html(_jsx(
|
|
51
|
+
return c.html(_jsx(ErrorMessage, { message: "Login failed." }));
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
catch (error) {
|
|
38
|
-
return c.html(
|
|
55
|
+
return c.html(_jsx(ErrorMessage, { message: `Server error: ${String(error)}` }));
|
|
39
56
|
}
|
|
40
57
|
});
|
|
41
|
-
|
|
58
|
+
app.get("/api/login/events", async (c) => {
|
|
59
|
+
return streamSSE(c, async (stream) => {
|
|
60
|
+
console.log("SSE connected");
|
|
61
|
+
let keepOpen = true;
|
|
62
|
+
const onLog = async (msg) => {
|
|
63
|
+
console.log("SSE log:", msg);
|
|
64
|
+
await stream.writeSSE({
|
|
65
|
+
data: msg,
|
|
66
|
+
event: "log",
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
const onComplete = async () => {
|
|
70
|
+
console.log("SSE complete");
|
|
71
|
+
await stream.writeSSE({
|
|
72
|
+
data: "done",
|
|
73
|
+
event: "login-complete",
|
|
74
|
+
});
|
|
75
|
+
keepOpen = false;
|
|
76
|
+
};
|
|
77
|
+
sessionManager.events.on("log", onLog);
|
|
78
|
+
sessionManager.events.once("login-complete", onComplete);
|
|
79
|
+
stream.onAbort(() => {
|
|
80
|
+
console.log("SSE aborted");
|
|
81
|
+
sessionManager.events.off("log", onLog);
|
|
82
|
+
sessionManager.events.off("login-complete", onComplete);
|
|
83
|
+
keepOpen = false;
|
|
84
|
+
});
|
|
85
|
+
while (keepOpen) {
|
|
86
|
+
// Keep the stream open
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
88
|
+
}
|
|
89
|
+
sessionManager.events.off("log", onLog);
|
|
90
|
+
sessionManager.events.off("login-complete", onComplete);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
42
93
|
app.get("/api/courses/html", async (c) => {
|
|
43
94
|
if (!sessionManager.isLoggedIn()) {
|
|
44
|
-
return c.html(_jsx(
|
|
95
|
+
return c.html(_jsx(SessionExpired, {}));
|
|
45
96
|
}
|
|
46
97
|
try {
|
|
47
98
|
const courses = await sessionManager.fetchCourseDetails();
|
|
48
99
|
if (courses.length === 0) {
|
|
49
|
-
return c.html(_jsx(
|
|
100
|
+
return c.html(_jsx(EmptyState, { message: "No course details found." }));
|
|
50
101
|
}
|
|
51
|
-
return c.html(_jsx(
|
|
52
|
-
? "text-red-500"
|
|
53
|
-
: course.attendanceColor === "warning"
|
|
54
|
-
? "text-yellow-500"
|
|
55
|
-
: "text-green-500"}`, children: [course.attendance, "%"] })] }), course.remarks && (_jsx("span", { class: `text-xs px-2 py-1 rounded bg-${course.attendanceColor === "danger" ? "red" : "green"}-500/10 text-${course.attendanceColor === "danger" ? "red" : "green"}-500`, children: course.remarks }))] })] }, i))) }));
|
|
102
|
+
return c.html(_jsx(CoursesList, { courses: courses }));
|
|
56
103
|
}
|
|
57
104
|
catch (error) {
|
|
58
|
-
return c.html(
|
|
105
|
+
return c.html(_jsx(ErrorMessage, { message: `Failed to load courses: ${String(error)}` }));
|
|
59
106
|
}
|
|
60
107
|
});
|
|
61
108
|
app.get("/api/assignments/html", async (c) => {
|
|
62
109
|
if (!sessionManager.isLoggedIn()) {
|
|
63
|
-
|
|
64
|
-
return c.html(_jsx("div", { class: "text-red-500", children: "Session expired. Please refresh to log in again." }));
|
|
110
|
+
return c.html(_jsx(SessionExpired, {}));
|
|
65
111
|
}
|
|
66
112
|
try {
|
|
67
113
|
const assignments = await sessionManager.fetchUpcomingAssignments();
|
|
68
114
|
if (assignments.length === 0) {
|
|
69
|
-
return c.html(
|
|
115
|
+
return c.html(_jsx(EmptyState, { message: "No assignments pending." }));
|
|
70
116
|
}
|
|
71
|
-
return c.html(_jsx(
|
|
72
|
-
? "bg-red-500/10 text-red-500 border border-red-500/20"
|
|
73
|
-
: "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] }, i))) }));
|
|
117
|
+
return c.html(_jsx(AssignmentsList, { assignments: assignments }));
|
|
74
118
|
}
|
|
75
119
|
catch (error) {
|
|
76
|
-
return c.html(
|
|
120
|
+
return c.html(_jsx(ErrorMessage, { message: `Failed to load assignments: ${String(error)}` }));
|
|
77
121
|
}
|
|
78
122
|
});
|
|
79
123
|
serve({
|
package/dist/parsers.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts CSRF token from HTML
|
|
3
|
+
*/
|
|
4
|
+
export function extractCsrf(html) {
|
|
5
|
+
const patterns = [
|
|
6
|
+
/name="_csrf"\s+value="([^"]+)"/,
|
|
7
|
+
/name='_csrf'\s+value='([^']+)'/,
|
|
8
|
+
/<input[^>]*name="_csrf"[^>]*value="([^"]+)"/,
|
|
9
|
+
/<input[^>]*value="([^"]+)"[^>]*name="_csrf"/,
|
|
10
|
+
];
|
|
11
|
+
for (const pattern of patterns) {
|
|
12
|
+
const match = html.match(pattern);
|
|
13
|
+
if (match && match[1]) {
|
|
14
|
+
return match[1];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extracts registration number (authorizedIDX) from HTML
|
|
21
|
+
*/
|
|
22
|
+
export function extractRegNo(html) {
|
|
23
|
+
const patterns = [
|
|
24
|
+
/name="authorizedIDX"\s+id="authorizedIDX"\s+value="([^"]+)"/,
|
|
25
|
+
/id="authorizedIDX"\s+name="authorizedIDX"\s+value="([^"]+)"/,
|
|
26
|
+
/<input[^>]*name="authorizedIDX"[^>]*value="([^"]+)"/,
|
|
27
|
+
/<input[^>]*id="authorizedIDX"[^>]*value="([^"]+)"/,
|
|
28
|
+
];
|
|
29
|
+
for (const pattern of patterns) {
|
|
30
|
+
const match = html.match(pattern);
|
|
31
|
+
if (match && match[1]) {
|
|
32
|
+
return match[1];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function detectCaptcha(html) {
|
|
38
|
+
const recaptchaDom = html.includes('id="recaptcha"') ||
|
|
39
|
+
html.includes('id="g-recaptcha"') ||
|
|
40
|
+
html.includes('class="g-recaptcha"');
|
|
41
|
+
const recaptchaJs = html.includes("var captchaType=2");
|
|
42
|
+
const isRecaptcha = recaptchaDom || recaptchaJs;
|
|
43
|
+
const hasCaptchaInput = html.includes('name="captchaStr"') || html.includes('id="captchaStr"');
|
|
44
|
+
const imgDataUriMatches = [
|
|
45
|
+
...html.matchAll(/src=["'](data:image\/[^"']+)["']/gi),
|
|
46
|
+
];
|
|
47
|
+
let imgDataUri = null;
|
|
48
|
+
for (const match of imgDataUriMatches) {
|
|
49
|
+
if (match[1] && !match[1].includes(";base64,null")) {
|
|
50
|
+
imgDataUri = match[1];
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!imgDataUri && imgDataUriMatches.length > 0) {
|
|
55
|
+
console.warn("Detected invalid captcha image (base64 is null)");
|
|
56
|
+
}
|
|
57
|
+
const isTextCaptcha = hasCaptchaInput && imgDataUri !== null;
|
|
58
|
+
const csrf = extractCsrf(html);
|
|
59
|
+
return { isTextCaptcha, isRecaptcha, csrf, imgDataUri };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parses assignments from HTML response
|
|
63
|
+
*/
|
|
64
|
+
export function parseAssignmentsHtml(html) {
|
|
65
|
+
const assignments = [];
|
|
66
|
+
try {
|
|
67
|
+
const jsonMatch = html.match(/\[[\s\S]*?\]/);
|
|
68
|
+
if (jsonMatch) {
|
|
69
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
70
|
+
if (Array.isArray(parsed)) {
|
|
71
|
+
return parsed.map((item) => ({
|
|
72
|
+
courseCode: String(item.courseCode || item.code || ""),
|
|
73
|
+
courseName: String(item.courseName || item.name || ""),
|
|
74
|
+
assignmentTitle: String(item.assignmentTitle || item.title || item.assignmentName || ""),
|
|
75
|
+
dueDate: String(item.dueDate || item.endDate || item.deadline || ""),
|
|
76
|
+
status: String(item.status || ""),
|
|
77
|
+
maxMarks: String(item.maxMarks || item.marks || ""),
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
84
|
+
const cellRegex = /<?<t[hd][^>]*>([\s\S]*?)(?:<\/t[hd]>|<\/tr|<tr|$)/gi;
|
|
85
|
+
let rowMatch;
|
|
86
|
+
while ((rowMatch = rowRegex.exec(html)) !== null) {
|
|
87
|
+
const cells = [];
|
|
88
|
+
let cellMatch;
|
|
89
|
+
const rowContent = rowMatch[1];
|
|
90
|
+
cellRegex.lastIndex = 0;
|
|
91
|
+
while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
|
|
92
|
+
// Strip HTML tags from cell content
|
|
93
|
+
const cellContent = cellMatch[1].replace(/<[^>]+>/g, "").trim();
|
|
94
|
+
cells.push(cellContent);
|
|
95
|
+
}
|
|
96
|
+
if (cells.length >= 4 && cells[0] !== "#" && !isNaN(Number(cells[0]))) {
|
|
97
|
+
assignments.push({
|
|
98
|
+
courseCode: "", // Not in this table format
|
|
99
|
+
courseName: cells[1] || "",
|
|
100
|
+
assignmentTitle: cells[2] || "",
|
|
101
|
+
dueDate: cells[3] || "",
|
|
102
|
+
status: cells[4] || "Pending",
|
|
103
|
+
maxMarks: "", // Not in this table format
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return assignments;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Parses course details from HTML response
|
|
111
|
+
*/
|
|
112
|
+
export function parseCourseDetailsHtml(html) {
|
|
113
|
+
const courses = [];
|
|
114
|
+
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/g;
|
|
115
|
+
const matches = [...html.matchAll(rowRegex)];
|
|
116
|
+
for (let i = 1; i < matches.length; i++) {
|
|
117
|
+
const rowContent = matches[i][1];
|
|
118
|
+
const codeMatch = rowContent.match(/<span[^>]*text-dark fw-bold[^>]*>([^<]+)<\/span>/);
|
|
119
|
+
const nameMatch = rowContent.match(/-[\s\n]*<span[^>]*text-dark[^>]*>([^<]+)<\/span>/);
|
|
120
|
+
const typeMatch = rowContent.match(/<td[^>]*fst-italic[^>]*>([^<]+)<\/td>/);
|
|
121
|
+
const attendanceMatch = rowContent.match(/<span class="text-([a-z]+)[^>]*fw-bold">([\d.]+)<\/span>/);
|
|
122
|
+
const remarksMatch = rowContent.match(/<span class="text-[^>]*>([^<]+)<\/span>[\s\n]*<\/td>[\s\n]*<\/tr>$/) ||
|
|
123
|
+
rowContent.match(/<td[^>]*text-nowrap text-start[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/);
|
|
124
|
+
if (codeMatch && nameMatch) {
|
|
125
|
+
courses.push({
|
|
126
|
+
code: codeMatch[1].trim(),
|
|
127
|
+
name: nameMatch[1].trim(),
|
|
128
|
+
type: typeMatch ? typeMatch[1].trim() : "N/A",
|
|
129
|
+
attendance: attendanceMatch ? attendanceMatch[2].trim() : "N/A",
|
|
130
|
+
attendanceColor: attendanceMatch
|
|
131
|
+
? attendanceMatch[1].trim()
|
|
132
|
+
: "secondary",
|
|
133
|
+
remarks: remarksMatch ? remarksMatch[1].trim() : "",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return courses;
|
|
138
|
+
}
|
package/dist/session-manager.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
//seperations of concerns my arse
|
|
2
|
+
//nvm i did sepearations of concerns
|
|
2
3
|
import { solve, extractDataUriParts, saveCaptchaImage, } from "./captcha-solver.js";
|
|
3
4
|
import * as path from "path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const OPEN_PAGE_ALT = `${BASE}/vtop/open/page`;
|
|
8
|
-
const PRELOGIN_SETUP = `${BASE}/vtop/prelogin/setup`;
|
|
9
|
-
const LOGIN_PAGE = `${BASE}/vtop/login`;
|
|
10
|
-
const INIT_PAGE = `${BASE}/vtop/init/page`;
|
|
11
|
-
const MAIN_PAGE = `${BASE}/vtop/main/page`;
|
|
12
|
-
const VTOP_OPEN = `${BASE}/vtop/open`;
|
|
13
|
-
const CONTENT = `${BASE}/vtop/content`;
|
|
14
|
-
const ACADEMICS_CHECK = `${BASE}/vtop/academics/common/AcademicsDefaultCheck`;
|
|
15
|
-
const UPCOMING_ASSIGNMENTS = `${BASE}/vtop/get/upcoming/digital/assignments`;
|
|
16
|
-
const COURSE_DETAILS = `${BASE}/vtop/get/dashboard/current/semester/course/details`;
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
import { BASE, VTOP, OPEN_PAGE, OPEN_PAGE_ALT, PRELOGIN_SETUP, LOGIN_PAGE, INIT_PAGE, MAIN_PAGE, VTOP_OPEN, CONTENT, ACADEMICS_CHECK, UPCOMING_ASSIGNMENTS, COURSE_DETAILS, BROWSER_HEADERS, LOGIN_POST_HEADERS, POST_LOGIN_HEADERS, API_REQUEST_HEADERS, ACADEMICS_CHECK_HEADERS, } from "./constants.js";
|
|
7
|
+
import { extractCsrf, extractRegNo, detectCaptcha, parseAssignmentsHtml, parseCourseDetailsHtml, } from "./parsers.js";
|
|
17
8
|
class VTOPSessionManager {
|
|
18
9
|
state = {
|
|
19
10
|
cookies: new Map(),
|
|
@@ -24,22 +15,7 @@ class VTOPSessionManager {
|
|
|
24
15
|
username: null,
|
|
25
16
|
regNo: null,
|
|
26
17
|
};
|
|
27
|
-
|
|
28
|
-
extractCsrf(html) {
|
|
29
|
-
const patterns = [
|
|
30
|
-
/name="_csrf"\s+value="([^"]+)"/,
|
|
31
|
-
/name='_csrf'\s+value='([^']+)'/,
|
|
32
|
-
/<input[^>]*name="_csrf"[^>]*value="([^"]+)"/,
|
|
33
|
-
/<input[^>]*value="([^"]+)"[^>]*name="_csrf"/,
|
|
34
|
-
];
|
|
35
|
-
for (const pattern of patterns) {
|
|
36
|
-
const match = html.match(pattern);
|
|
37
|
-
if (match && match[1]) {
|
|
38
|
-
return match[1];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
18
|
+
events = new EventEmitter();
|
|
43
19
|
storeCookies(response) {
|
|
44
20
|
const setCookieHeaders = response.headers.getSetCookie?.() || [];
|
|
45
21
|
for (const cookieStr of setCookieHeaders) {
|
|
@@ -53,7 +29,7 @@ class VTOPSessionManager {
|
|
|
53
29
|
getCookieHeader() {
|
|
54
30
|
return Array.from(this.state.cookies.entries())
|
|
55
31
|
.map(([name, value]) => `${name}=${value}`)
|
|
56
|
-
.join("; ");
|
|
32
|
+
.join("; "); //vvvimp
|
|
57
33
|
}
|
|
58
34
|
async fetchWithCookies(url, options = {}) {
|
|
59
35
|
const cookieHeader = this.getCookieHeader();
|
|
@@ -61,16 +37,13 @@ class VTOPSessionManager {
|
|
|
61
37
|
if (cookieHeader) {
|
|
62
38
|
headers.set("Cookie", cookieHeader);
|
|
63
39
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
headers.set("Accept-Encoding", "gzip, deflate, br");
|
|
68
|
-
headers.set("Connection", "keep-alive");
|
|
69
|
-
headers.set("Upgrade-Insecure-Requests", "1");
|
|
40
|
+
Object.entries(BROWSER_HEADERS).forEach(([key, value]) => {
|
|
41
|
+
headers.set(key, value);
|
|
42
|
+
});
|
|
70
43
|
const response = await fetch(url, {
|
|
71
44
|
...options,
|
|
72
45
|
headers,
|
|
73
|
-
redirect: "manual",
|
|
46
|
+
redirect: "manual",
|
|
74
47
|
});
|
|
75
48
|
this.storeCookies(response);
|
|
76
49
|
if (response.status >= 300 && response.status < 400) {
|
|
@@ -91,29 +64,29 @@ class VTOPSessionManager {
|
|
|
91
64
|
async initialize() {
|
|
92
65
|
try {
|
|
93
66
|
console.log("Initializing VTOP session...");
|
|
94
|
-
//
|
|
67
|
+
// 1: GET / to get SERVERID cookie
|
|
95
68
|
console.log("Step 1: GET / to get SERVERID...");
|
|
96
69
|
await this.fetchWithCookies(BASE);
|
|
97
|
-
console.log("
|
|
98
|
-
//
|
|
70
|
+
console.log("OK");
|
|
71
|
+
// 2: GET /vtop/ to get JSESSIONID cookie
|
|
99
72
|
console.log("Step 2: GET /vtop/ to get JSESSIONID...");
|
|
100
73
|
await this.fetchWithCookies(VTOP);
|
|
101
|
-
console.log("
|
|
102
|
-
//
|
|
74
|
+
console.log("OK");
|
|
75
|
+
// 3: GET /vtop/openPage for CSRF token
|
|
103
76
|
console.log("Step 3: GET /vtop/openPage for CSRF...");
|
|
104
77
|
const openPageRes = await this.fetchWithCookies(OPEN_PAGE);
|
|
105
78
|
const openPageHtml = await openPageRes.text();
|
|
106
|
-
console.log("
|
|
107
|
-
const csrf =
|
|
79
|
+
console.log("OK");
|
|
80
|
+
const csrf = extractCsrf(openPageHtml);
|
|
108
81
|
if (!csrf) {
|
|
109
|
-
console.warn("
|
|
82
|
+
console.warn("Warning: _csrf not found on /vtop/openPage.");
|
|
110
83
|
}
|
|
111
84
|
else {
|
|
112
85
|
this.state.csrf = csrf;
|
|
113
|
-
console.log(`
|
|
86
|
+
console.log(`Found _csrf: ${csrf.substring(0, 20)}...`);
|
|
114
87
|
}
|
|
115
|
-
//
|
|
116
|
-
console.log("Step 4: POST /vtop/prelogin/setup
|
|
88
|
+
// 4: POST /vtop/prelogin/setup
|
|
89
|
+
console.log("Step 4: POST /vtop/prelogin/setup flag=VTOP");
|
|
117
90
|
const formData = new URLSearchParams();
|
|
118
91
|
if (csrf) {
|
|
119
92
|
formData.set("_csrf", csrf);
|
|
@@ -164,32 +137,7 @@ class VTOPSessionManager {
|
|
|
164
137
|
this.state.username = null;
|
|
165
138
|
await this.initialize();
|
|
166
139
|
}
|
|
167
|
-
|
|
168
|
-
// reCAPTCHA signals
|
|
169
|
-
const recaptchaDom = html.includes('id="recaptcha"') ||
|
|
170
|
-
html.includes('id="g-recaptcha"') ||
|
|
171
|
-
html.includes('class="g-recaptcha"');
|
|
172
|
-
const recaptchaJs = html.includes("var captchaType=2");
|
|
173
|
-
const isRecaptcha = recaptchaDom || recaptchaJs;
|
|
174
|
-
const hasCaptchaInput = html.includes('name="captchaStr"') || html.includes('id="captchaStr"');
|
|
175
|
-
const imgDataUriMatches = [
|
|
176
|
-
...html.matchAll(/src=["'](data:image\/[^"']+)["']/gi),
|
|
177
|
-
];
|
|
178
|
-
let imgDataUri = null;
|
|
179
|
-
for (const match of imgDataUriMatches) {
|
|
180
|
-
if (match[1] && !match[1].includes(";base64,null")) {
|
|
181
|
-
imgDataUri = match[1];
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (!imgDataUri && imgDataUriMatches.length > 0) {
|
|
186
|
-
console.warn("Detected invalid captcha image (base64 is null)");
|
|
187
|
-
}
|
|
188
|
-
const isTextCaptcha = hasCaptchaInput && imgDataUri !== null;
|
|
189
|
-
const csrf = this.extractCsrf(html);
|
|
190
|
-
return { isTextCaptcha, isRecaptcha, csrf, imgDataUri };
|
|
191
|
-
}
|
|
192
|
-
async login(username, password, regNo, maxAttempts = 10) {
|
|
140
|
+
async login(username, password, maxAttempts = 10) {
|
|
193
141
|
if (!this.state.initialized) {
|
|
194
142
|
console.log("Session not initialized, initializing first...");
|
|
195
143
|
await this.initialize();
|
|
@@ -202,10 +150,13 @@ class VTOPSessionManager {
|
|
|
202
150
|
try {
|
|
203
151
|
const res = await this.fetchWithCookies(LOGIN_PAGE);
|
|
204
152
|
const body = await res.text();
|
|
205
|
-
const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } =
|
|
153
|
+
const { isTextCaptcha, isRecaptcha, csrf, imgDataUri } = detectCaptcha(body);
|
|
206
154
|
const curJsession = this.state.cookies.get("JSESSIONID") || "(?)";
|
|
207
155
|
const curServerID = this.state.cookies.get("SERVERID") || "(?)";
|
|
208
|
-
|
|
156
|
+
// const logMsg = `Attempt ${attempt}: status ${res.status}\ntext-captcha=${isTextCaptcha ? "YES" : "no"} | recaptcha=${isRecaptcha ? "YES" : "no"} | JSESSIONID=${curJsession}`;
|
|
157
|
+
const logMsg = `Attempt ${attempt} text-captcha=${isTextCaptcha ? "YES" : "no"} recaptcha=${isRecaptcha ? "YES" : "no"} `;
|
|
158
|
+
console.log(logMsg);
|
|
159
|
+
this.events.emit("log", logMsg);
|
|
209
160
|
if (isTextCaptcha) {
|
|
210
161
|
let solvedCaptcha = "";
|
|
211
162
|
if (imgDataUri) {
|
|
@@ -248,19 +199,7 @@ class VTOPSessionManager {
|
|
|
248
199
|
loginForm.set("captchaStr", solvedCaptcha);
|
|
249
200
|
const _cookies = `JSESSIONID=${curJsession}; SERVERID=${curServerID}`;
|
|
250
201
|
const postHeaders = {
|
|
251
|
-
|
|
252
|
-
Origin: BASE,
|
|
253
|
-
Referer: OPEN_PAGE_ALT,
|
|
254
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
255
|
-
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
256
|
-
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
257
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
258
|
-
"Sec-Fetch-Dest": "document",
|
|
259
|
-
"Sec-Fetch-Mode": "navigate",
|
|
260
|
-
"Sec-Fetch-Site": "same-origin",
|
|
261
|
-
"Sec-Fetch-User": "?1",
|
|
262
|
-
"Upgrade-Insecure-Requests": "1",
|
|
263
|
-
Priority: "u=0, i",
|
|
202
|
+
...LOGIN_POST_HEADERS,
|
|
264
203
|
Cookie: _cookies,
|
|
265
204
|
};
|
|
266
205
|
try {
|
|
@@ -287,7 +226,7 @@ class VTOPSessionManager {
|
|
|
287
226
|
console.log("Login POST submitted successfully!");
|
|
288
227
|
this.state.loggedIn = true;
|
|
289
228
|
this.state.username = username;
|
|
290
|
-
this.
|
|
229
|
+
this.events.emit("login-complete");
|
|
291
230
|
return true;
|
|
292
231
|
}
|
|
293
232
|
catch (e) {
|
|
@@ -307,6 +246,7 @@ class VTOPSessionManager {
|
|
|
307
246
|
}
|
|
308
247
|
}
|
|
309
248
|
console.log(`Login failed after ${maxAttempts} attempts`);
|
|
249
|
+
this.events.emit("login-complete");
|
|
310
250
|
return false;
|
|
311
251
|
}
|
|
312
252
|
isLoggedIn() {
|
|
@@ -322,14 +262,7 @@ class VTOPSessionManager {
|
|
|
322
262
|
}
|
|
323
263
|
const cookies = this.getCookieHeader();
|
|
324
264
|
const headers = {
|
|
325
|
-
|
|
326
|
-
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
|
|
327
|
-
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
328
|
-
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
329
|
-
"Cache-Control": "max-age=0",
|
|
330
|
-
"Upgrade-Insecure-Requests": "1",
|
|
331
|
-
Origin: BASE,
|
|
332
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
265
|
+
...POST_LOGIN_HEADERS,
|
|
333
266
|
Cookie: cookies,
|
|
334
267
|
};
|
|
335
268
|
try {
|
|
@@ -347,11 +280,19 @@ class VTOPSessionManager {
|
|
|
347
280
|
const contentRes = await this.fetchWithCookies(CONTENT, { headers });
|
|
348
281
|
const contentHtml = await contentRes.text();
|
|
349
282
|
console.log(` -> /vtop/content: ${contentRes.status}`);
|
|
350
|
-
const newCsrf =
|
|
283
|
+
const newCsrf = extractCsrf(contentHtml);
|
|
351
284
|
if (newCsrf) {
|
|
352
285
|
this.state.csrf = newCsrf;
|
|
353
286
|
console.log(`Updated CSRF token from content page`);
|
|
354
287
|
}
|
|
288
|
+
const regNo = extractRegNo(contentHtml);
|
|
289
|
+
if (regNo) {
|
|
290
|
+
this.state.regNo = regNo;
|
|
291
|
+
console.log(`Extracted registration number: ${regNo}`);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
console.warn("Failed to extract registration number from content page");
|
|
295
|
+
}
|
|
355
296
|
console.log("Post-login navigation complete");
|
|
356
297
|
return true;
|
|
357
298
|
}
|
|
@@ -360,102 +301,13 @@ class VTOPSessionManager {
|
|
|
360
301
|
return false;
|
|
361
302
|
}
|
|
362
303
|
}
|
|
363
|
-
//slop
|
|
364
|
-
parseAssignmentsHtml(html) {
|
|
365
|
-
const assignments = [];
|
|
366
|
-
try {
|
|
367
|
-
const jsonMatch = html.match(/\[[\s\S]*?\]/);
|
|
368
|
-
if (jsonMatch) {
|
|
369
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
370
|
-
if (Array.isArray(parsed)) {
|
|
371
|
-
return parsed.map((item) => ({
|
|
372
|
-
courseCode: String(item.courseCode || item.code || ""),
|
|
373
|
-
courseName: String(item.courseName || item.name || ""),
|
|
374
|
-
assignmentTitle: String(item.assignmentTitle || item.title || item.assignmentName || ""),
|
|
375
|
-
dueDate: String(item.dueDate || item.endDate || item.deadline || ""),
|
|
376
|
-
status: String(item.status || ""),
|
|
377
|
-
maxMarks: String(item.maxMarks || item.marks || ""),
|
|
378
|
-
}));
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
catch {
|
|
383
|
-
// Not JSON, try HTML parsing
|
|
384
|
-
}
|
|
385
|
-
// Parse HTML table rows using regex
|
|
386
|
-
// VTOP table structure: #(th), Course Name(td), Title(td), Last Date(td), Uploaded(td)
|
|
387
|
-
// Note: VTOP HTML sometimes has malformed tags like <<td
|
|
388
|
-
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
389
|
-
// Match both <th> and <td> cells, and handle malformed <<td
|
|
390
|
-
const cellRegex = /<?<t[hd][^>]*>([\s\S]*?)(?:<\/t[hd]>|<\/tr|<tr|$)/gi;
|
|
391
|
-
let rowMatch;
|
|
392
|
-
while ((rowMatch = rowRegex.exec(html)) !== null) {
|
|
393
|
-
const cells = [];
|
|
394
|
-
let cellMatch;
|
|
395
|
-
const rowContent = rowMatch[1];
|
|
396
|
-
// Reset lastIndex for cell regex
|
|
397
|
-
cellRegex.lastIndex = 0;
|
|
398
|
-
while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
|
|
399
|
-
// Strip HTML tags from cell content
|
|
400
|
-
const cellContent = cellMatch[1].replace(/<[^>]+>/g, "").trim();
|
|
401
|
-
cells.push(cellContent);
|
|
402
|
-
}
|
|
403
|
-
// Skip header rows (first cell would be "#" or similar header text)
|
|
404
|
-
if (cells.length >= 4 && cells[0] !== "#" && !isNaN(Number(cells[0]))) {
|
|
405
|
-
// Table structure: # (row num), Course Name, Title, Last Date, Uploaded
|
|
406
|
-
// cells[0] = row number (skip)
|
|
407
|
-
// cells[1] = Course Name
|
|
408
|
-
// cells[2] = Title (assignment title)
|
|
409
|
-
// cells[3] = Last Date (due date)
|
|
410
|
-
// cells[4] = Uploaded (status)
|
|
411
|
-
assignments.push({
|
|
412
|
-
courseCode: "", // Not in this table format
|
|
413
|
-
courseName: cells[1] || "",
|
|
414
|
-
assignmentTitle: cells[2] || "",
|
|
415
|
-
dueDate: cells[3] || "",
|
|
416
|
-
status: cells[4] || "Pending",
|
|
417
|
-
maxMarks: "", // Not in this table format
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return assignments;
|
|
422
|
-
}
|
|
423
|
-
parseCourseDetailsHtml(html) {
|
|
424
|
-
const courses = [];
|
|
425
|
-
// Regex to match table rows
|
|
426
|
-
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/g;
|
|
427
|
-
const matches = [...html.matchAll(rowRegex)];
|
|
428
|
-
// Skip header row
|
|
429
|
-
for (let i = 1; i < matches.length; i++) {
|
|
430
|
-
const rowContent = matches[i][1];
|
|
431
|
-
// Extract Code and Name: <span class="mx-2 text-dark fw-bold">BCSE204L</span>-<span class="mx-2 text-dark">Design and Analysis of Algorithms</span>
|
|
432
|
-
const codeMatch = rowContent.match(/<span[^>]*text-dark fw-bold[^>]*>([^<]+)<\/span>/);
|
|
433
|
-
const nameMatch = rowContent.match(/-[\s\n]*<span[^>]*text-dark[^>]*>([^<]+)<\/span>/);
|
|
434
|
-
// Extract Type: <td class="fst-italic text-primary fw-bold mx-1">TH</td>
|
|
435
|
-
const typeMatch = rowContent.match(/<td[^>]*fst-italic[^>]*>([^<]+)<\/td>/);
|
|
436
|
-
// Extract Attendance: <span class="text-danger fw-bold">65.0</span>
|
|
437
|
-
// Capture color class as well to determine status
|
|
438
|
-
const attendanceMatch = rowContent.match(/<span class="text-([a-z]+)[^>]*fw-bold">([\d.]+)<\/span>/);
|
|
439
|
-
// Extract Remarks: <span class="text-danger fw-bold">Critical - must improve</span>
|
|
440
|
-
// This usually comes after attendance in the last column
|
|
441
|
-
const remarksMatch = rowContent.match(/<span class="text-[^>]*>([^<]+)<\/span>[\s\n]*<\/td>[\s\n]*<\/tr>$/) ||
|
|
442
|
-
rowContent.match(/<td[^>]*text-nowrap text-start[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/);
|
|
443
|
-
if (codeMatch && nameMatch) {
|
|
444
|
-
courses.push({
|
|
445
|
-
code: codeMatch[1].trim(),
|
|
446
|
-
name: nameMatch[1].trim(),
|
|
447
|
-
type: typeMatch ? typeMatch[1].trim() : "N/A",
|
|
448
|
-
attendance: attendanceMatch ? attendanceMatch[2].trim() : "N/A",
|
|
449
|
-
attendanceColor: attendanceMatch
|
|
450
|
-
? attendanceMatch[1].trim()
|
|
451
|
-
: "secondary",
|
|
452
|
-
remarks: remarksMatch ? remarksMatch[1].trim() : "",
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return courses;
|
|
457
|
-
}
|
|
458
304
|
async performAcademicsCheck(headers) {
|
|
305
|
+
if (!headers) {
|
|
306
|
+
headers = {
|
|
307
|
+
...ACADEMICS_CHECK_HEADERS,
|
|
308
|
+
Cookie: this.getCookieHeader(),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
459
311
|
const now = new Date();
|
|
460
312
|
const accParams = new URLSearchParams();
|
|
461
313
|
accParams.set("authorizedID", this.state.regNo);
|
|
@@ -483,18 +335,8 @@ class VTOPSessionManager {
|
|
|
483
335
|
const cookies = this.getCookieHeader();
|
|
484
336
|
const now = new Date();
|
|
485
337
|
const apiHeaders = {
|
|
486
|
-
|
|
487
|
-
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
488
|
-
"Accept-Language": "en-US,en;q=0.7",
|
|
489
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
338
|
+
...API_REQUEST_HEADERS,
|
|
490
339
|
Cookie: cookies,
|
|
491
|
-
Origin: BASE,
|
|
492
|
-
Priority: "u=1, i",
|
|
493
|
-
Referer: CONTENT,
|
|
494
|
-
"Sec-Fetch-Dest": "empty",
|
|
495
|
-
"Sec-Fetch-Mode": "cors",
|
|
496
|
-
"Sec-Fetch-Site": "same-origin",
|
|
497
|
-
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
498
340
|
};
|
|
499
341
|
await this.performAcademicsCheck(apiHeaders);
|
|
500
342
|
const assParams = new URLSearchParams();
|
|
@@ -511,7 +353,7 @@ class VTOPSessionManager {
|
|
|
511
353
|
});
|
|
512
354
|
const html = await res.text();
|
|
513
355
|
console.log("Course details HTML:", html);
|
|
514
|
-
const courses =
|
|
356
|
+
const courses = parseCourseDetailsHtml(html);
|
|
515
357
|
console.log(`Parsed ${courses.length} courses`);
|
|
516
358
|
return courses;
|
|
517
359
|
}
|
|
@@ -525,29 +367,14 @@ class VTOPSessionManager {
|
|
|
525
367
|
console.error("Cannot fetch assignments: not logged in or no regNo");
|
|
526
368
|
return [];
|
|
527
369
|
}
|
|
528
|
-
await this.navigatePostLogin();
|
|
370
|
+
// await this.navigatePostLogin();
|
|
529
371
|
const cookies = this.getCookieHeader();
|
|
530
372
|
const now = new Date();
|
|
531
|
-
const accParams = new URLSearchParams();
|
|
532
|
-
accParams.set("authorizedID", this.state.regNo);
|
|
533
|
-
if (this.state.csrf)
|
|
534
|
-
accParams.set("_csrf", this.state.csrf);
|
|
535
|
-
accParams.set("x", now.toUTCString());
|
|
536
373
|
const apiHeaders = {
|
|
537
|
-
|
|
538
|
-
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
539
|
-
"Accept-Language": "en-US,en;q=0.7",
|
|
540
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
374
|
+
...API_REQUEST_HEADERS,
|
|
541
375
|
Cookie: cookies,
|
|
542
|
-
Origin: BASE,
|
|
543
|
-
Priority: "u=1, i",
|
|
544
|
-
Referer: CONTENT,
|
|
545
|
-
"Sec-Fetch-Dest": "empty",
|
|
546
|
-
"Sec-Fetch-Mode": "cors",
|
|
547
|
-
"Sec-Fetch-Site": "same-origin",
|
|
548
|
-
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
549
376
|
};
|
|
550
|
-
await this.performAcademicsCheck(apiHeaders);
|
|
377
|
+
// await this.performAcademicsCheck(apiHeaders);
|
|
551
378
|
const assParams = new URLSearchParams();
|
|
552
379
|
assParams.set("authorizedID", this.state.regNo);
|
|
553
380
|
if (this.state.csrf)
|
|
@@ -565,7 +392,7 @@ class VTOPSessionManager {
|
|
|
565
392
|
console.log(` -> Response length: ${assBody.length} chars`);
|
|
566
393
|
console.log(` -> Raw response:\n${assBody}`);
|
|
567
394
|
if (assBody) {
|
|
568
|
-
const assignments =
|
|
395
|
+
const assignments = parseAssignmentsHtml(assBody);
|
|
569
396
|
console.log(` Parsed ${assignments.length} assignments`);
|
|
570
397
|
return assignments;
|
|
571
398
|
}
|
package/dist/views/Dashboard.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
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-
|
|
3
|
+
export const Dashboard = ({ username, courses, assignments }) => {
|
|
4
|
+
return (_jsxs(BaseLayout, { title: "Dashboard - Open-VTOP", children: [_jsxs("div", { class: "flex justify-between items-center mb-6 border-b border-border pb-4", children: [_jsxs("div", { children: [_jsx("h1", { class: "text-xl font-bold tracking-tight", children: "Dashboard" }), _jsxs("p", { class: "text-muted text-xs", children: ["Welcome back, ", username] })] }), _jsx("div", {})] }), _jsxs("div", { class: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("section", { class: "lg:col-span-8 xl:col-span-9 space-y-4", children: [_jsxs("div", { class: "flex items-center justify-between", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Course Details" }), _jsx("span", { class: "text-[0.65rem] text-muted bg-surface border border-border px-2 py-1 rounded", children: "Winter Semester 2025-26" })] }), courses ? (courses.length === 0 ? (_jsx("div", { class: "p-8 text-center bg-surface border border-border rounded-lg text-muted text-sm", children: "No course details found." })) : (_jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3", children: courses.map((course, i) => (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg flex flex-col justify-between h-full hover:border-muted transition-colors", children: [_jsxs("div", { children: [_jsxs("div", { class: "flex justify-between items-start mb-1.5", children: [_jsx("span", { class: "text-[0.65rem] font-bold text-muted uppercase tracking-wider", children: course.code }), _jsx("span", { class: "text-[0.6rem] px-1.5 py-0.5 rounded border border-border text-muted", children: course.type })] }), _jsx("h4", { class: "font-semibold text-xs mb-2 leading-relaxed line-clamp-2", children: course.name })] }), _jsxs("div", { class: "flex items-end justify-between mt-2 pt-2 border-t border-border/50", children: [_jsxs("div", { class: "flex flex-col", children: [_jsx("span", { class: "text-[0.6rem] text-muted uppercase", children: "Attendance" }), _jsxs("span", { class: `text-base font-bold ${course.attendanceColor === "danger"
|
|
5
|
+
? "text-red-500"
|
|
6
|
+
: course.attendanceColor === "warning"
|
|
7
|
+
? "text-yellow-500"
|
|
8
|
+
: "text-green-500"}`, children: [course.attendance, "%"] })] }), course.remarks && (_jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded bg-${course.attendanceColor === "danger"
|
|
9
|
+
? "red"
|
|
10
|
+
: "green"}-500/10 text-${course.attendanceColor === "danger"
|
|
11
|
+
? "red"
|
|
12
|
+
: "green"}-500`, children: course.remarks }))] })] }, i))) }))) : (_jsx("div", { id: "courses-loader", "hx-get": "/api/courses/html", "hx-trigger": "load", "hx-swap": "innerHTML", class: "w-full", children: _jsx("div", { class: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 animate-pulse", children: [1, 2, 3, 4, 5, 6].map((i) => (_jsx("div", { class: "h-28 bg-surface/50 border border-border rounded-lg" }, i))) }) }))] }), _jsxs("section", { class: "lg:col-span-4 xl:col-span-3 space-y-4", children: [_jsx("h2", { class: "text-lg font-semibold", children: "Assignments" }), assignments ? (assignments.length === 0 ? (_jsx("div", { class: "p-6 text-center bg-surface border border-border rounded-lg", children: _jsx("p", { class: "text-sm text-muted", children: "No assignments pending." }) })) : (_jsx("div", { class: "flex flex-col gap-2", children: assignments.map((ass, i) => (_jsxs("div", { class: "p-3 bg-surface border border-border rounded-lg hover:border-muted transition-colors group flex flex-col gap-1", children: [_jsxs("div", { class: "flex justify-between items-start gap-2", children: [_jsx("span", { class: "font-bold text-xs text-foreground line-clamp-1", title: ass.courseName, children: ass.courseName }), _jsxs("span", { class: "text-[0.65rem] font-medium text-red-400 whitespace-nowrap shrink-0", children: ["Due: ", ass.dueDate || "N/A"] })] }), _jsxs("div", { class: "flex justify-between items-end gap-2", children: [_jsxs("div", { class: "flex flex-col min-w-0", children: [_jsx("span", { class: "text-[0.7rem] text-muted truncate", title: ass.assignmentTitle, children: ass.assignmentTitle }), _jsx("span", { class: "text-[0.6rem] text-muted/60 font-mono", children: ass.courseCode })] }), _jsx("span", { class: `text-[0.6rem] px-1.5 py-0.5 rounded font-medium whitespace-nowrap shrink-0 ${ass.status?.toLowerCase().includes("pending")
|
|
13
|
+
? "bg-red-500/10 text-red-500 border border-red-500/20"
|
|
14
|
+
: "bg-blue-500/10 text-blue-500 border border-blue-500/20"}`, children: ass.status || "Pending" })] })] }, i))) }))) : (_jsx("div", { id: "assignments-loader", "hx-get": "/api/assignments/html", "hx-trigger": courses ? "load" : "htmx:afterOnLoad from:#courses-loader", "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-12 text-muted", children: [_jsx("div", { class: "inline-block animate-spin rounded-full h-5 w-5 border-2 border-muted border-t-white mb-3" }), _jsx("p", { class: "text-xs", children: "Syncing assignments..." })] }) }) }))] })] })] }));
|
|
5
15
|
};
|
package/dist/views/Home.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
2
|
import { BaseLayout } from "./layouts/Base.js";
|
|
3
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: "
|
|
4
|
+
return (_jsxs(BaseLayout, { title: "Open-VTOP", children: [_jsx("h1", { children: "Open-VTOP" }), _jsxs("div", { class: "section", children: [_jsx("h2", { children: "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. 22BCE0001", 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
5
|
};
|
package/dist/views/Login.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
2
|
import { BaseLayout } from "./layouts/Base.js";
|
|
3
3
|
export const Login = () => {
|
|
4
|
-
return (_jsx(BaseLayout, { title: "Login - Open-VTOP", children:
|
|
4
|
+
return (_jsx(BaseLayout, { title: "Login - Open-VTOP", children: _jsx("div", { class: "min-h-[80vh] flex flex-col items-center justify-center w-full max-w-md mx-auto", "x-data": "{\n showPassword: false,\n loading: false\n }", children: _jsxs("div", { class: "w-full space-y-8", children: [_jsxs("div", { class: "flex flex-col items-center text-center", children: [_jsx("h1", { class: "text-2xl font-bold tracking-tight text-white", children: "Welcome back" }), _jsxs("p", { class: "text-sm mt-2", children: ["Enter your vtop username and password.This app runs completely clientside and has no telemetry", " "] })] }), _jsxs("div", { class: "bg-surface/50 backdrop-blur-sm border border-border rounded-xl p-8 shadow-xl", 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", class: "space-y-5", children: [_jsxs("div", { class: "space-y-2", children: [_jsx("label", { class: "text-xs font-medium text-muted uppercase tracking-wider", for: "username", children: "Username" }), _jsx("input", { type: "text", id: "username", name: "username", required: true, autofocus: true, placeholder: "Enter your username", class: "w-full bg-background border border-border rounded-lg px-4 py-2.5 text-sm text-foreground focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/40 transition-all placeholder-muted/50" })] }), _jsxs("div", { class: "space-y-2", children: [_jsx("label", { class: "text-xs font-medium text-muted uppercase tracking-wider", for: "password", children: "Password" }), _jsxs("div", { class: "relative", children: [_jsx("input", { "x-bind:type": "showPassword ? 'text' : 'password'", id: "password", name: "password", required: true, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", class: "w-full bg-background border border-border rounded-lg px-4 py-2.5 text-sm text-foreground focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/40 transition-all placeholder-muted/50 pr-12" }), _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-[0.65rem] font-bold tracking-wider opacity-70", children: "SHOW" }), _jsx("span", { "x-show": "showPassword", style: "display: none;", class: "text-[0.65rem] font-bold tracking-wider opacity-70", children: "HIDE" })] })] })] }), _jsxs("button", { type: "submit", class: "w-full bg-white text-black font-bold text-sm py-3 rounded-lg hover:bg-gray-100 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-white transition-all disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2 mt-2", "x-bind:disabled": "loading", children: [_jsx("span", { "x-show": "!loading", children: "Sign 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("template", { "x-if": "loading", children: _jsx("div", { class: "mt-6", "x-init": "htmx.process($el)", children: _jsxs("div", { class: "relative bg-black/40 border border-white/10 rounded-lg p-4 overflow-hidden group", children: [_jsx("div", { class: "absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" }), _jsxs("div", { class: "flex items-center gap-3", children: [_jsx("div", { class: "flex-shrink-0", children: _jsxs("div", { class: "relative flex h-3 w-3", children: [_jsx("span", { class: "animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" }), _jsx("span", { class: "relative inline-flex rounded-full h-3 w-3 bg-green-500" })] }) }), _jsx("div", { class: "font-mono text-xs text-green-200/90 w-full truncate", "hx-ext": "sse", "sse-connect": "/api/login/events", "sse-swap": "log", "sse-close": "login-complete", "hx-swap": "innerHTML", children: "Initializing connection..." })] })] }) }) })] })] }) }) }));
|
|
5
5
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
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: {
|
|
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: "/static/sse.js" }), _jsx("script", { src: "https://cdn.tailwindcss.com" }), _jsx("script", { dangerouslySetInnerHTML: {
|
|
4
4
|
__html: `
|
|
5
5
|
tailwind.config = {
|
|
6
6
|
theme: {
|
|
@@ -39,5 +39,5 @@ export const BaseLayout = ({ title, children, }) => {
|
|
|
39
39
|
opacity: 1;
|
|
40
40
|
}
|
|
41
41
|
`,
|
|
42
|
-
} })] }), _jsx("body", { class: "bg-background text-foreground antialiased min-h-screen
|
|
42
|
+
} })] }), _jsx("body", { class: "bg-background text-foreground antialiased min-h-screen", children: _jsx("main", { class: "w-full max-w-[1800px] mx-auto p-4 md:p-6 flex flex-col gap-6", children: children }) })] }));
|
|
43
43
|
};
|
package/package.json
CHANGED
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
"type": "module",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "tsx watch src/index.tsx",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"dev-cli": "node src/cli.cjs",
|
|
7
|
+
"build": "tsc && cp src/cli.cjs dist/cli.cjs",
|
|
8
|
+
"start": "node dist/cli.cjs",
|
|
9
|
+
"logs": "node dist/cli.cjs logs",
|
|
8
10
|
"prepare": "npm run build"
|
|
9
11
|
},
|
|
10
12
|
"dependencies": {
|
|
11
13
|
"@hono/node-server": "^1.19.9",
|
|
12
14
|
"hono": "^4.11.4",
|
|
15
|
+
"htmx-ext-sse": "^2.2.4",
|
|
13
16
|
"htmx.org": "^2.0.8",
|
|
14
17
|
"jpeg-js": "^0.4.4",
|
|
15
18
|
"pngjs": "^7.0.0"
|
|
16
19
|
},
|
|
17
20
|
"bin": {
|
|
18
|
-
"open-vtop": "dist/
|
|
21
|
+
"open-vtop": "dist/cli.cjs"
|
|
19
22
|
},
|
|
20
23
|
"files": [
|
|
21
24
|
"dist"
|
|
@@ -27,5 +30,5 @@
|
|
|
27
30
|
"tsx": "^4.7.1",
|
|
28
31
|
"typescript": "^5.8.3"
|
|
29
32
|
},
|
|
30
|
-
"version": "1.0.
|
|
33
|
+
"version": "1.0.7"
|
|
31
34
|
}
|
|
@@ -1,4 +0,0 @@
|
|
|
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
|
-
};
|