Skip to content

Commit

Permalink
Merge pull request #4 from helioauth/feature/openapi-user-api
Browse files Browse the repository at this point in the history
Refactored user controller to implement OpenAPI spec
  • Loading branch information
vstanchev authored Dec 5, 2024
2 parents 6b6f04a + 093c574 commit b02c83a
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 70 deletions.
6 changes: 1 addition & 5 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,4 @@ jobs:
distribution: 'temurin'
cache: maven
- name: Verify with Maven
run: mvn -B -DskipTests verify --file pom.xml

# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
run: mvn -B -DskipTests verify --file pom.xml
1 change: 1 addition & 0 deletions docs/openapi/components/schemas/SignInStartResponse.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ properties:
options:
type: string
description: Options for assertion to pass to `navigator.credentials.create()`
x-field-extra-annotation: '@com.fasterxml.jackson.annotation.JsonRawValue'
1 change: 1 addition & 0 deletions docs/openapi/components/schemas/SignUpStartResponse.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ properties:
options:
type: string
description: Options to pass to `navigator.credentials.create()`
x-field-extra-annotation: '@com.fasterxml.jackson.annotation.JsonRawValue'
2 changes: 1 addition & 1 deletion docs/openapi/paths/v1_credentials_add_start.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ post:
content:
application/json:
schema:
type: string
$ref: ../components/schemas/SignUpStartRequest.yaml
required: true
responses:
'200':
Expand Down
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@
<useSpringController>true</useSpringController>
<useSpringfox>false</useSpringfox>
<useSpringBoot3>true</useSpringBoot3>
<generatedConstructorWithRequiredArgs>true</generatedConstructorWithRequiredArgs>
<generateConstructorWithAllArgs>true</generateConstructorWithAllArgs>
</configOptions>
<modelPackage>com.helioauth.passkeys.api.generated.models</modelPackage>
<apiPackage>com.helioauth.passkeys.api.generated.api</apiPackage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,12 @@
import com.helioauth.passkeys.api.generated.models.AddApplicationRequest;
import com.helioauth.passkeys.api.generated.models.Application;
import com.helioauth.passkeys.api.generated.models.ApplicationApiKey;
import com.helioauth.passkeys.api.mapper.ClientApplicationMapper;
import com.helioauth.passkeys.api.service.ClientApplicationService;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.net.URI;
Expand All @@ -42,58 +36,42 @@
* @author Viktor Stanchev
*/
@RestController
@RequestMapping("/admin/v1/apps")
@RequiredArgsConstructor
public class ClientApplicationController implements ApplicationsApi {

private final ClientApplicationService clientApplicationService;

private final ClientApplicationMapper clientApplicationMapper;

@GetMapping
@Override
public ResponseEntity<List<Application>> listAll() {
return ResponseEntity.ok(clientApplicationService.listAll());
}

@GetMapping("/{id}")
@Override
public ResponseEntity<Application> get(@PathVariable UUID id) {
return clientApplicationService.get(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

@GetMapping("/{id}/api-key")
@Override
public ResponseEntity<ApplicationApiKey> getApiKey(@PathVariable UUID id) {
return clientApplicationService.getApiKey(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

@PostMapping
@Override
public ResponseEntity<Application> add(@RequestBody AddApplicationRequest request) {
Application created = clientApplicationService.add(request.getName());

return ResponseEntity.created(URI.create("/admin/v1/apps/" + created.getId()))
.body(created);
}

@PutMapping("/{id}")
@Override
public ResponseEntity<Application> edit(@PathVariable UUID id, @RequestBody String name) {

val updated = clientApplicationService.edit(id, name);

return updated
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

@DeleteMapping("/{id}")
@Override
public ResponseEntity<Void> delete(@PathVariable UUID id) {
boolean deleted = clientApplicationService.delete(id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,42 @@
package com.helioauth.passkeys.api.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.helioauth.passkeys.api.contract.*;
import com.helioauth.passkeys.api.service.UserCredentialManager;
import com.helioauth.passkeys.api.contract.SignInFinishRequest;
import com.helioauth.passkeys.api.contract.SignInFinishResponse;
import com.helioauth.passkeys.api.contract.SignInStartRequest;
import com.helioauth.passkeys.api.contract.SignInStartResponse;
import com.helioauth.passkeys.api.contract.SignUpFinishRequest;
import com.helioauth.passkeys.api.contract.SignUpFinishResponse;
import com.helioauth.passkeys.api.contract.SignUpStartRequest;
import com.helioauth.passkeys.api.contract.SignUpStartResponse;
import com.helioauth.passkeys.api.service.UserSignInService;
import com.helioauth.passkeys.api.service.UserSignupService;

import com.helioauth.passkeys.api.service.exception.SignInFailedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import jakarta.validation.Valid;

/**
* @author Viktor Stanchev
*/
@Slf4j
@RestController
@RequestMapping("/v1")
@CrossOrigin(origins = "*")
@Slf4j
@RequiredArgsConstructor
public class CredentialsController {

private final UserSignInService userSignInService;
private final UserSignupService userSignupService;
private final UserCredentialManager userCredentialManager;

@PostMapping(value = "/signup/start", produces = MediaType.APPLICATION_JSON_VALUE)
public SignUpStartResponse postSignupStart(@RequestBody @Valid SignUpStartRequest request) {
Expand Down Expand Up @@ -76,14 +84,4 @@ public SignInFinishResponse finishSignInCredential(@RequestBody SignInFinishRequ
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Sign in failed");
}
}

@PostMapping(value = "/credentials/add/start", produces = MediaType.APPLICATION_JSON_VALUE)
public SignUpStartResponse credentialsAddStart(@RequestBody String username) {
return userCredentialManager.createCredential(username);
}

@PostMapping(value = "/credentials/add/finish", produces = MediaType.APPLICATION_JSON_VALUE)
public SignUpFinishResponse credentialsAddFinish(@RequestBody SignUpFinishRequest request) {
return userCredentialManager.finishCreateCredential(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,50 @@

package com.helioauth.passkeys.api.controller;

import com.helioauth.passkeys.api.generated.api.UsersApi;
import com.helioauth.passkeys.api.generated.models.ListPasskeysResponse;
import com.helioauth.passkeys.api.generated.models.SignUpFinishRequest;
import com.helioauth.passkeys.api.generated.models.SignUpFinishResponse;
import com.helioauth.passkeys.api.generated.models.SignUpStartRequest;
import com.helioauth.passkeys.api.generated.models.SignUpStartResponse;
import com.helioauth.passkeys.api.service.UserAccountManager;
import com.helioauth.passkeys.api.service.UserCredentialManager;
import com.helioauth.passkeys.api.service.dto.ListPasskeysResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

/**
* @author Viktor Stanchev
*/
@RestController
@RequestMapping("/v1/users")
@Slf4j
@RestController
@RequiredArgsConstructor
public class UsersController {
public class UsersController implements UsersApi {
private final UserCredentialManager userCredentialManager;
private final UserAccountManager userAccountManager;

@GetMapping("/{uuid}/credentials")
public ListPasskeysResponse getUserCredentials(@PathVariable UUID uuid) {
return userCredentialManager.getUserCredentials(uuid);
public ResponseEntity<ListPasskeysResponse> getUserCredentials(@PathVariable UUID uuid) {
return ResponseEntity.ok(userCredentialManager.getUserCredentials(uuid));
}

@DeleteMapping("/{uuid}")
public void deleteUser(@PathVariable UUID uuid) {
public ResponseEntity<Void> deleteUser(@PathVariable UUID uuid) {
userAccountManager.deleteUser(uuid);
return ResponseEntity.noContent().build();
}

@CrossOrigin(origins = "*")
public ResponseEntity<SignUpStartResponse> credentialsAddStart(@RequestBody SignUpStartRequest request) {
return ResponseEntity.ok(userCredentialManager.createCredential(request.getName()));
}

@CrossOrigin(origins = "*")
public ResponseEntity<SignUpFinishResponse> credentialsAddFinish(@RequestBody SignUpFinishRequest request) {
return ResponseEntity.ok(userCredentialManager.finishCreateCredential(request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
package com.helioauth.passkeys.api.mapper;

import com.helioauth.passkeys.api.domain.UserCredential;
import com.helioauth.passkeys.api.generated.models.PasskeyCredential;
import com.helioauth.passkeys.api.generated.models.SignUpStartResponse;
import com.helioauth.passkeys.api.service.dto.CredentialRegistrationResult;
import com.helioauth.passkeys.api.service.dto.PasskeyItem;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
Expand All @@ -30,11 +31,13 @@
*/
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface UserCredentialMapper {
List<PasskeyItem> toDto(List<UserCredential> userCredentialList);
List<PasskeyCredential> toDto(List<UserCredential> userCredentialList);

@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "lastUsedAt", ignore = true)
@Mapping(target = "user", ignore = true)
@Mapping(target = "id", ignore = true)
UserCredential fromCredentialRegistrationResult(CredentialRegistrationResult registrationResultDto);

com.helioauth.passkeys.api.contract.SignUpStartResponse toLegacySignUpStartResponse(SignUpStartResponse response);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,16 @@
package com.helioauth.passkeys.api.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.helioauth.passkeys.api.contract.SignUpFinishRequest;
import com.helioauth.passkeys.api.contract.SignUpFinishResponse;
import com.helioauth.passkeys.api.contract.SignUpStartResponse;
import com.helioauth.passkeys.api.domain.User;
import com.helioauth.passkeys.api.domain.UserCredential;
import com.helioauth.passkeys.api.domain.UserCredentialRepository;
import com.helioauth.passkeys.api.domain.UserRepository;
import com.helioauth.passkeys.api.generated.models.ListPasskeysResponse;
import com.helioauth.passkeys.api.generated.models.SignUpFinishRequest;
import com.helioauth.passkeys.api.generated.models.SignUpFinishResponse;
import com.helioauth.passkeys.api.generated.models.SignUpStartResponse;
import com.helioauth.passkeys.api.mapper.UserCredentialMapper;
import com.helioauth.passkeys.api.service.dto.CredentialRegistrationResult;
import com.helioauth.passkeys.api.service.dto.ListPasskeysResponse;
import com.helioauth.passkeys.api.service.dto.PasskeyItem;
import com.helioauth.passkeys.api.service.exception.CreateCredentialFailedException;
import com.helioauth.passkeys.api.service.exception.SignUpFailedException;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -62,8 +61,8 @@ public SignUpStartResponse createCredential(String name) {
public SignUpFinishResponse finishCreateCredential(SignUpFinishRequest request) {
try {
CredentialRegistrationResult result = webAuthnAuthenticator.finishRegistration(
request.requestId(),
request.publicKeyCredential()
request.getRequestId(),
request.getPublicKeyCredential()
);

User user = userRepository.findByName(result.name()).orElseThrow(CreateCredentialFailedException::new);
Expand All @@ -72,7 +71,7 @@ public SignUpFinishResponse finishCreateCredential(SignUpFinishRequest request)
userCredential.setUser(user);
userCredentialRepository.save(userCredential);

return new SignUpFinishResponse(request.requestId(), user.getId());
return new SignUpFinishResponse(request.getRequestId(), user.getId());
} catch (IOException e) {
log.error("Register Credential failed", e);
throw new SignUpFailedException();
Expand All @@ -81,8 +80,7 @@ public SignUpFinishResponse finishCreateCredential(SignUpFinishRequest request)

public ListPasskeysResponse getUserCredentials(UUID userUuid) {
List<UserCredential> userCredentials = userCredentialRepository.findAllByUserId(userUuid);
List<PasskeyItem> passkeyItems = userCredentialMapper.toDto(userCredentials);
return new ListPasskeysResponse(passkeyItems);
return new ListPasskeysResponse(userCredentialMapper.toDto(userCredentials));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public SignUpStartResponse startRegistration(String name) {
});

try {
return webAuthnAuthenticator.startRegistration(name);
return usercredentialMapper.toLegacySignUpStartResponse(webAuthnAuthenticator.startRegistration(name));
} catch (JsonProcessingException e) {
log.error("Register Credential failed", e);
throw new SignUpFailedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import com.github.benmanes.caffeine.cache.Caffeine;
import com.helioauth.passkeys.api.config.properties.WebAuthnRelyingPartyProperties;
import com.helioauth.passkeys.api.contract.SignInStartResponse;
import com.helioauth.passkeys.api.contract.SignUpStartResponse;
import com.helioauth.passkeys.api.generated.models.SignUpStartResponse;
import com.helioauth.passkeys.api.mapper.CredentialRegistrationResultMapper;
import com.helioauth.passkeys.api.service.dto.CredentialAssertionResult;
import com.helioauth.passkeys.api.service.dto.CredentialRegistrationResult;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.helioauth.passkeys.api.service;

import com.helioauth.passkeys.api.domain.ClientApplication;
Expand Down
Loading

0 comments on commit b02c83a

Please sign in to comment.