diff --git a/build.gradle b/build.gradle index 8589ac2..e3b7309 100644 --- a/build.gradle +++ b/build.gradle @@ -33,12 +33,22 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'io.jsonwebtoken:jjwt-api:0.13.0' + implementation 'org.mapstruct:mapstruct:1.6.3' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + testImplementation 'org.springframework.boot:spring-boot-starter-data-mongodb-test' testImplementation 'org.springframework.boot:spring-boot-starter-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0' } dependencyManagement { diff --git a/src/main/java/com/javanauta/taskscheduler/TaskSchedulerApplication.java b/src/main/java/com/javanauta/taskscheduler/TaskSchedulerApplication.java index eae7aec..a5dc459 100644 --- a/src/main/java/com/javanauta/taskscheduler/TaskSchedulerApplication.java +++ b/src/main/java/com/javanauta/taskscheduler/TaskSchedulerApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication +@EnableFeignClients public class TaskSchedulerApplication { public static void main(String[] args) { diff --git a/src/main/java/com/javanauta/taskscheduler/business/TaskService.java b/src/main/java/com/javanauta/taskscheduler/business/TaskService.java new file mode 100644 index 0000000..aed3815 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/business/TaskService.java @@ -0,0 +1,68 @@ +package com.javanauta.taskscheduler.business; + +import com.javanauta.taskscheduler.business.dto.TaskDTO; +import com.javanauta.taskscheduler.business.mapper.TaskConverter; +import com.javanauta.taskscheduler.business.mapper.TaskUpdateConverter; +import com.javanauta.taskscheduler.infrastructure.entity.Task; +import com.javanauta.taskscheduler.infrastructure.enums.NotificationStatusEnum; +import com.javanauta.taskscheduler.infrastructure.exception.ResourceNotFoundException; +import com.javanauta.taskscheduler.infrastructure.repository.TaskRepository; +import com.javanauta.taskscheduler.infrastructure.security.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.osgi.resource.Resource; +import org.springframework.security.core.parameters.P; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TaskService { + + private final TaskRepository taskRepository; + private final TaskConverter taskConverter; + private final JwtUtil jwtUtil; + private final TaskUpdateConverter taskUpdateConverter; + + public TaskDTO saveTask(String token, TaskDTO taskDTO) { + + taskDTO.setUserEmail(jwtUtil.extractUsername(token.substring(7))); + taskDTO.setCreationDateTime(LocalDateTime.now()); + taskDTO.setNotificationStatusEnum(NotificationStatusEnum.PENDING); + + Task task = taskConverter.toTask(taskDTO); + + return taskConverter.toTaskDTO(taskRepository.save(task)); + } + + public List findTaskByTimePeriod(LocalDateTime initialDateTime, LocalDateTime finalDateTime) { + return taskConverter.toTaskDTOList(taskRepository.findByDueDateTimeBetween(initialDateTime, finalDateTime)); + } + + public List findTaskByUserEmail(String token) { + return taskConverter.toTaskDTOList(taskRepository.findByUserEmail(jwtUtil.extractUsername(token.substring(7)))); + } + + public void deleteTaskById(String id) { + if (taskRepository.existsById(id)) { + taskRepository.deleteById(id); + } else { + throw new ResourceNotFoundException(String.format("Task with ID '%s' not found.", id)); + } + } + + public TaskDTO modifyTaskStatusById (NotificationStatusEnum notificationStatusEnum, String id) { + Task task = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(String.format("Task with ID '%s' not found.", id))); + task.setNotificationStatusEnum(notificationStatusEnum); + + return taskConverter.toTaskDTO(taskRepository.save(task)); + } + + public TaskDTO updateTaskById (TaskDTO taskDTO, String id) { + Task task = taskRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(String.format("Task with ID '%s' not found.", id))); + + taskUpdateConverter.updateTasks(taskDTO, task); + return taskConverter.toTaskDTO(taskRepository.save(task)); + } +} diff --git a/src/main/java/com/javanauta/taskscheduler/business/dto/TaskDTO.java b/src/main/java/com/javanauta/taskscheduler/business/dto/TaskDTO.java new file mode 100644 index 0000000..a0ef054 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/business/dto/TaskDTO.java @@ -0,0 +1,27 @@ +package com.javanauta.taskscheduler.business.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.javanauta.taskscheduler.infrastructure.enums.NotificationStatusEnum; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TaskDTO { + + private String id; + private String name; + private String description; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss") + private LocalDateTime creationDateTime; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss") + private LocalDateTime dueDateTime; + private String userEmail; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss") + private LocalDateTime modificationDateTime; + private NotificationStatusEnum notificationStatusEnum; +} diff --git a/src/main/java/com/javanauta/taskscheduler/business/dto/UserDTO.java b/src/main/java/com/javanauta/taskscheduler/business/dto/UserDTO.java new file mode 100644 index 0000000..c5059be --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/business/dto/UserDTO.java @@ -0,0 +1,14 @@ +package com.javanauta.taskscheduler.business.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserDTO { + + private String email; + private String password; +} diff --git a/src/main/java/com/javanauta/taskscheduler/business/mapper/TaskConverter.java b/src/main/java/com/javanauta/taskscheduler/business/mapper/TaskConverter.java new file mode 100644 index 0000000..3cbb472 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/business/mapper/TaskConverter.java @@ -0,0 +1,16 @@ +package com.javanauta.taskscheduler.business.mapper; + +import com.javanauta.taskscheduler.business.dto.TaskDTO; +import com.javanauta.taskscheduler.infrastructure.entity.Task; +import org.mapstruct.Mapper; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface TaskConverter { + + Task toTask(TaskDTO taskDTO); + TaskDTO toTaskDTO(Task task); + List toTaskList(List taskDTOList); + List toTaskDTOList(List taskList); +} diff --git a/src/main/java/com/javanauta/taskscheduler/business/mapper/TaskUpdateConverter.java b/src/main/java/com/javanauta/taskscheduler/business/mapper/TaskUpdateConverter.java new file mode 100644 index 0000000..15b0bfe --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/business/mapper/TaskUpdateConverter.java @@ -0,0 +1,13 @@ +package com.javanauta.taskscheduler.business.mapper; + +import com.javanauta.taskscheduler.business.dto.TaskDTO; +import com.javanauta.taskscheduler.infrastructure.entity.Task; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; + +@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface TaskUpdateConverter { + + void updateTasks(TaskDTO taskDTO, @MappingTarget Task task); +} diff --git a/src/main/java/com/javanauta/taskscheduler/controller/TaskController.java b/src/main/java/com/javanauta/taskscheduler/controller/TaskController.java new file mode 100644 index 0000000..809b23d --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/controller/TaskController.java @@ -0,0 +1,54 @@ +package com.javanauta.taskscheduler.controller; + +import com.javanauta.taskscheduler.business.TaskService; +import com.javanauta.taskscheduler.business.dto.TaskDTO; +import com.javanauta.taskscheduler.infrastructure.enums.NotificationStatusEnum; +import feign.Param; +import lombok.RequiredArgsConstructor; +import org.apache.coyote.Response; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/task") +@RequiredArgsConstructor +public class TaskController { + + private final TaskService taskService; + + @PostMapping + public ResponseEntity saveTask(@RequestBody TaskDTO taskDTO, @RequestHeader("Authorization") String token) { + return ResponseEntity.ok(taskService.saveTask(token, taskDTO)); + } + + @GetMapping("/events") + public ResponseEntity> findTaskListByPeriod(@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime initialDateTime, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime finalDateTime) { + return ResponseEntity.ok(taskService.findTaskByTimePeriod(initialDateTime, finalDateTime)); + } + + @GetMapping + public ResponseEntity> findTaskListByUserEmail(@RequestHeader("Authorization") String token) { + return ResponseEntity.ok(taskService.findTaskByUserEmail(token)); + } + + @DeleteMapping + public ResponseEntity deleteTaskById(@RequestParam("id") String id){ + taskService.deleteTaskById(id); + return ResponseEntity.ok().build(); + } + + @PatchMapping + public ResponseEntity modifyTaskStatusById(@RequestParam("status") NotificationStatusEnum notificationStatusEnum, @RequestParam("id") String id) { + return ResponseEntity.ok(taskService.modifyTaskStatusById(notificationStatusEnum, id)); + } + + @PutMapping + public ResponseEntity updateTaskById(@RequestBody TaskDTO taskDTO, @RequestParam("id") String id) { + return ResponseEntity.ok(taskService.updateTaskById(taskDTO, id)); + } +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java new file mode 100644 index 0000000..7fa06d0 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java @@ -0,0 +1,14 @@ +package com.javanauta.taskscheduler.infrastructure.client; + +import com.javanauta.taskscheduler.business.dto.UserDTO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "user", url = "${user.url}") +public interface UserClient { + + @GetMapping("/user") + UserDTO getUserByEmail(@RequestParam("email") String email, @RequestHeader("Authorization") String token); +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/entity/Task.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/entity/Task.java new file mode 100644 index 0000000..b2cd884 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/entity/Task.java @@ -0,0 +1,27 @@ +package com.javanauta.taskscheduler.infrastructure.entity; + +import com.javanauta.taskscheduler.infrastructure.enums.NotificationStatusEnum; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Document("task") +public class Task { + + @Id + private String id; + private String name; + private String description; + private LocalDateTime creationDateTime; + private LocalDateTime dueDateTime; + private String userEmail; + private LocalDateTime modificationDateTime; + private NotificationStatusEnum notificationStatusEnum; +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/enums/NotificationStatusEnum.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/enums/NotificationStatusEnum.java new file mode 100644 index 0000000..093d896 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/enums/NotificationStatusEnum.java @@ -0,0 +1,7 @@ +package com.javanauta.taskscheduler.infrastructure.enums; + +public enum NotificationStatusEnum { + PENDING, + NOTIFIED, + CANCELED +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/exception/ResourceNotFoundException.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..3b14c96 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/exception/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package com.javanauta.taskscheduler.infrastructure.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } + public ResourceNotFoundException(String message, Throwable throwable) { super(message, throwable); } +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/repository/TaskRepository.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/repository/TaskRepository.java new file mode 100644 index 0000000..ec4fbd6 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/repository/TaskRepository.java @@ -0,0 +1,14 @@ +package com.javanauta.taskscheduler.infrastructure.repository; + +import com.javanauta.taskscheduler.infrastructure.entity.Task; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface TaskRepository extends MongoRepository { + List findByDueDateTimeBetween(LocalDateTime initialDateTime, LocalDateTime finalDateTime); + List findByUserEmail(String userEmail); +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtRequestFilter.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtRequestFilter.java new file mode 100644 index 0000000..30d8ce7 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtRequestFilter.java @@ -0,0 +1,60 @@ +package com.javanauta.taskscheduler.infrastructure.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +// Defines the JwtRequestFilter class, which extends OncePerRequestFilter +public class JwtRequestFilter extends OncePerRequestFilter { + + // Defines properties to store instances of JwtUtil and UserDetailsService + private final JwtUtil jwtUtil; + private final UserDetailsServiceImpl userDetailsService; + + // Constructor that initializes the properties with provided instances + public JwtRequestFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) { + this.jwtUtil = jwtUtil; + this.userDetailsService = userDetailsService; + } + + // Method called once per request to process the filter + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + // Gets the value of the "Authorization" header from the request + final String authorizationHeader = request.getHeader("Authorization"); + + // Checks whether the header exists and starts with "Bearer " + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + // Extracts the JWT token from the header + final String token = authorizationHeader.substring(7); + // Extracts the username from the JWT token + final String username = jwtUtil.extractUsername(token); + + // If the username is not null and the user is not yet authenticated + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // Loads user details from the username + UserDetails userDetails = userDetailsService.loadUserDetails(username, authorizationHeader); + // Validates the JWT token + if (jwtUtil.validateToken(token, username)) { + // Creates an authentication object with user information + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + // Sets authentication in the security context + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + } + + // Continues the filter chain, allowing the request to proceed + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtUtil.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtUtil.java new file mode 100644 index 0000000..530ba22 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtUtil.java @@ -0,0 +1,45 @@ +package com.javanauta.taskscheduler.infrastructure.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Service +public class JwtUtil { + + // Secret key used to sign and verify JWT tokens + private final String secretKey = "your-super-long-secret-key-that-needs-to-be-very-difficult"; + + // Extracts claims from the JWT token + public Claims extractClaims(String token) { + return Jwts.parser() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) // Sets secret key to validate signature + .build() + .parseClaimsJws(token) // Parses the JWT token and gets claims + .getBody(); // Returns claims body + } + + // Extracts username from the JWT token + public String extractUsername(String token) { + // Gets the subject (username) from the claims + return extractClaims(token).getSubject(); + } + + // Checks whether the JWT token is expired + public boolean isTokenExpired(String token) { + // Compares token expiration date with current date + return extractClaims(token).getExpiration().before(new Date()); + } + + // Validates the JWT token by checking username match and expiration + public boolean validateToken(String token, String username) { + // Extracts username from the token + final String extractedUsername = extractUsername(token); + // Checks if username matches and token is not expired + return (extractedUsername.equals(username) && !isTokenExpired(token)); + } +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/security/SecurityConfig.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/SecurityConfig.java new file mode 100644 index 0000000..f85ca7d --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.javanauta.taskscheduler.infrastructure.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + // JwtUtil and UserDetailsService instances injected by Spring + private final JwtUtil jwtUtil; + private final UserDetailsServiceImpl userDetailsService; + + // Constructor for dependency injection of JwtUtil and UserDetailsService + @Autowired + public SecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) { + this.jwtUtil = jwtUtil; + this.userDetailsService = userDetailsService; + } + + // Security filter configuration + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // Creates a JwtRequestFilter instance using JwtUtil and UserDetailsService + JwtRequestFilter jwtRequestFilter = new JwtRequestFilter(jwtUtil, userDetailsService); + + http + .csrf(AbstractHttpConfigurer::disable) // Disables CSRF protection for REST APIs (not needed in stateless APIs) + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() // Requires authentication for all other requests + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Sets session policy to stateless + ) + .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // Adds JWT filter before default authentication filter + + // Returns the built security configuration + return http.build(); + } +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..dc1c16c --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java @@ -0,0 +1,23 @@ +package com.javanauta.taskscheduler.infrastructure.security; + +import com.javanauta.taskscheduler.business.dto.UserDTO; +import com.javanauta.taskscheduler.infrastructure.client.UserClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailsServiceImpl { + + @Autowired + private UserClient userClient; + + public UserDetails loadUserDetails(String email, String token) { + UserDTO userDTO = userClient.getUserByEmail(email, token); + + return org.springframework.security.core.userdetails.User + .withUsername(userDTO.getEmail()) // Sets the username as the email + .password(userDTO.getPassword()) // Sets the user's password + .build(); // Builds the UserDetails object + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bbc2dd4..7705e91 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ spring.application.name=task-scheduler +spring.data.mongodb.uri=mongodb://localhost:27017 +spring.data.mongodb.database=task_scheduler_db + +user.url=localhost:8080 + +server.port=8081