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
This commit is contained in:
2026-02-21 13:27:50 -07:00
parent fb16da91e2
commit a12af3f0ee
10 changed files with 363 additions and 4 deletions

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,6 @@
package org.example.petshopdesktop.auth;
public enum Role {
ADMIN,
STAFF
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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

View File

@@ -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);
}
}
}

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>