diff --git a/src/main/java/net/geant/nmaas/portal/api/security/CustomAccessTokenController.java b/src/main/java/net/geant/nmaas/portal/api/security/CustomAccessTokenController.java new file mode 100644 index 0000000000000000000000000000000000000000..66839fd385cb64d9b4750081062a1813a348a2de --- /dev/null +++ b/src/main/java/net/geant/nmaas/portal/api/security/CustomAccessTokenController.java @@ -0,0 +1,52 @@ +package net.geant.nmaas.portal.api.security; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.geant.nmaas.portal.exceptions.ObjectNotFoundException; +import net.geant.nmaas.portal.persistent.entity.AccessToken; +import net.geant.nmaas.portal.persistent.entity.User; +import net.geant.nmaas.portal.service.CustomAccessTokenService; +import net.geant.nmaas.portal.service.UserService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; +import java.util.List; + +@RestController +@RequestMapping("/api/tokens") +@RequiredArgsConstructor +@Log4j2 +public class CustomAccessTokenController { + + private final CustomAccessTokenService accessTokenService; + private final UserService userService; + + @GetMapping() + public List<AccessToken> getAll(Principal principal) { + User user = getUser(principal); + return accessTokenService.getAll(user.getId()); + } + + @PostMapping() + public AccessToken createNewToken(Principal principal, @RequestBody String name) { + User user = getUser(principal); + return accessTokenService.createToken(user.getId(), name); + } + + @PutMapping("/{id}") + public void invalidateToken(@PathVariable Long id) { + accessTokenService.invalidate(id); + } + + private User getUser(Principal principal) { + String principalName = principal.getName(); + return userService.findByUsername(principalName) + .orElseThrow(() -> new ObjectNotFoundException("User not found")); + } +} diff --git a/src/main/java/net/geant/nmaas/portal/persistent/entity/AccessToken.java b/src/main/java/net/geant/nmaas/portal/persistent/entity/AccessToken.java new file mode 100644 index 0000000000000000000000000000000000000000..80c117b2a178e8aee330990a737638c0fdc42960 --- /dev/null +++ b/src/main/java/net/geant/nmaas/portal/persistent/entity/AccessToken.java @@ -0,0 +1,33 @@ +package net.geant.nmaas.portal.persistent.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "access_tokens") +@NoArgsConstructor +@Getter +@Setter +@AllArgsConstructor +public class AccessToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private Long userId; + + private String tokenValue; + + private boolean valid; +} diff --git a/src/main/java/net/geant/nmaas/portal/persistent/repositories/AccessTokenRepository.java b/src/main/java/net/geant/nmaas/portal/persistent/repositories/AccessTokenRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..7655ad0afff9c210f50cbf7c843ce22cf59fa7eb --- /dev/null +++ b/src/main/java/net/geant/nmaas/portal/persistent/repositories/AccessTokenRepository.java @@ -0,0 +1,12 @@ +package net.geant.nmaas.portal.persistent.repositories; + +import net.geant.nmaas.portal.persistent.entity.AccessToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface AccessTokenRepository extends JpaRepository<AccessToken, Long> { + List<AccessToken> findAllByUserId(Long userId); +} diff --git a/src/main/java/net/geant/nmaas/portal/service/CustomAccessTokenService.java b/src/main/java/net/geant/nmaas/portal/service/CustomAccessTokenService.java new file mode 100644 index 0000000000000000000000000000000000000000..96e59e1eb7a6960580be1e8a2d86095363fa7cf2 --- /dev/null +++ b/src/main/java/net/geant/nmaas/portal/service/CustomAccessTokenService.java @@ -0,0 +1,13 @@ +package net.geant.nmaas.portal.service; + +import net.geant.nmaas.portal.persistent.entity.AccessToken; + +import java.util.List; + +public interface CustomAccessTokenService { + + void invalidate(Long id); + AccessToken createToken(Long userId, String name); + List<AccessToken> getAll(Long userId); + +} diff --git a/src/main/java/net/geant/nmaas/portal/service/impl/CustomAccessTokenServiceImpl.java b/src/main/java/net/geant/nmaas/portal/service/impl/CustomAccessTokenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..58bea7e30a7400dfc9ec715effd725f4f1cf8858 --- /dev/null +++ b/src/main/java/net/geant/nmaas/portal/service/impl/CustomAccessTokenServiceImpl.java @@ -0,0 +1,56 @@ +package net.geant.nmaas.portal.service.impl; + +import lombok.RequiredArgsConstructor; +import net.geant.nmaas.portal.exceptions.ObjectNotFoundException; +import net.geant.nmaas.portal.persistent.entity.AccessToken; +import net.geant.nmaas.portal.persistent.repositories.AccessTokenRepository; +import net.geant.nmaas.portal.service.CustomAccessTokenService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class CustomAccessTokenServiceImpl implements CustomAccessTokenService { + + private final AccessTokenRepository accessTokenRepository; + + @Override + public void invalidate(Long id) { + AccessToken token = findToken(id); + token.setValid(false); + accessTokenRepository.save(token); + } + + @Override + public AccessToken createToken(Long userId, String name) { + AccessToken token = createNewToken(userId, name); + return accessTokenRepository.save(token); + } + + @Override + public List<AccessToken> getAll(Long userId) { + return accessTokenRepository.findAllByUserId(userId); + } + + private AccessToken createNewToken(Long userId, String name) { + AccessToken token = new AccessToken(); + token.setName(name); + token.setUserId(userId); + token.setTokenValue(generateToken()); + token.setValid(true); + return token; + } + + private String generateToken() { + // uuid is a placeholder for now + return UUID.randomUUID().toString(); + } + + private AccessToken findToken(Long id) { + return accessTokenRepository + .findById(id) + .orElseThrow(() -> new ObjectNotFoundException("Could not find access token with id: " + id)); + } +} diff --git a/src/main/resources/db/migration/common/V1.7.0_20240920_1430__addedAccessTokens.sql b/src/main/resources/db/migration/common/V1.7.0_20240920_1430__addedAccessTokens.sql new file mode 100644 index 0000000000000000000000000000000000000000..999454960500d4bfcdc76b137adea5ee0929f469 --- /dev/null +++ b/src/main/resources/db/migration/common/V1.7.0_20240920_1430__addedAccessTokens.sql @@ -0,0 +1,8 @@ +create table access_tokens ( + id bigint generated by default as identity, + name varchar(255), + token_value varchar(255), + user_id bigint, + valid boolean not null, + primary key (id) + ); \ No newline at end of file diff --git a/src/test/shell/data/i18n/de.json b/src/test/shell/data/i18n/de.json index b52b0a6004b08b2b98a258cac21fe5180623a4e1..3f309f2c926862104bb0340f28259ea99d40545c 100644 --- a/src/test/shell/data/i18n/de.json +++ b/src/test/shell/data/i18n/de.json @@ -1155,6 +1155,30 @@ } } }, + "TOKENS": { + "HEADER": "Tokens", + "TABLE": { + "ID": "ID", + "NAME": "Name", + "VALUE": "Value", + "VALID": "Valid", + "ACTIONS": "Actions" + }, + "MODAL": { + "HEADER": "Add new access token", + "NAME": "Name", + "BUTTON_ADD": "Submit", + "BUTTON_CANCEL": "Cancel", + "ERROR": { + "NAME_REQUIRED": "Name is required", + "NAME_MINLENGTH": "Name must have at least 1 character", + "NAME_MAXLENGTH": "Name cannot be longer than 16 characters" + } + }, + "BUTTON_INVALIDATE": "Invalidate", + "NO_TOKENS": "No tokens", + "NEW_TOKEN": "Add new token" + }, "JSON_EDIT": { "INVALID_JSON": "Invalid JSON format. Changes made to the content of the wizard will not be persisted." }, diff --git a/src/test/shell/data/i18n/en.json b/src/test/shell/data/i18n/en.json index dbfdaf12bd1c046a4c86d54aed4ac8bf846f5e4d..93077028cf154bffb800cabf2c4f5fb69f59e8ad 100644 --- a/src/test/shell/data/i18n/en.json +++ b/src/test/shell/data/i18n/en.json @@ -1156,6 +1156,30 @@ } } }, + "TOKENS": { + "HEADER": "Tokens", + "TABLE": { + "ID": "ID", + "NAME": "Name", + "VALUE": "Value", + "VALID": "Valid", + "ACTIONS": "Actions" + }, + "MODAL": { + "HEADER": "Add new access token", + "NAME": "Name", + "BUTTON_ADD": "Submit", + "BUTTON_CANCEL": "Cancel", + "ERROR": { + "NAME_REQUIRED": "Name is required", + "NAME_MINLENGTH": "Name must have at least 1 character", + "NAME_MAXLENGTH": "Name cannot be longer than 16 characters" + } + }, + "BUTTON_INVALIDATE": "Invalidate", + "NO_TOKENS": "No tokens", + "NEW_TOKEN": "Add new token" + }, "JSON_EDIT": { "INVALID_JSON": "Invalid JSON format. Changes made to the content of the wizard will not be persisted." }, diff --git a/src/test/shell/data/i18n/fr.json b/src/test/shell/data/i18n/fr.json index 6cc4d288c04ae373748290b6163f06dec9e79b8d..2a7b0829c24d5b9040062136bf67d4990d553d79 100644 --- a/src/test/shell/data/i18n/fr.json +++ b/src/test/shell/data/i18n/fr.json @@ -1156,6 +1156,30 @@ } } }, + "TOKENS": { + "HEADER": "Tokens", + "TABLE": { + "ID": "ID", + "NAME": "Name", + "VALUE": "Value", + "VALID": "Valid", + "ACTIONS": "Actions" + }, + "MODAL": { + "HEADER": "Add new access token", + "NAME": "Name", + "BUTTON_ADD": "Submit", + "BUTTON_CANCEL": "Cancel", + "ERROR": { + "NAME_REQUIRED": "Name is required", + "NAME_MINLENGTH": "Name must have at least 1 character", + "NAME_MAXLENGTH": "Name cannot be longer than 16 characters" + } + }, + "BUTTON_INVALIDATE": "Invalidate", + "NO_TOKENS": "No tokens", + "NEW_TOKEN": "Add new token" + }, "JSON_EDIT": { "INVALID_JSON": "Invalid JSON format. Changes made to the content of the wizard will not be persisted." }, diff --git a/src/test/shell/data/i18n/pl.json b/src/test/shell/data/i18n/pl.json index f6d296d186ce8f8e1f12eb3b4cf4c43281176f6c..582f4fc7dbf733607010587d6212b4f59ee9262b 100644 --- a/src/test/shell/data/i18n/pl.json +++ b/src/test/shell/data/i18n/pl.json @@ -1156,6 +1156,30 @@ } } }, + "TOKENS": { + "HEADER": "Tokens", + "TABLE": { + "ID": "ID", + "NAME": "Name", + "VALUE": "Value", + "VALID": "Valid", + "ACTIONS": "Actions" + }, + "MODAL": { + "HEADER": "Add new access token", + "NAME": "Name", + "BUTTON_ADD": "Submit", + "BUTTON_CANCEL": "Cancel", + "ERROR": { + "NAME_REQUIRED": "Name is required", + "NAME_MINLENGTH": "Name must have at least 1 character", + "NAME_MAXLENGTH": "Name cannot be longer than 16 characters" + } + }, + "BUTTON_INVALIDATE": "Invalidate", + "NO_TOKENS": "No tokens", + "NEW_TOKEN": "Add new token" + }, "JSON_EDIT": { "INVALID_JSON": "Niewłaściwy format JSON. Zmiany wprowadzone do zawartości formularza nie zostaną zapisane." },