Skip to content

Add 'Remember Me' Feature and Tests #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions build_and_run_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ else
SEARCH_FEATURE="-DenableSearchFeature=$1"
fi

if [ "$1" == "r" ]; then
docker run -d -p 6379:6379 --name redis_container redis
echo "Redis container is running."
exit 0
fi

cleanup() {
docker stop redis_container > /dev/null 2>&1 || true
docker rm redis_container > /dev/null 2>&1 || true
rm *db > /dev/null 2>&1 || true
}

# Trap the EXIT signal to perform cleanup
trap cleanup EXIT
# trap cleanup EXIT

set -e # Exit immediately if a command exits with a non-zero status.
mvn clean package -Dmaven.test.skip=true
docker run -d -p 6379:6379 --name redis_container redis
docker run -d -p 6379:6379 --name redis_container redis | exit 0
java $SEARCH_FEATURE -jar target/salesmanager-*-SNAPSHOT.jar --spring.redis.host=localhost --spring.redis.port=6379 --spring.redis.mode=standalone --server.port=8086 --spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
32 changes: 10 additions & 22 deletions src/main/java/net/codejava/AppController.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import java.time.ZoneId;
import javax.servlet.http.Cookie;

@EnableJpaRepositories(basePackages = "net.codejava")
@Controller
Expand Down Expand Up @@ -165,21 +166,27 @@
}

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String loginPost(HttpServletRequest request, Model model) {
public String loginPost(HttpServletRequest request, HttpServletResponse response, Model model) {
String username = request.getParameter("username");
String password = request.getParameter("password");
boolean rememberMe = "on".equals(request.getParameter("rememberMe"));

// Authenticate the user
Authentication auth = new UsernamePasswordAuthenticationToken(username, password);
try {
auth = authenticationManager.authenticate(auth);
SecurityContextHolder.getContext().setAuthentication(auth);

if (rememberMe) {
Cookie rememberMeCookie = new Cookie("rememberMe", "true");
rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
rememberMeCookie.setHttpOnly(true);
response.addCookie(rememberMeCookie);

Check warning

Code scanning / CodeQL

Failure to use secure cookies Medium

Cookie is added to response without the 'secure' flag being set.

Copilot Autofix

AI about 1 month ago

To fix the issue, the 'secure' flag must be explicitly set on the rememberMeCookie before it is added to the response. This ensures that the cookie is only transmitted over secure HTTPS connections. The fix involves calling the setSecure(true) method on the rememberMeCookie object before the response.addCookie() call. This change does not alter the existing functionality but enhances the security of the application.


Suggested changeset 1
src/main/java/net/codejava/AppController.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/net/codejava/AppController.java b/src/main/java/net/codejava/AppController.java
--- a/src/main/java/net/codejava/AppController.java
+++ b/src/main/java/net/codejava/AppController.java
@@ -179,6 +179,7 @@
 			if (rememberMe) {
-				Cookie rememberMeCookie = new Cookie("rememberMe", "true");
-				rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
-				rememberMeCookie.setHttpOnly(true);
-				response.addCookie(rememberMeCookie);
+				Cookie rememberMeCookie = new Cookie("rememberMe", "true");
+				rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
+				rememberMeCookie.setHttpOnly(true);
+				rememberMeCookie.setSecure(true);
+				response.addCookie(rememberMeCookie);
 			}
EOF
@@ -179,6 +179,7 @@
if (rememberMe) {
Cookie rememberMeCookie = new Cookie("rememberMe", "true");
rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
rememberMeCookie.setHttpOnly(true);
response.addCookie(rememberMeCookie);
Cookie rememberMeCookie = new Cookie("rememberMe", "true");
rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
rememberMeCookie.setHttpOnly(true);
rememberMeCookie.setSecure(true);
response.addCookie(rememberMeCookie);
}
Copilot is powered by AI and may make mistakes. Always verify output.
}
} catch (BadCredentialsException e) {
model.addAttribute("error", "Invalid username or password.");
return "login";
}

// User is authenticated, redirect to landing page
return "redirect:/";
}

Expand All @@ -200,25 +207,6 @@
return handleSale(sale, session, redirectAttributes, () -> dao.clearRecord(serialNumber));
}

