Compare commits
6 Commits
f58c98cd46
...
a19c36579e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a19c36579e | ||
|
|
6be263771d | ||
|
|
9275722e16 | ||
|
|
6740400788 | ||
|
|
6d386273fb | ||
|
|
56c3849357 |
@@ -69,6 +69,13 @@
|
|||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</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 -->
|
<!-- Spring Boot Starter Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<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.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -46,6 +47,24 @@ public class AdminController {
|
|||||||
@Value("${admin.transport.password:RIT@2026}")
|
@Value("${admin.transport.password:RIT@2026}")
|
||||||
private String transportPassword;
|
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}")
|
@Value("${admin.super.username:Admin}")
|
||||||
private String superAdminUsername;
|
private String superAdminUsername;
|
||||||
|
|
||||||
@@ -71,18 +90,62 @@ public class AdminController {
|
|||||||
if (p != null) inputPass = p.toString().trim();
|
if (p != null) inputPass = p.toString().trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isStandardPass = "RIT@2026".equalsIgnoreCase(inputPass);
|
||||||
|
|
||||||
// 1. Check Transport Admin ("Transport" / "RIT@2026")
|
// 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("success", true);
|
||||||
response.put("role", "ROLE_TRANSPORT");
|
response.put("role", "ROLE_TRANSPORT");
|
||||||
response.put("username", "Transport Admin");
|
response.put("username", "Transport Admin");
|
||||||
response.put("token", "TRANSPORT_SESSION_TOKEN_RIT_2026");
|
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);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check Super Admin ("Admin" / "RIT@2026" or "ritadmin" / "ritadmin2026")
|
// 2. Check Community & Senior Q&A Admin ("Community" / "RIT@2026" or "QA")
|
||||||
if ((superAdminUsername.equalsIgnoreCase(inputUser) && superAdminPassword.equals(inputPass)) ||
|
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))) {
|
("ritadmin".equalsIgnoreCase(inputUser) && "ritadmin2026".equals(inputPass))) {
|
||||||
response.put("success", true);
|
response.put("success", true);
|
||||||
response.put("role", "ROLE_SUPER_ADMIN");
|
response.put("role", "ROLE_SUPER_ADMIN");
|
||||||
@@ -93,7 +156,7 @@ public class AdminController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
response.put("success", false);
|
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);
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -257,15 +320,47 @@ public class AdminController {
|
|||||||
if (Files.exists(path)) {
|
if (Files.exists(path)) {
|
||||||
try {
|
try {
|
||||||
byte[] bytes = Files.readAllBytes(path);
|
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) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Map<String, Object> defaultCfg = new HashMap<>();
|
Map<String, Object> defaultCfg = new HashMap<>();
|
||||||
defaultCfg.put("community_bot_token", "");
|
defaultCfg.put("community_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY");
|
||||||
defaultCfg.put("telegram_bot_token", "");
|
defaultCfg.put("telegram_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY");
|
||||||
defaultCfg.put("helper_chat_ids", new ArrayList<Long>(List.of(971749136L, 5567776672L, 1873240361L)));
|
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");
|
defaultCfg.put("spring_backend_url", "http://localhost:8085");
|
||||||
return defaultCfg;
|
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("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("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("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);
|
saveTelegramConfigFile(current);
|
||||||
return ResponseEntity.ok(Map.of("success", true, "config", current));
|
return ResponseEntity.ok(Map.of("success", true, "config", current));
|
||||||
}
|
}
|
||||||
@@ -300,6 +414,7 @@ public class AdminController {
|
|||||||
@PostMapping("/telegram/helpers")
|
@PostMapping("/telegram/helpers")
|
||||||
public ResponseEntity<?> addSeniorHelper(@RequestBody Map<String, Object> payload) {
|
public ResponseEntity<?> addSeniorHelper(@RequestBody Map<String, Object> payload) {
|
||||||
Object chatIdObj = payload.get("chatId");
|
Object chatIdObj = payload.get("chatId");
|
||||||
|
String nameStr = payload.get("name") != null ? payload.get("name").toString().trim() : "Senior Responder";
|
||||||
if (chatIdObj == null) {
|
if (chatIdObj == null) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "chatId is required"));
|
return ResponseEntity.badRequest().body(Map.of("error", "chatId is required"));
|
||||||
}
|
}
|
||||||
@@ -313,38 +428,142 @@ public class AdminController {
|
|||||||
|
|
||||||
Map<String, Object> config = readTelegramConfigFile();
|
Map<String, Object> config = readTelegramConfigFile();
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<Object> rawHelpers = (List<Object>) config.getOrDefault("helper_chat_ids", new ArrayList<>());
|
List<Map<String, Object>> helpers = (List<Map<String, Object>>) config.getOrDefault("seniorHelpers", new ArrayList<>());
|
||||||
List<Long> helpers = new ArrayList<>();
|
if (helpers == null) helpers = new ArrayList<>();
|
||||||
for (Object o : rawHelpers) {
|
List<Long> helperIds = new ArrayList<>();
|
||||||
helpers.add(Long.parseLong(o.toString()));
|
|
||||||
|
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)) {
|
if (!found) {
|
||||||
helpers.add(chatId);
|
Map<String, Object> newHelper = new HashMap<>();
|
||||||
config.put("helper_chat_ids", helpers);
|
newHelper.put("chatId", chatId);
|
||||||
saveTelegramConfigFile(config);
|
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}")
|
@DeleteMapping("/telegram/helpers/{chatId}")
|
||||||
public ResponseEntity<?> removeSeniorHelper(@PathVariable Long chatId) {
|
public ResponseEntity<?> removeSeniorHelper(@PathVariable Long chatId) {
|
||||||
Map<String, Object> config = readTelegramConfigFile();
|
Map<String, Object> config = readTelegramConfigFile();
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<Object> rawHelpers = (List<Object>) config.getOrDefault("helper_chat_ids", new ArrayList<>());
|
List<Map<String, Object>> helpers = (List<Map<String, Object>>) config.getOrDefault("seniorHelpers", new ArrayList<>());
|
||||||
List<Long> helpers = new ArrayList<>();
|
if (helpers != null) {
|
||||||
for (Object o : rawHelpers) {
|
helpers.removeIf(h -> h.get("chatId") != null && Long.parseLong(h.get("chatId").toString()) == chatId);
|
||||||
helpers.add(Long.parseLong(o.toString()));
|
} else {
|
||||||
|
helpers = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean removed = helpers.removeIf(id -> id.equals(chatId));
|
List<Long> helperIds = new ArrayList<>();
|
||||||
if (removed) {
|
for (Map<String, Object> h : helpers) {
|
||||||
config.put("helper_chat_ids", helpers);
|
if (h.get("chatId") != null) {
|
||||||
saveTelegramConfigFile(config);
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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,388 @@
|
|||||||
|
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/candidate/profileview.xhtml";
|
||||||
|
private static final String PROFILE_URL = SKILLRACK_BASE + "/faces/candidate/profileview.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:
|
||||||
|
* 1. GET the SkillRack login page to obtain JSESSIONID cookie + ViewState
|
||||||
|
* 2. POST login form with email + password + ViewState
|
||||||
|
* 3. Parse the resulting profile/dashboard page for stats
|
||||||
|
*/
|
||||||
|
public boolean fetchAndUpdateProfile(SkillrackProfile profile, String rawPassword) {
|
||||||
|
try {
|
||||||
|
log.info("Fetching SkillRack stats for: {}", profile.getSkillrackEmail());
|
||||||
|
|
||||||
|
// Step 1: GET login page to get session cookie + ViewState
|
||||||
|
Connection.Response loginPageRes = Jsoup.connect(LOGIN_URL)
|
||||||
|
.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<>(loginPageRes.cookies());
|
||||||
|
Document loginDoc = loginPageRes.parse();
|
||||||
|
|
||||||
|
// Extract ViewState from hidden input
|
||||||
|
String viewState = "";
|
||||||
|
Element vsEl = loginDoc.selectFirst("input[name=javax.faces.ViewState]");
|
||||||
|
if (vsEl != null) {
|
||||||
|
viewState = vsEl.attr("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the login form and its ID
|
||||||
|
Element loginForm = loginDoc.selectFirst("form[id]");
|
||||||
|
String formId = loginForm != null ? loginForm.attr("id") : "j_idt4";
|
||||||
|
|
||||||
|
// Step 2: POST login credentials
|
||||||
|
Map<String, String> formData = new LinkedHashMap<>();
|
||||||
|
formData.put(formId, formId);
|
||||||
|
formData.put("javax.faces.ViewState", viewState);
|
||||||
|
|
||||||
|
// Find email/username and password input fields
|
||||||
|
Elements inputs = loginDoc.select("input[type=text], input[type=email], input[type=password]");
|
||||||
|
String emailFieldName = formId + ":j_idt6";
|
||||||
|
String passwordFieldName = formId + ":j_idt8";
|
||||||
|
|
||||||
|
for (Element input : inputs) {
|
||||||
|
String name = input.attr("name");
|
||||||
|
String type = input.attr("type");
|
||||||
|
if (type.equals("text") || type.equals("email")) {
|
||||||
|
emailFieldName = name;
|
||||||
|
} else if (type.equals("password")) {
|
||||||
|
passwordFieldName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.put(emailFieldName, profile.getSkillrackEmail());
|
||||||
|
formData.put(passwordFieldName, rawPassword);
|
||||||
|
|
||||||
|
// Find submit button name
|
||||||
|
Element submitBtn = loginDoc.selectFirst("input[type=submit], button[type=submit]");
|
||||||
|
if (submitBtn != null && submitBtn.hasAttr("name")) {
|
||||||
|
formData.put(submitBtn.attr("name"), submitBtn.attr("value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String formAction = LOGIN_URL;
|
||||||
|
if (loginForm != null && loginForm.hasAttr("action")) {
|
||||||
|
String action = loginForm.attr("action");
|
||||||
|
if (action.startsWith("http")) {
|
||||||
|
formAction = action;
|
||||||
|
} else {
|
||||||
|
formAction = SKILLRACK_BASE + action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection.Response loginRes = Jsoup.connect(formAction)
|
||||||
|
.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", LOGIN_URL)
|
||||||
|
.followRedirects(true)
|
||||||
|
.timeout(15000)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
cookies.putAll(loginRes.cookies());
|
||||||
|
|
||||||
|
// Step 3: Fetch the profile/dashboard 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();
|
||||||
|
|
||||||
|
// Check if we're still on the login page (auth failed)
|
||||||
|
String bodyText = profileDoc.body().text();
|
||||||
|
if (bodyText.contains("Incorrect email") || bodyText.contains("Login") && !bodyText.contains("Total Points")) {
|
||||||
|
// Try alternative dashboard URL
|
||||||
|
Connection.Response dashRes = Jsoup.connect(SKILLRACK_BASE + "/faces/candidate/manageprofile.xhtml")
|
||||||
|
.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(dashRes.cookies());
|
||||||
|
profileDoc = dashRes.parse();
|
||||||
|
bodyText = profileDoc.body().text();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
boolean foundAnything = false;
|
||||||
|
String fullText = doc.body().text();
|
||||||
|
|
||||||
|
// Try to find stats from structured HTML elements (cards, spans, divs)
|
||||||
|
// SkillRack typically shows stats in card-like containers
|
||||||
|
|
||||||
|
// Look for numbers near keywords
|
||||||
|
foundAnything = parseFromRawText(fullText, profile);
|
||||||
|
|
||||||
|
// Also try parsing from any table structures
|
||||||
|
Elements tables = doc.select("table");
|
||||||
|
for (Element table : tables) {
|
||||||
|
Elements rows = table.select("tr");
|
||||||
|
for (Element row : rows) {
|
||||||
|
Elements cells = row.select("td, th");
|
||||||
|
if (cells.size() >= 2) {
|
||||||
|
String label = cells.get(0).text().trim().toLowerCase();
|
||||||
|
String value = cells.get(cells.size() - 1).text().trim();
|
||||||
|
int num = parseIntSafe(value);
|
||||||
|
|
||||||
|
if (label.contains("code test") || label.contains("codetest")) {
|
||||||
|
profile.setCodeTestSolved(num);
|
||||||
|
foundAnything = true;
|
||||||
|
} else if (label.contains("code tutor") || label.contains("codetutor")) {
|
||||||
|
profile.setCodeTutorSolved(num);
|
||||||
|
foundAnything = true;
|
||||||
|
} else if (label.contains("code track") || label.contains("codetrack")) {
|
||||||
|
profile.setCodeTrackSolved(num);
|
||||||
|
foundAnything = true;
|
||||||
|
} else if (label.contains("daily challenge") || label.contains("dc")) {
|
||||||
|
profile.setDcSolved(num);
|
||||||
|
foundAnything = true;
|
||||||
|
} else if (label.contains("gold")) {
|
||||||
|
profile.setGoldMedals(num);
|
||||||
|
foundAnything = true;
|
||||||
|
} else if (label.contains("silver")) {
|
||||||
|
profile.setSilverMedals(num);
|
||||||
|
foundAnything = true;
|
||||||
|
} else if (label.contains("bronze")) {
|
||||||
|
profile.setBronzeMedals(num);
|
||||||
|
foundAnything = true;
|
||||||
|
} else if (label.contains("total") && label.contains("point")) {
|
||||||
|
profile.setTotalPoints(num);
|
||||||
|
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"
|
path="/leetcode"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
title="RIT LeetCode Leaderboard"
|
title="RIT Student Leaderboard"
|
||||||
description="Track campus competitive programming rankings and see top RIT coders."
|
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 />
|
<LeetcodeLeaderboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const exploreLinks = [
|
|||||||
{ label: 'Student Toolkit', path: '/toolkit', icon: Wrench },
|
{ label: 'Student Toolkit', path: '/toolkit', icon: Wrench },
|
||||||
{ label: 'RIT Chatbot', path: '/ai-assistant', icon: Bot },
|
{ label: 'RIT Chatbot', path: '/ai-assistant', icon: Bot },
|
||||||
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
|
{ label: 'Faculty Directory', path: '/faculty', icon: UserCheck },
|
||||||
{ label: 'LeetCode Arena', path: '/leetcode', icon: Trophy },
|
{ label: 'Student Leaderboard', path: '/student-leaderboard', icon: Trophy },
|
||||||
];
|
];
|
||||||
|
|
||||||
const campusLinks = [
|
const campusLinks = [
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const MORE_LINKS: NavItem[] = [
|
|||||||
{ label: 'Campus Map', path: '/campus', icon: Map },
|
{ label: 'Campus Map', path: '/campus', icon: Map },
|
||||||
{ label: 'Clubs & Centers', path: '/events', icon: Users },
|
{ label: 'Clubs & Centers', path: '/events', icon: Users },
|
||||||
{ label: 'Dev Collab', path: '/collab', icon: Code2 },
|
{ 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 Gate Pass', path: '/notes?portal=gatepass', icon: Ticket },
|
||||||
{ label: 'Student Proficiency', path: '/notes?portal=proficiency', icon: Award },
|
{ label: 'Student Proficiency', path: '/notes?portal=proficiency', icon: Award },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const NAV_LINKS = [
|
|||||||
{ label: 'Clubs & Centers', path: '/events' },
|
{ label: 'Clubs & Centers', path: '/events' },
|
||||||
{ label: 'Community', path: '/community' },
|
{ label: 'Community', path: '/community' },
|
||||||
{ label: 'Dev Collab', path: '/collab' },
|
{ label: 'Dev Collab', path: '/collab' },
|
||||||
{ label: 'LeetCode', path: '/leetcode' },
|
{ label: 'Student Leaderboard', path: '/student-leaderboard' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── Stats ────────────────────────────────────────────────────────────────────
|
// ─── Stats ────────────────────────────────────────────────────────────────────
|
||||||
@@ -110,9 +110,9 @@ export const FEATURES: Feature[] = [
|
|||||||
{
|
{
|
||||||
id: 'leetcode',
|
id: 'leetcode',
|
||||||
icon: 'Trophy',
|
icon: 'Trophy',
|
||||||
title: 'LeetCode Arena',
|
title: 'Student Leaderboard',
|
||||||
description: 'Track campus competitive programming rankings and see top RIT coders.',
|
description: 'Track campus competitive programming rankings (LeetCode & SkillRack) and verified skill certifications.',
|
||||||
path: '/leetcode',
|
path: '/student-leaderboard',
|
||||||
color: '#F59E0B',
|
color: '#F59E0B',
|
||||||
bgColor: '#FFFBEB',
|
bgColor: '#FFFBEB',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface AuthUser {
|
|||||||
name: string;
|
name: string;
|
||||||
pictureUrl?: string;
|
pictureUrl?: string;
|
||||||
verifiedStudent: boolean;
|
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;
|
regNumber?: string;
|
||||||
department?: string;
|
department?: string;
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,9 @@ interface AuthContextType {
|
|||||||
isVerifiedStudent: boolean;
|
isVerifiedStudent: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isTransportAdmin: boolean;
|
isTransportAdmin: boolean;
|
||||||
|
isCommunityAdmin: boolean;
|
||||||
|
isClubsAdmin: boolean;
|
||||||
|
isCurriculumAdmin: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isAuthModalOpen: boolean;
|
isAuthModalOpen: boolean;
|
||||||
loginWithGoogle: () => void;
|
loginWithGoogle: () => void;
|
||||||
@@ -59,6 +62,9 @@ const AuthContext = createContext<AuthContextType>({
|
|||||||
isVerifiedStudent: false,
|
isVerifiedStudent: false,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isTransportAdmin: false,
|
isTransportAdmin: false,
|
||||||
|
isCommunityAdmin: false,
|
||||||
|
isClubsAdmin: false,
|
||||||
|
isCurriculumAdmin: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isAuthModalOpen: false,
|
isAuthModalOpen: false,
|
||||||
loginWithGoogle: () => {},
|
loginWithGoogle: () => {},
|
||||||
@@ -200,9 +206,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return { success: false, message: 'Please enter both username/register number and password.' };
|
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) {
|
if (isAdminUsername) {
|
||||||
try {
|
try {
|
||||||
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
||||||
@@ -221,7 +233,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const adminUserObj: AuthUser = {
|
const adminUserObj: AuthUser = {
|
||||||
id: 999999,
|
id: 999999,
|
||||||
name: data.username,
|
name: data.username,
|
||||||
email: data.role === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in',
|
email: `${cleanedUser.toLowerCase()}@ritchennai.edu.in`,
|
||||||
verifiedStudent: false,
|
verifiedStudent: false,
|
||||||
role: data.role,
|
role: data.role,
|
||||||
};
|
};
|
||||||
@@ -241,11 +253,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct local check for Admin credentials fallback
|
const isStdPass = cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026';
|
||||||
if (
|
|
||||||
cleanedUser.toLowerCase() === 'transport' &&
|
// Direct local check for Admin credentials fallbacks
|
||||||
cleanedPass === 'RIT@2026'
|
if (['transport', 'transportadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
) {
|
|
||||||
const role = 'ROLE_TRANSPORT';
|
const role = 'ROLE_TRANSPORT';
|
||||||
localStorage.setItem(ADMIN_TOKEN_KEY, 'TRANSPORT_SESSION_TOKEN_RIT_2026');
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'TRANSPORT_SESSION_TOKEN_RIT_2026');
|
||||||
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
@@ -261,9 +272,57 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return { success: true, role, redirectTo: '/admin' };
|
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 (
|
if (
|
||||||
(cleanedUser.toLowerCase() === 'admin' && (cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026')) ||
|
(['admin', 'superadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) ||
|
||||||
(cleanedUser.toLowerCase() === 'ritadmin' && cleanedPass === 'ritadmin2026')
|
(cleanedUser.toLowerCase() === 'ritadmin' && (cleanedPass === 'ritadmin2026' || isStdPass))
|
||||||
) {
|
) {
|
||||||
const role = 'ROLE_SUPER_ADMIN';
|
const role = 'ROLE_SUPER_ADMIN';
|
||||||
localStorage.setItem(ADMIN_TOKEN_KEY, 'ADMIN_SESSION_TOKEN_RIT_2026');
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'ADMIN_SESSION_TOKEN_RIT_2026');
|
||||||
@@ -299,23 +358,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
role: 'ROLE_STUDENT',
|
role: 'ROLE_STUDENT',
|
||||||
redirectTo: '/timetable',
|
redirectTo: '/dashboard',
|
||||||
message: 'Student login successful',
|
message: `Welcome ${imsResult.student.name}!`,
|
||||||
};
|
};
|
||||||
} else if (!imsResult.success && imsResult.message) {
|
} else {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: imsResult.message,
|
message: imsResult.message || 'Authentication failed. Please check your credentials.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
// Fallback
|
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(() => {
|
const logout = useCallback(() => {
|
||||||
@@ -333,8 +390,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsAuthModalOpen(false);
|
setIsAuthModalOpen(false);
|
||||||
}, [user]);
|
}, [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 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 (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
@@ -344,6 +405,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
isVerifiedStudent: !!user?.verifiedStudent,
|
isVerifiedStudent: !!user?.verifiedStudent,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isTransportAdmin,
|
isTransportAdmin,
|
||||||
|
isCommunityAdmin,
|
||||||
|
isClubsAdmin,
|
||||||
|
isCurriculumAdmin,
|
||||||
isLoading,
|
isLoading,
|
||||||
isAuthModalOpen,
|
isAuthModalOpen,
|
||||||
loginWithGoogle,
|
loginWithGoogle,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,30 @@
|
|||||||
{
|
{
|
||||||
"community_bot_token" : "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
"community_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||||
"telegram_bot_token" : "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
"telegram_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
|
||||||
"helper_chat_ids" : [ 1873240361, 5567776672, 8518850169, 7238144438 ],
|
"bot_username": "FreshersCommunityBot",
|
||||||
"spring_backend_url" : "http://localhost:8085"
|
"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"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user