Merge pull request #26 from RecentRunner/main

Update
This commit is contained in:
2026-02-22 18:07:16 -07:00
committed by GitHub
18 changed files with 1081 additions and 36 deletions

5
.gitignore vendored
View File

@@ -36,4 +36,7 @@ build/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store
## Database related
connectionpetstore.properties

View File

@@ -0,0 +1,3 @@
url=jdbc:mysql://127.0.0.1:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
user=petapp
password=petapppass

View File

@@ -8,9 +8,9 @@
<artifactId>PetShopDesktop</artifactId>
<version>1.0-SNAPSHOT</version>
<name>PetShopDesktop</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>25.0.2</javafx.version>
<junit.version>5.12.1</junit.version>
</properties>
@@ -18,12 +18,12 @@
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>21.0.6</version>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>21.0.6</version>
<version>${javafx.version}</version>
</dependency>
<dependency>
@@ -66,8 +66,7 @@
<!-- Default configuration for running with: mvn clean javafx:run -->
<id>default-cli</id>
<configuration>
<mainClass>org.example.petshopdesktop/org.example.petshopdesktop.PetShopApplication
</mainClass>
<mainClass>org.example.petshopdesktop/org.example.petshopdesktop.PetShopApplication</mainClass>
<launcher>app</launcher>
<jlinkZipName>app</jlinkZipName>
<jlinkImageName>app</jlinkImageName>

View File

@@ -8,7 +8,9 @@ module org.example.petshopdesktop {
opens org.example.petshopdesktop to javafx.fxml;
opens org.example.petshopdesktop.controllers.dialogcontrollers to javafx.fxml;
opens org.example.petshopdesktop.controllers to javafx.fxml;
opens org.example.petshopdesktop.auth to javafx.fxml;
exports org.example.petshopdesktop;
exports org.example.petshopdesktop.controllers;
exports org.example.petshopdesktop.auth;
}

View File

@@ -4,15 +4,21 @@ import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.example.petshopdesktop.database.UserDB;
import java.io.IOException;
public class PetShopApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("main-layout-view.fxml"));
try {
UserDB.initializeTable();
} catch (Exception e) {
System.err.println("Warning: could not initialize users table: " + e.getMessage());
}
FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("login-view.fxml"));
Scene scene = new Scene(fxmlLoader.load());
stage.setTitle("Pet Shop Manager");
stage.setTitle("Pet Shop Manager - Login");
stage.setScene(scene);
stage.show();
}

View File

@@ -0,0 +1,13 @@
package org.example.petshopdesktop.auth;
/*
Petshop Desktop
Purpose: Application role definitions used by session state and role based access control.
*/
public enum Role {
// Administrative access, includes system management screens.
ADMIN,
// Staff access, limited to day to day operational screens.
STAFF
}

View File

@@ -0,0 +1,59 @@
package org.example.petshopdesktop.auth;
/*
Petshop Desktop
Purpose: In memory session state for the authenticated user.
Notes: Session is process local and cleared on logout or application restart.
*/
public class UserSession {
// Singleton instance used to share session state across controllers.
private static UserSession instance;
// Current authenticated username, null when logged out.
private String username;
// Current authenticated role, null when logged out.
private Role role;
private UserSession() {}
// Lazily initialised singleton accessor.
public static UserSession getInstance() {
if (instance == null) {
instance = new UserSession();
}
return instance;
}
// Stores identity and role for the active session.
public void login(String username, Role role) {
this.username = username;
this.role = role;
}
// Clears session state and returns the application to an unauthenticated state.
public void logout() {
this.username = null;
this.role = null;
}
public String getUsername() {
return username;
}
public Role getRole() {
return role;
}
// Convenience check for administrative privileges.
// Role.ADMIN.equals(role) remains safe when role is null.
public boolean isAdmin() {
return Role.ADMIN.equals(role);
}
// Session is considered active only when both username and role are set.
public boolean isLoggedIn() {
return username != null && role != null;
}
}