@RequestMapping("/export")
public void exportToCSV(HttpServletResponse response) throws IOException {
response.setContentType("text/csv");
response.setHeader("Content-Disposition", "attachment; filename=sales.csv");
List<Sale> listSale = dao.listAll();
// create a writer
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
// write header line
writer.write("Serial Number, Date, Amount, Item Name");
writer.newLine();
// write data lines
for (Sale sale : listSale) {
String line = String.format("%s, %s, %s, %s", sale.getSerialNumber(), sale.getDate(), sale.getAmount(), sale.getItem());
writer.write(line);
writer.newLine();
}
writer.flush();
}

@PostMapping("/import")
public String uploadFile(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
try {
Expand Down
12 changes: 5 additions & 7 deletions src/main/java/net/codejava/SalesDAO.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public List<Sale> list(int limit, int offset) {
return listSale;
}

public List<Sale> listAll() {
String sql = "SELECT * FROM sales ORDER BY serial_number ASC";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Sale.class));
}

public void save(Sale sale) throws DuplicateKeyException {
try {
System.out.println(sale); // log the Sale object
Expand Down Expand Up @@ -118,13 +123,6 @@ public Page<Sale> findAll(Pageable pageable) {
return new PageImpl<>(sales, pageable, total);
}

// a method to returns a list of all sales in a jdbctemplate query to use as a csv output
public List<Sale> listAll() {
String sql = "SELECT * FROM sales ORDER BY serial_number ASC";
List<Sale> listSale = jdbcTemplate.query(sql, BeanPropertyRowMapper.newInstance(Sale.class));
return listSale;
}

// save all sales in a list
public void saveAll(List<Sale> sales) {
if (sales == null) {
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/net/codejava/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ protected void configure(HttpSecurity http) throws Exception {
.logout()
.logoutUrl("/logout") // This is the URL to send the user to once they have logged out
.invalidateHttpSession(true)
.permitAll();
.permitAll()
.and()
.rememberMe()
.key("uniqueAndSecret")
.tokenValiditySeconds(7 * 24 * 60 * 60); // 7 days
}

@Bean
Expand Down
2 changes: 0 additions & 2 deletions src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
<div align="center" th:if="${listSale}">
<h1>Inventory Records</h1>
<a href="/new" class="modern-button">Enter New Product</a>
<a href="/export" class="modern-button">Export to CSV</a>
<form id="uploadForm" style="display: none;">
<input type="file" id="csvFile" name="file" accept=".csv">
<button type="submit" id="submitBtn">Submit</button>
Expand All @@ -47,7 +46,6 @@ <h1>Inventory Records</h1>
</div>
<br />
<form th:action="@{/logout}" method="post">
<!-- <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> -->
<input type="submit" value="Logout" />
</form>
<br />
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ <h1>Welcome, Sales Manager!</h1>
<input type="text" id="username" name="username" required />
<label for="password">Password:</label>
<input type="password" id="password" name="password" required />
<label for="rememberMe">Remember Me:</label>
<input type="checkbox" id="rememberMe" name="rememberMe" />
<button type="submit">Login</button>
</form>
</div>
Expand Down
48 changes: 48 additions & 0 deletions src/test/java/net/codejava/AppControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.codejava;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;

import javax.servlet.http.Cookie;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
public class AppControllerTest {

@Autowired
private AppController appController;

@Autowired
private PersistentTokenBasedRememberMeServices rememberMeServices;

@Test
public void testRememberMeFunctionality() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();

// Simulate login request with 'Remember Me' checked
request.setParameter("username", "testuser");
request.setParameter("password", "testpassword");
request.setParameter("rememberMe", "on");

String view = appController.loginPost(request, response, null);

// Assert that the user is redirected to the home page
assertEquals("redirect:/", view);

// Assert that the 'Remember Me' cookie is set
Cookie rememberMeCookie = response.getCookie("rememberMe");
assertNotNull(rememberMeCookie);
assertEquals("true", rememberMeCookie.getValue());
assertTrue(rememberMeCookie.getMaxAge() > 0);

// Assert that the security context is populated
assertNotNull(SecurityContextHolder.getContext().getAuthentication());
}
}
151 changes: 151 additions & 0 deletions src/test/java/net/codejava/JUnit5ExampleTest12.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package net.codejava;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
public class JUnit5ExampleTest12 {

// Global variables to control test behavior
private static boolean isFeatureEnabled = true;
private static int maxRecordsPerPage = 20;
private static String defaultSearchQuery = "Laptop";
private static String defaultItemName = "Smartphone";
private static double defaultItemPrice = 999.99;
private static String testLogPrefix = "[TEST LOG] "; // New global variable

@Autowired
private AppController appController;

@Test
void testEnableSearchFeatureDefaultValue() {
if (isFeatureEnabled) {
System.out.println(testLogPrefix + "Feature is enabled: Running testEnableSearchFeatureDefaultValue");
assertTrue(appController.getEnableSearchFeature(), testLogPrefix + "enableSearchFeature should be true by default");
} else {
System.out.println(testLogPrefix + "Feature is disabled: Skipping testEnableSearchFeatureDefaultValue");
}

System.out.println(testLogPrefix + "Checking additional conditions...");
System.out.println(testLogPrefix + "Test completed successfully.");
System.out.println(testLogPrefix + "Logging additional information.");
System.out.println(testLogPrefix + "Feature flag value: " + isFeatureEnabled);
System.out.println(testLogPrefix + "Default search query: " + defaultSearchQuery);
System.out.println(testLogPrefix + "Default item name: " + defaultItemName);
System.out.println(testLogPrefix + "Default item price: " + defaultItemPrice);
System.out.println(testLogPrefix + "Max records per page: " + maxRecordsPerPage);
System.out.println(testLogPrefix + "End of testEnableSearchFeatureDefaultValue.");
}

@Test
void testMaxRecordsPerPage() {
System.out.println("Max records per page: " + maxRecordsPerPage);
assertEquals(20, maxRecordsPerPage, "Max records per page should be 20");
}

@Test
void testDefaultSearchQuery() {
System.out.println("Default search query: " + defaultSearchQuery);
assertEquals("Laptop", defaultSearchQuery, "Default search query should be 'Laptop'");
}

@Test
void testDefaultItemName() {
System.out.println("Default item name: " + defaultItemName);
assertEquals("Smartphone", defaultItemName, "Default item name should be 'Smartphone'");
}

@Test
void testDefaultItemPrice() {
System.out.println("Default item price: " + defaultItemPrice);
assertEquals(999.99, defaultItemPrice, "Default item price should be 999.99");
}

@Test
void testEnableSearchFeatureInHomePage() {
if (isFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInHomePage");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("Home Page - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true on the home page");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInHomePage");
}
}

@Test
void testEnableSearchFeatureInNewForm() {
if (isFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInNewForm");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("New Form - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true in the new form");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInNewForm");
}
}

@Test
void testEnableSearchFeatureInEditForm() {
if (isFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInEditForm");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("Edit Form - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true in the edit form");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInEditForm");
}
}

@Test
void testEnableSearchFeatureInSearch() {
if (isFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInSearch");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("Search - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true during search");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInSearch");
}
}

@Test
void testMaxRecordsPerPageInSearch() {
System.out.println("Testing maxRecordsPerPage in search functionality");
assertEquals(20, maxRecordsPerPage, "Max records per page should be consistent in search functionality");
}

@Test
void testDefaultSearchQueryInSearch() {
System.out.println("Testing defaultSearchQuery in search functionality");
assertEquals("Laptop", defaultSearchQuery, "Default search query should be consistent in search functionality");
}

@Test
void testDefaultItemNameInSearch() {
System.out.println("Testing defaultItemName in search functionality");
assertEquals("Smartphone", defaultItemName, "Default item name should be consistent in search functionality");
}

@Test
void testDefaultItemPriceInSearch() {
System.out.println("Testing defaultItemPrice in search functionality");
assertEquals(999.99, defaultItemPrice, "Default item price should be consistent in search functionality");
}

@Test
void testEnableSearchFeatureInSave() {
if (isFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInSave");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("Save - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true during save");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInSave");
}
}
}
Loading