Merge branch 'wip-customer' into private-event-sourcing-examples-38

* wip-customer:
  Form validation for Sign In & Sign Up
  Error reporting for Sign In & Sign Up
  - shouldCreateAccountsAndTransferMoney fix
  Password on login & registration
  - fixed tests fix issue #24, fix issue #26, fix issue #27, fix issue #28
  - added password to CustomerInfo - added unique email constraint to CustomerQuerySide - updated authorization logic
  removed transferStates from AccountInfo cannot reproduce issue #37
This commit is contained in:
Andrew Revinsky (DART)
2016-09-15 17:06:20 +03:00
55 changed files with 2105 additions and 2141 deletions

View File

@@ -18,7 +18,6 @@ public class AccountInfo {
private long balance;
private List<AccountChangeInfo> changes;
private Map<String, AccountTransactionInfo> transactions;
private Map<String, TransferState> transferStates;
private String version;
private Date date;
@@ -77,12 +76,4 @@ public class AccountInfo {
public Date getDate() {
return date;
}
public Map<String, TransferState> getTransferStates() {
return transferStates;
}
public void setTransferStates(Map<String, TransferState> transferStates) {
this.transferStates = transferStates;
}
}

View File

@@ -59,10 +59,12 @@ public class AccountInfoUpdateService {
public void addTransaction(String accountId, AccountTransactionInfo ti) {
System.out.println("Start addTransaction for: "+ti.toString());
mongoTemplate.upsert(new Query(where("id").is(accountId)),
new Update().
set("transactions." + ti.getTransactionId(), ti),
AccountInfo.class);
System.out.println("End addTransaction for: "+ti.toString());
}
@@ -76,9 +78,11 @@ public class AccountInfoUpdateService {
}
public void updateTransactionStatus(String accountId, String transactionId, TransferState status) {
System.out.println("Start updateTransactionStatus "+accountId +" "+transactionId+" "+status);
mongoTemplate.upsert(new Query(where("id").is(accountId)),
new Update().
set("transferStates." + transactionId, status),
set("transactions." + transactionId + ".status", status),
AccountInfo.class);
System.out.println("End updateTransactionStatus "+accountId +" "+transactionId+" "+status);
}
}

View File

@@ -15,8 +15,6 @@ public class AccountQueryService {
if (account == null)
throw new AccountNotFoundException(accountId);
else
if(account.getTransferStates()!=null)
account.getTransactions().stream().forEach(ati -> ati.setStatus(account.getTransferStates().get(ati.getTransactionId())));
return account;
}

View File

@@ -41,7 +41,7 @@ public class AuthController {
@RequestMapping(value = "/login", method = POST)
public ResponseEntity<QuerySideCustomer> doAuth(@RequestBody @Valid AuthRequest request) throws IOException {
QuerySideCustomer customer = customerAuthService.findByEmail(request.getEmail());
QuerySideCustomer customer = customerAuthService.findByEmailAndPassword(request.getEmail(), request.getPassword());
Token token = tokenService.allocateToken(objectMapper.writeValueAsString(new User(request.getEmail())));
return ResponseEntity.status(HttpStatus.OK).header("access-token", token.getKey())

View File

@@ -12,11 +12,15 @@ public class AuthRequest {
@Email
private String email;
@NotBlank
private String password;
public AuthRequest() {
}
public AuthRequest(String email) {
public AuthRequest(String email, String password) {
this.email = email;
this.password = password;
}
public String getEmail() {
@@ -26,4 +30,12 @@ public class AuthRequest {
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -1,5 +1,6 @@
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.QuerySideCustomer;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.filter.StatelessAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -18,6 +19,7 @@ import org.springframework.security.core.token.KeyBasedPersistenceTokenService;
import org.springframework.security.core.token.TokenService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import java.security.SecureRandom;
@@ -50,15 +52,13 @@ public class AuthConfiguration extends WebSecurityConfigurerAdapter {
@Override
public UserDetailsService userDetailsServiceBean() {
return email -> {
/* QuerySideCustomer customer = customerAuthService.findByEmail(email);
if (customer != null) {
return new User(email);
} else {
throw new UsernameNotFoundException(String.format("could not find the customer '%s'", email));
}*/
//authorize everyone with basic authentication
return new User(email, "", true, true, true, true,
AuthorityUtils.createAuthorityList("USER"));
QuerySideCustomer customer = customerAuthService.findByEmail(email);
if (customer != null) {
return new User(email, customer.getPassword(), true, true, true, true,
AuthorityUtils.createAuthorityList("USER"));
} else {
throw new UsernameNotFoundException(String.format("could not find the customer '%s'", email));
}
};
}

View File

@@ -8,4 +8,6 @@ import java.util.List;
interface CustomerAuthRepository extends MongoRepository<QuerySideCustomer, String> {
List<QuerySideCustomer> findByEmail(String email);
List<QuerySideCustomer> findByEmailAndPassword(String email, String password);
}

View File

@@ -19,9 +19,14 @@ public class CustomerAuthService {
List<QuerySideCustomer> customers = customerAuthRepository.findByEmail(email);
if (customers.isEmpty())
throw new EmptyResultDataAccessException(1);
//TODO: add unique email constraint
/* else if(customers.size()>1)
throw new IncorrectResultSizeDataAccessException(1, customers.size());*/
else
return customers.get(0);
}
public QuerySideCustomer findByEmailAndPassword(String email, String password) {
List<QuerySideCustomer> customers = customerAuthRepository.findByEmailAndPassword(email, password);
if (customers.isEmpty())
throw new EmptyResultDataAccessException(1);
else
return customers.get(0);
}

View File

@@ -3,6 +3,7 @@ apply plugin: 'java'
dependencies {
compile "commons-lang:commons-lang:2.6"
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
compile "org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion"
testCompile group: 'junit', name: 'junit', version: '4.11'
}

View File

@@ -13,6 +13,8 @@ public class CustomerInfo {
@NotNull
protected String email;
@NotNull
protected String password;
@NotNull
protected String ssn;
@NotNull
protected String phoneNumber;
@@ -21,9 +23,10 @@ public class CustomerInfo {
public CustomerInfo() {
}
public CustomerInfo(Name name, String email, String ssn, String phoneNumber, Address address) {
public CustomerInfo(Name name, String email, String password, String ssn, String phoneNumber, Address address) {
this.name = name;
this.email = email;
this.password = password;
this.ssn = ssn;
this.phoneNumber = phoneNumber;
this.address = address;
@@ -37,6 +40,10 @@ public class CustomerInfo {
return email;
}
public String getPassword() {
return password;
}
public String getSsn() {
return ssn;
}

View File

@@ -1,14 +1,19 @@
package net.chrisrichardson.eventstore.javaexamples.banking.common.customers;
import org.springframework.data.mongodb.core.index.Indexed;
import java.util.Map;
/**
* Created by Main on 05.02.2016.
*/
public class QuerySideCustomer {
private String id;
private Name name;
@Indexed(unique=true)
private String email;
private String password;
private String ssn;
private String phoneNumber;
private Address address;
@@ -17,10 +22,11 @@ public class QuerySideCustomer {
public QuerySideCustomer() {
}
public QuerySideCustomer(String id, Name name, String email, String ssn, String phoneNumber, Address address, Map<String, ToAccountInfo> toAccounts) {
public QuerySideCustomer(String id, Name name, String email, String password, String ssn, String phoneNumber, Address address, Map<String, ToAccountInfo> toAccounts) {
this.id = id;
this.name = name;
this.email = email;
this.password = password;
this.ssn = ssn;
this.phoneNumber = phoneNumber;
this.address = address;
@@ -51,6 +57,14 @@ public class QuerySideCustomer {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSsn() {
return ssn;
}

View File

@@ -34,6 +34,7 @@ public class CustomerInfoUpdateService {
querySideCustomerRepository.save(new QuerySideCustomer(id,
customerInfo.getName(),
customerInfo.getEmail(),
customerInfo.getPassword(),
customerInfo.getSsn(),
customerInfo.getPhoneNumber(),
customerInfo.getAddress(),

View File

@@ -14,6 +14,7 @@ dependencies {
testCompile project(":testutil")
testCompile project(":customers-command-side-service")
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
}
test {

View File

@@ -48,8 +48,10 @@ public class CustomersQuerySideServiceIntegrationTest {
final CustomerResponse customerResponse = restTemplate.postForEntity(baseUrl("/customers"), customerInfo, CustomerResponse.class).getBody();
final String customerId = customerResponse.getId();
final String email = customerResponse.getCustomerInfo().getEmail();
final String password = customerResponse.getCustomerInfo().getPassword();
customersTestUtils.assertCustomerResponse(customerId, customerInfo);
customersTestUtils.assertCustomerResponse(customerId, email, password, customerInfo);
}
}

View File

@@ -1,6 +1,9 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.customers.CustomersCommandSideWebConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.CustomersQuerySideWebConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
@@ -14,7 +17,7 @@ import java.util.Arrays;
import java.util.List;
@Configuration
@Import({CustomersCommandSideServiceConfiguration.class, CustomersQuerySideServiceConfiguration.class, AuthConfiguration.class})
@Import({CustomersCommandSideWebConfiguration.class, CustomersQuerySideWebConfiguration.class, EventuateJdbcEventStoreConfiguration.class, AuthConfiguration.class})
@EnableAutoConfiguration
public class CustomersQuerySideServiceTestConfiguration {

View File

@@ -53,19 +53,14 @@ public class BankingAuthTest {
final CustomerResponse customerResponse = restTemplate.postForEntity(baseUrl("/customers"), customerInfo, CustomerResponse.class).getBody();
final String customerId = customerResponse.getId();
final String password = customerResponse.getCustomerInfo().getPassword();
Assert.assertNotNull(customerId);
Assert.assertEquals(customerInfo, customerResponse.getCustomerInfo());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
customersTestUtils.assertCustomerResponse(customerId, email, password, customerInfo);
customersTestUtils.assertCustomerResponse(customerId, customerInfo);
AuthRequest authRequest = new AuthRequest(email);
AuthRequest authRequest = new AuthRequest(email, password);
final QuerySideCustomer loginQuerySideCustomer = restTemplate.postForEntity(baseUrl("/login"), authRequest, QuerySideCustomer.class).getBody();

View File

@@ -27,6 +27,13 @@ public abstract class AbstractRestAPITest {
//@Test
public void shouldCreateAccountsAndTransferMoney() {
CustomerInfo customerInfo = generateCustomerInfo();
final CustomerResponse customerResponse = getRestTemplate().postForEntity(baseUrl("/customers"), customerInfo, CustomerResponse.class).getBody();
final String customerId = customerResponse.getId();
final String email = customerResponse.getCustomerInfo().getEmail();
final String password = customerResponse.getCustomerInfo().getPassword();
BigDecimal initialFromAccountBalance = new BigDecimal(500);
BigDecimal initialToAccountBalance = new BigDecimal(100);
BigDecimal amountToTransfer = new BigDecimal(150);
@@ -35,36 +42,36 @@ public abstract class AbstractRestAPITest {
BigDecimal finalToAccountBalance = initialToAccountBalance.add(amountToTransfer);
final CreateAccountResponse fromAccount = getAuthenticatedRestTemplate().postForEntity(baseUrl("/accounts"),
new CreateAccountRequest("00000000-00000000", "My 1 Account", "", initialFromAccountBalance),
CreateAccountResponse.class);
new CreateAccountRequest(customerId, "My 1 Account", "", initialFromAccountBalance),
CreateAccountResponse.class, email, password);
final String fromAccountId = fromAccount.getAccountId();
CreateAccountResponse toAccount = getAuthenticatedRestTemplate().postForEntity(baseUrl("/accounts"),
new CreateAccountRequest("00000000-00000000", "My 2 Account", "", initialToAccountBalance),
CreateAccountResponse.class);
new CreateAccountRequest(customerId, "My 2 Account", "", initialToAccountBalance),
CreateAccountResponse.class, email, password);
String toAccountId = toAccount.getAccountId();
Assert.assertNotNull(fromAccountId);
Assert.assertNotNull(toAccountId);
assertAccountBalance(fromAccountId, initialFromAccountBalance);
assertAccountBalance(toAccountId, initialToAccountBalance);
assertAccountBalance(email, password, fromAccountId, initialFromAccountBalance);
assertAccountBalance(email, password, toAccountId, initialToAccountBalance);
final CreateMoneyTransferResponse moneyTransfer = getAuthenticatedRestTemplate().postForEntity(baseUrl("/transfers"),
new CreateMoneyTransferRequest(fromAccountId, toAccountId, amountToTransfer, ""),
CreateMoneyTransferResponse.class);
CreateMoneyTransferResponse.class, email, password);
assertAccountBalance(fromAccountId, finalFromAccountBalance);
assertAccountBalance(toAccountId, finalToAccountBalance);
assertAccountBalance(email, password, fromAccountId, finalFromAccountBalance);
assertAccountBalance(email, password, toAccountId, finalToAccountBalance);
eventually(
new Producer<AccountHistoryResponse>() {
@Override
public CompletableFuture<AccountHistoryResponse> produce() {
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/accounts/" + fromAccountId + "/history"),
AccountHistoryResponse.class));
AccountHistoryResponse.class, email, password));
}
},
new Verifier<AccountHistoryResponse>() {
@@ -92,28 +99,30 @@ public abstract class AbstractRestAPITest {
final CustomerResponse customerResponse = getRestTemplate().postForEntity(baseUrl("/customers"), customerInfo, CustomerResponse.class).getBody();
final String customerId = customerResponse.getId();
final String email = customerResponse.getCustomerInfo().getEmail();
final String password = customerResponse.getCustomerInfo().getPassword();
Assert.assertNotNull(customerId);
assertEquals(customerInfo, customerResponse.getCustomerInfo());
getCustomersTestUtils().assertCustomerResponse(customerId, customerInfo);
getCustomersTestUtils().assertCustomerResponse(customerId, email, password, customerInfo);
final CreateAccountResponse account = getAuthenticatedRestTemplate().postForEntity(baseUrl("/accounts"),
new CreateAccountRequest(customerId, "My 1 Account", "", initialFromAccountBalance),
CreateAccountResponse.class);
CreateAccountResponse.class, email, password);
final String accountId = account.getAccountId();
Assert.assertNotNull(accountId);
assertAccountBalance(accountId, initialFromAccountBalance);
assertAccountBalance(email, password, accountId, initialFromAccountBalance);
eventually(
new Producer<GetAccountsResponse>() {
@Override
public CompletableFuture<GetAccountsResponse> produce() {
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/customers/"+customerId+"/accounts"),
GetAccountsResponse.class));
GetAccountsResponse.class, email, password));
}
},
new Verifier<GetAccountsResponse>() {
@@ -148,33 +157,35 @@ public abstract class AbstractRestAPITest {
final CustomerResponse customerResponse = getRestTemplate().postForEntity(baseUrl("/customers"), customerInfo, CustomerResponse.class).getBody();
final String customerId = customerResponse.getId();
final String email = customerResponse.getCustomerInfo().getEmail();
final String password = customerResponse.getCustomerInfo().getPassword();
Assert.assertNotNull(customerId);
assertEquals(customerInfo, customerResponse.getCustomerInfo());
getCustomersTestUtils().assertCustomerResponse(customerId, customerInfo);
getCustomersTestUtils().assertCustomerResponse(customerId, email, password, customerInfo);
ToAccountInfo toAccountInfo = generateToAccountInfo();
getAuthenticatedRestTemplate().postForEntity(baseUrl("/customers/" + customerId + "/toaccounts"),
toAccountInfo,
null);
null, email, password);
assertToAccountsContains(customerId, toAccountInfo);
assertToAccountsContains(customerId, email, password, toAccountInfo);
}
private BigDecimal toCents(BigDecimal dollarAmount) {
return dollarAmount.multiply(new BigDecimal(100));
}
private void assertAccountBalance(final String fromAccountId, final BigDecimal expectedBalanceInDollars) {
private void assertAccountBalance(final String email, final String password, final String fromAccountId, final BigDecimal expectedBalanceInDollars) {
final BigDecimal inCents = toCents(expectedBalanceInDollars);
eventually(
new Producer<GetAccountResponse>() {
@Override
public CompletableFuture<GetAccountResponse> produce() {
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/accounts/" + fromAccountId),
GetAccountResponse.class));
GetAccountResponse.class, email, password));
}
},
new Verifier<GetAccountResponse>() {
@@ -186,13 +197,13 @@ public abstract class AbstractRestAPITest {
});
}
private void assertToAccountsContains(final String customerId, final ToAccountInfo toAccountInfo) {
private void assertToAccountsContains(final String customerId, final String email, final String password, final ToAccountInfo toAccountInfo) {
eventually(
new Producer<QuerySideCustomer>() {
@Override
public CompletableFuture<QuerySideCustomer> produce() {
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/customers/" + customerId),
QuerySideCustomer.class));
QuerySideCustomer.class, email, password));
}
},
new Verifier<QuerySideCustomer>() {

View File

@@ -11,19 +11,23 @@ public class AuthenticatedRestTemplate {
this.restTemplate = restTemplate;
}
public <T> T getForEntity(String url, Class<T> clazz) {
public <T> T getForEntity(String url, Class<T> clazz, String email, String password) {
return BasicAuthUtils.doBasicAuthenticatedRequest(restTemplate,
url,
HttpMethod.GET,
clazz);
clazz,
email,
password);
}
public <T> T postForEntity(String url, Object requestObject, Class<T> clazz) {
public <T> T postForEntity(String url, Object requestObject, Class<T> clazz, String email, String password) {
return BasicAuthUtils.doBasicAuthenticatedRequest(restTemplate,
url,
HttpMethod.POST,
clazz,
requestObject
requestObject,
email,
password
);
}

View File

@@ -12,10 +12,10 @@ import java.nio.charset.Charset;
*/
public class BasicAuthUtils {
public static HttpHeaders basicAuthHeaders(String username) {
public static HttpHeaders basicAuthHeaders(String username, String password) {
return new HttpHeaders() {
{
String auth = username + ":";
String auth = username + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(
auth.getBytes(Charset.forName("US-ASCII")));
String authHeader = "Basic " + new String(encodedAuth);
@@ -24,16 +24,16 @@ public class BasicAuthUtils {
};
}
public static <T> T doBasicAuthenticatedRequest(RestTemplate restTemplate, String url, HttpMethod httpMethod, Class<T> responseType) {
return doBasicAuthenticatedRequest(restTemplate, url, httpMethod, responseType, null);
public static <T> T doBasicAuthenticatedRequest(RestTemplate restTemplate, String url, HttpMethod httpMethod, Class<T> responseType, String email, String password) {
return doBasicAuthenticatedRequest(restTemplate, url, httpMethod, responseType, null, email, password);
}
public static <T> T doBasicAuthenticatedRequest(RestTemplate restTemplate, String url, HttpMethod httpMethod, Class<T> responseType, Object requestObject) {
public static <T> T doBasicAuthenticatedRequest(RestTemplate restTemplate, String url, HttpMethod httpMethod, Class<T> responseType, Object requestObject, String email, String password) {
HttpEntity httpEntity;
if (requestObject != null) {
httpEntity = new HttpEntity<>(requestObject, BasicAuthUtils.basicAuthHeaders("test_user@mail.com"));
httpEntity = new HttpEntity<>(requestObject, BasicAuthUtils.basicAuthHeaders(email, password));
} else {
httpEntity = new HttpEntity(BasicAuthUtils.basicAuthHeaders("test_user@mail.com"));
httpEntity = new HttpEntity(BasicAuthUtils.basicAuthHeaders(email, password));
}
ResponseEntity<T> responseEntity = restTemplate.exchange(url,

View File

@@ -21,13 +21,13 @@ public class CustomersTestUtils {
this.customersBaseUrl = customersBaseUrl;
}
public void assertCustomerResponse(final String customerId, final CustomerInfo customerInfo) {
public void assertCustomerResponse(final String customerId, final String email, final String password, final CustomerInfo customerInfo) {
AuthenticatedRestTemplate art = new AuthenticatedRestTemplate(restTemplate);
eventually(
new Producer<QuerySideCustomer>() {
@Override
public CompletableFuture<QuerySideCustomer> produce() {
return CompletableFuture.completedFuture(art.getForEntity(customersBaseUrl + customerId, QuerySideCustomer.class));
return CompletableFuture.completedFuture(art.getForEntity(customersBaseUrl + customerId, QuerySideCustomer.class, email, password));
}
},
new Verifier<QuerySideCustomer>() {
@@ -55,6 +55,7 @@ public class CustomersTestUtils {
return new CustomerInfo(
new Name("John", "Doe"),
email,
"simple_password",
"000-00-0000",
"1-111-111-1111",
new Address("street 1",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -12,8 +12,8 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap-theme.min.css"><link href="/style.6d7a32b1405ea1bb2bdf.css" rel="stylesheet"></head>
<body><div id="root"></div><script src="/manifest.087a5454fa0c34daf3c9.js"></script><script src="/vendor.c882d66445aebc52c21b.js"></script><script src="/style.6d7a32b1405ea1bb2bdf.js"></script><script src="/app.fcbedf54f0345474ccc1.js"></script><script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap-theme.min.css"><link href="/style.6734b6ac5d23d61b8e5f.css" rel="stylesheet"></head>
<body><div id="root"></div><script src="/manifest.9a73c590403a64f40c41.js"></script><script src="/vendor.829a6cd5501868837732.js"></script><script src="/style.6734b6ac5d23d61b8e5f.js"></script><script src="/app.4d33d25a9b5872086180.js"></script><script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
@@ -27,5 +27,5 @@
ga('send', 'pageview');
</script><!--{"files":{"publicPath":"/","chunks":{"manifest":{"size":0,"entry":"/manifest.087a5454fa0c34daf3c9.js","hash":"087a5454fa0c34daf3c9","css":[]},"vendor":{"size":1670874,"entry":"/vendor.c882d66445aebc52c21b.js","hash":"c882d66445aebc52c21b","css":[]},"style":{"size":122,"entry":"/style.6d7a32b1405ea1bb2bdf.js","hash":"6d7a32b1405ea1bb2bdf","css":["/style.6d7a32b1405ea1bb2bdf.css"]},"app":{"size":352314,"entry":"/app.fcbedf54f0345474ccc1.js","hash":"fcbedf54f0345474ccc1","css":[]}},"js":["/manifest.087a5454fa0c34daf3c9.js","/vendor.c882d66445aebc52c21b.js","/style.6d7a32b1405ea1bb2bdf.js","/app.fcbedf54f0345474ccc1.js"],"css":["/style.6d7a32b1405ea1bb2bdf.css"]},"options":{"template":"/Users/andrew/dev/clients/ES/code/event-sourcing-examples/js-frontend/node_modules/html-webpack-plugin/lib/loader.js!/Users/andrew/dev/clients/ES/code/event-sourcing-examples/js-frontend/public/index.ejs","filename":"index.html","hash":false,"inject":false,"compile":true,"favicon":false,"minify":false,"cache":true,"showErrors":true,"chunks":"all","excludeChunks":[],"title":"Money Transfer App","xhtml":false,"description":"ES Money Transfer App","appMountId":"root","googleAnalytics":{"trackingId":"UA-XXXX-XX","pageViewOnLoad":true},"mobile":true}}--></body>
</script><!--{"files":{"publicPath":"/","chunks":{"manifest":{"size":0,"entry":"/manifest.9a73c590403a64f40c41.js","hash":"9a73c590403a64f40c41","css":[]},"vendor":{"size":1670874,"entry":"/vendor.829a6cd5501868837732.js","hash":"829a6cd5501868837732","css":[]},"style":{"size":122,"entry":"/style.6734b6ac5d23d61b8e5f.js","hash":"6734b6ac5d23d61b8e5f","css":["/style.6734b6ac5d23d61b8e5f.css"]},"app":{"size":351970,"entry":"/app.4d33d25a9b5872086180.js","hash":"4d33d25a9b5872086180","css":[]}},"js":["/manifest.9a73c590403a64f40c41.js","/vendor.829a6cd5501868837732.js","/style.6734b6ac5d23d61b8e5f.js","/app.4d33d25a9b5872086180.js"],"css":["/style.6734b6ac5d23d61b8e5f.css"]},"options":{"template":"/Users/andrew/dev/clients/ES/code/event-sourcing-examples/js-frontend/node_modules/html-webpack-plugin/lib/loader.js!/Users/andrew/dev/clients/ES/code/event-sourcing-examples/js-frontend/public/index.ejs","filename":"index.html","hash":false,"inject":false,"compile":true,"favicon":false,"minify":false,"cache":true,"showErrors":true,"chunks":"all","excludeChunks":[],"title":"Money Transfer App","xhtml":false,"description":"ES Money Transfer App","appMountId":"root","googleAnalytics":{"trackingId":"UA-XXXX-XX","pageViewOnLoad":true},"mobile":true}}--></body>
</html>

View File

@@ -76,7 +76,7 @@
/******/ script.charset = 'utf-8';
/******/ script.async = true;
/******/
/******/ script.src = __webpack_require__.p + "" + {"0":"fcbedf54f0345474ccc1","1":"6d7a32b1405ea1bb2bdf","2":"c882d66445aebc52c21b"}[chunkId] + ".js";
/******/ script.src = __webpack_require__.p + "" + {"0":"4d33d25a9b5872086180","1":"6734b6ac5d23d61b8e5f","2":"829a6cd5501868837732"}[chunkId] + ".js";
/******/ head.appendChild(script);
/******/ }
/******/ };
@@ -92,4 +92,4 @@
/******/ })
/************************************************************************/
/******/ ([]);
//# sourceMappingURL=manifest.087a5454fa0c34daf3c9.js.map
//# sourceMappingURL=manifest.9a73c590403a64f40c41.js.map

View File

@@ -1 +1 @@
{"version":3,"sources":["webpack:///webpack/bootstrap edd3ecd6d3192330eb69?"],"names":[],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAQ,oBAAoB;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,YAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,uDAA+C,iFAAiF;AAChI;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA","file":"manifest.087a5454fa0c34daf3c9.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, callbacks = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId])\n \t\t\t\tcallbacks.push.apply(callbacks, installedChunks[chunkId]);\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);\n \t\twhile(callbacks.length)\n \t\t\tcallbacks.shift().call(null, __webpack_require__);\n \t\tif(moreModules[0]) {\n \t\t\tinstalledModules[0] = 0;\n \t\t\treturn __webpack_require__(0);\n \t\t}\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// \"0\" means \"already loaded\"\n \t// Array means \"loading\", array contains callbacks\n \tvar installedChunks = {\n \t\t3:0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId, callback) {\n \t\t// \"0\" is the signal for \"already loaded\"\n \t\tif(installedChunks[chunkId] === 0)\n \t\t\treturn callback.call(null, __webpack_require__);\n\n \t\t// an array means \"currently loading\".\n \t\tif(installedChunks[chunkId] !== undefined) {\n \t\t\tinstalledChunks[chunkId].push(callback);\n \t\t} else {\n \t\t\t// start chunk loading\n \t\t\tinstalledChunks[chunkId] = [callback];\n \t\t\tvar head = document.getElementsByTagName('head')[0];\n \t\t\tvar script = document.createElement('script');\n \t\t\tscript.type = 'text/javascript';\n \t\t\tscript.charset = 'utf-8';\n \t\t\tscript.async = true;\n\n \t\t\tscript.src = __webpack_require__.p + \"\" + {\"0\":\"fcbedf54f0345474ccc1\",\"1\":\"6d7a32b1405ea1bb2bdf\",\"2\":\"c882d66445aebc52c21b\"}[chunkId] + \".js\";\n \t\t\thead.appendChild(script);\n \t\t}\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap edd3ecd6d3192330eb69\n **/"],"sourceRoot":""}
{"version":3,"sources":["webpack:///webpack/bootstrap 20c37f476a1d80966b41?"],"names":[],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAQ,oBAAoB;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,YAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,uDAA+C,iFAAiF;AAChI;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA","file":"manifest.9a73c590403a64f40c41.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, callbacks = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId])\n \t\t\t\tcallbacks.push.apply(callbacks, installedChunks[chunkId]);\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);\n \t\twhile(callbacks.length)\n \t\t\tcallbacks.shift().call(null, __webpack_require__);\n \t\tif(moreModules[0]) {\n \t\t\tinstalledModules[0] = 0;\n \t\t\treturn __webpack_require__(0);\n \t\t}\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// \"0\" means \"already loaded\"\n \t// Array means \"loading\", array contains callbacks\n \tvar installedChunks = {\n \t\t3:0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId, callback) {\n \t\t// \"0\" is the signal for \"already loaded\"\n \t\tif(installedChunks[chunkId] === 0)\n \t\t\treturn callback.call(null, __webpack_require__);\n\n \t\t// an array means \"currently loading\".\n \t\tif(installedChunks[chunkId] !== undefined) {\n \t\t\tinstalledChunks[chunkId].push(callback);\n \t\t} else {\n \t\t\t// start chunk loading\n \t\t\tinstalledChunks[chunkId] = [callback];\n \t\t\tvar head = document.getElementsByTagName('head')[0];\n \t\t\tvar script = document.createElement('script');\n \t\t\tscript.type = 'text/javascript';\n \t\t\tscript.charset = 'utf-8';\n \t\t\tscript.async = true;\n\n \t\t\tscript.src = __webpack_require__.p + \"\" + {\"0\":\"4d33d25a9b5872086180\",\"1\":\"6734b6ac5d23d61b8e5f\",\"2\":\"829a6cd5501868837732\"}[chunkId] + \".js\";\n \t\t\thead.appendChild(script);\n \t\t}\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap 20c37f476a1d80966b41\n **/"],"sourceRoot":""}

View File

@@ -438,4 +438,4 @@ body {
h1 {
margin-top: .5em;
}
/*# sourceMappingURL=style.6d7a32b1405ea1bb2bdf.css.map*/
/*# sourceMappingURL=style.6734b6ac5d23d61b8e5f.css.map*/

View File

@@ -1 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"style.6d7a32b1405ea1bb2bdf.css","sourceRoot":""}
{"version":3,"sources":[],"names":[],"mappings":"","file":"style.6734b6ac5d23d61b8e5f.css","sourceRoot":""}

View File

@@ -3,20 +3,20 @@ webpackJsonp([1,3],{
/***/ 0:
/***/ function(module, exports, __webpack_require__) {
__webpack_require__(614);
module.exports = __webpack_require__(618);
__webpack_require__(611);
module.exports = __webpack_require__(615);
/***/ },
/***/ 614:
/***/ 611:
/***/ function(module, exports) {
// removed by extract-text-webpack-plugin
/***/ },
/***/ 618:
/***/ 615:
/***/ function(module, exports) {
// removed by extract-text-webpack-plugin
@@ -24,4 +24,4 @@ webpackJsonp([1,3],{
/***/ }
});
//# sourceMappingURL=style.6d7a32b1405ea1bb2bdf.js.map
//# sourceMappingURL=style.6734b6ac5d23d61b8e5f.js.map

View File

@@ -1 +1 @@
{"version":3,"sources":["webpack:///./~/react-select/dist/react-select.css?","webpack:///./src/main.less?"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,0C;;;;;;;ACAA,0C","file":"style.6d7a32b1405ea1bb2bdf.js","sourcesContent":["// removed by extract-text-webpack-plugin\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./~/react-select/dist/react-select.css\n ** module id = 614\n ** module chunks = 1\n **/","// removed by extract-text-webpack-plugin\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/main.less\n ** module id = 618\n ** module chunks = 1\n **/"],"sourceRoot":""}
{"version":3,"sources":["webpack:///./~/react-select/dist/react-select.css?","webpack:///./src/main.less?"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,0C;;;;;;;ACAA,0C","file":"style.6734b6ac5d23d61b8e5f.js","sourcesContent":["// removed by extract-text-webpack-plugin\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./~/react-select/dist/react-select.css\n ** module id = 611\n ** module chunks = 1\n **/","// removed by extract-text-webpack-plugin\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/main.less\n ** module id = 615\n ** module chunks = 1\n **/"],"sourceRoot":""}

View File

@@ -8,6 +8,7 @@ import { Provider, connect} from "react-redux";
import thunk from "redux-thunk";
import createLogger from 'redux-logger';
import { Route, IndexRoute, Link, IndexLink } from "react-router";
import { RouterContext } from 'react-router';
import { ReduxRouter} from "redux-router";
import { createHistory, createHashHistory, createMemoryHistory } from "history";
import { pushState, routerStateReducer, reduxReactRouter as clientRouter} from "redux-router";
@@ -16,6 +17,7 @@ import { reduxReactRouter as serverRouter } from "redux-router/server";
import mainReducer from './reducers';
import { configure as endpointsConfig } from './actions/configure';
import { visitLocation } from './actions/navigate';
import { requireAuthentication } from './components/AuthComponent';
import Container from "./components/partials/Container";
import MyAccounts from "./views/MyAccounts";
@@ -25,7 +27,8 @@ import SignUp from "./views/SignUp";
class App extends React.Component {
render() {
return (<Container>
return (
<Container>
{this.props.children}
</Container>);
}
@@ -38,12 +41,19 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
router: routerStateReducer
});
let dispatch = null;
const onEnter = (nextState) => {
const { location } = nextState;
dispatch && dispatch(visitLocation(location));
};
const routes = (
<Route path="/" component={App}>
<IndexRoute component={requireAuthentication(MyAccounts)} />
<Route path="signin" component={SignIn} />
<Route path="register" component={SignUp} />
<Route path="account/:accountId" component={requireAuthentication(Account)} />
<Route path="/" component={ App }>
<IndexRoute component={ requireAuthentication(MyAccounts) } />
<Route path="signin" component={ SignIn } onEnter={ onEnter } />
<Route path="register" component={ SignUp } onEnter={ onEnter } />
<Route path="account/:accountId" component={ requireAuthentication(Account) } />
</Route>
);
@@ -60,6 +70,7 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
})
)(createStore)(reducer);
dispatch = store.dispatch;
/**
* The React Router 1.0 routes for both the server and the client.
@@ -85,13 +96,11 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
},
handleLoginResponse: function(resp) {
debugger;
return resp.data;
},
handleAccountUpdateResponse: function(resp) {
debugger;
return resp.data;
},

View File

@@ -1,27 +0,0 @@
/**
* Created by andrew on 25/02/16.
*/
export function configureStart({...props} = {}) {
return {
...props,
type: T.AUTH.CONFIGURE_START
};
}
export function configureComplete({config, ...props} = {}) {
return {
...props,
type: T.AUTH.CONFIGURE_COMPLETE,
config
};
}
export function configureError({errors, ...props} = {}) {
return {
...props,
type: T.AUTH.CONFIGURE_ERROR,
error: errors
};
}

View File

@@ -1,32 +1,12 @@
/**
* Created by andrew on 26/02/16.
*/
import * as C from "../utils/constants";
import {
authenticate,
authenticateStart,
authenticateComplete,
authenticateError
} from "./authenticate";
import {
retrieveData,
} from "../utils/sessionStorage";
import {applyConfig} from "../utils/clientSettings";
//import {
// showFirstTimeLoginSuccessModal,
// showFirstTimeLoginErrorModal,
// showPasswordResetSuccessModal,
// showPasswordResetErrorModal
//} from "./ui";
import getRedirectInfo from "../utils/parseUrl";
import { pushState } from "redux-router";
import root from '../utils/root';
export const SET_ENDPOINT_KEYS = "SET_ENDPOINT_KEYS";
export const STORE_CURRENT_ENDPOINT_KEY = "STORE_CURRENT_ENDPOINT_KEY";

View File

@@ -0,0 +1,7 @@
/**
* Created by andrew on 26/02/16.
*/
import T from '../constants/ACTION_TYPES';
import { makeActionCreator } from '../utils/actions';
export const visitLocation = makeActionCreator(T.LOCATION.ENTER, 'location');

View File

@@ -1,29 +0,0 @@
import T from '../constants/ACTION_TYPES'
import ROUTES from '../constants/ROUTES'
// `navigate` is used to facilitate changing routes within another action
// without rendering any other changes first
export function start(name, options) {
return dispatch => {
const currentURI = window.location.hash.substr(1)
const newURI = ROUTES.generate(name, options)
if (currentURI != newURI) {
dispatch({
type: T.NAVIGATION.START,
})
window.location.replace(
window.location.pathname + window.location.search + '#' + newURI
)
}
}
}
export function complete() {
return {
type: T.NAVIGATION.COMPLETE,
location: ROUTES.lookup(window.location.hash.substr(1)),
}
}

View File

@@ -1,23 +1,11 @@
/**
* Created by andrew on 26/02/16.
*/
import {
setCurrentEndpointKey,
getCurrentEndpointKey,
persistUserData
} from "../utils/sessionStorage";
import { entityReceived } from './entities';
import { storeCurrentEndpointKey } from "./configure";
//import { parseResponse } from "../utils/handleFetchResponse";
//import fetch from "../utils/fetch";
import { apiSignIn } from '../utils/api';
import { makeActionCreator } from '../utils/actions';
import T from '../constants/ACTION_TYPES';
//import root from '../utils/root';
import { makeActionCreator } from '../utils/actions';
import { persistUserData } from "../utils/sessionStorage";
import { entityReceived } from './entities';
import { apiSignIn } from '../utils/api';
export const emailSignInFormUpdate = makeActionCreator(T.AUTH.SIGN_IN_FORM_UPDATE, 'key', 'value');
export const emailSignInStart = makeActionCreator(T.AUTH.SIGN_IN_START);

View File

@@ -1,48 +1,34 @@
/**
* Created by andrew on 11/03/16.
*/
import {
getEmailSignUpUrl
} from "../utils/sessionStorage";
import { entityReceived } from './entities';
import { storeCurrentEndpointKey } from "./configure";
//import { parseResponse } from "../utils/handleFetchResponse";
import { push } from 'redux-router';
import T from '../constants/ACTION_TYPES';
import { makeActionCreator } from '../utils/actions';
import { apiSignUp } from "../utils/api";
import { emailSignInFormUpdate } from './signIn';
import { push } from 'redux-router';
import T from '../constants/ACTION_TYPES';
export const emailSignUpFormUpdate = makeActionCreator(T.AUTH.SIGN_UP_FORM_UPDATE, 'key', 'value');
export const emailSignUpStart = makeActionCreator(T.AUTH.SIGN_UP_START);
export const emailSignUpComplete = makeActionCreator(T.AUTH.SIGN_UP_COMPLETE, 'user');
export const emailSignUpError = makeActionCreator(T.AUTH.SIGN_UP_ERROR, 'error');
export function emailSignUpFormUpdate(key, value) {
return { type: T.AUTH.SIGN_UP_FORM_UPDATE, key, value };
}
export function emailSignUpStart() {
return { type: T.AUTH.SIGN_UP_START };
}
export function emailSignUpComplete(user) {
return { type: T.AUTH.SIGN_UP_COMPLETE, user };
}
export function emailSignUpError(errors) {
return { type: T.AUTH.SIGN_UP_ERROR, errors };
}
export function emailSignUp(body) {
return dispatch => {
dispatch(emailSignUpStart());
return apiSignUp(body)
.then(({data}) => {
.then(({ data }) => {
dispatch(emailSignUpComplete(data));
const { email } = body;
dispatch(emailSignInFormUpdate('email', email));
dispatch(push('/signin'));
})
.catch(({errors}) => dispatch(emailSignUpError(errors)));
.catch(({ errors }) => {
dispatch(emailSignUpError({
errors
}))
});
};
}

View File

@@ -1,7 +0,0 @@
import redirector from './redirector'
import renderer from './renderer'
export default [
redirector,
renderer,
]

View File

@@ -1,20 +0,0 @@
import * as navigation from '../actions/navigation'
import ROUTES from '../constants/ROUTES'
export default function redirector(state, dispatch) {
const {name, options} = state.navigation.location || {}
const currentURI = window.location.hash.substr(1)
const canonicalURI = name && ROUTES.generate(name, options)
if (canonicalURI && canonicalURI !== currentURI) {
// If the URL entered includes extra `/` characters, or otherwise
// differs from the canonical URL, navigate the user to the
// canonical URL (which will result in `complete` being called again)
dispatch(navigation.start(name, options))
}
else if (name == 'root') {
// If we've hit the root location, redirect the user to the main page
dispatch(navigation.start('documentList'))
}
}

View File

@@ -1,18 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import Application from '../App'
// Store a reference to our application's root DOM node to prevent repeating
// this on every state update
const APP_NODE = document.getElementById('react-app')
export default function renderer(state, dispatch) {
// Don't re-render if we're in the process of navigating to a new page
if (!state.navigation.transitioning) {
ReactDOM.render(
<Application state={state} dispatch={dispatch} />,
APP_NODE
)
}
}

View File

@@ -20,7 +20,7 @@ export class TransfersTable extends React.Component {
}, v) => {
if (v.entryType == 'account') {
balance = v.initialBalance;
} else if (v.entryType == 'transaction') {
} else if (v.entryType == 'transaction' && (v.status !== 'FAILED_DUE_TO_INSUFFICIENT_FUNDS')) {
const isOriginating = v.fromAccountId == currentAccountId;
balance += (isOriginating ? -1 : 1) * v.amount;
}

View File

@@ -1,6 +1,10 @@
import { TODO_DEFINE, defineActionType } from '../utils/defineActionTypes'
export default defineActionType({
LOCATION: {
ENTER: TODO_DEFINE
},
/*
* View model
*/

View File

@@ -10,47 +10,48 @@ import Input from "./Input";
import ButtonLoader from "./ButtonLoader";
import AuxErrorLabel from './AuxErrorLabel';
import { emailSignInFormUpdate, emailSignIn } from "../../actions/signIn";
import * as AS from "../../actions/signIn";
/*
<Input type="password"
label="Password"
className="email-sign-in-password"
placeholder="Password"
disabled={disabled}
value={this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "form", "password"])}
errors={this.props.auth.getIn(["emailSignIn", this.getEndpoint(), "errors", "password"])}
onChange={this.handleInput.bind(this, "password")}
{...this.props.inputProps.password} />
*/
const formValidation = (payload) => [
'email',
'password'
].reduce((memo, prop) => {
let result = [];
const value = (payload[prop] || '').replace(/(^\s+)|(\s+$)/g, '');
class EmailSignInForm extends React.Component {
switch (prop) {
case 'email':
case 'password':
if (/^$/.test(value)) {
result.push('required');
}
}
static propTypes = {
endpoint: PropTypes.string,
inputProps: PropTypes.shape({
email: PropTypes.object,
password: PropTypes.object,
submit: PropTypes.object
})
};
if (result.length) {
memo[prop] = result;
memo.hasErrors = true;
}
return memo;
}, {});
static defaultProps = {
inputProps: {
email: {},
password: {},
submit: {}
}
};
export class EmailSignInForm extends React.Component {
handleInput (key, val) {
this.props.dispatch(emailSignInFormUpdate(key, val));
this.props.dispatch(AS.emailSignInFormUpdate(key, val));
}
handleSubmit (event) {
event.preventDefault();
let formData = { ...this.props.auth.signIn.form };
this.props.dispatch(emailSignIn(formData));
const formData = read(this.props.auth, 'signIn.form');
const validationErrors = formValidation(formData);
if (validationErrors.hasErrors) {
this.props.dispatch(AS.emailSignInError(validationErrors));
return;
}
this.props.dispatch(AS.emailSignIn(formData));
}
render () {
@@ -62,10 +63,8 @@ class EmailSignInForm extends React.Component {
);
//const error = read(this.props.auth, 'signIn.errors.email', null);
//debugger;
const formErrors = read(this.props.auth, 'signIn.errors.errors', '');
return (
<form className='redux-auth email-sign-in-form clearfix'
onSubmit={this.handleSubmit.bind(this)}>
@@ -75,7 +74,7 @@ class EmailSignInForm extends React.Component {
}}>
<AuxErrorLabel
label="Form:"
errors={formErrors.length ? [formErrors] : [] }
errors={ formErrors.length ? [ formErrors ] : [] }
/>
</div>
@@ -90,6 +89,17 @@ class EmailSignInForm extends React.Component {
onChange={this.handleInput.bind(this, "email")}
{...this.props.inputProps.email} />
<Input type="password"
className="password-sign-in-email"
label="Password"
placeholder="Password"
name="password"
disabled={disabled}
value={read(this.props.auth, 'signIn.form.password', '')}
errors={read(this.props.auth, 'signIn.errors.password', [])}
onChange={this.handleInput.bind(this, "password")}
{...this.props.inputProps.password} />
<ButtonLoader loading={read(this.props.auth, 'signIn.loading', false)}
type="submit"
icon={<BS.Glyphicon glyph="log-in" />}
@@ -108,4 +118,21 @@ class EmailSignInForm extends React.Component {
}
}
export default connect(({app}) => ({auth: app.auth}))(EmailSignInForm);
EmailSignInForm.propTypes = {
endpoint: PropTypes.string,
inputProps: PropTypes.shape({
email: PropTypes.object,
password: PropTypes.object,
submit: PropTypes.object
})
};
EmailSignInForm.defaultProps = {
inputProps: {
email: {},
password: {},
submit: {}
}
};
// export default connect(({app}) => ({auth: app.auth}))(EmailSignInForm);

View File

@@ -2,40 +2,77 @@
* Created by andrew on 15/02/16.
*/
import React, {PropTypes} from "react";
//import auth from "redux-auth";
import { connect } from "react-redux";
import { Glyphicon } from "react-bootstrap";
import Input from "./Input";
import ButtonLoader from "./ButtonLoader";
//import { emailSignUpFormUpdate, emailSignUp } from "redux-auth";
import IndexPanel from "./../../components/partials/IndexPanel";
import AuxErrorLabel from './AuxErrorLabel';
import { customerInfoMap } from '../../entities/formToPayloadMappers';
import read from '../../utils/readProp';
import * as AS from '../../actions/signUp';
import { Glyphicon } from "react-bootstrap";
import { connect } from "react-redux";
const formValidation = (payload) => [
'fname',
'lname',
'email',
'password',
'passwordConfirm',
'ssn',
'phoneNumber',
'address1',
'address2',
'city',
'state',
'zip'
].reduce((memo, prop) => {
let result = [];
const value = (payload[prop] || '').replace(/(^\s+)|(\s+$)/g, '');
import {emailSignUpFormUpdate, emailSignUp} from '../../actions/signUp';
switch (prop) {
case 'fname':
case 'lname':
case 'email':
case 'ssn':
case 'password':
case 'passwordConfirm':
if (/^$/.test(value)) {
result.push('required');
}
}
switch (prop) {
case 'passwordConfirm':
if (value != payload['password']) {
result.push('need to be equal to password');
}
}
if (result.length) {
memo[prop] = result;
memo.hasErrors = true;
}
return memo;
}, {});
class EmailSignUpForm extends React.Component {
getEndpoint () {
return (
this.props.endpoint ||
this.props.auth.getIn(["configure", "currentEndpointKey"]) ||
this.props.auth.getIn(["configure", "defaultEndpointKey"])
);
}
handleInput (key, val) {
this.props.dispatch(emailSignUpFormUpdate(key, val));
this.props.dispatch(AS.emailSignUpFormUpdate(key, val));
}
handleSubmit (event) {
event.preventDefault();
let formData = { ...this.props.auth.signUp.form };
this.props.dispatch(emailSignUp(customerInfoMap(formData)));
const formData = read(this.props.auth, 'signUp.form');
const validationErrors = formValidation(formData);
if (validationErrors.hasErrors) {
this.props.dispatch(AS.emailSignUpError(validationErrors));
return;
}
this.props.dispatch(AS.emailSignUp(customerInfoMap(formData)));
}
render () {
@@ -45,10 +82,21 @@ class EmailSignUpForm extends React.Component {
this.props.auth.signUp.loading
);
const formErrors = read(this.props.auth, 'signUp.errors.errors', '');
return (
<form className='redux-auth email-sign-up-form clearfix'
onSubmit={this.handleSubmit.bind(this)}>
<div className="form-group" style={{
display: formErrors ? 'block' : 'none'
}}>
<AuxErrorLabel
label="Form:"
errors={ formErrors.length ? [ formErrors ] : [] }
/>
</div>
<IndexPanel header="basic">
<Input type="text"
@@ -81,6 +129,28 @@ class EmailSignUpForm extends React.Component {
onChange={this.handleInput.bind(this, "email")}
/>
<Input type="password"
className="password-sign-in-email"
label="Password"
placeholder="Password"
name="password"
disabled={disabled}
value={read(this.props.auth, 'signUp.form.password', '')}
errors={read(this.props.auth, 'signUp.errors.password', [])}
onChange={this.handleInput.bind(this, "password")}
/>
<Input type="password"
className="password-sign-in-email"
label="Confirm password"
placeholder="Confirm password"
name="password-confirm"
disabled={disabled}
value={read(this.props.auth, 'signUp.form.passwordConfirm', '')}
errors={read(this.props.auth, 'signUp.errors.passwordConfirm', [])}
onChange={this.handleInput.bind(this, "passwordConfirm")}
/>
</IndexPanel>

View File

@@ -3,6 +3,7 @@
*/
export const customerInfoMap = ({
ssn,
password,
address1,
address2,
city, //: "Moscow"
@@ -17,6 +18,7 @@ export const customerInfoMap = ({
"firstName": fname,
"lastName": lname
},
password,
email,
ssn,
"phoneNumber": phoneNumber,

View File

@@ -1,41 +0,0 @@
import { createStore, applyMiddleware } from 'redux'
import reduxThunk from 'redux-thunk'
import reduxMulti from 'redux-multi'
import { batchedSubscribe } from 'redux-batched-subscribe'
import * as navigation from './actions/navigation'
import actors from './actors'
import rootReducer from './reducers';
import { blocked } from './utils/blockedExecution';
// Add middleware to allow our action creators to return functions and arrays
const createStoreWithMiddleware = applyMiddleware(
reduxThunk,
reduxMulti
)(createStore);
// Ensure our listeners are only called once, even when one of the above
// middleware call the underlying store's `dispatch` multiple times
const createStoreWithBatching = batchedSubscribe(
fn => fn()
)(createStoreWithMiddleware);
// Create a store with our application reducer
const store = createStoreWithBatching(rootReducer)
// Handle changes to our store with a list of actor functions, but ensure
// that the actor sequence cannot be started by a dispatch from an actor
store.subscribe(blocked(() => {
for (let actor of actors) {
actor(store.getState(), store.dispatch)
}
}));
// Dispatch navigation events when the URL's hash changes, and when the
// application loads
function onHashChange() {
store.dispatch(navigation.complete())
}
window.addEventListener('hashchange', onHashChange, false);
onHashChange();

View File

@@ -4,9 +4,29 @@
import T from '../../constants/ACTION_TYPES';
import createFormReducer from '../createFormReducer';
export const signInReducer = createFormReducer([
const internalSignInReducer = createFormReducer([
T.AUTH.SIGN_IN_START,
T.AUTH.SIGN_IN_COMPLETE,
T.AUTH.SIGN_IN_ERROR,
T.AUTH.SIGN_IN_FORM_UPDATE
]);
export const signInReducer = (state, action) => {
switch (action.type) {
case T.LOCATION.ENTER: {
const { location } = action;
const { pathname } = location;
if (pathname == '/signin') {
return internalSignInReducer(state, {
type: T.AUTH.SIGN_IN_ERROR,
error: null
});
}
return state;
}
default: {
return internalSignInReducer(state, action);
}
}
};

View File

@@ -4,9 +4,30 @@
import T from '../../constants/ACTION_TYPES';
import createFormReducer from '../createFormReducer';
export const signUpReducer = createFormReducer([
export const internalSignUpReducer = createFormReducer([
T.AUTH.SIGN_UP_START,
T.AUTH.SIGN_UP_COMPLETE,
T.AUTH.SIGN_UP_ERROR,
T.AUTH.SIGN_UP_FORM_UPDATE
]);
export const signUpReducer = (state, action) => {
switch (action.type) {
case T.LOCATION.ENTER: {
const { location } = action;
const { pathname } = location;
if (pathname == '/register') {
return internalSignUpReducer(state, {
type: T.AUTH.SIGN_UP_ERROR,
error: null
});
}
return state;
}
default: {
return internalSignUpReducer(state, action);
}
}
};

View File

@@ -3,10 +3,9 @@
*/
export function makeActionCreator(type, ...argNames) {
return function(...args) {
const action = { type };
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
});
return action;
return argNames.reduce((action, arg, index) => {
action[arg] = args[index];
return action;
}, { type });
};
}

View File

@@ -2,28 +2,24 @@
* Created by andrew on 12/02/16.
*/
import React from "react";
//import { PageHeader } from "react-bootstrap";
import { connect } from "react-redux";
import { PageHeader, OverlayTrigger, Tooltip, Grid, Col, Row, Nav, NavItem, ButtonGroup, Button, Table } from "react-bootstrap";
import * as BS from "react-bootstrap";
import Spinner from "react-loader";
// import Spinner from "react-loader";
import Select from "react-select";
import Input from "../controls/bootstrap/Input";
import { Money, moneyText } from '../components/Money';
import { TransfersTable } from '../components/TransfersTable';
import { Link, IndexLink} from "react-router";
import { Link, IndexLink } from "react-router";
import IndexPanel from "./../components/partials/IndexPanel";
import * as Modals from './modals';
import * as A from '../actions/entities';
import read from '../utils/readProp';
import { blocked } from '../utils/blockedExecution';
const resetModals = {
showAccountModal: false,
unsaved: false
@@ -162,7 +158,10 @@ export class Account extends React.Component {
if (!account) {
if (errors.length) {
return (<h2>Error loading specified account</h2>);
return (<div>
<h2>Error loading specified account</h2>
<div>Return <Link to="/">Home</Link> to pick another</div>
</div>);
} else {
return spinnerResult;
}

View File

@@ -4,27 +4,21 @@
import React from "react";
import { PageHeader } from "react-bootstrap";
import { connect } from "react-redux";
//import ButtonLoader from "./ButtonLoader";
import * as BS from "react-bootstrap";
//import ButtonLoader from "../controls/bootstrap/ButtonLoader";
import {pushState} from "redux-router";
//export {bootstrap, materialUi} from "./views";
// bootstrap theme
//import { EmailSignInForm } from "redux-auth/bootstrap-theme";
import EmailSignInForm from "../controls/bootstrap/EmailSignInForm";
import { pushState } from "redux-router";
import { EmailSignInForm } from "../controls/bootstrap/EmailSignInForm";
import read from '../utils/readProp';
export class SignIn extends React.Component {
checkRedirect(props) {
if (props.auth.user.isSignedIn) {
props.dispatch(pushState(null, props.location.query.next));
const isSignedIn = read(props.auth, 'user.isSignedIn');
if (isSignedIn) {
const nextLocation = read(props.location, 'query.next');
props.dispatch(pushState(null, nextLocation));
//// redirect to login and add next param so we can redirect again after login
//const redirectAfterLogin = this.props.location.pathname;
//this.props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`));
@@ -40,16 +34,6 @@ export class SignIn extends React.Component {
}
render () {
const signInProps = {
inputProps: {
password: {
className: 'hide hidden',
style: { display: 'none' },
value: null,
disabled: true
}
}
};
return (
<BS.Well>

View File

@@ -34,14 +34,10 @@ export class SignUp extends React.Component {
render () {
return (
<div>
<PageHeader>
Register
</PageHeader>
<BS.Well>
<EmailSignUpForm />
</BS.Well>
</div>
<BS.Well>
<BS.PageHeader>Register</BS.PageHeader>
<EmailSignUpForm />
</BS.Well>
);
}

View File

@@ -140,7 +140,7 @@ export class Add3rdPartyAccountModal extends React.Component {
options={read(this.props.data, 'accountsLookup.options', [])}
onChange={this.handleInput.bind(this, 'account')} />
<AuxErrorLabel
label="Owner:"
label="Account:"
errors={read(this.props.data, 'errors.account', [])}
/>
</div>