diff --git a/messages-service/.gitignore b/messages-service/.gitignore
new file mode 100644
index 0000000..549e00a
--- /dev/null
+++ b/messages-service/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/messages-service/pom.xml b/messages-service/pom.xml
new file mode 100644
index 0000000..5ff89ee
--- /dev/null
+++ b/messages-service/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.4
+
+
+ com.sivalabs
+ messages-service
+ 0.0.1-SNAPSHOT
+ messages-service
+ messages-service
+
+ 17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/messages-service/src/main/java/com/sivalabs/messages/MessagesServiceApplication.java b/messages-service/src/main/java/com/sivalabs/messages/MessagesServiceApplication.java
new file mode 100644
index 0000000..d44a3c3
--- /dev/null
+++ b/messages-service/src/main/java/com/sivalabs/messages/MessagesServiceApplication.java
@@ -0,0 +1,13 @@
+package com.sivalabs.messages;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class MessagesServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(MessagesServiceApplication.class, args);
+ }
+
+}
diff --git a/messages-service/src/main/java/com/sivalabs/messages/api/MessageController.java b/messages-service/src/main/java/com/sivalabs/messages/api/MessageController.java
new file mode 100644
index 0000000..555fee7
--- /dev/null
+++ b/messages-service/src/main/java/com/sivalabs/messages/api/MessageController.java
@@ -0,0 +1,43 @@
+package com.sivalabs.messages.api;
+
+import com.sivalabs.messages.domain.Message;
+import com.sivalabs.messages.domain.MessageRepository;
+import jakarta.validation.Valid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/messages")
+class MessageController {
+ private static final Logger log = LoggerFactory.getLogger(MessageController.class);
+
+ private final MessageRepository messageRepository;
+
+ MessageController(MessageRepository messageRepository) {
+ this.messageRepository = messageRepository;
+ }
+
+ @GetMapping
+ List getMessages() {
+ return messageRepository.getMessages();
+ }
+
+ @PostMapping
+ Message createMessage(@RequestBody @Valid Message message) {
+ return messageRepository.createMessage(message);
+ }
+
+ @PostMapping("/archive")
+ Map archiveMessages() {
+ log.info("Archiving all messages");
+ return Map.of("status", "success");
+ }
+}
diff --git a/messages-service/src/main/java/com/sivalabs/messages/api/UserInfoController.java b/messages-service/src/main/java/com/sivalabs/messages/api/UserInfoController.java
new file mode 100644
index 0000000..45c697b
--- /dev/null
+++ b/messages-service/src/main/java/com/sivalabs/messages/api/UserInfoController.java
@@ -0,0 +1,44 @@
+package com.sivalabs.messages.api;
+
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+class UserInfoController {
+
+ @GetMapping("/api/me")
+ Map currentUserDetails() {
+ return getLoginUserDetails();
+ }
+
+ Map getLoginUserDetails() {
+ Map map = new HashMap<>();
+ JwtAuthenticationToken authentication =
+ (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
+ Jwt jwt = (Jwt) authentication.getPrincipal();
+
+ map.put("username", jwt.getClaimAsString("preferred_username"));
+ map.put("email", jwt.getClaimAsString("email"));
+ map.put("name", jwt.getClaimAsString("name"));
+ map.put("token", jwt.getTokenValue());
+ map.put("authorities", authentication.getAuthorities());
+ map.put("roles", getRoles(jwt));
+
+ return map;
+ }
+
+ List getRoles(Jwt jwt) {
+ Map realm_access = (Map) jwt.getClaims().get("realm_access");
+ if(realm_access != null && !realm_access.isEmpty()) {
+ return (List) realm_access.get("roles");
+ }
+ return List.of();
+ }
+}
diff --git a/messages-service/src/main/java/com/sivalabs/messages/config/KeycloakJwtAuthenticationConverter.java b/messages-service/src/main/java/com/sivalabs/messages/config/KeycloakJwtAuthenticationConverter.java
new file mode 100644
index 0000000..d5b5bed
--- /dev/null
+++ b/messages-service/src/main/java/com/sivalabs/messages/config/KeycloakJwtAuthenticationConverter.java
@@ -0,0 +1,42 @@
+package com.sivalabs.messages.config;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+class KeycloakJwtAuthenticationConverter implements Converter {
+ private final Converter> delegate = new JwtGrantedAuthoritiesConverter();
+
+ @Override
+ public AbstractAuthenticationToken convert(Jwt jwt) {
+ List authorityList = extractRoles(jwt);
+ Collection authorities = delegate.convert(jwt);
+ if (authorities != null) {
+ authorityList.addAll(authorities);
+ }
+ return new JwtAuthenticationToken(jwt, authorityList);
+ }
+
+ private List extractRoles(Jwt jwt) {
+ Map realm_access = (Map) jwt.getClaims().get("realm_access");
+ if(realm_access == null || realm_access.isEmpty()) {
+ return List.of();
+ }
+ List roles = (List) realm_access.get("roles");
+ if (roles == null || roles.isEmpty()) {
+ roles = List.of("ROLE_USER");
+ }
+ return roles.stream()
+ .filter(role -> role.startsWith("ROLE_"))
+ .map(SimpleGrantedAuthority::new).collect(Collectors.toList());
+ }
+}
diff --git a/messages-service/src/main/java/com/sivalabs/messages/config/SecurityConfig.java b/messages-service/src/main/java/com/sivalabs/messages/config/SecurityConfig.java
new file mode 100644
index 0000000..a3d4143
--- /dev/null
+++ b/messages-service/src/main/java/com/sivalabs/messages/config/SecurityConfig.java
@@ -0,0 +1,35 @@
+package com.sivalabs.messages.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+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.CorsConfigurer;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+ @Bean
+ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .authorizeHttpRequests(c ->
+ c
+ .requestMatchers(HttpMethod.GET, "/api/messages").permitAll()
+ .requestMatchers(HttpMethod.POST, "/api/messages/archive").hasAnyRole("ADMIN")
+ .anyRequest().authenticated()
+ )
+ .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .cors(CorsConfigurer::disable)
+ .csrf(CsrfConfigurer::disable)
+ .oauth2ResourceServer(oauth2 ->
+ //oauth2.jwt(Customizer.withDefaults())
+ oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtAuthenticationConverter()))
+ );
+ return http.build();
+ }
+}
diff --git a/messages-service/src/main/java/com/sivalabs/messages/domain/Message.java b/messages-service/src/main/java/com/sivalabs/messages/domain/Message.java
new file mode 100644
index 0000000..7346d36
--- /dev/null
+++ b/messages-service/src/main/java/com/sivalabs/messages/domain/Message.java
@@ -0,0 +1,56 @@
+package com.sivalabs.messages.domain;
+
+import jakarta.validation.constraints.NotEmpty;
+
+import java.time.Instant;
+
+public class Message {
+ private Long id;
+ @NotEmpty
+ private String content;
+ @NotEmpty
+ private String createdBy;
+ private Instant createdAt;
+
+ public Message() {
+ }
+
+ public Message(Long id, String content, String createdBy, Instant createdAt) {
+ this.id = id;
+ this.content = content;
+ this.createdBy = createdBy;
+ this.createdAt = createdAt;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getCreatedBy() {
+ return createdBy;
+ }
+
+ public void setCreatedBy(String createdBy) {
+ this.createdBy = createdBy;
+ }
+
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Instant createdAt) {
+ this.createdAt = createdAt;
+ }
+}
diff --git a/messages-service/src/main/java/com/sivalabs/messages/domain/MessageRepository.java b/messages-service/src/main/java/com/sivalabs/messages/domain/MessageRepository.java
new file mode 100644
index 0000000..5f00426
--- /dev/null
+++ b/messages-service/src/main/java/com/sivalabs/messages/domain/MessageRepository.java
@@ -0,0 +1,41 @@
+package com.sivalabs.messages.domain;
+
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Repository;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Repository
+public class MessageRepository {
+ private static final AtomicLong ID = new AtomicLong(0L);
+ private static final List MESSAGES = new ArrayList<>();
+
+ @PostConstruct
+ void init() {
+ getDefaultMessages().forEach( p -> {
+ p.setId(ID.incrementAndGet());
+ MESSAGES.add(p);
+ });
+ }
+
+ public List getMessages() {
+ return MESSAGES;
+ }
+
+ public Message createMessage(Message message) {
+ message.setId(ID.incrementAndGet());
+ message.setCreatedAt(Instant.now());
+ MESSAGES.add(message);
+ return message;
+ }
+
+ private List getDefaultMessages() {
+ List messages = new ArrayList<>();
+ messages.add(new Message(null, "Test Message 1", "admin", Instant.now()));
+ messages.add(new Message(null, "Test Message 2", "admin", Instant.now()));
+ return messages;
+ }
+}
diff --git a/messages-service/src/main/resources/application.properties b/messages-service/src/main/resources/application.properties
new file mode 100644
index 0000000..917edb7
--- /dev/null
+++ b/messages-service/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+spring.application.name=messages-service
+server.port=8181
+OAUTH_SERVER=http://localhost:9191/realms/sivalabs
+spring.security.oauth2.resourceserver.jwt.issuer-uri=${OAUTH_SERVER}
diff --git a/messages-service/src/test/java/com/sivalabs/messages/MessagesServiceApplicationTests.java b/messages-service/src/test/java/com/sivalabs/messages/MessagesServiceApplicationTests.java
new file mode 100644
index 0000000..b96230c
--- /dev/null
+++ b/messages-service/src/test/java/com/sivalabs/messages/MessagesServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.sivalabs.messages;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class MessagesServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 904f8a1..e4551e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,6 +13,7 @@
messages-webapp
+ messages-service