diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..4267c4b1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.settings
+.vscode
+.DS_Store
+.project
+.classpath
+target
+bin
+.idea
+*.iml
\ No newline at end of file
diff --git a/README.md b/README.md
index 15d8f685..64f1fc00 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,46 @@
+# Desafio de desenvolvimento back end - Santander
+
+Passos para executar a solução:
+
+1) garanta que o computador possui o JDK versão 1.8, Maven versão 3.6.0, MongoDB versão 4.0.4 e git (qualquer versão recente) instalados. Para isso, abra uma janela de seu terminal e execute os comandos
+
+```
+java -version
+mvn -version
+mongod -version
+git --version
+```
+
+As versões de cada um devem ser impressas na saída padrão.
+
+2) clone este repositório para o seu computador
+3) inicie o MongoDB executando o comando `mongod`. Isso impedirá você de utilizar esta janela do terminal, portanto abra uma nova e passe a usá-la, mas deixe a anterior aberta
+3) utilizando a nova janela do terminal, navegue até o diretório local onde você clonou este repositório
+4) entre no o diretório do microserviço de autenticação, ou `./auth_microservice`
+5) execute o comando `mvn spring-boot:run`
+6) abra uma terceira janela de seu terminal e, novamente, navegue até o diretório local onde se encontra este repositório
+7) entre no diretório do microserviço de gastos, ou `./spend_microservice`, e finalmente, execute o comando `mvn spring-boot:run`
+8) desfrute da aplicação!
+
+
+#### Observações
+- Os microserviços estão configurados para ocuparem as portas 8080 e 8081, respectivamente.
+- Para executar os testes unitários do microserviço de gastos, é preciso que o microserviço de autenticação esteja online
+- Este repositório conta com uma collection e um environment do Postman, uma ferramenta muito útil para testar as funcionalidades de APIs. Para utilizá-los, faça o download e instale o Postman em seu computador (caso ainda não o possua intalado) e importe tanto collection quanto environment para o seu ambiente.
+
+
+#### Possíveis otimizações (próximos passos)
+1) Para melhorar a velocidade de resposta da rota de inserção de gastos, seria interessante refatorá-la para que ela inserisse os documentos a serem persistidos primeiramente em um sistema de mensageria, ou no próprio Redis (devido a sua velocidade de inserção e consulta). Após isso, workers assíncronos subscritos ao canal de mensagens seriam responsáveis por persistir os dados no banco de dados de fato
+2) Outra possível melhoria seria tornar assíncronas as execuções dos métodos dos controllers (no momento, apenas os métodos da camada de serviço estão sendo processados de forma assíncrona). Isto não foi feito pois o uso da classe `HandlerInterceptorAdapter` faz com que duas chamadas de seu método `preHandle()` ocorram a cada requisição assíncrona recebida, e como a lógica de validação de tokens se encontra dentro deste método, o resultado seria que o número de requisições que o microserviço de autentição precisaria responder para o sistema funcionasse normalmente dobraria
+3) Containerizar cada um dos microserviços tornaria muito mais simples a tarefa de movê-los de um infraestrutura para outra, escalá-los e administrá-los no geral
+
+
+
+Por fim, estou me candidando pela **IBM**.
+
+
+
+#### O texto original do desafio se encontra abaixo
# Show me the code
### # DESAFIO:
diff --git a/auth_microservice/pom.xml b/auth_microservice/pom.xml
new file mode 100644
index 00000000..020e76b9
--- /dev/null
+++ b/auth_microservice/pom.xml
@@ -0,0 +1,72 @@
+
+
+ 4.0.0
+
+ br.com.testesantander.api
+ authentication_microservice
+ 0.1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.5.RELEASE
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.mindrot
+ jbcrypt
+ 0.4
+
+
+
+ com.auth0
+ java-jwt
+ 3.4.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.jayway.jsonpath
+ json-path
+ test
+
+
+
+
+ 1.8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ spring-releases
+ https://repo.spring.io/libs-release
+
+
+
+
\ No newline at end of file
diff --git a/auth_microservice/src/main/java/microservice/AuthMicroservice.java b/auth_microservice/src/main/java/microservice/AuthMicroservice.java
new file mode 100644
index 00000000..a00b5aad
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/AuthMicroservice.java
@@ -0,0 +1,39 @@
+package microservice;
+
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class AuthMicroservice {
+
+ @Value("${thread.pool.core.size}")
+ private int corePoolSize;
+
+ @Value("${thread.pool.max.size}")
+ private int maxPoolSize;
+
+ @Value("${thread.queue.capacity}")
+ private int queueCapacity;
+
+ @Bean(name = "ThreadPoolExecutor")
+ public TaskExecutor getAsyncExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(corePoolSize);
+ executor.setMaxPoolSize(maxPoolSize);
+ executor.setQueueCapacity(queueCapacity);
+ executor.setThreadNamePrefix("ThreadPoolExecutor-");
+ executor.initialize();
+ return executor;
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(AuthMicroservice.class, args);
+ }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/configurations/WebMvcConfig.java b/auth_microservice/src/main/java/microservice/configurations/WebMvcConfig.java
new file mode 100644
index 00000000..0efd4b02
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/configurations/WebMvcConfig.java
@@ -0,0 +1,22 @@
+package microservice.configurations;
+
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import microservice.interceptors.JWTInterceptor;
+
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ @Autowired
+ private JWTInterceptor interceptor;
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry){
+ registry.addInterceptor(interceptor).addPathPatterns("/**");
+ }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/controllers/SystemController.java b/auth_microservice/src/main/java/microservice/controllers/SystemController.java
new file mode 100644
index 00000000..747560c2
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/controllers/SystemController.java
@@ -0,0 +1,73 @@
+package microservice.controllers;
+
+
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.beans.factory.annotation.Autowired;
+import microservice.services.SystemService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestBody;
+import javax.validation.Valid;
+import org.springframework.http.ResponseEntity;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.springframework.web.util.UriComponentsBuilder;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import org.springframework.http.MediaType;
+import microservice.models.Message;
+import microservice.models.System;
+import microservice.models.Authorization;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.RequestHeader;
+
+
+@RestController
+public class SystemController {
+
+ @Autowired
+ private SystemService systemService;
+
+ @RequestMapping(value = "/systems",
+ method = RequestMethod.POST,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity inserNewSystem(UriComponentsBuilder builder,
+ @Valid @RequestBody System system)
+ throws URISyntaxException, InterruptedException, ExecutionException {
+
+ CompletableFuture systemFuture = systemService.register(system);
+ System storedSystem = systemFuture.get();
+ return ResponseEntity
+ .created(new URI(builder.toUriString() + "/systems/" + storedSystem.get_id()))
+ .body(new Message("system " + system.getUsername() + " successfully registered", storedSystem.get_id(), true));
+ }
+
+ @RequestMapping(value = "/systems/authentication",
+ method = RequestMethod.POST,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity> authenticateSystem(@Valid @RequestBody System system)
+ throws InterruptedException, ExecutionException {
+
+ CompletableFuture> systemFuture = systemService.authenticate(system);
+ Object result = systemFuture.get();
+ if (result.getClass() == Authorization.class)
+ return ResponseEntity.ok(result);
+ else
+ return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED);
+ }
+
+ @RequestMapping(value = "/systems/authorization",
+ method = RequestMethod.GET,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity authorizeSystem(@RequestHeader("Authorization") String accessToken)
+ throws InterruptedException, ExecutionException {
+
+ CompletableFuture systemFuture = systemService.authorize(accessToken);
+ Message msg = systemFuture.get();
+ if (msg.getStatus().equals("success"))
+ return ResponseEntity.ok(msg);
+ else
+ return new ResponseEntity<>(msg, HttpStatus.UNAUTHORIZED);
+ }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/controllers/UserController.java b/auth_microservice/src/main/java/microservice/controllers/UserController.java
new file mode 100644
index 00000000..4bbf7f4b
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/controllers/UserController.java
@@ -0,0 +1,73 @@
+package microservice.controllers;
+
+
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.beans.factory.annotation.Autowired;
+import microservice.services.UserService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import javax.validation.Valid;
+import org.springframework.http.ResponseEntity;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.springframework.web.util.UriComponentsBuilder;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import org.springframework.http.MediaType;
+import org.springframework.http.HttpStatus;
+import microservice.models.Authorization;
+import microservice.models.Message;
+import microservice.models.User;
+
+
+@RestController
+public class UserController {
+
+ @Autowired
+ private UserService userService;
+
+ @RequestMapping(value = "/users",
+ method = RequestMethod.POST,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity register(UriComponentsBuilder builder,
+ @Valid @RequestBody User user)
+ throws URISyntaxException, InterruptedException, ExecutionException {
+
+ CompletableFuture userFuture = userService.register(user);
+ User storedUser = userFuture.get();
+ return ResponseEntity
+ .created(new URI(builder.toUriString() + "/users/" + storedUser.get_id()))
+ .body(new Message("user " + user.getUsername() + " successfully registered", storedUser.get_id(), true));
+ }
+
+ @RequestMapping(value = "/users/authentication",
+ method = RequestMethod.POST,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity> authenticateUser(@Valid @RequestBody User user)
+ throws InterruptedException, ExecutionException {
+
+ CompletableFuture> userFuture = userService.authenticate(user);
+ Object result = userFuture.get();
+ if (result.getClass() == Authorization.class)
+ return ResponseEntity.ok(result);
+ else
+ return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED);
+ }
+
+ @RequestMapping(value = "/users/authorization",
+ method = RequestMethod.GET,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity authorizeUser(@RequestHeader("Authorization") String accessToken)
+ throws InterruptedException, ExecutionException {
+
+ CompletableFuture userFuture = userService.authorize(accessToken);
+ Message msg = userFuture.get();
+ if (msg.getStatus().equals("success"))
+ return ResponseEntity.ok(msg);
+ else
+ return new ResponseEntity<>(msg, HttpStatus.UNAUTHORIZED);
+ }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java b/auth_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java
new file mode 100644
index 00000000..1e1d8cb0
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java
@@ -0,0 +1,34 @@
+package microservice.interceptors;
+
+
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+@Component
+public class JWTInterceptor extends HandlerInterceptorAdapter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JWTInterceptor.class);
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, PATCH, DELETE, OPTIONS");
+ response.setHeader("Access-Control-Max-Age", "6000");
+ response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
+ response.setContentType("application/json");
+ response.setCharacterEncoding("UTF-8");
+
+ String requestMethod = request.getMethod();
+ String requestURI = request.getRequestURI();
+
+ LOGGER.info("[" + requestMethod + "] " + requestURI);
+
+ return super.preHandle(request, response, handler);
+ }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/models/Authorization.java b/auth_microservice/src/main/java/microservice/models/Authorization.java
new file mode 100644
index 00000000..61c53c90
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/models/Authorization.java
@@ -0,0 +1,45 @@
+package microservice.models;
+
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+
+public class Authorization {
+
+ private String accessToken;
+ private boolean status;
+ private String clientId;
+
+ @JsonFormat(shape=JsonFormat.Shape.STRING,
+ pattern="yyyy-MM-dd'T'HH:mm:ss.SSSZ",
+ timezone="UTC")
+ private Date expiryDate;
+
+
+ public Authorization() { }
+
+ public Authorization(String accessToken, Date expiryDate, String clientId, boolean status) {
+ this.accessToken = accessToken;
+ this.status = status;
+ this.expiryDate = expiryDate;
+ this.clientId = clientId;
+ }
+
+ public String getAccessToken() { return accessToken; }
+
+ public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
+
+ public Date getExpiryDate() { return expiryDate; }
+
+ public void setExpiryDate(Date expiryDate) { this.expiryDate = expiryDate; }
+
+ public String getStatus() { return status ? "success" : "failed"; }
+
+ public void setStatus(boolean status) { this.status = status; }
+
+ public String getClientId() { return clientId; }
+
+ public void setClientId(String clientId) { this.clientId = clientId; }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/models/Message.java b/auth_microservice/src/main/java/microservice/models/Message.java
new file mode 100644
index 00000000..364b2790
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/models/Message.java
@@ -0,0 +1,30 @@
+package microservice.models;
+
+
+public class Message {
+
+ private String content;
+ private boolean status;
+ private String clientId;
+
+ public Message() { }
+
+ public Message(String content, String clientId, boolean status) {
+ this.content = content;
+ this.clientId = clientId;
+ this.status = status;
+ }
+
+ public String getContent() { return content; }
+
+ public void setContent(String content) { this.content = content; }
+
+ public String getStatus() { return status ? "success" : "failed"; }
+
+ public void setStatus(boolean status) { this.status = status; }
+
+ public String getClientId() { return clientId; }
+
+ public void setClientId(String clientId) { this.clientId = clientId; }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/models/System.java b/auth_microservice/src/main/java/microservice/models/System.java
new file mode 100644
index 00000000..35b545bd
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/models/System.java
@@ -0,0 +1,43 @@
+package microservice.models;
+
+
+import org.springframework.data.annotation.Id;
+import javax.validation.constraints.NotNull;
+import org.bson.types.ObjectId;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+
+@Document(collection = "systems")
+public class System {
+
+ @Id
+ private ObjectId _id;
+
+ @NotNull
+ @Indexed(unique = true)
+ private String username;
+
+ @NotNull
+ private String password;
+
+ public System() { }
+
+ public System(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ public String get_id() { return _id.toHexString(); }
+
+ public String getUsername() { return username; }
+
+ public String getPassword() { return password; }
+
+ public void set_id(ObjectId _id) { this._id = _id; }
+
+ public void setUsername(String username) { this.username = username; }
+
+ public void setPassword(String password) { this.password = password; }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/models/User.java b/auth_microservice/src/main/java/microservice/models/User.java
new file mode 100644
index 00000000..abdd6490
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/models/User.java
@@ -0,0 +1,43 @@
+package microservice.models;
+
+
+import org.springframework.data.annotation.Id;
+import javax.validation.constraints.NotNull;
+import org.bson.types.ObjectId;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+
+@Document(collection = "users")
+public class User {
+
+ @Id
+ private ObjectId _id;
+
+ @NotNull
+ @Indexed(unique = true)
+ private String username;
+
+ @NotNull
+ private String password;
+
+ public User() { }
+
+ public User(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ public String get_id() { return _id.toHexString(); }
+
+ public String getUsername() { return username; }
+
+ public String getPassword() { return password; }
+
+ public void set_id(ObjectId _id) { this._id = _id; }
+
+ public void setUsername(String username) { this.username = username; }
+
+ public void setPassword(String password) { this.password = password; }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/repositories/SystemRepository.java b/auth_microservice/src/main/java/microservice/repositories/SystemRepository.java
new file mode 100644
index 00000000..4a46980b
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/repositories/SystemRepository.java
@@ -0,0 +1,12 @@
+package microservice.repositories;
+
+
+import microservice.models.System;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+
+public interface SystemRepository extends MongoRepository {
+
+ public System findByUsername(String username);
+
+}
diff --git a/auth_microservice/src/main/java/microservice/repositories/UserRepository.java b/auth_microservice/src/main/java/microservice/repositories/UserRepository.java
new file mode 100644
index 00000000..fb0766a4
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/repositories/UserRepository.java
@@ -0,0 +1,12 @@
+package microservice.repositories;
+
+
+import microservice.models.User;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+
+public interface UserRepository extends MongoRepository {
+
+ public User findByUsername(String username);
+
+}
diff --git a/auth_microservice/src/main/java/microservice/services/SystemService.java b/auth_microservice/src/main/java/microservice/services/SystemService.java
new file mode 100644
index 00000000..05ba2834
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/services/SystemService.java
@@ -0,0 +1,110 @@
+package microservice.services;
+
+
+import microservice.repositories.SystemRepository;
+import microservice.util.PasswordHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import microservice.models.Authorization;
+import microservice.models.Message;
+import microservice.models.System;
+import java.util.concurrent.CompletableFuture;
+import org.springframework.scheduling.annotation.Async;
+import org.bson.types.ObjectId;
+import java.util.Calendar;
+import com.auth0.jwt.algorithms.Algorithm;
+import org.springframework.beans.factory.annotation.Value;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTCreationException;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+
+
+@Service
+public class SystemService {
+
+ @Value("${jwt.issuer}")
+ private String ISSUER;
+
+ @Value("${jwt.sys.secret}")
+ private String SECRET;
+
+ @Value("${jwt.duration.in.days}")
+ private int ACCESS_TOKEN_DURATION;
+
+ @Autowired
+ private SystemRepository systemRepo;
+
+
+ @Async("ThreadPoolExecutor")
+ public CompletableFuture register(System system) {
+ system.set_id(ObjectId.get());
+ system.setPassword(PasswordHandler.encryptPassword(system.getPassword()));
+ return CompletableFuture.completedFuture(systemRepo.save(system));
+ }
+
+ @Async("ThreadPoolExecutor")
+ public CompletableFuture> authenticate(System system) {
+ System storedSystem = systemRepo.findByUsername(system.getUsername());
+ String msg = "invalid username or password";
+
+ if (storedSystem != null) {
+ boolean isValidPassword = PasswordHandler.checkPassword(system.getPassword(),
+ storedSystem.getPassword());
+
+ if (isValidPassword) {
+ Calendar issuedAt = Calendar.getInstance();
+ Calendar expiresAt = Calendar.getInstance();
+ expiresAt.add(Calendar.DATE, ACCESS_TOKEN_DURATION);
+
+ try {
+ Algorithm algorithm = Algorithm.HMAC256(SECRET);
+ String token = JWT.create()
+ .withIssuer(ISSUER)
+ .withIssuedAt(issuedAt.getTime())
+ .withExpiresAt(expiresAt.getTime())
+ .withClaim("systemId", storedSystem.get_id())
+ .sign(algorithm);
+
+ Authorization auth = new Authorization(token, expiresAt.getTime(), storedSystem.get_id(), true);
+ return CompletableFuture.completedFuture(auth);
+ }
+ catch (JWTCreationException e) {
+ // invalid signing configuration / couldn't convert claims
+ msg = e.getMessage();
+ }
+ }
+ }
+
+ return CompletableFuture.completedFuture(new Message(msg, null, false));
+ }
+
+ @Async("ThreadPoolExecutor")
+ public CompletableFuture authorize(String token) {
+ String msg = "and error has occurred";
+ boolean status = false;
+ String systemId = null;
+ try {
+ Algorithm algorithm = Algorithm.HMAC256(SECRET);
+ JWTVerifier verifier = JWT.require(algorithm)
+ .withIssuer(ISSUER)
+ .build();
+
+ DecodedJWT jwt = verifier.verify(token);
+ systemId = jwt.getClaim("systemId").asString();
+ msg = "valid token";
+ status = true;
+ }
+ catch (JWTVerificationException e) {
+ msg = "invalid token";
+ }
+ catch (NullPointerException e) {
+ msg = "no token provided";
+ }
+
+ return CompletableFuture.completedFuture(new Message(msg, systemId, status));
+ }
+
+
+}
diff --git a/auth_microservice/src/main/java/microservice/services/UserService.java b/auth_microservice/src/main/java/microservice/services/UserService.java
new file mode 100644
index 00000000..5166b24c
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/services/UserService.java
@@ -0,0 +1,109 @@
+package microservice.services;
+
+
+import microservice.repositories.UserRepository;
+import microservice.util.PasswordHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import microservice.models.Authorization;
+import microservice.models.Message;
+import microservice.models.User;
+import java.util.concurrent.CompletableFuture;
+import org.springframework.scheduling.annotation.Async;
+import org.bson.types.ObjectId;
+import java.util.Calendar;
+import com.auth0.jwt.algorithms.Algorithm;
+import org.springframework.beans.factory.annotation.Value;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.exceptions.JWTCreationException;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+
+
+@Service
+public class UserService {
+
+ @Value("${jwt.issuer}")
+ private String ISSUER;
+
+ @Value("${jwt.usr.secret}")
+ private String SECRET;
+
+ @Value("${jwt.duration.in.days}")
+ private int ACCESS_TOKEN_DURATION;
+
+ @Autowired
+ private UserRepository userRepo;
+
+
+ @Async("ThreadPoolExecutor")
+ public CompletableFuture register(User user) {
+ user.set_id(ObjectId.get());
+ user.setPassword(PasswordHandler.encryptPassword(user.getPassword()));
+ return CompletableFuture.completedFuture(userRepo.save(user));
+ }
+
+ @Async("ThreadPoolExecutor")
+ public CompletableFuture> authenticate(User user) {
+ User storedUser = userRepo.findByUsername(user.getUsername());
+ String msg = "invalid username or password";
+
+ if (storedUser != null) {
+ boolean isValidPassword = PasswordHandler.checkPassword(user.getPassword(),
+ storedUser.getPassword());
+
+ if (isValidPassword) {
+ Calendar issuedAt = Calendar.getInstance();
+ Calendar expiresAt = Calendar.getInstance();
+ expiresAt.add(Calendar.DATE, ACCESS_TOKEN_DURATION);
+
+ try {
+ Algorithm algorithm = Algorithm.HMAC256(SECRET);
+ String token = JWT.create()
+ .withIssuer(ISSUER)
+ .withIssuedAt(issuedAt.getTime())
+ .withExpiresAt(expiresAt.getTime())
+ .withClaim("userId", storedUser.get_id())
+ .sign(algorithm);
+
+ Authorization auth = new Authorization(token, expiresAt.getTime(), storedUser.get_id(), true);
+ return CompletableFuture.completedFuture(auth);
+ }
+ catch (JWTCreationException e) {
+ // invalid signing configuration / couldn't convert claims
+ msg = e.getMessage();
+ }
+ }
+ }
+
+ return CompletableFuture.completedFuture(new Message(msg, null, false));
+ }
+
+ @Async("ThreadPoolExecutor")
+ public CompletableFuture authorize(String token) {
+ String msg = "an error has occurred";
+ boolean status = false;
+ String userId = null;
+ try {
+ Algorithm algorithm = Algorithm.HMAC256(SECRET);
+ JWTVerifier verifier = JWT.require(algorithm)
+ .withIssuer(ISSUER)
+ .build();
+
+ DecodedJWT jwt = verifier.verify(token);
+ userId = jwt.getClaim("userId").asString();
+ msg = "valid token";
+ status = true;
+ }
+ catch (JWTVerificationException e) {
+ msg = "invalid token";
+ }
+ catch (NullPointerException e) {
+ msg = "no token provided";
+ }
+
+ return CompletableFuture.completedFuture(new Message(msg, userId, status));
+ }
+
+}
diff --git a/auth_microservice/src/main/java/microservice/util/PasswordHandler.java b/auth_microservice/src/main/java/microservice/util/PasswordHandler.java
new file mode 100644
index 00000000..77bd1289
--- /dev/null
+++ b/auth_microservice/src/main/java/microservice/util/PasswordHandler.java
@@ -0,0 +1,29 @@
+package microservice.util;
+
+
+import org.mindrot.jbcrypt.BCrypt;
+import org.springframework.stereotype.Component;
+
+
+@Component
+public class PasswordHandler {
+
+ private static int workload = 12;
+
+ public static String encryptPassword(String plaintextPassword) {
+ String salt = BCrypt.gensalt(workload);
+ String hashedPassword = BCrypt.hashpw(plaintextPassword, salt);
+ return hashedPassword;
+ }
+
+ public static boolean checkPassword(String plaintextPassword, String storedHash) {
+ boolean verifiedPassword = false;
+
+ if (null == storedHash || !storedHash.startsWith("$2a$"))
+ throw new java.lang.IllegalArgumentException("invalid hash provided for comparison");
+
+ verifiedPassword = BCrypt.checkpw(plaintextPassword, storedHash);
+
+ return verifiedPassword;
+ }
+}
\ No newline at end of file
diff --git a/auth_microservice/src/main/resources/application.properties b/auth_microservice/src/main/resources/application.properties
new file mode 100644
index 00000000..9628d8ac
--- /dev/null
+++ b/auth_microservice/src/main/resources/application.properties
@@ -0,0 +1,8 @@
+jwt.usr.secret=dev@ibm#santanderTEST$11043512/Uz3r}Auth
+jwt.sys.secret=dev@ibm#santanderTEST$11043512/SYZZ{Auth
+jwt.issuer=http://api.santandertest.com.br
+thread.pool.core.size=7
+thread.pool.max.size=42
+thread.queue.capacity=11
+jwt.duration.in.days=14
+server.port=8080
\ No newline at end of file
diff --git a/postman_testbench/IBM-Santander.postman_collection.json b/postman_testbench/IBM-Santander.postman_collection.json
new file mode 100644
index 00000000..14171723
--- /dev/null
+++ b/postman_testbench/IBM-Santander.postman_collection.json
@@ -0,0 +1,543 @@
+{
+ "info": {
+ "_postman_id": "a0d948d5-d8b6-4712-9527-17a8717d6bfc",
+ "name": "IBM-Santander",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "auth microservice",
+ "item": [
+ {
+ "name": "register new user",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "id": "10762c41-aadb-42f9-abe5-2fd62e7727a9",
+ "exec": [
+ "pm.test(\"Status code is 201\", function () {",
+ " pm.response.to.have.status(201);",
+ " if (pm.response.to.have.header(\"Location\")) {",
+ " const id = postman.getResponseHeader('Location').replace(pm.variables.get(\"auth_url\"), '');",
+ " pm.environment.set(\"usr_id\", id);",
+ " }",
+ "});",
+ "",
+ "",
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "value": "application/json",
+ "type": "text"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\t\"username\": \"{{usr}}\",\n\t\"password\": \"{{usr_pwd}}\"\n}"
+ },
+ "url": {
+ "raw": "{{auth_url}}users",
+ "host": [
+ "{{auth_url}}users"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "register new system",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "id": "10762c41-aadb-42f9-abe5-2fd62e7727a9",
+ "exec": [
+ "pm.test(\"Status code is 201\", function () {",
+ " pm.response.to.have.status(201);",
+ " if (pm.response.to.have.header(\"Location\")) {",
+ " const id = postman.getResponseHeader('Location').replace(pm.variables.get(\"auth_url\"), '');",
+ " pm.environment.set(\"sys_id\", id);",
+ " }",
+ "});",
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\t\"username\": \"{{sys}}\",\n\t\"password\": \"{{sys_pwd}}\"\n}"
+ },
+ "url": {
+ "raw": "{{auth_url}}systems",
+ "host": [
+ "{{auth_url}}systems"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "user login",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "id": "1117d661-8bb3-44af-9ca3-2cec7cd1347c",
+ "exec": [
+ "pm.test(\"Status code is 200\", function () {",
+ " pm.environment.set(\"token\", pm.response.json().accessToken);",
+ " pm.environment.set(\"usr_id\", pm.response.json().clientId);",
+ "});",
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "prerequest",
+ "script": {
+ "id": "cab6fe3e-4c98-43bc-96a1-4640849ac2b9",
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "value": "application/json",
+ "type": "text"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\t\"username\": \"{{usr}}\",\n\t\"password\": \"{{usr_pwd}}\"\n}"
+ },
+ "url": {
+ "raw": "{{auth_url}}users/authentication",
+ "host": [
+ "{{auth_url}}users"
+ ],
+ "path": [
+ "authentication"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "system login",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "id": "1117d661-8bb3-44af-9ca3-2cec7cd1347c",
+ "exec": [
+ "pm.test(\"Status code is 200\", function () {",
+ " pm.environment.set(\"token\", pm.response.json().accessToken);",
+ " pm.environment.set(\"sys_id\", pm.response.json().clientId);",
+ "});",
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\t\"username\": \"{{sys}}\",\n\t\"password\": \"{{sys_pwd}}\"\n}"
+ },
+ "url": {
+ "raw": "{{auth_url}}systems/authentication",
+ "host": [
+ "{{auth_url}}systems"
+ ],
+ "path": [
+ "authentication"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "validate user token",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "id": "1117d661-8bb3-44af-9ca3-2cec7cd1347c",
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ },
+ {
+ "key": "Authorization",
+ "value": "{{token}}",
+ "type": "text"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": ""
+ },
+ "url": {
+ "raw": "{{auth_url}}users/authorization",
+ "host": [
+ "{{auth_url}}users"
+ ],
+ "path": [
+ "authorization"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "validate system token",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "id": "1117d661-8bb3-44af-9ca3-2cec7cd1347c",
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ },
+ {
+ "key": "Authorization",
+ "value": "{{token}}",
+ "type": "text"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": ""
+ },
+ "url": {
+ "raw": "{{auth_url}}systems/authorization",
+ "host": [
+ "{{auth_url}}systems"
+ ],
+ "path": [
+ "authorization"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "spends microservice",
+ "item": [
+ {
+ "name": "register new spend",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "id": "7075ca05-67af-45ac-943f-e4ad20c8d6b7",
+ "exec": [
+ "pm.test(\"Status code is 201\", function () {",
+ " pm.response.to.have.status(201);",
+ " pm.environment.set(\"spend_id\", pm.response.json()._id);",
+ "});",
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "prerequest",
+ "script": {
+ "id": "2164e194-f49f-4265-8f7d-57724ef44607",
+ "exec": [
+ "pm.environment.set(\"spend_date\", (new Date()).toISOString().replace('Z', '+0000'));"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "disabled": false,
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Authorization",
+ "type": "text",
+ "value": "{{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{ \n\t\"description\": \"testSpend1\", \n\t\"value\": 99.98123456, \n\t\"userCode\": \"{{usr_id}}\",\n\t\"date\": \"{{spend_date}}\"\n}"
+ },
+ "url": {
+ "raw": "{{spend_url}}spends",
+ "host": [
+ "{{spend_url}}spends"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "retrieve user spends",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "id": "7075ca05-67af-45ac-943f-e4ad20c8d6b7",
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "prerequest",
+ "script": {
+ "id": "2164e194-f49f-4265-8f7d-57724ef44607",
+ "exec": [
+ "const now = new Date();",
+ "pm.environment.set(\"endDate\", now.toISOString().replace('Z', '+0000'));",
+ "",
+ "let past = new Date();",
+ "past.setDate(now.getDate() - 21);",
+ "pm.environment.set(\"startDate\", past.toISOString().replace('Z', '+0000'));"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "disabled": false,
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Authorization",
+ "type": "text",
+ "value": "{{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": ""
+ },
+ "url": {
+ "raw": "{{spend_url}}users/spends?start_date={{startDate}}&end_date={{endDate}}",
+ "host": [
+ "{{spend_url}}users"
+ ],
+ "path": [
+ "spends"
+ ],
+ "query": [
+ {
+ "key": "start_date",
+ "value": "{{startDate}}"
+ },
+ {
+ "key": "end_date",
+ "value": "{{endDate}}"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "update category",
+ "request": {
+ "method": "PATCH",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ },
+ {
+ "key": "Authorization",
+ "value": "{{token}}",
+ "equals": true
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\t\"category\": \"newCategory\"\n}"
+ },
+ "url": {
+ "raw": "{{spend_url}}spends/{{spend_id}}/categories",
+ "host": [
+ "{{spend_url}}spends"
+ ],
+ "path": [
+ "{{spend_id}}",
+ "categories"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "category suggestions",
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "{{token}}",
+ "type": "text"
+ }
+ ],
+ "body": {},
+ "url": {
+ "raw": "{{spend_url}}categories/suggestions?partial_name=cat",
+ "host": [
+ "{{spend_url}}categories"
+ ],
+ "path": [
+ "suggestions"
+ ],
+ "query": [
+ {
+ "key": "partial_name",
+ "value": "cat"
+ }
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "id": "55867996-6115-4d11-84bf-f549f04450af",
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "id": "1f5beed3-6e7b-44c1-b9a7-2dfa0104c40a",
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
+ ],
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "id": "3f578014-977b-4640-a289-ffd79438b179",
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "id": "751bf649-adb9-4061-b73d-511aae36d620",
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/postman_testbench/IBM-Santander.postman_environment.json b/postman_testbench/IBM-Santander.postman_environment.json
new file mode 100644
index 00000000..4f59167a
--- /dev/null
+++ b/postman_testbench/IBM-Santander.postman_environment.json
@@ -0,0 +1,119 @@
+{
+ "id": "e4e4105e-af64-4847-a690-7dbaa64883eb",
+ "name": "IBM-Santander",
+ "values": [
+ {
+ "key": "token",
+ "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzeXN0ZW1JZCI6IjVjMWIyOGVlZGQzYjdlM2VlZGZkNjQ4NCIsImlzcyI6Imh0dHA6Ly9hcGkuc2FudGFuZGVydGVzdC5jb20uYnIiLCJleHAiOjE1NDY0OTM0MzUsImlhdCI6MTU0NTI4MzgzNX0.aqKw1Aodhjf2rYLJ6XeS7Y6bDzMnAwi80ekj85UPIyg",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "usr_id",
+ "value": "5c1b1cc9dd3b7e3eedfd6482",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "usr",
+ "value": "testUser",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "usr_pwd",
+ "value": "testUser12345",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "sys_id",
+ "value": "5c1b28eedd3b7e3eedfd6484",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "sys",
+ "value": "testSystem",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "sys_pwd",
+ "value": "testSystem12345",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "auth_url",
+ "value": "http://localhost:8080/",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "spend_url",
+ "value": "http://localhost:8081/",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ },
+ {
+ "key": "spend_date",
+ "value": "2018-12-20T05:31:04.054+0000",
+ "enabled": true
+ },
+ {
+ "key": "endDate",
+ "value": "2018-12-20T05:25:06.567+0000",
+ "enabled": true
+ },
+ {
+ "key": "startDate",
+ "value": "2018-11-29T05:25:06.567+0000",
+ "enabled": true
+ },
+ {
+ "key": "now",
+ "value": "2018-12-16T07:54:32.744+0000",
+ "enabled": true
+ },
+ {
+ "key": "spend_id",
+ "value": "5c1b2918dd3b7e47dfcc6ee3",
+ "description": {
+ "content": "",
+ "type": "text/plain"
+ },
+ "enabled": true
+ }
+ ],
+ "_postman_variable_scope": "environment",
+ "_postman_exported_at": "2018-12-20T05:32:42.129Z",
+ "_postman_exported_using": "Postman/6.6.1"
+}
\ No newline at end of file
diff --git a/spend_microservice/pom.xml b/spend_microservice/pom.xml
new file mode 100644
index 00000000..bfa9d905
--- /dev/null
+++ b/spend_microservice/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ br.com.testesantander.api
+ spend_microservice
+ 0.1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.5.RELEASE
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.jayway.jsonpath
+ json-path
+ test
+
+
+
+
+ 1.8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ spring-releases
+ https://repo.spring.io/libs-release
+
+
+
+
\ No newline at end of file
diff --git a/spend_microservice/src/main/java/microservice/SpendsMicroservice.java b/spend_microservice/src/main/java/microservice/SpendsMicroservice.java
new file mode 100644
index 00000000..e2f1774b
--- /dev/null
+++ b/spend_microservice/src/main/java/microservice/SpendsMicroservice.java
@@ -0,0 +1,41 @@
+package microservice;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.beans.factory.annotation.Value;
+
+@EnableAsync
+@SpringBootApplication
+public class SpendsMicroservice {
+
+ @Value("${thread.pool.core.size}")
+ private int corePoolSize;
+
+ @Value("${thread.pool.max.size}")
+ private int maxPoolSize;
+
+ @Value("${thread.queue.capacity}")
+ private int queueCapacity;
+
+
+ @Bean(name = "ThreadPoolExecutor")
+ public TaskExecutor getAsyncExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(corePoolSize);
+ executor.setMaxPoolSize(maxPoolSize);
+ executor.setQueueCapacity(queueCapacity);
+ executor.setThreadNamePrefix("ThreadPoolExecutor-");
+ executor.initialize();
+ return executor;
+ }
+
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpendsMicroservice.class, args);
+ }
+
+}
diff --git a/spend_microservice/src/main/java/microservice/configurations/WebMvcConfig.java b/spend_microservice/src/main/java/microservice/configurations/WebMvcConfig.java
new file mode 100644
index 00000000..d76fab25
--- /dev/null
+++ b/spend_microservice/src/main/java/microservice/configurations/WebMvcConfig.java
@@ -0,0 +1,24 @@
+package microservice.configurations;
+
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import microservice.interceptors.JWTInterceptor;
+import org.springframework.context.annotation.Bean;
+
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ @Bean
+ public JWTInterceptor jwtInterceptor() {
+ return new JWTInterceptor();
+ }
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry){
+ registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**");
+ }
+
+}
diff --git a/spend_microservice/src/main/java/microservice/controllers/CategoryController.java b/spend_microservice/src/main/java/microservice/controllers/CategoryController.java
new file mode 100644
index 00000000..a3c93c7b
--- /dev/null
+++ b/spend_microservice/src/main/java/microservice/controllers/CategoryController.java
@@ -0,0 +1,41 @@
+package microservice.controllers;
+
+
+import java.util.List;
+import microservice.models.Category;
+import javax.validation.constraints.Size;
+import org.springframework.http.ResponseEntity;
+import microservice.services.CategoryService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import org.springframework.http.MediaType;
+
+
+@RestController
+@Validated
+public class CategoryController {
+
+ @Autowired
+ private CategoryService categoryService;
+
+
+ @RequestMapping(value = "/categories/suggestions",
+ method = RequestMethod.GET,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity> getSuggestedCategories(
+ @Size(min=3, message="partial_name should contain at least 3 characters")
+ @RequestParam(value="partial_name") String partialCategoryName) throws InterruptedException, ExecutionException {
+
+ CompletableFuture> categoryFuture = categoryService.listSimilarCategories(partialCategoryName);
+ List categoryList = categoryFuture.get();
+ return ResponseEntity.ok(categoryList);
+
+ }
+
+}
diff --git a/spend_microservice/src/main/java/microservice/controllers/SpendController.java b/spend_microservice/src/main/java/microservice/controllers/SpendController.java
new file mode 100644
index 00000000..3e5e4c74
--- /dev/null
+++ b/spend_microservice/src/main/java/microservice/controllers/SpendController.java
@@ -0,0 +1,91 @@
+package microservice.controllers;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+import org.bson.types.ObjectId;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import javax.validation.Valid;
+import javax.xml.bind.ValidationException;
+import microservice.models.Message;
+import microservice.models.Spend;
+import org.springframework.http.ResponseEntity;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.ParseException;
+import org.springframework.web.util.UriComponentsBuilder;
+import microservice.services.SpendService;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestParam;
+import javax.servlet.http.HttpServletRequest;
+import microservice.models.Category;
+
+
+@RestController
+public class SpendController {
+
+
+ @Autowired
+ private SpendService spendService;
+
+
+ @RequestMapping(value = "/spends",
+ method = RequestMethod.POST,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity register(UriComponentsBuilder builder,
+ @Valid @RequestBody Spend spend) throws URISyntaxException, InterruptedException, ExecutionException {
+
+ CompletableFuture spendFuture = spendService.insert(spend);
+ Spend storedSpend = spendFuture.get();
+ return ResponseEntity
+ .created(new URI(builder.toUriString() + "/spends/" + spend.get_id()))
+ .body(storedSpend);
+
+ }
+
+
+ @RequestMapping(value = "/users/spends",
+ method = RequestMethod.GET,
+ produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseEntity