Compare commits

..

30 Commits

Author SHA1 Message Date
Shanmuga Krishnan S M
f368c83246 fix(skillrack): update PROFILE_URL to manageprofile.xhtml (fixing 404 error) and simplify auth failure check
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-28 09:48:50 +05:30
Shanmuga Krishnan S M
db5ac8e2ea fix(skillrack): update login to j_security_check endpoint, exact ion-icon medal and points regex extraction, and rename input label to SkillRack Login ID
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-28 09:44:56 +05:30
Shanmuga Krishnan S M
a19c36579e feat(skillrack): add SkillRack leaderboard with server-side login, AES-encrypted credential storage, Jsoup HTML scraping, daily auto-sync, and full frontend tab with registration modal
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-28 09:35:42 +05:30
Shanmuga Krishnan S M
6be263771d feat(admin): add dedicated role-based logins and scoped permissions for Community Q&A, Clubs & Centers, and GPA Curriculum 2026-08-24 15:22:35 +05:30
Shanmuga Krishnan S M
9275722e16 feat(admin): make telegram bot config and senior helper id management fully functional with live test ping and verification 2026-08-24 14:04:11 +05:30
Shanmuga Krishnan S M
6740400788 Merge remote-tracking branch 'github/main' 2026-08-24 12:22:38 +05:30
Devesh S
6d386273fb Merge branch 'main' of https://github.com/Amudieshwar-AG/Freshers-Hub 2026-08-24 11:47:28 +05:30
Devesh S
56c3849357 student leaderboard - certifications 2026-08-24 11:46:58 +05:30
Shanmuga Krishnan S M
f58c98cd46 fix(ims): add cache: no-store and timestamp cache-busters to all live IMS requests
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 10:56:08 +05:30
Shanmuga Krishnan S M
8c7a3d7f1a fix(ims): restore 100% real-time direct IMS scraping and fix Vite proxy Location header rewrite for VPS
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 10:49:57 +05:30
Shanmuga Krishnan S M
f8047b6cd3 fix(gpa): normalize department code to AIDS in fetchImsSemesterResults for reliable CGPA, GPA curve & grade distribution rendering
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 10:42:52 +05:30
Shanmuga Krishnan S M
72c213f2b6 fix(auth): add resilient fallback for missing CSRF token and proxy unavailability on VPS
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 10:38:53 +05:30
Shanmuga Krishnan S M
e6ea5cb302 fix(ims): resolve department code mapping and add curriculum fallbacks for attendance, CAT marks, assignment, lab & semester grades
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-24 08:59:51 +05:30
Shanmuga Krishnan S M
64f0c5d754 Merge https://github.com/Amudieshwar-AG/Freshers-Hub
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-08-24 08:12:53 +05:30
Amudieshwar A G
d9ed15b481 Redesign Admin UI theme, add Merge Routes & Bus Status Manager features 2026-08-21 12:10:26 +05:30
Shanmuga Krishnan S M
87865f810f latest update 2026-08-21 10:39:40 +05:30
Anbu-dev-creator
906e9c757b New 2026-08-21 09:26:47 +05:30
Shanmuga Krishnan S M
23bf4fd86a feat(dashboard): complete professional UI overhaul for student dashboard with executive analytics & matrix agenda 2026-08-20 11:55:15 +05:30
Shanmuga Krishnan S M
95c97e81fa perf: complete industry-standard optimization for frontend & backend 2026-08-20 10:18:55 +05:30
Shanmuga Krishnan S M
128e033a9a fix(backend): add @Builder.Default to entity fields and add unit test suite 2026-08-20 10:14:29 +05:30
Shanmuga Krishnan S M
4e2edb7c52 feat: implement reference agenda weekly timetable grid with category pills, live indicator, and star favorites 2026-08-20 08:40:14 +05:30
Shanmuga Krishnan S M
bbb2cf7d30 feat: add LAB and Assignment marks fetching with lazy loading and system protection 2026-08-19 14:58:59 +05:30
Shanmuga Krishnan S M
b712b1e2f1 feat: match reference timetable grid matrix UI, support period cell arrays, preserve empty cells with dash without fake Free Period fallback 2026-08-19 14:20:44 +05:30
Shanmuga Krishnan S M
1641bf2110 fix: guarantee 7 full period slots per day for all working days (Monday-Friday) with free period padding 2026-08-19 14:11:53 +05:30
Shanmuga Krishnan S M
2f69b0a29d fix: universal Anna University 2021 Regulation credit engine (IC=1, VA=1, Lab=1, Mandatory=0) 2026-08-19 14:10:01 +05:30
Shanmuga Krishnan S M
5ea7de558b fix: completely remove Saturday from timetable model, parser, fallback, and UI 2026-08-19 14:07:48 +05:30
Shanmuga Krishnan S M
596504840e feat: Anna University CGPA formula, credit fixes, academic overview dashboard, lab/theory badges 2026-08-19 14:06:20 +05:30
Shanmuga Krishnan S M
56ce10fc55 Integrate real RIT-IMS-API: live student profile, attendance breakdown, CAT marks, auto-calculated CGPA and GPA sync
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-19 10:31:54 +05:30
Shanmuga Krishnan S M
0c86c087e4 Integrate new Telegram bot with DevCollab workflow for notifications, actions, and project creation
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-19 09:28:44 +05:30
Shanmuga Krishnan S M
fe7ec3d61e Configure new Telegram bot token across frontend Admin dashboard and Python bot service
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
2026-08-19 09:25:26 +05:30
38 changed files with 15239 additions and 2177 deletions

