diff --git a/backend/pom.xml b/backend/pom.xml index 22dbe26..d7ba451 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -39,6 +39,14 @@ io.quarkus quarkus-rest-jackson + + io.quarkus + quarkus-smallrye-jwt + + + io.quarkus + quarkus-smallrye-jwt-build + io.quarkus quarkus-smallrye-openapi diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/User.java b/backend/src/main/java/net/beetechgroup/beetask/entities/User.java index 06bfd16..bafa69b 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/User.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/User.java @@ -4,6 +4,8 @@ public class User { private Long id; private String name; private String email; + private String password; + private String photo; public Long getId() { return id; @@ -28,4 +30,20 @@ public class User { public void setEmail(String email) { this.email = email; } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPhoto() { + return photo; + } + + public void setPhoto(String photo) { + this.photo = photo; + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/organization/UserOrganization.java b/backend/src/main/java/net/beetechgroup/beetask/entities/organization/UserOrganization.java index 8e90f7f..6120b60 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/organization/UserOrganization.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/organization/UserOrganization.java @@ -6,4 +6,28 @@ public class UserOrganization { private User user; private Organization organization; private UserOrganizationRole role; + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Organization getOrganization() { + return organization; + } + + public void setOrganization(Organization organization) { + this.organization = organization; + } + + public UserOrganizationRole getRole() { + return role; + } + + public void setRole(UserOrganizationRole role) { + this.role = role; + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/UserUseCaseConfig.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/UserUseCaseConfig.java new file mode 100644 index 0000000..8da2010 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/UserUseCaseConfig.java @@ -0,0 +1,32 @@ +package net.beetechgroup.beetask.frameworks.config; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import net.beetechgroup.beetask.usecase.auth.login.LoginUseCase; +import net.beetechgroup.beetask.usecase.auth.refresh.RefreshTokenUseCase; +import net.beetechgroup.beetask.usecase.user.create.CreateUserUseCase; +import net.beetechgroup.beetask.usecase.repository.UserRepository; +import io.smallrye.jwt.auth.principal.JWTParser; + +@ApplicationScoped +public class UserUseCaseConfig { + + @Inject + JWTParser jwtParser; + + @Produces + public CreateUserUseCase createUserUseCase(UserRepository userRepository) { + return new CreateUserUseCase(userRepository); + } + + @Produces + public LoginUseCase loginUseCase(UserRepository userRepository) { + return new LoginUseCase(userRepository); + } + + @Produces + public RefreshTokenUseCase refreshTokenUseCase(UserRepository userRepository) { + return new RefreshTokenUseCase(userRepository, jwtParser); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/UserEntity.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/UserEntity.java index d1bd525..74e8f1a 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/UserEntity.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/UserEntity.java @@ -14,6 +14,8 @@ public class UserEntity { private Long id; private String name; private String email; + private String password; + private String photo; public Long getId() { return id; @@ -38,4 +40,20 @@ public class UserEntity { public void setEmail(String email) { this.email = email; } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPhoto() { + return photo; + } + + public void setPhoto(String photo) { + this.photo = photo; + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserEntityMapper.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserEntityMapper.java new file mode 100644 index 0000000..b5c7192 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserEntityMapper.java @@ -0,0 +1,29 @@ +package net.beetechgroup.beetask.frameworks.persistence.mapper; + +import net.beetechgroup.beetask.entities.User; +import net.beetechgroup.beetask.frameworks.persistence.entities.UserEntity; + +public class UserEntityMapper { + + public static UserEntity toEntity(User user) { + if (user == null) return null; + UserEntity entity = new UserEntity(); + entity.setId(user.getId()); + entity.setName(user.getName()); + entity.setEmail(user.getEmail()); + entity.setPassword(user.getPassword()); + entity.setPhoto(user.getPhoto()); + return entity; + } + + public static User toDomain(UserEntity entity) { + if (entity == null) return null; + User user = new User(); + user.setId(entity.getId()); + user.setName(entity.getName()); + user.setEmail(entity.getEmail()); + user.setPassword(entity.getPassword()); + user.setPhoto(entity.getPhoto()); + return user; + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserOrganizationEntityMapper.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserOrganizationEntityMapper.java new file mode 100644 index 0000000..ee38027 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/UserOrganizationEntityMapper.java @@ -0,0 +1,16 @@ +package net.beetechgroup.beetask.frameworks.persistence.mapper; + +import net.beetechgroup.beetask.entities.organization.UserOrganization; +import net.beetechgroup.beetask.frameworks.persistence.entities.UserOrganizationEntity; + +public class UserOrganizationEntityMapper { + + public static UserOrganization toDomain(UserOrganizationEntity entity) { + if (entity == null) return null; + UserOrganization domain = new UserOrganization(); + domain.setUser(UserEntityMapper.toDomain(entity.getUser())); + domain.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization())); + domain.setRole(entity.getRole()); + return domain; + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserOrganizationRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserOrganizationRepositoryImpl.java new file mode 100644 index 0000000..4817814 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserOrganizationRepositoryImpl.java @@ -0,0 +1,21 @@ +package net.beetechgroup.beetask.frameworks.persistence.repository; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import net.beetechgroup.beetask.entities.organization.UserOrganization; +import net.beetechgroup.beetask.frameworks.persistence.entities.UserOrganizationEntity; +import net.beetechgroup.beetask.frameworks.persistence.mapper.UserOrganizationEntityMapper; +import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository; + +import java.util.List; + +@ApplicationScoped +public class UserOrganizationRepositoryImpl implements UserOrganizationRepository, PanacheRepository { + + @Override + public List findByUserId(Long userId) { + return find("user.id", userId).stream() + .map(UserOrganizationEntityMapper::toDomain) + .toList(); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserRepositoryImpl.java new file mode 100644 index 0000000..79fbb98 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/UserRepositoryImpl.java @@ -0,0 +1,51 @@ +package net.beetechgroup.beetask.frameworks.persistence.repository; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import net.beetechgroup.beetask.entities.User; +import net.beetechgroup.beetask.frameworks.persistence.entities.UserEntity; +import net.beetechgroup.beetask.frameworks.persistence.mapper.UserEntityMapper; +import net.beetechgroup.beetask.entities.organization.UserOrganization; +import net.beetechgroup.beetask.usecase.repository.UserRepository; +import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository; + +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class UserRepositoryImpl implements UserRepository, PanacheRepository { + + @Inject + UserOrganizationRepository userOrganizationRepository; + + @Override + @Transactional + public User save(User user) { + UserEntity entity = UserEntityMapper.toEntity(user); + if (entity.getId() == null) { + persist(entity); + } else { + entity = getEntityManager().merge(entity); + } + return UserEntityMapper.toDomain(entity); + } + + @Override + public Optional findByEmail(String email) { + return find("email", email).firstResultOptional() + .map(UserEntityMapper::toDomain); + } + + @Override + public Optional findUserById(Long id) { + return findByIdOptional(id) + .map(UserEntityMapper::toDomain); + } + + @Override + public List findUserOrganizations(Long userId) { + return userOrganizationRepository.findByUserId(userId); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthController.java index 35e68f8..4bd5885 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthController.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthController.java @@ -9,50 +9,58 @@ import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import jakarta.inject.Inject; +import net.beetechgroup.beetask.usecase.auth.login.LoginInput; +import net.beetechgroup.beetask.usecase.auth.login.LoginOutput; +import net.beetechgroup.beetask.usecase.auth.login.LoginUseCase; +import net.beetechgroup.beetask.usecase.auth.refresh.RefreshTokenInput; +import net.beetechgroup.beetask.usecase.auth.refresh.RefreshTokenUseCase; +import net.beetechgroup.beetask.usecase.user.create.CreateUserInput; +import net.beetechgroup.beetask.usecase.user.create.CreateUserOutput; +import net.beetechgroup.beetask.usecase.user.create.CreateUserUseCase; + import java.time.LocalDateTime; import java.util.List; @Path("/auth") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@Tag(name = "Authentication", description = "Authentication operations (Mocked)") +@Tag(name = "Authentication", description = "Authentication operations") public class AuthController { + @Inject + CreateUserUseCase createUserUseCase; + + @Inject + LoginUseCase loginUseCase; + + @Inject + RefreshTokenUseCase refreshTokenUseCase; + + @Path("/register") + @POST + @Operation(summary = "Register", description = "Creates a new user account") + public CreateAccountResponse register(CreateAccountRequest request) { + CreateUserInput input = AuthControllerMapper.toCreateUserInput(request); + CreateUserOutput output = createUserUseCase.execute(input); + return AuthControllerMapper.toCreateAccountResponse(output); + } + @Path("/login") @POST - @Operation(summary = "Login", description = "Mocked login that returns user data") - public LoginResponse login() { - return new LoginResponse( - "Gabriel Menezes", - "gabriel@beetech.net", - "https://ui-avatars.com/api/?name=Gabriel+Menezes&background=random", - "mocked-jwt-token-abcd-1234", - "mocked-refresh-token-wxyz-5678", - 3600L, // 1 hour - LocalDateTime.now().toString(), - List.of( - new LoginResponse.OrganizationDTO("BeeTech Group", List.of("ADMIN", "OWNER")), - new LoginResponse.OrganizationDTO("SkyMob Dev", List.of("DEVELOPER")) - ) - ); + @Operation(summary = "Login", description = "Login that returns real user data and JWT") + public LoginResponse login(LoginRequest request) { + LoginInput input = AuthControllerMapper.toLoginInput(request); + LoginOutput output = loginUseCase.execute(input); + return AuthControllerMapper.toLoginResponse(output); } @Path("/refresh") @POST - @Operation(summary = "Refresh token", description = "Mocked token refresh") - public LoginResponse refresh() { - return new LoginResponse( - "Gabriel Menezes", - "gabriel@beetech.net", - "https://ui-avatars.com/api/?name=Gabriel+Menezes&background=random", - "refreshed-jwt-token-" + System.currentTimeMillis(), - "refreshed-refresh-token-" + System.currentTimeMillis(), - 3600L, - LocalDateTime.now().toString(), - List.of( - new LoginResponse.OrganizationDTO("BeeTech Group", List.of("ADMIN", "OWNER")), - new LoginResponse.OrganizationDTO("SkyMob Dev", List.of("DEVELOPER")) - ) - ); + @Operation(summary = "Refresh token", description = "Token refresh using refresh token") + public LoginResponse refresh(RefreshTokenRequest request) { + RefreshTokenInput input = AuthControllerMapper.toRefreshTokenInput(request); + LoginOutput output = refreshTokenUseCase.execute(input); + return AuthControllerMapper.toLoginResponse(output); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthControllerMapper.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthControllerMapper.java new file mode 100644 index 0000000..b9a705d --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/AuthControllerMapper.java @@ -0,0 +1,42 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.auth; + +import net.beetechgroup.beetask.usecase.auth.login.LoginInput; +import net.beetechgroup.beetask.usecase.auth.login.LoginOutput; +import net.beetechgroup.beetask.usecase.auth.refresh.RefreshTokenInput; +import net.beetechgroup.beetask.usecase.user.create.CreateUserInput; +import net.beetechgroup.beetask.usecase.user.create.CreateUserOutput; +import java.util.stream.Collectors; + +public class AuthControllerMapper { + + public static CreateUserInput toCreateUserInput(CreateAccountRequest request) { + return new CreateUserInput(request.name(), request.email(), request.password()); + } + + public static CreateAccountResponse toCreateAccountResponse(CreateUserOutput output) { + return new CreateAccountResponse(output.id(), output.name(), output.email()); + } + + public static LoginInput toLoginInput(LoginRequest request) { + return new LoginInput(request.email(), request.password()); + } + + public static RefreshTokenInput toRefreshTokenInput(RefreshTokenRequest request) { + return new RefreshTokenInput(request.refreshToken()); + } + + public static LoginResponse toLoginResponse(LoginOutput output) { + return new LoginResponse( + output.name(), + output.email(), + output.photo(), + output.jwt(), + output.refreshToken(), + output.expiresIn(), + output.issuedAt(), + output.organizations().stream() + .map(org -> new LoginResponse.Organization(org.name(), org.roles())) + .collect(Collectors.toList()) + ); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/CreateAccountRequest.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/CreateAccountRequest.java new file mode 100644 index 0000000..37f1f7b --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/CreateAccountRequest.java @@ -0,0 +1,4 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.auth; + +public record CreateAccountRequest(String name, String email, String password) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/CreateAccountResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/CreateAccountResponse.java new file mode 100644 index 0000000..af765f1 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/CreateAccountResponse.java @@ -0,0 +1,4 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.auth; + +public record CreateAccountResponse(Long id, String name, String email) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginRequest.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginRequest.java new file mode 100644 index 0000000..da8a8bb --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginRequest.java @@ -0,0 +1,4 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.auth; + +public record LoginRequest(String email, String password) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginResponse.java index 2226c4b..67bc0f4 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginResponse.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/LoginResponse.java @@ -10,9 +10,9 @@ public record LoginResponse( String refreshToken, Long expiresIn, String issuedAt, - List organizations + List organizations ) { - public record OrganizationDTO( + public record Organization( String name, List roles ) {} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/RefreshTokenRequest.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/RefreshTokenRequest.java new file mode 100644 index 0000000..7f6c7af --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/auth/RefreshTokenRequest.java @@ -0,0 +1,4 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.auth; + +public record RefreshTokenRequest(String refreshToken) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/CreateTaskResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/CreateTaskResponse.java index 6777a5b..4d53dc4 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/CreateTaskResponse.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/CreateTaskResponse.java @@ -1,5 +1,6 @@ package net.beetechgroup.beetask.interfaceadapters.controllers.task; +import java.time.LocalDateTime; import java.util.List; import net.beetechgroup.beetask.entities.task.TaskStatus; @@ -11,4 +12,9 @@ public record CreateTaskResponse( TaskStatus status, String project, List history) { -} \ No newline at end of file +} + +record TaskHistoryItemResponse( + LocalDateTime startAt, + LocalDateTime endAt) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskHistoryItemResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskHistoryItemResponse.java deleted file mode 100644 index dfca8f5..0000000 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskHistoryItemResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.beetechgroup.beetask.interfaceadapters.controllers.task; - -import java.time.LocalDateTime; - -public record TaskHistoryItemResponse( - LocalDateTime startAt, - LocalDateTime endAt) { -} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginInput.java new file mode 100644 index 0000000..b984767 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginInput.java @@ -0,0 +1,4 @@ +package net.beetechgroup.beetask.usecase.auth.login; + +public record LoginInput(String email, String password) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginOutput.java new file mode 100644 index 0000000..2553434 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginOutput.java @@ -0,0 +1,19 @@ +package net.beetechgroup.beetask.usecase.auth.login; + +import java.util.List; + +public record LoginOutput( + String name, + String email, + String photo, + String jwt, + String refreshToken, + Long expiresIn, + String issuedAt, + List organizations +) { + public record OrganizationOutput( + String name, + List roles + ) {} +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginUseCase.java new file mode 100644 index 0000000..aca3955 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/login/LoginUseCase.java @@ -0,0 +1,74 @@ +package net.beetechgroup.beetask.usecase.auth.login; + +import io.smallrye.jwt.build.Jwt; +import net.beetechgroup.beetask.entities.User; +import net.beetechgroup.beetask.entities.organization.UserOrganization; +import net.beetechgroup.beetask.usecase.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class LoginUseCase { + + private final UserRepository userRepository; + + public LoginUseCase(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public LoginOutput execute(LoginInput input) { + Optional userOptional = userRepository.findByEmail(input.email()); + + if (userOptional.isEmpty()) { + throw new RuntimeException("Invalid credentials"); + } + + User user = userOptional.get(); + + // Simple password check for now (should use Bcrypt) + if (!user.getPassword().equals(input.password())) { + throw new RuntimeException("Invalid credentials"); + } + + List userOrganizations = userRepository.findUserOrganizations(user.getId()); + + Set groups = new HashSet<>(); + userOrganizations.forEach(uo -> groups.add(uo.getRole().name())); + + String token = Jwt.issuer("https://beetech.net") + .upn(user.getEmail()) + .groups(groups) + .claim("name", user.getName()) + .expiresIn(3600) + .sign(); + + List orgs = userOrganizations.stream() + .collect(Collectors.groupingBy(uo -> uo.getOrganization().getName())) + .entrySet().stream() + .map(entry -> new LoginOutput.OrganizationOutput( + entry.getKey(), + entry.getValue().stream().map(uo -> uo.getRole().name()).collect(Collectors.toList()) + )) + .collect(Collectors.toList()); + + String refreshToken = Jwt.issuer("https://beetech.net") + .upn(user.getEmail()) + .expiresIn(86400) // 24 hours + .sign(); + + return new LoginOutput( + user.getName(), + user.getEmail(), + user.getPhoto() != null ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random", + token, + refreshToken, + 3600L, + LocalDateTime.now().toString(), + orgs + ); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenInput.java new file mode 100644 index 0000000..3e7ff0f --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenInput.java @@ -0,0 +1,4 @@ +package net.beetechgroup.beetask.usecase.auth.refresh; + +public record RefreshTokenInput(String refreshToken) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenUseCase.java new file mode 100644 index 0000000..bf80e9d --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/auth/refresh/RefreshTokenUseCase.java @@ -0,0 +1,85 @@ +package net.beetechgroup.beetask.usecase.auth.refresh; + +import io.smallrye.jwt.auth.principal.JWTParser; +import io.smallrye.jwt.auth.principal.ParseException; +import io.smallrye.jwt.build.Jwt; +import net.beetechgroup.beetask.entities.User; +import net.beetechgroup.beetask.entities.organization.UserOrganization; +import net.beetechgroup.beetask.usecase.auth.login.LoginOutput; +import net.beetechgroup.beetask.usecase.repository.UserRepository; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class RefreshTokenUseCase { + + private final UserRepository userRepository; + private final JWTParser jwtParser; + + public RefreshTokenUseCase(UserRepository userRepository, JWTParser jwtParser) { + this.userRepository = userRepository; + this.jwtParser = jwtParser; + } + + public LoginOutput execute(RefreshTokenInput input) { + try { + JsonWebToken token = jwtParser.parse(input.refreshToken()); + String email = token.getSubject(); // Or token.getName() / token.getClaim(Claims.upn.name()) + + if (email == null) { + throw new RuntimeException("Invalid refresh token: email not found"); + } + + Optional userOptional = userRepository.findByEmail(email); + if (userOptional.isEmpty()) { + throw new RuntimeException("User not found for the provided refresh token"); + } + + User user = userOptional.get(); + List userOrganizations = userRepository.findUserOrganizations(user.getId()); + + Set groups = new HashSet<>(); + userOrganizations.forEach(uo -> groups.add(uo.getRole().name())); + + String newToken = Jwt.issuer("https://beetech.net") + .upn(user.getEmail()) + .groups(groups) + .claim("name", user.getName()) + .expiresIn(3600) + .sign(); + + // Generate a new refresh token (also a JWT but with longer expiration) + String newRefreshToken = Jwt.issuer("https://beetech.net") + .upn(user.getEmail()) + .expiresIn(86400) // 24 hours + .sign(); + + List orgs = userOrganizations.stream() + .collect(Collectors.groupingBy(uo -> uo.getOrganization().getName())) + .entrySet().stream() + .map(entry -> new LoginOutput.OrganizationOutput( + entry.getKey(), + entry.getValue().stream().map(uo -> uo.getRole().name()).collect(Collectors.toList()) + )) + .collect(Collectors.toList()); + + return new LoginOutput( + user.getName(), + user.getEmail(), + user.getPhoto() != null ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random", + newToken, + newRefreshToken, + 3600L, + LocalDateTime.now().toString(), + orgs + ); + } catch (ParseException e) { + throw new RuntimeException("Invalid refresh token: " + e.getMessage()); + } + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserOrganizationRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserOrganizationRepository.java new file mode 100644 index 0000000..2746dd5 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserOrganizationRepository.java @@ -0,0 +1,8 @@ +package net.beetechgroup.beetask.usecase.repository; + +import net.beetechgroup.beetask.entities.organization.UserOrganization; +import java.util.List; + +public interface UserOrganizationRepository { + List findByUserId(Long userId); +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserRepository.java new file mode 100644 index 0000000..858c4b8 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/UserRepository.java @@ -0,0 +1,13 @@ +package net.beetechgroup.beetask.usecase.repository; + +import net.beetechgroup.beetask.entities.User; +import java.util.List; +import java.util.Optional; +import net.beetechgroup.beetask.entities.organization.UserOrganization; + +public interface UserRepository { + User save(User user); + Optional findByEmail(String email); + Optional findUserById(Long id); + List findUserOrganizations(Long userId); +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserInput.java new file mode 100644 index 0000000..0edb2b8 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserInput.java @@ -0,0 +1,4 @@ +package net.beetechgroup.beetask.usecase.user.create; + +public record CreateUserInput(String name, String email, String password) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserMapper.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserMapper.java new file mode 100644 index 0000000..4a5afaf --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserMapper.java @@ -0,0 +1,18 @@ +package net.beetechgroup.beetask.usecase.user.create; + +import net.beetechgroup.beetask.entities.User; + +public class CreateUserMapper { + + public static User toUser(CreateUserInput input) { + User user = new User(); + user.setName(input.name()); + user.setEmail(input.email()); + user.setPassword(input.password()); + return user; + } + + public static CreateUserOutput toCreateUserOutput(User user) { + return new CreateUserOutput(user.getId(), user.getName(), user.getEmail()); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserOutput.java new file mode 100644 index 0000000..71c643b --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserOutput.java @@ -0,0 +1,4 @@ +package net.beetechgroup.beetask.usecase.user.create; + +public record CreateUserOutput(Long id, String name, String email) { +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserUseCase.java new file mode 100644 index 0000000..e9b00bb --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/user/create/CreateUserUseCase.java @@ -0,0 +1,24 @@ +package net.beetechgroup.beetask.usecase.user.create; + +import net.beetechgroup.beetask.entities.User; +import net.beetechgroup.beetask.usecase.repository.UserRepository; + +public class CreateUserUseCase { + + private final UserRepository userRepository; + + public CreateUserUseCase(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public CreateUserOutput execute(CreateUserInput input) { + if (userRepository.findByEmail(input.email()).isPresent()) { + throw new RuntimeException("User already exists with this email"); + } + + User user = CreateUserMapper.toUser(input); + User savedUser = userRepository.save(user); + + return CreateUserMapper.toCreateUserOutput(savedUser); + } +}