-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rest): added testing version of OperationHandler
- Loading branch information
1 parent
ade002f
commit a923637
Showing
8 changed files
with
389 additions
and
29 deletions.
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
...runtime/src/main/java/dev/cloudeko/zenei/extension/core/feature/AuthorizationManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package dev.cloudeko.zenei.extension.core.feature; | ||
|
||
import dev.cloudeko.zenei.extension.core.model.session.SessionToken; | ||
|
||
public interface AuthorizationManager { | ||
|
||
SessionToken loginWithPassword(String identifier, String password); | ||
|
||
SessionToken loginWithAuthorizationCode(String provider, String code); | ||
|
||
SessionToken swapRefreshToken(String refreshToken); | ||
} |
19 changes: 19 additions & 0 deletions
19
...core/runtime/src/main/java/dev/cloudeko/zenei/extension/core/feature/UserDataManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package dev.cloudeko.zenei.extension.core.feature; | ||
|
||
import dev.cloudeko.zenei.extension.core.model.user.CreateUserInput; | ||
import dev.cloudeko.zenei.extension.core.model.user.User; | ||
|
||
import java.util.List; | ||
|
||
public interface UserDataManager { | ||
|
||
User findUserByIdentifier(String identifier); | ||
|
||
List<User> listUsers(int offset, int limit); | ||
|
||
User createUser(CreateUserInput input); | ||
|
||
void updateUser(CreateUserInput input); | ||
|
||
void deleteUser(String identifier); | ||
} |
180 changes: 180 additions & 0 deletions
180
...main/java/dev/cloudeko/zenei/extension/core/feature/impl/DefaultAuthorizationManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
package dev.cloudeko.zenei.extension.core.feature.impl; | ||
|
||
import dev.cloudeko.zenei.extension.core.exception.*; | ||
import dev.cloudeko.zenei.extension.core.feature.AuthorizationManager; | ||
import dev.cloudeko.zenei.extension.core.feature.util.TokenUtil; | ||
import dev.cloudeko.zenei.extension.core.model.account.ExternalAccessToken; | ||
import dev.cloudeko.zenei.extension.core.model.account.ExternalAccount; | ||
import dev.cloudeko.zenei.extension.core.model.email.EmailAddress; | ||
import dev.cloudeko.zenei.extension.core.model.session.SessionToken; | ||
import dev.cloudeko.zenei.extension.core.model.user.User; | ||
import dev.cloudeko.zenei.extension.core.provider.HashProvider; | ||
import dev.cloudeko.zenei.extension.core.provider.RefreshTokenProvider; | ||
import dev.cloudeko.zenei.extension.core.provider.TokenProvider; | ||
import dev.cloudeko.zenei.extension.core.repository.RefreshTokenRepository; | ||
import dev.cloudeko.zenei.extension.core.repository.UserPasswordRepository; | ||
import dev.cloudeko.zenei.extension.core.repository.UserRepository; | ||
import dev.cloudeko.zenei.extension.external.ExternalAuthProvider; | ||
import dev.cloudeko.zenei.extension.external.ExternalAuthResolver; | ||
import dev.cloudeko.zenei.extension.external.ExternalUserProfile; | ||
import dev.cloudeko.zenei.extension.external.providers.AvailableProvider; | ||
import dev.cloudeko.zenei.extension.external.web.client.ExternalProviderAccessToken; | ||
import dev.cloudeko.zenei.extension.external.web.client.LoginOAuthClient; | ||
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; | ||
import jakarta.enterprise.context.ApplicationScoped; | ||
import lombok.AllArgsConstructor; | ||
|
||
import java.net.URI; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
@ApplicationScoped | ||
@AllArgsConstructor | ||
public class DefaultAuthorizationManager implements AuthorizationManager { | ||
|
||
private final UserPasswordRepository userPasswordRepository; | ||
private final RefreshTokenRepository refreshTokenRepository; | ||
private final UserRepository userRepository; | ||
|
||
private final RefreshTokenProvider refreshTokenProvider; | ||
private final ExternalAuthResolver externalAuthenticationProvider; | ||
private final TokenProvider tokenProvider; | ||
private final HashProvider hashProvider; | ||
|
||
@Override | ||
public SessionToken loginWithPassword(String identifier, String password) { | ||
final var userPassword = userPasswordRepository.getUserPasswordByEmail(identifier); | ||
|
||
if (userPassword.isEmpty()) { | ||
throw new UserNotFoundException(); | ||
} | ||
|
||
if (hashProvider.checkPassword(password, userPassword.get().getPasswordHash())) { | ||
final var user = userPassword.get().getUser(); | ||
|
||
final var refreshTokenData = refreshTokenProvider.generateRefreshToken(user); | ||
final var accessTokenData = tokenProvider.generateToken(user); | ||
|
||
final var refreshToken = TokenUtil.createRefreshToken(user, refreshTokenData); | ||
final var token = refreshTokenRepository.createRefreshToken(refreshToken); | ||
|
||
return TokenUtil.createToken(user, accessTokenData, refreshToken); | ||
} else { | ||
throw new InvalidPasswordException(); | ||
} | ||
} | ||
|
||
@Override | ||
public SessionToken loginWithAuthorizationCode(String provider, String code) { | ||
final var externalProvider = getExternalAuthProvider(provider); | ||
|
||
final var client = QuarkusRestClientBuilder.newBuilder() | ||
.baseUri(URI.create(externalProvider.getTokenEndpoint())) | ||
.build(LoginOAuthClient.class); | ||
|
||
final var accessToken = client.getAccessToken("authorization_code", | ||
externalProvider.config().clientId(), | ||
externalProvider.config().clientSecret(), | ||
code, AvailableProvider.getProvider(provider).getRedirectUri()); | ||
|
||
if (accessToken == null) { | ||
throw new IllegalArgumentException("Invalid authorization code"); | ||
} | ||
|
||
final var externalUserProfile = externalProvider.getExternalUserProfile(accessToken); | ||
|
||
final var externalEmail = getEmailFromUserProfile(externalUserProfile); | ||
|
||
if (externalEmail.isEmpty()) { | ||
throw new EmailNotFoundException(); | ||
} | ||
|
||
var user = userRepository.getByAccountProviderId(externalUserProfile.getId()) | ||
.orElse(createUserFromExternalUserProfile(externalUserProfile, externalEmail.get(), provider, accessToken)); | ||
|
||
final var refreshTokenData = refreshTokenProvider.generateRefreshToken(user); | ||
final var accessTokenData = tokenProvider.generateToken(user); | ||
|
||
final var refreshToken = TokenUtil.createRefreshToken(user, refreshTokenData); | ||
final var token = refreshTokenRepository.createRefreshToken(refreshToken); | ||
|
||
return TokenUtil.createToken(user, accessTokenData, refreshToken); | ||
} | ||
|
||
@Override | ||
public SessionToken swapRefreshToken(String refreshToken) { | ||
final var token = refreshTokenRepository.findRefreshTokenByToken(refreshToken); | ||
|
||
if (token.isEmpty()) { | ||
throw new InvalidRefreshTokenException(); | ||
} | ||
|
||
final var user = token.get().getUser(); | ||
|
||
final var refreshTokenData = refreshTokenProvider.generateRefreshToken(user); | ||
final var accessTokenData = tokenProvider.generateToken(user); | ||
|
||
final var newRefreshToken = TokenUtil.createRefreshToken(user, refreshTokenData); | ||
final var newToken = refreshTokenRepository.swapRefreshToken(token.get(), newRefreshToken); | ||
if (newToken == null) { | ||
throw new InvalidRefreshTokenException(); | ||
} | ||
|
||
return TokenUtil.createToken(user, accessTokenData, newRefreshToken); | ||
} | ||
|
||
private ExternalAuthProvider getExternalAuthProvider(String provider) { | ||
return externalAuthenticationProvider.getAuthProvider(provider).orElseThrow(InvalidExternalAuthProvider::new); | ||
} | ||
|
||
private Optional<ExternalUserProfile.ExternalUserEmail> getEmailFromUserProfile(ExternalUserProfile externalUserProfile) { | ||
return externalUserProfile.getEmails().stream().filter(email -> email.primary() && email.verified()).findFirst(); | ||
} | ||
|
||
private User createUserFromExternalUserProfile(ExternalUserProfile externalUserProfile, | ||
ExternalUserProfile.ExternalUserEmail externalEmail, | ||
String provider, | ||
ExternalProviderAccessToken accessToken) { | ||
|
||
final var emailAddress = EmailAddress.builder().email(externalEmail.email()).emailVerified(true).build(); | ||
final var externalAccessToken = ExternalAccessToken.builder() | ||
.accessToken(accessToken.getAccessToken()) | ||
.refreshToken(accessToken.getRefreshToken()) | ||
.tokenType(accessToken.getTokenType()) | ||
.scope(accessToken.getScope()) | ||
.build(); | ||
|
||
final var account = ExternalAccount.builder() | ||
.provider(provider) | ||
.providerId(externalUserProfile.getId()) | ||
.accessTokens(new ArrayList<>(List.of(externalAccessToken))) | ||
.build(); | ||
|
||
final var user = User.builder() | ||
.username(externalUserProfile.getUsername()) | ||
.primaryEmailAddress(emailAddress.getEmail()) | ||
.emailAddresses(new ArrayList<>(List.of(emailAddress))) | ||
.accounts(new ArrayList<>(List.of(account))) | ||
.build(); | ||
|
||
checkExistingUsername(user.getUsername()); | ||
checkExistingEmail(user.getPrimaryEmailAddress().getEmail()); | ||
|
||
userRepository.createUser(user); | ||
|
||
return user; | ||
} | ||
|
||
private void checkExistingUsername(String username) { | ||
if (userRepository.existsByUsername(username)) { | ||
throw new UsernameAlreadyExistsException(); | ||
} | ||
} | ||
|
||
private void checkExistingEmail(String email) { | ||
if (userRepository.existsByEmail(email)) { | ||
throw new EmailAlreadyExistsException(); | ||
} | ||
} | ||
} |
110 changes: 110 additions & 0 deletions
110
.../src/main/java/dev/cloudeko/zenei/extension/core/feature/impl/DefaultUserDataManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package dev.cloudeko.zenei.extension.core.feature.impl; | ||
|
||
import dev.cloudeko.zenei.extension.core.config.ApplicationConfig; | ||
import dev.cloudeko.zenei.extension.core.exception.EmailAlreadyExistsException; | ||
import dev.cloudeko.zenei.extension.core.exception.UserNotFoundException; | ||
import dev.cloudeko.zenei.extension.core.exception.UsernameAlreadyExistsException; | ||
import dev.cloudeko.zenei.extension.core.feature.UserDataManager; | ||
import dev.cloudeko.zenei.extension.core.model.email.EmailAddress; | ||
import dev.cloudeko.zenei.extension.core.model.user.CreateUserInput; | ||
import dev.cloudeko.zenei.extension.core.model.user.User; | ||
import dev.cloudeko.zenei.extension.core.model.user.UserPassword; | ||
import dev.cloudeko.zenei.extension.core.provider.HashProvider; | ||
import dev.cloudeko.zenei.extension.core.provider.StringTokenProvider; | ||
import dev.cloudeko.zenei.extension.core.repository.UserPasswordRepository; | ||
import dev.cloudeko.zenei.extension.core.repository.UserRepository; | ||
import jakarta.enterprise.context.ApplicationScoped; | ||
import jakarta.transaction.Transactional; | ||
import lombok.AllArgsConstructor; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.UUID; | ||
|
||
@ApplicationScoped | ||
@AllArgsConstructor | ||
public class DefaultUserDataManager implements UserDataManager { | ||
|
||
private final ApplicationConfig config; | ||
|
||
private final HashProvider hashProvider; | ||
private final StringTokenProvider stringTokenProvider; | ||
|
||
private final UserRepository userRepository; | ||
private final UserPasswordRepository userPasswordRepository; | ||
|
||
@Override | ||
public User findUserByIdentifier(String identifier) { | ||
if (identifier == null) { | ||
throw new UserNotFoundException(); | ||
} | ||
|
||
if (identifier.matches("\\d+")) { | ||
return userRepository.getUserById(Long.parseLong(identifier)).orElseThrow(UserNotFoundException::new); | ||
} | ||
|
||
return userRepository.getUserByUsername(identifier).orElseThrow(UserNotFoundException::new); | ||
} | ||
|
||
@Override | ||
public List<User> listUsers(int offset, int limit) { | ||
return userRepository.listUsers(offset, limit); | ||
} | ||
|
||
@Override | ||
@Transactional | ||
public User createUser(CreateUserInput input) { | ||
final var emailAddress = EmailAddress.builder().email(input.getEmail()).emailVerified(true).build(); | ||
if (!config.getAutoConfirm()) { | ||
final var token = stringTokenProvider.generateToken("mail", emailAddress.getEmail() + UUID.randomUUID()); | ||
|
||
emailAddress.setEmailVerificationToken(token); | ||
emailAddress.setEmailVerificationTokenExpiresAt(LocalDateTime.now().plusDays(1)); | ||
emailAddress.setEmailVerified(false); | ||
} | ||
|
||
final var user = User.builder() | ||
.username(input.getUsername()) | ||
.primaryEmailAddress(emailAddress.getEmail()) | ||
.emailAddresses(new ArrayList<>(List.of(emailAddress))).build(); | ||
|
||
checkExistingUsername(user.getUsername()); | ||
checkExistingEmail(user.getPrimaryEmailAddress().getEmail()); | ||
|
||
userRepository.createUser(user); | ||
|
||
if (input.isPasswordEnabled()) { | ||
final var userPassword = UserPassword.builder() | ||
.user(user) | ||
.passwordHash(hashProvider.hashPassword(input.getPassword())) | ||
.build(); | ||
|
||
userPasswordRepository.createUserPassword(userPassword); | ||
} | ||
|
||
return user; | ||
} | ||
|
||
@Override | ||
public void updateUser(CreateUserInput input) { | ||
|
||
} | ||
|
||
@Override | ||
public void deleteUser(String identifier) { | ||
|
||
} | ||
|
||
private void checkExistingUsername(String username) { | ||
if (userRepository.existsByUsername(username)) { | ||
throw new UsernameAlreadyExistsException(); | ||
} | ||
} | ||
|
||
private void checkExistingEmail(String email) { | ||
if (userRepository.existsByEmail(email)) { | ||
throw new EmailAlreadyExistsException(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
...ime/src/main/java/dev/cloudeko/zenei/extension/rest/endpoint/client/OperationHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package dev.cloudeko.zenei.extension.rest.endpoint.client; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import io.vertx.core.Handler; | ||
import io.vertx.core.http.HttpMethod; | ||
import io.vertx.core.http.HttpServerRequest; | ||
import io.vertx.ext.web.RoutingContext; | ||
import jakarta.ws.rs.core.MediaType; | ||
import jakarta.ws.rs.core.Response; | ||
|
||
public abstract class OperationHandler implements Handler<RoutingContext> { | ||
|
||
protected final ObjectMapper objectMapper = new ObjectMapper(); | ||
protected final HttpMethod method; | ||
|
||
public OperationHandler(HttpMethod method) { | ||
this.method = method; | ||
} | ||
|
||
protected abstract Response handleRequest(RoutingContext event); | ||
|
||
@Override | ||
public void handle(RoutingContext event) { | ||
if (event.request().method().equals(HttpMethod.OPTIONS)) { | ||
event.response().putHeader("Allow", method.toString() + ", OPTIONS"); | ||
event.next(); | ||
return; | ||
} | ||
|
||
if (!event.request().method().equals(method)) { | ||
event.response().setStatusCode(405).end(); | ||
return; | ||
} | ||
|
||
try (Response response = handleRequest(event)) { | ||
event.response() | ||
.putHeader("Content-Type", MediaType.APPLICATION_JSON) | ||
.setStatusCode(response.getStatus()) | ||
.end(objectMapper.writeValueAsString(response.getEntity())); | ||
} catch (JsonProcessingException e) { | ||
event.response().setStatusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).end(); | ||
} | ||
} | ||
} |
Oops, something went wrong.