diff --git a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java index abdf402b..ae6962a6 100644 --- a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java +++ b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java @@ -164,7 +164,7 @@ public class AccountDAO extends AbstractCRUDDAO implements SqlViewSearc // create an uninitialized device for the account, but only if this is a regular node network // sage networks do not allow devices, they launch and manage other regular node networks - if (!account.isRoot() && !configuration.isSage()) { + if (!isFirstAdmin(account) && !configuration.isSage()) { deviceDAO.ensureAllSpareDevices(account.getUuid(), configuration.getThisNetwork().getUuid()); deviceDAO.refreshVpnUsers(); } @@ -475,7 +475,8 @@ public class AccountDAO extends AbstractCRUDDAO implements SqlViewSearc private final Refreshable firstAdmin = new Refreshable<>("firstAdmin", FIRST_ADMIN_CACHE_MILLIS, this::findFirstAdmin); public Account getFirstAdmin() { return firstAdmin.get(); } - public boolean isFirstAdmin(Account account) { return getFirstAdmin().getUuid().equals(account.getUuid()); } + public boolean isFirstAdmin(Account account) { return isFirstAdmin(account.getUuid()); } + public boolean isFirstAdmin(String accountUuid) { return getFirstAdmin().getUuid().equals(accountUuid); } public Account findFirstAdmin() { final List admins = findByField("admin", true); diff --git a/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java b/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java index 7490b5ad..619dc76e 100644 --- a/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java +++ b/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java @@ -81,7 +81,7 @@ public class DeviceDAO extends AccountOwnedEntityDAO { var uninitializedDevices = findByAccountAndUninitialized(accountUuid); if (uninitializedDevices.size() <= SPARE_DEVICES_PER_ACCOUNT_THRESHOLD - && !configuration.getBean(AccountDAO.class).findByUuid(accountUuid).isRoot()) { + && !configuration.getBean(AccountDAO.class).isFirstAdmin(accountUuid)) { if (ensureAllSpareDevices(accountUuid, device.getNetwork())) refreshVpnUsers(); } diff --git a/bubble-server/src/main/java/bubble/model/account/Account.java b/bubble-server/src/main/java/bubble/model/account/Account.java index fc192478..fdf78ab0 100644 --- a/bubble-server/src/main/java/bubble/model/account/Account.java +++ b/bubble-server/src/main/java/bubble/model/account/Account.java @@ -112,7 +112,6 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci @Override @Transient public String getName() { return getEmail(); } public Account setName(String n) { return setEmail(n); } - @Transient @JsonIgnore public boolean isRoot() { return getName().equals(ROOT_USERNAME); } // make this updatable if we ever want accounts to be able to change parents // there might be a lot more involved in that action though (read-only parent objects that will no longer be visible, must be copied in?) 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 8001fe32..151df3bc 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java @@ -166,6 +166,9 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { @Transient @Getter @Setter private transient String forkHost = null; public boolean hasForkHost () { return !empty(forkHost); } + @Transient @Getter @Setter private transient String adminEmail = null; + public boolean hasAdminEmail() { return !empty(adminEmail); } + @Transient @Getter @Setter private transient Boolean syncAccount = null; public boolean syncAccount() { return syncAccount == null || syncAccount; } @@ -200,7 +203,8 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { .setComputeSizeType(plan.getComputeSizeType()) .setStorage(storage.getUuid()) .setLaunchType(hasForkHost() && hasLaunchType() ? getLaunchType() : LaunchType.node) - .setForkHost(hasForkHost() ? getForkHost() : null); + .setForkHost(hasForkHost() ? getForkHost() : null) + .setAdminEmail(hasAdminEmail() ? getAdminEmail() : null); } } diff --git a/bubble-server/src/main/java/bubble/model/boot/ActivationRequest.java b/bubble-server/src/main/java/bubble/model/boot/ActivationRequest.java index 648abdfb..884c5c87 100644 --- a/bubble-server/src/main/java/bubble/model/boot/ActivationRequest.java +++ b/bubble-server/src/main/java/bubble/model/boot/ActivationRequest.java @@ -23,7 +23,8 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.empty; public class ActivationRequest { @HasValue(message="err.email.required") - @Getter @Setter private String email; + @Getter private String email; + public ActivationRequest setEmail(String e) { this.email = empty(e) ? e : e.trim(); return this; } public boolean hasEmail() { return !empty(email); } public String getName() { return getEmail(); } @@ -55,6 +56,9 @@ public class ActivationRequest { @Getter @Setter private Boolean skipTests = false; public boolean skipTests () { return bool(skipTests); }; + @Getter @Setter private Boolean skipPacker = false; + public boolean skipPacker () { return bool(skipPacker); }; + @Getter @Setter private AccountSshKey sshKey; public boolean hasSshKey () { return sshKey != null; } diff --git a/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java b/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java index f7f8aa83..09f9ecb5 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java +++ b/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java @@ -208,6 +208,9 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu public boolean hasForkHost () { return !empty(forkHost); } public boolean fork() { return hasForkHost(); } + @Transient @Getter @Setter private transient String adminEmail; + public boolean hasAdminEmail () { return !empty(adminEmail); } + @ECField(index=190) @Column(length=20, updatable=false) @Enumerated(EnumType.STRING) @Getter @Setter private LaunchType launchType = null; public boolean hasLaunchType () { return launchType != null; } diff --git a/bubble-server/src/main/java/bubble/model/cloud/LaunchType.java b/bubble-server/src/main/java/bubble/model/cloud/LaunchType.java index eb6c8a7b..56da1fd1 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/LaunchType.java +++ b/bubble-server/src/main/java/bubble/model/cloud/LaunchType.java @@ -5,13 +5,20 @@ package bubble.model.cloud; import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.AllArgsConstructor; import static bubble.ApiConstants.enumFromString; +@AllArgsConstructor public enum LaunchType { - node, fork_node, fork_sage; + node (false), + fork_node (true), + fork_sage (true); @JsonCreator public static LaunchType fromString(String v) { return enumFromString(LaunchType.class, v); } + private final boolean fork; + public boolean fork () { return fork; } + } diff --git a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java index 90fca0ba..a74390ee 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -166,6 +166,11 @@ public class AuthResource { if (accountDAO.activated()) { return invalid("err.activation.alreadyDone", "activation has already been done"); } + + if (!request.hasEmail()) return invalid("err.email.required", "email is required"); + if (request.getEmail().contains("{{") && request.getEmail().contains("}}")) { + request.setEmail(configuration.applyHandlebars(request.getEmail()).trim()); + } if (!request.hasEmail()) return invalid("err.email.required", "email is required"); if (!request.hasPassword()) return invalid("err.password.required", "password is required"); diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java index 3aa040a3..d36cd8c7 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java @@ -41,10 +41,10 @@ import java.util.List; import java.util.stream.Collectors; import static bubble.ApiConstants.*; +import static bubble.model.account.Account.ROOT_EMAIL; import static bubble.model.cloud.BubbleNetwork.validateHostname; import static org.cobbzilla.util.daemon.ZillaRuntime.*; -import static org.cobbzilla.util.string.ValidationRegexes.HOST_PATTERN; -import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches; +import static org.cobbzilla.util.string.ValidationRegexes.*; import static org.cobbzilla.wizard.model.NamedEntity.NAME_MAXLEN; import static org.cobbzilla.wizard.resources.ResourceUtil.*; @@ -167,6 +167,18 @@ public class AccountPlansResource extends AccountOwnedResource> objects = modelSetupService.setupModel(api, account, "manifest-defaults"); log.info("bootstrapThisNode: created default objects\n"+json(objects)); + + if (!request.skipPacker()) initialPacker(account); + }, "ActivationService.bootstrapThisNode.createDefaultObjects"); + + } else if (!request.skipPacker()) { + initialPacker(account); } return node; } + private void initialPacker(Account account) { + for (CloudService cloud : cloudDAO.findByAccountAndType(account.getUuid(), CloudServiceType.compute)) { + log.info("initialPacker: creating images for compute cloud: "+cloud.getName()); + packerService.writePackerImages(cloud, AnsibleInstallType.sage, null); + packerService.writePackerImages(cloud, AnsibleInstallType.node, null); + } + } + public BubbleNetwork createRootNetwork(BubbleNetwork network) { network.setUuid(ROOT_NETWORK_UUID); return networkDAO.create(network); diff --git a/bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java b/bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java index 809cc506..aa82bb4f 100644 --- a/bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java +++ b/bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java @@ -15,10 +15,12 @@ import bubble.dao.cloud.BubbleNodeDAO; import bubble.dao.cloud.BubbleNodeKeyDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.dao.device.DeviceDAO; -import bubble.model.account.Account; import bubble.model.bill.AccountPlan; import bubble.model.bill.BubblePlan; -import bubble.model.cloud.*; +import bubble.model.cloud.BubbleNetwork; +import bubble.model.cloud.BubbleNode; +import bubble.model.cloud.BubbleNodeKey; +import bubble.model.cloud.BubbleNodeState; import bubble.model.cloud.notify.NotificationReceipt; import bubble.model.cloud.notify.NotificationType; import bubble.server.BubbleConfiguration; @@ -29,7 +31,6 @@ import lombok.Getter; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.cache.Refreshable; -import org.cobbzilla.util.daemon.SimpleDaemon; import org.cobbzilla.util.http.HttpSchemes; import org.cobbzilla.util.http.HttpUtil; import org.cobbzilla.util.io.FileUtil; @@ -53,7 +54,6 @@ import static bubble.server.BubbleServer.disableRestoreMode; import static bubble.server.BubbleServer.isRestoreMode; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MINUTES; -import static java.util.function.Predicate.not; import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.io.FileUtil.abs; import static org.cobbzilla.util.io.FileUtil.toFileOrDie; @@ -162,7 +162,7 @@ public class StandardSelfNodeService implements SelfNodeService { background(() -> { if (accountDAO.findAll() .stream() - .filter(not(Account::isRoot)) + .filter(a -> !accountDAO.isFirstAdmin(a)) .map(a -> deviceDAO.ensureAllSpareDevices(a.getUuid(), thisNetworkUuid)) .reduce(false, Boolean::logicalOr) .booleanValue()) { diff --git a/bubble-server/src/main/java/bubble/service/dbfilter/EntityIterator.java b/bubble-server/src/main/java/bubble/service/dbfilter/EntityIterator.java index 67a6b46d..24befdc3 100644 --- a/bubble-server/src/main/java/bubble/service/dbfilter/EntityIterator.java +++ b/bubble-server/src/main/java/bubble/service/dbfilter/EntityIterator.java @@ -7,9 +7,7 @@ package bubble.service.dbfilter; import bubble.cloud.CloudServiceType; import bubble.cloud.storage.local.LocalStorageConfig; import bubble.cloud.storage.local.LocalStorageDriver; -import bubble.model.account.Account; -import bubble.model.account.AccountSshKey; -import bubble.model.account.AccountTemplate; +import bubble.model.account.*; import bubble.model.app.AppTemplateEntity; import bubble.model.app.BubbleApp; import bubble.model.bill.AccountPaymentMethod; @@ -135,7 +133,29 @@ public abstract class EntityIterator implements Iterator { .forEach(this::add); } else if (Account.class.isAssignableFrom(c)) { - entities.forEach(e -> add(((Account) e).setPreferredPlan(null))); + entities.forEach(e -> { + if (network.hasAdminEmail() && network.getAccount().equals(e.getUuid())) { + final Account a = (Account) e; + a.setEmail(network.getAdminEmail()); + } + add(((Account) e).setPreferredPlan(null)); + }); + + } else if (AccountPolicy.class.isAssignableFrom(c) && network.hasAdminEmail()) { + entities.forEach(e -> { + if (network.hasAdminEmail()) { + final AccountPolicy p = (AccountPolicy) e; + if (p.getAccount().equals(network.getAccount())) { + final AccountContact adminContact = new AccountContact() + .setType(CloudServiceType.email) + .setInfo(network.getAdminEmail()) + .setVerified(true) + .setRemovable(false); + p.setAccountContacts(new AccountContact[]{adminContact}); + } + } + add(e); + }); } else if (AccountSshKey.class.isAssignableFrom(c)) { entities.forEach(e -> add(setInstallKey((AccountSshKey) e, network))); diff --git a/bubble-server/src/main/resources/messages b/bubble-server/src/main/resources/messages index 6557ae47..c0070a05 160000 --- a/bubble-server/src/main/resources/messages +++ b/bubble-server/src/main/resources/messages @@ -1 +1 @@ -Subproject commit 6557ae47aae789ce6dc6ff1aca189787a89103bb +Subproject commit c0070a05666df680157c6740879ad5a3c98c3bba diff --git a/bubble-web b/bubble-web index b7120e40..29001799 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit b7120e4099a382278c43813644f99e928c9ae828 +Subproject commit 290017992084da8d799aba24409099d54c1a9305 diff --git a/config/activation.json b/config/activation.json index 2f5e9ab1..1f9b4341 100644 --- a/config/activation.json +++ b/config/activation.json @@ -54,9 +54,9 @@ } }, - // You must configure the an SMTP service, it is required to send emails - // SendGrid, MailGun, or any other SMTP service should work fine - "SmtpServer": { + // You must configure an email service, it is required to send emails + // Comment out the email clouds that you don't use + "SmtpEmail": { "config": { "tlsEnabled": true }, @@ -68,6 +68,21 @@ } }, + "SendGridEmail": { + "config": {}, + "credentials": { + "apiKey": "your_sendgrid_api_key" + } + }, + + "MailgunEmail": { + "config": {}, + "credentials": { + "domain": "your_mailgun_domain", + "apiKey": "your_mailgun_api_key" + } + }, + // Required for TOTP-based authentication. Nothing to configure, just leave this as-is "TOTPAuthenticator": {}, diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index d7ea904f..d3b5e525 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit d7ea904f35401302cbfb4bde5d70475d2721df07 +Subproject commit d3b5e5254c2eca16290dc5746d0ab489160a8ff1