1606
attendance_report.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -69,6 +69,13 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Jsoup HTML Parser for SkillRack profile scraping -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.18.1</version>
</dependency>
<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.IOException;
@@ -46,6 +47,24 @@ public class AdminController {
@Value("${admin.transport.password:RIT@2026}")
private String transportPassword;
@Value("${admin.community.username:Community}")
private String communityUsername;
@Value("${admin.community.password:RIT@2026}")
private String communityPassword;
@Value("${admin.clubs.username:Clubs}")
private String clubsUsername;
@Value("${admin.clubs.password:RIT@2026}")
private String clubsPassword;
@Value("${admin.curriculum.username:Curriculum}")
private String curriculumUsername;
@Value("${admin.curriculum.password:RIT@2026}")
private String curriculumPassword;
@Value("${admin.super.username:Admin}")
private String superAdminUsername;
@@ -71,18 +90,62 @@ public class AdminController {
if (p != null) inputPass = p.toString().trim();
}
boolean isStandardPass = "RIT@2026".equalsIgnoreCase(inputPass);
// 1. Check Transport Admin ("Transport" / "RIT@2026")
if (transportUsername.equalsIgnoreCase(inputUser) && transportPassword.equals(inputPass)) {
if ((transportUsername.equalsIgnoreCase(inputUser) && (transportPassword.equals(inputPass) || isStandardPass)) ||
"transportadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
response.put("success", true);
response.put("role", "ROLE_TRANSPORT");
response.put("username", "Transport Admin");
response.put("token", "TRANSPORT_SESSION_TOKEN_RIT_2026");
response.put("message", "Transport admin login successful");
response.put("message", "Transport fleet admin login successful");
return ResponseEntity.ok(response);
}
// 2. Check Super Admin ("Admin" / "RIT@2026" or "ritadmin" / "ritadmin2026")
if ((superAdminUsername.equalsIgnoreCase(inputUser) && superAdminPassword.equals(inputPass)) ||
// 2. Check Community & Senior Q&A Admin ("Community" / "RIT@2026" or "QA")
if ((communityUsername.equalsIgnoreCase(inputUser) && (communityPassword.equals(inputPass) || isStandardPass)) ||
"qa".equalsIgnoreCase(inputUser) && isStandardPass ||
"communityadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
"seniorqa".equalsIgnoreCase(inputUser) && isStandardPass) {
response.put("success", true);
response.put("role", "ROLE_COMMUNITY");
response.put("username", "Community & Q&A Admin");
response.put("token", "COMMUNITY_SESSION_TOKEN_RIT_2026");
response.put("message", "Community & Senior Q&A admin login successful");
return ResponseEntity.ok(response);
}
// 3. Check Clubs & Centers Admin ("Clubs" / "RIT@2026" or "Club")
if ((clubsUsername.equalsIgnoreCase(inputUser) && (clubsPassword.equals(inputPass) || isStandardPass)) ||
"club".equalsIgnoreCase(inputUser) && isStandardPass ||
"clubadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
"clubsadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
response.put("success", true);
response.put("role", "ROLE_CLUBS");
response.put("username", "Clubs & Centers Admin");
response.put("token", "CLUBS_SESSION_TOKEN_RIT_2026");
response.put("message", "Clubs & Centers admin login successful");
return ResponseEntity.ok(response);
}
// 4. Check GPA Curriculum Admin ("Curriculum" / "RIT@2026" or "GPA" or "Academics")
if ((curriculumUsername.equalsIgnoreCase(inputUser) && (curriculumPassword.equals(inputPass) || isStandardPass)) ||
"gpa".equalsIgnoreCase(inputUser) && isStandardPass ||
"gpaadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
"academics".equalsIgnoreCase(inputUser) && isStandardPass ||
"curriculumadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
response.put("success", true);
response.put("role", "ROLE_CURRICULUM");
response.put("username", "GPA Curriculum Admin");
response.put("token", "CURRICULUM_SESSION_TOKEN_RIT_2026");
response.put("message", "GPA Curriculum admin login successful");
return ResponseEntity.ok(response);
}
// 5. Check Super Admin ("Admin" / "RIT@2026" or "ritadmin" / "ritadmin2026" or "superadmin")
if ((superAdminUsername.equalsIgnoreCase(inputUser) && (superAdminPassword.equals(inputPass) || isStandardPass)) ||
"superadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
("ritadmin".equalsIgnoreCase(inputUser) && "ritadmin2026".equals(inputPass))) {
response.put("success", true);
response.put("role", "ROLE_SUPER_ADMIN");
@@ -93,7 +156,7 @@ public class AdminController {
}
response.put("success", false);
response.put("message", "Invalid credentials. Use 'Transport' or 'Admin' with password 'RIT@2026'.");
response.put("message", "Invalid credentials. Use 'Transport', 'Community', 'Clubs', 'Curriculum', or 'Admin' with password 'RIT@2026'.");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
} catch (Exception e) {
@@ -257,15 +320,47 @@ public class AdminController {
if (Files.exists(path)) {
try {
byte[] bytes = Files.readAllBytes(path);
return objectMapper.readValue(bytes, Map.class);
Map<String, Object> loaded = objectMapper.readValue(bytes, Map.class);
if (loaded != null) {
List<Object> rawChatIds = (List<Object>) loaded.get("helper_chat_ids");
List<Map<String, Object>> helpers = (List<Map<String, Object>>) loaded.get("seniorHelpers");
if (helpers == null && rawChatIds != null) {
helpers = new ArrayList<>();
for (Object id : rawChatIds) {
Map<String, Object> h = new HashMap<>();
h.put("chatId", Long.parseLong(id.toString()));
h.put("name", "Senior Responder");
helpers.add(h);
}
loaded.put("seniorHelpers", helpers);
} else if (helpers != null && rawChatIds == null) {
List<Long> ids = new ArrayList<>();
for (Map<String, Object> h : helpers) {
if (h.get("chatId") != null) {
try {
ids.add(Long.parseLong(h.get("chatId").toString()));
} catch (Exception ignored) {}
}
}
loaded.put("helper_chat_ids", ids);
}
return loaded;
}
} catch (Exception e) {
e.printStackTrace();
}
}
Map<String, Object> defaultCfg = new HashMap<>();
defaultCfg.put("community_bot_token", "");
defaultCfg.put("telegram_bot_token", "");
defaultCfg.put("helper_chat_ids", new ArrayList<Long>(List.of(971749136L, 5567776672L, 1873240361L)));
defaultCfg.put("community_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY");
defaultCfg.put("telegram_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY");
defaultCfg.put("helper_chat_ids", new ArrayList<Long>(List.of(1873240361L, 5567776672L, 8518850169L, 7238144438L)));
List<Map<String, Object>> defaultHelpers = new ArrayList<>();
defaultHelpers.add(Map.of("chatId", 1873240361L, "name", "Senior Mentor (CSE)"));
defaultHelpers.add(Map.of("chatId", 5567776672L, "name", "Senior Responder (ECE)"));
defaultHelpers.add(Map.of("chatId", 8518850169L, "name", "Senior Responder (IT)"));
defaultHelpers.add(Map.of("chatId", 7238144438L, "name", "Senior Responder (AIDS)"));
defaultCfg.put("seniorHelpers", defaultHelpers);
defaultCfg.put("spring_backend_url", "http://localhost:8085");
return defaultCfg;
}
@@ -293,6 +388,25 @@ public class AdminController {
if (payload.containsKey("community_bot_token")) current.put("community_bot_token", payload.get("community_bot_token"));
if (payload.containsKey("telegram_bot_token")) current.put("telegram_bot_token", payload.get("telegram_bot_token"));
if (payload.containsKey("spring_backend_url")) current.put("spring_backend_url", payload.get("spring_backend_url"));
if (payload.containsKey("bot_username")) current.put("bot_username", payload.get("bot_username"));
if (payload.containsKey("seniorHelpers")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> helpers = (List<Map<String, Object>>) payload.get("seniorHelpers");
current.put("seniorHelpers", helpers);
List<Long> ids = new ArrayList<>();
if (helpers != null) {
for (Map<String, Object> h : helpers) {
if (h.get("chatId") != null) {
try {
ids.add(Long.parseLong(h.get("chatId").toString()));
} catch (Exception ignored) {}
}
}
}
current.put("helper_chat_ids", ids);
} else if (payload.containsKey("helper_chat_ids")) {
current.put("helper_chat_ids", payload.get("helper_chat_ids"));
}
saveTelegramConfigFile(current);
return ResponseEntity.ok(Map.of("success", true, "config", current));
}
@@ -300,6 +414,7 @@ public class AdminController {
@PostMapping("/telegram/helpers")
public ResponseEntity<?> addSeniorHelper(@RequestBody Map<String, Object> payload) {
Object chatIdObj = payload.get("chatId");
String nameStr = payload.get("name") != null ? payload.get("name").toString().trim() : "Senior Responder";
if (chatIdObj == null) {
return ResponseEntity.badRequest().body(Map.of("error", "chatId is required"));
}
@@ -313,38 +428,142 @@ public class AdminController {
Map<String, Object> config = readTelegramConfigFile();
@SuppressWarnings("unchecked")
List<Object> rawHelpers = (List<Object>) config.getOrDefault("helper_chat_ids", new ArrayList<>());
List<Long> helpers = new ArrayList<>();
for (Object o : rawHelpers) {
helpers.add(Long.parseLong(o.toString()));
List<Map<String, Object>> helpers = (List<Map<String, Object>>) config.getOrDefault("seniorHelpers", new ArrayList<>());
if (helpers == null) helpers = new ArrayList<>();
List<Long> helperIds = new ArrayList<>();
boolean found = false;
for (Map<String, Object> h : helpers) {
if (h.get("chatId") != null) {
Long cId = Long.parseLong(h.get("chatId").toString());
helperIds.add(cId);
if (cId.equals(chatId)) {
h.put("name", nameStr);
found = true;
}
}
}
if (!helpers.contains(chatId)) {
helpers.add(chatId);
config.put("helper_chat_ids", helpers);
saveTelegramConfigFile(config);
if (!found) {
Map<String, Object> newHelper = new HashMap<>();
newHelper.put("chatId", chatId);
newHelper.put("name", nameStr);
helpers.add(newHelper);
helperIds.add(chatId);
}
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers));
config.put("seniorHelpers", helpers);
config.put("helper_chat_ids", helperIds);
saveTelegramConfigFile(config);
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers, "helper_chat_ids", helperIds));
}
@DeleteMapping("/telegram/helpers/{chatId}")
public ResponseEntity<?> removeSeniorHelper(@PathVariable Long chatId) {
Map<String, Object> config = readTelegramConfigFile();
@SuppressWarnings("unchecked")
List<Object> rawHelpers = (List<Object>) config.getOrDefault("helper_chat_ids", new ArrayList<>());
List<Long> helpers = new ArrayList<>();
for (Object o : rawHelpers) {
helpers.add(Long.parseLong(o.toString()));
List<Map<String, Object>> helpers = (List<Map<String, Object>>) config.getOrDefault("seniorHelpers", new ArrayList<>());
if (helpers != null) {
helpers.removeIf(h -> h.get("chatId") != null && Long.parseLong(h.get("chatId").toString()) == chatId);
} else {
helpers = new ArrayList<>();
}
boolean removed = helpers.removeIf(id -> id.equals(chatId));
if (removed) {
config.put("helper_chat_ids", helpers);
saveTelegramConfigFile(config);
List<Long> helperIds = new ArrayList<>();
for (Map<String, Object> h : helpers) {
if (h.get("chatId") != null) {
try {
helperIds.add(Long.parseLong(h.get("chatId").toString()));
} catch (Exception ignored) {}
}
}
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers));
config.put("seniorHelpers", helpers);
config.put("helper_chat_ids", helperIds);
saveTelegramConfigFile(config);
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers, "helper_chat_ids", helperIds));
}
@PostMapping("/telegram/test-bot")
public ResponseEntity<?> testTelegramBot(@RequestBody Map<String, Object> payload) {
String token = (String) payload.get("token");
if (token == null || token.trim().isEmpty()) {
Map<String, Object> cfg = readTelegramConfigFile();
token = (String) cfg.getOrDefault("community_bot_token", "");
}
if (token == null || token.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("ok", false, "description", "Bot token is required"));
}
try {
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.telegram.org/bot" + token.trim() + "/getMe";
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
return ResponseEntity.ok(response.getBody());
} catch (Exception e) {
return ResponseEntity.ok(Map.of("ok", false, "description", e.getMessage()));
}
}
@PostMapping("/telegram/test-dispatch")
public ResponseEntity<?> testTelegramDispatch(@RequestBody Map<String, Object> payload) {
String token = (String) payload.get("token");
Object chatIdObj = payload.get("chatId");
String message = (String) payload.get("message");
if (token == null || token.trim().isEmpty()) {
Map<String, Object> cfg = readTelegramConfigFile();
token = (String) cfg.getOrDefault("community_bot_token", "");
}
if (token == null || token.trim().isEmpty() || chatIdObj == null) {
return ResponseEntity.badRequest().body(Map.of("ok", false, "description", "Bot token and chatId are required"));
}
if (message == null || message.trim().isEmpty()) {
message = "🔔 *RIT Nexus Admin Test Ping*\n\nThis is a verified test notification confirming your Telegram Chat ID is active and connected to the Q&A dispatch system!";
}
try {
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.telegram.org/bot" + token.trim() + "/sendMessage";
Map<String, Object> body = new HashMap<>();
body.put("chat_id", chatIdObj);
body.put("text", message);
body.put("parse_mode", "Markdown");
ResponseEntity<Map> response = restTemplate.postForEntity(url, body, Map.class);
return ResponseEntity.ok(response.getBody());
} catch (Exception e) {
return ResponseEntity.ok(Map.of("ok", false, "description", e.getMessage()));
}
}
@PostMapping("/telegram/test-broadcast")
public ResponseEntity<?> testTelegramBroadcast(@RequestBody Map<String, Object> payload) {
Map<String, Object> cfg = readTelegramConfigFile();
String token = (String) payload.getOrDefault("token", cfg.get("community_bot_token"));
String message = (String) payload.getOrDefault("message", "📢 *RIT Nexus Q&A Dispatch Network Test*\n\nAll senior responders are currently being pinged to verify real-time dispatch connectivity.");
@SuppressWarnings("unchecked")
List<Object> rawHelpers = (List<Object>) cfg.getOrDefault("helper_chat_ids", new ArrayList<>());
int delivered = 0;
List<Map<String, Object>> results = new ArrayList<>();
RestTemplate restTemplate = new RestTemplate();
if (token != null && !token.trim().isEmpty() && rawHelpers != null) {
for (Object idObj : rawHelpers) {
try {
String url = "https://api.telegram.org/bot" + token.trim() + "/sendMessage";
Map<String, Object> body = new HashMap<>();
body.put("chat_id", idObj);
body.put("text", message);
body.put("parse_mode", "Markdown");
ResponseEntity<Map> res = restTemplate.postForEntity(url, body, Map.class);
results.add(Map.of("chatId", idObj, "status", "success", "response", res.getBody()));
delivered++;
} catch (Exception e) {
results.add(Map.of("chatId", idObj, "status", "failed", "error", e.getMessage()));
}
}
}
return ResponseEntity.ok(Map.of("success", true, "delivered", delivered, "total", rawHelpers != null ? rawHelpers.size() : 0, "details", results));
}
// ─────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,68 @@
package com.rit.portal.controller;
import com.rit.portal.entity.SkillrackProfile;
import com.rit.portal.service.SkillrackService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/skillrack")
@CrossOrigin(originPatterns = "*")
@RequiredArgsConstructor
public class SkillrackController {
private final SkillrackService skillrackService;
@GetMapping("/leaderboard")
public List<SkillrackProfile> getLeaderboard() {
return skillrackService.getAllRankedProfiles();
}
@PostMapping("/register")
public ResponseEntity<?> registerProfile(@RequestBody RegisterRequest request) {
if (request.getSkillrackEmail() == null || request.getSkillrackEmail().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "SkillRack email is required"));
}
if (request.getSkillrackPassword() == null || request.getSkillrackPassword().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "SkillRack password is required"));
}
if (request.getStudentName() == null || request.getStudentName().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Student name is required"));
}
try {
SkillrackProfile profile = skillrackService.registerOrUpdateStudent(
request.getStudentName(),
request.getSkillrackEmail(),
request.getSkillrackPassword(),
request.getDepartment() != null ? request.getDepartment() : "CSE",
request.getYear() != null ? request.getYear() : "1st Year"
);
return ResponseEntity.ok(profile);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "Failed to register profile: " + e.getMessage()));
}
}
@PostMapping("/sync")
public ResponseEntity<?> triggerSync() {
skillrackService.syncAllProfilesWithSpacing();
return ResponseEntity.ok(Map.of("message", "SkillRack background sync initiated with 5-second spacing."));
}
@Data
public static class RegisterRequest {
private String studentName;
private String skillrackEmail;
private String skillrackPassword;
private String department;
private String year;
}
}

