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)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Badge link is fine; just confirm it’s appropriate for the repo’s visibility (public vs classroom-only).
If this repo is public, consider whether you want to expose the Classroom assignment link.

🤖 Prompt for AI Agents
In README.md around lines 1 to 1, the Classroom assignment badge may expose a
classroom-only link if the repo is public; confirm the repository visibility and
either remove the badge from the public README or replace it with a
non-sensitive/public-facing badge. If the repo must remain public and the badge
should not be exposed, delete the badge line and optionally add a note
indicating the assignment is in a private classroom; if the repo is private or
intended for classroom use, leave the badge but add a short comment in the
README clarifying that it links to a classroom assignment and is safe for this
repository.

### 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
289 changes: 285 additions & 4 deletions src/main/java/com/example/Main.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.example;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.*;
import java.util.Arrays;

public class Main {

static void main(String[] args) {
public static void main(String[] args) {
//Kollar om applikationen körs i utvecklarläge genom anrop till isDevMode-metoden
if (isDevMode(args)) {
//Körs den i utvecklarläge, startar den en testdatabas
DevDatabaseInitializer.start();
}
//Skapar en ny instans av Main och anropar run() metoden för att starta applikationen
new Main().run();
}

Expand All @@ -31,6 +35,280 @@ public void run() {
throw new RuntimeException(e);
}
//Todo: Starting point for your code

//Inloggning
boolean inloggad = false;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
while (!inloggad) {
try {
System.out.print("Username: ");
String username = reader.readLine();
System.out.print("Password: ");
String password = reader.readLine();
inloggad = validateLogin(jdbcUrl, dbUser, dbPass, username, password);
if (!inloggad) {
System.out.println("Invalid username or password. Press 0 to exit.");
if (reader.readLine().equals("0")) {
return;
}
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}

Comment on lines +39 to +59
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid potential NPE on exit prompt when stdin closes (EOF).
At Line 51 reader.readLine().equals("0") can throw if readLine() returns null.

-                        if (reader.readLine().equals("0")) {
+                        String exitChoice = reader.readLine();
+                        if ("0".equals(exitChoice)) {
                             return;
                         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//Inloggning
boolean inloggad = false;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
while (!inloggad) {
try {
System.out.print("Username: ");
String username = reader.readLine();
System.out.print("Password: ");
String password = reader.readLine();
inloggad = validateLogin(jdbcUrl, dbUser, dbPass, username, password);
if (!inloggad) {
System.out.println("Invalid username or password. Press 0 to exit.");
if (reader.readLine().equals("0")) {
return;
}
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
//Inloggning
boolean inloggad = false;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
while (!inloggad) {
try {
System.out.print("Username: ");
String username = reader.readLine();
System.out.print("Password: ");
String password = reader.readLine();
inloggad = validateLogin(jdbcUrl, dbUser, dbPass, username, password);
if (!inloggad) {
System.out.println("Invalid username or password. Press 0 to exit.");
String exitChoice = reader.readLine();
if ("0".equals(exitChoice)) {
return;
}
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
🤖 Prompt for AI Agents
In src/main/java/com/example/Main.java around lines 39 to 59, the call
reader.readLine().equals("0") can throw a NullPointerException if stdin is
closed (readLine() returns null); to fix, read the line into a local variable,
check it for null before calling equals (or use "0".equals(line)), and treat
null the same as an exit condition (return or break) so the program handles EOF
safely.

//Meny
boolean running = true;
while (running) {
printMenu();
try {
String choice = reader.readLine();
switch (choice) {
case "1":
listMoonMissions(jdbcUrl, dbUser, dbPass);
break;
case "2":
getMoonMissionById(reader, jdbcUrl, dbUser, dbPass);
break;
case "3":
countMissionsByYear(reader, jdbcUrl, dbUser, dbPass);
break;
case "4":
createAccount(reader, jdbcUrl, dbUser, dbPass);
break;
case "5":
updateAccountPassword(reader, jdbcUrl, dbUser, dbPass);
break;
case "6":
deleteAccount(reader, jdbcUrl, dbUser, dbPass);
break;
case "0":
running = false; //Avsluta applikationen
break;
default:
System.out.println("Invalid option. Try again");
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
} catch (IOException e) {
System.err.println("Failed to initialize reader: " + e.getMessage());
}
}

private void printMenu() {

System.out.println(
"""
========== MENU ==========
1) List moon missions
2) Get a moon mission by ID
3) Count missions for a given year
4) Create an account
5) Update an account password
6) Delete an account
0) Exit
==========================
"""
);
System.out.print("Choose an option: ");
}

private void listMoonMissions(String jdbcUrl, String dbUser, String dbPass) {
String sql = "SELECT spacecraft FROM moon_mission";
try (
Connection databaseConnection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass);
Statement statement = databaseConnection.createStatement();
ResultSet spacecraftResult = statement.executeQuery(sql)
) {
System.out.println("\nMoon Missions:");
while (spacecraftResult.next()) {
System.out.println("- " + spacecraftResult.getString("spacecraft"));
}
} catch (SQLException e) {
System.err.println("Error listing moon missions: " + e.getMessage());
}

}

private void getMoonMissionById(BufferedReader reader, String jdbcUrl, String dbUser, String dbPass) {
try {
System.out.print("Enter mission ID: ");
long missionId = Long.parseLong(reader.readLine());

String sql = "SELECT mission_id, spacecraft, launch_date FROM moon_mission WHERE mission_id = ?";
try (
Connection databaseConnection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass);
PreparedStatement missionQuery = databaseConnection.prepareStatement(sql)
) {
missionQuery.setLong(1, missionId);
ResultSet missionResult = missionQuery.executeQuery();

if (missionResult.next()) {
System.out.println("\nMission Details:"); // Rubrik för tydlighet
System.out.println("Mission ID: " + missionResult.getLong("mission_id")); // Innehåller "id"
System.out.println("Spacecraft: " + missionResult.getString("spacecraft")); // Innehåller "Luna 3"
System.out.println("Launch Date: " + missionResult.getDate("launch_date")); // Extra info
} else {
System.out.println("No mission found with ID: " + missionId);
}
}
} catch (SQLException | IOException | NumberFormatException e) {
System.err.println("Error: " + e.getMessage());
}

}

private void countMissionsByYear(BufferedReader reader, String jdbcUrl, String dbUser, String dbPass) {
try {
System.out.print("Enter year: ");
int year = Integer.parseInt(reader.readLine());

String sql = "SELECT COUNT(*) AS count FROM moon_mission WHERE YEAR(launch_date) = ?";
try (
Connection databaseConnection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass);
PreparedStatement countQuery = databaseConnection.prepareStatement(sql)
) {
countQuery.setInt(1, year);
ResultSet countResult = countQuery.executeQuery();

if (countResult.next()) {
int missionCount = countResult.getInt("count");
System.out.printf(
"\nNumber of missions in %d: %d\n", // Innehåller året och antalet
year, // 2019
missionCount // 3
);
}
}
} catch (SQLException | IOException | NumberFormatException e) {
System.err.println("Error: " + e.getMessage());
}
}

private void createAccount(BufferedReader reader, String jdbcUrl, String dbUser, String dbPass) {
try {
System.out.print("Enter first name: ");
String firstName = reader.readLine().trim();
System.out.print("Enter last name: ");
String lastName = reader.readLine().trim();
System.out.print("Enter SSN: ");
String ssn = reader.readLine();
System.out.print("Enter password: ");
String password = reader.readLine();

//Validera att namn inte är tomma
if (firstName.isEmpty() || lastName.isEmpty()) {
System.out.println("Error: First name and last name cannot be empty.");
return;
}

//Generera användarnamn på ett säkert sätt
String firstPart = firstName.length() >= 3 ?
firstName.substring(0, 3).toLowerCase() :
firstName.toLowerCase();

String lastPart = lastName.length() >= 3 ?
lastName.substring(0, 3).toLowerCase() :
lastName.toLowerCase();

String name = firstPart + lastPart;

String sql = "INSERT INTO account (first_name, last_name, ssn, password, name) VALUES (?, ?, ?, ?, ?)";
try (
Connection databaseConnection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass);
PreparedStatement insertAccount = databaseConnection.prepareStatement(sql)
) {
insertAccount.setString(1, firstName);
insertAccount.setString(2, lastName);
insertAccount.setString(3, ssn);
insertAccount.setString(4, password);
insertAccount.setString(5, name);

int rowsAffected = insertAccount.executeUpdate();
if (rowsAffected > 0) {
System.out.println("Account created!"); // Bekräftelse som testet letar efter
} else {
System.out.println("Failed to create account.");
}
}
} catch (SQLException | IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
Comment on lines +218 to +239
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

Critical: Plaintext password storage.

Line 227 stores passwords in plaintext in the database. This is a severe security vulnerability. Passwords should always be hashed using a strong algorithm (BCrypt, Argon2, PBKDF2) before storage.

Consider using a library like BCrypt:

import org.mindrot.jbcrypt.BCrypt;

// When creating account:
String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12));
insertAccount.setString(4, hashedPassword);

Then update validateLogin to use BCrypt.checkpw(password, hashedPassword) for comparison.

Note: This also impacts the updateAccountPassword and validateLogin methods.


private void updateAccountPassword(BufferedReader reader, String jdbcUrl, String dbUser, String dbPass) {
try {
System.out.print("Enter user ID: ");
long userId = Long.parseLong(reader.readLine());
System.out.print("New password: ");
String newPassword = reader.readLine();

String sql = "UPDATE account SET password = ? WHERE user_id = ?";
try (
Connection databaseConnection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass);
PreparedStatement updatePassword = databaseConnection.prepareStatement(sql)
) {
updatePassword.setString(1, newPassword);
updatePassword.setLong(2, userId);

int rowsAffected = updatePassword.executeUpdate();
if (rowsAffected > 0) {
System.out.println("Password updated!"); // Bekräftelse som testet letar efter
} else {
System.out.println("User not found.");
}
}
} catch (SQLException | IOException | NumberFormatException e) {
System.err.println("Error: " + e.getMessage());
}
}
Comment on lines +241 to +266
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

Plaintext password issue also applies here.

Line 253 updates passwords in plaintext. This method should hash the new password before storing it, consistent with the security concern raised in createAccount.

🤖 Prompt for AI Agents
In src/main/java/com/example/Main.java around lines 241 to 266, the method
currently stores the new password in plaintext (line ~253); instead hash the new
password before saving using the same hashing utility/mechanism used in
createAccount (e.g., bcrypt with a generated salt or your existing
PasswordHasher.hash(password) method). Replace the direct newPassword usage with
a call that produces a hashedPassword, perform any same validation
(length/requirements) as createAccount, then set the hashedPassword on the
PreparedStatement parameter before executeUpdate, preserving the existing
try-with-resources and exception handling.


private void deleteAccount(BufferedReader reader, String jdbcUrl, String dbUser, String dbPass) {
try {
System.out.print("Enter user ID: ");
long userId = Long.parseLong(reader.readLine());

String sql = "DELETE FROM account WHERE user_id = ?";
try (
Connection databaseConnection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass);
PreparedStatement deleteAccount = databaseConnection.prepareStatement(sql)
) {
deleteAccount.setLong(1, userId);

int rowsAffected = deleteAccount.executeUpdate();
if (rowsAffected > 0) {
System.out.println("Account deleted!"); // Bekräftelse som testet letar efter
} else {
System.out.println("User not found.");
}
}
} catch (SQLException | IOException | NumberFormatException e) {
System.err.println("Error: " + e.getMessage());
}
}

private boolean validateLogin(String jdbcUrl, String dbUser, String dbPass, String username, String password){
String sqlQuery = "SELECT COUNT(*) FROM account WHERE name = ? AND password = ?";

try (
Connection databaseConnection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass);
PreparedStatement preparedStatement = databaseConnection.prepareStatement(sqlQuery)
) {
preparedStatement.setString(1, username);
preparedStatement.setString(2, password);

ResultSet resultSet = preparedStatement.executeQuery();

if (resultSet.next()) {
return resultSet.getInt(1) > 0; // Returnera true om användaren finns
}
} catch (SQLException e) {
System.err.println("Fel vid validering av inloggning: " + e.getMessage());
}

return false; // Returnera false om något gick fel eller om användaren inte finns
}
Comment on lines +292 to 312
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

Multiple security concerns in login validation.

  1. Plaintext password comparison: Line 300 compares passwords in plaintext. Should verify against hashed passwords.
  2. Timing attack risk: Using COUNT(*) with both username and password in the WHERE clause can leak information through timing differences between "user doesn't exist" and "wrong password" scenarios.

Better approach after implementing password hashing:

private boolean validateLogin(String jdbcUrl, String dbUser, String dbPass, String username, String password) {
    String sqlQuery = "SELECT password FROM account WHERE name = ?";
    
    try (
            Connection databaseConnection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass);
            PreparedStatement preparedStatement = databaseConnection.prepareStatement(sqlQuery)
    ) {
        preparedStatement.setString(1, username);
        ResultSet resultSet = preparedStatement.executeQuery();
        
        if (resultSet.next()) {
            String hashedPassword = resultSet.getString("password");
            return BCrypt.checkpw(password, hashedPassword);
        }
    } catch (SQLException e) {
        System.err.println("Fel vid validering av inloggning: " + e.getMessage());
    }
    
    return false;
}

This fetches the hash first, then verifies, providing more consistent timing.

🤖 Prompt for AI Agents
In src/main/java/com/example/Main.java around lines 292 to 312, the current
login validation uses COUNT(*) with plaintext password comparison which leaks
information and is insecure; change it to SELECT the stored password hash by
username, then verify the supplied password using a secure password-hashing
library (e.g., BCrypt.checkpw), avoid using COUNT(*) for auth checks, and to
mitigate timing attacks always perform a hash verification even when the user is
not found (use a fixed/dummy hash) so timing is consistent; keep using
PreparedStatement to avoid SQL injection and ensure exceptions are logged
appropriately.


/**
Expand Down Expand Up @@ -59,4 +337,7 @@ private static String resolveConfig(String propertyKey, String envKey) {
}
return (v == null || v.trim().isEmpty()) ? null : v.trim();
}


}

Loading