diff --git a/src/main/java/de/hs_mannheim/informatik/ct/controller/RoomController.java b/src/main/java/de/hs_mannheim/informatik/ct/controller/RoomController.java index 31f2863f..2fc89da8 100644 --- a/src/main/java/de/hs_mannheim/informatik/ct/controller/RoomController.java +++ b/src/main/java/de/hs_mannheim/informatik/ct/controller/RoomController.java @@ -46,6 +46,7 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; +import de.hs_mannheim.informatik.ct.util.CookieManager; import de.hs_mannheim.informatik.ct.controller.exception.InvalidRoomPinException; import de.hs_mannheim.informatik.ct.model.Room; import de.hs_mannheim.informatik.ct.model.RoomVisit; @@ -136,7 +137,7 @@ public String checkIn(@PathVariable String roomId, @PostMapping("/checkIn") @Transactional - public String checkIn(@ModelAttribute RoomVisit.Data visitData, Model model) throws UnsupportedEncodingException, InvalidRoomPinException, InvalidEmailException, InvalidExternalUserdataException { + public String checkIn(@ModelAttribute RoomVisit.Data visitData, Model model, CookieManager cookieManager) throws UnsupportedEncodingException, InvalidRoomPinException, InvalidEmailException, InvalidExternalUserdataException { isRoomPinValidOrThrow(visitData); val room = roomService.getRoomOrThrow(visitData.getRoomId()); @@ -167,6 +168,8 @@ public String checkIn(@ModelAttribute RoomVisit.Data visitData, Model model) thr // room manager should always be allowed to check-in // and needs to be checked in before the browser is forwarded to a different page! val visit = roomVisitService.visitRoom(visitor, room); + + cookieManager.addCookie(CookieManager.Cookies.CHECKED_IN_EMAIL, visitorEmail); if (visitData.isPrivileged()) { val encodedVisitorEmail = URLEncoder.encode(visitorEmail, "UTF-8"); @@ -201,7 +204,7 @@ private void isRoomPinValidOrThrow(Data visitData) throws InvalidRoomPinExceptio */ @PostMapping("/checkInOverride") @Transactional - public String checkInWithOverride(@ModelAttribute RoomVisit.Data visitData, Model model) throws + public String checkInWithOverride(@ModelAttribute RoomVisit.Data visitData, Model model, CookieManager cookieManager) throws UnsupportedEncodingException, InvalidEmailException, InvalidExternalUserdataException, InvalidRoomPinException { // TODO: this method is very similar with the normal check-in, maybe this should be refactored? @@ -219,6 +222,9 @@ public String checkInWithOverride(@ModelAttribute RoomVisit.Data visitData, Mode roomVisitService.checkOutVisitor(visitor); val visit = roomVisitService.visitRoom(visitor, room); + + cookieManager.addCookie(CookieManager.Cookies.CHECKED_IN_EMAIL, visitorEmail); + val currentVisitCount = roomVisitService.getVisitorCount(room); visitData = new RoomVisit.Data(visit, currentVisitCount); model.addAttribute("visitData", visitData); @@ -227,9 +233,9 @@ public String checkInWithOverride(@ModelAttribute RoomVisit.Data visitData, Mode } @PostMapping("/checkOut") - public String checkOut(@ModelAttribute RoomVisit.Data visitData) { + public String checkOut(@ModelAttribute RoomVisit.Data visitData, CookieManager cookieManager) { val visitor = getVisitorOrThrow(visitData.getVisitorEmail()); - + cookieManager.removeCookie(CookieManager.Cookies.CHECKED_IN_EMAIL); roomVisitService.checkOutVisitor(visitor); return "redirect:/r/checkedOut"; } diff --git a/src/main/java/de/hs_mannheim/informatik/ct/controller/interceptor/CheckInInterceptor.java b/src/main/java/de/hs_mannheim/informatik/ct/controller/interceptor/CheckInInterceptor.java new file mode 100644 index 00000000..c53e9d8e --- /dev/null +++ b/src/main/java/de/hs_mannheim/informatik/ct/controller/interceptor/CheckInInterceptor.java @@ -0,0 +1,89 @@ +/* + * Corona Tracking Tool der Hochschule Mannheim + * Copyright (C) 2021 Hochschule Mannheim + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package de.hs_mannheim.informatik.ct.controller.interceptor; + +import de.hs_mannheim.informatik.ct.controller.Utilities; +import de.hs_mannheim.informatik.ct.model.RoomVisit; +import de.hs_mannheim.informatik.ct.persistence.services.RoomVisitService; +import de.hs_mannheim.informatik.ct.persistence.services.VisitorService; +import de.hs_mannheim.informatik.ct.util.CookieManager; +import lombok.val; +import lombok.var; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +public class CheckInInterceptor implements HandlerInterceptor { + + @Autowired + private VisitorService visitorService; + + @Autowired + private RoomVisitService roomVisitService; + + @Autowired + private Utilities util; + + private static final String CHECKED_IN_COOKIE_NAME = "checkedInEmail"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable ModelAndView modelAndView) throws Exception { + val cookieManager = new CookieManager(request, response); + var isCheckedIn = false; + val checkedInEmail = cookieManager.getCookieValue(CookieManager.Cookies.CHECKED_IN_EMAIL); + if(checkedInEmail != null){ + val checkedInRoom = getCheckedInRoomName(checkedInEmail); + if(checkedInRoom != null){ + isCheckedIn = true; + request.setAttribute("checkedInRoom", checkedInRoom); + }else{ + cookieManager.removeCookie(CookieManager.Cookies.CHECKED_IN_EMAIL); + } + } + request.setAttribute("checkedInEmail", checkedInEmail); + request.setAttribute("isCheckedIn", isCheckedIn); + } + + private String getCheckedInRoomName(String email){ + List roomVisits = findCurrentRoomVisitsByEmail(email); + return roomVisits.size() > 0 ? roomVisits.get(0).getRoom().getName() : null; + } + + private List findCurrentRoomVisitsByEmail(String email){ + List roomVisits = new ArrayList<>(); + val visitor = visitorService.findVisitorByEmail(email); + if(visitor.isPresent()) { + for(val roomVisit : roomVisitService.getCheckedInRoomVisits(visitor.get())){ + roomVisits.add(roomVisit); + } + } + return roomVisits; + } +} diff --git a/src/main/java/de/hs_mannheim/informatik/ct/controller/resolver/CookieManagerResolver.java b/src/main/java/de/hs_mannheim/informatik/ct/controller/resolver/CookieManagerResolver.java new file mode 100644 index 00000000..df72095f --- /dev/null +++ b/src/main/java/de/hs_mannheim/informatik/ct/controller/resolver/CookieManagerResolver.java @@ -0,0 +1,50 @@ +/* + * Corona Tracking Tool der Hochschule Mannheim + * Copyright (C) 2021 Hochschule Mannheim + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package de.hs_mannheim.informatik.ct.controller.resolver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import de.hs_mannheim.informatik.ct.util.CookieManager; + +public class CookieManagerResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter.getParameter().getType() == CookieManager.class; + } + + @Override + public Object resolveArgument( + MethodParameter methodParameter, + ModelAndViewContainer modelAndViewContainer, + NativeWebRequest nativeWebRequest, + WebDataBinderFactory webDataBinderFactory) throws Exception { + + HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest(); + HttpServletResponse response = (HttpServletResponse) nativeWebRequest.getNativeResponse(); + + return new CookieManager(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/de/hs_mannheim/informatik/ct/persistence/services/ContactTracingService.java b/src/main/java/de/hs_mannheim/informatik/ct/persistence/services/ContactTracingService.java index e2bb5d96..e4acd080 100644 --- a/src/main/java/de/hs_mannheim/informatik/ct/persistence/services/ContactTracingService.java +++ b/src/main/java/de/hs_mannheim/informatik/ct/persistence/services/ContactTracingService.java @@ -37,7 +37,8 @@ public class ContactTracingService { @NonNull public List> getVisitorContacts(@NonNull Visitor visitor) { - val contacts = new ArrayList>(); + val contacts = new ArrayList>(); + for (val service : visitServices) { contacts.addAll(service.getVisitorContacts(visitor)); } diff --git a/src/main/java/de/hs_mannheim/informatik/ct/util/CookieManager.java b/src/main/java/de/hs_mannheim/informatik/ct/util/CookieManager.java new file mode 100644 index 00000000..e61472e8 --- /dev/null +++ b/src/main/java/de/hs_mannheim/informatik/ct/util/CookieManager.java @@ -0,0 +1,151 @@ +/* + * Corona Tracking Tool der Hochschule Mannheim + * Copyright (C) 2021 Hochschule Mannheim + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.hs_mannheim.informatik.ct.util; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.time.LocalTime; + +import org.springframework.web.util.WebUtils; + +import lombok.val; + +public class CookieManager { + private HttpServletRequest request; + private HttpServletResponse response; + + public CookieManager(HttpServletRequest request){ + this.request = request; + } + + public CookieManager(HttpServletRequest request, HttpServletResponse response){ + this(request); + this.response = response; + } + + public enum Cookies{ + CHECKED_IN_EMAIL("checkedInEmail"); + + String name; + + Cookies(String name) { + this.name = name; + } + + String getName(){ + return this.name; + } + } + + /** + * create a new cookie with a cookie factory + * + * @param cookieType cookie type + * @param value value of the cookie + */ + private Cookie createCookie(Cookies cookieType, String value) { + Cookie cookie = null; + switch (cookieType){ + case CHECKED_IN_EMAIL: + cookie = new CookieBuilder(cookieType.getName(), value) + .maxAge(getSecondsTill(LocalTime.parse(ScheduledMaintenanceTasks.FORCED_END_TIME))) + .build(); + break; + } + return cookie; + } + + /** + * add a cookie to the http response + * + * @param cookieType cookie type + * @param value value of the cookie + */ + public void addCookie(Cookies cookieType, String value) { + val cookie = createCookie(cookieType, value); + response.addCookie(cookie); + } + + /** + * remove a cookie from the http response + * + * @param cookieType cookie type + */ + public void removeCookie(Cookies cookieType) { + Cookie cookie = new Cookie(cookieType.getName(), ""); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + + /** + * get a cookie from http request + * + * @param cookieType cookie type + */ + public String getCookieValue(Cookies cookieType){ + val cookie = WebUtils.getCookie(request, cookieType.getName()); + return cookie!=null ? cookie.getValue() : null; + } + + /** + * remove a cookie from the http response + * + * @param maxAgeEndTime time when the cookie should be invalid + * @return max age of the cookie + */ + private int getSecondsTill(LocalTime maxAgeEndTime){ + int now = LocalTime.now().toSecondOfDay(); + int endTime = maxAgeEndTime.toSecondOfDay(); + int nextDayEndTime = ((24 * 60 * 60) + endTime); + return (now > endTime) ? nextDayEndTime - now : endTime - now; + } + + private static class CookieBuilder{ + private String name, value, path; + private int maxAge; + + public CookieBuilder(String name, String value){ + this.name = name; + this.value = value; + this.path = "/"; + this.maxAge = 0; + } + + public CookieBuilder maxAge(int maxAge){ + this.maxAge = maxAge; + return this; + } + + public CookieBuilder path(String path){ + this.path = path; + return this; + } + + public Cookie build(){ + val cookie = new Cookie(name, value); + cookie.setPath(path); + if(maxAge>0){ + cookie.setMaxAge(maxAge); + } + return cookie; + } + } +} diff --git a/src/main/java/de/hs_mannheim/informatik/ct/util/ScheduledMaintenanceTasks.java b/src/main/java/de/hs_mannheim/informatik/ct/util/ScheduledMaintenanceTasks.java index 5e63a05d..ef91b13a 100644 --- a/src/main/java/de/hs_mannheim/informatik/ct/util/ScheduledMaintenanceTasks.java +++ b/src/main/java/de/hs_mannheim/informatik/ct/util/ScheduledMaintenanceTasks.java @@ -43,7 +43,7 @@ public class ScheduledMaintenanceTasks { private final int CRON_HOUR = 3; private final int CRON_MINUTE = 55; - private final String FORCED_END_TIME = "00:00:00"; + public static final String FORCED_END_TIME = "00:00:00"; //@Scheduled(fixedRate = 5 * 60 * 1000) // Every 5 Minutes @Scheduled(cron = "0 " + CRON_MINUTE + " " + CRON_HOUR + " * * *") // 3:55 AM diff --git a/src/main/java/de/hs_mannheim/informatik/ct/web/WebConfig.java b/src/main/java/de/hs_mannheim/informatik/ct/web/WebConfig.java new file mode 100644 index 00000000..e8400fa8 --- /dev/null +++ b/src/main/java/de/hs_mannheim/informatik/ct/web/WebConfig.java @@ -0,0 +1,51 @@ +/* + * Corona Tracking Tool der Hochschule Mannheim + * Copyright (C) 2021 Hochschule Mannheim + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.hs_mannheim.informatik.ct.web; + +import de.hs_mannheim.informatik.ct.controller.interceptor.CheckInInterceptor; +import de.hs_mannheim.informatik.ct.controller.resolver.CookieManagerResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(checkInInterceptor()) + .addPathPatterns("/") + .addPathPatterns("/r/**"); + } + + @Override + public void addArgumentResolvers( + List resolvers) { + resolvers.add(new CookieManagerResolver()); + } + + @Bean + public CheckInInterceptor checkInInterceptor(){ + return new CheckInInterceptor(); + } +} diff --git a/src/main/resources/static/general.css b/src/main/resources/static/general.css new file mode 100644 index 00000000..190af93e --- /dev/null +++ b/src/main/resources/static/general.css @@ -0,0 +1,40 @@ +.danger-color { + background: #f8d7da; + border-color: #d9534f !important; +} + +.custom-card { + display: flex; + flex-direction: column; + border: 2px solid #22376F; + border-radius: 3px; + margin: 5px; + padding: 5px; + background: #E9EBF1; +} + +.custom-card .custom-card-footer { + display: flex; + align-items: center; +} + +#check-in-alert-form{ + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.hs-button { + border: 2px solid #22376F; + border-radius: 3px; + background: #22376F; + color: #FFF; + padding: 5px; + margin: 0; +} + +.hs-button:hover { + background: #FFF; + color: #22376F; + cursor: pointer; +} diff --git a/src/main/resources/static/main.js b/src/main/resources/static/main.js new file mode 100644 index 00000000..4c48c9c2 --- /dev/null +++ b/src/main/resources/static/main.js @@ -0,0 +1,7 @@ +const checkInAlertForm = document.getElementById('check-in-alert-form') + +checkInAlertForm.onsubmit = e => { + if(!confirm("Wollen Sie sich wirklich auschecken?")){ + e.preventDefault(); + } +} diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index cb7fdec9..68a6cfb7 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -3,6 +3,7 @@ HSMA CTT + @@ -100,6 +101,7 @@
+ + +

Body contents

@@ -174,5 +184,7 @@ }); + + diff --git a/src/main/resources/templates/rooms/checkedIn.html b/src/main/resources/templates/rooms/checkedIn.html index 5bc1c0c9..f01ed706 100644 --- a/src/main/resources/templates/rooms/checkedIn.html +++ b/src/main/resources/templates/rooms/checkedIn.html @@ -15,25 +15,25 @@

Eingecheckt!

-

+

-

+

-
+

Bitte beim Verlassen des Raums auch an das - Abmelden denken. + Auschecken denken.

- \ No newline at end of file + diff --git a/src/main/resources/templates/rooms/layout.html b/src/main/resources/templates/rooms/layout.html index 37dd01f7..4434650f 100644 --- a/src/main/resources/templates/rooms/layout.html +++ b/src/main/resources/templates/rooms/layout.html @@ -4,8 +4,8 @@ HSMA CTT + - @@ -28,8 +28,17 @@

-
+ + + +
+
+ + diff --git a/src/test/java/de/hs_mannheim/informatik/ct/end_to_end/RoomControllerOverrideTest.java b/src/test/java/de/hs_mannheim/informatik/ct/end_to_end/RoomControllerOverrideTest.java index cbb410ed..9b363f0f 100644 --- a/src/test/java/de/hs_mannheim/informatik/ct/end_to_end/RoomControllerOverrideTest.java +++ b/src/test/java/de/hs_mannheim/informatik/ct/end_to_end/RoomControllerOverrideTest.java @@ -21,8 +21,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -103,7 +102,8 @@ public void checkInFullRoomWithOverride() throws Exception { .param("roomPin", TEST_ROOM_PIN) .with(csrf())) .andExpect(forwardedUrl(null)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(cookie().value("checkedInEmail", TEST_USER_EMAIL)); } /** diff --git a/src/test/java/de/hs_mannheim/informatik/ct/end_to_end/RoomControllerTest.java b/src/test/java/de/hs_mannheim/informatik/ct/end_to_end/RoomControllerTest.java index b3ec2a3a..b7381dc5 100644 --- a/src/test/java/de/hs_mannheim/informatik/ct/end_to_end/RoomControllerTest.java +++ b/src/test/java/de/hs_mannheim/informatik/ct/end_to_end/RoomControllerTest.java @@ -22,10 +22,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.net.URLDecoder; import java.net.URLEncoder; @@ -132,7 +129,8 @@ public void checkInEmptyRoom() throws Exception { .param("roomId", TEST_ROOM_NAME) .param("roomPin", TEST_ROOM_PIN) .with(csrf())) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(cookie().value("checkedInEmail", TEST_USER_EMAIL)); } @Test @@ -180,7 +178,8 @@ public void checkInFilledRoom() throws Exception { .param("roomId", TEST_ROOM_NAME) .param("roomPin", TEST_ROOM_PIN) .with(csrf())) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(cookie().value("checkedInEmail", TEST_USER_EMAIL)); } @Test @@ -233,6 +232,7 @@ public void checkOut() throws Exception { .param("roomPin", TEST_ROOM_PIN) .with(csrf())) .andExpect(status().isOk()) + .andExpect(cookie().value("checkedInEmail", TEST_USER_EMAIL)) .andDo( // check out result -> mockMvc.perform( @@ -241,6 +241,7 @@ public void checkOut() throws Exception { .param("visitorEmail", TEST_USER_EMAIL) .with(csrf())) .andExpect(status().isFound()) + .andExpect(cookie().value("checkedInEmail", "")) .andExpect(redirectedUrl("/r/checkedOut"))); } @@ -255,6 +256,7 @@ public void checkOutInvalidCredentials() throws Exception { .param("roomPin", TEST_ROOM_PIN) .with(csrf())) .andExpect(status().isOk()) + .andExpect(cookie().value("checkedInEmail", TEST_USER_EMAIL)) .andDo( // check out result -> mockMvc.perform(