View File

@@ -30,11 +30,14 @@ public class CommunityAnswer {
private String author;
@Column(name = "upvotes")
@Builder.Default
private Integer upvotes = 0;
@Column(name = "is_accepted")
@Builder.Default
private Boolean isAccepted = false;
@Column(name = "created_at")
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
}

View File

@@ -29,15 +29,19 @@ public class CommunityQuestion {
private String author;
@Column(name = "upvotes")
@Builder.Default
private Integer upvotes = 0;
@Column(name = "tags", columnDefinition = "text[]")
@Builder.Default
private List<String> tags = new ArrayList<>();
@Column(name = "is_answered")
@Builder.Default
private Boolean isAnswered = false;
@Column(name = "created_at")
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.EAGER)

View File

@@ -39,8 +39,10 @@ public class NotePyq {
private String fileSize;
@Column(name = "downloads_count")
@Builder.Default
private Integer downloadsCount = 0;
@Column(name = "uploaded_at")
@Builder.Default
private LocalDateTime uploadedAt = LocalDateTime.now();
}

View File

@@ -0,0 +1,78 @@
package com.rit.portal.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "skillrack_profiles")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SkillrackProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "student_name", nullable = false)
private String studentName;
@Column(name = "skillrack_email", nullable = false, unique = true)
private String skillrackEmail;
@Column(name = "encrypted_password", nullable = false, length = 512)
private String encryptedPassword;
@Column(nullable = false)
private String department;
@Column(nullable = false)
private String year;
@Column(name = "total_points")
@Builder.Default
private Integer totalPoints = 0;
@Column(name = "code_test_solved")
@Builder.Default
private Integer codeTestSolved = 0;
@Column(name = "code_tutor_solved")
@Builder.Default
private Integer codeTutorSolved = 0;
@Column(name = "code_track_solved")
@Builder.Default
private Integer codeTrackSolved = 0;
@Column(name = "dc_solved")
@Builder.Default
private Integer dcSolved = 0;
@Column(name = "gold_medals")
@Builder.Default
private Integer goldMedals = 0;
@Column(name = "silver_medals")
@Builder.Default
private Integer silverMedals = 0;
@Column(name = "bronze_medals")
@Builder.Default
private Integer bronzeMedals = 0;
@Column(name = "last_updated")
private LocalDateTime lastUpdated;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
if (createdAt == null) createdAt = LocalDateTime.now();
if (lastUpdated == null) lastUpdated = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,15 @@
package com.rit.portal.repository;
import com.rit.portal.entity.SkillrackProfile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface SkillrackProfileRepository extends JpaRepository<SkillrackProfile, Integer> {
Optional<SkillrackProfile> findBySkillrackEmailIgnoreCase(String skillrackEmail);
List<SkillrackProfile> findAllByOrderByTotalPointsDesc();
boolean existsBySkillrackEmailIgnoreCase(String skillrackEmail);
}

View File

@@ -45,8 +45,17 @@ public class BusLocationService {
public List<BusLocation> getActiveLocations() {
Instant threshold = Instant.now().minus(STALE_DURATION);
// Clean up stale entries on read
return locationCache.values().stream()
.filter(loc -> !loc.getLastUpdated().isBefore(threshold))
.toList();
}
/**
* Periodic background eviction of stale GPS coordinates every 5 minutes
*/
@org.springframework.scheduling.annotation.Scheduled(fixedRate = 300000)
public void evictStaleLocations() {
Instant threshold = Instant.now().minus(STALE_DURATION);
locationCache.entrySet().removeIf(entry -> entry.getValue().getLastUpdated().isBefore(threshold));
return List.copyOf(locationCache.values());
}
}

View File

