Compare commits
13 Commits
64f0c5d754
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f368c83246 | ||
|
|
db5ac8e2ea | ||
|
|
a19c36579e | ||
|
|
6be263771d | ||
|
|
9275722e16 | ||
|
|
6740400788 | ||
|
|
6d386273fb | ||
|
|
56c3849357 | ||
|
|
f58c98cd46 | ||
|
|
8c7a3d7f1a | ||
|
|
f8047b6cd3 | ||
|
|
72c213f2b6 | ||
|
|
e6ea5cb302 |
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
if (!found) {
|
||||
Map<String, Object> newHelper = new HashMap<>();
|
||||
newHelper.put("chatId", chatId);
|
||||
newHelper.put("name", nameStr);
|
||||
helpers.add(newHelper);
|
||||
helperIds.add(chatId);
|
||||
}
|
||||
|
||||
config.put("seniorHelpers", helpers);
|
||||
config.put("helper_chat_ids", helperIds);
|
||||
saveTelegramConfigFile(config);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers));
|
||||
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);
|
||||
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) {}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers));
|
||||
@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));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
15
src/App.tsx
15
src/App.tsx
@@ -102,8 +102,19 @@ function App() {
|
||||
path="/leetcode"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
title="RIT LeetCode Leaderboard"
|
||||
description="Track campus competitive programming rankings and see top RIT coders."
|
||||
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>
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,9 +206,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
return { success: false, message: 'Please enter both username/register number and password.' };
|
||||
}
|
||||
|
||||
const isAdminUsername = ['admin', 'transport', 'ritadmin', 'superadmin'].includes(cleanedUser.toLowerCase());
|
||||
const isAdminUsername = [
|
||||
'admin', 'transport', 'transportadmin',
|
||||
'community', 'communityadmin', 'qa', 'seniorqa',
|
||||
'clubs', 'club', 'clubsadmin',
|
||||
'curriculum', 'curriculumadmin', 'gpa', 'gpaadmin', 'academics',
|
||||
'ritadmin', 'superadmin'
|
||||
].includes(cleanedUser.toLowerCase());
|
||||
|
||||
// 1. First, check if credentials match Admin / Transport (only for admin accounts)
|
||||
// 1. First, check if credentials match Admin / Role Endpoints
|
||||
if (isAdminUsername) {
|
||||
try {
|
||||
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
||||
@@ -221,7 +233,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const adminUserObj: AuthUser = {
|
||||
id: 999999,
|
||||
name: data.username,
|
||||
email: data.role === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in',
|
||||
email: `${cleanedUser.toLowerCase()}@ritchennai.edu.in`,
|
||||
verifiedStudent: false,
|
||||
role: data.role,
|
||||
};
|
||||
@@ -241,11 +253,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -261,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');
|
||||
@@ -299,23 +358,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
return {
|
||||
success: true,
|
||||
role: 'ROLE_STUDENT',
|
||||
redirectTo: '/timetable',
|
||||
message: 'Student login successful',
|
||||
redirectTo: '/dashboard',
|
||||
message: `Welcome ${imsResult.student.name}!`,
|
||||
};
|
||||
} else if (!imsResult.success && imsResult.message) {
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: imsResult.message,
|
||||
message: imsResult.message || 'Authentication failed. Please check your credentials.',
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Invalid credentials. Enter your Register Number or Admin credentials (Password: rit@2026 / RIT@2026).',
|
||||
message: err.message || 'Network error while attempting login. Please try again.',
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
@@ -333,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
|
||||
@@ -344,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
File diff suppressed because it is too large
Load Diff
@@ -176,6 +176,20 @@ export default function StudentDashboard() {
|
||||
);
|
||||
if (firstActive) setSelectedDay(firstActive);
|
||||
}
|
||||
|
||||
// Auto-load Attendance, Internal Marks, and Grades for immediate Overview KPI display
|
||||
const [attData, catData, assignData, labData, gData] = await Promise.all([
|
||||
fetchImsAttendance(token, p.registerNumber, force),
|
||||
fetchImsCatMarks(token, p.registerNumber, force),
|
||||
fetchImsAssignmentMarks(p.departmentCode || p.department, p.semester),
|
||||
fetchImsLabMarks(p.departmentCode || p.department, p.semester),
|
||||
fetchImsSemesterResults(token, p.registerNumber, p.semester, force)
|
||||
]);
|
||||
setAttendance(attData);
|
||||
setCatMarks(catData);
|
||||
setAssignmentMarks(assignData);
|
||||
setLabMarks(labData);
|
||||
setGradesData(gData);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading initial IMS data:', err);
|
||||
setError(err.message || 'Failed to load timetable.');
|
||||
@@ -187,10 +201,10 @@ export default function StudentDashboard() {
|
||||
// ─── Lazy On-Demand Fetchers ────────────────────────────────────────
|
||||
|
||||
const ensureAttendanceLoaded = async (force = false) => {
|
||||
if ((attendance && !force) || !session?.token || !profile) return;
|
||||
if ((attendance && attendance.subjects.length > 0 && !force) || !profile) return;
|
||||
setIsTabLoading(true);
|
||||
try {
|
||||
const data = await fetchImsAttendance(session.token, profile.registerNumber, force);
|
||||
const data = await fetchImsAttendance(session?.token || '', profile.registerNumber, force);
|
||||
setAttendance(data);
|
||||
} catch (err) {
|
||||
console.warn('Attendance load notice:', err);
|
||||
@@ -200,13 +214,13 @@ export default function StudentDashboard() {
|
||||
};
|
||||
|
||||
const ensureCatMarksLoaded = async (force = false) => {
|
||||
if ((catMarks && assignmentMarks && labMarks && !force) || !session?.token || !profile) return;
|
||||
if ((catMarks && catMarks.subjects.length > 0 && assignmentMarks && labMarks && !force) || !profile) return;
|
||||
setIsTabLoading(true);
|
||||
try {
|
||||
const [cat, assign, lab] = await Promise.all([
|
||||
fetchImsCatMarks(session.token, profile.registerNumber, force),
|
||||
fetchImsAssignmentMarks(),
|
||||
fetchImsLabMarks(),
|
||||
fetchImsCatMarks(session?.token || '', profile.registerNumber, force),
|
||||
fetchImsAssignmentMarks(profile.departmentCode || profile.department, profile.semester),
|
||||
fetchImsLabMarks(profile.departmentCode || profile.department, profile.semester),
|
||||
]);
|
||||
setCatMarks(cat);
|
||||
setAssignmentMarks(assign);
|
||||
@@ -219,10 +233,10 @@ export default function StudentDashboard() {
|
||||
};
|
||||
|
||||
const ensureGradesLoaded = async (force = false) => {
|
||||
if ((gradesData && !force) || !session?.token || !profile) return;
|
||||
if ((gradesData && gradesData.results.length > 0 && !force) || !profile) return;
|
||||
setIsTabLoading(true);
|
||||
try {
|
||||
const data = await fetchImsSemesterResults(session.token, profile.registerNumber, profile.semester, force);
|
||||
const data = await fetchImsSemesterResults(session?.token || '', profile.registerNumber, profile.semester, force);
|
||||
setGradesData(data);
|
||||
} catch (err) {
|
||||
console.warn('Grades load notice:', err);
|
||||
|
||||
@@ -241,7 +241,7 @@ export function deduceStudentDetails(regNumber: string): { department: string; d
|
||||
|
||||
export function parseDepartmentCode(rawDept: string = ''): string {
|
||||
const d = rawDept.toLowerCase();
|
||||
if (d.includes('artificial intelligence') && d.includes('data')) return 'AI&DS';
|
||||
if (d.includes('artificial intelligence') && d.includes('data')) return 'AIDS';
|
||||
if (d.includes('artificial intelligence') || d.includes('machine learning') || d.includes('aiml')) return 'AIML';
|
||||
if (d.includes('computer science and business') || d.includes('csbs')) return 'CSBS';
|
||||
if (d.includes('computer science') || d.includes('cse')) return 'CSE';
|
||||
@@ -251,7 +251,7 @@ export function parseDepartmentCode(rawDept: string = ''): string {
|
||||
if (d.includes('civil')) return 'CIVIL';
|
||||
if (d.includes('biotech')) return 'BIOTECH';
|
||||
if (d.includes('information technology') || d.includes('it')) return 'IT';
|
||||
return rawDept ? rawDept.slice(0, 4).toUpperCase() : 'AI&DS';
|
||||
return rawDept ? rawDept.slice(0, 4).toUpperCase() : 'AIDS';
|
||||
}
|
||||
|
||||
export function generateCurriculumSchedule(deptCode: string, semester: number): WeeklySchedule {
|
||||
@@ -442,14 +442,14 @@ export async function loginWithIms(regNumber: string, password: string): Promise
|
||||
try {
|
||||
// Step A: Logout any existing session first
|
||||
try {
|
||||
const loginPageRes = await fetch('/ims/login', { credentials: 'same-origin' });
|
||||
const loginPageRes = await fetch(`/ims/login?_t=${Date.now()}`, { credentials: 'same-origin', cache: 'no-store' });
|
||||
if (loginPageRes.ok) {
|
||||
const html = await loginPageRes.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const csrf = doc.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
if (csrf) {
|
||||
await fetch('/ims/admin/logout-rit', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
method: 'POST', credentials: 'same-origin', cache: 'no-store',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-TOKEN': csrf, 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: new URLSearchParams({ _token: csrf }),
|
||||
});
|
||||
@@ -458,9 +458,9 @@ export async function loginWithIms(regNumber: string, password: string): Promise
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// Step B: Get fresh CSRF token from login page
|
||||
const loginPageRes = await fetch('/ims/login', { credentials: 'same-origin' });
|
||||
const loginPageRes = await fetch(`/ims/login?_t=${Date.now()}`, { credentials: 'same-origin', cache: 'no-store' });
|
||||
if (!loginPageRes.ok) {
|
||||
return { success: false, message: 'RIT IMS Portal is currently unavailable.' };
|
||||
return { success: false, message: 'RIT IMS Portal is currently unreachable from server proxy.' };
|
||||
}
|
||||
const loginHtml = await loginPageRes.text();
|
||||
const loginDoc = new DOMParser().parseFromString(loginHtml, 'text/html');
|
||||
@@ -469,12 +469,12 @@ export async function loginWithIms(regNumber: string, password: string): Promise
|
||||
csrfToken = loginDoc.querySelector('input[name="_token"]')?.getAttribute('value') || '';
|
||||
}
|
||||
if (!csrfToken) {
|
||||
return { success: false, message: 'Could not initialize IMS session (CSRF missing).' };
|
||||
return { success: false, message: 'Could not initialize IMS session (CSRF missing from /ims/login).' };
|
||||
}
|
||||
|
||||
// Step C: POST login credentials
|
||||
const loginRes = await fetch('/ims/login', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
method: 'POST', credentials: 'same-origin', cache: 'no-store',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-TOKEN': csrfToken, 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: new URLSearchParams({ _token: csrfToken, email: cleanedReg, password: cleanedPass }),
|
||||
});
|
||||
@@ -484,7 +484,7 @@ export async function loginWithIms(regNumber: string, password: string): Promise
|
||||
}
|
||||
|
||||
// Step D: Fetch report page to verify auth + get student name + fresh CSRF
|
||||
const reportRes = await fetch('/ims/admin/grade/student/mark/report', { credentials: 'same-origin' });
|
||||
const reportRes = await fetch(`/ims/admin/grade/student/mark/report?_t=${Date.now()}`, { credentials: 'same-origin', cache: 'no-store' });
|
||||
if (!reportRes.ok) {
|
||||
return { success: false, message: 'Authentication failed — could not access student portal.' };
|
||||
}
|
||||
@@ -873,7 +873,7 @@ export async function fetchImsAttendance(_token: string, _regNumber: string, _fo
|
||||
let totalCond = 0, totalPres = 0, totalAbs = 0;
|
||||
|
||||
try {
|
||||
const res = await fetch('/ims/admin/student-personal-attendance/report', { credentials: 'same-origin' });
|
||||
const res = await fetch(`/ims/admin/student-personal-attendance/report?_t=${Date.now()}`, { credentials: 'same-origin', cache: 'no-store' });
|
||||
if (res.ok) {
|
||||
const html = await res.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
@@ -912,7 +912,7 @@ export async function fetchImsCatMarks(_token: string, _regNumber: string, _forc
|
||||
const subjects: SubjectCatMark[] = [];
|
||||
|
||||
try {
|
||||
const res = await fetch('/ims/admin/student-cat-mark/report', { credentials: 'same-origin' });
|
||||
const res = await fetch(`/ims/admin/student-cat-mark/report?_t=${Date.now()}`, { credentials: 'same-origin', cache: 'no-store' });
|
||||
if (res.ok) {
|
||||
const html = await res.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
@@ -1064,7 +1064,7 @@ export function getExactSubjectCredits(deptCodeOrName: string, _semester: number
|
||||
*/
|
||||
export async function fetchImsSemesterResults(_token: string, regNumber: string, currentSem = 4, _forceRefresh = false): Promise<{ results: SemesterResult[]; cgpa: number; totalCredits: number; activeArrears: number }> {
|
||||
const deduced = deduceStudentDetails(regNumber);
|
||||
const deptCode = deduced.departmentCode || 'AIDS';
|
||||
const deptCode = DEPARTMENT_CODE_MAP[deduced.department] || parseDepartmentCode(deduced.departmentCode) || 'AIDS';
|
||||
const results: SemesterResult[] = [];
|
||||
const maxSemToCheck = Math.max(2, currentSem || 4);
|
||||
|
||||
@@ -1077,6 +1077,7 @@ export async function fetchImsSemesterResults(_token: string, regNumber: string,
|
||||
const res = await fetch('/ims/admin/grade/student/mark/get_marks', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
@@ -1133,50 +1134,6 @@ export async function fetchImsSemesterResults(_token: string, regNumber: string,
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// If results were empty or fewer than previous semesters, generate exact curriculum results for finished semesters
|
||||
if (results.length === 0 && DEPARTMENT_CURRICULUM[deptCode]) {
|
||||
const completedSems = Math.min(4, Math.max(1, (currentSem || 5) - 1));
|
||||
const gradeTemplates = ['O', 'A+', 'O', 'A+', 'O', 'A', 'A+', 'O', 'A+'];
|
||||
|
||||
for (let s = 1; s <= completedSems; s++) {
|
||||
const semSubjects = DEPARTMENT_CURRICULUM[deptCode][s] || [];
|
||||
let semCredits = 0;
|
||||
let semPoints = 0;
|
||||
|
||||
const grades: SemesterGrade[] = semSubjects.map((sub, idx) => {
|
||||
const matchCode = sub.name.match(/\(([A-Z0-9]+)\)/i)?.[1] || sub.code || `SUB${s}0${idx+1}`;
|
||||
const cleanTitle = sub.name.replace(/\([A-Z0-9]+\)/gi, '').trim();
|
||||
const credits = sub.credits;
|
||||
const grade = sub.credits === 0 ? 'PASS' : (gradeTemplates[idx % gradeTemplates.length] || 'A+');
|
||||
const gp = GRADE_POINTS[grade] ?? 9;
|
||||
|
||||
if (credits > 0) {
|
||||
semCredits += credits;
|
||||
semPoints += (gp * credits);
|
||||
}
|
||||
|
||||
return {
|
||||
code: matchCode,
|
||||
name: cleanTitle,
|
||||
credits,
|
||||
internalMark: credits > 0 ? 38 + (idx % 3) * 3 : undefined,
|
||||
externalMark: credits > 0 ? 52 + (idx % 3) * 3 : undefined,
|
||||
totalMark: credits > 0 ? 90 + (idx % 3) * 3 : undefined,
|
||||
grade,
|
||||
result: 'PASS',
|
||||
};
|
||||
});
|
||||
|
||||
const sgpa = semCredits > 0 ? Number((semPoints / semCredits).toFixed(2)) : 0;
|
||||
results.push({
|
||||
semester: s,
|
||||
gpa: sgpa,
|
||||
totalCredits: semCredits,
|
||||
subjects: grades,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Cumulative CGPA & Active Arrears strictly according to Anna University Regulations
|
||||
let cumCredits = 0;
|
||||
let cumPoints = 0;
|
||||
@@ -1229,22 +1186,20 @@ export async function fetchStudentDashboard(regNumber?: string): Promise<{ resul
|
||||
/**
|
||||
* Fetch Assignment Marks from /ims/admin/assignment/student/mark/report
|
||||
*/
|
||||
export async function fetchImsAssignmentMarks(): Promise<AssignmentMarkItem[]> {
|
||||
export async function fetchImsAssignmentMarks(_deptCodeOrName?: string, _semester?: number): Promise<AssignmentMarkItem[]> {
|
||||
const items: AssignmentMarkItem[] = [];
|
||||
try {
|
||||
const response = await fetch('/ims/admin/assignment/student/mark/report', {
|
||||
const response = await fetch(`/ims/admin/assignment/student/mark/report?_t=${Date.now()}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!response.ok) return [];
|
||||
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const table = doc.querySelector('table');
|
||||
if (!table) return [];
|
||||
|
||||
if (table) {
|
||||
const rows = Array.from(table.querySelectorAll('tr'));
|
||||
const items: AssignmentMarkItem[] = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
const tds = Array.from(row.querySelectorAll('td')).map(td => td.textContent?.trim() || '');
|
||||
if (tds.length >= 4) {
|
||||
@@ -1265,43 +1220,37 @@ export async function fetchImsAssignmentMarks(): Promise<AssignmentMarkItem[]> {
|
||||
const total = tds[8] || tds[tds.length - 1] || '-';
|
||||
|
||||
if (subjectName || subjectCode) {
|
||||
items.push({
|
||||
subjectName,
|
||||
subjectCode,
|
||||
facultyName,
|
||||
a1, a2, a3, a4, a5,
|
||||
total,
|
||||
});
|
||||
items.push({ subjectName, subjectCode, facultyName, a1, a2, a3, a4, a5, total });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch assignment marks:', err);
|
||||
return [];
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch LAB Marks from /ims/admin/student_lab_mark/report
|
||||
*/
|
||||
export async function fetchImsLabMarks(): Promise<LabMarkItem[]> {
|
||||
export async function fetchImsLabMarks(_deptCodeOrName?: string, _semester?: number): Promise<LabMarkItem[]> {
|
||||
const items: LabMarkItem[] = [];
|
||||
|
||||
try {
|
||||
const response = await fetch('/ims/admin/student_lab_mark/report', {
|
||||
const response = await fetch(`/ims/admin/student_lab_mark/report?_t=${Date.now()}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!response.ok) return [];
|
||||
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const table = doc.querySelector('table');
|
||||
if (!table) return [];
|
||||
|
||||
if (table) {
|
||||
const rows = Array.from(table.querySelectorAll('tr'));
|
||||
const items: LabMarkItem[] = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
const tds = Array.from(row.querySelectorAll('td')).map(td => td.textContent?.trim() || '');
|
||||
if (tds.length >= 3) {
|
||||
@@ -1318,20 +1267,15 @@ export async function fetchImsLabMarks(): Promise<LabMarkItem[]> {
|
||||
const total = tds[tds.length - 1] || '-';
|
||||
|
||||
if (subjectName || subjectCode) {
|
||||
items.push({
|
||||
subjectName,
|
||||
subjectCode,
|
||||
facultyName,
|
||||
marks,
|
||||
total,
|
||||
});
|
||||
items.push({ subjectName, subjectCode, facultyName, marks, total });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch lab marks:', err);
|
||||
return [];
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
{
|
||||
"community_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||
"telegram_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||
"helper_chat_ids" : [ 1873240361, 5567776672, 8518850169, 7238144438 ],
|
||||
"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"
|
||||
}
|
||||
@@ -87,25 +87,24 @@ export default defineConfig({
|
||||
secure: false,
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyRes', (proxyRes) => {
|
||||
// Rewrite Location header to keep redirects through local proxy
|
||||
// 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, 'http://localhost:5173/ims');
|
||||
} else if (rewritten.startsWith('/')) {
|
||||
rewritten = rewritten.replace(/https?:\/\/ims\.ritchennai\.edu\.in/gi, '/ims');
|
||||
} else if (rewritten.startsWith('/') && !rewritten.startsWith('/ims')) {
|
||||
rewritten = '/ims' + rewritten;
|
||||
}
|
||||
proxyRes.headers['location'] = rewritten;
|
||||
}
|
||||
// Tweak cookies for localhost
|
||||
// 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(/Secure/gi, '')
|
||||
.replace(/samesite=none/gi, 'SameSite=Lax')
|
||||
.replace(/domain=[^;]+/gi, '')
|
||||
.replace(/path=[^;]+/gi, 'Path=/')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user