Add Local Account Deletion #108

Merged
kske merged 3 commits from f/account-deletion into develop 2020-10-31 16:57:10 +01:00
14 changed files with 221 additions and 55 deletions
Showing only changes of commit f67ca1d61d - Show all commits

View File

@ -22,20 +22,21 @@ import envoy.client.net.WriteProxy;
*/ */
public class Chat implements Serializable { public class Chat implements Serializable {
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected final Contact recipient;
protected boolean disabled; protected boolean disabled;
protected boolean underlyingContactDeleted;
/** /**
* Stores the last time an {@link envoy.event.IsTyping} event has been sent. * Stores the last time an {@link envoy.event.IsTyping} event has been sent.
*/ */
protected transient long lastWritingEvent; protected transient long lastWritingEvent;
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected int unreadAmount; protected int unreadAmount;
protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty(); protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty();
protected final Contact recipient;
private static final long serialVersionUID = 2L; private static final long serialVersionUID = 2L;
/** /**

View File

@ -248,6 +248,10 @@ public final class LocalDB implements EventListener {
*/ */
@Event(eventType = EnvoyCloseEvent.class, priority = 500) @Event(eventType = EnvoyCloseEvent.class, priority = 500)
private synchronized void save() { private synchronized void save() {
// Stop saving if this account has been deleted
if (userFile == null)
return;
EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database..."); EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database...");
// Save users // Save users
@ -273,6 +277,20 @@ public final class LocalDB implements EventListener {
} }
} }
/**
* Deletes any local remnant of this user.
*
* @since Envoy Client v0.3-beta
*/
public void delete() {
if (lastLoginFile != null)
lastLoginFile.delete();
userFile.delete();
users.remove(user.getName());
userFile = null;
onLogout();
}
@Event(priority = 500) @Event(priority = 500)
private void onMessage(Message msg) { private void onMessage(Message msg) {
if (msg.getStatus() == MessageStatus.SENT) if (msg.getStatus() == MessageStatus.SENT)
@ -404,6 +422,17 @@ public final class LocalDB implements EventListener {
getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true)); getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true));
} }
@Event(priority = 500)
private void onAccountDeletion(AccountDeletion deletion) {
if (user.getID() == deletion.get())
logger.log(Level.WARNING,
"I have been informed by the server that I have been deleted without even knowing it...");
getChat(deletion.get()).ifPresent(chat -> {
chat.setDisabled(true);
chat.setUnderlyingContactDeleted(true);
});
}
/** /**
* @return a {@code Map<String, User>} of all users stored locally with their user names as keys * @return a {@code Map<String, User>} of all users stored locally with their user names as keys
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha

View File

@ -6,7 +6,7 @@ import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.Map;
import java.util.logging.*; import java.util.logging.*;
import javafx.animation.RotateTransition; import javafx.animation.RotateTransition;
@ -25,9 +25,8 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration; import javafx.util.Duration;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event; import dev.kske.eventbus.Event;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.EventListener;
import envoy.data.*; import envoy.data.*;
import envoy.data.Attachment.AttachmentType; import envoy.data.Attachment.AttachmentType;
@ -882,24 +881,22 @@ public final class ChatScene implements EventListener, Restorable, KeyboardMappi
@Override @Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() { public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
final var map = new HashMap<KeyCombination, Runnable>(); return Map.<KeyCombination, Runnable>of(
// Delete text before the caret with "Control" + U // Delete text before the caret with "Control" + U
map.put(new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN), () -> { new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN), () -> {
messageTextArea messageTextArea
.setText(messageTextArea.getText().substring(messageTextArea.getCaretPosition())); .setText(
messageTextArea.getText().substring(messageTextArea.getCaretPosition()));
checkPostConditions(false); checkPostConditions(false);
});
// Delete text after the caret with "Control" + K // Delete text after the caret with "Control" + K
map.put(new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN), () -> { }, new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN), () -> {
messageTextArea messageTextArea
.setText( .setText(
messageTextArea.getText().substring(0, messageTextArea.getCaretPosition())); messageTextArea.getText().substring(0, messageTextArea.getCaretPosition()));
checkPostConditions(false); checkPostConditions(false);
messageTextArea.positionCaret(messageTextArea.getText().length()); messageTextArea.positionCaret(messageTextArea.getText().length());
}); });
return map;
} }
} }

View File

@ -14,7 +14,7 @@ import dev.kske.eventbus.*;
import envoy.data.*; import envoy.data.*;
import envoy.event.GroupCreation; import envoy.event.GroupCreation;
import envoy.event.contact.UserOperation; import envoy.event.contact.*;
import envoy.util.Bounds; import envoy.util.Bounds;
import envoy.client.data.*; import envoy.client.data.*;
@ -252,4 +252,10 @@ public class GroupCreationTab implements EventListener {
} }
}); });
} }
@Event
private void onAccountDeletion(AccountDeletion deletion) {
final var deletedID = deletion.get();
Platform.runLater(() -> userList.getItems().removeIf(user -> (user.getID() == deletedID)));
}
} }

View File

@ -20,7 +20,7 @@ import envoy.event.*;
import envoy.util.*; import envoy.util.*;
import envoy.client.ui.control.ProfilePicImageView; import envoy.client.ui.control.ProfilePicImageView;
import envoy.client.util.IconUtil; import envoy.client.util.*;
/** /**
* @author Leon Hofmeister * @author Leon Hofmeister
@ -38,6 +38,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
private final PasswordField newPasswordField = new PasswordField(); private final PasswordField newPasswordField = new PasswordField();
private final PasswordField repeatNewPasswordField = new PasswordField(); private final PasswordField repeatNewPasswordField = new PasswordField();
private final Button saveButton = new Button("Save"); private final Button saveButton = new Button("Save");
private final Button deleteAccountButton = new Button("Delete Account");
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class); private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class);
@ -112,15 +113,18 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
final PasswordField[] passwordFields = final PasswordField[] passwordFields =
{ currentPasswordField, newPasswordField, repeatNewPasswordField }; { currentPasswordField, newPasswordField, repeatNewPasswordField };
final EventHandler<? super InputEvent> passwordEntered = e -> { final EventHandler<? super InputEvent> passwordEntered =
e -> {
newPassword = newPassword =
newPasswordField.getText(); newPasswordField
validPassword = newPassword .getText();
.equals( validPassword =
newPassword.equals(
repeatNewPasswordField repeatNewPasswordField
.getText()) .getText())
&& !newPasswordField && !newPasswordField
.getText().isBlank(); .getText()
.isBlank();
}; };
newPasswordField.setOnInputMethodTextChanged(passwordEntered); newPasswordField.setOnInputMethodTextChanged(passwordEntered);
newPasswordField.setOnKeyTyped(passwordEntered); newPasswordField.setOnKeyTyped(passwordEntered);
@ -140,6 +144,11 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
.setOnAction(e -> save(currentPasswordField.getText())); .setOnAction(e -> save(currentPasswordField.getText()));
saveButton.setAlignment(Pos.BOTTOM_RIGHT); saveButton.setAlignment(Pos.BOTTOM_RIGHT);
getChildren().add(saveButton); getChildren().add(saveButton);
// Displaying the delete account button
deleteAccountButton.setAlignment(Pos.BASELINE_CENTER);
deleteAccountButton.setOnAction(e -> UserUtil.deleteAccount());
getChildren().add(deleteAccountButton);
} }
/** /**

View File

@ -2,7 +2,7 @@ package envoy.client.util;
import java.util.logging.*; import java.util.logging.*;
import javafx.scene.control.Alert; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import dev.kske.eventbus.EventBus; import dev.kske.eventbus.EventBus;
@ -10,7 +10,7 @@ import dev.kske.eventbus.EventBus;
import envoy.data.*; import envoy.data.*;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import envoy.event.*; import envoy.event.*;
import envoy.event.contact.UserOperation; import envoy.event.contact.*;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.Context; import envoy.client.data.Context;
@ -121,4 +121,40 @@ public final class UserUtil {
}); });
} }
} }
/**
* Deletes anything pointing to this user, independent of client or server. Will do nothing if
* the client is currently offline.
*
* @since Envoy Client v0.3-beta
*/
public static void deleteAccount() {
if (!context.getClient().isOnline())
return;
else {
// Show the first wall of defense, if not disabled by the user
final var outerAlert = new Alert(AlertType.CONFIRMATION);
outerAlert.setContentText(
"Are you sure you want to delete your account entirely? This action can seriously not be undone.");
outerAlert.setTitle("Delete Account?");
AlertHelper.confirmAction(outerAlert, () -> {
// Show the final wall of defense in every case
final var lastAlert = new Alert(AlertType.WARNING,
"Do you REALLY want to delete your account? Last Warning. Proceed?",
ButtonType.CANCEL, ButtonType.OK);
lastAlert.setTitle("Delete Account?");
lastAlert.showAndWait().filter(ButtonType.OK::equals).ifPresent(b2 -> {
// Delete the account
context.getClient()
.send(new AccountDeletion(context.getLocalDB().getUser().getID()));
context.getLocalDB().delete();
logger.log(Level.INFO, "The user just deleted his account. Goodbye.");
ShutdownHelper.exit(true);
});
});
}
}
} }

