Skip to content

Commit

Permalink
feat(rest): added testing version of OperationHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
zZHorizonZz committed Aug 20, 2024
1 parent ade002f commit a923637
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 29 deletions.
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);
}
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);
}
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();
}
}
}
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();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.cloudeko.zenei.extension.rest.deployment;

import dev.cloudeko.zenei.extension.rest.endpoint.client.UserInfoHandler;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
Expand All @@ -17,8 +18,8 @@ public FeatureBuildItem feature() {
@BuildStep
public RouteBuildItem route(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
return nonApplicationRootPathBuildItem.routeBuilder()
.route("custom-endpoint")
.handler(new MyCustomHandler())
.route("user")
.handler(new UserInfoHandler())
.displayOnNotFoundPage()
.build();
}
Expand Down
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();
}
}
}
Loading

0 comments on commit a923637

Please sign in to comment.