From a12af3f0ee9dee7c041883e9f3a21dd1d321a285 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sat, 21 Feb 2026 13:27:50 -0700 Subject: [PATCH 1/2] Implement RBAC with login screen for admin and staff roles - Add Role enum (ADMIN, STAFF) and UserSession singleton - Add User model and UserDB with SHA2-256 authentication - Add login screen (login-view.fxml + LoginController) - Update main-layout-view.fxml: dynamic username/role labels, logout button - Update MainLayoutController: hide Inventory/Suppliers/Product-Suppliers for STAFF, show username/role, handle logout - Update PetShopApplication to start with login screen and initialize users table - Update module-info.java to open/export auth package --- src/main/java/module-info.java | 2 + .../petshopdesktop/PetShopApplication.java | 10 ++- .../org/example/petshopdesktop/auth/Role.java | 6 ++ .../petshopdesktop/auth/UserSession.java | 44 ++++++++++ .../controllers/LoginController.java | 67 ++++++++++++++++ .../controllers/MainLayoutController.java | 45 +++++++++++ .../petshopdesktop/database/UserDB.java | 71 ++++++++++++++++ .../example/petshopdesktop/models/User.java | 27 +++++++ .../example/petshopdesktop/login-view.fxml | 80 +++++++++++++++++++ .../petshopdesktop/main-layout-view.fxml | 15 +++- 10 files changed, 363 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/example/petshopdesktop/auth/Role.java create mode 100644 src/main/java/org/example/petshopdesktop/auth/UserSession.java create mode 100644 src/main/java/org/example/petshopdesktop/controllers/LoginController.java create mode 100644 src/main/java/org/example/petshopdesktop/database/UserDB.java create mode 100644 src/main/java/org/example/petshopdesktop/models/User.java create mode 100644 src/main/resources/org/example/petshopdesktop/login-view.fxml diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 7459e876..d1f07dd9 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -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; } \ No newline at end of file diff --git a/src/main/java/org/example/petshopdesktop/PetShopApplication.java b/src/main/java/org/example/petshopdesktop/PetShopApplication.java index 8a8fcd65..06def37b 100644 --- a/src/main/java/org/example/petshopdesktop/PetShopApplication.java +++ b/src/main/java/org/example/petshopdesktop/PetShopApplication.java @@ -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(); } diff --git a/src/main/java/org/example/petshopdesktop/auth/Role.java b/src/main/java/org/example/petshopdesktop/auth/Role.java new file mode 100644 index 00000000..64f38459 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/auth/Role.java @@ -0,0 +1,6 @@ +package org.example.petshopdesktop.auth; + +public enum Role { + ADMIN, + STAFF +} diff --git a/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/src/main/java/org/example/petshopdesktop/auth/UserSession.java new file mode 100644 index 00000000..748c9546 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/auth/UserSession.java @@ -0,0 +1,44 @@ +package org.example.petshopdesktop.auth; + +public class UserSession { + + private static UserSession instance; + + private String username; + private Role role; + + private UserSession() {} + + public static UserSession getInstance() { + if (instance == null) { + instance = new UserSession(); + } + return instance; + } + + public void login(String username, Role role) { + this.username = username; + this.role = role; + } + + public void logout() { + this.username = null; + this.role = null; + } + + public String getUsername() { + return username; + } + + public Role getRole() { + return role; + } + + public boolean isAdmin() { + return Role.ADMIN.equals(role); + } + + public boolean isLoggedIn() { + return username != null && role != null; + } +} diff --git a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java new file mode 100644 index 00000000..43bb3334 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java @@ -0,0 +1,67 @@ +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; + +public class LoginController { + + @FXML + private TextField txtUsername; + + @FXML + private PasswordField txtPassword; + + @FXML + private Label lblError; + + @FXML + void btnLoginClicked(ActionEvent event) { + String username = txtUsername.getText().trim(); + String password = txtPassword.getText(); + + if (username.isEmpty() || password.isEmpty()) { + lblError.setText("Please enter username and password."); + return; + } + + try { + User user = UserDB.authenticate(username, password); + if (user == null) { + lblError.setText("Invalid username or password."); + txtPassword.clear(); + return; + } + + UserSession.getInstance().login(user.getUsername(), user.getRole()); + openMainLayout(); + + } catch (SQLException e) { + lblError.setText("Database error: " + e.getMessage()); + } + } + + private void openMainLayout() { + try { + 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(); + } + } +} diff --git a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index 431d38d4..3584a0cc 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -4,8 +4,12 @@ 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; public class MainLayoutController { @@ -18,6 +22,9 @@ public class MainLayoutController { @FXML private Button btnInventory; + @FXML + private Button btnLogout; + @FXML private Button btnPets; @@ -36,6 +43,12 @@ public class MainLayoutController { @FXML private Button btnSuppliers; + @FXML + private Label lblUsername; + + @FXML + private Label lblRole; + @FXML private StackPane spContentArea; @@ -93,11 +106,43 @@ public class MainLayoutController { updateButtons(btnSuppliers); } + @FXML + void btnLogoutClicked(ActionEvent event) { + 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() { + applyRBAC(); loadView("pet-view.fxml"); } + private void applyRBAC() { + UserSession session = UserSession.getInstance(); + + lblUsername.setText(session.getUsername()); + lblRole.setText(session.getRole().toString()); + + boolean isAdmin = session.isAdmin(); + btnInventory.setVisible(isAdmin); + btnInventory.setManaged(isAdmin); + btnSuppliers.setVisible(isAdmin); + btnSuppliers.setManaged(isAdmin); + btnProductSuppliers.setVisible(isAdmin); + btnProductSuppliers.setManaged(isAdmin); + } + /** * Load a view when a button is clicked on the navigation * @param fxmlFile the fxmlFile name to be loaded diff --git a/src/main/java/org/example/petshopdesktop/database/UserDB.java b/src/main/java/org/example/petshopdesktop/database/UserDB.java new file mode 100644 index 00000000..8909d211 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/database/UserDB.java @@ -0,0 +1,71 @@ +package org.example.petshopdesktop.database; + +import org.example.petshopdesktop.auth.Role; +import org.example.petshopdesktop.models.User; + +import java.sql.*; + +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 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 + ) + """; + + 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); + } + } +} diff --git a/src/main/java/org/example/petshopdesktop/models/User.java b/src/main/java/org/example/petshopdesktop/models/User.java new file mode 100644 index 00000000..02089be4 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/models/User.java @@ -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; + } +} diff --git a/src/main/resources/org/example/petshopdesktop/login-view.fxml b/src/main/resources/org/example/petshopdesktop/login-view.fxml new file mode 100644 index 00000000..65199c95 --- /dev/null +++ b/src/main/resources/org/example/petshopdesktop/login-view.fxml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml index 9579dff6..983d07a9 100644 --- a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml +++ b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -16,12 +16,12 @@ - From bbf06456f5b12c4a35b02c88681116e0957fd002 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 22 Feb 2026 17:39:42 -0700 Subject: [PATCH 2/2] comments --- .../org/example/petshopdesktop/auth/Role.java | 7 +++++++ .../example/petshopdesktop/auth/UserSession.java | 15 +++++++++++++++ .../controllers/LoginController.java | 9 +++++++++ .../controllers/MainLayoutController.java | 13 +++++++++++++ .../example/petshopdesktop/database/UserDB.java | 9 +++++++++ 5 files changed, 53 insertions(+) diff --git a/src/main/java/org/example/petshopdesktop/auth/Role.java b/src/main/java/org/example/petshopdesktop/auth/Role.java index 64f38459..e631ddf7 100644 --- a/src/main/java/org/example/petshopdesktop/auth/Role.java +++ b/src/main/java/org/example/petshopdesktop/auth/Role.java @@ -1,6 +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 } diff --git a/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/src/main/java/org/example/petshopdesktop/auth/UserSession.java index 748c9546..2cc4538a 100644 --- a/src/main/java/org/example/petshopdesktop/auth/UserSession.java +++ b/src/main/java/org/example/petshopdesktop/auth/UserSession.java @@ -1,14 +1,24 @@ 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(); @@ -16,11 +26,13 @@ public class 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; @@ -34,10 +46,13 @@ public class UserSession { 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; } diff --git a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java index 43bb3334..440c5686 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java @@ -14,6 +14,10 @@ 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 @@ -27,15 +31,18 @@ public class LoginController { @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."); @@ -43,6 +50,7 @@ public class LoginController { return; } + // Session state is stored in memory for use by controllers and UI RBAC. UserSession.getInstance().login(user.getUsername(), user.getRole()); openMainLayout(); @@ -53,6 +61,7 @@ public class LoginController { 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()); diff --git a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index 3584a0cc..a910432e 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -11,6 +11,10 @@ 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 @@ -108,6 +112,7 @@ public class MainLayoutController { @FXML void btnLogoutClicked(ActionEvent event) { + // Logout clears session state before returning to the login view. UserSession.getInstance().logout(); try { FXMLLoader loader = new FXMLLoader( @@ -124,16 +129,22 @@ public class MainLayoutController { @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); @@ -141,6 +152,8 @@ public class MainLayoutController { btnSuppliers.setManaged(isAdmin); btnProductSuppliers.setVisible(isAdmin); btnProductSuppliers.setManaged(isAdmin); + + // Privileged operations should still be enforced within the relevant controllers and database methods. } /** diff --git a/src/main/java/org/example/petshopdesktop/database/UserDB.java b/src/main/java/org/example/petshopdesktop/database/UserDB.java index 8909d211..3b5f78b4 100644 --- a/src/main/java/org/example/petshopdesktop/database/UserDB.java +++ b/src/main/java/org/example/petshopdesktop/database/UserDB.java @@ -5,6 +5,10 @@ import org.example.petshopdesktop.models.User; import java.sql.*; +/* +Petshop Desktop +Purpose: User authentication and role lookup against the users table. +*/ public class UserDB { /** @@ -29,7 +33,11 @@ public class UserDB { 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); } } @@ -51,6 +59,7 @@ public class UserDB { ) """; + // 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')