View File

@ -0,0 +1,22 @@
package envoy.event.contact;
import envoy.event.Event;
/**
* Signifies the deletion of an account.
*
* @author Leon Hofmeister
* @since Envoy Common v0.3-beta
*/
public class AccountDeletion extends Event<Long> {
private static final long serialVersionUID = 1L;
/**
* @param value the ID of the contact that was deleted
* @since Envoy Common v0.3-beta
*/
public AccountDeletion(Long value) {
super(value);
}
}

View File

@ -59,7 +59,8 @@ public final class Startup {
new NameChangeProcessor(), new NameChangeProcessor(),
new ProfilePicChangeProcessor(), new ProfilePicChangeProcessor(),
new PasswordChangeRequestProcessor(), new PasswordChangeRequestProcessor(),
new IssueProposalProcessor()))); new IssueProposalProcessor(),
new AccountDeletionProcessor())));
// Initialize the current message ID // Initialize the current message ID
final var persistenceManager = PersistenceManager.getInstance(); final var persistenceManager = PersistenceManager.getInstance();

View File

@ -27,6 +27,7 @@ import envoy.data.Message.MessageStatus;
@Entity @Entity
@Table(name = "messages") @Table(name = "messages")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@NamedQueries({
@NamedQuery(name = Message.getPending, query = "SELECT m FROM Message m WHERE " @NamedQuery(name = Message.getPending, query = "SELECT m FROM Message m WHERE "
// Send to or by the user before last seen // Send to or by the user before last seen
+ "(m.sender = :user OR m.recipient = :user) AND m.creationDate > :lastSeen " + "(m.sender = :user OR m.recipient = :user) AND m.creationDate > :lastSeen "
@ -34,7 +35,10 @@ import envoy.data.Message.MessageStatus;
+ "OR m.recipient = :user AND m.status = envoy.data.Message$MessageStatus.SENT " + "OR m.recipient = :user AND m.status = envoy.data.Message$MessageStatus.SENT "
// Sent by the user and RECEIVED / READ after last seen // Sent by the user and RECEIVED / READ after last seen
+ "OR m.sender = :user AND (m.status = envoy.data.Message$MessageStatus.RECEIVED AND m.receivedDate > :lastSeen " + "OR m.sender = :user AND (m.status = envoy.data.Message$MessageStatus.RECEIVED AND m.receivedDate > :lastSeen "
+ "OR m.status = envoy.data.Message$MessageStatus.READ AND m.readDate > :lastSeen)") + "OR m.status = envoy.data.Message$MessageStatus.READ AND m.readDate > :lastSeen)"),
@NamedQuery(name = Message.deleteByRecipient, query = "DELETE FROM Message m WHERE m.recipient = :deleted OR m.sender = :deleted")
})
public class Message { public class Message {
/** /**
@ -45,6 +49,13 @@ public class Message {
*/ */
public static final String getPending = "Message.getPending"; public static final String getPending = "Message.getPending";
/**
* Named query deleting all messages of a user (parameter {@code :deleted}).
*
* @since Envoy Server v0.3-beta
*/
public static final String deleteByRecipient = "Message.deleteByRecipient";
@Id @Id
protected long id; protected long id;

View File

@ -123,6 +123,10 @@ public final class PersistenceManager {
// Remove this contact from the contact list of his contacts // Remove this contact from the contact list of his contacts
for (final var remainingContact : contact.getContacts()) for (final var remainingContact : contact.getContacts())
remainingContact.getContacts().remove(contact); remainingContact.getContacts().remove(contact);
entityManager
.createNamedQuery(Message.deleteByRecipient).setParameter("deleted", contact)
.executeUpdate();
}); });
remove(contact); remove(contact);
} }