@@ -0,0 +1,330 @@
package com.rit.portal.service;
import com.rit.portal.entity.SkillrackProfile;
import com.rit.portal.repository.SkillrackProfileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@RequiredArgsConstructor
@Slf4j
public class SkillrackService {
private final SkillrackProfileRepository repository;
// AES-128 key (16 bytes) — in production, move to environment variable
private static final String AES_KEY = "RitSkillR@ck2026";
private static final String SKILLRACK_BASE = "https://www.skillrack.com";
private static final String LOGIN_URL = SKILLRACK_BASE + "/faces/ui/profile.xhtml";
private static final String PROFILE_URL = SKILLRACK_BASE + "/faces/candidate/manageprofile.xhtml";
// ─── Public API ───────────────────────────────────────────────────
public List<SkillrackProfile> getAllRankedProfiles() {
return repository.findAllByOrderByTotalPointsDesc();
}
public SkillrackProfile registerOrUpdateStudent(String studentName, String email, String password,
String department, String year) {
String cleanEmail = email.trim().toLowerCase();
SkillrackProfile profile = repository.findBySkillrackEmailIgnoreCase(cleanEmail)
.orElse(SkillrackProfile.builder()
.studentName(studentName.trim())
.skillrackEmail(cleanEmail)
.department(department.trim())
.year(year.trim())
.build());
profile.setStudentName(studentName.trim());
profile.setDepartment(department.trim());
profile.setYear(year.trim());
profile.setEncryptedPassword(encrypt(password));
boolean success = fetchAndUpdateProfile(profile, password);
if (!success) {
throw new IllegalArgumentException(
"Could not authenticate with SkillRack using '" + cleanEmail +
"'. Please verify your email and password are correct.");
}
return repository.save(profile);
}
public boolean fetchAndUpdateProfile(SkillrackProfile profile) {
String password = decrypt(profile.getEncryptedPassword());
if (password == null || password.isEmpty()) {
log.warn("Cannot decrypt password for SkillRack profile: {}", profile.getSkillrackEmail());
return false;
}
return fetchAndUpdateProfile(profile, password);
}
/**
* Core scraping method using SkillRack's container security:
* 1. GET https://www.skillrack.com/faces/ui/profile.xhtml to establish JSESSIONID session
* 2. POST to https://www.skillrack.com/faces/ui/j_security_check with j_username (Login ID) & j_password
* 3. GET https://www.skillrack.com/faces/candidate/profileview.xhtml with authenticated session & parse HTML
*/
public boolean fetchAndUpdateProfile(SkillrackProfile profile, String rawPassword) {
try {
log.info("Fetching SkillRack stats for Login ID: {}", profile.getSkillrackEmail());
String loginPageUrl = SKILLRACK_BASE + "/faces/ui/profile.xhtml";
String jSecurityCheckUrl = SKILLRACK_BASE + "/faces/ui/j_security_check";
// Step 1: GET login page to establish session cookie
Connection.Response pageRes = Jsoup.connect(loginPageUrl)
.method(Connection.Method.GET)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.timeout(15000)
.execute();
Map<String, String> cookies = new HashMap<>(pageRes.cookies());
// Step 2: POST login credentials to j_security_check
Map<String, String> formData = new LinkedHashMap<>();
formData.put("j_username", profile.getSkillrackEmail().trim());
formData.put("j_password", rawPassword);
Connection.Response loginRes = Jsoup.connect(jSecurityCheckUrl)
.method(Connection.Method.POST)
.cookies(cookies)
.data(formData)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", loginPageUrl)
.followRedirects(true)
.timeout(15000)
.execute();
cookies.putAll(loginRes.cookies());
// Step 3: Fetch candidate profile page with authenticated session
Connection.Response profileRes = Jsoup.connect(PROFILE_URL)
.method(Connection.Method.GET)
.cookies(cookies)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.followRedirects(true)
.timeout(15000)
.execute();
cookies.putAll(profileRes.cookies());
Document profileDoc = profileRes.parse();
String bodyText = profileDoc.body().text();
// Check if login failed (still on login page containing j_security_check form)
if (bodyText.contains("j_security_check") || bodyText.contains("Invalid User ID")) {
log.warn("SkillRack authentication failed for: {}", profile.getSkillrackEmail());
return false;
}
// Step 4: Parse profile stats from the HTML
boolean parsed = parseProfileStats(profileDoc, profile);
if (!parsed) {
// Fallback: Try to extract from raw text with regex patterns
parsed = parseFromRawText(bodyText, profile);
}
if (parsed) {
profile.setLastUpdated(LocalDateTime.now());
log.info("Successfully scraped SkillRack for {}: Points={}, CT={}, Tutor={}, Track={}, DC={}, Gold={}, Silver={}, Bronze={}",
profile.getSkillrackEmail(), profile.getTotalPoints(),
profile.getCodeTestSolved(), profile.getCodeTutorSolved(),
profile.getCodeTrackSolved(), profile.getDcSolved(),
profile.getGoldMedals(), profile.getSilverMedals(), profile.getBronzeMedals());
return true;
}
log.warn("Could not parse SkillRack profile data for: {}", profile.getSkillrackEmail());
return false;
} catch (Exception e) {
log.error("Failed to fetch SkillRack data for {}: {}", profile.getSkillrackEmail(), e.getMessage());
return false;
}
}
// ─── HTML Parsing ─────────────────────────────────────────────────
private boolean parseProfileStats(Document doc, SkillrackProfile profile) {
String html = doc.html();
boolean foundAnything = false;
// 1. Extract Real Name from .ui-chip-text if available
Element chipEl = doc.selectFirst(".ui-chip-text");
if (chipEl != null) {
String chipText = chipEl.text().trim();
if (chipText.contains("-")) {
String[] parts = chipText.split("-");
if (parts.length >= 1 && !parts[0].trim().isEmpty()) {
profile.setStudentName(parts[0].trim());
}
}
}
// 2. Gold Medals (orange medal icon)
Matcher goldM = Pattern.compile("ion-md-medal\"\\s+style=\"color:orange\"></i>\\s*(\\d+)").matcher(html);
if (goldM.find()) {
profile.setGoldMedals(Integer.parseInt(goldM.group(1)));
foundAnything = true;
}
// 3. Silver Medals (grey medal icon)
Matcher silverM = Pattern.compile("ion-md-medal\"\\s+style=\"color:grey\"></i>\\s*(\\d+)").matcher(html);
if (silverM.find()) {
profile.setSilverMedals(Integer.parseInt(silverM.group(1)));
foundAnything = true;
}
// 4. Bronze Medals (brown medal icon)
Matcher bronzeM = Pattern.compile("ion-md-medal\"\\s+style=\"color:brown\"></i>\\s*(\\d+)").matcher(html);
if (bronzeM.find()) {
profile.setBronzeMedals(Integer.parseInt(bronzeM.group(1)));
foundAnything = true;
}
// 5. Total Points / Score (brown flag icon)
Matcher pointsM = Pattern.compile("ion-md-flag\"\\s+style=\"color:brown\"></i>\\s*(\\d+)").matcher(html);
if (pointsM.find()) {
profile.setTotalPoints(Integer.parseInt(pointsM.group(1)));
foundAnything = true;
}
return foundAnything;
}
private boolean parseFromRawText(String text, SkillrackProfile profile) {
boolean found = false;
// Pattern: "Code Test 123" or "CodeTest: 123" or "Code Test Solved: 123"
found |= extractAndSet(text, "(?i)code\\s*test[^\\d]*(\\d+)", val -> profile.setCodeTestSolved(val));
found |= extractAndSet(text, "(?i)code\\s*tutor[^\\d]*(\\d+)", val -> profile.setCodeTutorSolved(val));
found |= extractAndSet(text, "(?i)code\\s*track[^\\d]*(\\d+)", val -> profile.setCodeTrackSolved(val));
found |= extractAndSet(text, "(?i)daily\\s*challenge[^\\d]*(\\d+)", val -> profile.setDcSolved(val));
found |= extractAndSet(text, "(?i)(?:dc|DC)[^\\d]*(\\d+)", val -> {
if (profile.getDcSolved() == 0) profile.setDcSolved(val);
});
found |= extractAndSet(text, "(?i)gold[^\\d]*(\\d+)", val -> profile.setGoldMedals(val));
found |= extractAndSet(text, "(?i)silver[^\\d]*(\\d+)", val -> profile.setSilverMedals(val));
found |= extractAndSet(text, "(?i)bronze[^\\d]*(\\d+)", val -> profile.setBronzeMedals(val));
found |= extractAndSet(text, "(?i)total\\s*(?:point|score)s?[^\\d]*(\\d+)", val -> profile.setTotalPoints(val));
// If total points not found, compute from components
if (profile.getTotalPoints() == 0 && found) {
int computed = profile.getCodeTestSolved() + profile.getCodeTutorSolved()
+ profile.getCodeTrackSolved() + profile.getDcSolved();
if (computed > 0) {
profile.setTotalPoints(computed);
}
}
return found;
}
private boolean extractAndSet(String text, String regex, java.util.function.IntConsumer setter) {
Matcher m = Pattern.compile(regex).matcher(text);
if (m.find()) {
int val = parseIntSafe(m.group(1));
if (val > 0) {
setter.accept(val);
return true;
}
}
return false;
}
private int parseIntSafe(String s) {
try {
return Integer.parseInt(s.replaceAll("[^\\d]", ""));
} catch (NumberFormatException e) {
return 0;
}
}
// ─── AES Encryption Helpers ───────────────────────────────────────
private String encrypt(String plainText) {
try {
SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
log.error("AES encryption failed: {}", e.getMessage());
throw new RuntimeException("Encryption failed", e);
}
}
private String decrypt(String encryptedBase64) {
try {
SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decoded = Base64.getDecoder().decode(encryptedBase64);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("AES decryption failed: {}", e.getMessage());
return null;
}
}
// ─── Scheduled & Manual Sync ──────────────────────────────────────
/**
* Automatic daily sync at 2:30 AM — spaced 5s apart to avoid rate limits
*/
@Scheduled(cron = "0 30 2 * * ?")
public void scheduledDailySync() {
log.info("Starting scheduled daily SkillRack sync...");
syncAllProfilesWithSpacing();
}
/**
* Manual or scheduled trigger to sync all profiles with 5-second delays.
*/
public void syncAllProfilesWithSpacing() {
List<SkillrackProfile> profiles = repository.findAll();
log.info("Queued {} SkillRack profiles for sync.", profiles.size());
new Thread(() -> {
for (int i = 0; i < profiles.size(); i++) {
SkillrackProfile p = profiles.get(i);
try {
boolean success = fetchAndUpdateProfile(p);
if (success) {
repository.save(p);
}
if (i < profiles.size() - 1) {
log.info("Waiting 5s before next SkillRack profile sync...");
Thread.sleep(5000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("SkillRack sync interrupted.");
break;
} catch (Exception e) {
log.error("Error syncing SkillRack profile {}: {}", p.getSkillrackEmail(), e.getMessage());
}
}
log.info("Finished SkillRack sync for all profiles.");
}).start();
}
}

View File

@@ -3,14 +3,24 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/rit_freshers_hub?sslmode=
spring.datasource.username=postgres
spring.datasource.password=${DB_PASSWORD:Amudiesh22@.}
# ─── HIKARICP CONNECTION POOL OPTIMIZATIONS ───
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=1200000
spring.datasource.hikari.connection-timeout=20000
# ─── JPA / HIBERNATE SETTINGS ───
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=false
# ─── SERVER PORT ───
# ─── SERVER PORT & HTTP GZIP COMPRESSION ───
server.port=8085
server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css
server.compression.min-response-size=1024
# ─── GOOGLE OAUTH2 CONFIGURATION ───
google.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com}
@@ -23,10 +33,8 @@ admin.super.password=${SUPER_ADMIN_PASSWORD:RIT@2026}
admin.username=${ADMIN_USERNAME:ritadmin}
admin.password=${ADMIN_PASSWORD:ritadmin2026}
# ─── IMS INTEGRATION CONFIGURATION ───
# Options: "mock" (active now for testing & simulation) | "real" (for live college IMS)
ims.client.type=mock
ims.base-url=${IMS_BASE_URL:https://api.ims.ritchennai.edu.in}
ims.api-key=${IMS_API_KEY:rit_nexus_vps_secret_key_2026}

View File

@@ -0,0 +1,58 @@
package com.rit.portal;
import com.rit.portal.model.BusLocation;
import com.rit.portal.service.BusLocationService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class BusLocationServiceTest {
private BusLocationService busLocationService;
@BeforeEach
void setUp() {
busLocationService = new BusLocationService();
}
@Test
void testUpdateAndGetLocation() {
busLocationService.updateLocation("24A", 13.0827, 80.2707);
BusLocation loc = busLocationService.getLocation("24A");
assertNotNull(loc);
assertEquals("24A", loc.getRouteNumber());
assertEquals(13.0827, loc.getLatitude());
assertEquals(80.2707, loc.getLongitude());
assertFalse(loc.isStopped());
assertNotNull(loc.getLastUpdated());
}
@Test
void testStopLocation() {
busLocationService.updateLocation("15B", 12.9716, 77.5946);
busLocationService.stopLocation("15B");
BusLocation loc = busLocationService.getLocation("15B");
assertNotNull(loc);
assertTrue(loc.isStopped());
}
@Test
void testGetNonExistentLocationReturnsNull() {
BusLocation loc = busLocationService.getLocation("NON_EXISTENT");
assertNull(loc);
}
@Test
void testGetActiveLocations() {
busLocationService.updateLocation("R1", 13.0, 80.0);
busLocationService.updateLocation("R2", 13.1, 80.1);
List<BusLocation> active = busLocationService.getActiveLocations();
assertEquals(2, active.size());
}
}

View File

@@ -0,0 +1,60 @@
package com.rit.portal;
import com.rit.portal.entity.CommunityAnswer;
import com.rit.portal.entity.CommunityQuestion;
import com.rit.portal.entity.NotePyq;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
public class EntityMappingTest {
@Test
void testNotePyqBuilderDefaults() {
NotePyq note = NotePyq.builder()
.title("Data Structures Notes")
.subject("Data Structures")
.department("CSE")
.semester(3)
.fileType("notes")
.downloadUrl("https://example.com/ds.pdf")
.fileSize("2.4 MB")
.build();
assertNotNull(note);
assertEquals(0, note.getDownloadsCount());
assertNotNull(note.getUploadedAt());
assertTrue(note.getUploadedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
}
@Test
void testCommunityQuestionBuilderDefaults() {
CommunityQuestion question = CommunityQuestion.builder()
.title("How to prepare for CAT-1?")
.body("Looking for reference materials and past questions.")
.author("Student123")
.build();
assertNotNull(question);
assertEquals(0, question.getUpvotes());
assertFalse(question.getIsAnswered());
assertNotNull(question.getTags());
assertNotNull(question.getAnswers());
assertNotNull(question.getCreatedAt());
}
@Test
void testCommunityAnswerBuilderDefaults() {
CommunityAnswer answer = CommunityAnswer.builder()
.body("Focus on Unit 1 and Unit 2 standard questions.")
.author("Senior456")
.build();
assertNotNull(answer);
assertEquals(0, answer.getUpvotes());
assertFalse(answer.getIsAccepted());
assertNotNull(answer.getCreatedAt());
}
}

1540
grade_report.html Normal file

File diff suppressed because it is too large Load Diff

3543
real_timetable.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
import re
with open('real_timetable.html', 'r', encoding='utf-8') as f:
content = f.read()
# Find rows in the timetable table
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', content, re.DOTALL)
print(f"Total tr rows: {len(rows)}")
for idx, r in enumerate(rows):
if 'tuesday' in r.lower() or 'tue' in r.lower():
print(f"\n--- ROW {idx} (TUESDAY) ---")
tds = re.findall(r'<td[^>]*>(.*?)</td>', r, re.DOTALL)
print(f"Total td cells in Tuesday row: {len(tds)}")
for col_i, td in enumerate(tds):
clean_td = ' '.join(re.sub(r'<[^>]+>', ' ', td).split())
forms_in_td = re.findall(r'<form[^>]*>(.*?)</form>', td, re.DOTALL)
print(f" Col {col_i}: forms_count={len(forms_in_td)} | text={clean_td[:80]}")

View File

@@ -0,0 +1,24 @@
import re
with open('real_timetable.html', 'r', encoding='utf-8') as f:
content = f.read()
forms = re.findall(r'<form[^>]*class="[^"]*period_form[^"]*"[^>]*>(.*?)</form>', content, re.DOTALL)
print(f"Total forms found: {len(forms)}")
by_day = {}
for form in forms:
day_m = re.search(r'name="day"\s+value="([^"]+)"', form)
per_m = re.search(r'name="period"\s+value="([^"]+)"', form)
day = day_m.group(1).lower() if day_m else 'unknown'
per = per_m.group(1) if per_m else '0'
by_day.setdefault(day, []).append((per, form))
for day in sorted(by_day.keys()):
print(f"\n=== DAY: {day.upper()} (Count: {len(by_day[day])}) ===")
for per, form in by_day[day]:
primaries = re.findall(r'class="[^"]*text-primary[^"]*"[^>]*>(.*?)</span>', form, re.DOTALL)
clean_p = [re.sub(r'<[^>]+>', '', p).strip() for p in primaries]
full_txt = re.sub(r'<[^>]+>', ' ', form).strip()
full_txt = ' '.join(full_txt.split())
print(f" Period {per}: primaries={clean_p} | full_txt={full_txt[:100]}")

View File

@@ -0,0 +1,13 @@
import re
with open('real_timetable.html', 'r', encoding='utf-8') as f:
content = f.read()
print("File length:", len(content))
# Search for Tuesday in content
tue_matches = [m.start() for m in re.finditer(r'tuesday', content, re.IGNORECASE)]
print("Tuesday matches at indices:", tue_matches)
for pos in tue_matches[:3]:
print("\n--- Context around Tuesday ---")
print(content[max(0, pos-200):min(len(content), pos+400)])

View File

@@ -0,0 +1,53 @@
import urllib.request
import urllib.parse
import http.cookiejar
import re
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
login_url = "https://ims.rajalakshmi.edu.in/ims/login"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
}
# Step 1: GET login page
req = urllib.request.Request(login_url, headers=headers)
with opener.open(req) as resp:
html = resp.read().decode('utf-8')
csrf_m = re.search(r'name="_token"\s+value="([^"]+)"', html)
csrf_token = csrf_m.group(1) if csrf_m else ''
print(f"CSRF Token: {csrf_token}")
# Step 2: POST login
post_data = urllib.parse.urlencode({
'_token': csrf_token,
'username': '2117240070293',
'password': '7010406809'
}).encode('utf-8')
req_post = urllib.request.Request(login_url, data=post_data, headers=headers)
with opener.open(req_post) as resp_post:
dash_html = resp_post.read().decode('utf-8')
print(f"Login success! Dash HTML length: {len(dash_html)}")
# Step 3: Find all <a> hrefs containing lab or assignment or mark or report
all_links = re.findall(r'<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>', dash_html, re.DOTALL)
print(f"Total links found: {len(all_links)}")
for href, label in all_links:
clean_label = re.sub(r'<[^>]+>', '', label).strip()
clean_label_lower = clean_label.lower()
if 'lab' in clean_label_lower or 'assignment' in clean_label_lower or 'mark' in clean_label_lower or 'grade' in clean_label_lower or 'cat' in clean_label_lower:
print(f"Link: '{clean_label}' -> Href: '{href}'")
# Also search for all hrefs in the full HTML to be sure
all_hrefs = re.findall(r'href="([^"]+)"', dash_html)
print("\n--- ALL UNIQUE HREFS IN DASHBOARD ---")
for h in set(all_hrefs):
if 'admin' in h or 'student' in h or 'mark' in h or 'report' in h:
print(" ", h)

View File

@@ -1,29 +1,35 @@
import { useEffect } from 'react';
import { useEffect, lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from '@/context/AuthContext';
import ProtectedRoute from '@/components/Auth/ProtectedRoute';
import MainLayout from '@/layouts/MainLayout';
import Home from '@/pages/Home/Home';
import Notes from '@/pages/Notes/Notes';
import Toolkit from '@/pages/Toolkit/Toolkit';
import AIAssistant from '@/pages/AIAssistant/AIAssistant';
import Campus from '@/pages/Campus/Campus';
import Events from '@/pages/Events/Events';
import Community from '@/pages/Community/Community';
import BusRoutes from '@/pages/BusRoutes/BusRoutes';
import Faculty from '@/pages/Faculty/Faculty';
import DevCollab from '@/pages/DevCollab/DevCollab';
import LeetcodeLeaderboard from '@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard';
import AdminDashboard from '@/pages/Admin/AdminDashboard';
import Raise from '@/pages/Raise/Raise';
import StudentDashboard from '@/pages/StudentDashboard/StudentDashboard';
import PageLoader from '@/components/PageLoader/PageLoader';
import ErrorBoundary from '@/components/ErrorBoundary/ErrorBoundary';
// Code-split route components with dynamic imports for minimal initial bundle size
const Home = lazy(() => import('@/pages/Home/Home'));
const Notes = lazy(() => import('@/pages/Notes/Notes'));
const Toolkit = lazy(() => import('@/pages/Toolkit/Toolkit'));
const AIAssistant = lazy(() => import('@/pages/AIAssistant/AIAssistant'));
const Campus = lazy(() => import('@/pages/Campus/Campus'));
const Events = lazy(() => import('@/pages/Events/Events'));
const Community = lazy(() => import('@/pages/Community/Community'));
const BusRoutes = lazy(() => import('@/pages/BusRoutes/BusRoutes'));
const Faculty = lazy(() => import('@/pages/Faculty/Faculty'));
const DevCollab = lazy(() => import('@/pages/DevCollab/DevCollab'));
const LeetcodeLeaderboard = lazy(() => import('@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard'));
const AdminDashboard = lazy(() => import('@/pages/Admin/AdminDashboard'));
const Raise = lazy(() => import('@/pages/Raise/Raise'));
const StudentDashboard = lazy(() => import('@/pages/StudentDashboard/StudentDashboard'));
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
},
},
});
@@ -40,71 +46,87 @@ function ScrollToTop() {
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
<ScrollToTop />
<Routes>
{/* Standalone Admin Route */}
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/admin/*" element={<AdminDashboard />} />
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
<ScrollToTop />
<Suspense fallback={<PageLoader />}>
<Routes>
{/* Standalone Admin Route */}
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/admin/*" element={<AdminDashboard />} />
{/* Standalone RAISE Route */}
<Route path="/raise" element={<Raise />} />
{/* Standalone RAISE Route */}
<Route path="/raise" element={<Raise />} />
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/notes" element={<Notes />} />
<Route path="/toolkit" element={<Toolkit />} />
<Route path="/ai-assistant" element={<AIAssistant />} />
<Route path="/campus" element={<Campus />} />
<Route path="/bus-routes" element={<BusRoutes />} />
<Route path="/faculty" element={<Faculty />} />
<Route path="/events" element={<Events />} />
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/notes" element={<Notes />} />
<Route path="/toolkit" element={<Toolkit />} />
<Route path="/ai-assistant" element={<AIAssistant />} />
<Route path="/campus" element={<Campus />} />
<Route path="/bus-routes" element={<BusRoutes />} />
<Route path="/faculty" element={<Faculty />} />
<Route path="/events" element={<Events />} />
{/* Student Dashboard — Timetable, Staff, Location (IMS Reg No & Password) */}
<Route path="/dashboard" element={<StudentDashboard />} />
{/* Student Dashboard & Timetable — Real IMS Reg No & Password */}
<Route path="/dashboard" element={<StudentDashboard />} />
<Route path="/timetable" element={<StudentDashboard />} />
{/* Protected Routes — Require Google Sign-In */}
<Route
path="/community"
element={
<ProtectedRoute
title="RIT Q&A Community"
description="Connect with fellow RIT students and ask senior helpers any question about campus life and academics."
>
<Community />
</ProtectedRoute>
}
/>
<Route
path="/collab"
element={
<ProtectedRoute
title="Developer Collab Hub"
description="Post open-source projects, search for co-developers, and connect with fellow programmers."
requireVerifiedStudent={true}
>
<DevCollab />
</ProtectedRoute>
}
/>
<Route
path="/leetcode"
element={
<ProtectedRoute
title="RIT LeetCode Leaderboard"
description="Track campus competitive programming rankings and see top RIT coders."
>
<LeetcodeLeaderboard />
</ProtectedRoute>
}
/>
</Route>
</Routes>
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
{/* Protected Routes — Require Google Sign-In */}
<Route
path="/community"
element={
<ProtectedRoute
title="RIT Q&A Community"
description="Connect with fellow RIT students and ask senior helpers any question about campus life and academics."
>
<Community />
</ProtectedRoute>
}
/>
<Route
path="/collab"
element={
<ProtectedRoute
title="Developer Collab Hub"
description="Post open-source projects, search for co-developers, and connect with fellow programmers."
requireVerifiedStudent={true}
>
<DevCollab />
</ProtectedRoute>
}
/>
<Route
path="/leetcode"
element={
<ProtectedRoute
title="RIT Student Leaderboard"
description="Track campus competitive programming rankings (LeetCode & SkillRack) and verified skill certifications."
>
<LeetcodeLeaderboard />
</ProtectedRoute>
}
/>
<Route
path="/student-leaderboard"
element={
<ProtectedRoute
title="RIT Student Leaderboard"
description="Track campus competitive programming rankings (LeetCode & SkillRack) and verified skill certifications."
>
<LeetcodeLeaderboard />
</ProtectedRoute>
}
/>
</Route>
</Routes>
</Suspense>
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
</ErrorBoundary>
);
}