View File

@@ -0,0 +1,76 @@
package org.example.petshopdesktop.controllers;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.database.UserDB;
import org.example.petshopdesktop.models.User;
import java.sql.SQLException;
/*
Petshop Desktop
Purpose: Authentication controller responsible for validating credentials and initialising the user session.
*/
public class LoginController {
@FXML
private TextField txtUsername;
@FXML
private PasswordField txtPassword;
@FXML
private Label lblError;
@FXML
void btnLoginClicked(ActionEvent event) {
// Input normalisation keeps authentication behaviour consistent.
String username = txtUsername.getText().trim();
String password = txtPassword.getText();
// Basic validation to avoid unnecessary database calls.
if (username.isEmpty() || password.isEmpty()) {
lblError.setText("Please enter username and password.");
return;
}
try {
// Credential verification returns a fully populated User on success.
User user = UserDB.authenticate(username, password);
if (user == null) {
lblError.setText("Invalid username or password.");
txtPassword.clear();
return;
}
// Session state is stored in memory for use by controllers and UI RBAC.
UserSession.getInstance().login(user.getUsername(), user.getRole());
openMainLayout();
} catch (SQLException e) {
lblError.setText("Database error: " + e.getMessage());
}
}
private void openMainLayout() {
try {
// View transition into the post login application shell.
FXMLLoader loader = new FXMLLoader(
getClass().getResource("/org/example/petshopdesktop/main-layout-view.fxml"));
Scene scene = new Scene(loader.load());
Stage stage = (Stage) txtUsername.getScene().getWindow();
stage.setScene(scene);
stage.setTitle("Pet Shop Manager");
} catch (Exception e) {
lblError.setText("Error loading application: " + e.getMessage());
e.printStackTrace();
}
}
}

View File

