diff --git a/Dockerfile.dev b/Dockerfile.dev
index 7b84d42..e9ed051 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,10 +1,9 @@
FROM openjdk:21-jdk-slim
WORKDIR /app
-COPY pom.xml .
-COPY mvnw .
COPY .mvn .mvn
+COPY mvnw pom.xml ./
RUN chmod +x mvnw
-RUN ./mvnw dependency:go-offline
+RUN ./mvnw -T 4 dependency:go-offline
COPY src ./src
EXPOSE 9096 5005
CMD ["./mvnw", "spring-boot:run", "-Dspring-boot.run.jvmArguments=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"]
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 9a85c78..2aacd74 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,9 @@
21
2025.0.0
+ 1.5.5.Final
+ 21
+ UTF-8
@@ -45,18 +48,18 @@
org.springframework.boot
spring-boot-starter-webflux
-
- org.springframework.cloud
- spring-cloud-starter-config
-
-
- org.springframework.cloud
- spring-cloud-starter-netflix-eureka-client
-
-
- org.springframework.cloud
- spring-cloud-starter-bootstrap
-
+
+ org.springframework.cloud
+ spring-cloud-starter-config
+
+
+ org.springframework.cloud
+ spring-cloud-starter-netflix-eureka-client
+
+
+ org.springframework.cloud
+ spring-cloud-starter-bootstrap
+
org.springframework.boot
spring-boot-devtools
@@ -89,32 +92,39 @@
org.springframework.boot
spring-boot-starter-aop
-
- org.mapstruct
- mapstruct
- 1.5.5.Final
-
-
- org.mapstruct
- mapstruct-processor
- 1.5.5.Final
- provided
-
+
org.springdoc
springdoc-openapi-starter-webflux-api
2.8.11
- org.springdoc
- springdoc-openapi-starter-webflux-ui
- 2.8.11
-
+ org.springdoc
+ springdoc-openapi-starter-webflux-ui
+ 2.8.11
+
org.springframework.cloud
spring-cloud-starter-circuitbreaker-reactor-resilience4j
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+ provided
+
+
+
@@ -135,15 +145,15 @@
maven-compiler-plugin
-
- org.mapstruct
- mapstruct-processor
- 1.5.5.Final
-
org.projectlombok
lombok
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
diff --git a/src/main/java/com/msvcchat/DTOs/.gitkeep b/src/main/java/com/msvcchat/DTOs/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/java/com/msvcchat/config/CorsConfig.java b/src/main/java/com/msvcchat/config/CorsConfig.java
new file mode 100644
index 0000000..c0e3edb
--- /dev/null
+++ b/src/main/java/com/msvcchat/config/CorsConfig.java
@@ -0,0 +1,25 @@
+package com.msvcchat.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.reactive.CorsWebFilter;
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
+
+@Configuration
+public class CorsConfig {
+ @Bean
+ public CorsWebFilter corsWebFilter() {
+ CorsConfiguration configuration = new CorsConfiguration();
+
+ // Especifica los orígenes permitidos
+ configuration.addAllowedOrigin("http://localhost:5173"); // Cambia esto según tu frontend
+ configuration.addAllowedMethod("*");
+ configuration.addAllowedHeader("*");
+ configuration.setAllowCredentials(true);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return new CorsWebFilter(source);
+ }
+}
diff --git a/src/main/java/com/msvcchat/config/ReactiveMapperConfig.java b/src/main/java/com/msvcchat/config/ReactiveMapperConfig.java
index 60072fe..fee16c1 100644
--- a/src/main/java/com/msvcchat/config/ReactiveMapperConfig.java
+++ b/src/main/java/com/msvcchat/config/ReactiveMapperConfig.java
@@ -15,12 +15,4 @@
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
)
public interface ReactiveMapperConfig {
-
- default Mono mapMono(Mono mono, Function mapper) {
- return mono.map(mapper);
- }
-
- default Flux mapFlux(Flux flux, Function mapper) {
- return flux.map(mapper);
- }
}
\ No newline at end of file
diff --git a/src/main/java/com/msvcchat/config/SwaggerConfig.java b/src/main/java/com/msvcchat/config/SwaggerConfig.java
index d8dfd3c..b0de96c 100644
--- a/src/main/java/com/msvcchat/config/SwaggerConfig.java
+++ b/src/main/java/com/msvcchat/config/SwaggerConfig.java
@@ -26,26 +26,13 @@
servers = {
@Server(
description = "Local Server",
- url = "http://localhost:9005"
+ url = "http://localhost:9096"
),
@Server(
description = "Production Server",
url = "https://"
)
}
-// ,
-// security = @SecurityRequirement(
-// name = "securityToken"
-// )
-//)
-//@SecurityScheme(
-// name = "securityToken",
-// description = "Access Token For My API",
-// type = SecuritySchemeType.HTTP,
-// paramName = HttpHeaders.AUTHORIZATION,
-// in = SecuritySchemeIn.HEADER,
-// scheme = "bearer",
-// bearerFormat = "JWT"
)
public class SwaggerConfig {
diff --git a/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java b/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java
index 06ac8e5..7932bda 100644
--- a/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java
+++ b/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java
@@ -1,9 +1,18 @@
package com.msvcchat.config.websockets;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.msvcchat.dtos.ChatMessageDto;
+import com.msvcchat.dtos.CreateChatMessageDto;
import com.msvcchat.entity.ChatMessage;
+import com.msvcchat.mappers.ChatMessageMapper;
import com.msvcchat.repositories.ChatMessageRepository;
+import com.msvcchat.service.ChatRoomManager;
+import com.msvcchat.service.ChatService;
+import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.mongodb.core.ChangeStreamEvent;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.socket.WebSocketHandler;
@@ -18,12 +27,15 @@
@Component
@RequiredArgsConstructor
+@Slf4j
public class ChatWebSocketHandler implements WebSocketHandler {
-
+ private final ChatService chatService;
private final ChatMessageRepository repo;
- private final ObjectMapper mapper = new ObjectMapper();
+ private final ChatRoomManager roomManager;
+ private final ChatMessageMapper mapper;
private final Map> sinks = new ConcurrentHashMap<>();
private final ReactiveMongoTemplate mongoTemplate;
+ private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
// public ChatWebSocketHandler(ChatMessageRepository repo, ReactiveMongoTemplate mongoTemplate) {
// this.repo = repo;
@@ -39,48 +51,67 @@ private Sinks.Many sinkFor(String roomId) {
public Mono handle(WebSocketSession session) {
String path = session.getHandshakeInfo().getUri().getPath();
String roomId = path.substring(path.lastIndexOf('/') + 1);
- Sinks.Many sink = sinkFor(roomId);
+
+ Sinks.Many sink = roomManager.sinkFor(roomId);
Mono inbound = session.receive()
.map(WebSocketMessage::getPayloadAsText)
.flatMap(text -> {
try {
- ChatMessage msg = mapper.readValue(text, ChatMessage.class);
- msg.setRoomId(roomId);
- return repo.save(msg)
- .doOnNext(saved -> sink.tryEmitNext(saved));
- } catch (Exception e) {
+ CreateChatMessageDto createDto = objectMapper.readValue(text, CreateChatMessageDto.class);
+ log.info("**** ENVIANDO MENSAJE {} ****", text);
+ log.info("***** CHAT DTO ****");
+ log.info(createDto.toString());
+ Mono response = chatService.saveMessage(roomId, createDto).then();
+ log.info("***** RESPONSE ****");
+ log.info(response.toString());
+ return response;
+ } catch (
+ Exception e) {
return Mono.empty();
}
})
.then();
Flux outbound = sink.asFlux()
+ .distinct(ChatMessage::getId)
.map(m -> {
- try { return mapper.writeValueAsString(m); } catch (Exception e) { return "{}";}
+ try {
+ ChatMessageDto dto = mapper.toDto(m);
+ dto.setId(m.getId());
+ log.info("****** CHAT MESSAGE DTO {} ", dto);
+ return objectMapper.writeValueAsString(dto);
+ } catch (
+ Exception e) {
+ e.printStackTrace();
+ log.error("*************ERRROR *********");
+ log.error(e.getMessage());
+ return "{}";
+ }
})
.map(session::textMessage);
- // cuando cliente se conecta, opcional: enviar historial reciente
- Flux history = repo.findByRoomIdOrderByCreatedAtAsc(roomId)
- .map(m -> {
- try { return mapper.writeValueAsString(m); } catch (Exception e) { return "{}"; }
+ Flux history = chatService.getHistory(roomId)
+ .map(dto -> {
+ try {
+ return objectMapper.writeValueAsString(dto);
+ } catch (
+ Exception e) {
+ return "{}";
+ }
})
.map(session::textMessage);
- return session.send(Flux.concat(history, outbound)).and(inbound);
+ return session.send(history.concatWith(outbound)).and(inbound);
}
- /**
- * Change Stream listener: cuando hay inserts en collection "messages",
- * emitimos a los sinks locales (para propagar mensajes entre instancias).
- * Requiere replica set.
- */
- private void startChangeStreamListener() {
- // escucha sin filtro (puedes filtrar por ns/collection o por roomId)
+
+ @PostConstruct
+ void startChangeStreamListener() {
mongoTemplate.changeStream(ChatMessage.class)
- .listen() // devuelve Flux>
- .map(event -> event.getBody()) // ChatMessage
+ .listen()
+ .mapNotNull(ChangeStreamEvent::getBody)
+ .distinct(ChatMessage::getId) // Evita procesar mensajes duplicados
.subscribe(msg -> {
if (msg != null && msg.getRoomId() != null) {
Sinks.Many s = sinks.get(msg.getRoomId());
@@ -89,8 +120,32 @@ private void startChangeStreamListener() {
}
}
}, err -> {
- // en prod haz reintentos/monitorización
- err.printStackTrace();
+ log.error("Error en ChangeStream: {}", err.getMessage(), err);
});
}
+
}
+
+/**
+ * Change Stream listener: cuando hay inserts en collection "messages",
+ * emitimos a los sinks locales (para propagar mensajes entre instancias).
+ * Requiere replica set.
+ */
+//private void startChangeStreamListener() {
+// // escucha sin filtro (puedes filtrar por ns/collection o por roomId)
+// mongoTemplate.changeStream(ChatMessage.class)
+// .listen() // devuelve Flux>
+// .map(event -> event.getBody()) // ChatMessage
+// .subscribe(msg -> {
+// if (msg != null && msg.getRoomId() != null) {
+// Sinks.Many s = sinks.get(msg.getRoomId());
+// if (s != null) {
+// s.tryEmitNext(msg);
+// }
+// }
+// }, err -> {
+// // en prod haz reintentos/monitorización
+// err.printStackTrace();
+// });
+//}
+//}
diff --git a/src/main/java/com/msvcchat/config/websockets/WebSocketConfig.java b/src/main/java/com/msvcchat/config/websockets/WebSocketConfig.java
index 27fe767..4a78cd7 100644
--- a/src/main/java/com/msvcchat/config/websockets/WebSocketConfig.java
+++ b/src/main/java/com/msvcchat/config/websockets/WebSocketConfig.java
@@ -13,7 +13,7 @@ public class WebSocketConfig {
@Bean
public SimpleUrlHandlerMapping webSocketMapping(ChatWebSocketHandler handler) {
- Map map = Map.of("/ws/chat/{roomId}", handler);
+ Map map = Map.of("/ws/chat/*", handler);
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(10);
diff --git a/src/main/java/com/msvcchat/controller/ChatController.java b/src/main/java/com/msvcchat/controller/ChatController.java
new file mode 100644
index 0000000..7790a64
--- /dev/null
+++ b/src/main/java/com/msvcchat/controller/ChatController.java
@@ -0,0 +1,22 @@
+package com.msvcchat.controller;
+
+import com.msvcchat.dtos.UserDto;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/chat/users")
+public class ChatController {
+ private final ReactiveMongoTemplate mongoTemplate;
+
+ @GetMapping
+ public Flux getAllUsers() {
+ return mongoTemplate.findAll(UserDto.class, "users");
+ }
+
+}
diff --git a/src/main/java/com/msvcchat/dtos/ChatMessageDto.java b/src/main/java/com/msvcchat/dtos/ChatMessageDto.java
new file mode 100644
index 0000000..b373f5b
--- /dev/null
+++ b/src/main/java/com/msvcchat/dtos/ChatMessageDto.java
@@ -0,0 +1,17 @@
+package com.msvcchat.dtos;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.Instant;
+
+@Data
+@NoArgsConstructor
+public class ChatMessageDto {
+ private String id;
+ private String roomId;
+ private String fromId;
+ private String fromRole;
+ private String text;
+ private Instant createdAt;
+}
diff --git a/src/main/java/com/msvcchat/dtos/CreateChatMessageDto.java b/src/main/java/com/msvcchat/dtos/CreateChatMessageDto.java
new file mode 100644
index 0000000..960a30c
--- /dev/null
+++ b/src/main/java/com/msvcchat/dtos/CreateChatMessageDto.java
@@ -0,0 +1,13 @@
+package com.msvcchat.dtos;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class CreateChatMessageDto {
+ private String fromId;
+ private String fromRole;
+ private String text;
+
+}
diff --git a/src/main/java/com/msvcchat/dtos/UserDto.java b/src/main/java/com/msvcchat/dtos/UserDto.java
new file mode 100644
index 0000000..4630d53
--- /dev/null
+++ b/src/main/java/com/msvcchat/dtos/UserDto.java
@@ -0,0 +1,9 @@
+package com.msvcchat.dtos;
+
+public record UserDto(
+ String id,
+ String name,
+ String role,
+ String avatar
+) {
+}
diff --git a/src/main/java/com/msvcchat/entity/ChatMessage.java b/src/main/java/com/msvcchat/entity/ChatMessage.java
index 8b1a5a1..4b50699 100644
--- a/src/main/java/com/msvcchat/entity/ChatMessage.java
+++ b/src/main/java/com/msvcchat/entity/ChatMessage.java
@@ -1,8 +1,6 @@
package com.msvcchat.entity;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@@ -11,13 +9,13 @@
@Document(collection = "messages")
@Data
@NoArgsConstructor
-//@Builder
+@AllArgsConstructor
public class ChatMessage {
@Id
private String id;
- private String roomId; // room identificador (por ejemplo "user:{userId}:trainer:{trainerId}")
- private String fromId; // id del emisor (user o trainer)
- private String fromRole; // "USER" o "TRAINER"
+ private String roomId;
+ private String fromId;
+ private String fromRole;
private String text;
private Instant createdAt = Instant.now();
}
diff --git a/src/main/java/com/msvcchat/entity/Room.java b/src/main/java/com/msvcchat/entity/Room.java
index b9a2c79..9db18dc 100644
--- a/src/main/java/com/msvcchat/entity/Room.java
+++ b/src/main/java/com/msvcchat/entity/Room.java
@@ -14,7 +14,7 @@
@AllArgsConstructor
public class Room {
@Id
- private String id; // roomId
- private Set users; // ids de usuarios
- private Set trainers;// ids de entrenadores
+ private String id;
+ private Set users;
+ private Set trainers;
}
\ No newline at end of file
diff --git a/src/main/java/com/msvcchat/mappers/.gitkeep b/src/main/java/com/msvcchat/mappers/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/java/com/msvcchat/mappers/ChatMessageMapper.java b/src/main/java/com/msvcchat/mappers/ChatMessageMapper.java
new file mode 100644
index 0000000..c20cf06
--- /dev/null
+++ b/src/main/java/com/msvcchat/mappers/ChatMessageMapper.java
@@ -0,0 +1,27 @@
+package com.msvcchat.mappers;
+
+import com.msvcchat.config.ReactiveMapperConfig;
+import com.msvcchat.dtos.ChatMessageDto;
+import com.msvcchat.dtos.CreateChatMessageDto;
+import com.msvcchat.entity.ChatMessage;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+
+@Mapper(config = ReactiveMapperConfig.class)
+public interface ChatMessageMapper {
+ ChatMessageDto toDto(ChatMessage entity);
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "roomId", ignore = true)
+ @Mapping(target = "createdAt", ignore = true)
+ ChatMessage toEntity(CreateChatMessageDto dto);
+
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "roomId", ignore = true)
+ @Mapping(target = "createdAt", ignore = true)
+ void updateEntityFromDto(CreateChatMessageDto dto, @MappingTarget ChatMessage entity);
+
+
+}
diff --git a/src/main/java/com/msvcchat/service/.gitkeep b/src/main/java/com/msvcchat/service/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/java/com/msvcchat/service/ChatRoomManager.java b/src/main/java/com/msvcchat/service/ChatRoomManager.java
new file mode 100644
index 0000000..56c247d
--- /dev/null
+++ b/src/main/java/com/msvcchat/service/ChatRoomManager.java
@@ -0,0 +1,54 @@
+package com.msvcchat.service;
+
+import com.msvcchat.entity.ChatMessage;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.mongodb.core.ChangeStreamEvent;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Sinks;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+@RequiredArgsConstructor
+public class ChatRoomManager {
+
+ private final ReactiveMongoTemplate mongoTemplate;
+ private final Map> sinks = new ConcurrentHashMap<>();
+
+ public Sinks.Many sinkFor(String roomId) {
+ return sinks.computeIfAbsent(roomId, rid -> Sinks.many().multicast().onBackpressureBuffer());
+ }
+
+
+ public void broadcast(ChatMessage msg) {
+ if (msg == null || msg.getRoomId() == null)
+ return;
+ Sinks.Many s = sinks.get(msg.getRoomId());
+ if (s != null) {
+ s.tryEmitNext(msg);
+ }
+ }
+
+
+ @PostConstruct
+ void startChangeStreamListener() {
+ //Va a estar esuchando inserts o updates en la colencion de ChatMessage
+ mongoTemplate.changeStream(ChatMessage.class)
+ .listen()
+ .mapNotNull(ChangeStreamEvent::getBody)
+ .subscribe(msg -> {
+ if (msg != null && msg.getRoomId() != null) {
+ Sinks.Many s = sinks.get(msg.getRoomId());
+ if (s != null)
+ s.tryEmitNext(msg);
+ }
+ }, err -> {
+ err.printStackTrace();
+ });
+ }
+
+
+}
diff --git a/src/main/java/com/msvcchat/service/ChatService.java b/src/main/java/com/msvcchat/service/ChatService.java
new file mode 100644
index 0000000..5efb998
--- /dev/null
+++ b/src/main/java/com/msvcchat/service/ChatService.java
@@ -0,0 +1,13 @@
+package com.msvcchat.service;
+
+import com.msvcchat.dtos.ChatMessageDto;
+import com.msvcchat.dtos.CreateChatMessageDto;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface ChatService {
+ Mono saveMessage(String roomId, CreateChatMessageDto dto);
+
+ Flux getHistory(String roomId);
+
+}
diff --git a/src/main/java/com/msvcchat/service/Impl/ChatServiceImpl.java b/src/main/java/com/msvcchat/service/Impl/ChatServiceImpl.java
new file mode 100644
index 0000000..ced8c40
--- /dev/null
+++ b/src/main/java/com/msvcchat/service/Impl/ChatServiceImpl.java
@@ -0,0 +1,39 @@
+package com.msvcchat.service.Impl;
+
+import com.msvcchat.dtos.ChatMessageDto;
+import com.msvcchat.dtos.CreateChatMessageDto;
+import com.msvcchat.entity.ChatMessage;
+import com.msvcchat.mappers.ChatMessageMapper;
+import com.msvcchat.repositories.ChatMessageRepository;
+import com.msvcchat.service.ChatRoomManager;
+import com.msvcchat.service.ChatService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class ChatServiceImpl implements ChatService {
+
+ private final ChatMessageRepository chatMessageRepository;
+ private final ChatMessageMapper mapper;
+ private final ChatRoomManager chatRoomManager;
+
+ @Override
+ public Mono saveMessage(String roomId, CreateChatMessageDto dto) {
+ ChatMessage entity = mapper.toEntity(dto);
+ entity.setRoomId(roomId);
+ return chatMessageRepository
+ .save(entity)
+ .doOnNext(chatRoomManager::broadcast)
+ .map(mapper::toDto);
+ }
+
+ @Override
+ public Flux getHistory(String roomId) {
+ return chatMessageRepository.findByRoomIdOrderByCreatedAtAsc(roomId).map(mapper::toDto);
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
deleted file mode 100644
index 6877265..0000000
--- a/src/main/resources/application.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-spring:
- application:
- name: msvc-chat
-springdoc:
- api-docs:
- path: /v3/api-docs
- swagger-ui:
- path: /
- url: /v3/api-docs
\ No newline at end of file
diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt
new file mode 100644
index 0000000..8680f88
--- /dev/null
+++ b/src/main/resources/banner.txt
@@ -0,0 +1,8 @@
+______ ____________ __________ ______________ ________________
+___ |/ /_ ___/_ | / /_ ____/ __ ____/__ / / /__ |__ __/
+__ /|_/ /_____ \__ | / /_ / ________ / __ /_/ /__ /| |_ /
+_ / / / ____/ /__ |/ / / /___/_____/ /___ _ __ / _ ___ | /
+/_/ /_/ /____/ _____/ \____/ \____/ /_/ /_/ /_/ |_/_/
+
+${application.title} ${application.version}
+Powered by Spring Boot ${spring-boot.version}
\ No newline at end of file