View File

@@ -224,43 +224,27 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp
)}
</button>
{/* Quick Demo Fill Buttons */}
{/* Quick Demo Fill Buttons for Admins */}
<div className="pt-3 border-t border-white/10 space-y-1.5">
<span className="text-[10px] text-slate-400 font-semibold block text-center uppercase tracking-wider">
Quick Demo Credentials:
Official Admin Portals:
</span>
<div className="grid grid-cols-2 gap-1.5 text-[11px]">
<button
type="button"
onClick={() => handleQuickFill('2114251001', 'rit@2026')}
className="p-2 rounded-lg bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
>
<span className="truncate">🎓 CSE Student</span>
<span className="text-[9px] text-orange-400 font-mono">Fill</span>
</button>
<button
type="button"
onClick={() => handleQuickFill('2114251002', 'rit@2026')}
className="p-2 rounded-lg bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
>
<span className="truncate">🎓 AI&DS Student</span>
<span className="text-[9px] text-orange-400 font-mono">Fill</span>
</button>
<button
type="button"
onClick={() => handleQuickFill('Transport', 'RIT@2026')}
className="p-2 rounded-lg bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
className="p-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
>
<span className="truncate">🚌 Transport Admin</span>
<span className="text-[9px] text-blue-400 font-mono">Fill</span>
<span className="text-[9px] text-blue-400 font-mono">Select</span>
</button>
<button
type="button"
onClick={() => handleQuickFill('Admin', 'RIT@2026')}
className="p-2 rounded-lg bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
className="p-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left flex items-center justify-between transition-colors cursor-pointer"
>
<span className="truncate">⚡ Super Admin</span>
<span className="text-[9px] text-emerald-400 font-mono">Fill</span>
<span className="text-[9px] text-emerald-400 font-mono">Select</span>
</button>
</div>
</div>

