Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/339Lr3BJ)
### How the tests work (and Docker requirement)

This project ships with an end‑to‑end CLI integration test suite that uses Testcontainers to spin up a temporary MySQL database.
Expand Down
Binary file added databas-jdbc-Ericthilen.zip
Binary file not shown.
Empty file modified mvnw
100644 → 100755
Empty file.
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
<artifactId>mysql</artifactId>
<version>1.21.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.17</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand All @@ -70,4 +75,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
97 changes: 97 additions & 0 deletions src/main/java/com/example/AccountRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.example;

import javax.sql.DataSource;
import java.sql.*;

public class AccountRepository {
private final DataSource ds;

public AccountRepository(DataSource ds) {
this.ds = ds;
}

/** Kontrollera login mot name + password */
public boolean login(String username, String password) throws SQLException {
String sql = "SELECT 1 FROM account WHERE name=? AND password=?";
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
ps.setString(2, password);
try (ResultSet rs = ps.executeQuery()) {
return rs.next();
}
}
}
Comment on lines +14 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Plaintext password storage and comparison.

The login method compares passwords in plaintext, which is a severe security vulnerability. Passwords must be hashed using a secure algorithm like BCrypt or Argon2.

This affects the entire account system:

  • Line 15: Query retrieves plaintext passwords
  • Line 19: Compares unhashed password parameter
  • Related: createAccount (line 45) and updatePassword (line 80) also handle plaintext passwords

Recommended fix:

  1. Hash passwords before storing (in createAccount)
  2. Hash the login password and compare hashes
  3. Update updatePassword to hash new passwords

Example using BCrypt:

import org.mindrot.jbcrypt.BCrypt;

public boolean login(String username, String password) throws SQLException {
    String sql = "SELECT password FROM account WHERE name=?";
    try (Connection conn = ds.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, username);
        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) {
                String hashedPassword = rs.getString("password");
                return BCrypt.checkpw(password, hashedPassword);
            }
            return false;
        }
    }
}

Add BCrypt dependency to pom.xml:

<dependency>
    <groupId>org.mindrot</groupId>
    <artifactId>jbcrypt</artifactId>
    <version>0.4</version>
</dependency>
🤖 Prompt for AI Agents
In src/main/java/com/example/AccountRepository.java around lines 14 to 24, the
login method currently retrieves and compares plaintext passwords; change it to
retrieve the stored hashed password for the user and verify the provided
password with a secure password-hashing library (e.g., BCrypt.checkpw). Also
update createAccount (around line 45) to hash passwords before inserting into
the DB and update updatePassword (around line 80) to hash new passwords before
saving; add the BCrypt dependency to pom.xml and ensure any migration plan is in
place to re-hash existing plaintext passwords or force password resets. Ensure
prepared statements still use parameter binding and do not log raw passwords.


/** Skapa nytt konto och generera unikt username (name). Vid kollision läggs siffersuffix till (t.ex. AngFra1). */
public String createAccount(String first, String last, String ssn, String password) throws SQLException {
String base = (first == null ? "" : first.trim());
String sur = (last == null ? "" : last.trim());
String ssnTrim = (ssn == null ? "" : ssn.trim());

String baseName = base.substring(0, Math.min(3, base.length()))
+ sur.substring(0, Math.min(3, sur.length()));

String sql = "INSERT INTO account(name, password, first_name, last_name, ssn) VALUES (?,?,?,?,?)";

try (Connection conn = ds.getConnection()) {
conn.setAutoCommit(true);

String candidate = baseName;
int suffix = 0;
while (true) {
try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, candidate);
ps.setString(2, password); // medvetet: lösenord trimmas inte
ps.setString(3, base);
ps.setString(4, sur);
ps.setString(5, ssnTrim);
ps.executeUpdate();

try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) {
System.out.println("Account created with user_id=" + keys.getLong(1)
+ " and username=" + candidate);
} else {
System.out.println("Account created with username=" + candidate);
}
}
return candidate;
} catch (SQLException e) {
// SQLState 23000 = integrity constraint violation (includes unique constraint)
String sqlState = e.getSQLState();
if ("23000".equals(sqlState) || e.getMessage().toLowerCase().contains("duplicate") || e.getMessage().toLowerCase().contains("unique")) {
suffix++;
candidate = baseName + suffix;
// prova igen med nytt kandidatnamn
continue;
}
throw e;
}
}
Comment on lines +42 to +71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add safety limit to prevent infinite retry loop.

