diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/config/SpringSecurityConfiguration.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/config/SpringSecurityConfiguration.java index 0da86f43c..479ca08df 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/config/SpringSecurityConfiguration.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/config/SpringSecurityConfiguration.java @@ -82,7 +82,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // SECURED by Web3 auth .requestMatchers(new AntPathRequestMatcher("/api/vote/cast", POST.name())).authenticated() // SECURED by JWT auth - .requestMatchers(new AntPathRequestMatcher("/api/vote/casting-available/**", HEAD.name())).authenticated() + .requestMatchers(new AntPathRequestMatcher("/api/vote/vote-changing-available/**", HEAD.name())).authenticated() // without auth .requestMatchers(new AntPathRequestMatcher("/api/leaderboard/**", GET.name())).permitAll() diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/CategoryProposalPair.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/CategoryProposalPair.java new file mode 100644 index 000000000..882a9ea1e --- /dev/null +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/CategoryProposalPair.java @@ -0,0 +1,4 @@ +package org.cardano.foundation.voting.domain; + +public record CategoryProposalPair(String categoryId, String proposalId) { +} diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/Role.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/Role.java index b8ee7a281..c12bd0a39 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/Role.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/Role.java @@ -3,13 +3,13 @@ import org.cardano.foundation.voting.domain.web3.Web3Action; import java.util.List; +import java.util.stream.Stream; -import static org.cardano.foundation.voting.domain.web3.Web3Action.IS_VOTE_CASTING_ALLOWED; -import static org.cardano.foundation.voting.domain.web3.Web3Action.VIEW_VOTE_RECEIPT; +import static org.cardano.foundation.voting.domain.web3.Web3Action.*; public enum Role { - VOTER(List.of(VIEW_VOTE_RECEIPT, IS_VOTE_CASTING_ALLOWED)); + VOTER(List.of(VIEW_VOTE_RECEIPT, IS_VOTE_CASTING_ALLOWED, VOTED_ON)); private final List allowedActions; @@ -17,6 +17,10 @@ public enum Role { this.allowedActions = allowedActions; } + public static String supportedRoles() { + return String.join(", ", Stream.of(Role.values()).map(Role::name).toList()); + } + public List allowedActions() { return allowedActions; } diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/web3/Web3Action.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/web3/Web3Action.java index e42094e47..8953cfd4c 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/web3/Web3Action.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/web3/Web3Action.java @@ -2,6 +2,16 @@ public enum Web3Action { - CAST_VOTE, VIEW_VOTE_RECEIPT, LOGIN, IS_VOTE_CASTING_ALLOWED + CAST_VOTE, // casting vote + + VIEW_VOTE_RECEIPT, // obtaining vote receipt + + LOGIN, // JWT login based on WEB3 login + + IS_VOTE_CASTING_ALLOWED, // checking if vote casting is still allowed + + IS_VOTE_CHANGING_ALLOWED, // checking if vote casting changing + + VOTED_ON // getting list of categories and proposals user already voted on } diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/repository/VoteRepository.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/repository/VoteRepository.java index 9ea472c89..1d4a5f915 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/repository/VoteRepository.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/repository/VoteRepository.java @@ -13,6 +13,9 @@ @Repository public interface VoteRepository extends JpaRepository { + @Query("SELECT v.categoryId as categoryId, v.proposalId as proposalId FROM Vote v WHERE v.eventId = :eventId AND v.stakeAddress = :stakeAddress ORDER BY v.votedAtSlot") + List getVotedOn(@Param("eventId") String eventId, @Param("stakeAddress") String stakeAddress); + @Query("SELECT v FROM Vote v WHERE v.eventId = :eventId ORDER BY v.votedAtSlot, v.createdAt DESC") List findAllCompactVotesByEventId(@Param("eventId") String eventId); @@ -27,6 +30,14 @@ public interface VoteRepository extends JpaRepository { @Query("SELECT v.categoryId as categoryId, v.proposalId AS proposalId, COUNT(v) AS totalVoteCount, SUM(v.votingPower) AS totalVotingPower FROM Vote v WHERE v.eventId = :eventId AND v.categoryId = :categoryId GROUP BY proposalId") List getCategoryLevelStats(@Param("eventId") String eventId, @Param("categoryId") String categoryId); + interface CategoryProposalProjection { + + String getCategoryId(); + + String getProposalId(); + + } + interface HighLevelEventVoteCount { @Nullable diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/LoginResource.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/LoginResource.java index 429cdc911..94ee02d01 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/LoginResource.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/LoginResource.java @@ -25,7 +25,7 @@ public class LoginResource { @RequestMapping(value = "/login", method = GET, produces = "application/json") @Timed(value = "resource.auth.login", histogram = true) public ResponseEntity login(Authentication authentication) { - if (!(authentication instanceof Web3AuthenticationToken)) { + if (!(authentication instanceof Web3AuthenticationToken web3AuthenticationToken)) { var problem = Problem.builder() .withTitle("WEB3_AUTH_REQUIRED") .withDetail("CIP-93 auth headers tokens needed!") @@ -35,7 +35,7 @@ public ResponseEntity login(Authentication authentication) { return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); } - return loginService.login((Web3AuthenticationToken) authentication) + return loginService.login(web3AuthenticationToken) .fold(problem -> ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem), ResponseEntity::ok ); diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/VoteResource.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/VoteResource.java index 9fd88bcda..12b6894a7 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/VoteResource.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/VoteResource.java @@ -13,6 +13,8 @@ import org.springframework.web.bind.annotation.RestController; import org.zalando.problem.Problem; +import java.util.Optional; + import static org.springframework.http.HttpStatus.NOT_ACCEPTABLE; import static org.springframework.web.bind.annotation.RequestMethod.*; import static org.zalando.problem.Status.BAD_REQUEST; @@ -25,12 +27,49 @@ public class VoteResource { private final VoteService voteService; + @RequestMapping(value = "/voted-on/{eventId}", method = GET, produces = "application/json") + @Timed(value = "resource.vote.votedOn", histogram = true) + public ResponseEntity votedOn(@PathVariable(value = "eventId", required = false) Optional maybeEventId, + Authentication authentication) { + if (!(authentication instanceof JwtAuthenticationToken jwtAuth)) { + var problem = Problem.builder() + .withTitle("JWT_REQUIRED") + .withDetail("JWT auth Bearer Auth token needed!") + .withStatus(BAD_REQUEST) + .build(); + + return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); + } + + if (maybeEventId.isPresent() && !maybeEventId.orElseThrow().equals(jwtAuth.eventDetails().id())) { + var problem = Problem.builder() + .withTitle("EVENT_ID_MISMATCH") + .withDetail("Event id in path and in JWT token do not match!") + .withStatus(BAD_REQUEST) + .build(); + + return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); + } + + return voteService.getVotedOn(jwtAuth) + .fold(problem -> { + log.warn("Vote get voted on failed, problem:{}", problem); + + return ResponseEntity + .status(problem.getStatus().getStatusCode()) + .body(problem); + }, + categoryProposalPairs -> { + return ResponseEntity.ok().body(categoryProposalPairs); + }); + } + @RequestMapping(value = "/cast", method = POST, produces = "application/json") @Timed(value = "resource.vote.cast", histogram = true) public ResponseEntity castVote(Authentication authentication) { log.info("Casting vote..."); - if (!(authentication instanceof Web3AuthenticationToken)) { + if (!(authentication instanceof Web3AuthenticationToken web3Auth)) { var problem = Problem.builder() .withTitle("WEB3_AUTH_REQUIRED") .withDetail("CIP-93 auth headers tokens needed!") @@ -40,7 +79,7 @@ public ResponseEntity castVote(Authentication authentication) { return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); } - return voteService.castVote((Web3AuthenticationToken) authentication) + return voteService.castVote(web3Auth) .fold(problem -> { log.warn("Vote cast failed, problem:{}", problem); @@ -59,7 +98,7 @@ public ResponseEntity castVote(Authentication authentication) { @Timed(value = "resource.vote.receipt.web3", histogram = true) public ResponseEntity getVoteReceipt(Authentication authentication) { - if (!(authentication instanceof Web3AuthenticationToken)) { + if (!(authentication instanceof Web3AuthenticationToken web3Auth)) { var problem = Problem.builder() .withTitle("WEB3_AUTH_REQUIRED") .withDetail("CIP-93 auth headers tokens needed!") @@ -69,19 +108,19 @@ public ResponseEntity getVoteReceipt(Authentication authentication) { return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); } - return voteService.voteReceipt((Web3AuthenticationToken) authentication) + return voteService.voteReceipt(web3Auth) .fold(problem -> ResponseEntity .status(problem.getStatus().getStatusCode()) .body(problem), voteReceipt -> ResponseEntity.ok().body(voteReceipt)); } - @RequestMapping(value = "/receipt/{eventId}/{categoryId}", method = GET, produces = "application/json") + @RequestMapping(value = "/receipt/{maybeEventId}/{categoryId}", method = GET, produces = "application/json") @Timed(value = "resource.vote.receipt.jwt", histogram = true) - public ResponseEntity getVoteReceipt(@PathVariable("eventId") String eventId, + public ResponseEntity getVoteReceipt(@PathVariable(value = "eventId", required = false) Optional maybeEventId, @PathVariable("categoryId") String categoryId, Authentication authentication) { - if (!(authentication instanceof JwtAuthenticationToken)) { + if (!(authentication instanceof JwtAuthenticationToken jwtAuth)) { var problem = Problem.builder() .withTitle("JWT_REQUIRED") .withDetail("JWT auth token needed!") @@ -91,21 +130,29 @@ public ResponseEntity getVoteReceipt(@PathVariable("eventId") String eventId, return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); } - var jwtAuth = (JwtAuthenticationToken) authentication; + if (maybeEventId.isPresent() && !maybeEventId.orElseThrow().equals(jwtAuth.eventDetails().id())) { + var problem = Problem.builder() + .withTitle("EVENT_ID_MISMATCH") + .withDetail("Event id in path and in JWT token do not match!") + .withStatus(BAD_REQUEST) + .build(); - return voteService.voteReceipt(jwtAuth, eventId, categoryId) + return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); + } + + return voteService.voteReceipt(categoryId, jwtAuth) .fold(problem -> ResponseEntity .status(problem.getStatus().getStatusCode()) .body(problem), voteReceipt -> ResponseEntity.ok().body(voteReceipt)); } - @RequestMapping(value = "/casting-available/{eventId}/{voteId}", method = HEAD, produces = "application/json") + @RequestMapping(value = "/vote-changing-available/{eventId}/{voteId}", method = HEAD, produces = "application/json") @Timed(value = "resource.voteId.receipt", histogram = true) - public ResponseEntity isVoteCastingStillPossible(@PathVariable("eventId") String eventId, - @PathVariable("voteId") String voteId, - Authentication authentication) { - if (!(authentication instanceof JwtAuthenticationToken)) { + public ResponseEntity isVoteChangingAvailable(@PathVariable(value = "eventId", required = false) Optional maybeEventId, + @PathVariable("voteId") String voteId, + Authentication authentication) { + if (!(authentication instanceof JwtAuthenticationToken jwtAuth)) { var problem = Problem.builder() .withTitle("JWT_REQUIRED") .withDetail("JWT auth Bearer Auth token needed!") @@ -115,9 +162,17 @@ public ResponseEntity isVoteCastingStillPossible(@PathVariable("eventId") Str return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); } - var jwtAuth = (JwtAuthenticationToken) authentication; + if (maybeEventId.isPresent() && maybeEventId.orElseThrow().equals(jwtAuth.eventDetails().id())) { + var problem = Problem.builder() + .withTitle("EVENT_ID_MISMATCH") + .withDetail("Event id in path and in JWT token do not match!") + .withStatus(BAD_REQUEST) + .build(); + + return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); + } - return voteService.isVoteCastingStillPossible(jwtAuth, eventId, voteId) + return voteService.isVoteChangingPossible(voteId, jwtAuth) .fold(problem -> ResponseEntity .status(problem.getStatus().getStatusCode()) .body(problem), diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtAuthenticationToken.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtAuthenticationToken.java index cc3a92ee0..435a1ec86 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtAuthenticationToken.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtAuthenticationToken.java @@ -1,21 +1,53 @@ package org.cardano.foundation.voting.service.auth.jwt; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.experimental.Accessors; +import org.cardano.foundation.voting.client.ChainFollowerClient; +import org.cardano.foundation.voting.domain.Role; +import org.cardano.foundation.voting.domain.web3.Web3Action; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import javax.annotation.Nullable; import java.util.Collection; +@Setter +@Getter +@Accessors(fluent = true) public class JwtAuthenticationToken extends AbstractAuthenticationToken { private final JwtPrincipal principal; - public JwtAuthenticationToken(JwtPrincipal principal, Collection authorities) { + private ChainFollowerClient.EventDetailsResponse eventDetails; + + private String stakeAddress; + + private Role role; + + public JwtAuthenticationToken(JwtPrincipal principal, + Collection authorities) { super(authorities); this.principal = principal; this.setAuthenticated(true); } + public boolean isActionAllowed(Web3Action action) { + var allowedRoles = role.allowedActions(); + + return allowedRoles.contains(action); + } + + public boolean isActionNotAllowed(Web3Action action) { + return !isActionAllowed(action); + } + + @SneakyThrows + public String getStakeAddress() { + return stakeAddress; + } + @Override public Object getDetails() { return principal.getSignedJWT(); diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtFilter.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtFilter.java index edd836433..ab99d6e93 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtFilter.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtFilter.java @@ -7,7 +7,10 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.cardano.foundation.voting.client.ChainFollowerClient; +import org.cardano.foundation.voting.domain.Role; import org.cardano.foundation.voting.service.auth.LoginSystemDetector; +import org.cardano.foundation.voting.utils.Enums; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -22,12 +25,15 @@ import static com.google.common.net.HttpHeaders.AUTHORIZATION; import static org.cardano.foundation.voting.service.auth.LoginSystem.JWT; import static org.zalando.problem.Status.BAD_REQUEST; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; @Component @Slf4j @RequiredArgsConstructor public class JwtFilter extends OncePerRequestFilter { + private final ChainFollowerClient chainFollowerClient; + private final JwtService jwtService; private final LoginSystemDetector loginSystemDetector; @@ -63,9 +69,44 @@ protected void doFilterInternal(HttpServletRequest request, var signedJwt = verificationResultE.get(); try { - var role = signedJwt.getJWTClaimsSet().getStringClaim("role"); + var jwtClaimsSet = signedJwt.getJWTClaimsSet(); + var eventId = jwtClaimsSet.getStringClaim("eventId"); + var stakeAddress = jwtClaimsSet.getStringClaim("stakeAddress"); + var role = Enums.getIfPresent(Role.class, jwtClaimsSet.getStringClaim("role")).orElseThrow(); + var principal = new JwtPrincipal(signedJwt); + var authorities = List.of(new SimpleGrantedAuthority(role.name())); + + var eventDetailsE = chainFollowerClient.getEventDetails(eventId); + if (eventDetailsE.isEmpty()) { + var problem = Problem.builder() + .withTitle("ERROR_GETTING_EVENT_DETAILS") + .withDetail("Unable to get eventDetails details from chain-tip follower service, eventDetails:" + eventId) + .withStatus(INTERNAL_SERVER_ERROR) + .build(); + + sendBackProblem(response, problem); + return; + } + + var maybeEventDetails = eventDetailsE.get(); + if (maybeEventDetails.isEmpty()) { + var problem = Problem.builder() + .withTitle("EVENT_NOT_FOUND") + .withDetail("Event not found, id:" + eventId) + .withStatus(BAD_REQUEST) + .build(); + + sendBackProblem(response, problem); + return; + } + var eventDetails = maybeEventDetails.orElseThrow(); + + var authentication = new JwtAuthenticationToken(principal, authorities) + .eventDetails(eventDetails) + .role(role) + .stakeAddress(stakeAddress) + ; - var authentication = new JwtAuthenticationToken(new JwtPrincipal(signedJwt), List.of(new SimpleGrantedAuthority(role))); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtService.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtService.java index 1bde79050..1c706f433 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtService.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/jwt/JwtService.java @@ -152,6 +152,18 @@ public Either verify(String token) { log.info("Verified sub:{}, stakeAddress:{}, ", sub, jwtStakeAddress); + var maybeRole = Enums.getIfPresent(Role.class, jwtClaimsSet.getStringClaim("role")); + + if (maybeRole.isEmpty()) { + log.warn("Invalid role, role:{}", jwtClaimsSet.getStringClaim("role")); + + return Either.left(Problem.builder() + .withTitle("INVALID_ROLE") + .withDetail("Invalid role, supported roles:" + Role.supportedRoles()) + .withStatus(BAD_REQUEST) + .build()); + } + return Either.right(signedJWT); } diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/DefaultVoteService.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/DefaultVoteService.java index 5fe6cf8dc..f55b4a125 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/DefaultVoteService.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/DefaultVoteService.java @@ -1,13 +1,12 @@ package org.cardano.foundation.voting.service.vote; -import com.nimbusds.jwt.SignedJWT; import io.micrometer.core.annotation.Timed; import io.vavr.control.Either; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cardano.foundation.voting.client.ChainFollowerClient; import org.cardano.foundation.voting.client.UserVerificationClient; -import org.cardano.foundation.voting.domain.Role; +import org.cardano.foundation.voting.domain.CategoryProposalPair; import org.cardano.foundation.voting.domain.VoteReceipt; import org.cardano.foundation.voting.domain.entity.Vote; import org.cardano.foundation.voting.domain.entity.VoteMerkleProof; @@ -17,14 +16,12 @@ import org.cardano.foundation.voting.service.json.JsonService; import org.cardano.foundation.voting.service.merkle_tree.MerkleProofSerdeService; import org.cardano.foundation.voting.service.merkle_tree.VoteMerkleProofService; -import org.cardano.foundation.voting.utils.Enums; import org.cardano.foundation.voting.utils.MoreUUID; import org.cardanofoundation.merkle.ProofItem; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.zalando.problem.Problem; -import java.text.ParseException; import java.util.List; import java.util.Optional; @@ -56,109 +53,70 @@ public class DefaultVoteService implements VoteService { @Override @Transactional(readOnly = true) - public List findAllCompactVotesByEventId(String eventId) { - return voteRepository.findAllCompactVotesByEventId(eventId); - } - - @Transactional(readOnly = true) - @Timed(value = "service.vote.isVoteCastingStillPossible", histogram = true) - public Either isVoteCastingStillPossible(JwtAuthenticationToken jwtAuth, String eventId, String voteId) { - try { - log.info("JWT: {}", jwtAuth); - - var signedJWT = (SignedJWT) jwtAuth.getDetails(); - - var jwtClaimsSet = signedJWT.getJWTClaimsSet(); - - var maybeRole = Enums.getIfPresent(Role.class, jwtClaimsSet.getStringClaim("role")); - - if (maybeRole.isEmpty()) { - return Either.left(Problem.builder() - .withTitle("UNKNOWN_ROLE") - .withDetail("Unknown role") - .withStatus(BAD_REQUEST) - .build()); - } - - var role = maybeRole.get(); - log.info("Role: {}", role); + @Timed(value = "service.vote.getVotedOn", histogram = true) + public Either> getVotedOn(JwtAuthenticationToken auth) { + var jwtEventId = auth.eventDetails().id(); + var jwtStakeAddress = auth.getStakeAddress(); - var jwtEventId = jwtClaimsSet.getStringClaim("eventId"); - var jwtStakeAddress = jwtClaimsSet.getStringClaim("stakeAddress"); - - if (!jwtEventId.equals(eventId)) { - return Either.left(Problem.builder() - .withTitle("EVENT_ID_MISMATCH") - .withDetail("Requested event id mismatch, JWT has no permission for this event.") - .withStatus(BAD_REQUEST) - .build()); - } - - var allowedRoles = role.allowedActions(); - - if (!allowedRoles.contains(IS_VOTE_CASTING_ALLOWED)) { - return Either.left(Problem.builder() - .withTitle("ACTION_NOT_ALLOWED") - .withDetail("Action IS_VOTE_CASTING_ALLOWED not allowed for the role:" + role.name()) - .withStatus(BAD_REQUEST) - .build()); - } - var eventDetailsE = chainFollowerClient.getEventDetails(eventId); - if (eventDetailsE.isEmpty()) { - return Either.left(Problem.builder() - .withTitle("ERROR_GETTING_EVENT_DETAILS") - .withDetail("Unable to get event details from chain-tip follower service, event:" + eventId) - .withStatus(INTERNAL_SERVER_ERROR) - .build() - ); - } + if (auth.isActionNotAllowed(VOTED_ON)) { + return Either.left(Problem.builder() + .withTitle("ACTION_NOT_ALLOWED") + .withDetail("Action VOTED_ON not allowed for the role:" + auth.role().name()) + .withStatus(BAD_REQUEST) + .build()); + } - var maybeEventDetails = eventDetailsE.get(); - if (maybeEventDetails.isEmpty()) { - return Either.left(Problem.builder() - .withTitle("EVENT_NOT_FOUND") - .withDetail("Event not found, id:" + eventId) - .withStatus(BAD_REQUEST) - .build()); - } - var event = maybeEventDetails.orElseThrow(); + var votedOn = voteRepository.getVotedOn(jwtEventId, jwtStakeAddress).stream() + .map(r -> new CategoryProposalPair(r.getCategoryId(), r.getCategoryId())).toList(); - if (event.isEventInactive()) { - return Either.right(false); - } + return Either.right(votedOn); + } - var maybeExistingVote = voteRepository.findById(voteId); - if (maybeExistingVote.isEmpty()) { - return Either.left(Problem.builder() - .withTitle("VOTE_NOT_FOUND") - .withDetail("Vote not found, voteId:" + voteId) - .withStatus(BAD_REQUEST) - .build() - ); - } + @Override + @Transactional(readOnly = true) + @Timed(value = "service.vote.findAllCompactVotesByEventId", histogram = true) + public List findAllCompactVotesByEventId(String eventId) { + return voteRepository.findAllCompactVotesByEventId(eventId); + } - var maybeExistingProof = voteMerkleProofService.findLatestProof(eventId, voteId); - if (maybeExistingProof.isPresent()) { - return Either.left(Problem.builder() - .withTitle("VOTE_CANNOT_BE_CHANGED") - .withDetail("Vote cannot be changed, voteId:" + voteId) - .withStatus(BAD_REQUEST) - .build() - ); - } + @Transactional(readOnly = true) + @Timed(value = "service.vote.isVoteChangingPossible", histogram = true) + public Either isVoteChangingPossible(String voteId, + JwtAuthenticationToken auth) { + var jwtEventId = auth.eventDetails().id(); - return Either.right(true); - } catch (ParseException e) { - log.warn("JWT parse exception", e); + if (auth.isActionNotAllowed(IS_VOTE_CHANGING_ALLOWED)) { + return Either.left(Problem.builder() + .withTitle("ACTION_NOT_ALLOWED") + .withDetail("Action IS_VOTE_CASTING_ALLOWED not allowed for the role:" + auth.role().name()) + .withStatus(BAD_REQUEST) + .build()); + } + if (auth.eventDetails().isEventInactive()) { + return Either.right(false); + } - var problem = Problem.builder() - .withTitle("JWT_PARSE_EXCEPTION") - .withDetail("JWT processing exception") + var maybeExistingVote = voteRepository.findById(voteId); + if (maybeExistingVote.isEmpty()) { + return Either.left(Problem.builder() + .withTitle("VOTE_NOT_FOUND") + .withDetail("Vote not found, voteId:" + voteId) .withStatus(BAD_REQUEST) - .build(); + .build() + ); + } - return Either.left(problem); + var maybeExistingProof = voteMerkleProofService.findLatestProof(jwtEventId, voteId); + if (maybeExistingProof.isPresent()) { + return Either.left(Problem.builder() + .withTitle("VOTE_CANNOT_BE_CHANGED") + .withDetail("Vote cannot be changed, voteId:" + voteId) + .withStatus(BAD_REQUEST) + .build() + ); } + + return Either.right(true); } @Override @@ -478,83 +436,20 @@ public Either voteReceipt(Web3AuthenticationToken web3Auth } @Override - public Either voteReceipt(JwtAuthenticationToken jwtAuth, - String eventId, - String categoryId) { - try { - log.info("JWT: {}", jwtAuth); - - var signedJWT = (SignedJWT) jwtAuth.getDetails(); - - var jwtClaimsSet = signedJWT.getJWTClaimsSet(); - - var maybeRole = Enums.getIfPresent(Role.class, jwtClaimsSet.getStringClaim("role")); - - if (maybeRole.isEmpty()) { - return Either.left(Problem.builder() - .withTitle("UNKNOWN_ROLE") - .withDetail("Unknown role") - .withStatus(BAD_REQUEST) - .build()); - } - - var role = maybeRole.get(); - log.info("Role: {}", role); - - var jwtEventId = jwtClaimsSet.getStringClaim("eventId"); - var jwtStakeAddress = jwtClaimsSet.getStringClaim("stakeAddress"); - - if (!jwtEventId.equals(eventId)) { - return Either.left(Problem.builder() - .withTitle("EVENT_ID_MISMATCH") - .withDetail("Requested event id mismatch, JWT has no permission for this event.") - .withStatus(BAD_REQUEST) - .build()); - } - - var allowedRoles = role.allowedActions(); - - if (!allowedRoles.contains(VIEW_VOTE_RECEIPT)) { - return Either.left(Problem.builder() - .withTitle("ACTION_NOT_ALLOWED") - .withDetail("Action VIEW_VOTE_RECEIPT not allowed for the role:" + role.name()) - .withStatus(BAD_REQUEST) - .build()); - } - - var eventDetailsE = chainFollowerClient.getEventDetails(eventId); - if (eventDetailsE.isEmpty()) { - return Either.left(Problem.builder() - .withTitle("ERROR_GETTING_EVENT_DETAILS") - .withDetail("Unable to get event details from chain-tip follower service, event:" + eventId) - .withStatus(INTERNAL_SERVER_ERROR) - .build() - ); - } - var maybeEvent = eventDetailsE.get(); - if (maybeEvent.isEmpty()) { - log.warn("Unrecognised event, id:{}", eventId); - - return Either.left(Problem.builder() - .withTitle("UNRECOGNISED_EVENT") - .withDetail("Unrecognised event, id:" + eventId) - .withStatus(BAD_REQUEST) - .build()); - } - var event = maybeEvent.get(); - - return actualVoteReceipt(event, categoryId, jwtStakeAddress); - } catch (ParseException e) { - log.warn("JWT parse exception", e); + @Transactional(readOnly = true) + public Either voteReceipt(String categoryId, + JwtAuthenticationToken auth) { + var jwtStakeAddress = auth.getStakeAddress(); - var problem = Problem.builder() - .withTitle("JWT_PARSE_EXCEPTION") - .withDetail("JWT processing exception") + if (auth.isActionNotAllowed(VIEW_VOTE_RECEIPT)) { + return Either.left(Problem.builder() + .withTitle("ACTION_NOT_ALLOWED") + .withDetail("Action VIEW_VOTE_RECEIPT not allowed for the role:" + auth.role().name()) .withStatus(BAD_REQUEST) - .build(); - - return Either.left(problem); + .build()); } + + return actualVoteReceipt(auth.eventDetails(), categoryId, jwtStakeAddress); } private Either actualVoteReceipt(ChainFollowerClient.EventDetailsResponse event, diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/VoteService.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/VoteService.java index f0a0cd48e..7c9334ae7 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/VoteService.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/VoteService.java @@ -1,6 +1,7 @@ package org.cardano.foundation.voting.service.vote; import io.vavr.control.Either; +import org.cardano.foundation.voting.domain.CategoryProposalPair; import org.cardano.foundation.voting.domain.VoteReceipt; import org.cardano.foundation.voting.domain.entity.Vote; import org.cardano.foundation.voting.repository.VoteRepository; @@ -14,12 +15,14 @@ public interface VoteService { List findAllCompactVotesByEventId(String eventId); - Either isVoteCastingStillPossible(JwtAuthenticationToken jwtAuth, String eventId, String voteId); + Either> getVotedOn(JwtAuthenticationToken auth); + + Either isVoteChangingPossible(String voteId, JwtAuthenticationToken auth); Either castVote(Web3AuthenticationToken web3AuthenticationToken); Either voteReceipt(Web3AuthenticationToken web3AuthenticationToken); - Either voteReceipt(JwtAuthenticationToken jwtAuthenticationToken, String eventId, String categoryId); + Either voteReceipt(String categoryId, JwtAuthenticationToken auth); }