View File

@@ -0,0 +1,91 @@
import { Component, type ErrorInfo, type ReactNode } from 'react';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('[ErrorBoundary caught error]:', error, errorInfo);
}
private handleReset = () => {
this.setState({ hasError: false, error: null });
window.location.reload();
};
private handleGoHome = () => {
this.setState({ hasError: false, error: null });
window.location.href = '/';
};
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-[70vh] flex items-center justify-center p-6">
<div className="max-w-md w-full bg-card/80 backdrop-blur-xl border border-destructive/20 rounded-2xl p-8 shadow-2xl text-center space-y-6">
<div className="w-16 h-16 mx-auto rounded-2xl bg-destructive/10 border border-destructive/20 flex items-center justify-center text-destructive shadow-inner">
<AlertTriangle className="w-8 h-8 animate-bounce" />
</div>
<div className="space-y-2">
<h2 className="text-xl font-bold tracking-tight text-foreground">
Something went wrong
</h2>
<p className="text-sm text-muted-foreground leading-relaxed">
An unexpected error occurred while rendering this section. You can try refreshing the page or navigating back home.
</p>
</div>
{this.state.error && (
<div className="p-3 rounded-lg bg-muted/50 border border-border text-left overflow-x-auto text-xs font-mono text-muted-foreground max-h-24">
{this.state.error.message}
</div>
)}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-2">
<button
onClick={this.handleReset}
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl bg-orange-600 hover:bg-orange-700 text-white text-sm font-medium transition-all shadow-lg shadow-orange-600/25 hover:shadow-orange-600/40 active:scale-95 cursor-pointer"
>
<RefreshCw className="w-4 h-4" />
Try Again
</button>
<button
onClick={this.handleGoHome}
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl bg-secondary hover:bg-secondary/80 text-foreground text-sm font-medium transition-all active:scale-95 border border-border cursor-pointer"
>
<Home className="w-4 h-4" />
Go to Home
</button>
</div>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -10,7 +10,7 @@ const exploreLinks = [
{ label: 'Student Toolkit', path: '/toolkit', icon: Wrench },
{ label: 'RIT Chatbot', path: '/ai-assistant', icon: Bot },
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
{ label: 'LeetCode Arena', path: '/leetcode', icon: Trophy },
{ label: 'Student Leaderboard', path: '/student-leaderboard', icon: Trophy },
];
const campusLinks = [

View File

@@ -36,7 +36,7 @@ interface NavItem {
const PRIMARY_LINKS: NavItem[] = [
{ label: 'Home', path: '/', icon: HomeIcon },
{ label: 'Time table', path: '/dashboard', icon: LayoutDashboard },
{ label: 'Student dashboard', path: '/dashboard', icon: LayoutDashboard },
{ label: 'Notes', path: '/notes', icon: BookOpen },
{ label: 'Bus Routes', path: '/bus-routes', icon: Bus },
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
@@ -49,7 +49,7 @@ const MORE_LINKS: NavItem[] = [
{ label: 'Campus Map', path: '/campus', icon: Map },
{ label: 'Clubs & Centers', path: '/events', icon: Users },
{ label: 'Dev Collab', path: '/collab', icon: Code2 },
{ label: 'LeetCode', path: '/leetcode', icon: Trophy },
{ label: 'Student Leaderboard', path: '/student-leaderboard', icon: Trophy },
{ label: 'Student Gate Pass', path: '/notes?portal=gatepass', icon: Ticket },
{ label: 'Student Proficiency', path: '/notes?portal=proficiency', icon: Award },
];
@@ -318,7 +318,7 @@ export default function Navbar() {
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-orange-500/15 hover:border-orange-500/20 text-xs font-semibold transition-all cursor-pointer"
>
<GraduationCap className="w-4 h-4 text-orange-400" />
My Time Table
Student Dashboard
</Link>
)}
<button

View File

@@ -0,0 +1,24 @@
import React from 'react';
const PageLoader: React.FC = () => {
return (
<div className="min-h-[60vh] flex flex-col items-center justify-center p-6 space-y-4">
<div className="relative flex items-center justify-center">
{/* Outer glowing ripple */}
<div className="absolute w-16 h-16 rounded-full bg-orange-500/20 animate-ping" />
{/* Spinning gradient ring */}
<div className="w-12 h-12 rounded-full border-3 border-transparent border-t-orange-500 border-r-orange-400 animate-spin" />
{/* Center dot */}
<div className="absolute w-4 h-4 rounded-full bg-gradient-to-tr from-orange-600 to-amber-400 shadow-lg shadow-orange-500/50" />
</div>
<div className="flex flex-col items-center space-y-1">
<p className="text-sm font-semibold tracking-wide bg-gradient-to-r from-orange-600 via-amber-500 to-orange-400 bg-clip-text text-transparent animate-pulse">
Loading Freshers Hub
</p>
<p className="text-xs text-muted-foreground">Preparing your campus experience...</p>
</div>
</div>
);
};
export default PageLoader;

View File

@@ -12,7 +12,7 @@ export const NAV_LINKS = [
{ label: 'Clubs & Centers', path: '/events' },
{ label: 'Community', path: '/community' },
{ label: 'Dev Collab', path: '/collab' },
{ label: 'LeetCode', path: '/leetcode' },
{ label: 'Student Leaderboard', path: '/student-leaderboard' },
];
// ─── Stats ────────────────────────────────────────────────────────────────────
@@ -110,9 +110,9 @@ export const FEATURES: Feature[] = [
{
id: 'leetcode',
icon: 'Trophy',
title: 'LeetCode Arena',
description: 'Track campus competitive programming rankings and see top RIT coders.',
path: '/leetcode',
title: 'Student Leaderboard',
description: 'Track campus competitive programming rankings (LeetCode & SkillRack) and verified skill certifications.',
path: '/student-leaderboard',
color: '#F59E0B',
bgColor: '#FFFBEB',
},
@@ -3832,9 +3832,5 @@ export const DEPARTMENTS = [
'Computer Science & Engineering',
'Electronics & Communication Engineering',
'Electronics Engineering VLSI (Design and Technology)',
'Humanities & Sciences',
'M.E. (VLSI Design)',
'M.Tech (Data Science)',
'Mathematics',
'Mechanical Engineering',
];

View File

@@ -14,7 +14,7 @@ export interface AuthUser {
name: string;
pictureUrl?: string;
verifiedStudent: boolean;
role?: 'ROLE_STUDENT' | 'ROLE_TRANSPORT' | 'ROLE_SUPER_ADMIN' | 'ROLE_USER';
role?: 'ROLE_STUDENT' | 'ROLE_TRANSPORT' | 'ROLE_COMMUNITY' | 'ROLE_CLUBS' | 'ROLE_CURRICULUM' | 'ROLE_SUPER_ADMIN' | 'ROLE_USER';
regNumber?: string;
department?: string;
}
@@ -32,6 +32,9 @@ interface AuthContextType {
isVerifiedStudent: boolean;
isAdmin: boolean;
isTransportAdmin: boolean;
isCommunityAdmin: boolean;
isClubsAdmin: boolean;
isCurriculumAdmin: boolean;
isLoading: boolean;
isAuthModalOpen: boolean;
loginWithGoogle: () => void;
@@ -59,6 +62,9 @@ const AuthContext = createContext<AuthContextType>({
isVerifiedStudent: false,
isAdmin: false,
isTransportAdmin: false,
isCommunityAdmin: false,
isClubsAdmin: false,
isCurriculumAdmin: false,
isLoading: true,
isAuthModalOpen: false,
loginWithGoogle: () => {},
@@ -200,48 +206,57 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return { success: false, message: 'Please enter both username/register number and password.' };
}
// 1. First, check if credentials match Admin / Transport
try {
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: cleanedUser, password: cleanedPass }),
});
const isAdminUsername = [
'admin', 'transport', 'transportadmin',
'community', 'communityadmin', 'qa', 'seniorqa',
'clubs', 'club', 'clubsadmin',
'curriculum', 'curriculumadmin', 'gpa', 'gpaadmin', 'academics',
'ritadmin', 'superadmin'
].includes(cleanedUser.toLowerCase());
if (adminRes.ok) {
const data = await adminRes.json();
if (data.success && data.token && data.role) {
localStorage.setItem(ADMIN_TOKEN_KEY, data.token);
localStorage.setItem(ADMIN_ROLE_KEY, data.role);
localStorage.setItem(ADMIN_USER_KEY, data.username);
// 1. First, check if credentials match Admin / Role Endpoints
if (isAdminUsername) {
try {
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: cleanedUser, password: cleanedPass }),
});
const adminUserObj: AuthUser = {
id: 999999,
name: data.username,
email: data.role === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in',
verifiedStudent: false,
role: data.role,
};
setUser(adminUserObj);
setIsAuthModalOpen(false);
if (adminRes.ok) {
const data = await adminRes.json();
if (data.success && data.token && data.role) {
localStorage.setItem(ADMIN_TOKEN_KEY, data.token);
localStorage.setItem(ADMIN_ROLE_KEY, data.role);
localStorage.setItem(ADMIN_USER_KEY, data.username);
return {
success: true,
role: data.role,
redirectTo: '/admin',
message: `${data.username} logged in successfully`,
};
const adminUserObj: AuthUser = {
id: 999999,
name: data.username,
email: `${cleanedUser.toLowerCase()}@ritchennai.edu.in`,
verifiedStudent: false,
role: data.role,
};
setUser(adminUserObj);
setIsAuthModalOpen(false);
return {
success: true,
role: data.role,
redirectTo: '/admin',
message: `${data.username} logged in successfully`,
};
}
}
} catch {
// If admin endpoint fails or is offline, check fallback
}
} catch {
// If admin endpoint fails or is offline, check fallback
}
// Direct local check for Admin credentials fallback
if (
cleanedUser.toLowerCase() === 'transport' &&
cleanedPass === 'RIT@2026'
) {
const isStdPass = cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026';
// Direct local check for Admin credentials fallbacks
if (['transport', 'transportadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
const role = 'ROLE_TRANSPORT';
localStorage.setItem(ADMIN_TOKEN_KEY, 'TRANSPORT_SESSION_TOKEN_RIT_2026');
localStorage.setItem(ADMIN_ROLE_KEY, role);
@@ -257,9 +272,57 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return { success: true, role, redirectTo: '/admin' };
}
if (['community', 'qa', 'communityadmin', 'seniorqa'].includes(cleanedUser.toLowerCase()) && isStdPass) {
const role = 'ROLE_COMMUNITY';
localStorage.setItem(ADMIN_TOKEN_KEY, 'COMMUNITY_SESSION_TOKEN_RIT_2026');
localStorage.setItem(ADMIN_ROLE_KEY, role);
localStorage.setItem(ADMIN_USER_KEY, 'Community & Q&A Admin');
setUser({
id: 999999,
name: 'Community & Q&A Admin',
email: 'community@ritchennai.edu.in',
verifiedStudent: false,
role,
});
setIsAuthModalOpen(false);
return { success: true, role, redirectTo: '/admin' };
}
if (['clubs', 'club', 'clubsadmin', 'clubadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
const role = 'ROLE_CLUBS';
localStorage.setItem(ADMIN_TOKEN_KEY, 'CLUBS_SESSION_TOKEN_RIT_2026');
localStorage.setItem(ADMIN_ROLE_KEY, role);
localStorage.setItem(ADMIN_USER_KEY, 'Clubs & Centers Admin');
setUser({
id: 999999,
name: 'Clubs & Centers Admin',
email: 'clubs@ritchennai.edu.in',
verifiedStudent: false,
role,
});
setIsAuthModalOpen(false);
return { success: true, role, redirectTo: '/admin' };
}
if (['curriculum', 'gpa', 'academics', 'gpaadmin', 'curriculumadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
const role = 'ROLE_CURRICULUM';
localStorage.setItem(ADMIN_TOKEN_KEY, 'CURRICULUM_SESSION_TOKEN_RIT_2026');
localStorage.setItem(ADMIN_ROLE_KEY, role);
localStorage.setItem(ADMIN_USER_KEY, 'GPA Curriculum Admin');
setUser({
id: 999999,
name: 'GPA Curriculum Admin',
email: 'curriculum@ritchennai.edu.in',
verifiedStudent: false,
role,
});
setIsAuthModalOpen(false);
return { success: true, role, redirectTo: '/admin' };
}
if (
(cleanedUser.toLowerCase() === 'admin' && (cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026')) ||
(cleanedUser.toLowerCase() === 'ritadmin' && cleanedPass === 'ritadmin2026')
(['admin', 'superadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) ||
(cleanedUser.toLowerCase() === 'ritadmin' && (cleanedPass === 'ritadmin2026' || isStdPass))
) {
const role = 'ROLE_SUPER_ADMIN';
localStorage.setItem(ADMIN_TOKEN_KEY, 'ADMIN_SESSION_TOKEN_RIT_2026');
@@ -296,17 +359,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
success: true,
role: 'ROLE_STUDENT',
redirectTo: '/dashboard',
message: 'Student login successful',
message: `Welcome ${imsResult.student.name}!`,
};
} else {
return {
success: false,
message: imsResult.message || 'Authentication failed. Please check your credentials.',
};
}
} catch {
// Fallback
} catch (err: any) {
return {
success: false,
message: err.message || 'Network error while attempting login. Please try again.',
};
}
return {
success: false,
message: 'Invalid credentials. Enter your Register Number or Admin credentials (Password: rit@2026 / RIT@2026).',
};
}, []);
const logout = useCallback(() => {
@@ -324,8 +390,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setIsAuthModalOpen(false);
}, [user]);
const isAdmin = user?.role === 'ROLE_SUPER_ADMIN' || user?.role === 'ROLE_TRANSPORT';
const isSuperAdmin = user?.role === 'ROLE_SUPER_ADMIN';
const isTransportAdmin = user?.role === 'ROLE_TRANSPORT';
const isCommunityAdmin = user?.role === 'ROLE_COMMUNITY';
const isClubsAdmin = user?.role === 'ROLE_CLUBS';
const isCurriculumAdmin = user?.role === 'ROLE_CURRICULUM';
const isAdmin = isSuperAdmin || isTransportAdmin || isCommunityAdmin || isClubsAdmin || isCurriculumAdmin;
return (
<AuthContext.Provider
@@ -335,6 +405,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
isVerifiedStudent: !!user?.verifiedStudent,
isAdmin,
isTransportAdmin,
isCommunityAdmin,
isClubsAdmin,
isCurriculumAdmin,
isLoading,
isAuthModalOpen,
loginWithGoogle,

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,8 @@ import { getBackendUrl } from '@/lib/utils';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
import { useAuth } from '@/context/AuthContext';
const TRENDING_TAGS = ['hostel', 'academics', 'clubs', 'campus', 'canteen', 'sports', 'placement', 'library', 'cat exam', 'semester', 'labs', 'exams'];
const AVATARS: Record<string, string> = {

View File

@@ -45,11 +45,13 @@ function normalizeDepartmentName(input?: string | null): string | null {
export default function Faculty() {
const [searchParams] = useSearchParams();
const [searchFaculty, setSearchFaculty] = useState('');
const [searchFaculty, setSearchFaculty] = useState(() => searchParams.get('search') || '');
const [sortBy, setSortBy] = useState('Name A-Z');
// Initial department detection (from URL query string or logged-in IMS student session)
const initialDepartment = useMemo(() => {
if (searchParams.has('search')) return 'All Departments';
const queryDept = searchParams.get('dept');
const matchedQuery = normalizeDepartmentName(queryDept);
if (matchedQuery) return matchedQuery;
@@ -65,10 +67,16 @@ export default function Faculty() {
const [selectedDept, setSelectedDept] = useState<string>(initialDepartment);
// Update selectedDept if URL parameter changes
// Update selectedDept and searchFaculty if URL parameter changes
useEffect(() => {
setSelectedDept(initialDepartment);
}, [initialDepartment]);
const querySearch = searchParams.get('search');
if (querySearch !== null) {
setSearchFaculty(querySearch);
setSelectedDept('All Departments');
} else {
setSelectedDept(initialDepartment);
}
}, [searchParams, initialDepartment]);
const studentSession = useMemo(() => getStoredImsSession(), []);
const isStudentDeptAutoFiltered = selectedDept !== 'All Departments' && (
@@ -286,7 +294,7 @@ export default function Faculty() {
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer min-w-[200px]"
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
>
{DEPARTMENTS.filter(dept => dept === 'All Departments' || FACULTY_DATA.filter((f) => f.department === dept).length > 0).map((dept) => {
{Array.from(new Set(['All Departments', ...DEPARTMENTS, ...FACULTY_DATA.map(f => f.department)])).filter(dept => dept === 'All Departments' || FACULTY_DATA.filter((f) => f.department === dept).length > 0).map((dept) => {
const count = dept === 'All Departments' ? FACULTY_DATA.length : FACULTY_DATA.filter((f) => f.department === dept).length;
return <option key={dept} value={dept}>{dept} ({count})</option>;
})}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,30 @@
{
"community_bot_token": "",
"telegram_bot_token": "",
"helper_chat_ids": [971749136, 5567776672, 1873240361],
"community_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
"telegram_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
"bot_username": "FreshersCommunityBot",
"helper_chat_ids": [
1873240361,
5567776672,
8518850169,
7238144438
],
"seniorHelpers": [
{
"chatId": 1873240361,
"name": "Senior Mentor (CSE)"
},
{
"chatId": 5567776672,
"name": "Senior Responder (ECE)"
},
{
"chatId": 8518850169,
"name": "Senior Responder (IT)"
},
{
"chatId": 7238144438,
"name": "Senior Responder (AIDS)"
}
],
"spring_backend_url": "http://localhost:8085"
}

View File

@@ -37,8 +37,8 @@ CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
def load_config():
if not os.path.exists(CONFIG_PATH):
default_config = {
"community_bot_token": "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo",
"telegram_bot_token": "8913773505:AAHASuKLLOto3Ax573_dxg8bnvQy2ML6yLk",
"community_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
"telegram_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
"helper_chat_ids": [],
"spring_backend_url": "http://localhost:8085"
}
@@ -50,7 +50,7 @@ def load_config():
return json.load(f)
config = load_config()
COMMUNITY_BOT_TOKEN = os.environ.get("COMMUNITY_BOT_TOKEN") or config.get("community_bot_token", "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo")
COMMUNITY_BOT_TOKEN = os.environ.get("COMMUNITY_BOT_TOKEN") or config.get("community_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY")
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") or config.get("telegram_bot_token")
BACKEND_URL = os.environ.get("SPRING_BACKEND_URL") or config.get("spring_backend_url", "http://localhost:8085")
@@ -483,6 +483,33 @@ def telegram_polling_thread():
logging.info(f"Received message from chat {chat_id}: '{text}'")
# Handle Community Q&A reply if this is a reply to a forwarded question
reply_to = message.get("reply_to_message")
if reply_to:
original_message_id = reply_to["message_id"]
question_id = get_question_id(chat_id, original_message_id)
if question_id:
first_name = message["from"].get("first_name", "")
last_name = message["from"].get("last_name", "")
author_name = f"{first_name} {last_name}".strip() or "Senior Helper"
logging.info(f"Submitting answer for question {question_id} by helper '{author_name}'")
backend_endpoint = f"{backend_url}/api/questions/{question_id}/answers"
answer_payload = {
"body": text,
"author": author_name
}
try:
res = requests.post(backend_endpoint, json=answer_payload, timeout=10)
if res.status_code in [200, 201]:
send_telegram_message(chat_id, "✅ *Answer posted successfully to the Q&A board!*", reply_to_message_id=message["message_id"], token=bot_token)
else:
send_telegram_message(chat_id, f"❌ *Failed to post answer to backend.* (Status: {res.status_code})\nResponse: {res.text[:100]}", reply_to_message_id=message["message_id"], token=bot_token)
except Exception as e:
logging.error(f"Error calling backend endpoint {backend_endpoint}: {e}")
send_telegram_message(chat_id, f"❌ *Connection error to backend.* ({e})", reply_to_message_id=message["message_id"], token=bot_token)
continue
# Handle /remove command in Telegram
if text.lower() in ["/remove", "/cancel"]:
try:
@@ -714,9 +741,16 @@ async def run_uvicorn():
await server.serve()
async def main():
comm_thread = threading.Thread(target=community_bot_polling_thread, daemon=True)
comm_thread.start()
current_config = load_config()
comm_token = os.environ.get("COMMUNITY_BOT_TOKEN") or current_config.get("community_bot_token", COMMUNITY_BOT_TOKEN)
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") or current_config.get("telegram_bot_token", BOT_TOKEN)
# If separate bot tokens are configured, run dedicated community polling thread
if comm_token and bot_token and comm_token != bot_token:
comm_thread = threading.Thread(target=community_bot_polling_thread, daemon=True)
comm_thread.start()
# Main unified thread handles Dev Collab, Chatbot, and unified Q&A
chat_thread = threading.Thread(target=telegram_polling_thread, daemon=True)
chat_thread.start()

View File

@@ -50,8 +50,67 @@ export default defineConfig({
}
})
],
build: {
target: 'esnext',
chunkSizeWarningLimit: 800,
rollupOptions: {
output: {
manualChunks(id: string) {
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom') || id.includes('react-router-dom')) {
return 'vendor-react';
}
if (id.includes('framer-motion') || id.includes('lucide-react')) {
return 'vendor-ui';
}
if (id.includes('@radix-ui')) {
return 'vendor-radix';
}
if (id.includes('@tanstack')) {
return 'vendor-tanstack';
}
if (id.includes('leaflet') || id.includes('react-leaflet')) {
return 'vendor-map';
}
}
}
}
}
},
server: {
host: true,
proxy: {
'/ims': {
target: 'https://ims.ritchennai.edu.in',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/ims/, ''),
secure: false,
configure: (proxy) => {
proxy.on('proxyRes', (proxyRes) => {
// Rewrite Location header to keep redirects through local / VPS proxy
const location = proxyRes.headers['location'];
if (location) {
let rewritten = location;
if (rewritten.includes('ims.ritchennai.edu.in')) {
rewritten = rewritten.replace(/https?:\/\/ims\.ritchennai\.edu\.in/gi, '/ims');
} else if (rewritten.startsWith('/') && !rewritten.startsWith('/ims')) {
rewritten = '/ims' + rewritten;
}
proxyRes.headers['location'] = rewritten;
}
// Strip domain from cookies so session cookies bind to current host (localhost or VPS domain)
const setCookie = proxyRes.headers['set-cookie'];
if (setCookie) {
proxyRes.headers['set-cookie'] = (Array.isArray(setCookie) ? setCookie : [setCookie]).map((cookie: string) =>
cookie
.replace(/domain=[^;]+/gi, '')
.replace(/path=[^;]+/gi, 'Path=/')
);
}
});
}
}
},
watch: {
ignored: [
'**/backend/**',