diff --git a/src/main/java/com/uniovi/components/generators/QuestionGeneratorV2.java b/src/main/java/com/uniovi/components/generators/QuestionGeneratorV2.java index efb1d8f6..c15a81c0 100644 --- a/src/main/java/com/uniovi/components/generators/QuestionGeneratorV2.java +++ b/src/main/java/com/uniovi/components/generators/QuestionGeneratorV2.java @@ -16,8 +16,10 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; +import java.util.Random; public class QuestionGeneratorV2 implements QuestionGenerator{ @@ -27,6 +29,8 @@ public class QuestionGeneratorV2 implements QuestionGenerator{ private String answer_placeholder; private String language; + private Random random = new SecureRandom(); + public QuestionGeneratorV2(JsonNode jsonNode) { this.jsonNode = jsonNode; this.language_placeholder = jsonNode.get("language_placeholder").textValue(); @@ -103,8 +107,8 @@ private List generateOptions(JsonNode results, String correctAnswer, Str int size = results.size(); int tries = 0; - while (options.size() < 3 && tries < 10){ - int random = (int) (Math.random() * size); + while (options.size() < 3 && tries < 10) { + int random = (int) (this.random.nextFloat() * size); String option = results.get(random).path(answerLabel).path("value").asText(); if (!option.equals(correctAnswer) && !usedOptions.contains(option) ) { usedOptions.add(option); diff --git a/src/main/java/com/uniovi/configuration/SecurityConfig.java b/src/main/java/com/uniovi/configuration/SecurityConfig.java index c0af5d20..068364b0 100644 --- a/src/main/java/com/uniovi/configuration/SecurityConfig.java +++ b/src/main/java/com/uniovi/configuration/SecurityConfig.java @@ -48,6 +48,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/api/**").permitAll() .requestMatchers("/game/**").authenticated() .requestMatchers("/ranking/playerRanking").authenticated() + .requestMatchers("/player/admin/**").hasAuthority("ROLE_ADMIN") .requestMatchers("/**").permitAll() ).formLogin( form -> form diff --git a/src/main/java/com/uniovi/controllers/GameController.java b/src/main/java/com/uniovi/controllers/GameController.java index ad8a7506..f8c0ffa3 100644 --- a/src/main/java/com/uniovi/controllers/GameController.java +++ b/src/main/java/com/uniovi/controllers/GameController.java @@ -79,32 +79,23 @@ public String getCheckResult(@PathVariable Long idQuestion, @PathVariable Long i if(idAnswer == -1 || getRemainingTime(gameSession) <= 0) { - //model.addAttribute("correctAnswer", gameSession.getCurrentQuestion().getCorrectAnswer()); - //model.addAttribute("messageKey", "timeRunOut.result"); - //model.addAttribute("logoImage", "/images/logo_incorrect.svg"); gameSession.addAnsweredQuestion(gameSession.getCurrentQuestion()); gameSession.addQuestion(false, 0); } else if(questionService.checkAnswer(idQuestion, idAnswer)) { - //model.addAttribute("messageKey", "correctAnswer.result"); - //model.addAttribute("logoImage", "/images/logo_correct.svg"); - if (!gameSession.isAnswered(gameSession.getCurrentQuestion())) { gameSession.addQuestion(true, getRemainingTime(gameSession)); gameSession.addAnsweredQuestion(gameSession.getCurrentQuestion()); } } else { - //model.addAttribute("correctAnswer", gameSession.getCurrentQuestion().getCorrectAnswer()); - //model.addAttribute("messageKey", "failedAnswer.result"); - //model.addAttribute("logoImage", "/images/logo_incorrect.svg"); gameSession.addAnsweredQuestion(gameSession.getCurrentQuestion()); gameSession.addQuestion(false, 0); } session.setAttribute("hasJustAnswered", true); gameSession.getNextQuestion(); - //return "game/fragments/questionResult"; + return updateGame(model, session); } diff --git a/src/main/java/com/uniovi/controllers/PlayersController.java b/src/main/java/com/uniovi/controllers/PlayersController.java index d291b6c6..0df52d19 100644 --- a/src/main/java/com/uniovi/controllers/PlayersController.java +++ b/src/main/java/com/uniovi/controllers/PlayersController.java @@ -1,42 +1,56 @@ package com.uniovi.controllers; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.uniovi.configuration.SecurityConfig; +import com.uniovi.dto.RoleDto; +import com.uniovi.entities.Associations; import com.uniovi.entities.GameSession; import com.uniovi.entities.Player; -import com.uniovi.services.GameSessionService; -import com.uniovi.services.PlayerService; +import com.uniovi.entities.Role; +import com.uniovi.services.*; import com.uniovi.validators.SignUpValidator; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.*; import com.uniovi.dto.PlayerDto; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.security.Principal; import java.util.Optional; +import java.util.List; @Controller public class PlayersController { private final PlayerService playerService; + private final RoleService roleService; + private QuestionService questionService; private final SignUpValidator signUpValidator; private final GameSessionService gameSessionService; @Autowired - public PlayersController(PlayerService playerService, SignUpValidator signUpValidator, GameSessionService gameSessionService) { + public PlayersController(PlayerService playerService, SignUpValidator signUpValidator, GameSessionService gameSessionService, + RoleService roleService, QuestionService questionService) { this.playerService = playerService; this.signUpValidator = signUpValidator; this.gameSessionService = gameSessionService; + this.roleService = roleService; } @GetMapping("/signup") @@ -122,4 +136,154 @@ public String showPlayerRanking(Pageable pageable, Model model, Principal princi return "ranking/playerRanking"; } + + // ----- Admin endpoints ----- + + @GetMapping("/player/admin") + public String showAdminPanel(Model model) { + return "player/admin/admin"; + } + + @GetMapping("/player/admin/userManagement") + public String showUserManagementFragment(Model model, Pageable pageable) { + model.addAttribute("endpoint", "/player/admin/userManagement"); + Page users = playerService.getPlayersPage(pageable); + model.addAttribute("page", users); + model.addAttribute("users", users.getContent()); + + return "player/admin/userManagement"; + } + + @GetMapping("/player/admin/deleteUser") + @ResponseBody + public String deleteUser(HttpServletResponse response, @RequestParam String username, Principal principal) { + Player player = playerService.getUserByUsername(username).orElse(null); + if (player == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return "User not found"; + } + + if (principal.getName().equals(player.getUsername())) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return "You can't delete yourself"; + } + + playerService.deletePlayer(player.getId()); + return "User deleted"; + } + + @GetMapping("/player/admin/changePassword") + @ResponseBody + public String changePassword(HttpServletResponse response, @RequestParam String username, @RequestParam String password) { + Player player = playerService.getUserByUsername(username).orElse(null); + if (player == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return "User not found"; + } + + playerService.updatePassword(player, password); + return "User password changed"; + } + + @GetMapping("/player/admin/getRoles") + @ResponseBody + public String getRoles(@RequestParam String username) { + List roles = roleService.getAllRoles(); + Player player = playerService.getUserByUsername(username).orElse(null); + + roles.remove(roleService.getRole("ROLE_USER")); + + if (player == null) { + return "{}"; + } + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode rolesJson = mapper.createObjectNode(); + for (Role role : roles) { + boolean hasRole = player.getRoles().contains(role); + rolesJson.put(role.getName(), hasRole); + } + + return rolesJson.toString(); + } + + @GetMapping("/player/admin/changeRoles") + @ResponseBody + public String changeRoles(HttpServletResponse response, @RequestParam String username, @RequestParam String roles) { + Player player = playerService.getUserByUsername(username).orElse(null); + if (player == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return "User not found"; + } + + JsonNode rolesJson; + try { + rolesJson = new ObjectMapper().readTree(roles); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return "Invalid roles"; + } + + rolesJson.fieldNames().forEachRemaining(roleName -> { + boolean hasRole = rolesJson.get(roleName).asBoolean(); + + Role role = roleService.getRole(roleName); + if (role == null && !hasRole) { + return; + } else if (role == null) { + role = roleService.addRole(new RoleDto(roleName)); + } + + if (hasRole) { + Associations.PlayerRole.addRole(player, role); + } else { + Associations.PlayerRole.removeRole(player, role); + } + }); + + playerService.savePlayer(player); + return "User roles changed"; + } + + @GetMapping("/player/admin/questionManagement") + public String showQuestionManagementFragment(Model model) throws IOException { + File jsonFile = new File(QuestionGeneratorService.jsonFilePath); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode json = objectMapper.readTree(jsonFile); + model.addAttribute("jsonContent", json.toString()); + + return "player/admin/questionManagement"; + } + + @GetMapping("/player/admin/deleteAllQuestions") + @ResponseBody + public String deleteAllQuestions() { + questionService.deleteAllQuestions(); + return "Questions deleted"; + } + + @GetMapping("/player/admin/saveQuestions") + @ResponseBody + public String saveQuestions(HttpServletResponse response, @RequestParam String json) throws IOException { + try { + JsonNode node = new ObjectMapper().readTree(json); + DefaultPrettyPrinter printer = new DefaultPrettyPrinter(); + DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter(); + printer.indentObjectsWith(indenter); // Indent JSON objects + printer.indentArraysWith(indenter); // Indent JSON arrays + + ObjectMapper mapper = new ObjectMapper(); + mapper.writer(printer).writeValue(new FileOutputStream(QuestionGeneratorService.jsonFilePath), node); + return "Questions saved"; + } + catch (Exception e) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return "Invalid JSON"; + } + } + + @GetMapping("/player/admin/monitoring") + public String showMonitoring(Model model) { + return "player/admin/monitoring"; + } } diff --git a/src/main/java/com/uniovi/entities/Role.java b/src/main/java/com/uniovi/entities/Role.java index c767c425..0d4d45c0 100644 --- a/src/main/java/com/uniovi/entities/Role.java +++ b/src/main/java/com/uniovi/entities/Role.java @@ -22,4 +22,9 @@ public class Role { public Role(String name) { this.name = name; } + + @Override + public String toString() { + return name; + } } diff --git a/src/main/java/com/uniovi/repositories/PlayerRepository.java b/src/main/java/com/uniovi/repositories/PlayerRepository.java index 77c2d9f8..cf4f0a98 100644 --- a/src/main/java/com/uniovi/repositories/PlayerRepository.java +++ b/src/main/java/com/uniovi/repositories/PlayerRepository.java @@ -1,9 +1,13 @@ package com.uniovi.repositories; import com.uniovi.entities.Player; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; public interface PlayerRepository extends CrudRepository { Player findByEmail(String email); Player findByUsername(String nickname); + + Page findAll(Pageable pageable); } diff --git a/src/main/java/com/uniovi/services/InsertSampleDataService.java b/src/main/java/com/uniovi/services/InsertSampleDataService.java index f22b1aa8..0b2b1b83 100644 --- a/src/main/java/com/uniovi/services/InsertSampleDataService.java +++ b/src/main/java/com/uniovi/services/InsertSampleDataService.java @@ -41,12 +41,15 @@ public InsertSampleDataService(PlayerService playerService, QuestionService ques @Transactional @EventListener(ApplicationReadyEvent.class) // Uncomment this line to insert sample data on startup public void insertSampleQuestions() throws InterruptedException, IOException { - if (!playerService.getUserByEmail("test@test.com").isPresent()) { + if (playerService.getUserByEmail("test@test.com").isEmpty()) { PlayerDto player = new PlayerDto(); player.setEmail("test@test.com"); player.setUsername("test"); player.setPassword("test"); - player.setRoles(new String[]{"ROLE_USER"}); + if (Arrays.asList(environment.getActiveProfiles()).contains("test")) + player.setRoles(new String[]{"ROLE_USER", "ROLE_ADMIN"}); + else + player.setRoles(new String[]{"ROLE_USER"}); playerService.generateApiKey(playerService.addNewPlayer(player)); } } diff --git a/src/main/java/com/uniovi/services/PlayerService.java b/src/main/java/com/uniovi/services/PlayerService.java index f59669e3..fc9da433 100644 --- a/src/main/java/com/uniovi/services/PlayerService.java +++ b/src/main/java/com/uniovi/services/PlayerService.java @@ -4,6 +4,8 @@ import com.uniovi.entities.Player; import com.uniovi.repositories.PlayerRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.List; @@ -72,4 +74,24 @@ public interface PlayerService { * @param id The id of the player to delete */ void deletePlayer(Long id); + + /** + * Get a page with all the players in the database + * @param pageable The page information + * @return A page with all the players + */ + Page getPlayersPage(Pageable pageable); + + /** + * Update the password of a player + * @param player The player to update the password + * @param password The new password + */ + void updatePassword(Player player, String password); + + /** + * Save a player in the database + * @param player The player to save + */ + void savePlayer(Player player); } diff --git a/src/main/java/com/uniovi/services/QuestionGeneratorService.java b/src/main/java/com/uniovi/services/QuestionGeneratorService.java index ec2992b2..91ee1725 100644 --- a/src/main/java/com/uniovi/services/QuestionGeneratorService.java +++ b/src/main/java/com/uniovi/services/QuestionGeneratorService.java @@ -8,7 +8,10 @@ import com.uniovi.entities.Answer; import com.uniovi.entities.Category; import com.uniovi.entities.Question; +import com.uniovi.services.impl.QuestionServiceImpl; import jakarta.transaction.Transactional; +import lombok.AllArgsConstructor; +import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -43,6 +46,7 @@ public class QuestionGeneratorService { public QuestionGeneratorService(QuestionService questionService) { this.questionService = questionService; + ((QuestionServiceImpl)questionService).setQuestionGeneratorService(this); parseQuestionTypes(); this.started = true; } @@ -123,24 +127,15 @@ public void generateTestQuestions(String cat) throws IOException { questionService.addNewQuestion(new QuestionDto(q)); } - private class QuestionType { - - private JsonNode question; - private Category category; - - public QuestionType(JsonNode question, Category category) { - this.question = question; - this.category = category; - } - - public JsonNode getQuestion() { - return question; - } - - public Category getCategory() { - return category; - } + public void resetGeneration() { + types.clear(); + parseQuestionTypes(); } - + @Getter + @AllArgsConstructor + private static class QuestionType { + private final JsonNode question; + private final Category category; + } } diff --git a/src/main/java/com/uniovi/services/RoleService.java b/src/main/java/com/uniovi/services/RoleService.java index e620105f..fd196940 100644 --- a/src/main/java/com/uniovi/services/RoleService.java +++ b/src/main/java/com/uniovi/services/RoleService.java @@ -20,4 +20,10 @@ public interface RoleService { * @return The role with the given name */ Role getRole(String name); + + /** + * Get all the roles in the database + * @return A list with all the roles + */ + List getAllRoles(); } diff --git a/src/main/java/com/uniovi/services/impl/PlayerServiceImpl.java b/src/main/java/com/uniovi/services/impl/PlayerServiceImpl.java index f4fa462d..9d0fb497 100644 --- a/src/main/java/com/uniovi/services/impl/PlayerServiceImpl.java +++ b/src/main/java/com/uniovi/services/impl/PlayerServiceImpl.java @@ -10,6 +10,8 @@ import com.uniovi.services.PlayerService; import com.uniovi.services.RoleService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import com.uniovi.entities.Role; @@ -136,4 +138,20 @@ public void updatePlayer(Long id, PlayerDto playerDto) { public void deletePlayer(Long id) { playerRepository.deleteById(id); } + + @Override + public Page getPlayersPage(Pageable pageable) { + return playerRepository.findAll(pageable); + } + + @Override + public void updatePassword(Player player, String password) { + player.setPassword(passwordEncoder.encode(password)); + playerRepository.save(player); + } + + @Override + public void savePlayer(Player player) { + playerRepository.save(player); + } } diff --git a/src/main/java/com/uniovi/services/impl/QuestionServiceImpl.java b/src/main/java/com/uniovi/services/impl/QuestionServiceImpl.java index 5a80136b..163fc20a 100644 --- a/src/main/java/com/uniovi/services/impl/QuestionServiceImpl.java +++ b/src/main/java/com/uniovi/services/impl/QuestionServiceImpl.java @@ -9,9 +9,12 @@ import com.uniovi.repositories.QuestionRepository; import com.uniovi.services.AnswerService; import com.uniovi.services.CategoryService; +import com.uniovi.services.QuestionGeneratorService; import com.uniovi.services.QuestionService; import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -31,6 +34,9 @@ public class QuestionServiceImpl implements QuestionService { private final AnswerRepository answerRepository; private final EntityManager entityManager; + @Setter + private QuestionGeneratorService questionGeneratorService; + private final Random random = new SecureRandom(); public QuestionServiceImpl(QuestionRepository questionRepository, CategoryService categoryService, @@ -168,6 +174,7 @@ public void deleteQuestion(Long id) { @Override public void deleteAllQuestions() { + questionGeneratorService.resetGeneration(); questionRepository.deleteAll(); } } diff --git a/src/main/java/com/uniovi/services/impl/RoleServiceImpl.java b/src/main/java/com/uniovi/services/impl/RoleServiceImpl.java index b10bbcc1..bec8464c 100644 --- a/src/main/java/com/uniovi/services/impl/RoleServiceImpl.java +++ b/src/main/java/com/uniovi/services/impl/RoleServiceImpl.java @@ -35,4 +35,11 @@ public Role addRole(RoleDto role) { public Role getRole(String name) { return roleRepository.findById(name).orElse(null); } + + @Override + public List getAllRoles() { + List roles = new ArrayList<>(); + roleRepository.findAll().forEach(roles::add); + return roles; + } } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 2b91abb1..e2304c4b 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -13,6 +13,7 @@ navbar.toEnglish=Inglés navbar.toSpanish=Español navbar.toFrench=Francés navbar.currentLanguage=Español +navbar.section.admin=Panel de administración # Buttons for non-authenticated users navbar.signup=Regístrate @@ -22,6 +23,7 @@ navbar.login=Inicia sesión navbar.profile=Perfil navbar.logout=Cerrar sesión navbar.profile.apikey=Clave de la API +navbar.admin.zone=Zona de administración # -------------------Statements for the footer.html file--------------------- footer.copyright=© ASW - Grupo 04 B @@ -114,4 +116,29 @@ game.continue=Siguiente pregunta answer.correct=La respuesta correcta era: game.points=Puntos: game.currentQuestion=Pregunta: -game.finish=El juego ha terminado. Tu puntuación ha sido: \ No newline at end of file +game.finish=El juego ha terminado. Tu puntuación ha sido: + + +# -------------------Statements for the admin section--------------------- +admin.section.user.management=Administración de usuarios +admin.section.question.management=Administración de preguntas +role.label=Roles +user.details=Acciones +admin.user.delete=Eliminar usuario +admin.user.delete.title=Confirmar borrado de usuario +admin.user.delete.message=¿Está seguro de que desea eliminar este usuario?\nTodos los datos asociados con esta cuenta se eliminarán.\nLa acción es irreversible. +admin.changepassword=Cambiar contraseña +admin.changeroles=Modificar roles +modal.password.title=Confirmar cambio de contraseña para +admin.password.change.input=Nueva contraseña +admin.roles.change=Confirmar cambio de roles para +modal.new.role=Nuevo rol +modal.close=Cerrar +modal.confirm=Confirmar +admin.questions.delete.title=Borrar todas las preguntas +admin.questions.delete=¿Está seguro de que desea eliminar todas las preguntas?\nEsta acción es irreversible.\nSe generaran de nuevo según pase el tiempo. +admin.monitoring=Monitorización de la aplicación + +# -------------------Statements for the page management--------------------- +page.first=Primera +page.last=Última \ No newline at end of file diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 97f0f133..331afdb3 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -13,6 +13,7 @@ navbar.toEnglish=English navbar.toSpanish=Spanish navbar.toFrench=French navbar.currentLanguage=English +navbar.section.admin=Administration Section # Buttons for non-authenticated users navbar.signup=Sign Up @@ -22,6 +23,7 @@ navbar.login=Log In navbar.profile=Profile navbar.logout=Log Out navbar.profile.apikey=API Key +navbar.admin.zone=Admin Zone # -------------------Statements for the footer.html file--------------------- footer.copyright=© ASW - Group 04 B @@ -117,4 +119,28 @@ game.points=Points: game.currentQuestion=Question: game.finish=The game has finished. Your score is: +# -------------------Statements for the admin section--------------------- +admin.section.user.management=Users management +admin.section.question.management=Questions management +role.label=Roles +user.details=Details +admin.user.delete=Delete user +admin.user.delete.title=Confirm deleting user +admin.user.delete.message=Are you sure you want to delete this user?\nAll data associated with this account will be erased\nThis action cannot be undone +admin.changepassword=Change password +admin.changeroles=Modify roles +modal.password.title=Confirm password change for +admin.password.change.input=New password +admin.roles.change=Confirm role change for +modal.new.role=New role +modal.close=Close +modal.confirm=Save changes +admin.questions.delete.title=Delete all questions +admin.questions.delete=Are you sure you want to delete all questions?\nThis action cannot be undone.\nQuestions will generate again as time passes. +admin.monitoring=Monitoring + +# -------------------Statements for the page management--------------------- +page.first=First +page.last=Last + diff --git a/src/main/resources/messages_es.properties b/src/main/resources/messages_es.properties index 174dad48..f4e31807 100644 --- a/src/main/resources/messages_es.properties +++ b/src/main/resources/messages_es.properties @@ -13,6 +13,7 @@ navbar.toEnglish=Inglés navbar.toSpanish=Español navbar.toFrench=Francés navbar.currentLanguage=Español +navbar.section.admin=Panel de administración # Buttons for non-authenticated users navbar.signup=Regístrate @@ -22,7 +23,7 @@ navbar.profile.apikey=Clave de la API # Buttons for authenticated users navbar.profile=Perfil navbar.logout=Cerrar sesión - +navbar.admin.zone=Zona de administración # -------------------Statements for the footer.html file--------------------- footer.copyright=© ASW - Grupo 04 B @@ -116,4 +117,25 @@ game.continue=Siguiente pregunta answer.correct=La respuesta correcta era: game.points=Puntos: game.currentQuestion=Pregunta: -game.finish=El juego ha terminado. Tu puntuación ha sido: \ No newline at end of file +game.finish=El juego ha terminado. Tu puntuación ha sido: + +# -------------------Statements for the admin section--------------------- +admin.section.user.management=Administración de usuarios +admin.section.question.management=Administración de preguntas +role.label=Roles +user.details=Acciones +admin.user.delete=Eliminar usuario +admin.user.delete.title=Confirmar borrado de usuario +admin.user.delete.message=¿Está seguro de que desea eliminar este usuario?\nTodos los datos asociados con esta cuenta se eliminarán.\nLa acción es irreversible. +admin.changepassword=Cambiar contraseña +admin.changeroles=Modificar roles +modal.password.title=Confirmar cambio de contraseña para +admin.password.change.input=Nueva contraseña +admin.roles.change=Confirmar cambio de roles para +modal.new.role=Nuevo rol +modal.close=Cerrar +modal.confirm=Confirmar + +# -------------------Statements for the page management--------------------- +page.first=Primera +page.last=Última \ No newline at end of file diff --git a/src/main/resources/messages_fr.properties b/src/main/resources/messages_fr.properties index 1f5d2b22..7f233db5 100644 --- a/src/main/resources/messages_fr.properties +++ b/src/main/resources/messages_fr.properties @@ -12,6 +12,7 @@ navbar.toEnglish=Anglais navbar.toSpanish=Espagnol navbar.toFrench=Français navbar.currentLanguage=Français +navbar.section.admin=Espace administrateur navbar.signup=S'inscrire navbar.login=Se connecter @@ -20,7 +21,7 @@ navbar.profile.apikey=Clé d'API # Buttons for authenticated users navbar.profile=Profil navbar.logout=Se déconnecter - +navbar.admin.zone=Espace administrateur # -------------------Statements for the footer.html file--------------------- footer.copyright=© ASW - Groupe 04 B @@ -112,3 +113,27 @@ game.points=Points: game.currentQuestion=Question: game.finish=Le jeu est terminé. Votre score est : +# -------------------Déclarations pour la section administrateur--------------------- +admin.section.user.management=Gestion des utilisateurs +admin.section.question.management=Gestion des questions +role.label=Rôles +user.details=Actions +admin.user.delete=Supprimer l'utilisateur +admin.user.delete.title=Confirmer la suppression de l'utilisateur +admin.user.delete.message=Êtes-vous sûr de vouloir supprimer cet utilisateur ?\nToutes les données associées à ce compte seront supprimées.\nL'action est irréversible. +admin.changepassword=Changer le mot de passe +admin.changeroles=Modifier les rôles +modal.password.title=Confirmer le changement de mot de passe pour +admin.password.change.input=Nouveau mot de passe +admin.roles.change=Confirmer le changement de rôles pour +modal.new.role=Nouveau rôle +modal.close=Fermer +modal.confirm=Confirmer +admin.questions.delete.title=Confirmer la suppression de toutes les questions +admin.questions.delete=Vous êtes sur le point de supprimer toutes les questions. Êtes-vous sûr de vouloir continuer ? +admin.monitoring=Surveillance + +# -------------------Déclarations pour la gestion de la page--------------------- +page.first=Première +page.last=Dernière + diff --git a/src/main/resources/static/JSON/QuestionTemplates.json b/src/main/resources/static/JSON/QuestionTemplates.json index 825176a9..a910f19b 100644 --- a/src/main/resources/static/JSON/QuestionTemplates.json +++ b/src/main/resources/static/JSON/QuestionTemplates.json @@ -1,97 +1,97 @@ { - "language_placeholder": "[LANGUAGE]", - "question_placeholder": "[QUESTION]", - "answer_placeholder": "[ANSWER]", - "categories": [ + "language_placeholder" : "[LANGUAGE]", + "question_placeholder" : "[QUESTION]", + "answer_placeholder" : "[ANSWER]", + "categories" : [ { - "name": "Geography", - "questions": [ + "name" : "Geography", + "questions" : [ { - "type": "capital", - "statements": [ + "type" : "capital", + "statements" : [ { - "language": "es", - "statement": "¿Cuál es la capital de [QUESTION]?" + "language" : "es", + "statement" : "¿Cuál es la capital de [QUESTION]?" }, { - "language": "en", - "statement": "What is the capital of [QUESTION]?" + "language" : "en", + "statement" : "What is the capital of [QUESTION]?" }, { - "language": "fr", - "statement": "Quelle est la capitale de [QUESTION]?" + "language" : "fr", + "statement" : "Quelle est la capitale de [QUESTION]?" } ], - "question": "countryLabel", - "answer": "capitalLabel", - "sparqlQuery": "select distinct ?country ?[QUESTION] ?capital ?[ANSWER] where {\n ?country wdt:P31 wd:Q6256 .\n ?capital wdt:P31 wd:Q5119 .\n ?country wdt:P36 ?capital .\n ?country rdfs:label ?[QUESTION] .\n ?capital rdfs:label ?[ANSWER] .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\" && LANG(?[ANSWER])=\"[LANGUAGE]\")\n }" + "question" : "countryLabel", + "answer" : "capitalLabel", + "sparqlQuery" : "select distinct ?country ?[QUESTION] ?capital ?[ANSWER] where {\n ?country wdt:P31 wd:Q6256 .\n ?capital wdt:P31 wd:Q5119 .\n ?country wdt:P36 ?capital .\n ?country rdfs:label ?[QUESTION] .\n ?capital rdfs:label ?[ANSWER] .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\" && LANG(?[ANSWER])=\"[LANGUAGE]\")\n }" }, { - "type": "currency", - "statements": [ - { - "language": "es", - "statement": "¿Cuál es la moneda de [QUESTION]?" - }, - { - "language": "en", - "statement": "What is the currency of [QUESTION]?" - }, - { - "language": "fr", - "statement": "Quelle est la monnaie de [QUESTION]?" - } - ], - "question": "countryLabel", - "answer": "currencyLabel", - "sparqlQuery": "select distinct ?country ?[QUESTION] ?currency ?[ANSWER] where {\n ?country wdt:P31 wd:Q6256 .\n ?currency wdt:P31 wd:Q8142 .\n ?country wdt:P38 ?currency .\n ?country rdfs:label ?[QUESTION] .\n ?currency rdfs:label ?[ANSWER] .\n FILTER NOT EXISTS {?country wdt:P31 wd:Q3024240} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\" && LANG(?[ANSWER])=\"[LANGUAGE]\")\n }" + "type" : "currency", + "statements" : [ + { + "language" : "es", + "statement" : "¿Cuál es la moneda de [QUESTION]?" + }, + { + "language" : "en", + "statement" : "What is the currency of [QUESTION]?" + }, + { + "language" : "fr", + "statement" : "Quelle est la monnaie de [QUESTION]?" + } + ], + "question" : "countryLabel", + "answer" : "currencyLabel", + "sparqlQuery" : "select distinct ?country ?[QUESTION] ?currency ?[ANSWER] where {\n ?country wdt:P31 wd:Q6256 .\n ?currency wdt:P31 wd:Q8142 .\n ?country wdt:P38 ?currency .\n ?country rdfs:label ?[QUESTION] .\n ?currency rdfs:label ?[ANSWER] .\n FILTER NOT EXISTS {?country wdt:P31 wd:Q3024240} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\" && LANG(?[ANSWER])=\"[LANGUAGE]\")\n }" } ] }, { - "name": "Science", - "questions": [ + "name" : "Science", + "questions" : [ { - "type": "element", - "statements": [ + "type" : "element", + "statements" : [ { - "language": "es", - "statement": "¿Cuál es el símbolo químico del [QUESTION]?" + "language" : "es", + "statement" : "¿Cuál es el símbolo químico del [QUESTION]?" }, { - "language": "en", - "statement": "What is the chemical symbol of [QUESTION]?" + "language" : "en", + "statement" : "What is the chemical symbol of [QUESTION]?" }, { - "language": "fr", - "statement": "Quel est le symbole chimique du [QUESTION]?" + "language" : "fr", + "statement" : "Quel est le symbole chimique du [QUESTION]?" } ], - "question": "elementLabel", - "answer": "symbol", - "sparqlQuery": "select distinct ?element ?[QUESTION] ?[ANSWER] where {\n ?element wdt:P31 wd:Q11344 .\n ?element wdt:P246 ?[ANSWER] .\n ?element rdfs:label ?[QUESTION] .\n FILTER NOT EXISTS {?element wdt:P31 wd:Q1299291} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\")\n }" + "question" : "elementLabel", + "answer" : "symbol", + "sparqlQuery" : "select distinct ?element ?[QUESTION] ?[ANSWER] where {\n ?element wdt:P31 wd:Q11344 .\n ?element wdt:P246 ?[ANSWER] .\n ?element rdfs:label ?[QUESTION] .\n FILTER NOT EXISTS {?element wdt:P31 wd:Q1299291} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\")\n }" }, { - "type": "atomic_number", - "statements": [ + "type" : "atomic_number", + "statements" : [ { - "language": "es", - "statement": "¿Cuál es el número atómico del [QUESTION]?" + "language" : "es", + "statement" : "¿Cuál es el número atómico del [QUESTION]?" }, { - "language": "en", - "statement": "What is the atomic number of [QUESTION]?" + "language" : "en", + "statement" : "What is the atomic number of [QUESTION]?" }, { - "language": "fr", - "statement": "Quel est le numéro atomique du [QUESTION]?" + "language" : "fr", + "statement" : "Quel est le numéro atomique du [QUESTION]?" } ], - "question": "elementLabel", - "answer": "atomicNumber", - "sparqlQuery": "select distinct ?element ?[QUESTION] ?[ANSWER] where {\n ?element wdt:P31 wd:Q11344 .\n ?element wdt:P1086 ?[ANSWER] .\n ?element rdfs:label ?[QUESTION] .\n FILTER NOT EXISTS {?element wdt:P31 wd:Q1299291} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\")\n }" + "question" : "elementLabel", + "answer" : "atomicNumber", + "sparqlQuery" : "select distinct ?element ?[QUESTION] ?[ANSWER] where {\n ?element wdt:P31 wd:Q11344 .\n ?element wdt:P1086 ?[ANSWER] .\n ?element rdfs:label ?[QUESTION] .\n FILTER NOT EXISTS {?element wdt:P31 wd:Q1299291} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\")\n }" } ] } ] -} +} \ No newline at end of file diff --git a/src/main/resources/static/css/admin.css b/src/main/resources/static/css/admin.css new file mode 100644 index 00000000..db24eb8e --- /dev/null +++ b/src/main/resources/static/css/admin.css @@ -0,0 +1,26 @@ +.nav .nav-link { + color: white; + border-color: white; +} + +.nav .nav-item { + margin: 0 5px; + flex: 1; +} + +.nav .nav-link.active { + color: black !important; +} + +.nav-tabs { + border-bottom: 0px; +} + +.separator { + border-bottom: 1px solid white; + margin: 10px 0; +} + +.text-danger-light { + color: #ff5e5e; +} \ No newline at end of file diff --git a/src/main/resources/static/css/custom.css b/src/main/resources/static/css/custom.css index 1499a864..18ebb97e 100644 --- a/src/main/resources/static/css/custom.css +++ b/src/main/resources/static/css/custom.css @@ -8,14 +8,6 @@ body { margin-bottom: 60px; color: #fff; } -footer { - position: absolute; - bottom: 0; - width: 100%; - height: 60px; - text-align:center; - line-height:60px -} .bg-primary { background-color: rgb(1, 85, 20) !important; @@ -66,4 +58,64 @@ footer { .prueba { font-weight: bold; +} + +.button-container { + display: flex; + justify-content: space-between; +} + +.button-container a { + flex: 1; + margin: 0 5px; +} + +.modal { + color: black; +} + +.modal .btn.btn-primary { + background-color: #007bff; + border-color: #007bff; +} + +.btn-close { + box-sizing: content-box; + width: 1em; + height: 1em; + padding: .25em .25em; + color: #000; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; + border: 0; + border-radius: .25rem; + opacity: .5 +} + +.btn-close:hover { + color: #000; + text-decoration: none; + opacity: .75 +} + +.btn-close:focus { + outline: 0; + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25); + opacity: 1 +} + +.btn-close.disabled, +.btn-close:disabled { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + opacity: .25 +} + +.modal-body { + white-space: pre; +} + +.container, .container-fluid { + flex: 1 1 auto !important; } \ No newline at end of file diff --git a/src/main/resources/static/css/footer.css b/src/main/resources/static/css/footer.css index 77a89c6c..e9e67a72 100644 --- a/src/main/resources/static/css/footer.css +++ b/src/main/resources/static/css/footer.css @@ -1,14 +1,9 @@ /* Estilo del footer */ .footer { - position: absolute; bottom: 0; - min-height: 10%; width: 100%; - padding: 0; /* Eliminar relleno */ - text-align: center; /* Alineación del texto */ - display: flex; /* Usar flexbox para centrar verticalmente */ - /*align-items: center; /* Centrar verticalmente */ - margin-top: 5%; + height: 60px; /* Set the fixed height of the footer here */ + line-height: 60px; /* Vertically center the text there */ background-color: transparent !important; /* Hace que el footer sea transparente */ } diff --git a/src/main/resources/static/css/nav.css b/src/main/resources/static/css/nav.css index 79c8ca3c..102f4e28 100644 --- a/src/main/resources/static/css/nav.css +++ b/src/main/resources/static/css/nav.css @@ -18,7 +18,7 @@ /* Estilo para los desplegables */ .dropdown-menu { color: #fff; - background-color: black; + background-color: rgb(19, 19, 19); border: 2px solid #fff; } diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index 84d2796c..a28db0d5 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -6,4 +6,12 @@ #apiKeyDiv form { margin: 5% 20% 0 20%; +} + +html { + position: relative; + min-height: 100%; +} +body { + margin-bottom: 60px; /* Margin bottom by footer height */ } \ No newline at end of file diff --git a/src/main/resources/static/script/adminModals.js b/src/main/resources/static/script/adminModals.js new file mode 100644 index 00000000..aa468c2e --- /dev/null +++ b/src/main/resources/static/script/adminModals.js @@ -0,0 +1,112 @@ +function setupUserEvents() { + $("#deleteUserAdminModal").on('show.bs.modal', function (event) { + let button = $(event.relatedTarget); + let username = button.attr('data-bs-username'); + $(".modal-title b").text('"' + username + '"'); + $("#deleteModalConfirm").attr('data-bs-username', username); + }); + + $("#deleteModalConfirm").click(function () { + let username = $(this).attr('data-bs-username'); + $.ajax({ + url: "/player/admin/deleteUser", + type: "GET", + data: { + username: username + }, + success: function (data) { + $('#tab-content').load('/player/admin/userManagement'); + $("#deleteUserAdminModal").modal('hide'); + } + }); + }); + + $("#changePasswordAdminModal").on('show.bs.modal', function (event) { + let button = $(event.relatedTarget); + let username = button.attr('data-bs-username'); + $(".modal-title b").text('"' + username + '"'); + $("#changePasswordConfirm").attr('data-bs-username', username); + }); + + $("#changePasswordConfirm").click(function () { + let username = $(this).attr('data-bs-username'); + let newPass = $("#changePasswordInput").val(); + $.ajax({ + url: "/player/admin/changePassword", + type: "GET", + data: { + username: username, + password: newPass + }, + success: function (data) { + $('#tab-content').load('/player/admin/userManagement'); + $("#changePasswordAdminModal").modal('hide'); + } + }); + }); + + $("#changeRolesAdminModal").on('show.bs.modal', function (event) { + let button = $(event.relatedTarget); + let username = button.attr('data-bs-username'); + $(".modal-title b").text('"' + username + '"'); + $("#changeRolesConfirm").attr('data-bs-username', username); + $.ajax({ + url: "/player/admin/getRoles", + type: "GET", + data: { + username: username + }, + success: function (data) { + let roles = JSON.parse(data); + let rolesContainer = $("#rolesContainer"); + rolesContainer.empty(); + let i = 0; + for (const role in roles) { + let hasRole = roles[role]; + let div = $('
'); + let input = $(''); + let label = $(''); + div.append(input); + div.append(label); + rolesContainer.append(div); + i = i + 1; + } + }, + error: function (data) { + alert("Error: " + data); + } + }); + }); + + $("#changeRolesConfirm").click(function () { + let username = $(this).attr('data-bs-username'); + + let allRoles = $("#rolesContainer input"); + let roles = {}; + allRoles.each(function() { + roles[$(this).val()] = $(this).is(':checked'); + }); + let newRoleInput = $("#newRole").val(); + if (newRoleInput !== "") { + roles[newRoleInput] = true; + } + + let rolesString = JSON.stringify(roles); + + $.ajax({ + url: "/player/admin/changeRoles", + type: "GET", + data: { + username: username, + roles: rolesString + }, + success: function (data) { + $('#tab-content').load('/player/admin/userManagement'); + $("#changeRolesAdminModal").modal('hide'); + }, + error: function (data) { + alert("Error: " + data); + } + }); + }); +} \ No newline at end of file diff --git a/src/main/resources/static/script/questionManagement.js b/src/main/resources/static/script/questionManagement.js new file mode 100644 index 00000000..1d3280dd --- /dev/null +++ b/src/main/resources/static/script/questionManagement.js @@ -0,0 +1,38 @@ +function setupQuestionManagement() { + var editor; + $("#deleteQuestionsConfirm").on("click", function () { + $.ajax({ + url: "/player/admin/deleteAllQuestions", + type: "GET", + success: function () { + $('#tab-content').load('/player/admin/questionManagement'); + } + }); + }); + + $("#saveButton").on("click", function () { + $.ajax({ + url: "/player/admin/saveQuestions", + type: "GET", + data: { + json: JSON.stringify(editor.get()) + }, + contentType: "application/json" + }); + }); + + $.ajax({ + url: '/JSON/QuestionTemplates.json', + type: 'GET', + success: function (data) { + let json = data; + const element = document.getElementById('jsonEditorElement'); + const options = {} + editor = new JSONEditor(element, options) + editor.set(json) + }, + error: function (error) { + console.log(error); + } + }); +} \ No newline at end of file diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html index 66d23f12..e4ac7290 100644 --- a/src/main/resources/templates/error.html +++ b/src/main/resources/templates/error.html @@ -1,7 +1,7 @@ - + diff --git a/src/main/resources/templates/fragments/adminModals.html b/src/main/resources/templates/fragments/adminModals.html new file mode 100644 index 00000000..d5c19e74 --- /dev/null +++ b/src/main/resources/templates/fragments/adminModals.html @@ -0,0 +1,76 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/footer.html b/src/main/resources/templates/fragments/footer.html index 0579216d..bebd50d9 100644 --- a/src/main/resources/templates/fragments/footer.html +++ b/src/main/resources/templates/fragments/footer.html @@ -1,5 +1,5 @@ -