The retry logic could loop indefinitely if a non-duplicate constraint violation occurs repeatedly (e.g., invalid SSN format, NULL constraint on another field).

Add a maximum retry limit:

     String candidate = baseName;
     int suffix = 0;
+    int maxRetries = 100;
+    int attempts = 0;
-    while (true) {
+    while (attempts < maxRetries) {
+        attempts++;
         try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
             ps.setString(1, candidate);
             ps.setString(2, password);
             ps.setString(3, base);
             ps.setString(4, sur);
             ps.setString(5, ssnTrim);
             ps.executeUpdate();
             
             try (ResultSet keys = ps.getGeneratedKeys()) {
                 if (keys.next()) {
                     System.out.println("Account created with user_id=" + keys.getLong(1)
                             + " and username=" + candidate);
                 } else {
                     System.out.println("Account created with username=" + candidate);
                 }
             }
             return candidate;
         } catch (SQLException e) {
             String sqlState = e.getSQLState();
             if ("23000".equals(sqlState) || e.getMessage().toLowerCase().contains("duplicate") || e.getMessage().toLowerCase().contains("unique")) {
                 suffix++;
                 candidate = baseName + suffix;
                 continue;
             }
             throw e;
         }
     }
+    throw new SQLException("Failed to create account after " + maxRetries + " attempts");

Committable suggestion skipped: line range outside the PR's diff.

}
}

/** Uppdatera lösenord, returnerar true om lyckades */
public boolean updatePassword(long userId, String newPassword) throws SQLException {
String sql = "UPDATE account SET password=? WHERE user_id=?";
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, newPassword);
ps.setLong(2, userId);
int rows = ps.executeUpdate();
return rows > 0;
}
}

/** Ta bort konto, returnerar true om lyckades */
public boolean deleteAccount(long userId) throws SQLException {
String sql = "DELETE FROM account WHERE user_id=?";
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId);
int rows = ps.executeUpdate();
return rows > 0;
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/example/DevDatabaseInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ public static void start() {
System.setProperty("APP_DB_PASS", mysql.getPassword());
}
}
}
}
156 changes: 134 additions & 22 deletions src/main/java/com/example/Main.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package com.example;

import java.sql.Connection;
import java.sql.DriverManager;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Scanner;

