# Conflicts: # bubble-server/src/main/java/bubble/server/BubbleConfiguration.java # bubble-webpull/20/head
@@ -11,6 +11,7 @@ import com.warrenstrange.googleauth.GoogleAuthenticator; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.apache.commons.lang3.RandomUtils; | |||
import org.cobbzilla.util.daemon.ZillaRuntime; | |||
import org.cobbzilla.util.io.FileUtil; | |||
import org.glassfish.grizzly.http.server.Request; | |||
import org.glassfish.jersey.server.ContainerRequest; | |||
@@ -232,7 +233,10 @@ public class ApiConstants { | |||
return val == null ? null : val.textValue(); | |||
} | |||
@Getter(lazy=true) private static final String[] hostPrefixes = stream2string("bubble/host-prefixes.txt").split("\n"); | |||
@Getter(lazy=true) private static final String[] hostPrefixes = Arrays.stream(stream2string("bubble/host-prefixes.txt") | |||
.split("\n")) | |||
.filter(ZillaRuntime::notEmpty) | |||
.toArray(String[]::new); | |||
public static String newNodeHostname() { | |||
final String rand0 = getHostPrefixes()[RandomUtils.nextInt(0, getHostPrefixes().length)]; | |||
@@ -243,6 +247,14 @@ public class ApiConstants { | |||
return rand0+"-"+(rand1 < 10 ? "0"+rand1 : rand1)+rand2+"-"+rand3+rand4; | |||
} | |||
public static String newNetworkName() { | |||
final String rand0 = getHostPrefixes()[RandomUtils.nextInt(0, getHostPrefixes().length)]; | |||
final String rand1 = randomAlphanumeric(2).toLowerCase() + RandomUtils.nextInt(10, 100) + randomAlphanumeric(1).toLowerCase(); | |||
final String rand2 = randomAlphanumeric(2).toLowerCase() + RandomUtils.nextInt(10, 100) + randomAlphanumeric(1).toLowerCase(); | |||
final String rand3 = randomAlphanumeric(2).toLowerCase() + RandomUtils.nextInt(10, 100) + randomAlphanumeric(1).toLowerCase(); | |||
return rand0+"-"+rand1+"-"+rand2+"-"+rand3; | |||
} | |||
public static String getRemoteHost(Request req) { | |||
final String xff = req.getHeader("X-Forwarded-For"); | |||
final String remoteHost = xff == null ? req.getRemoteAddr() : xff; | |||
@@ -162,7 +162,7 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||
} | |||
if (account.hasParent()) { | |||
final AccountInitializer init = new AccountInitializer(account, this, messageDAO, selfNodeService); | |||
final AccountInitializer init = new AccountInitializer(account, this, policyDAO, messageDAO, selfNodeService); | |||
account.setAccountInitializer(init); | |||
daemon(init); | |||
} | |||
@@ -6,6 +6,7 @@ package bubble.dao.account; | |||
import bubble.dao.account.message.AccountMessageDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.account.AccountPolicy; | |||
import bubble.model.account.message.AccountAction; | |||
import bubble.model.account.message.AccountMessage; | |||
import bubble.model.account.message.AccountMessageType; | |||
@@ -31,6 +32,7 @@ public class AccountInitializer implements Runnable { | |||
private Account account; | |||
private AccountDAO accountDAO; | |||
private AccountPolicyDAO policyDAO; | |||
private AccountMessageDAO messageDAO; | |||
private SelfNodeService selfNodeService; | |||
@@ -50,9 +52,14 @@ public class AccountInitializer implements Runnable { | |||
public Exception getError() { return error.get(); } | |||
public boolean hasError () { return getError() != null; } | |||
public AccountInitializer(Account account, AccountDAO accountDAO, AccountMessageDAO messageDAO, SelfNodeService selfNodeService) { | |||
public AccountInitializer(Account account, | |||
AccountDAO accountDAO, | |||
AccountPolicyDAO policyDAO, | |||
AccountMessageDAO messageDAO, | |||
SelfNodeService selfNodeService) { | |||
this.account = account; | |||
this.accountDAO = accountDAO; | |||
this.policyDAO = policyDAO; | |||
this.messageDAO = messageDAO; | |||
this.selfNodeService = selfNodeService; | |||
} | |||
@@ -73,10 +80,6 @@ public class AccountInitializer implements Runnable { | |||
log.warn("aborting!"); | |||
return; | |||
} | |||
if (account.hasPolicy() && account.getPolicy().hasAccountContacts()) { | |||
messageDAO.sendVerifyRequest(account.getRemoteHost(), account, account.getPolicy().getAccountContacts()[0]); | |||
} | |||
success = true; | |||
break; | |||
} catch (Exception e) { | |||
@@ -87,18 +90,22 @@ public class AccountInitializer implements Runnable { | |||
if (!success) throw lastEx; | |||
if (account.sendWelcomeEmail()) { | |||
final BubbleNetwork thisNetwork = selfNodeService.getThisNetwork(); | |||
final String accountUuid = account.getUuid(); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(accountUuid); | |||
final String contact = policy != null && policy.hasAccountContacts() ? policy.getAccountContacts()[0].getUuid() : null; | |||
if (contact == null) die("no contact found for welcome message: account="+accountUuid); | |||
messageDAO.create(new AccountMessage() | |||
.setRemoteHost(account.getRemoteHost()) | |||
.setAccount(account.getUuid()) | |||
.setName(account.getUuid()) | |||
.setAccount(accountUuid) | |||
.setName(accountUuid) | |||
.setNetwork(thisNetwork.getUuid()) | |||
.setMessageType(AccountMessageType.notice) | |||
.setAction(AccountAction.welcome) | |||
.setTarget(ActionTarget.account)); | |||
.setTarget(ActionTarget.account) | |||
.setContact(contact)); | |||
} | |||
} catch (Exception e) { | |||
error.set(e); | |||
// todo: send to errbit | |||
die("error: "+e, e); | |||
} finally { | |||
completed.set(true); | |||
@@ -166,6 +166,9 @@ public class AccountMessageDAO extends AccountOwnedEntityDAO<AccountMessage> { | |||
} | |||
public AccountMessage findOperationRequest(AccountMessage basis) { | |||
if (basis.getAction() == AccountAction.welcome && basis.getTarget() == ActionTarget.account) { | |||
return findWelcomeNotice(basis); | |||
} | |||
return findByUniqueFields("account", basis.getAccount(), | |||
"name", basis.getName(), | |||
"requestId", basis.getRequestId(), | |||
@@ -174,6 +177,15 @@ public class AccountMessageDAO extends AccountOwnedEntityDAO<AccountMessage> { | |||
"target", basis.getTarget()); | |||
} | |||
public AccountMessage findWelcomeNotice(AccountMessage basis) { | |||
return findByUniqueFields("account", basis.getAccount(), | |||
"name", basis.getName(), | |||
"requestId", basis.getRequestId(), | |||
"messageType", AccountMessageType.notice, | |||
"action", AccountAction.welcome, | |||
"target", ActionTarget.account); | |||
} | |||
public List<AccountMessage> findOperationDenials(AccountMessage basis) { | |||
if (basis == null) { | |||
return Collections.emptyList(); | |||
@@ -45,6 +45,7 @@ public class BubbleNetworkDAO extends AccountOwnedEntityDAO<BubbleNetwork> { | |||
if (errors.isInvalid()) throw invalidEx(errors); | |||
if (errors.hasSuggestedName()) network.setName(errors.getSuggestedName()); | |||
} | |||
if (!network.hasNickname()) network.setNickname(network.getName()); | |||
final AnsibleInstallType installType = network.hasForkHost() && configuration.isSageLauncher() | |||
? AnsibleInstallType.sage | |||
: AnsibleInstallType.node; | |||
@@ -1,3 +1,7 @@ | |||
/** | |||
* Copyright (c) 2020 Bubble, Inc. All rights reserved. | |||
* For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ | |||
*/ | |||
package bubble.model.account; | |||
import org.cobbzilla.wizard.auth.LoginRequest; | |||
@@ -97,13 +97,17 @@ public class AccountMessage extends IdentifiableBase implements HasAccount { | |||
public String templateName(String basename) { return getMessageType()+"/"+ getAction()+"/"+getTarget()+"/"+basename+".hbs"; } | |||
public long tokenTimeoutSeconds(AccountPolicy policy) { | |||
if (getMessageType() != AccountMessageType.request) return -1; | |||
switch (getTarget()) { | |||
case account: return policy.getAccountOperationTimeout()/1000; | |||
case network: return policy.getNodeOperationTimeout()/1000; | |||
default: | |||
log.warn("tokenTimeout: invalid target: "+getTarget()); | |||
return -1; | |||
// only requests and welcome message get tokens (welcome messages also verify the initial email address) | |||
if (getMessageType() == AccountMessageType.request || (getMessageType() == AccountMessageType.notice && getAction() == AccountAction.welcome)) { | |||
switch (getTarget()) { | |||
case account: return policy.getAccountOperationTimeout()/1000; | |||
case network: return policy.getNodeOperationTimeout()/1000; | |||
default: | |||
log.warn("tokenTimeout: invalid target: "+getTarget()); | |||
return -1; | |||
} | |||
} else { | |||
return -1; | |||
} | |||
} | |||
@@ -132,6 +132,10 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
@Getter @Setter private String refundError; | |||
// Fields below are used when creating a new plan, to also create the network associated with it | |||
@Size(max=NAME_MAXLEN, message="err.nick.tooLong") | |||
@Transient @Getter @Setter private transient String nickname; | |||
public boolean hasNickname () { return !empty(nickname); } | |||
@Size(max=10000, message="err.description.length") | |||
@Transient @Getter @Setter private transient String description; | |||
@@ -171,6 +175,7 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
CloudService storage) { | |||
return new BubbleNetwork() | |||
.setName(getName()) | |||
.setNickname(getNickname()) | |||
.setDescription(getDescription()) | |||
.setLocale(getLocale()) | |||
.setTimezone(getTimezone()) | |||
@@ -61,7 +61,7 @@ import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; | |||
public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBubbleTags<BubbleNetwork> { | |||
public static final String[] UPDATE_FIELDS = { | |||
"footprint", "description", "locale", "timezone", "state", "syncPassword", "launchLock" | |||
"nickname", "footprint", "description", "locale", "timezone", "state", "syncPassword", "launchLock" | |||
}; | |||
public static final String[] CREATE_FIELDS = ArrayUtil.append(UPDATE_FIELDS, | |||
"name", "domain", "sendErrors", "sendMetrics"); | |||
@@ -110,7 +110,13 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
@Transient @JsonIgnore public String getNetworkDomain () { return name + "." + domainName; } | |||
@Column(nullable=false) @ECField(index=50) | |||
@ECSearchable(filter=true) @ECField(index=50) | |||
@ECIndex @Column(nullable=false, length=NAME_MAXLEN) | |||
@Size(min=1, max=NAME_MAXLEN, message="err.nick.tooLong") | |||
@Getter @Setter private String nickname; | |||
public boolean hasNickname () { return !empty(nickname); } | |||
@Column(nullable=false) @ECField(index=60) | |||
@Getter @Setter private Integer sslPort; | |||
@Transient @JsonIgnore public String getPublicUri() { | |||
@@ -121,37 +127,37 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
return getUuid().equals(ROOT_NETWORK_UUID) ? configuration.getPublicUriBase() : getPublicUri(); | |||
} | |||
@ECIndex @Column(nullable=false, updatable=false, length=60) | |||
@ECIndex @Column(nullable=false, updatable=false, length=60) @ECField(index=70) | |||
@Enumerated(EnumType.STRING) | |||
@Getter @Setter private AnsibleInstallType installType; | |||
@ECSearchable @ECField(index=70) | |||
@ECSearchable @ECField(index=80) | |||
@ECForeignKey(entity=AccountSshKey.class) | |||
@Column(length=UUID_MAXLEN) | |||
@Getter @Setter private String sshKey; | |||
public boolean hasSshKey () { return !empty(sshKey); } | |||
@ECSearchable @ECField(index=80) | |||
@ECSearchable @ECField(index=90) | |||
@ECIndex @Column(nullable=false, updatable=false, length=20) | |||
@Enumerated(EnumType.STRING) @Getter @Setter private ComputeNodeSizeType computeSizeType; | |||
@ECSearchable @ECField(index=90) | |||
@ECSearchable @ECField(index=100) | |||
@ECForeignKey(entity=BubbleFootprint.class) | |||
@Column(nullable=false, length=UUID_MAXLEN) | |||
@Getter @Setter private String footprint; | |||
public boolean hasFootprint () { return footprint != null; } | |||
@ECSearchable @ECField(index=100) | |||
@ECSearchable @ECField(index=110) | |||
@ECForeignKey(entity=CloudService.class) | |||
@Column(nullable=false, updatable=false, length=UUID_MAXLEN) | |||
@Getter @Setter private String storage; | |||
@ECSearchable(filter=true) @ECField(index=110) | |||
@ECSearchable(filter=true) @ECField(index=120) | |||
@Size(max=10000, message="err.description.length") | |||
@Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(10000+ENC_PAD)+")") | |||
@Getter @Setter private String description; | |||
@ECSearchable @ECField(index=120) | |||
@ECSearchable @ECField(index=130) | |||
@Size(max=20, message="err.locale.length") | |||
@Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(20+ENC_PAD)+") NOT NULL") | |||
@Getter @Setter private String locale = getDEFAULT_LOCALE(); | |||
@@ -159,27 +165,27 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
// A unicode timezone alias from: cobbzilla-utils/src/main/resources/org/cobbzilla/util/time/unicode-timezones.xml | |||
// All unicode aliases are guaranteed to map to a Linux timezone and a Java timezone | |||
@ECSearchable @ECField(index=130) | |||
@ECSearchable @ECField(index=140) | |||
@Size(max=100, message="err.timezone.length") | |||
@Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(100+ENC_PAD)+") NOT NULL") | |||
@Getter @Setter private String timezone = "America/New_York"; | |||
@ECSearchable @ECField(index=140) | |||
@ECSearchable @ECField(index=150) | |||
@Column(nullable=false) | |||
@ECIndex @Getter @Setter private Boolean syncPassword; | |||
public boolean syncPassword() { return bool(syncPassword); } | |||
@ECSearchable @ECField(index=150) | |||
@ECSearchable @ECField(index=160) | |||
@Column(nullable=false) | |||
@ECIndex @Getter @Setter private Boolean launchLock; | |||
public boolean launchLock() { return bool(launchLock); } | |||
@ECSearchable @ECField(index=160) | |||
@ECSearchable @ECField(index=170) | |||
@Column(nullable=false) | |||
@ECIndex @Getter @Setter private Boolean sendErrors; | |||
public boolean sendErrors() { return bool(sendErrors); } | |||
@ECSearchable @ECField(index=170) | |||
@ECSearchable @ECField(index=180) | |||
@Column(nullable=false) | |||
@ECIndex @Getter @Setter private Boolean sendMetrics; | |||
public boolean sendMetrics() { return bool(sendMetrics); } | |||
@@ -190,7 +196,7 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
public boolean hasForkHost () { return !empty(forkHost); } | |||
public boolean fork() { return hasForkHost(); } | |||
@ECSearchable @ECField(index=160) | |||
@ECSearchable @ECField(index=190) | |||
@Column(length=20) | |||
@Enumerated(EnumType.STRING) @Getter @Setter private BubbleNetworkState state = created; | |||
@@ -214,7 +214,7 @@ public class AuthResource { | |||
final ValidationResult errors = request.validateEmail(); | |||
if (errors.isValid()) { | |||
final Account existing = accountDAO.findByEmail(request.getEmail()); | |||
if (existing != null) errors.addViolation("err.name.registered", "Name is already registered: ", request.getEmail()); | |||
if (existing != null) errors.addViolation("err.email.registered", "Email is already registered: ", request.getEmail()); | |||
} | |||
final ConstraintViolationBean passwordViolation = validatePassword(request.getPassword()); | |||
@@ -45,6 +45,7 @@ import static bubble.model.cloud.BubbleNetwork.validateHostname; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.string.ValidationRegexes.HOST_PATTERN; | |||
import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches; | |||
import static org.cobbzilla.wizard.model.NamedEntity.NAME_MAXLEN; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@Slf4j | |||
@@ -144,7 +145,18 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
} | |||
} | |||
} else { | |||
validateName(request, errors); | |||
if (!request.hasNickname()) { | |||
if (request.hasName()) { | |||
request.setNickname(request.getName()); | |||
} else { | |||
errors.addViolation("err.name.required"); | |||
} | |||
} | |||
if (request.hasNickname() && request.getNickname().length() > NAME_MAXLEN) { | |||
errors.addViolation("err.name.tooLong"); | |||
} | |||
// assign a random name for the network | |||
request.setName(newNetworkName()); | |||
} | |||
log.info("setReferences: after calling validateName, request.name="+request.getName()); | |||
@@ -38,10 +38,7 @@ import org.cobbzilla.wizard.cache.redis.HasRedisConfiguration; | |||
import org.cobbzilla.wizard.cache.redis.RedisConfiguration; | |||
import org.cobbzilla.wizard.client.ApiClientBase; | |||
import org.cobbzilla.wizard.server.RestServerHarness; | |||
import org.cobbzilla.wizard.server.config.HasDatabaseConfiguration; | |||
import org.cobbzilla.wizard.server.config.LegalInfo; | |||
import org.cobbzilla.wizard.server.config.PgRestServerConfiguration; | |||
import org.cobbzilla.wizard.server.config.RecaptchaConfig; | |||
import org.cobbzilla.wizard.server.config.*; | |||
import org.cobbzilla.wizard.util.ClasspathScanner; | |||
import org.springframework.context.annotation.Bean; | |||
import org.springframework.context.annotation.Configuration; | |||
@@ -87,6 +84,7 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||
public static final String TAG_SSL_PORT = "sslPort"; | |||
public static final String TAG_PROMO_CODE_POLICY = "promoCodePolicy"; | |||
public static final String TAG_REQUIRE_SEND_METRICS = "requireSendMetrics"; | |||
public static final String TAG_SUPPORT = "support"; | |||
public static final String TAG_RESTORE_MODE = "isInRestoringStatus"; | |||
public static final String DEFAULT_LOCAL_STORAGE_DIR = HOME_DIR + "/.bubble_local_storage"; | |||
@@ -296,7 +294,8 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||
{TAG_LOCKED, accountDAO.locked()}, | |||
{TAG_LAUNCH_LOCK, isSageLauncher() || thisNetwork == null ? null : thisNetwork.launchLock()}, | |||
{TAG_RESTORE_MODE, thisNode.wasRestored()}, | |||
{TAG_SSL_PORT, getDefaultSslPort()} | |||
{TAG_SSL_PORT, getDefaultSslPort()}, | |||
{TAG_SUPPORT, getSupport()} | |||
})); | |||
} | |||
return publicSystemConfigs.get(); | |||
@@ -52,7 +52,8 @@ public class StandardAccountMessageService implements AccountMessageService { | |||
final Account account = accountDAO.findByUuid(accountUuid); | |||
AccountPolicy policy = policyDAO.findSingleByAccount(accountUuid); | |||
if (policy == null) { | |||
policy = policyDAO.create(new AccountPolicy().setAccount(accountUuid)); | |||
log.warn("send("+message+"): no policy for account"); | |||
return false; | |||
} | |||
final List<AccountContact> contacts = policy.getAllowedContacts(message); | |||
if (contacts.isEmpty()) { | |||
@@ -136,9 +137,11 @@ public class StandardAccountMessageService implements AccountMessageService { | |||
if (account == null) account = accountDAO.findByUuid(approval.getAccount()); | |||
final AccountMessageApprovalStatus approvalStatus = messageDAO.requestApproved(account, approval, token, data); | |||
if (approvalStatus == AccountMessageApprovalStatus.ok_confirmed) { | |||
final AccountMessage request = messageDAO.findOperationRequest(approval); | |||
if (request == null) throw invalidEx("err.approvalToken.invalid", "Request could not be found for approval: "+approval); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid()); | |||
final AccountMessage confirm = messageDAO.create(new AccountMessage(approval).setMessageType(AccountMessageType.confirmation)); | |||
approval.setRequest(messageDAO.findOperationRequest(approval)); | |||
approval.setRequest(request); | |||
approval.setRequestContact(policy.findContactByUuid(approval.getRequest().getContact())); | |||
getCompletionHandler(approval).confirm(approval, data); | |||
@@ -158,6 +161,7 @@ public class StandardAccountMessageService implements AccountMessageService { | |||
throw invalidEx("err.approvalToken.invalid", "Approval cannot proceed: "+approvalStatus, approvalStatus.name()); | |||
} | |||
@Getter(lazy=true) private final AccountMessageCompletionHandler accountWelcomeHandler = configuration.autowire(new AccountVerifyHandler()); | |||
@Getter(lazy=true) private final AccountMessageCompletionHandler accountLoginHandler = configuration.autowire(new AccountLoginHandler()); | |||
@Getter(lazy=true) private final AccountMessageCompletionHandler accountPasswordHandler = configuration.autowire(new AccountPasswordHandler()); | |||
@Getter(lazy=true) private final AccountMessageCompletionHandler accountVerifyHandler = configuration.autowire(new AccountVerifyHandler()); | |||
@@ -170,6 +174,7 @@ public class StandardAccountMessageService implements AccountMessageService { | |||
@Getter(lazy=true) private final Map<String, AccountMessageCompletionHandler> confirmationHandlers = initConfirmationHandlers(); | |||
private HashMap<String, AccountMessageCompletionHandler> initConfirmationHandlers() { | |||
final HashMap<String, AccountMessageCompletionHandler> handlers = new HashMap<>(); | |||
handlers.put(ActionTarget.account+":"+AccountAction.welcome, getAccountWelcomeHandler()); | |||
handlers.put(ActionTarget.account+":"+AccountAction.login, getAccountLoginHandler()); | |||
handlers.put(ActionTarget.account+":"+AccountAction.password, getAccountPasswordHandler()); | |||
handlers.put(ActionTarget.account+":"+AccountAction.verify, getAccountVerifyHandler()); | |||
@@ -1 +1 @@ | |||
bubble.version=0.11.1 | |||
bubble.version=0.11.2 |
@@ -71,6 +71,10 @@ errorApi: | |||
key: {{ERRBIT_KEY}} | |||
env: {{ERRBIT_ENV}} | |||
support: | |||
email: '{{SUPPORT_EMAIL}}' | |||
site: '{{SUPPORT_SITE}}' | |||
localNotificationStrategy: {{#exists BUBBLE_LOCAL_NOTIFY}}{{BUBBLE_LOCAL_NOTIFY}}{{else}}inline{{/exists}} | |||
letsencryptEmail: {{LETSENCRYPT_EMAIL}} | |||
@@ -2,8 +2,10 @@ Hello {{account.name}}, | |||
Welcome to Bubble! | |||
You can login with your username and password here: {{publicUri}}/login | |||
Please confirm your email address using this link: | |||
If you have any questions, please contact your Bubble Administrator. | |||
{{publicUri}}/me/action?approve={{confirmationToken}} | |||
{{#if configuration.hasSupportInfo}}If you have any questions or need help, please {{#if configuration.support.hasEmailAndSite}}contact {{configuration.support.email}} or visit {{configuration.support.site}}{{else}}{{#if configuration.support.hasEmail}}contact {{configuration.support.email}}{{else}}visit {{configuration.support.site}}{{/if}}{{/if}}{{/if}} | |||
Happy bubbling! |
@@ -3,24 +3,10 @@ Hello {{account.name}}, | |||
Contact information has been added to your account named '{{account.name}}' on {{network.networkDomain}} | |||
{{#string_compare contact.uuid '==' message.contact}}{{contact.type}} - {{contact.info}}{{else}}{{message.requestContact.type}}{{#if message.requestContact.isSms}}{{message.requestContact.info}}{{/if}}{{/string_compare}} {{#if message.requestContact.nick}}({{message.requestContact.nick}}){{/if}} | |||
If you did not make this request or would like to cancel this request, please click this link: | |||
{{publicUri}}/me/action?deny={{confirmationToken}} | |||
{{#string_compare contact.uuid '==' message.contact}} | |||
If you DID make this request and are ready to verify this contact information, click the link below, | |||
or enter the value {{confirmationToken}} when the verification code is requested. | |||
To confirm this contact information, follow this link: | |||
{{publicUri}}/me/action?approve={{confirmationToken}} | |||
{{/string_compare}} | |||
Thank you for using Bubble! |
@@ -184,8 +184,8 @@ label_field_nodes_ip6=IPv6 | |||
# New Network page | |||
message_no_contacts=No authorized contact info found | |||
link_label_no_contacts=Add an email address or SMS-enabled phone number | |||
message_no_verified_contacts=No verified contact info found | |||
message_no_verified_contacts_subtext=Before creating your first Bubble, please verify the contact information shown below | |||
message_no_verified_contacts=Please verify your email address | |||
message_no_verified_contacts_subtext=Check your email and follow the link to verify your email address | |||
form_title_new_network=New Bubble | |||
field_label_network_name=Name | |||
@@ -742,7 +742,7 @@ err.tgzB64.invalid.missingTasksMainYml=No tasks/main.yml file found for role in | |||
err.tgzB64.invalid.writingToStorage=Error writing tgz to storage | |||
err.tgzB64.invalid.readingFromStorage=Error reading tgz from storage | |||
err.tgzB64.required=tgzB64 is required | |||
err.totpKey.length=TOTP key is required | |||
err.totpKey.length=TOTP key is too long | |||
err.type.notVerifiable=Type is not verifiable | |||
err.type.invalid=Type is invalid | |||
err.type.required=Type is required | |||
@@ -12,6 +12,13 @@ message_undefined=undefined | |||
# Display of percent values has localized variations | |||
label_percent={{percent}}% | |||
# Support | |||
title_support=Bubble Support | |||
support_preamble=To get help with Bubble: | |||
support_site_link=Visit our Support Website | |||
support_email_link=Send us an email | |||
support_not_available=Sorry, no support options are available | |||
# Legal page links | |||
title_legal_topics=Legal Stuff | |||
legal_topics=terms,privacy,source,license,3rdParty_licenses | |||
@@ -264,6 +271,7 @@ form_title_login=Login | |||
form_title_restore=Restore | |||
field_label_username=Username | |||
field_label_password=Password | |||
field_label_password_guidance=Password must be at least 8 characters long.<br/>Password must contain at least one letter, one number, and one special character. | |||
field_label_confirm_password=Confirm Password | |||
field_label_unlock_key=Unlock Key | |||
field_label_restore_short_key=Short Key (6 letters) | |||
@@ -1,2 +1,2 @@ | |||
{{network.networkDomain}}: Welcome to Bubble, {{account.name}}! | |||
Login here: {{publicUri}}/login | |||
Verify your email: {{publicUri}}/me/action?approve={{confirmationToken}} |
@@ -1 +1,2 @@ | |||
{{network.networkDomain}}: {{#string_compare contact.uuid '==' message.contact}}SMS Phone added: {{contact.info}} - To approve, use code {{confirmationToken}} or use: {{publicUri}}/me/action?approve={{confirmationToken}} - To deny: {{publicUri}}/me/action?deny={{confirmationToken}}{{else}}{{#if message.requestContact.isEmail}}Email added: {{message.requestContact.info}}{{else}}Auth added: {{message.requestContact.type}}{{/if}} - To deny: {{publicUri}}/me/action?deny={{confirmationToken}}{{/string_compare}} | |||
{{network.networkDomain}}: {{#string_compare contact.uuid '==' message.contact}}SMS Phone added: {{contact.info}} | |||
To approve: {{publicUri}}/me/action?approve={{confirmationToken}}{{else}}{{#if message.requestContact.isEmail}}Email added: {{message.requestContact.info}}{{else}}Auth added: {{message.requestContact.type}}{{/if}}{{/string_compare}} |
@@ -195,7 +195,7 @@ | |||
}, | |||
{ | |||
"comment": "new session, register new user, fails because username is already used", | |||
"comment": "new session, register new user, fails because email is already used", | |||
"request": { | |||
"session": "new", | |||
"uri": "auth/register", | |||
@@ -207,7 +207,7 @@ | |||
}, | |||
"response": { | |||
"status": 422, | |||
"check": [ {"condition": "json.has('err.name.registered')"} ] | |||
"check": [ {"condition": "json.has('err.email.registered')"} ] | |||
} | |||
}, | |||
@@ -3,7 +3,55 @@ | |||
"comment": "create new account and login", | |||
"include": "new_account", | |||
"params": { | |||
"email": "user-multifactor_auth@example.com" | |||
"email": "user-multifactor_auth_registered@example.com" | |||
} | |||
}, | |||
{ | |||
"comment": "resend verification message for registration email", | |||
"request": { | |||
"uri": "users/{{userAccount.name}}/policy/contacts/verify", | |||
"entity": { | |||
"type": "email", | |||
"info": "user-multifactor_auth_registered@example.com" | |||
} | |||
} | |||
}, | |||
{ | |||
"before": "sleep 1s", | |||
"comment": "as root, check inbox for email verification message for registration email", | |||
"request": { | |||
"session": "rootSession", | |||
"uri": "debug/inbox/email/user-multifactor_auth_registered@example.com?action=verify" | |||
}, | |||
"response": { | |||
"store": "userInbox", | |||
"check": [ | |||
{"condition": "'{{json.[0].ctx.message.messageType}}' == 'request'"}, | |||
{"condition": "'{{json.[0].ctx.message.action}}' == 'verify'"}, | |||
{"condition": "'{{json.[0].ctx.message.target}}' == 'account'"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "as user, approve email verification request for registration email", | |||
"request": { | |||
"session": "userSession", | |||
"uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", | |||
"entity": [{"name": "account", "value": "user-multifactor_auth_registered@example.com"}] | |||
} | |||
}, | |||
{ | |||
"comment": "add second email to account policy", | |||
"request": { | |||
"uri": "users/{{userAccount.name}}/policy/contacts", | |||
"entity": { | |||
"type": "email", | |||
"info": "user-multifactor_auth@example.com" | |||
} | |||
} | |||
}, | |||
@@ -14,9 +62,9 @@ | |||
"store": "policy", | |||
"check": [ | |||
{"condition": "json.getAccountContacts() != null"}, | |||
{"condition": "json.getAccountContacts().length == 1"}, | |||
{"condition": "!json.getAccountContacts()[0].authFactor()"}, | |||
{"condition": "!json.getAccountContacts()[0].verified()"} | |||
{"condition": "json.getAccountContacts().length == 2"}, | |||
{"condition": "!json.getAccountContacts()[1].authFactor()"}, | |||
{"condition": "!json.getAccountContacts()[1].verified()"} | |||
] | |||
} | |||
}, | |||
@@ -72,7 +120,7 @@ | |||
"request": { | |||
"session": "userSession", | |||
"uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", | |||
"entity": [{"name": "account", "value": "user-multifactor_auth@example.com"}] | |||
"entity": [{"name": "account", "value": "user-multifactor_auth_registered@example.com"}] | |||
} | |||
}, | |||
@@ -84,9 +132,9 @@ | |||
"store": "policy", | |||
"check": [ | |||
{"condition": "json.getAccountContacts() != null"}, | |||
{"condition": "json.getAccountContacts().length == 1"}, | |||
{"condition": "!json.getAccountContacts()[0].authFactor()"}, | |||
{"condition": "json.getAccountContacts()[0].verified()"} | |||
{"condition": "json.getAccountContacts().length == 2"}, | |||
{"condition": "!json.getAccountContacts()[1].authFactor()"}, | |||
{"condition": "json.getAccountContacts()[1].verified()"} | |||
] | |||
} | |||
}, | |||
@@ -117,8 +165,8 @@ | |||
"store": "policy", | |||
"check": [ | |||
{"condition": "json.getAccountContacts() != null"}, | |||
{"condition": "json.getAccountContacts().length == 1"}, | |||
{"condition": "json.getAccountContacts()[0].requiredAuthFactor()"} | |||
{"condition": "json.getAccountContacts().length == 2"}, | |||
{"condition": "json.getAccountContacts()[1].requiredAuthFactor()"} | |||
] | |||
} | |||
}, | |||
@@ -170,7 +218,7 @@ | |||
"request": { | |||
"session": "userSession", | |||
"uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", | |||
"entity": [{"name": "account", "value": "user-multifactor_auth@example.com"}] | |||
"entity": [{"name": "account", "value": "user-multifactor_auth_registered@example.com"}] | |||
}, | |||
"response": { | |||
"sessionName": "userSession", | |||
@@ -194,9 +242,9 @@ | |||
"store": "policy", | |||
"check": [ | |||
{"condition": "json.getAccountContacts() != null"}, | |||
{"condition": "json.getAccountContacts().length == 2"}, | |||
{"condition": "json.getAccountContacts()[0].requiredAuthFactor()"}, | |||
{"condition": "json.getAccountContacts()[1].requiredAuthFactor()"} | |||
{"condition": "json.getAccountContacts().length == 3"}, | |||
{"condition": "json.getAccountContacts()[1].requiredAuthFactor()"}, | |||
{"condition": "json.getAccountContacts()[2].requiredAuthFactor()"} | |||
] | |||
} | |||
}, | |||
@@ -248,7 +296,7 @@ | |||
"request": { | |||
"session": "userSession", | |||
"uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", | |||
"entity": [{"name": "account", "value": "user-multifactor_auth@example.com"}] | |||
"entity": [{"name": "account", "value": "user-multifactor_auth_registered@example.com"}] | |||
}, | |||
"response": { | |||
"status": 422, | |||
@@ -262,7 +310,7 @@ | |||
"session": "userSession", | |||
"uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", | |||
"entity": [ | |||
{"name": "account", "value": "user-multifactor_auth@example.com"}, | |||
{"name": "account", "value": "user-multifactor_auth_registered@example.com"}, | |||
{"name": "totpToken", "value": "{{authenticator_token authenticator.totpKey}}"} | |||
] | |||
}, | |||
@@ -308,8 +356,8 @@ | |||
"store": "policy", | |||
"check": [ | |||
{"condition": "json.getAccountContacts() != null"}, | |||
{"condition": "json.getAccountContacts().length == 1"}, | |||
{"condition": "json.getAccountContacts()[0].requiredAuthFactor()"} | |||
{"condition": "json.getAccountContacts().length == 2"}, | |||
{"condition": "json.getAccountContacts()[1].requiredAuthFactor()"} | |||
] | |||
} | |||
}, | |||
@@ -372,7 +420,7 @@ | |||
"request": { | |||
"session": "userSession", | |||
"uri": "auth/approve/{{smsInbox.[0].ctx.confirmationToken}}", | |||
"entity": [{"name": "account", "value": "user-multifactor_auth@example.com"}] | |||
"entity": [{"name": "account", "value": "user-multifactor_auth_registered@example.com"}] | |||
} | |||
}, | |||
@@ -384,10 +432,10 @@ | |||
"store": "policy", | |||
"check": [ | |||
{"condition": "json.getAccountContacts() != null"}, | |||
{"condition": "json.getAccountContacts().length == 2"}, | |||
{"condition": "json.getAccountContacts()[1].getType().name() == 'sms'"}, | |||
{"condition": "!json.getAccountContacts()[1].authFactor()"}, | |||
{"condition": "json.getAccountContacts()[1].verified()"} | |||
{"condition": "json.getAccountContacts().length == 3"}, | |||
{"condition": "json.getAccountContacts()[2].getType().name() == 'sms'"}, | |||
{"condition": "!json.getAccountContacts()[2].authFactor()"}, | |||
{"condition": "json.getAccountContacts()[2].verified()"} | |||
] | |||
} | |||
}, | |||
@@ -459,7 +507,7 @@ | |||
"request": { | |||
"session": "userSession", | |||
"uri": "auth/approve/{{smsInbox.[0].ctx.confirmationToken}}", | |||
"entity": [{"name": "account", "value": "user-multifactor_auth@example.com"}] | |||
"entity": [{"name": "account", "value": "user-multifactor_auth_registered@example.com"}] | |||
}, | |||
"response": { | |||
"status": 422, | |||
@@ -473,7 +521,7 @@ | |||
"session": "userSession", | |||
"uri": "auth/approve/{{smsInbox.[0].ctx.confirmationToken}}", | |||
"entity": [ | |||
{"name": "account", "value": "user-multifactor_auth@example.com"}, | |||
{"name": "account", "value": "user-multifactor_auth_registered@example.com"}, | |||
{"name": "totpToken", "value": "{{authenticator_token authenticator.totpKey}}"} | |||
] | |||
}, | |||
@@ -505,8 +553,8 @@ | |||
"store": "policy", | |||
"check": [ | |||
{"condition": "json.getAccountContacts() != null"}, | |||
{"condition": "json.getAccountContacts().length == 1"}, | |||
{"condition": "json.getAccountContacts()[0].getType().name() == 'sms'"} | |||
{"condition": "json.getAccountContacts().length == 2"}, | |||
{"condition": "json.getAccountContacts()[1].getType().name() == 'sms'"} | |||
] | |||
} | |||
}, | |||
@@ -550,7 +598,7 @@ | |||
"request": { | |||
"session": "userSession", | |||
"uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", | |||
"entity": [{"name": "account", "value": "user-multifactor_auth@example.com"}] | |||
"entity": [{"name": "account", "value": "user-multifactor_auth_registered@example.com"}] | |||
} | |||
}, | |||
@@ -619,7 +667,7 @@ | |||
"request": { | |||
"session": "userSession", | |||
"uri": "auth/approve/{{userInbox.[0].ctx.confirmationToken}}", | |||
"entity": [{"name": "account", "value": "user-multifactor_auth@example.com"}] | |||
"entity": [{"name": "account", "value": "user-multifactor_auth_registered@example.com"}] | |||
}, | |||
"response": { | |||
"sessionName": "userSession", | |||
@@ -736,9 +784,9 @@ | |||
"store": "policy", | |||
"check": [ | |||
{"condition": "json.getAccountContacts() != null"}, | |||
{"condition": "json.getAccountContacts().length == 2"}, | |||
{"condition": "!json.getAccountContacts()[0].getInfo().startsWith('bar')"}, | |||
{"condition": "!json.getAccountContacts()[1].getInfo().startsWith('bar')"} | |||
{"condition": "json.getAccountContacts().length == 3"}, | |||
{"condition": "!json.getAccountContacts()[1].getInfo().startsWith('bar')"}, | |||
{"condition": "!json.getAccountContacts()[2].getInfo().startsWith('bar')"} | |||
] | |||
} | |||
} |
@@ -1 +1 @@ | |||
Subproject commit 62a5a97abc1e28ba23874394eef34ff03e828a1f | |||
Subproject commit 9158e9660c4382363713b115ddb46637af99558e |
@@ -1 +1 @@ | |||
Subproject commit b1273943835c8002c7aae24b880f2038fa71e73c | |||
Subproject commit e5d7abc4b58a339a5da90fcfe53ba21c20e40c75 |
@@ -1 +1 @@ | |||
Subproject commit 3b1649f05991edaf82d117ecf3080e46a16b63b4 | |||
Subproject commit c8c831c10c9d31957cc99f72662530bcb38f9af8 |