@@ -4,9 +4,17 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.example.petshopdesktop.auth.UserSession;
/*
Petshop Desktop
Purpose: Main application shell controller, includes navigation and UI level role based access control.
*/
public class MainLayoutController {
@FXML
@@ -18,6 +26,9 @@ public class MainLayoutController {
@FXML
private Button btnInventory;
@FXML
private Button btnLogout;
@FXML
private Button btnPets;
@@ -36,6 +47,12 @@ public class MainLayoutController {
@FXML
private Button btnSuppliers;
@FXML
private Label lblUsername;
@FXML
private Label lblRole;
@FXML
private StackPane spContentArea;
@@ -93,11 +110,52 @@ public class MainLayoutController {
updateButtons(btnSuppliers);
}
@FXML
void btnLogoutClicked(ActionEvent event) {
// Logout clears session state before returning to the login view.
UserSession.getInstance().logout();
try {
FXMLLoader loader = new FXMLLoader(
getClass().getResource("/org/example/petshopdesktop/login-view.fxml"));
Scene scene = new Scene(loader.load());
Stage stage = (Stage) btnLogout.getScene().getWindow();
stage.setScene(scene);
stage.setTitle("Pet Shop Manager - Login");
} catch (Exception e) {
System.err.println("Error loading login view: " + e.getMessage());
e.printStackTrace();
}
}
@FXML
public void initialize() {
// RBAC state is applied once during initial layout load.
applyRBAC();
// Default landing view after successful authentication.
loadView("pet-view.fxml");
}
private void applyRBAC() {
UserSession session = UserSession.getInstance();
// Session identity is displayed in the header for clarity and auditing.
lblUsername.setText(session.getUsername());
lblRole.setText(session.getRole().toString());
// UI level RBAC hides admin only navigation entries for non admin users.
// setManaged(false) removes the node from layout calculations to avoid empty spacing.
boolean isAdmin = session.isAdmin();
btnInventory.setVisible(isAdmin);
btnInventory.setManaged(isAdmin);
btnSuppliers.setVisible(isAdmin);
btnSuppliers.setManaged(isAdmin);
btnProductSuppliers.setVisible(isAdmin);
btnProductSuppliers.setManaged(isAdmin);
// Privileged operations should still be enforced within the relevant controllers and database methods.
}
/**
* Load a view when a button is clicked on the navigation
* @param fxmlFile the fxmlFile name to be loaded
@@ -111,7 +169,8 @@ public class MainLayoutController {
spContentArea.getChildren().clear();
spContentArea.getChildren().add(view);
} catch (Exception e) {
System.out.println("Error loading view: " + fxmlFile);
System.err.println("Error loading view: " + fxmlFile);
e.printStackTrace();
}
}

View File

@@ -1,11 +1,24 @@
package org.example.petshopdesktop.controllers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController;
import org.example.petshopdesktop.database.PetDB;
import org.example.petshopdesktop.database.ProductDB;
import org.example.petshopdesktop.models.Pet;
import java.io.IOException;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.Optional;
public class PetController {
@@ -19,45 +32,191 @@ public class PetController {
private Button btnEdit;
@FXML
private TableColumn<?, ?> colPetAge;
private TableColumn<Pet, Integer> colPetAge;
@FXML
private TableColumn<?, ?> colPetBreed;
private TableColumn<Pet, String> colPetBreed;
@FXML
private TableColumn<?, ?> colPetId;
private TableColumn<Pet, Integer> colPetId;
@FXML
private TableColumn<?, ?> colPetName;
private TableColumn<Pet, String> colPetName;
@FXML
private TableColumn<?, ?> colPetPrice;
private TableColumn<Pet, Double> colPetPrice;
@FXML
private TableColumn<?, ?> colPetSpecies;
private TableColumn<Pet, String> colPetSpecies;
@FXML
private TableColumn<?, ?> colPetStatus;
private TableColumn<Pet, String> colPetStatus;
@FXML
private TableView<?> tvPets;
private TableView<Pet> tvPets;
@FXML
private TextField txtSearch;
@FXML
void btnAddClicked(ActionEvent event) {
mode = "Add";
openDialog(null,mode);
}
@FXML
void btnDeleteClicked(ActionEvent event) {
int numRows = 0;
Pet selectedPet = tvPets.getSelectionModel().getSelectedItem();
//ask user to confirm
Alert question = new Alert(Alert.AlertType.CONFIRMATION);
question.setHeaderText("Please confirm delete");
question.setContentText("Are you sure you want to delete this pet?");
Optional<ButtonType> result = question.showAndWait(); //show alert and wait for response
//if confirmed,start deletion
if (result.isPresent() && result.get() == ButtonType.OK) {
int petId = selectedPet.getPetId();
//try deleting
try{
numRows = PetDB.deletePet(petId);
}
catch (SQLIntegrityConstraintViolationException e){
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Database Operation Error");
alert.setContentText("Delete failed\n" +
"the selected pet is being referred in another table");
alert.showAndWait();
return;
}
catch (SQLException e) {
throw new RuntimeException(e);
}
//prompt user of any errors
if (numRows == 0){
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Database Operation Error");
alert.setContentText("Delete failed");
alert.showAndWait();
}
else{
//prompt user of delete conformation
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setHeaderText("Database Operation Confirmed");
alert.setContentText("Delete successful");
alert.showAndWait();
//refresh display and reset inputs
displayPets();
btnDelete.setDisable(true);
btnEdit.setDisable(true);
txtSearch.setText("");
}
}
}
@FXML
void btnEditClicked(ActionEvent event) {
Pet selectedPet = tvPets.getSelectionModel().getSelectedItem();
if(selectedPet != null){
mode = "Edit";
openDialog(selectedPet,mode);
}
}
}
private ObservableList<Pet> data = FXCollections.observableArrayList();
String mode = null;
@FXML
void initialize() {
btnEdit.setDisable(true);
btnDelete.setDisable(true);
colPetId.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petId"));
colPetName.setCellValueFactory(new PropertyValueFactory<Pet,String>("petName"));
colPetSpecies.setCellValueFactory(new PropertyValueFactory<Pet,String>("petSpecies"));
colPetBreed.setCellValueFactory(new PropertyValueFactory<Pet,String>("petBreed"));
colPetAge.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petAge"));
colPetStatus.setCellValueFactory(new PropertyValueFactory<Pet,String>("petStatus"));
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice"));
displayPets();
tvPets.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
btnEdit.setDisable(false);
btnDelete.setDisable(false);
});
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
displayFilteredPet(newValue);
});
}
private void displayFilteredPet(String filter) {
data.clear();
try{
if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){
displayPets();
}
else {
data = PetDB.getFilteredPets(filter);
tvPets.setItems(data);
}
} catch (Exception e) {
System.out.println("Error while fetching table data: " + e.getMessage());
}
}
private void displayPets() {
data.clear();
try{
data = PetDB.getPets();
}
catch(SQLException e){
System.out.println("Error while fetching table data: " + e.getMessage());
}
tvPets.setItems(data);
}
private void openDialog(Pet pet, String mode){
//Get new view
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml"));
Scene scene = null;
try{
scene = new Scene(fxmlLoader.load());
} catch (IOException e) {
throw new RuntimeException(e);
}
PetDialogController dialogController = fxmlLoader.getController(); //controller associated with this view
dialogController.setMode(mode);
//Open the dialog depending on the mode
if(mode.equals("Edit")){
//Make it display pet details in dialog
dialogController.displayPetDetails(pet);
}
Stage dialogStage = new Stage();
dialogStage.initModality(Modality.APPLICATION_MODAL); //make it modal
if(mode.equals("Add")){
dialogStage.setTitle("Add Pet");
}
else {
dialogStage.setTitle("Edit Pet");
}
dialogStage.setScene(scene);
dialogStage.showAndWait();
//When dialog closes update table view and disable edit and delete buttons, and reset search bar
displayPets();
btnDelete.setDisable(true);
btnEdit.setDisable(true);
txtSearch.setText("");
}
}

View File

@@ -0,0 +1,203 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.example.petshopdesktop.DTOs.ProductDTO;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.database.PetDB;
import org.example.petshopdesktop.models.Category;
import org.example.petshopdesktop.models.Pet;
import java.sql.SQLException;
public class PetDialogController {
@FXML
private Button btnCancel;
@FXML
private Button btnSave;
@FXML
private ComboBox<String> cbPetStatus;
@FXML
private Label lblMode;
@FXML
private Label lblPetId;
@FXML
private TextField txtPetAge;
@FXML
private TextField txtPetBreed;
@FXML
private TextField txtPetName;
@FXML
private TextField txtPetPrice;
@FXML
private TextField txtPetSpecies;
private String mode = null;
private ObservableList<String> statusList = FXCollections.observableArrayList(
"Available", "Adopted"
);
@FXML
void initialize() {
cbPetStatus.setItems(statusList); //set status combobox
//Set up mouse handlers for buttons
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
buttonSaveClicked(mouseEvent);
}
});
btnCancel.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
closeStage(mouseEvent);
}
});
}
private void buttonSaveClicked(MouseEvent mouseEvent) {
int numRow = 0;
String errorMsg = "";
//Check validation (input required)
errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name");
errorMsg += Validator.isPresent(txtPetAge.getText(), "Age");
errorMsg += Validator.isPresent(txtPetBreed.getText(), "Breed");
errorMsg += Validator.isPresent(txtPetSpecies.getText(), "Species");
errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price");
if (cbPetStatus.getSelectionModel().getSelectedItem() == null){
errorMsg += "Status is required";
}
//Check validation (length size)
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
errorMsg += Validator.isLessThanVarChars(txtPetSpecies.getText(), "Species", 50);
errorMsg += Validator.isLessThanVarChars(txtPetBreed.getText(), "Breed", 50);
errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12);
errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11);
//Check validation (format)
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
errorMsg += Validator.isNonNegativeInteger(txtPetAge.getText(), "Age");
if(errorMsg.isEmpty()){
Pet pet = collectPet();
if(mode.equals("Add")) {
try{
numRow = PetDB.insertPet(pet);
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
else {
try {
numRow = PetDB.updatePet(pet.getPetId(), pet);
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
//if no rows were affected then there was an error (prompt user of error)
if (numRow == 0){
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Database Operation Error");
alert.setContentText(mode + " failed");
alert.showAndWait();
closeStage(mouseEvent);
}
else {
//tell the user operation was successful
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setHeaderText("Database Operation Confirmed");
alert.setContentText(mode + " succeeded");
alert.showAndWait();
closeStage(mouseEvent);
}
}
else{
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error");
alert.setContentText(errorMsg);
alert.showAndWait();
}
}
private Pet collectPet() {
int petId =0;
Pet pet = null;
if(lblPetId.isVisible()){
petId = Integer.parseInt(lblPetId.getText().split(": ")[1]);
}
pet = new Pet(
petId,
txtPetName.getText(),
txtPetSpecies.getText(),
txtPetBreed.getText(),
Integer.parseInt(txtPetAge.getText()),
cbPetStatus.getValue(),
Double.parseDouble(txtPetPrice.getText())
);
return pet;
}
private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow();
stage.close();
}
public void displayPetDetails(Pet pet){
if (pet!=null){
lblPetId.setText("ID: " + pet.getPetId());
txtPetName.setText(pet.getPetName());
txtPetSpecies.setText(pet.getPetSpecies());
txtPetBreed.setText(pet.getPetBreed());
txtPetAge.setText(pet.getPetAge() + "");
txtPetPrice.setText(pet.getPetPrice() + "");
//get the right combobox selection
for (String status : cbPetStatus.getItems()) {
if(status.equals(pet.getPetStatus())){
cbPetStatus.getSelectionModel().select(status);
}
}
}
}
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Pet");
if(mode.equals("Add")) {
lblPetId.setVisible(false);
}
else if(mode.equals("Edit")) {
lblPetId.setVisible(true);
}
}
}

View File

@@ -2,6 +2,9 @@ package org.example.petshopdesktop.database;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@@ -9,8 +12,7 @@ import java.util.Properties;
public class ConnectionDB {
/**
* Method to try and connect to the database sing cnnection.properties located in the
* root of C drive
* Method to try and connect to the database using connectionpetstore.properties.
* @return Connection to the database
*/
public static Connection getConnection(){
@@ -18,10 +20,23 @@ public class ConnectionDB {
String user = "";
String password = "";
try{
//Read connection.properties file
FileInputStream fis = new FileInputStream("c:\\connectionpetstore.properties"); //location of connection can be changed here
Properties prop = new Properties();
Properties prop = new Properties();
Path propsPath;
String explicitPath = System.getenv("PETSTORE_DB_PROPS");
if (explicitPath != null && !explicitPath.isBlank()) {
propsPath = Paths.get(explicitPath);
} else {
Path cwd = Paths.get(System.getProperty("user.dir"), "connectionpetstore.properties");
Path xdg = Paths.get(System.getProperty("user.home"), ".config", "petstore", "connectionpetstore.properties");
Path legacyWindows = Paths.get("./connectionpetstore.properties");
if (Files.exists(cwd)) propsPath = cwd;
else if (Files.exists(xdg)) propsPath = xdg;
else propsPath = legacyWindows;
}
try (FileInputStream fis = new FileInputStream(propsPath.toString())) {
prop.load(fis);
url = prop.getProperty("url");
user = prop.getProperty("user");
@@ -31,12 +46,8 @@ public class ConnectionDB {
throw new RuntimeException("Problem with reading connection info: "+e.getMessage());
}
Connection conn = null;
try{
//try to get connection with the data taken from connection.properties
conn = DriverManager.getConnection(url,user,password);
return conn;
return DriverManager.getConnection(url,user,password);
}
catch (SQLException e) {
throw new RuntimeException("Problem with database connection: "+e.getMessage());

View File

@@ -0,0 +1,145 @@
package org.example.petshopdesktop.database;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.example.petshopdesktop.models.Pet;
import java.sql.*;
public class PetDB {
public static ObservableList<Pet> getPets() throws SQLException {
//Connect to the database
ObservableList<Pet> pets = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
//Execute Query
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM pet");
//While there is still data add pets to the list
while(rs.next()){
Pet pet = new Pet(
rs.getInt(1),
rs.getString(2),
rs.getString(3),
rs.getString(4),
rs.getInt(5),
rs.getString(6),
rs.getDouble(7)
);
pets.add(pet);
}
//close connection and return pets
conn.close();
return pets;
}
public static ObservableList<Pet> getFilteredPets(String filter) throws SQLException {
//Connect to the database
ObservableList<Pet> pets = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
//Get SQL query for filtered word
String sql = "SELECT * FROM pet " +
" WHERE " +
"petName LIKE ? OR " +
"petSpecies LIKE ? OR " +
"petBreed LIKE ? OR " +
"petAge LIKE ? OR " +
"petStatus LIKE ? OR " +
"petPrice LIKE ?";
String filteredString = "%" + filter + "%";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, filteredString);
stmt.setString(2, filteredString);
stmt.setString(3, filteredString);
stmt.setString(4, filteredString);
stmt.setString(5, filteredString);
stmt.setString(6, filteredString);
ResultSet rs = stmt.executeQuery();
while(rs.next()){
Pet pet = new Pet(
rs.getInt(1),
rs.getString(2),
rs.getString(3),
rs.getString(4),
rs.getInt(5),
rs.getString(6),
rs.getDouble(7)
);
pets.add(pet);
}
conn.close();
return pets;
}
public static int insertPet(Pet pet) throws SQLException {
int numRows = 0;
Connection conn = ConnectionDB.getConnection();
String sql = "INSERT INTO pet (petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice)" +
" VALUES (?, ?, ?, ?, ?, ?, ?)";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setInt(1, pet.getPetId());
stmt.setString(2, pet.getPetName());
stmt.setString(3, pet.getPetSpecies());
stmt.setString(4, pet.getPetBreed());
stmt.setInt(5, pet.getPetAge());
stmt.setString(6, pet.getPetStatus());
stmt.setDouble(7, pet.getPetPrice());
numRows = stmt.executeUpdate();
conn.close();
return numRows;
}
public static int updatePet(int petId, Pet pet) throws SQLException {
int numRows = 0;
Connection conn = ConnectionDB.getConnection();
String sql = "UPDATE pet SET " +
" petName = ?, " +
" petSpecies = ?, " +
" petBreed = ?, " +
" petAge = ?, " +
" petStatus = ?, " +
" petPrice = ? " +
" WHERE petId = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, pet.getPetName());
stmt.setString(2, pet.getPetSpecies());
stmt.setString(3, pet.getPetBreed());
stmt.setInt(4, pet.getPetAge());
stmt.setString(5, pet.getPetStatus());
stmt.setDouble(6, pet.getPetPrice());
stmt.setInt(7, petId);
numRows = stmt.executeUpdate();
conn.close();
return numRows;
}
public static int deletePet(int petId) throws SQLException {
int numRows = 0;
Connection conn = ConnectionDB.getConnection();
String sql = "DELETE FROM pet WHERE petId = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setInt(1, petId);
numRows = stmt.executeUpdate();
conn.close();
return numRows;
}
}

View File

@@ -0,0 +1,80 @@
package org.example.petshopdesktop.database;
import org.example.petshopdesktop.auth.Role;
import org.example.petshopdesktop.models.User;
import java.sql.*;
/*
Petshop Desktop
Purpose: User authentication and role lookup against the users table.
*/
public class UserDB {
/**
* Authenticate a user by username and password.
* Passwords are stored as SHA-256 hex digests in the database.
*
* @param username the username to authenticate
* @param password the plaintext password
* @return the User if credentials are valid, or null if authentication fails
*/
public static User authenticate(String username, String password) throws SQLException {
String sql = "SELECT user_id, username, role FROM users " +
"WHERE username = ? AND password_hash = SHA2(?, 256)";
try (Connection conn = ConnectionDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
ps.setString(2, password);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
int userId = rs.getInt("user_id");
String uname = rs.getString("username");
// Role values are stored in the database as strings and normalised to match the enum.
// Table constraints limit role values, Role.valueOf is expected to be safe under normal operation.
Role role = Role.valueOf(rs.getString("role").toUpperCase());
return new User(userId, uname, role);
}
}
}
return null;
}
/**
* Create the users table and seed default admin/staff accounts if they do not exist.
* Passwords are stored as SHA2-256 hashes.
*/
public static void initializeTable() throws SQLException {
String createTable = """
CREATE TABLE IF NOT EXISTS users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash CHAR(64) NOT NULL,
role ENUM('ADMIN','STAFF') NOT NULL
)
""";
// Default accounts support initial development and testing, credentials should be rotated or removed for deployment.
String seedAdmin = """
INSERT IGNORE INTO users (username, password_hash, role)
VALUES ('admin', SHA2('admin123', 256), 'ADMIN')
""";
String seedStaff = """
INSERT IGNORE INTO users (username, password_hash, role)
VALUES ('staff', SHA2('staff123', 256), 'STAFF')
""";
try (Connection conn = ConnectionDB.getConnection();
Statement st = conn.createStatement()) {
st.executeUpdate(createTable);
st.executeUpdate(seedAdmin);
st.executeUpdate(seedStaff);
}
}
}

View File

@@ -0,0 +1,109 @@
package org.example.petshopdesktop.models;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
public class Pet {
private SimpleIntegerProperty petId;
private SimpleStringProperty petName;
private SimpleStringProperty petSpecies;
private SimpleStringProperty petBreed;
private SimpleIntegerProperty petAge;
private SimpleStringProperty petStatus;
private SimpleDoubleProperty petPrice;
public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice) {
this.petId = new SimpleIntegerProperty(petId);
this.petName = new SimpleStringProperty(petName);
this.petSpecies = new SimpleStringProperty(petSpecies);
this.petBreed = new SimpleStringProperty(petBreed);
this.petAge = new SimpleIntegerProperty(petAge);
this.petStatus = new SimpleStringProperty(petStatus);
this.petPrice = new SimpleDoubleProperty(petPrice);
}
public int getPetId() {
return petId.get();
}
public void setPetId(int petId) {
this.petId.set(petId);
}
public SimpleIntegerProperty petIdProperty() {
return petId;
}
public String getPetName() {
return petName.get();
}
public void setPetName(String petName) {
this.petName.set(petName);
}
public SimpleStringProperty petNameProperty() {
return petName;
}
public String getPetSpecies() {
return petSpecies.get();
}
public void setPetSpecies(String petSpecies) {
this.petSpecies.set(petSpecies);
}
public SimpleStringProperty petSpeciesProperty() {
return petSpecies;
}
public String getPetBreed() {
return petBreed.get();
}
public void setPetBreed(String petBreed) {
this.petBreed.set(petBreed);
}
public SimpleStringProperty petBreedProperty() {
return petBreed;
}
public int getPetAge() {
return petAge.get();
}
public void setPetAge(int petAge) {
this.petAge.set(petAge);
}
public SimpleIntegerProperty petAgeProperty() {
return petAge;
}
public String getPetStatus() {
return petStatus.get();
}
public void setPetStatus(String petStatus) {
this.petStatus.set(petStatus);
}
public SimpleStringProperty petStatusProperty() {
return petStatus;
}
public double getPetPrice() {
return petPrice.get();
}
public void setPetPrice(double petPrice) {
this.petPrice.set(petPrice);
}
public SimpleDoubleProperty petPriceProperty() {
return petPrice;
}
}

View File

@@ -0,0 +1,27 @@
package org.example.petshopdesktop.models;
import org.example.petshopdesktop.auth.Role;
public class User {
private int userId;
private String username;
private Role role;
public User(int userId, String username, Role role) {
this.userId = userId;
this.username = username;
this.role = role;
}
public int getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public Role getRole() {
return role;
}
}

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" prefHeight="400.0" prefWidth="380.0" spacing="16.0"
style="-fx-background-color: #2C3E50;"
xmlns="http://javafx.com/javafx/25"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="org.example.petshopdesktop.controllers.LoginController">
<padding>
<Insets bottom="40.0" left="50.0" right="50.0" top="40.0" />
</padding>
<children>
<Label text="🐾 Pet Shop Manager" textFill="WHITE">
<font>
<Font name="Comic Sans MS Bold" size="22.0" />
</font>
<VBox.margin>
<Insets bottom="10.0" />
</VBox.margin>
</Label>
<Label text="Username" textFill="#cccccc">
<font>
<Font name="System Bold" size="13.0" />
</font>
</Label>
<TextField fx:id="txtUsername" promptText="Enter username"
style="-fx-background-color: #3d5166; -fx-text-fill: white; -fx-prompt-text-fill: #888; -fx-background-radius: 8; -fx-border-width: 0;">
<padding>
<Insets bottom="10.0" left="12.0" right="12.0" top="10.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</TextField>
<Label text="Password" textFill="#cccccc">
<font>
<Font name="System Bold" size="13.0" />
</font>
</Label>
<PasswordField fx:id="txtPassword" promptText="Enter password"
style="-fx-background-color: #3d5166; -fx-text-fill: white; -fx-prompt-text-fill: #888; -fx-background-radius: 8; -fx-border-width: 0;">
<padding>
<Insets bottom="10.0" left="12.0" right="12.0" top="10.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</PasswordField>
<Label fx:id="lblError" text="" textFill="#FF6B6B" wrapText="true">
<font>
<Font size="13.0" />
</font>
</Label>
<Button fx:id="btnLogin" mnemonicParsing="false" onAction="#btnLoginClicked"
prefWidth="280.0"
style="-fx-background-color: #FF6B6B; -fx-background-radius: 8; -fx-cursor: hand;"
text="Login" textFill="WHITE">
<font>
<Font name="System Bold" size="15.0" />
</font>
<padding>
<Insets bottom="12.0" left="12.0" right="12.0" top="12.0" />
</padding>
<VBox.margin>
<Insets top="8.0" />
</VBox.margin>
</Button>
</children>
</VBox>

View File

@@ -16,12 +16,12 @@
<Insets bottom="30.0" left="30.0" right="30.0" top="30.0" />
</padding>
<children>
<Label text="Name" textFill="WHITE">
<Label fx:id="lblUsername" text="Name" textFill="WHITE">
<font>
<Font name="Comic Sans MS Bold" size="30.0" />
</font>
</Label>
<Label text="Pet Store Manager" textFill="#ffe66d">
<Label fx:id="lblRole" text="Pet Store Manager" textFill="#ffe66d">
<font>
<Font name="Comic Sans MS Bold" size="16.0" />
</font>
@@ -106,6 +106,17 @@
<Insets bottom="12.0" left="12.0" right="45.0" top="12.0" />
</padding>
</Button>
<Button fx:id="btnLogout" mnemonicParsing="false" onAction="#btnLogoutClicked" prefWidth="250.0" style="-fx-background-color: #34495E; -fx-background-radius: 8; -fx-cursor: hand;" text="🚪 Logout" textFill="#cccccc">
<font>
<Font name="Comic Sans MS Bold" size="14.0" />
</font>
<padding>
<Insets bottom="12.0" left="12.0" right="94.0" top="12.0" />
</padding>
<VBox.margin>
<Insets top="20.0" />
</VBox.margin>
</Button>
</children>
</VBox>
</left>