public class Main {

static void main(String[] args) {
public static void main(String[] args) {
if (isDevMode(args)) {
DevDatabaseInitializer.start();
}
new Main().run();
}

public void run() {
// Resolve DB settings with precedence: System properties -> Environment variables
// Konfigurera databas
String jdbcUrl = resolveConfig("APP_JDBC_URL", "APP_JDBC_URL");
String dbUser = resolveConfig("APP_DB_USER", "APP_DB_USER");
String dbPass = resolveConfig("APP_DB_PASS", "APP_DB_PASS");
Expand All @@ -26,37 +26,149 @@ public void run() {
"as system properties (-Dkey=value) or environment variables.");
}

try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) {
DataSource ds = new SimpleDriverManagerDataSource(jdbcUrl, dbUser, dbPass);
AccountRepository accountRepo = new AccountRepository(ds);
MoonMissionRepository missionRepo = new MoonMissionRepository(ds);

Scanner scanner = new Scanner(System.in);

System.out.println("Welcome to the Moon Mission CLI!");

// ---- Login flow: prompt for username/password first ----
String currentUser = null;
try {
System.out.print("Username: ");
String username = scanner.nextLine().trim();
System.out.print("Password: ");
String password = scanner.nextLine();

boolean validLogin = accountRepo.login(username, password);
if (!validLogin) {
System.out.println("Invalid username or password");
return; // stop program after invalid attempt (tests provide an extra 0 which will be ignored)
}
currentUser = username;
System.out.println("Login successful! Welcome, " + currentUser);
} catch (SQLException e) {
throw new RuntimeException(e);
System.err.println("Database error: " + e.getMessage());
return;
} catch (Exception e) {
// In case input is exhausted
return;
}
//Todo: Starting point for your code

// ---- Authenticated menu ----
boolean running = true;
while (running) {
System.out.println("\nMenu:");
System.out.println("1) List moon missions");
System.out.println("2) Get mission by id");
System.out.println("3) Count missions for year");
System.out.println("4) Create an account");
System.out.println("5) Update an account password");
System.out.println("6) Delete an account");
System.out.println("7) Log out (switch user)");
System.out.println("0) Exit");
System.out.print("Choose an option: ");

String choice = scanner.nextLine().trim();

try {
switch (choice) {
case "1":
missionRepo.listMissions(); // skriver direkt till konsolen
break;

case "2":
System.out.print("Mission ID: ");
long id = Long.parseLong(scanner.nextLine());
missionRepo.getMissionById(id); // skriver direkt till konsolen
break;

case "3":
System.out.print("Year: ");
int year = Integer.parseInt(scanner.nextLine());
missionRepo.countByYear(year); // skriver direkt till konsolen
break;

case "4":
System.out.print("First name: ");
String firstName = scanner.nextLine().trim();
System.out.print("Last name: ");
String lastName = scanner.nextLine().trim();
System.out.print("SSN: ");
String ssnNew = scanner.nextLine().trim();
System.out.print("Password: ");
String newPass = scanner.nextLine();
String newUsername = accountRepo.createAccount(firstName, lastName, ssnNew, newPass);
System.out.println("Account created. Username: " + newUsername);
break;

case "5":
System.out.print("User ID: ");
long uid = Long.parseLong(scanner.nextLine());
System.out.print("New password: ");
String passUpdate = scanner.nextLine();
boolean updated = accountRepo.updatePassword(uid, passUpdate);
System.out.println(updated ? "Password updated" : "Update failed");
break;

case "6":
System.out.print("User ID to delete: ");
long delId = Long.parseLong(scanner.nextLine());
boolean deleted = accountRepo.deleteAccount(delId);
System.out.println(deleted ? "Account deleted" : "Delete failed");
break;

case "7":
System.out.print("Username: ");
String newUser = scanner.nextLine().trim();
System.out.print("Password: ");
String newPassLogin = scanner.nextLine();
try {
boolean ok = accountRepo.login(newUser, newPassLogin);
if (ok) {
currentUser = newUser;
System.out.println("Login successful! Welcome, " + currentUser);
} else {
System.out.println("Invalid username or password");
}
} catch (SQLException e) {
System.err.println("Database error: " + e.getMessage());
}
break;

case "0":
running = false;
System.out.println("Exiting program...");
break;

default:
System.out.println("Invalid choice. Try again.");
}
} catch (SQLException e) {
System.err.println("Database error: " + e.getMessage());
} catch (NumberFormatException e) {
System.out.println("Invalid input format");
}
}

scanner.close();
}

/**
* Determines if the application is running in development mode based on system properties,
* environment variables, or command-line arguments.
*
* @param args an array of command-line arguments
* @return {@code true} if the application is in development mode; {@code false} otherwise
*/
private static boolean isDevMode(String[] args) {
if (Boolean.getBoolean("devMode")) //Add VM option -DdevMode=true
if (Boolean.getBoolean("devMode"))
return true;
if ("true".equalsIgnoreCase(System.getenv("DEV_MODE"))) //Environment variable DEV_MODE=true
if ("true".equalsIgnoreCase(System.getenv("DEV_MODE")))
return true;
return Arrays.asList(args).contains("--dev"); //Argument --dev
return Arrays.asList(args).contains("--dev");
}

/**
* Reads configuration with precedence: Java system property first, then environment variable.
* Returns trimmed value or null if neither source provides a non-empty value.
*/
private static String resolveConfig(String propertyKey, String envKey) {
String v = System.getProperty(propertyKey);
if (v == null || v.trim().isEmpty()) {
v = System.getenv(envKey);
}
return (v == null || v.trim().isEmpty()) ? null : v.trim();
}
}
}
51 changes: 51 additions & 0 deletions src/main/java/com/example/MoonMissionRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.example;

import javax.sql.DataSource;
import java.sql.*;

public class MoonMissionRepository {
private final DataSource ds;

public MoonMissionRepository(DataSource ds) {
this.ds = ds;
}

public void listMissions() throws SQLException {
String sql = "SELECT spacecraft FROM moon_mission ORDER BY mission_id";
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("spacecraft"));
}
}
}

public void getMissionById(long id) throws SQLException {
String sql = "SELECT mission_id, spacecraft, launch_date FROM moon_mission WHERE mission_id=?";
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
System.out.println("Mission ID: " + rs.getLong("mission_id"));
System.out.println("Spacecraft: " + rs.getString("spacecraft"));
System.out.println("Launch Date: " + rs.getDate("launch_date"));
}
}
}
}

public void countByYear(int year) throws SQLException {
String sql = "SELECT COUNT(*) as count FROM moon_mission WHERE YEAR(launch_date)=?";
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, year);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
System.out.println("Number of missions in " + year + ": " + rs.getInt("count"));
}
}
}
}
}
Loading
Loading