diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java index b78bbbc4..06998f35 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java @@ -9,6 +9,7 @@ import bubble.dao.account.AccountDAO; import bubble.dao.account.AccountOwnedEntityDAO; import bubble.dao.cloud.BubbleNetworkDAO; import bubble.dao.cloud.CloudServiceDAO; +import bubble.model.account.Account; import bubble.model.bill.AccountPlan; import bubble.model.bill.Bill; import bubble.model.bill.BubblePlan; @@ -18,8 +19,10 @@ import bubble.model.cloud.CloudService; import bubble.model.cloud.HostnameValidationResult; import bubble.notify.payment.PaymentValidationResult; import bubble.server.BubbleConfiguration; +import bubble.service.account.SyncAccountService; import bubble.service.bill.RefundService; import bubble.service.cloud.NetworkService; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -166,9 +169,20 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { paymentDriver.purchase(accountPlanUuid, paymentMethodUuid, billUuid); }); } + + final var account = accountDAO.findByUuid(accountPlan.getAccount()); + configuration.getBean(SyncAccountService.class).syncPlan(account, accountPlan); + return super.postCreate(accountPlan, context); } + @Override public AccountPlan postUpdate(@NonNull final AccountPlan accountPlan, @NonNull final Object context) { + final var account = accountDAO.findByUuid(accountPlan.getAccount()); + configuration.getBean(SyncAccountService.class).syncPlan(account, accountPlan); + + return super.postUpdate(accountPlan, context); + } + @Override public void delete(String uuid) { final AccountPlan accountPlan = findByUuid(uuid); if (accountPlan == null) return; @@ -193,6 +207,14 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { } } - @Override public void forceDelete(String uuid) { super.delete(uuid); } + @Override public void forceDelete(String uuid) { + final AccountPlan accountPlan = findByUuid(uuid); + final Account account = accountDAO.findByUuid(accountPlan.getAccount()); + + super.delete(uuid); + + configuration.getBean(SyncAccountService.class).syncForceDeletedPlan(account, accountPlan.getName()); + } + public void forceDeleteWithoutSync(String uuid) { super.delete(uuid); } } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java index b2506ebb..800c43e3 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java @@ -174,6 +174,9 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { @Transient @Getter @Setter private transient Boolean sendMetrics = null; public boolean sendMetrics () { return sendMetrics == null || sendMetrics; } + @JsonIgnore @Transient @Getter @Setter private Boolean skipSync; + public boolean skipSync() { return bool(skipSync); } + public BubbleNetwork bubbleNetwork(Account account, BubbleDomain domain, BubblePlan plan, diff --git a/bubble-server/src/main/java/bubble/notify/NotificationHandler_sync_account.java b/bubble-server/src/main/java/bubble/notify/NotificationHandler_sync_account.java index db01116e..5d4862c2 100644 --- a/bubble-server/src/main/java/bubble/notify/NotificationHandler_sync_account.java +++ b/bubble-server/src/main/java/bubble/notify/NotificationHandler_sync_account.java @@ -5,12 +5,14 @@ package bubble.notify; import bubble.dao.account.AccountDAO; +import bubble.dao.bill.AccountPlanDAO; import bubble.dao.cloud.BubbleNodeDAO; import bubble.model.account.Account; +import bubble.model.bill.AccountPlan; import bubble.model.cloud.AnsibleInstallType; -import bubble.model.cloud.BubbleNode; import bubble.model.cloud.notify.ReceivedNotification; import bubble.service.account.SyncAccountNotification; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -22,34 +24,74 @@ public class NotificationHandler_sync_account extends ReceivedNotificationHandle @Autowired private BubbleNodeDAO nodeDAO; @Autowired private AccountDAO accountDAO; + @Autowired private AccountPlanDAO accountPlanDAO; - @Override public void handleNotification(ReceivedNotification n) { - final BubbleNode node = nodeDAO.findByUuid(n.getFromNode()); + @Override + public void handleNotification(ReceivedNotification n) { + final var node = nodeDAO.findByUuid(n.getFromNode()); if (node == null) { - log.warn("sync_account: node not found: "+n.getFromNode()); + log.warn("sync_account: node not found: " + n.getFromNode()); + return; + } + + final var notification = json(n.getPayloadJson(), SyncAccountNotification.class); + + final var localAccount = accountDAO.findByUuid(notification.getAccountUuid()); + if (localAccount == null) { + reportError("sync_account: localAccount not found: " + notification.getAccountUuid()); + return; + } + if (!localAccount.sync()) { + log.info("sync_account: localAccount " + localAccount.getName() + " has sync disabled, no sync done"); + return; + } + + if (notification.getHashedPassword().isPresent()) { + syncAccountPassword(localAccount, notification.getHashedPassword().get()); + } + if (notification.getAccountPlan().isPresent()) { + syncAccountPlan(localAccount.getUuid(), notification.getAccountPlan().get()); + } + if (notification.getForceDeletedAccountPlanName().isPresent()) { + syncForceDelAccountPlan(localAccount.getUuid(), notification.getForceDeletedAccountPlanName().get()); + } + } + + private void syncAccountPassword(@NonNull final Account localAccount, + @NonNull final String incomingHashedPassword) { + localAccount.getHashedPassword().setHashedPassword(incomingHashedPassword); + + // if we are a node, set skipSync so we don't get caught in an infinite loop + // (the node would notify the sage, which would notify the node, ad infinitum) + localAccount.setSkipSync(configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node); + + // update password, if we are a sage, this will notify all networks of password change + accountDAO.update(localAccount); + } + + private void syncAccountPlan(@NonNull final String accountUuid, @NonNull final AccountPlan incomingPlan) { + final var localPlan = accountPlanDAO.findByAccountAndId(accountUuid, incomingPlan.getName()); + + if (localPlan == null) { + // create new localPlan + final var newPlan = new AccountPlan(incomingPlan); + newPlan.setSkipSync(configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node); + accountPlanDAO.create(newPlan); + } else { + // update localPlan + localPlan.update(incomingPlan); + localPlan.setSkipSync(configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node); + accountPlanDAO.update(localPlan); + } + } + + private void syncForceDelAccountPlan(@NonNull final String accountUuid, @NonNull final String planName) { + final var localPlan = accountPlanDAO.findByAccountAndId(accountUuid, planName); + + if (configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node) { + accountPlanDAO.forceDeleteWithoutSync(localPlan.getUuid()); } else { - final SyncAccountNotification notification = json(n.getPayloadJson(), SyncAccountNotification.class); - - final Account account = accountDAO.findByUuid(notification.getAccountUuid()); - if (account == null) { - reportError("sync_account: account not found: "+notification.getAccountUuid()); - return; - } - if (!account.sync()) { - log.info("sync_account: account "+account.getName()+" has sync disabled, not synchronizing"); - return; - } - - account.getHashedPassword().setHashedPassword(notification.getHashedPassword()); - - // if we are a node, set skipSync so we don't get caught in an infinite loop - // (the node would notify the sage, which would notify the node, ad infinitum) - if (configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node) { - account.setSkipSync(true); - } - - // update password, if we are a sage, this will notify all networks of password change - accountDAO.update(account); + accountPlanDAO.forceDelete(localPlan.getUuid()); } } diff --git a/bubble-server/src/main/java/bubble/service/account/StandardSyncAccountService.java b/bubble-server/src/main/java/bubble/service/account/StandardSyncAccountService.java index 70e86bae..ae05a53a 100644 --- a/bubble-server/src/main/java/bubble/service/account/StandardSyncAccountService.java +++ b/bubble-server/src/main/java/bubble/service/account/StandardSyncAccountService.java @@ -7,12 +7,14 @@ package bubble.service.account; import bubble.dao.cloud.BubbleNetworkDAO; import bubble.dao.cloud.BubbleNodeDAO; import bubble.model.account.Account; +import bubble.model.bill.AccountPlan; import bubble.model.cloud.AnsibleInstallType; import bubble.model.cloud.BubbleNetwork; import bubble.model.cloud.BubbleNetworkState; import bubble.model.cloud.BubbleNode; import bubble.server.BubbleConfiguration; import bubble.service.notify.NotificationService; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -28,51 +30,62 @@ public class StandardSyncAccountService implements SyncAccountService { @Autowired private NotificationService notificationService; @Autowired private BubbleConfiguration configuration; - public void syncAccount(Account account) { + public void syncAccount(@NonNull final Account account) { + sync(account, new SyncAccountNotification(account)); + } + + public void syncPlan(@NonNull final Account account, @NonNull final AccountPlan accountPlan) { + sync(account, new SyncAccountNotification(account.getUuid(), accountPlan)); + } + + public void syncForceDeletedPlan(@NonNull final Account account, @NonNull final String accountPlanName) { + sync(account, new SyncAccountNotification(account.getUuid(), accountPlanName)); + } + + private void sync(@NonNull final Account account, @NonNull final SyncAccountNotification notification) { final BubbleNetwork thisNetwork = configuration.getThisNetwork(); if (thisNetwork == null) { // should never happen - log.warn("syncAccount: thisNetwork was null, sync_account is impossible"); + log.warn("sync: thisNetwork was null, sync_account is impossible"); return; } if (!account.admin()) { - log.info("syncAccount: not syncing non-admin password"); + log.info("sync: not syncing non-admin account"); return; } if (!account.sync()) { - log.info("syncAccount: password sync disabled for account: "+account.getName()); + log.info("sync: account sync disabled for account: "+account.getName()); return; } final AnsibleInstallType installType = thisNetwork.getInstallType(); - final SyncAccountNotification notification = new SyncAccountNotification(account); if (installType == AnsibleInstallType.sage) { - // changing password on sage, notify all bubbles launched by user that have syncAccount == true + // changing account on sage, notify all bubbles launched by user that have syncAccount == true for (BubbleNetwork network : networkDAO.findByAccount(account.getUuid())) { if (network.getState() != BubbleNetworkState.running) continue; if (!network.syncAccount()) continue; for (BubbleNode node : nodeDAO.findByNetwork(network.getUuid())) { if (node.getUuid().equals(configuration.getThisNode().getUuid())) { - log.info("syncAccount: not notifying self"); + log.info("sync: not notifying self"); continue; } - log.info("syncAccount: sending sync_account notification from sage to node: "+node.id()); + log.info("sync: sending sync_account notification from sage to node: "+node.id()); notificationService.notify(node, sync_account, notification); } } } else if (installType == AnsibleInstallType.node) { if (!thisNetwork.syncAccount()) { - log.info("syncAccount: disabled for node, not sending sync_account notification"); + log.info("sync: disabled for node, not sending sync_account notification"); return; } - // changing password on node, notify sage, which will then notify all bubbles launched by user that have + // changing account on node, notify sage, which will then notify all bubbles launched by user that have // syncAccount == true - log.info("syncAccount: sending sync_account notification from node to sage: "+configuration.getSageNode()); + log.info("sync: sending sync_account notification from node to sage: "+configuration.getSageNode()); notificationService.notify(configuration.getSageNode(), sync_account, notification); } else { - reportError("syncAccount("+account.getEmail()+"/"+account.getUuid()+"): invalid installType: "+installType); + reportError("sync("+account.getEmail()+"/"+account.getUuid()+"): invalid installType: "+installType); } } diff --git a/bubble-server/src/main/java/bubble/service/account/SyncAccountNotification.java b/bubble-server/src/main/java/bubble/service/account/SyncAccountNotification.java index 624ee447..4362de3d 100644 --- a/bubble-server/src/main/java/bubble/service/account/SyncAccountNotification.java +++ b/bubble-server/src/main/java/bubble/service/account/SyncAccountNotification.java @@ -5,20 +5,35 @@ package bubble.service.account; import bubble.model.account.Account; +import bubble.model.bill.AccountPlan; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.NonNull; import lombok.Setter; import lombok.experimental.Accessors; +import java.util.Optional; + @NoArgsConstructor @Accessors(chain=true) public class SyncAccountNotification { @Getter @Setter private String accountUuid; - @Getter @Setter private String hashedPassword; + @Getter @Setter private Optional hashedPassword = Optional.empty(); + @Getter @Setter private Optional accountPlan = Optional.empty(); + @Getter @Setter private Optional forceDeletedAccountPlanName = Optional.empty(); - public SyncAccountNotification(Account account) { + public SyncAccountNotification(@NonNull final Account account) { this.accountUuid = account.getUuid(); - this.hashedPassword = account.getHashedPassword().getHashedPassword(); + this.hashedPassword = Optional.of(account.getHashedPassword().getHashedPassword()); + } + + public SyncAccountNotification(@NonNull final String accountUuid, @NonNull final AccountPlan accountPlan) { + this.accountUuid = accountUuid; + this.accountPlan = Optional.of(accountPlan); } + public SyncAccountNotification(@NonNull final String accountUuid, @NonNull final String forceDeletedPlanUuid) { + this.accountUuid = accountUuid; + this.forceDeletedAccountPlanName = Optional.of(forceDeletedPlanUuid); + } } diff --git a/bubble-server/src/main/java/bubble/service/account/SyncAccountService.java b/bubble-server/src/main/java/bubble/service/account/SyncAccountService.java index d51750b2..9f682fa0 100644 --- a/bubble-server/src/main/java/bubble/service/account/SyncAccountService.java +++ b/bubble-server/src/main/java/bubble/service/account/SyncAccountService.java @@ -5,9 +5,13 @@ package bubble.service.account; import bubble.model.account.Account; +import bubble.model.bill.AccountPlan; +import lombok.NonNull; public interface SyncAccountService { - void syncAccount(Account account); + void syncAccount(@NonNull final Account account); + void syncPlan(@NonNull final Account account, @NonNull final AccountPlan accountPlan); + void syncForceDeletedPlan(@NonNull final Account account, @NonNull final String accountPlanName); } diff --git a/bubble-server/src/main/java/bubble/service_dbfilter/DbFilterSyncAccountService.java b/bubble-server/src/main/java/bubble/service_dbfilter/DbFilterSyncAccountService.java index 914455e2..7854813f 100644 --- a/bubble-server/src/main/java/bubble/service_dbfilter/DbFilterSyncAccountService.java +++ b/bubble-server/src/main/java/bubble/service_dbfilter/DbFilterSyncAccountService.java @@ -5,7 +5,9 @@ package bubble.service_dbfilter; import bubble.model.account.Account; +import bubble.model.bill.AccountPlan; import bubble.service.account.SyncAccountService; +import lombok.NonNull; import org.springframework.stereotype.Service; import static org.cobbzilla.util.daemon.ZillaRuntime.notSupported; @@ -13,6 +15,13 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.notSupported; @Service public class DbFilterSyncAccountService implements SyncAccountService { - @Override public void syncAccount(Account account) { notSupported("syncAccount"); } + @Override public void syncAccount(@NonNull final Account account) { notSupported("syncAccount"); } + @Override public void syncPlan(@NonNull final Account account, @NonNull final AccountPlan accountPlan) { + notSupported("syncPlan"); + } + + @Override public void syncForceDeletedPlan(@NonNull final Account account, @NonNull final String accountPlanName) { + notSupported("syncForceDeletedPlan"); + } }