diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d36e593 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/mvnw b/mvnw old mode 100755 new mode 100644 diff --git a/pom.xml b/pom.xml index fbe03aa..bcd7a9e 100644 --- a/pom.xml +++ b/pom.xml @@ -72,11 +72,24 @@ spring-boot-starter-test test + + org.springframework.security + spring-security-ldap + + + org.springframework.boot + spring-boot-starter-security + com.vaadin vaadin-testbench-junit5 test + + org.slf4j + slf4j-api + 2.0.13 + @@ -166,7 +179,7 @@ org.apache.maven.plugins - maven-failsafe-plugin + diff --git a/src/main/java/com/example/application/api/ApiRedmine.java b/src/main/java/com/example/application/api/ApiRedmine.java new file mode 100644 index 0000000..bcfd9ad --- /dev/null +++ b/src/main/java/com/example/application/api/ApiRedmine.java @@ -0,0 +1,75 @@ +package com.example.application.api; + +import com.nimbusds.jose.shaded.gson.*; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.*; + +public class ApiRedmine { + private static final String REDMINE_URL = "http://localhost:3000"; + private static final String API_KEY = "cf3be6168e66c99892c6212ea0bc64e8ab1c6848"; + public static final Gson GSON = new Gson(); + + public static String createIssue(Map issueDetails) { + Map payload = new HashMap<>(); + payload.put("issue", issueDetails); + String jsonPayload = GSON.toJson(payload); + + try { + //Crear la solicitud HTTP POST + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(REDMINE_URL + "/issues.json")) + .header("Content-Type", "application/json") + .header("X-Redmine-API-Key", API_KEY) + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + //Enviar la solicitud + HttpClient client = HttpClient.newHttpClient(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + return response.body(); + } catch (Exception e) { + e.printStackTrace(); + return "Error: "+e.getMessage(); + } + } + + public List getTicketTypes() { + List ticketTypes = new ArrayList<>(); + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(REDMINE_URL + "/trackers.json")) + .header("Content-Type", "application/json") + .header("X-Redmine-API-Key", API_KEY) + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + String responseBody = response.body(); + ticketTypes = parseTicketTypes(responseBody); + } else { + System.err.println("Error en la respuesta: " + response.statusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + } + return ticketTypes; + } + + private List parseTicketTypes(String json) { + List names = new ArrayList<>(); + JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject(); + JsonArray trackers = jsonObject.getAsJsonArray("trackers"); + for (JsonElement trackerElement : trackers) { + JsonObject tracker = trackerElement.getAsJsonObject(); + String name = tracker.get("name").getAsString(); + names.add(name); + } + return names; + } +} diff --git a/src/main/java/com/example/application/api/SecurityConfig.java b/src/main/java/com/example/application/api/SecurityConfig.java new file mode 100644 index 0000000..dfd6d6c --- /dev/null +++ b/src/main/java/com/example/application/api/SecurityConfig.java @@ -0,0 +1,56 @@ +package com.example.application.api; + +import com.example.application.views.login.LoginView; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinServletRequest; +import com.vaadin.flow.spring.security.VaadinWebSecurity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends VaadinWebSecurity { + + + private static final String LOGOUT_SUCCESS_URL = "/"; + + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + setLoginView(http, LoginView.class); + } + + + @Autowired + public void configure(AuthenticationManagerBuilder auth) throws Exception { + ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("JUMAPACELAYA.GOB.MX", "ldap://172.16.0.1"); + provider.setConvertSubErrorCodesToExceptions(true); + provider.setUseAuthenticationRequestCredentials(true); + auth.authenticationProvider(provider); + + } + + public void logout() { + UI.getCurrent().getPage().setLocation(LOGOUT_SUCCESS_URL); + SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); + logoutHandler.logout( + VaadinServletRequest.getCurrent().getHttpServletRequest(), null, null + ); + } +} + +/* + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory(contextSource, new BCryptPasswordEncoder()); + factory.setUserDnPatterns("DC=JUMAPACELAYA,DC=GOB,DC=MX"); + factory.setPasswordAttribute("pwd"); //pwd + return factory.createAuthenticationManager(); + } +*/ + diff --git a/src/main/java/com/example/application/views/MainLayout.java b/src/main/java/com/example/application/views/MainLayout.java index 3157304..71fdd94 100644 --- a/src/main/java/com/example/application/views/MainLayout.java +++ b/src/main/java/com/example/application/views/MainLayout.java @@ -1,13 +1,18 @@ package com.example.application.views; -import com.example.application.views.about.AboutView; -import com.example.application.views.helloworld.HelloWorldView; +import com.example.application.api.SecurityConfig; +import com.example.application.views.crearnuevoticket.CrearnuevoTicketView; +import com.example.application.views.tickets.MisTicketsView; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Footer; import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.html.Header; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.Scroller; import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNavItem; @@ -21,8 +26,10 @@ import org.vaadin.lineawesome.LineAwesomeIcon; public class MainLayout extends AppLayout { private H2 viewTitle; + private final SecurityConfig securityConfig; - public MainLayout() { + public MainLayout(SecurityConfig securityConfig) { + this.securityConfig = securityConfig; setPrimarySection(Section.DRAWER); addDrawerContent(); addHeaderContent(); @@ -35,7 +42,21 @@ public class MainLayout extends AppLayout { viewTitle = new H2(); viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); - addToNavbar(true, toggle, viewTitle); + Button logoutButton = new Button("Cerrar sesion", event -> { + securityConfig.logout(); + }); + logoutButton.getStyle().set("margin-right", "50px"); + logoutButton.addThemeVariants(ButtonVariant.LUMO_ERROR); + + HorizontalLayout headerContent = new HorizontalLayout(); + headerContent.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + headerContent.setWidthFull(); + headerContent.add(viewTitle); + headerContent.setFlexGrow(1, viewTitle); + headerContent.add(logoutButton); + + + addToNavbar(true, toggle, headerContent); } private void addDrawerContent() { @@ -51,8 +72,9 @@ public class MainLayout extends AppLayout { private SideNav createNavigation() { SideNav nav = new SideNav(); - nav.addItem(new SideNavItem("Hello World", HelloWorldView.class, LineAwesomeIcon.GLOBE_SOLID.create())); - nav.addItem(new SideNavItem("About", AboutView.class, LineAwesomeIcon.FILE.create())); + nav.addItem(new SideNavItem("Crear nuevo ticket", CrearnuevoTicketView.class, LineAwesomeIcon.EDIT.create())); + nav.addItem(new SideNavItem("Mis tickets", MisTicketsView.class, LineAwesomeIcon.TICKET_ALT_SOLID.create())); + return nav; } diff --git a/src/main/java/com/example/application/views/about/AboutView.java b/src/main/java/com/example/application/views/about/AboutView.java deleted file mode 100644 index 71c869e..0000000 --- a/src/main/java/com/example/application/views/about/AboutView.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.application.views.about; - -import com.example.application.views.MainLayout; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.Image; -import com.vaadin.flow.component.html.Paragraph; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.theme.lumo.LumoUtility.Margin; - -@PageTitle("About") -@Route(value = "about", layout = MainLayout.class) -public class AboutView extends VerticalLayout { - - public AboutView() { - setSpacing(false); - - Image img = new Image("images/empty-plant.png", "placeholder plant"); - img.setWidth("200px"); - add(img); - - H2 header = new H2("This place intentionally left empty"); - header.addClassNames(Margin.Top.XLARGE, Margin.Bottom.MEDIUM); - add(header); - add(new Paragraph("It’s a place where you can grow your own UI 🤗")); - - setSizeFull(); - setJustifyContentMode(JustifyContentMode.CENTER); - setDefaultHorizontalComponentAlignment(Alignment.CENTER); - getStyle().set("text-align", "center"); - } - -} diff --git a/src/main/java/com/example/application/views/crearnuevoticket/CrearnuevoTicketView.java b/src/main/java/com/example/application/views/crearnuevoticket/CrearnuevoTicketView.java new file mode 100644 index 0000000..f826c1e --- /dev/null +++ b/src/main/java/com/example/application/views/crearnuevoticket/CrearnuevoTicketView.java @@ -0,0 +1,103 @@ +package com.example.application.views.crearnuevoticket; + +import com.example.application.api.ApiRedmine; +import com.example.application.views.MainLayout; +import com.nimbusds.jose.shaded.gson.Gson; +import com.nimbusds.jose.shaded.gson.JsonObject; +import com.nimbusds.jose.shaded.gson.JsonParser; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; +import jakarta.annotation.security.PermitAll; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Route(value="home", layout = MainLayout.class) +@PermitAll +public class CrearnuevoTicketView extends VerticalLayout { + + public CrearnuevoTicketView() { + TextField projectId = new TextField("Project ID"); + projectId.setReadOnly(true); + + + //Combo de los tipos de tickets + ComboBox tipoTickets = new ComboBox<>("Tipo de ticket"); + ApiRedmine api = new ApiRedmine(); + List types = api.getTicketTypes(); + tipoTickets.setItems(types); + + + //Campo de texto para el asunto + TextField asunto = new TextField("Asunto"); + asunto.setWidth("700px"); + + + //Campo de texto para la descripcion + TextArea descripcion = new TextArea("Descripcion"); + descripcion.setWidth("1000px"); + descripcion.setHeight("300px"); + + + //Respuestas Json para verificar posibles errores al enviar los nuevos tickets no visibles en la interfaz + TextArea jsonOutput = new TextArea("JSON Output"); + jsonOutput.setReadOnly(true); + TextArea responseField = new TextArea("Response"); + responseField.setReadOnly(true); + + + + Button createButton = new Button("Enviar ticket", event -> { + Map issueDetails = new HashMap<>(); + issueDetails.put("project_id", "proyecto-de-prueba"); + issueDetails.put("subject", asunto.getValue()); + issueDetails.put("description", descripcion.getValue()); + + String response = ApiRedmine.createIssue(issueDetails); + if (response.startsWith("{\"issue\":")) { + //Aqui extraigo el numero del ticket de la respuesta json + JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject(); + int issueNumber = jsonResponse.getAsJsonObject("issue").get("id").getAsInt(); + + String notificationMessage = "¡Su ticket se ha enviado corrrectamente! Numero de ticket: #" + issueNumber; + Notification issueNtf = new Notification(notificationMessage, 5000, Notification.Position.MIDDLE); + issueNtf.addThemeVariants(NotificationVariant.LUMO_SUCCESS); + issueNtf.open(); + + //Limpiando los campos + asunto.clear(); + descripcion.clear(); + tipoTickets.clear(); + } else { + Notification errNtf = Notification.show("Ha ocurrido un error al enviar el ticket: "+response, 5000, Notification.Position.MIDDLE); + errNtf.addThemeVariants(NotificationVariant.LUMO_ERROR); + } + }); + createButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + VerticalLayout fieldsLayout = new VerticalLayout(asunto, descripcion); + fieldsLayout.setAlignItems(Alignment.CENTER); + + VerticalLayout buttonLayout = new VerticalLayout(createButton); + buttonLayout.setAlignItems(Alignment.END); + buttonLayout.setMargin(true); + + add(new H2("Crear nuevo ticket"),projectId, tipoTickets, fieldsLayout,buttonLayout/*,jsonOutput,responseField*/); + } + + + //Metodo para convertir un Map a json usando la libreria GSON + private static String mapToJson(Map map) { + Gson gson = new Gson(); + return gson.toJson(map); + } +} diff --git a/src/main/java/com/example/application/views/helloworld/HelloWorldView.java b/src/main/java/com/example/application/views/helloworld/HelloWorldView.java deleted file mode 100644 index dd80e91..0000000 --- a/src/main/java/com/example/application/views/helloworld/HelloWorldView.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.application.views.helloworld; - -import com.example.application.views.MainLayout; -import com.vaadin.flow.component.Key; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouteAlias; - -@PageTitle("Hello World") -@Route(value = "hello", layout = MainLayout.class) -@RouteAlias(value = "", layout = MainLayout.class) -public class HelloWorldView extends HorizontalLayout { - - private TextField name; - private Button sayHello; - - public HelloWorldView() { - name = new TextField("Your name"); - sayHello = new Button("Say hello"); - sayHello.addClickListener(e -> { - Notification.show("Hello " + name.getValue()); - }); - sayHello.addClickShortcut(Key.ENTER); - - setMargin(true); - setVerticalComponentAlignment(Alignment.END, name, sayHello); - - add(name, sayHello); - } - -} diff --git a/src/main/java/com/example/application/views/login/LoginView.java b/src/main/java/com/example/application/views/login/LoginView.java new file mode 100644 index 0000000..4a9394b --- /dev/null +++ b/src/main/java/com/example/application/views/login/LoginView.java @@ -0,0 +1,29 @@ +package com.example.application.views.login; + +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.login.LoginForm; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; + +@Route("login") +@AnonymousAllowed +public class LoginView extends VerticalLayout { + + + public LoginView() { + setSizeFull(); + setAlignItems(Alignment.CENTER); + setJustifyContentMode(JustifyContentMode.CENTER); + + //Formulario de login + var login = new LoginForm(); + login.setAction("login"); + + add( + new H1("Soporte tecnico T.I"), + login + ); + } +} + diff --git a/src/main/java/com/example/application/views/tickets/MisTicketsView.java b/src/main/java/com/example/application/views/tickets/MisTicketsView.java new file mode 100644 index 0000000..aed1817 --- /dev/null +++ b/src/main/java/com/example/application/views/tickets/MisTicketsView.java @@ -0,0 +1,26 @@ +package com.example.application.views.tickets; + +import com.example.application.views.MainLayout; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; + +@Route(value="mytickets", layout = MainLayout.class) +@AnonymousAllowed +public class MisTicketsView extends VerticalLayout { + + public MisTicketsView() { + setSizeFull(); + Div banner = new Div(); + banner.setText("Aqui es donde el usuario vera sus tickets creados"); + banner.getStyle() + .set("display", "flex") + .set("justify-content", "center") + .set("align-items", "center") + .set("height", "100%"); + + add(new H2("Mis tickets"), banner); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8509a29..7a68262 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,3 +8,11 @@ vaadin.launch-browser=true # For more information https://vaadin.com/docs/latest/integrations/spring/configuration#special-configuration-parameters vaadin.allowed-packages = com.vaadin,org.vaadin,dev.hilla,com.example.application spring.jpa.defer-datasource-initialization = true + +# Configuracion de LDAP +spring: +ldap: +urls: ldap://172.16.0.1:389 +base: DC=JUMAPACELAYA, DC=GOB, DC=MX +username: administrator +password: Dr3na%134$4guA