View File

@ -49,8 +49,10 @@ public final class ConnectionManager implements ISocketIdListener {
// Notify contacts of this users offline-going // Notify contacts of this users offline-going
final envoy.server.data.User user = final envoy.server.data.User user =
PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID)); PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
if (user != null) {
user.setLastSeen(Instant.now()); user.setLastSeen(Instant.now());
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE); UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE);
}
// Remove the socket // Remove the socket
sockets.entrySet().removeIf(e -> e.getValue() == socketID); sockets.entrySet().removeIf(e -> e.getValue() == socketID);

View File

@ -0,0 +1,33 @@
package envoy.server.processors;
import java.io.IOException;
import java.time.Instant;
import envoy.event.contact.AccountDeletion;
import envoy.server.data.*;
import envoy.server.net.ObjectWriteProxy;
/**
* @author Leon Hofmeister
* @since Envoy Server v0.3-beta
*/
public class AccountDeletionProcessor implements ObjectProcessor<AccountDeletion> {
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override
public void process(AccountDeletion input, long socketID, ObjectWriteProxy writeProxy)
throws IOException {
final var contact = persistenceManager.getContactByID(input.get());
contact.getContacts().forEach(c -> {
persistenceManager.removeContactBidirectional(contact, c);
if (c instanceof User)
((User) c).setLatestContactDeletion(Instant.now());
});
writeProxy.writeToOnlineContacts(contact.getContacts(), input);
persistenceManager.deleteContact(contact);
}
}

