diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ApiKeyController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ApiKeyController.java index 71e330042..bdfe0c2c4 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ApiKeyController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ApiKeyController.java @@ -5,6 +5,7 @@ import it.chalmers.gamma.app.apikey.ApiKeyFacade; import it.chalmers.gamma.app.apikey.ApiKeySettingsFacade; import it.chalmers.gamma.app.supergroup.SuperGroupFacade; +import jakarta.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -167,6 +168,9 @@ public ModelAndView createApiKey( @DeleteMapping("/api-keys/{id}") public ModelAndView deleteApiKey( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, + @RequestHeader(value = "return-empty-on-delete", required = false) + boolean returnEmptyOnDelete, + HttpServletResponse response, @PathVariable("id") UUID id) { try { this.apiKeyFacade.delete(id); @@ -174,7 +178,14 @@ public ModelAndView deleteApiKey( throw new RuntimeException(e); } - return new ModelAndView("redirect:/api-keys"); + if (returnEmptyOnDelete) { + response.addHeader("HX-Reswap", "delete"); + response.addHeader("HX-Retarget", "closest article"); + } else { + response.addHeader("HX-Redirect", "/api-keys"); + } + + return new ModelAndView("common/empty"); } public static final class ApiKeySettings { diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ClientsController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ClientsController.java index 5374d5134..bfad21a73 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ClientsController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/ClientsController.java @@ -8,6 +8,9 @@ import it.chalmers.gamma.app.client.domain.authority.ClientAuthorityRepository; import it.chalmers.gamma.app.supergroup.SuperGroupFacade; import it.chalmers.gamma.app.user.UserFacade; +import it.chalmers.gamma.security.authentication.AuthenticationExtractor; +import it.chalmers.gamma.security.authentication.UserAuthentication; +import jakarta.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -62,7 +65,7 @@ public ModelAndView getClients( } @GetMapping("/clients/{id}") - public ModelAndView getClients( + public ModelAndView getClient( @RequestHeader(value = "HX-Request", required = false) boolean htmxRequest, @PathVariable("id") String clientUid) { if (!isValidUUID(clientUid)) { @@ -93,6 +96,17 @@ public ModelAndView getClients( mv.addObject("clientAuthorities", clientAuthorities); mv.addObject("userApprovals", userApprovals); + if (client.get().owner() instanceof ClientFacade.ClientDTO.UserOwner userOwner) { + if (AuthenticationExtractor.getAuthentication() instanceof UserAuthentication userPrincipal) { + boolean isOwner = userOwner.user().id().equals(userPrincipal.gammaUser().id().value()); + mv.addObject("amIOwner", isOwner); + + if (!isOwner) { + mv.addObject("owner", userOwner.user()); + } + } + } + return mv; } @@ -237,10 +251,11 @@ public ModelAndView newRestrictionRow( public ModelAndView createClient( @RequestHeader(value = "HX-Request", required = true) boolean htmxRequest, CreateClient form, - BindingResult bindingResult) { + BindingResult bindingResult, + HttpServletResponse response) { ModelAndView mv = new ModelAndView(); - ClientFacade.ClientAndApiKeySecrets secrets = + ClientFacade.CreatedClientDTO result = this.clientFacade.createOfficialClient( new ClientFacade.NewClient( form.redirectUrl, @@ -251,11 +266,16 @@ public ModelAndView createClient( form.emailScope, new ClientFacade.NewClientRestrictions(form.restrictions))); - mv.setViewName("pages/client-credentials"); - mv.addObject("clientUid", secrets.clientUid()); - mv.addObject("clientSecret", secrets.clientSecret()); - mv.addObject("apiKeyToken", secrets.apiKeyToken()); - mv.addObject("name", form.prettyName); + mv.setViewName("pages/client-details"); + + mv.addObject("clientUid", result.client().clientUid()); + mv.addObject("client", result.client()); + mv.addObject("clientAuthorities", new ArrayList<>()); + mv.addObject("userApprovals", new ArrayList<>()); + mv.addObject("clientSecret", result.clientSecret()); + mv.addObject("apiKeyToken", result.apiKeyToken()); + + response.addHeader("HX-Push-Url", "/clients/" + result.client().clientUid().toString()); return mv; } @@ -421,6 +441,7 @@ public ModelAndView createAuthority( @DeleteMapping("/clients/{id}") public ModelAndView deleteClient( @RequestHeader(value = "HX-Request", required = true) boolean htmxRequest, + @RequestHeader(value = "owner", required = false) boolean wasOwner, @PathVariable("id") UUID clientUid) { try { this.clientFacade.delete(clientUid); @@ -428,7 +449,11 @@ public ModelAndView deleteClient( throw new RuntimeException(e); } - return new ModelAndView("redirect:/clients"); + if (wasOwner) { + return new ModelAndView("redirect:/my-clients"); + } else { + return new ModelAndView("redirect:/clients"); + } } @DeleteMapping("/clients/{id}/authority/{name}") diff --git a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/MyClientsController.java b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/MyClientsController.java index 0b505f84c..022499223 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/primary/web/MyClientsController.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/primary/web/MyClientsController.java @@ -1,6 +1,8 @@ package it.chalmers.gamma.adapter.primary.web; import it.chalmers.gamma.app.client.ClientFacade; +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; @@ -131,10 +133,11 @@ public ModelAndView getCreateUserClient( public ModelAndView createUserClient( @RequestHeader(value = "HX-Request", required = true) boolean htmxRequest, CreateUserClient form, - BindingResult bindingResult) { + BindingResult bindingResult, + HttpServletResponse response) { ModelAndView mv = new ModelAndView(); - ClientFacade.ClientAndApiKeySecrets secrets = + ClientFacade.CreatedClientDTO result = this.clientFacade.createUserClient( new ClientFacade.NewClient( form.redirectUrl, @@ -145,11 +148,16 @@ public ModelAndView createUserClient( form.emailScope, null)); - mv.setViewName("pages/client-credentials"); - mv.addObject("clientUid", secrets.clientUid()); - mv.addObject("clientSecret", secrets.clientSecret()); - mv.addObject("apiKeyToken", secrets.apiKeyToken()); - mv.addObject("name", form.prettyName); + mv.setViewName("pages/client-details"); + + mv.addObject("clientUid", result.client().clientUid()); + mv.addObject("client", result.client()); + mv.addObject("clientAuthorities", new ArrayList<>()); + mv.addObject("userApprovals", new ArrayList<>()); + mv.addObject("clientSecret", result.clientSecret()); + mv.addObject("apiKeyToken", result.apiKeyToken()); + + response.addHeader("HX-Push-Url", "/clients/" + result.client().clientUid().toString()); return mv; } diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyRepositoryAdapter.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyRepositoryAdapter.java index 6e7a7b009..71c2bf2c5 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyRepositoryAdapter.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyRepositoryAdapter.java @@ -5,7 +5,6 @@ import it.chalmers.gamma.app.apikey.domain.ApiKey; import it.chalmers.gamma.app.apikey.domain.ApiKeyId; import it.chalmers.gamma.app.apikey.domain.ApiKeyRepository; -import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; import jakarta.persistence.EntityExistsException; import jakarta.transaction.Transactional; import java.util.List; @@ -75,14 +74,6 @@ public Optional getById(ApiKeyId apiKeyId) { return this.repository.findById(apiKeyId.value()).map(this.apiKeyEntityConverter::toDomain); } - @Override - public Optional getByToken(ApiKeyToken apiKeyToken) { - System.out.println(apiKeyToken.value()); - return this.repository - .findByToken(apiKeyToken.value()) - .map(this.apiKeyEntityConverter::toDomain); - } - private ApiKeyEntity toEntity(ApiKey apiKey) { ApiKeyEntity apiKeyEntity = new ApiKeyEntity(); diff --git a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientRepositoryAdapter.java b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientRepositoryAdapter.java index 8d155eae3..f3d290885 100644 --- a/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientRepositoryAdapter.java +++ b/app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/client/ClientRepositoryAdapter.java @@ -7,6 +7,7 @@ import it.chalmers.gamma.adapter.secondary.jpa.user.UserJpaRepository; import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorHelper; import it.chalmers.gamma.adapter.secondary.jpa.util.PersistenceErrorState; +import it.chalmers.gamma.app.apikey.domain.ApiKeyId; import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; import it.chalmers.gamma.app.client.domain.Client; import it.chalmers.gamma.app.client.domain.ClientId; @@ -143,4 +144,12 @@ public Optional getByApiKey(ApiKeyToken apiKeyToken) { .map(ClientApiKeyEntity::getClient) .map(this.clientEntityConverter::toDomain); } + + @Override + public Optional getByApiKey(ApiKeyId apiKeyId) { + return this.clientApiKeyJpaRepository + .findById(apiKeyId.value()) + .map(ClientApiKeyEntity::getClient) + .map(this.clientEntityConverter::toDomain); + } } diff --git a/app/src/main/java/it/chalmers/gamma/app/apikey/ApiKeyFacade.java b/app/src/main/java/it/chalmers/gamma/app/apikey/ApiKeyFacade.java index ab1a3a7a8..8e116e25d 100644 --- a/app/src/main/java/it/chalmers/gamma/app/apikey/ApiKeyFacade.java +++ b/app/src/main/java/it/chalmers/gamma/app/apikey/ApiKeyFacade.java @@ -1,7 +1,6 @@ package it.chalmers.gamma.app.apikey; -import static it.chalmers.gamma.app.authentication.AccessGuard.isAdmin; -import static it.chalmers.gamma.app.authentication.AccessGuard.isLocalRunner; +import static it.chalmers.gamma.app.authentication.AccessGuard.*; import it.chalmers.gamma.app.Facade; import it.chalmers.gamma.app.apikey.domain.*; @@ -90,9 +89,11 @@ public void delete(UUID apiKeyId) throws ApiKeyNotFoundException { } public Optional getById(UUID apiKeyId) { - this.accessGuard.require(isAdmin()); + ApiKeyId id = new ApiKeyId(apiKeyId); + + this.accessGuard.requireEither(isAdmin(), ownerOfClientApi(id)); - return this.apiKeyRepository.getById(new ApiKeyId(apiKeyId)).map(ApiKeyDTO::new); + return this.apiKeyRepository.getById(id).map(ApiKeyDTO::new); } public List getAll() { diff --git a/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyRepository.java b/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyRepository.java index ed88bf527..159792f48 100644 --- a/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyRepository.java +++ b/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyRepository.java @@ -13,8 +13,6 @@ public interface ApiKeyRepository { Optional getById(ApiKeyId apiKeyId); - Optional getByToken(ApiKeyToken apiKeyToken); - class ApiKeyNotFoundException extends Exception {} /** diff --git a/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java b/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java index b6725cd39..3c83a2449 100644 --- a/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java +++ b/app/src/main/java/it/chalmers/gamma/app/apikey/domain/ApiKeyToken.java @@ -22,7 +22,7 @@ public record GeneratedApiKeyToken(ApiKeyToken apiKeyToken, String rawToken) {} public static GeneratedApiKeyToken generate(PasswordEncoder passwordEncoder) { String value = TokenUtils.generateToken( - 50, + 32, TokenUtils.CharacterTypes.LOWERCASE, TokenUtils.CharacterTypes.UPPERCASE, TokenUtils.CharacterTypes.NUMBERS); diff --git a/app/src/main/java/it/chalmers/gamma/app/authentication/AccessGuard.java b/app/src/main/java/it/chalmers/gamma/app/authentication/AccessGuard.java index aa0762b4d..521d80025 100644 --- a/app/src/main/java/it/chalmers/gamma/app/authentication/AccessGuard.java +++ b/app/src/main/java/it/chalmers/gamma/app/authentication/AccessGuard.java @@ -136,6 +136,24 @@ public static AccessChecker ownerOfClient(ClientUid clientUid) { }; } + public static AccessChecker ownerOfClientApi(ApiKeyId apiKeyId) { + return (clientRepository, userRepository) -> { + if (AuthenticationExtractor.getAuthentication() instanceof UserAuthentication userPrincipal) { + Optional client = clientRepository.getByApiKey(apiKeyId); + + if (client.isPresent()) { + ClientOwner clientOwner = client.get().owner(); + + if (clientOwner instanceof ClientUserOwner(UserId userId)) { + return userId.equals(userPrincipal.gammaUser().id()); + } + } + } + + return false; + }; + } + /** Such as Bootstrap */ public static AccessChecker isLocalRunner() { return (clientRepository, userRepository) -> diff --git a/app/src/main/java/it/chalmers/gamma/app/client/ClientFacade.java b/app/src/main/java/it/chalmers/gamma/app/client/ClientFacade.java index 05d3ae4ed..0887612ad 100644 --- a/app/src/main/java/it/chalmers/gamma/app/client/ClientFacade.java +++ b/app/src/main/java/it/chalmers/gamma/app/client/ClientFacade.java @@ -3,6 +3,7 @@ import static it.chalmers.gamma.app.authentication.AccessGuard.*; import it.chalmers.gamma.app.Facade; +import it.chalmers.gamma.app.apikey.ApiKeyFacade; import it.chalmers.gamma.app.apikey.domain.ApiKey; import it.chalmers.gamma.app.apikey.domain.ApiKeyId; import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; @@ -51,14 +52,14 @@ public ClientFacade( } @Transactional - public ClientAndApiKeySecrets createOfficialClient(NewClient newClient) { + public CreatedClientDTO createOfficialClient(NewClient newClient) { this.accessGuard.require(isAdmin()); return this.create(newClient, new ClientOwnerOfficial()); } @Transactional - public ClientAndApiKeySecrets createUserClient(NewClient newClient) { + public CreatedClientDTO createUserClient(NewClient newClient) { this.accessGuard.require(isSignedIn()); if (newClient.restrictions != null) { @@ -73,7 +74,7 @@ public ClientAndApiKeySecrets createUserClient(NewClient newClient) { throw new IllegalStateException(); } - private ClientAndApiKeySecrets create(NewClient newClient, ClientOwner clientOwner) { + private CreatedClientDTO create(NewClient newClient, ClientOwner clientOwner) { ClientSecret.GeneratedClientSecret generatedClientSecret = ClientSecret.generate(passwordEncoder); ApiKey apiKey = null; @@ -131,9 +132,8 @@ private ClientAndApiKeySecrets create(NewClient newClient, ClientOwner clientOwn this.clientRepository.save(client); - return new ClientAndApiKeySecrets( - clientUid.value(), - clientId.value(), + return new CreatedClientDTO( + createDTO(client), generatedClientSecret.rawSecret(), generatedApiKeyToken == null ? null : generatedApiKeyToken.rawToken()); } @@ -215,8 +215,7 @@ public record NewClient( boolean emailScope, NewClientRestrictions restrictions) {} - public record ClientAndApiKeySecrets( - UUID clientUid, String clientId, String clientSecret, String apiKeyToken) {} + public record CreatedClientDTO(ClientDTO client, String clientSecret, String apiKeyToken) {} private ClientDTO createDTO(Client client) { return new ClientDTO( @@ -226,7 +225,7 @@ private ClientDTO createDTO(Client client) { client.prettyName().value(), client.description().sv().value(), client.description().en().value(), - client.clientApiKey().isPresent(), + client.clientApiKey().map(ApiKeyFacade.ApiKeyDTO::new), client .restrictions() .map( @@ -255,7 +254,7 @@ public record ClientDTO( String prettyName, String svDescription, String enDescription, - boolean hasApiKey, + Optional apiKey, ClientRestrictionDTO restriction, Owner owner) { diff --git a/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientRepository.java b/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientRepository.java index 1543babf5..492e954e0 100644 --- a/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientRepository.java +++ b/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientRepository.java @@ -1,5 +1,6 @@ package it.chalmers.gamma.app.client.domain; +import it.chalmers.gamma.app.apikey.domain.ApiKeyId; import it.chalmers.gamma.app.apikey.domain.ApiKeyToken; import it.chalmers.gamma.app.user.domain.UserId; import java.util.List; @@ -32,6 +33,8 @@ void save(Client client) Optional getByApiKey(ApiKeyToken apiKeyToken); + Optional getByApiKey(ApiKeyId apiKeyId); + class ClientNotFoundException extends Exception {} class ClientIdAlreadyExistsRuntimeException extends RuntimeException {} diff --git a/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java b/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java index af194d98b..b7794e753 100644 --- a/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java +++ b/app/src/main/java/it/chalmers/gamma/app/client/domain/ClientSecret.java @@ -22,7 +22,7 @@ public record GeneratedClientSecret(ClientSecret clientSecret, String rawSecret) public static GeneratedClientSecret generate(PasswordEncoder passwordEncoder) { String value = TokenUtils.generateToken( - 128, + 32, TokenUtils.CharacterTypes.LOWERCASE, TokenUtils.CharacterTypes.UPPERCASE, TokenUtils.CharacterTypes.NUMBERS); diff --git a/app/src/main/resources/static/css/main.css b/app/src/main/resources/static/css/main.css index 08bc0db66..070269d7a 100644 --- a/app/src/main/resources/static/css/main.css +++ b/app/src/main/resources/static/css/main.css @@ -88,6 +88,23 @@ ul.tuple > li { gap: 0.5rem; } +div.tuple-right-expand > div { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +div.tuple-right-expand > div > *:nth-child(1) { + text-align: right; + flex: 1; +} + +div.tuple-right-expand > div > *:nth-child(2) { + text-align: left; + flex: 10; + max-width: max-content; +} + ul.tuple > li > * { flex: 1; } @@ -241,4 +258,14 @@ main { [data-loading] { display: none; +} + +code { + overflow-x: scroll; + white-space: nowrap; + max-width: 100%; +} + +.contents { + display: contents; } \ No newline at end of file diff --git a/app/src/main/resources/templates/pages/api-key-credentials.html b/app/src/main/resources/templates/pages/api-key-credentials.html deleted file mode 100644 index 67d2ff731..000000000 --- a/app/src/main/resources/templates/pages/api-key-credentials.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
-
-
-
-

These are the credentials for your api key. They will not be shown again.

-
    -
  • - Api key - -
  • -
- -
-
\ No newline at end of file diff --git a/app/src/main/resources/templates/pages/api-key-details.html b/app/src/main/resources/templates/pages/api-key-details.html index 17d53e28f..eff6fee19 100644 --- a/app/src/main/resources/templates/pages/api-key-details.html +++ b/app/src/main/resources/templates/pages/api-key-details.html @@ -2,8 +2,9 @@
-
-
+
+
+ Api key details
  • @@ -24,7 +25,9 @@
-
+ + +
diff --git a/app/src/main/resources/templates/pages/client-credentials.html b/app/src/main/resources/templates/pages/client-credentials.html deleted file mode 100644 index b1d79bc47..000000000 --- a/app/src/main/resources/templates/pages/client-credentials.html +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
-
-

These are the credentials for your client. They will not be shown again.

-
    -
  • - Client secret: - -
  • -
  • - Api key - -
  • -
- -
-
\ No newline at end of file diff --git a/app/src/main/resources/templates/pages/client-details.html b/app/src/main/resources/templates/pages/client-details.html index 3974c7a66..aa1a74241 100644 --- a/app/src/main/resources/templates/pages/client-details.html +++ b/app/src/main/resources/templates/pages/client-details.html @@ -1,6 +1,10 @@
-
+
+ +
+
+
Client details @@ -22,10 +26,6 @@ Client id: -
  • - Has Api Key: - -
  • Redirect: @@ -34,6 +34,12 @@ Super group restrictions:
  • +
  • + Owned by: + + + +
  • @@ -42,6 +48,12 @@
    + +
    +
    +
    +
    +
    diff --git a/app/src/main/resources/templates/partial/api-key-credentials.html b/app/src/main/resources/templates/partial/api-key-credentials.html new file mode 100644 index 000000000..9818043a3 --- /dev/null +++ b/app/src/main/resources/templates/partial/api-key-credentials.html @@ -0,0 +1,14 @@ +
    +
    +
    +

    These are the credentials for your api key. They will not be shown again.

    +
      +
    • + Api key + +
    • +
    + +
    \ No newline at end of file diff --git a/app/src/main/resources/templates/partial/client-credentials.html b/app/src/main/resources/templates/partial/client-credentials.html new file mode 100644 index 000000000..7d7cc16ec --- /dev/null +++ b/app/src/main/resources/templates/partial/client-credentials.html @@ -0,0 +1,27 @@ +
    +
    + Credentials +
    +

    These are the credentials for your client. They will not be shown again.

    +
    +
    + Client secret: + +
    +
    + Api key: + +
    +
    + +

    + Read more here about how to use Client API here:
    github.com/cthit/Gamma/wiki/Client-API +

    +
    + To authorize when doing API requests, simply add this header: +
    +

    + Authorization: pre-shared : +

    +
    +
    \ No newline at end of file