diff --git a/pom.xml b/pom.xml
index 846bf0c..8d02cfd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -87,6 +87,12 @@
spring-boot-starter-webmvc-test
test
+
+
+ cn.hutool
+ hutool-core
+ 5.8.43
+
diff --git a/src/main/java/dev/mednikov/social/users/config/IdentifierGeneratorConfig.java b/src/main/java/dev/mednikov/social/users/config/IdentifierGeneratorConfig.java
new file mode 100644
index 0000000..35abd6c
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/config/IdentifierGeneratorConfig.java
@@ -0,0 +1,16 @@
+package dev.mednikov.social.users.config;
+
+import dev.mednikov.social.users.repositories.IdentifierGenerator;
+import dev.mednikov.social.users.repositories.IdentifierGeneratorImpl;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class IdentifierGeneratorConfig {
+
+ @Bean
+ public IdentifierGenerator identifierGenerator() {
+ return new IdentifierGeneratorImpl();
+ }
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/config/ObjectMapperConfig.java b/src/main/java/dev/mednikov/social/users/config/ObjectMapperConfig.java
index 5d1f809..6f47312 100644
--- a/src/main/java/dev/mednikov/social/users/config/ObjectMapperConfig.java
+++ b/src/main/java/dev/mednikov/social/users/config/ObjectMapperConfig.java
@@ -1,5 +1,7 @@
package dev.mednikov.social.users.config;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -10,6 +12,7 @@ public class ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return objectMapper;
}
}
diff --git a/src/main/java/dev/mednikov/social/users/domain/OnboardRequestDto.java b/src/main/java/dev/mednikov/social/users/domain/OnboardRequestDto.java
new file mode 100644
index 0000000..2d39992
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/domain/OnboardRequestDto.java
@@ -0,0 +1,32 @@
+package dev.mednikov.social.users.domain;
+
+public final class OnboardRequestDto {
+
+ private String organizationName;
+ private String description;
+ private boolean student;
+
+ public String getOrganizationName() {
+ return organizationName;
+ }
+
+ public void setOrganizationName(String organizationName) {
+ this.organizationName = organizationName;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public boolean isStudent() {
+ return student;
+ }
+
+ public void setStudent(boolean student) {
+ this.student = student;
+ }
+}
diff --git a/src/main/java/dev/mednikov/social/users/domain/UserProfileDto.java b/src/main/java/dev/mednikov/social/users/domain/UserProfileDto.java
new file mode 100644
index 0000000..af18e5d
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/domain/UserProfileDto.java
@@ -0,0 +1,86 @@
+package dev.mednikov.social.users.domain;
+
+public final class UserProfileDto {
+
+ private String id;
+ private String firstName;
+ private String lastName;
+ private String email;
+ private boolean active;
+ private boolean onboarded;
+ private boolean emailVerified;
+ private String avatarUrl;
+ private String headline;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ public boolean isOnboarded() {
+ return onboarded;
+ }
+
+ public void setOnboarded(boolean onboarded) {
+ this.onboarded = onboarded;
+ }
+
+ public boolean isEmailVerified() {
+ return emailVerified;
+ }
+
+ public void setEmailVerified(boolean emailVerified) {
+ this.emailVerified = emailVerified;
+ }
+
+ public String getAvatarUrl() {
+ return avatarUrl;
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = avatarUrl;
+ }
+
+ public String getHeadline() {
+ return headline;
+ }
+
+ public void setHeadline(String headline) {
+ this.headline = headline;
+ }
+}
diff --git a/src/main/java/dev/mednikov/social/users/domain/UserProfileDtoMapper.java b/src/main/java/dev/mednikov/social/users/domain/UserProfileDtoMapper.java
new file mode 100644
index 0000000..90a6912
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/domain/UserProfileDtoMapper.java
@@ -0,0 +1,24 @@
+package dev.mednikov.social.users.domain;
+
+import dev.mednikov.social.users.models.User;
+
+import java.util.function.Function;
+
+public final class UserProfileDtoMapper implements Function {
+
+ @Override
+ public UserProfileDto apply(User user) {
+ UserProfileDto result = new UserProfileDto();
+ result.setId(user.getId().toString());
+ result.setFirstName(user.getFirstName());
+ result.setLastName(user.getLastName());
+ result.setEmail(user.getEmail());
+ result.setHeadline(user.getHeadline());
+ result.setAvatarUrl(user.getAvatarUrl());
+ result.setActive(user.getActive());
+ result.setEmailVerified(user.getEmailVerified());
+ result.setOnboarded(user.getOnboarded());
+ return result;
+ }
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/events/UserCreatedEvent.java b/src/main/java/dev/mednikov/social/users/events/UserCreatedEvent.java
new file mode 100644
index 0000000..07f1904
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/events/UserCreatedEvent.java
@@ -0,0 +1,18 @@
+package dev.mednikov.social.users.events;
+
+import dev.mednikov.social.users.models.User;
+import org.springframework.context.ApplicationEvent;
+
+public final class UserCreatedEvent extends ApplicationEvent {
+
+ private final User user;
+
+ public UserCreatedEvent(Object source, User user) {
+ super(source);
+ this.user = user;
+ }
+
+ public User getUser() {
+ return user;
+ }
+}
diff --git a/src/main/java/dev/mednikov/social/users/events/UserUpdatedEvent.java b/src/main/java/dev/mednikov/social/users/events/UserUpdatedEvent.java
new file mode 100644
index 0000000..43c6603
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/events/UserUpdatedEvent.java
@@ -0,0 +1,19 @@
+package dev.mednikov.social.users.events;
+
+import dev.mednikov.social.users.models.User;
+import org.springframework.context.ApplicationEvent;
+
+public final class UserUpdatedEvent extends ApplicationEvent {
+
+ private final User user;
+
+ public UserUpdatedEvent(Object source, User user) {
+ super(source);
+ this.user = user;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/exceptions/UserAlreadyOnboardedException.java b/src/main/java/dev/mednikov/social/users/exceptions/UserAlreadyOnboardedException.java
new file mode 100644
index 0000000..f1c3eec
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/exceptions/UserAlreadyOnboardedException.java
@@ -0,0 +1,9 @@
+package dev.mednikov.social.users.exceptions;
+
+public class UserAlreadyOnboardedException extends ObjectAlreadyExistsException{
+
+ public UserAlreadyOnboardedException(Long userId) {
+ super("User id " + userId.toString() + " is already onboarded");
+ }
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/messaging/MessageService.java b/src/main/java/dev/mednikov/social/users/messaging/MessageService.java
new file mode 100644
index 0000000..4e6742c
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/messaging/MessageService.java
@@ -0,0 +1,50 @@
+package dev.mednikov.social.users.messaging;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.mednikov.social.users.events.UserCreatedEvent;
+import dev.mednikov.social.users.events.UserUpdatedEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+@Component
+public class MessageService {
+
+ private final static Logger logger = LoggerFactory.getLogger(MessageService.class);
+
+ private final ObjectMapper objectMapper;
+ private final RabbitTemplate rabbitTemplate;
+
+ public MessageService(ObjectMapper objectMapper, RabbitTemplate rabbitTemplate) {
+ this.objectMapper = objectMapper;
+ this.rabbitTemplate = rabbitTemplate;
+ }
+
+ @EventListener
+ @Async
+ public void onUserCreatedEventListener (UserCreatedEvent event) {
+ UserPayload payload = new UserPayload(event.getUser());
+ try {
+ String userEncoded = this.objectMapper.writeValueAsString(payload);
+ this.rabbitTemplate.convertAndSend("", "social_users_created", userEncoded);
+ } catch (Exception ex){
+ logger.error(ex.getMessage());
+ }
+ }
+
+ @EventListener
+ @Async
+ public void onUserUpdatedEventListener (UserUpdatedEvent event) {
+ UserPayload payload = new UserPayload(event.getUser());
+ try {
+ String userEncoded = this.objectMapper.writeValueAsString(payload);
+ this.rabbitTemplate.convertAndSend("", "social_users_updated", userEncoded);
+ } catch (Exception ex){
+ logger.error(ex.getMessage());
+ }
+ }
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/messaging/UserPayload.java b/src/main/java/dev/mednikov/social/users/messaging/UserPayload.java
new file mode 100644
index 0000000..b7a069d
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/messaging/UserPayload.java
@@ -0,0 +1,112 @@
+package dev.mednikov.social.users.messaging;
+
+import dev.mednikov.social.users.models.User;
+
+final class UserPayload {
+
+ private String id;
+ private String firstName;
+ private String lastName;
+ private String email;
+ private String avatarUrl;
+ private boolean emailVerified;
+ private boolean active;
+ private boolean onboarded;
+ private String headline;
+ private Long version;
+
+ UserPayload() {}
+
+ UserPayload(User user) {
+ this.id = user.getId().toString();
+ this.firstName = user.getFirstName();
+ this.lastName = user.getLastName();
+ this.email = user.getEmail();
+ this.avatarUrl = user.getAvatarUrl();
+ this.emailVerified = user.getEmailVerified();
+ this.active = user.getActive();
+ this.onboarded = user.getOnboarded();
+ this.headline = user.getHeadline();
+ this.version = user.getVersion();
+ }
+
+ String getId() {
+ return id;
+ }
+
+ String getFirstName() {
+ return firstName;
+ }
+
+ String getLastName() {
+ return lastName;
+ }
+
+ String getEmail() {
+ return email;
+ }
+
+ String getAvatarUrl() {
+ return avatarUrl;
+ }
+
+ boolean isEmailVerified() {
+ return emailVerified;
+ }
+
+ boolean isActive() {
+ return active;
+ }
+
+ boolean isOnboarded() {
+ return onboarded;
+ }
+
+ String getHeadline() {
+ return headline;
+ }
+
+ Long getVersion() {
+ return version;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = avatarUrl;
+ }
+
+ public void setEmailVerified(boolean emailVerified) {
+ this.emailVerified = emailVerified;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ public void setOnboarded(boolean onboarded) {
+ this.onboarded = onboarded;
+ }
+
+ public void setHeadline(String headline) {
+ this.headline = headline;
+ }
+
+ public void setVersion(Long version) {
+ this.version = version;
+ }
+}
diff --git a/src/main/java/dev/mednikov/social/users/models/User.java b/src/main/java/dev/mednikov/social/users/models/User.java
new file mode 100644
index 0000000..f737f05
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/models/User.java
@@ -0,0 +1,144 @@
+package dev.mednikov.social.users.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Entity
+@Table(name = "users_user")
+public class User {
+
+ @Id private Long id;
+ @Column(nullable = false, unique = true, name = "auth_id") private UUID authId;
+ @Column(nullable = false, name = "email", unique = true) private String email;
+ @Column(nullable = false, name = "first_name") private String firstName;
+ @Column(nullable = false, name = "last_name") private String lastName;
+ @Column(nullable = false, name = "avatar_url") private String avatarUrl;
+ @Column(nullable = false, name = "headline") private String headline;
+ @Column(nullable = false, name = "is_active") private Boolean active;
+ @Column(nullable = false, name = "is_email_verified") private Boolean emailVerified;
+ @Column(nullable = false, name = "is_onboarded") private Boolean onboarded;
+ @Column(nullable = false, name = "version") private Long version;
+ @Column(name = "created_at") @CreationTimestamp private LocalDateTime createdAt;
+ @Column(name = "updated_at") @UpdateTimestamp private LocalDateTime updatedAt;
+
+ @Override
+ public final boolean equals(Object o) {
+ if (!(o instanceof User user)) return false;
+
+ return authId.equals(user.authId)
+ && email.equals(user.email)
+ && firstName.equals(user.firstName)
+ && lastName.equals(user.lastName)
+ && active.equals(user.active)
+ && onboarded.equals(user.onboarded)
+ && version.equals(user.version);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = authId.hashCode();
+ result = 31 * result + email.hashCode();
+ result = 31 * result + firstName.hashCode();
+ result = 31 * result + lastName.hashCode();
+ result = 31 * result + active.hashCode();
+ result = 31 * result + onboarded.hashCode();
+ result = 31 * result + version.hashCode();
+ return result;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getVersion() {
+ return version;
+ }
+
+ public void setVersion(Long version) {
+ this.version = version;
+ }
+
+ public Boolean getOnboarded() {
+ return onboarded;
+ }
+
+ public void setOnboarded(Boolean onboarded) {
+ this.onboarded = onboarded;
+ }
+
+ public Boolean getEmailVerified() {
+ return emailVerified;
+ }
+
+ public void setEmailVerified(Boolean emailVerified) {
+ this.emailVerified = emailVerified;
+ }
+
+ public String getAvatarUrl() {
+ return avatarUrl;
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = avatarUrl;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getHeadline() {
+ return headline;
+ }
+
+ public void setHeadline(String headline) {
+ this.headline = headline;
+ }
+
+ public Boolean getActive() {
+ return active;
+ }
+
+ public void setActive(Boolean active) {
+ this.active = active;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public UUID getAuthId() {
+ return authId;
+ }
+
+ public void setAuthId(UUID authId) {
+ this.authId = authId;
+ }
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/repositories/IdentifierGenerator.java b/src/main/java/dev/mednikov/social/users/repositories/IdentifierGenerator.java
new file mode 100644
index 0000000..6b2f9d0
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/repositories/IdentifierGenerator.java
@@ -0,0 +1,7 @@
+package dev.mednikov.social.users.repositories;
+
+public interface IdentifierGenerator {
+
+ public Long getNextId();
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/repositories/IdentifierGeneratorImpl.java b/src/main/java/dev/mednikov/social/users/repositories/IdentifierGeneratorImpl.java
new file mode 100644
index 0000000..e84c15f
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/repositories/IdentifierGeneratorImpl.java
@@ -0,0 +1,17 @@
+package dev.mednikov.social.users.repositories;
+
+import cn.hutool.core.lang.generator.SnowflakeGenerator;
+
+public class IdentifierGeneratorImpl implements IdentifierGenerator {
+
+ private final SnowflakeGenerator snowflakeGenerator;
+
+ public IdentifierGeneratorImpl() {
+ this.snowflakeGenerator = new SnowflakeGenerator();
+ }
+
+ @Override
+ public Long getNextId() {
+ return this.snowflakeGenerator.next();
+ }
+}
diff --git a/src/main/java/dev/mednikov/social/users/repositories/UserRepository.java b/src/main/java/dev/mednikov/social/users/repositories/UserRepository.java
new file mode 100644
index 0000000..4e96f75
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/repositories/UserRepository.java
@@ -0,0 +1,13 @@
+package dev.mednikov.social.users.repositories;
+
+import dev.mednikov.social.users.models.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+public interface UserRepository extends JpaRepository {
+
+ Optional findByAuthId (UUID authId);
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/services/CurrentUserService.java b/src/main/java/dev/mednikov/social/users/services/CurrentUserService.java
new file mode 100644
index 0000000..983e5e8
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/services/CurrentUserService.java
@@ -0,0 +1,10 @@
+package dev.mednikov.social.users.services;
+
+import dev.mednikov.social.users.models.User;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+public interface CurrentUserService {
+
+ User getCurrentUser (Jwt authPrincipal);
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/services/CurrentUserServiceImpl.java b/src/main/java/dev/mednikov/social/users/services/CurrentUserServiceImpl.java
new file mode 100644
index 0000000..8396ad9
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/services/CurrentUserServiceImpl.java
@@ -0,0 +1,90 @@
+package dev.mednikov.social.users.services;
+
+import dev.mednikov.social.users.events.UserCreatedEvent;
+import dev.mednikov.social.users.events.UserUpdatedEvent;
+import dev.mednikov.social.users.models.User;
+import dev.mednikov.social.users.repositories.IdentifierGenerator;
+import dev.mednikov.social.users.repositories.UserRepository;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+import java.util.UUID;
+
+@Service
+public class CurrentUserServiceImpl implements CurrentUserService {
+
+ private final IdentifierGenerator identifierGenerator;
+ private final UserRepository userRepository;
+ private final ApplicationEventPublisher eventPublisher;
+
+ public CurrentUserServiceImpl(IdentifierGenerator identifierGenerator, UserRepository userRepository, ApplicationEventPublisher eventPublisher) {
+ this.identifierGenerator = identifierGenerator;
+ this.userRepository = userRepository;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @Override
+ public User getCurrentUser(Jwt authPrincipal) {
+ UUID authId = UUID.fromString(authPrincipal.getSubject());
+ Optional userResult = this.userRepository.findByAuthId(authId);
+ if (userResult.isEmpty()) {
+ // Create new user
+ User user = new User();
+ user.setAuthId(authId);
+ user.setFirstName(authPrincipal.getClaim("given_name"));
+ user.setLastName(authPrincipal.getClaim("family_name"));
+ user.setEmailVerified(authPrincipal.getClaim("email_verified"));
+ user.setActive(true);
+ user.setOnboarded(false);
+ user.setId(this.identifierGenerator.getNextId());
+ user.setVersion(1L);
+ user.setAvatarUrl("");
+ user.setHeadline("");
+
+ // Set email
+ String email = authPrincipal.getClaim("email");
+ user.setEmail(email);
+
+ User result = this.userRepository.save(user);
+ UserCreatedEvent event = new UserCreatedEvent(this, result);
+ this.eventPublisher.publishEvent(event);
+ return result;
+ } else {
+ User user = userResult.get();
+ // Check if updates are needed
+ if (requiresUpdate(user, authPrincipal)) {
+ // Handle update
+ String email = authPrincipal.getClaim("email");
+ user.setEmail(email);
+ user.setAvatarUrl("");
+ user.setFirstName(authPrincipal.getClaim("given_name"));
+ user.setLastName(authPrincipal.getClaim("family_name"));
+ user.setEmailVerified(authPrincipal.getClaim("email_verified"));
+ Long newVersion = user.getVersion() + 1;
+ user.setVersion(newVersion);
+ User result = this.userRepository.save(user);
+ UserUpdatedEvent event = new UserUpdatedEvent(this, result);
+ this.eventPublisher.publishEvent(event);
+ return result;
+ }
+ return user;
+ }
+ }
+
+ boolean requiresUpdate (User savedUser, Jwt principal){
+ boolean result = !savedUser.getFirstName().equals(principal.getClaim("given_name"));
+ if (!savedUser.getLastName().equals(principal.getClaim("family_name"))) {
+ result = true;
+ }
+ if (!savedUser.getEmail().equals(principal.getClaim("email"))) {
+ result = true;
+ }
+ if (!savedUser.getEmailVerified().equals(principal.getClaim("email_verified"))) {
+ result = true;
+ }
+ return result;
+ }
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/services/UserProfileService.java b/src/main/java/dev/mednikov/social/users/services/UserProfileService.java
new file mode 100644
index 0000000..38d8cc7
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/services/UserProfileService.java
@@ -0,0 +1,14 @@
+package dev.mednikov.social.users.services;
+
+import dev.mednikov.social.users.domain.OnboardRequestDto;
+import dev.mednikov.social.users.domain.UserProfileDto;
+import dev.mednikov.social.users.models.User;
+
+import java.util.Optional;
+
+public interface UserProfileService {
+
+ UserProfileDto onboardCurrentUser (User currentUser, OnboardRequestDto request);
+
+ Optional getUserProfileById (Long id);
+}
diff --git a/src/main/java/dev/mednikov/social/users/services/UserProfileServiceImpl.java b/src/main/java/dev/mednikov/social/users/services/UserProfileServiceImpl.java
new file mode 100644
index 0000000..d4e2fcc
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/services/UserProfileServiceImpl.java
@@ -0,0 +1,56 @@
+package dev.mednikov.social.users.services;
+
+import dev.mednikov.social.users.domain.OnboardRequestDto;
+import dev.mednikov.social.users.domain.UserProfileDto;
+import dev.mednikov.social.users.domain.UserProfileDtoMapper;
+import dev.mednikov.social.users.events.UserUpdatedEvent;
+import dev.mednikov.social.users.exceptions.UserAlreadyOnboardedException;
+import dev.mednikov.social.users.models.User;
+import dev.mednikov.social.users.repositories.UserRepository;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+public class UserProfileServiceImpl implements UserProfileService {
+
+ private final static UserProfileDtoMapper mapper = new UserProfileDtoMapper();
+
+ private final UserRepository userRepository;
+ private final ApplicationEventPublisher eventPublisher;
+
+ public UserProfileServiceImpl(UserRepository userRepository, ApplicationEventPublisher eventPublisher) {
+ this.userRepository = userRepository;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @Override
+ public UserProfileDto onboardCurrentUser(User currentUser, OnboardRequestDto request) {
+ if (currentUser.getOnboarded()){
+ // User is already onboarded
+ throw new UserAlreadyOnboardedException(currentUser.getId());
+ }
+ if (request.isStudent()){
+ currentUser.setHeadline("Student @ " + request.getOrganizationName());
+ } else {
+ currentUser.setHeadline(request.getDescription() + " @ " + request.getOrganizationName());
+ }
+ currentUser.setOnboarded(true);
+ Long newVersion = currentUser.getVersion() + 1;
+ currentUser.setVersion(newVersion);
+
+ User updatedUser = this.userRepository.save(currentUser);
+
+ UserUpdatedEvent event = new UserUpdatedEvent(this, updatedUser);
+ this.eventPublisher.publishEvent(event);
+
+ return mapper.apply(updatedUser);
+ }
+
+ @Override
+ public Optional getUserProfileById(Long id) {
+ return this.userRepository.findById(id).map(mapper);
+ }
+
+}
diff --git a/src/main/java/dev/mednikov/social/users/web/UserProfileRestController.java b/src/main/java/dev/mednikov/social/users/web/UserProfileRestController.java
new file mode 100644
index 0000000..ee1bead
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/web/UserProfileRestController.java
@@ -0,0 +1,38 @@
+package dev.mednikov.social.users.web;
+
+import dev.mednikov.social.users.domain.OnboardRequestDto;
+import dev.mednikov.social.users.domain.UserProfileDto;
+import dev.mednikov.social.users.models.User;
+import dev.mednikov.social.users.services.CurrentUserService;
+import dev.mednikov.social.users.services.UserProfileService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Optional;
+
+@RestController
+@RequestMapping("/profiles")
+public class UserProfileRestController {
+
+ private final UserProfileService userProfileService;
+ private final CurrentUserService currentUserService;
+
+ public UserProfileRestController(UserProfileService userProfileService, CurrentUserService currentUserService) {
+ this.userProfileService = userProfileService;
+ this.currentUserService = currentUserService;
+ }
+
+ @PostMapping("/onboard")
+ public @ResponseBody UserProfileDto onboardCurrentUser (@AuthenticationPrincipal Jwt jwt, @RequestBody OnboardRequestDto body){
+ User currentUser = this.currentUserService.getCurrentUser(jwt);
+ return this.userProfileService.onboardCurrentUser(currentUser, body);
+ }
+
+ @GetMapping("/user/{userId}")
+ public ResponseEntity getUserProfile (@PathVariable Long userId){
+ Optional result = this.userProfileService.getUserProfileById(userId);
+ return ResponseEntity.of(result);
+ }
+}
diff --git a/src/main/java/dev/mednikov/social/users/web/UserRestController.java b/src/main/java/dev/mednikov/social/users/web/UserRestController.java
new file mode 100644
index 0000000..c283703
--- /dev/null
+++ b/src/main/java/dev/mednikov/social/users/web/UserRestController.java
@@ -0,0 +1,32 @@
+package dev.mednikov.social.users.web;
+
+import dev.mednikov.social.users.domain.UserProfileDto;
+import dev.mednikov.social.users.domain.UserProfileDtoMapper;
+import dev.mednikov.social.users.models.User;
+import dev.mednikov.social.users.services.CurrentUserService;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/users")
+public class UserRestController {
+
+ private final CurrentUserService currentUserService;
+
+ public UserRestController(CurrentUserService currentUserService) {
+ this.currentUserService = currentUserService;
+ }
+
+ @GetMapping("/current")
+ public @ResponseBody UserProfileDto getCurrentUser (@AuthenticationPrincipal Jwt jwt){
+ User user = this.currentUserService.getCurrentUser(jwt);
+ UserProfileDtoMapper mapper = new UserProfileDtoMapper();
+ return mapper.apply(user);
+ }
+
+
+}
diff --git a/src/main/resources/db/migration/V1__initial.sql b/src/main/resources/db/migration/V1__initial.sql
new file mode 100644
index 0000000..2330f4f
--- /dev/null
+++ b/src/main/resources/db/migration/V1__initial.sql
@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS users_user (
+ id BIGINT PRIMARY KEY,
+ auth_id UUID NOT NULL UNIQUE,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ first_name VARCHAR(255) NOT NULL,
+ last_name VARCHAR(255) NOT NULL,
+ avatar_url VARCHAR(255) NOT NULL,
+ is_active BOOLEAN NOT NULL,
+ is_onboarded BOOLEAN NOT NULL,
+ is_email_verified BOOLEAN NOT NULL,
+ version BIGINT NOT NULL,
+ headline VARCHAR(255),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file