added login finally

This commit is contained in:
Ksan 2026-04-01 03:11:23 +02:00
parent 80ca3240f1
commit a659e1217c
15 changed files with 670 additions and 25 deletions

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -3,6 +3,7 @@ package dev.ksan.CASystem;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@ -18,7 +19,7 @@ public class KeystoreService {
private static String KEYSTORE_TYPE = "PKCS12";
private static String KEY_ALIAS = "key";
private static final Path USER_KEYSORE_DIR = Path.of("keystores/users");
private static final Path USER_KEYSTORE_DIR = Path.of("keystores/users");
private static final Path CA_KEYSTORE_DIR = Path.of("keystores/ca");
@ -35,7 +36,7 @@ public class KeystoreService {
keyStore.setKeyEntry(KEY_ALIAS, privateKey, keystorePassword, chain);
Path path = USER_KEYSORE_DIR.resolve(username + ".p12");
Path path = USER_KEYSTORE_DIR.resolve(username + ".p12");
Files.createDirectories(path.getParent());
@ -46,14 +47,14 @@ public class KeystoreService {
}
public static PrivateKey getPrivateKey(String username, char[] keystorePassword) throws Exception {
KeyStore keystore = loadKeyStore(USER_KEYSORE_DIR.resolve(username+".p12"), keystorePassword);
KeyStore keystore = loadKeyStore(USER_KEYSTORE_DIR.resolve(username+".p12"), keystorePassword);
return (PrivateKey) keystore.getKey(KEY_ALIAS, keystorePassword);
}
public static X509Certificate getCertificate(String username, char[] keystorePassword) throws Exception {
KeyStore keyStore = loadKeyStore(USER_KEYSORE_DIR.resolve(username+".p12"), keystorePassword);
KeyStore keyStore = loadKeyStore(USER_KEYSTORE_DIR.resolve(username+".p12"), keystorePassword);
return (X509Certificate) keyStore.getCertificate(KEY_ALIAS);
}
@ -122,6 +123,7 @@ public class KeystoreService {
KeyStore ks = loadKeystore(CA_KEYSTORE_DIR.resolve(caName + ".p12"), caPassword);
return (PrivateKey) ks.getKey(KEY_ALIAS, caPassword);
}
private static KeyStore loadKeystore(Path path, char[] password) throws Exception {
KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
try (InputStream is = Files.newInputStream(path)) {
@ -129,4 +131,5 @@ public class KeystoreService {
}
return ks;
}
}

View File

@ -108,6 +108,7 @@ public class LoginService {
user.setFailedLoginAttempts(user.getFailedLoginAttempts() + 1);
System.out.println("Attempt failed: " + user.getFailedLoginAttempts());
if (user.getFailedLoginAttempts() >= 3) {
user.setRevoked(true);

View File

@ -17,7 +17,6 @@ public abstract class User implements Serializable {
public User(){
//generate key
}
public PublicKey getPublicKey() {

View File

@ -136,20 +136,35 @@ public class UserRepository {
StandardCopyOption.REPLACE_EXISTING);
}
//this was annoying i dont wannt look at java.nio.channels.ClosedChannelException again
private static Object deserialize(Path path) throws Exception {
if (!Files.exists(path)) return null;
try (FileInputStream fis = new FileInputStream(path.toFile());
FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true); // shared read lock
ObjectInputStream ois = new ObjectInputStream(fis)) {
FileInputStream fis = new FileInputStream(path.toFile());
FileLock fileLock = null;
try {
fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true);
if (fileLock == null) {
throw new IllegalStateException("Could not acquire read lock on: " + path);
}
return ois.readObject();
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close(); // close stream AFTER reading
return obj;
} finally {
if (fileLock != null && fileLock.isValid()) {
fileLock.release(); // release FIRST
}
fis.close(); // then close channel
}
}
private static <T extends User> List<T> findAll(
Path dir, Class<T> type, String lockPrefix) throws Exception {

View File

@ -9,7 +9,7 @@ public class Voter extends User {
private String lastName;
private String username;
public Voter(String name, String username, String lastName){
public Voter(String name, String lastName, String username){
super();
this.name = name;
this.username = username;

View File

@ -23,7 +23,7 @@ public class HomeController {
@FXML
public void initialize() {
loginButton.setOnAction(e -> System.out.println("Go to login"));
loginButton.setOnAction(e -> goToLogin());
registerButton.setOnAction(e -> goToRegister());
}
@ -47,4 +47,23 @@ public class HomeController {
}
}
private void goToLogin() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/LoginView.fxml"));
Scene oldScene = stage.getScene();
Scene scene = new Scene(loader.load(),
oldScene.getWidth(),
oldScene.getHeight()
);
LoginController controller = loader.getController();
controller.setStage(stage);
stage.setScene(scene);
stage.setTitle("Register");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

View File

@ -0,0 +1,313 @@
package dev.ksan.ui;
import dev.ksan.CASystem.CAType;
import dev.ksan.CASystem.KeystoreService;
import dev.ksan.CASystem.LoginService;
import dev.ksan.Users.Organizer;
import dev.ksan.Users.User;
import dev.ksan.Users.Voter;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Arrays;
public class LoginController {
@FXML private StackPane dropZone;
//currently everything is saved as .p12? so we also need to insert password here???? maybe??
// or change it so that the user just has the plain cert and we hand that in? and we get
// the private only key then after they login finally
// this is the password field for the p12 keystore
@FXML private PasswordField passwordField;
@FXML private Label dropZoneLabel;
@FXML private Label errorLabel;
@FXML private Button loginButton;
@FXML private VBox form1;
@FXML private VBox form2;
@FXML
private Button finishLoginButton;
@FXML
private TextField usernameField;
@FXML
private PasswordField userPasswordField;
private User certUser;
private Stage stage;
private File droppedFile;
private X509Certificate loadedCert;
private CAType detectedCAType;
public void setStage(Stage stage) {
this.stage = stage;
}
@FXML
public void initialize() {
showForm1();
dropZone.setStyle(
"-fx-border-color: #888;" +
"-fx-border-width: 2;" +
"-fx-border-radius: 8;" +
"-fx-background-radius: 8;" +
"-fx-background-color: #f9f9f9;"
);
errorLabel.setVisible(false);
loginButton.setDisable(true); // disabled until cert + password are provided
setupDropZone();
passwordField.textProperty().addListener((obs, oldVal, newVal) -> {
loginButton.setDisable(droppedFile == null || newVal.trim().isEmpty());
});
userPasswordField.textProperty().addListener((obs, oldVal, newVal) -> {
finishLoginButton.setDisable( newVal.trim().isEmpty());
});
}
private void setupDropZone() {
dropZone.setOnDragEntered(e -> {
if (e.getDragboard().hasFiles())
dropZone.setStyle(dropZone.getStyle() + "-fx-border-color: #4A90D9;");
});
dropZone.setOnDragExited(e -> {
dropZone.setStyle(dropZone.getStyle().replace("-fx-border-color: #4A90D9;", "-fx-border-color: #888;"));
});
dropZone.setOnDragOver(event -> {
if (event.getGestureSource() != dropZone && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
}
event.consume();
});
dropZone.setOnDragDropped(event -> {
Dragboard db = event.getDragboard();
boolean success = false;
if (db.hasFiles()) {
File file = db.getFiles().get(0);
if (file.getName().endsWith(".p12")) {
droppedFile = file;
dropZoneLabel.setText("📄 " + file.getName());
success = true;
showError("Certificate loaded. Enter your password and click Login.");
} else {
showError("Please drop a .p12 keystore file.");
}
}
event.setDropCompleted(success);
event.consume();
});
dropZone.setOnMouseClicked(e -> browseForKeystore());
}
private void browseForKeystore() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select your keystore (.p12)");
fileChooser.getExtensionFilters().add(
new FileChooser.ExtensionFilter("PKCS12 Keystore", "*.p12"));
File file = fileChooser.showOpenDialog(stage);
if (file != null) {
droppedFile = file;
dropZoneLabel.setText("📄 " + file.getName());
}
}
@FXML
public void onLoginClicked() {
clearError();
if (droppedFile == null) {
showError("Please provide your keystore (.p12) file.");
return;
}
String password = passwordField.getText();
if (password.isBlank()) {
showError("Please enter your password.");
return;
}
String identity = droppedFile.getName().replace(".p12", "");
loginButton.setDisable(true);
Task<User> loginTask = new Task<>() {
@Override
protected User call() throws Exception {
char[] ksPassword = KeystoreService.deriveKeystorePassword(password, identity);
KeyStore ks = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream(droppedFile)) {
ks.load(is, Arrays.copyOf(ksPassword, ksPassword.length));
} catch (Exception e) {
// TODO
// if this also counts as unsuccessful attempt then we need
// to add handle failed attempt here too
}
X509Certificate cert = (X509Certificate) ks.getCertificate("key");
if (cert == null)
throw new SecurityException("No certificate found in keystore.");
CAType caType = LoginService.validateCertificate(cert);
showForm2();
loadedCert = cert;
detectedCAType = caType;
return LoginService.validateCredentials(identity, password, cert, caType);
}
};
loginTask.setOnSucceeded(e -> {
User user = loginTask.getValue();
certUser=user;
});
loginTask.setOnFailed(e -> {
certUser=null;
Throwable ex = loginTask.getException();
showError(ex.getMessage());
loginButton.setDisable(false);
});
new Thread(loginTask).start();
}
@FXML
public void onFinalLoginClicked() {
clearError();
String password = userPasswordField.getText();
if (password.isBlank()) {
showError("Please enter your password.");
return;
}
finishLoginButton.setDisable(true);
Task<Boolean> loginTask = new Task<>() {
@Override
protected Boolean call() throws Exception {
return userLogin();
}
};
loginTask.setOnSucceeded(e -> {
navigateToUserUI(certUser);
});
loginTask.setOnFailed(e -> {
certUser = null;
loadedCert = null;
detectedCAType = null;
Throwable ex = loginTask.getException();
showError(ex.getMessage());
finishLoginButton.setDisable(true);
});
new Thread(loginTask).start();
}
private boolean userLogin() {
try{
User user = LoginService.validateCredentials(usernameField.getText().trim(),
userPasswordField.getText().trim(), loadedCert, detectedCAType);
return user == certUser;
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
// TODO maybe pass in the certificate and/or keypair?
private void navigateToUserUI(User user) {
try {
if (user instanceof Organizer organizer) {
FXMLLoader loader = new FXMLLoader(
getClass().getResource("/fxml/OrganizerView.fxml"));
Parent root = loader.load();
OrganizerController controller = loader.getController();
controller.setOrganizer(organizer);
controller.setStage(stage);
stage.setScene(new Scene(root));
} else if (user instanceof Voter voter) {
FXMLLoader loader = new FXMLLoader(
getClass().getResource("/fxml/VoterView.fxml"));
Parent root = loader.load();
VoterController controller = loader.getController();
controller.setVoter(voter);
controller.setStage(stage);
stage.setScene(new Scene(root));
}
} catch (Exception e) {
showError("Failed to load UI: " + e.getMessage());
loginButton.setDisable(false);
}
}
private void showError(String message) {
Platform.runLater(() -> {
errorLabel.setText(message);
errorLabel.setVisible(true);
});
}
private void clearError() {
errorLabel.setText("");
errorLabel.setVisible(false);
}
@FXML
private void showForm2() {
form1.setVisible(false);
form2.setVisible(true);
}
@FXML
private void showForm1() {
form1.setVisible(true);
form2.setVisible(false);
}
}

View File

@ -0,0 +1,18 @@
package dev.ksan.ui;
import dev.ksan.Users.Organizer;
import javafx.stage.Stage;
public class OrganizerController {
private Organizer organizer;
private Stage stage;
public void setOrganizer(Organizer organizer) {
this.organizer = organizer;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

View File

@ -1,5 +1,8 @@
package dev.ksan.ui;
import dev.ksan.CASystem.RegistrationService;
import dev.ksan.Users.Organizer;
import dev.ksan.Users.Voter;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.PasswordField;
@ -31,4 +34,69 @@ public class RegisterOrganizerController {
public void setStage(Stage stage) {
this.stage = stage;
}
@FXML
public void initialize() {
registerButton.setOnAction(event -> tryRegisterVoter());
}
private void tryRegisterVoter() {
try {
if (!checkAndHighlightFields()) return;
Organizer organizer = new Organizer(nameField.getText().trim(),idField.getText().trim());
RegistrationService.registerOrganizer(organizer, passwordField.getText());
}catch (Exception e) {
displayError("Username already Taken");
}
}
//TODO
private void loginUser(Voter voter) {
}
//TODO
private void displayError(String usernameAlreadyTaken) {
}
// return true if everything is filled
private boolean checkAndHighlightFields() {
if (nameField.getText().trim().isEmpty()) {
nameField.setStyle("-fx-text-fill: red");
return false;
}else{
nameField.setStyle("-fx-text-fill: white");
}
if (idField.getText().isEmpty()) {
idField.setStyle("-fx-text-fill: red");
return false;
}else{
idField.setStyle("-fx-text-fill: white");
}
if (passwordField.getText().isEmpty()) {
passwordField.setStyle("-fx-text-fill: red");
return false;
}else{
passwordField.setStyle("-fx-text-fill: white");
}
if (confirmPasswordField.getText().isEmpty() || !confirmPasswordField.getText().equals(passwordField.getText())) {
confirmPasswordField.setStyle("-fx-text-fill: red");
displayError("Passwords do not match");
return false;
}else{
confirmPasswordField.setStyle("-fx-text-fill: white");
}
return true;
}
}

View File

@ -1,5 +1,8 @@
package dev.ksan.ui;
import dev.ksan.CASystem.RegistrationService;
import dev.ksan.Users.UserRepository;
import dev.ksan.Users.Voter;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.PasswordField;
@ -38,6 +41,79 @@ public class RegisterVoterController {
}
@FXML
public void initialize() {
registerButton.setOnAction(event -> tryRegisterVoter());
}
private void tryRegisterVoter() {
System.out.println("Registering voter");
try {
if (!checkAndHighlightFields()) return;
Voter voter = new Voter(nameField.getText().trim(), surnameField.getText().trim(),
usernameField.getText().trim());
RegistrationService.registerVoter(voter, passwordField.getText());
loginUser(voter);
}catch (Exception e) {
displayError("Username already Taken");
}
}
//TODO
private void loginUser(Voter voter) {
System.out.println("registered: " + voter);
}
//TODO
private void displayError(String usernameAlreadyTaken) {
}
// return true if everything is filled
private boolean checkAndHighlightFields() {
if (nameField.getText().isEmpty()) {
nameField.setStyle("-fx-text-fill: red");
return false;
}else{
nameField.setStyle("-fx-text-fill: black");
}
if (surnameField.getText().isEmpty()) {
surnameField.setStyle("-fx-text-fill: red");
return false;
}else{
surnameField.setStyle("-fx-text-fill: black");
}
if (usernameField.getText().isEmpty()) {
usernameField.setStyle("-fx-text-fill: red");
return false;
}else{
usernameField.setStyle("-fx-text-fill: black");
}
if (passwordField.getText().isEmpty()) {
passwordField.setStyle("-fx-text-fill: red");
return false;
}else{
passwordField.setStyle("-fx-text-fill: black");
}
if (confirmPasswordField.getText().isEmpty() || !confirmPasswordField.getText().equals(passwordField.getText())) {
confirmPasswordField.setStyle("-fx-text-fill: red");
displayError("Passwords do not match");
return false;
}else{
confirmPasswordField.setStyle("-fx-text-fill: black");
}
if(UserRepository.voterExists(usernameField.getText().trim())){
displayError("Username already Taken");
return false;
}
return true;
}
}

View File

@ -0,0 +1,17 @@
package dev.ksan.ui;
import dev.ksan.Users.Voter;
import javafx.stage.Stage;
public class VoterController {
private Voter voter;
private Stage stage;
public void setVoter(Voter voter) {
this.voter = voter;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

View File

@ -1,14 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?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.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="fxml.LoginView"
prefHeight="400.0" prefWidth="600.0">
</AnchorPane>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.ksan.ui.LoginController">
<center>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" BorderPane.alignment="CENTER">
<children>
<StackPane fx:id="rootStack" prefHeight="150.0" prefWidth="200.0">
<children>
<VBox fx:id="form1" alignment="CENTER" prefHeight="200.0" prefWidth="100.0">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="E-Voting">
<VBox.margin>
<Insets bottom="75.0" />
</VBox.margin>
</Text>
<StackPane fx:id="dropZone" prefHeight="150.0" prefWidth="200.0">
<children>
<Label fx:id="dropZoneLabel" text="dropZoneLabel">
<padding>
<Insets bottom="50.0" left="50.0" right="50.0" top="50.0" />
</padding>
</Label>
</children>
<VBox.margin>
<Insets left="100.0" right="100.0" />
</VBox.margin>
</StackPane>
<PasswordField fx:id="passwordField">
<VBox.margin>
<Insets left="150.0" right="150.0" top="15.0" />
</VBox.margin>
</PasswordField>
<Label fx:id="errorLabel" text="ErrorLabel">
<VBox.margin>
<Insets bottom="4.0" top="4.0" />
</VBox.margin>
</Label>
<Button fx:id="loginButton" mnemonicParsing="false" onAction="#onLoginClicked" text="login" />
</children>
</VBox>
<VBox fx:id="form2" alignment="CENTER" prefHeight="200.0" prefWidth="100.0">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="E-Voting" />
<GridPane alignment="CENTER">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<VBox.margin>
<Insets left="100.0" right="100.0" />
</VBox.margin>
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Username:" textAlignment="CENTER" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Password:" GridPane.rowIndex="1" />
<TextField fx:id="usernameField" GridPane.columnIndex="1" />
<PasswordField fx:id="userPasswordField" GridPane.columnIndex="1" GridPane.rowIndex="1" />
</children>
</GridPane>
<Button fx:id="finishLoginButton" mnemonicParsing="false" onAction="#onFinalLoginClicked" text="login">
<VBox.margin>
<Insets top="50.0" />
</VBox.margin>
</Button>
</children>
</VBox>
</children>
</StackPane>
</children>
</VBox>
</center>
</BorderPane>

View File

@ -12,7 +12,6 @@
<?import javafx.scene.text.Font?>
<?import javafx.scene.text.Text?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.ksan.ui.RegisterVoterController">
<center>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" BorderPane.alignment="CENTER">
@ -83,7 +82,7 @@
<Insets right="100.0" />
</GridPane.margin>
</PasswordField>
<PasswordField fx:id="passwordConfirmField" GridPane.columnIndex="1" GridPane.rowIndex="4">
<PasswordField fx:id="confirmPasswordField" GridPane.columnIndex="1" GridPane.rowIndex="4">
<GridPane.margin>
<Insets right="100.0" />
</GridPane.margin>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.gluonhq.charm.glisten.control.Avatar?>
<?import com.gluonhq.charm.glisten.control.DropdownButton?>
<?import com.gluonhq.charm.glisten.control.ExpansionPanel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
<left>
<VBox prefHeight="200.0" prefWidth="100.0" BorderPane.alignment="CENTER">
<children>
<HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0">
<children>
<Avatar>
<HBox.margin>
<Insets left="5.0" right="5.0" />
</HBox.margin>
</Avatar>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
</children>
</HBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0">
<children>
<DropdownButton>
<items>
<MenuItem text="Choice 1" />
<MenuItem text="Choice 2" />
<MenuItem text="Choice 3" />
</items>
</DropdownButton>
<ExpansionPanel />
</children>
</VBox>
</children>
</VBox>
</left>
</BorderPane>