create account, login e refresh token

This commit is contained in:
Gabriel Santana de Menezes 2026-05-02 22:25:38 -04:00
parent 953730fffd
commit 4a2f4371e0
29 changed files with 580 additions and 42 deletions

View file

@ -39,6 +39,14 @@
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId> <artifactId>quarkus-rest-jackson</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId> <artifactId>quarkus-smallrye-openapi</artifactId>

View file

@ -4,6 +4,8 @@ public class User {
private Long id; private Long id;
private String name; private String name;
private String email; private String email;
private String password;
private String photo;
public Long getId() { public Long getId() {
return id; return id;
@ -28,4 +30,20 @@ public class User {
public void setEmail(String email) { public void setEmail(String email) {
this.email = 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;
}
} }

View file

@ -6,4 +6,28 @@ public class UserOrganization {
private User user; private User user;
private Organization organization; private Organization organization;
private UserOrganizationRole role; 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;
}
} }

View file

@ -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);
}
}

View file

@ -14,6 +14,8 @@ public class UserEntity {
private Long id; private Long id;
private String name; private String name;
private String email; private String email;
private String password;
private String photo;
public Long getId() { public Long getId() {
return id; return id;
@ -38,4 +40,20 @@ public class UserEntity {
public void setEmail(String email) { public void setEmail(String email) {
this.email = 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;
}
} }

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<UserOrganizationEntity> {
@Override
public List<UserOrganization> findByUserId(Long userId) {
return find("user.id", userId).stream()
.map(UserOrganizationEntityMapper::toDomain)
.toList();
}
}

View file

@ -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<UserEntity> {
@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<User> findByEmail(String email) {
return find("email", email).firstResultOptional()
.map(UserEntityMapper::toDomain);
}
@Override
public Optional<User> findUserById(Long id) {
return findByIdOptional(id)
.map(UserEntityMapper::toDomain);
}
@Override
public List<UserOrganization> findUserOrganizations(Long userId) {
return userOrganizationRepository.findByUserId(userId);
}
}

View file

@ -9,50 +9,58 @@ import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag; 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.time.LocalDateTime;
import java.util.List; import java.util.List;
@Path("/auth") @Path("/auth")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Authentication", description = "Authentication operations (Mocked)") @Tag(name = "Authentication", description = "Authentication operations")
public class AuthController { 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") @Path("/login")
@POST @POST
@Operation(summary = "Login", description = "Mocked login that returns user data") @Operation(summary = "Login", description = "Login that returns real user data and JWT")
public LoginResponse login() { public LoginResponse login(LoginRequest request) {
return new LoginResponse( LoginInput input = AuthControllerMapper.toLoginInput(request);
"Gabriel Menezes", LoginOutput output = loginUseCase.execute(input);
"gabriel@beetech.net", return AuthControllerMapper.toLoginResponse(output);
"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"))
)
);
} }
@Path("/refresh") @Path("/refresh")
@POST @POST
@Operation(summary = "Refresh token", description = "Mocked token refresh") @Operation(summary = "Refresh token", description = "Token refresh using refresh token")
public LoginResponse refresh() { public LoginResponse refresh(RefreshTokenRequest request) {
return new LoginResponse( RefreshTokenInput input = AuthControllerMapper.toRefreshTokenInput(request);
"Gabriel Menezes", LoginOutput output = refreshTokenUseCase.execute(input);
"gabriel@beetech.net", return AuthControllerMapper.toLoginResponse(output);
"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"))
)
);
} }
} }

View file

@ -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())
);
}
}

View file

@ -0,0 +1,4 @@
package net.beetechgroup.beetask.interfaceadapters.controllers.auth;
public record CreateAccountRequest(String name, String email, String password) {
}

View file

@ -0,0 +1,4 @@
package net.beetechgroup.beetask.interfaceadapters.controllers.auth;
public record CreateAccountResponse(Long id, String name, String email) {
}

View file

@ -0,0 +1,4 @@
package net.beetechgroup.beetask.interfaceadapters.controllers.auth;
public record LoginRequest(String email, String password) {
}

View file

@ -10,9 +10,9 @@ public record LoginResponse(
String refreshToken, String refreshToken,
Long expiresIn, Long expiresIn,
String issuedAt, String issuedAt,
List<OrganizationDTO> organizations List<Organization> organizations
) { ) {
public record OrganizationDTO( public record Organization(
String name, String name,
List<String> roles List<String> roles
) {} ) {}

View file

@ -0,0 +1,4 @@
package net.beetechgroup.beetask.interfaceadapters.controllers.auth;
public record RefreshTokenRequest(String refreshToken) {
}

View file

@ -1,5 +1,6 @@
package net.beetechgroup.beetask.interfaceadapters.controllers.task; package net.beetechgroup.beetask.interfaceadapters.controllers.task;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import net.beetechgroup.beetask.entities.task.TaskStatus; import net.beetechgroup.beetask.entities.task.TaskStatus;
@ -12,3 +13,8 @@ public record CreateTaskResponse(
String project, String project,
List<TaskHistoryItemResponse> history) { List<TaskHistoryItemResponse> history) {
} }
record TaskHistoryItemResponse(
LocalDateTime startAt,
LocalDateTime endAt) {
}

View file

@ -1,8 +0,0 @@
package net.beetechgroup.beetask.interfaceadapters.controllers.task;
import java.time.LocalDateTime;
public record TaskHistoryItemResponse(
LocalDateTime startAt,
LocalDateTime endAt) {
}

View file

@ -0,0 +1,4 @@
package net.beetechgroup.beetask.usecase.auth.login;
public record LoginInput(String email, String password) {
}

View file

@ -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<OrganizationOutput> organizations
) {
public record OrganizationOutput(
String name,
List<String> roles
) {}
}

View file

@ -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<User> 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<UserOrganization> userOrganizations = userRepository.findUserOrganizations(user.getId());
Set<String> 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<LoginOutput.OrganizationOutput> 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
);
}
}

View file

@ -0,0 +1,4 @@
package net.beetechgroup.beetask.usecase.auth.refresh;
public record RefreshTokenInput(String refreshToken) {
}

View file

@ -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<User> userOptional = userRepository.findByEmail(email);
if (userOptional.isEmpty()) {
throw new RuntimeException("User not found for the provided refresh token");
}
User user = userOptional.get();
List<UserOrganization> userOrganizations = userRepository.findUserOrganizations(user.getId());
Set<String> 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<LoginOutput.OrganizationOutput> 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());
}
}
}

View file

@ -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<UserOrganization> findByUserId(Long userId);
}

View file

@ -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<User> findByEmail(String email);
Optional<User> findUserById(Long id);
List<UserOrganization> findUserOrganizations(Long userId);
}

View file

@ -0,0 +1,4 @@
package net.beetechgroup.beetask.usecase.user.create;
public record CreateUserInput(String name, String email, String password) {
}

View file

@ -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());
}
}

View file

@ -0,0 +1,4 @@
package net.beetechgroup.beetask.usecase.user.create;
public record CreateUserOutput(Long id, String name, String email) {
}

View file

@ -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);
}
}