View File

@ -4,6 +4,7 @@ import java.time.Instant;
import java.util.logging.Level; import java.util.logging.Level;
import envoy.event.GroupResize; import envoy.event.GroupResize;
import envoy.event.contact.AccountDeletion;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.server.data.*; import envoy.server.data.*;
@ -24,6 +25,12 @@ public final class GroupResizeProcessor implements ObjectProcessor<GroupResize>
final var group = persistenceManager.getGroupByID(groupResize.getGroupID()); final var group = persistenceManager.getGroupByID(groupResize.getGroupID());
final var sender = persistenceManager.getUserByID(groupResize.get().getID()); final var sender = persistenceManager.getUserByID(groupResize.get().getID());
// Inform the sender that this group has already been deleted
if (group == null) {
writeProxy.write(socketID, new AccountDeletion(groupResize.getGroupID()));
return;
}
// Perform the desired operation // Perform the desired operation
switch (groupResize.getOperation()) { switch (groupResize.getOperation()) {
case ADD: case ADD:

View File

@ -4,7 +4,7 @@ import java.time.Instant;
import java.util.logging.*; import java.util.logging.*;
import envoy.event.ElementOperation; import envoy.event.ElementOperation;
import envoy.event.contact.UserOperation; import envoy.event.contact.*;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
@ -22,9 +22,17 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override @Override
public void process(UserOperation evt, long socketId, ObjectWriteProxy writeProxy) { public void process(UserOperation evt, long socketID, ObjectWriteProxy writeProxy) {
final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketId); final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketID);
final long contactID = evt.get().getID(); final long contactID = evt.get().getID();
final var recipient = persistenceManager.getUserByID(contactID);
// Inform the sender if the requested contact has already been deleted
if (recipient == null) {
writeProxy.write(socketID, new AccountDeletion(contactID));
return;
}
final var sender = persistenceManager.getUserByID(userID); final var sender = persistenceManager.getUserByID(userID);
switch (evt.getOperationType()) { switch (evt.getOperationType()) {
case ADD: case ADD:
@ -45,7 +53,7 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
sender.setLatestContactDeletion(Instant.now()); sender.setLatestContactDeletion(Instant.now());
// Notify the removed contact on next startup(s) of this deletion // Notify the removed contact on next startup(s) of this deletion
persistenceManager.getUserByID(contactID).setLatestContactDeletion(Instant.now()); recipient.setLatestContactDeletion(Instant.now());
// Notify the removed contact if online // Notify the removed contact if online
if (connectionManager.isOnline(contactID)) if (connectionManager.isOnline(contactID))