commit 166a1bfe2dacc3377626f7c6af9fe1fe5970e709 parent d577c5400f097bc6aa7f33930bae2fea4bc67d45 Author: Wim Dupont <wim@wimdupont.com> Date: Mon, 17 Jul 2023 21:22:20 +0200 api added Former-commit-id: 1e322ad43305c4c9a197c004cde37a5f185df4fc Diffstat:
15 files changed, 332 insertions(+), 124 deletions(-)
diff --git a/src/main/java/com/wimdupont/personalweb/config/SecurityConfig.java b/src/main/java/com/wimdupont/personalweb/config/SecurityConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec; import org.springframework.security.config.web.server.ServerHttpSecurity.FormLoginSpec; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -15,6 +16,8 @@ public class SecurityConfig { @Bean public SecurityWebFilterChain filterChain(ServerHttpSecurity http) { - return http.formLogin(FormLoginSpec::disable).build(); + return http.formLogin(FormLoginSpec::disable) + .csrf(CsrfSpec::disable) + .build(); } } diff --git a/src/main/java/com/wimdupont/personalweb/controller/api/CryptographyController.java b/src/main/java/com/wimdupont/personalweb/controller/api/CryptographyController.java @@ -0,0 +1,30 @@ +package com.wimdupont.personalweb.controller.api; + +import com.wimdupont.personalweb.model.Hash; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.xml.bind.DatatypeConverter; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +@RestController +@RequestMapping("/api/cryptography") +@Tag(name = "Cryptography") +public class CryptographyController { + + @PostMapping(value = "/hash") + @Operation(summary = "Hash value using the algorithm provided") + public String hash(@RequestBody String value, + @RequestParam Hash algorithm) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance(algorithm.getValue()); + byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8)); + return DatatypeConverter.printHexBinary(hash); + } +} diff --git a/src/main/java/com/wimdupont/personalweb/controller/api/EncodingController.java b/src/main/java/com/wimdupont/personalweb/controller/api/EncodingController.java @@ -0,0 +1,36 @@ +package com.wimdupont.personalweb.controller.api; + +import com.wimdupont.personalweb.exception.Base64DecodingException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 java.nio.charset.StandardCharsets; +import java.util.Base64; + +@RestController +@RequestMapping("/api/encoding") +@Tag(name = "Encoding") +public class EncodingController { + + + @PostMapping(value = "/base64/encode") + @Operation(summary = "Encode value to base64") + public String encodeBase64(@RequestBody String value) { + return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8)); + } + + @PostMapping(value = "/base64/decode") + @Operation(summary = "Decode value from base64") + public String decodeBase64(@RequestBody String value) { + try { + return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new Base64DecodingException(); + } + } + +} diff --git a/src/main/java/com/wimdupont/personalweb/controller/api/InszController.java b/src/main/java/com/wimdupont/personalweb/controller/api/InszController.java @@ -1,13 +1,18 @@ package com.wimdupont.personalweb.controller.api; +import com.wimdupont.personalweb.model.Gender; +import com.wimdupont.personalweb.model.InszModel; import com.wimdupont.personalweb.service.InszService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.time.LocalDate; + @RestController @RequestMapping("/api/insz") @Tag(name = "INSZ", description = "Belgian national identification number") @@ -19,29 +24,32 @@ public class InszController { this.inszService = inszService; } - //TODO -// @GetMapping(value = "/generate") -// @Operation(summary = "Generate INSZ", -// description = "Generate random INSZ taking filled in parameters in account") -// public String generate( -// @RequestParam(required = false) Integer year, -// @RequestParam(required = false) Integer month, -// @RequestParam(required = false) Integer day, -// @RequestParam(required = false) Integer birthCounter, -// @RequestParam(required = false) Gender gender) { -// return inszService.generate(InszGenerator.Builder.newBuilder() -// .year(year) -// .month(month) -// .day(day) -// .birthCounter(birthCounter) -// .gender(gender) -// .build()); -// } + @GetMapping(value = "/generate") + @Operation(summary = "Generate INSZ", + description = "Generate random INSZ taking filled in parameters in account") + public String generate( + @RequestParam(required = false) + @Parameter(description = "yyyy-MM-dd") + LocalDate birthDate, + @RequestParam(required = false) + Integer birthCounter, + @RequestParam(required = false) + Gender gender) { + return inszService.generate(new InszModel(birthDate, birthCounter, gender)); + } + @GetMapping(value = "/valid") - @Operation(summary = "Validate INSZ", - description = "Removes '.' and '-' characters before validation") - public boolean validate(@RequestParam(required = false) String insz) { - return inszService.validate(insz); + @Operation(summary = "Validate INSZ") + public boolean validate(@RequestParam + @Parameter(description = "'.' and '-' characters are automatically removed.") + String insz, + @RequestParam(required = false) + @Parameter(description = "Both are considered when empty.") + Boolean bornAtOrAfter2000, + @RequestParam(required = false) + @Parameter(description = "Both are considered when empty.") + Gender gender) { + return inszService.validate(insz, bornAtOrAfter2000, gender); } } diff --git a/src/main/java/com/wimdupont/personalweb/controller/api/RandomController.java b/src/main/java/com/wimdupont/personalweb/controller/api/RandomController.java @@ -3,11 +3,15 @@ package com.wimdupont.personalweb.controller.api; import com.wimdupont.personalweb.model.Coin; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotEmpty; import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; import java.util.Random; import java.util.UUID; @@ -26,7 +30,8 @@ public class RandomController { @Operation(summary = "Flip a coin") public String coinFlip() { return Math.random() < 0.5 - ? Coin.HEADS.name() : Coin.TAILS.name(); + ? Coin.HEADS.name() + : Coin.TAILS.name(); } @GetMapping(value = "/number") @@ -37,13 +42,9 @@ public class RandomController { return new Random().nextInt(max - min + 1) + min; } -// TODO FIX CSRF -// @PostMapping(value = "/name") -// @Operation(summary = "Get random value of list") -// public String name(@RequestBody List<String> names) { -// if (names != null && !names.isEmpty()) { -// return names.get(new Random().nextInt(names.size())); -// } -// throw new AtleastOneNameRequiredException(); -// } + @PostMapping(value = "/name") + @Operation(summary = "Get random value of list") + public String name(@RequestBody @NotEmpty List<String> names) { + return names.get(new Random().nextInt(names.size())); + } } diff --git a/src/main/java/com/wimdupont/personalweb/exception/Base64DecodingException.java b/src/main/java/com/wimdupont/personalweb/exception/Base64DecodingException.java @@ -0,0 +1,13 @@ +package com.wimdupont.personalweb.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +public class Base64DecodingException extends RuntimeException { + + public Base64DecodingException() { + super("Unable to decode given value. Please make sure it is base64 encoded."); + } + +} diff --git a/src/main/java/com/wimdupont/personalweb/exception/InvalidInszGenerated.java b/src/main/java/com/wimdupont/personalweb/exception/InvalidInszGenerated.java @@ -0,0 +1,12 @@ +package com.wimdupont.personalweb.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +public class InvalidInszGenerated extends RuntimeException { + + public InvalidInszGenerated(String insz) { + super(String.format("Generated invalid INSZ: %s", insz)); + } +} diff --git a/src/main/java/com/wimdupont/personalweb/exceptions/AtleastOneNameRequiredException.java b/src/main/java/com/wimdupont/personalweb/exceptions/AtleastOneNameRequiredException.java @@ -1,10 +0,0 @@ -package com.wimdupont.personalweb.exceptions; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.BAD_REQUEST) -public class AtleastOneNameRequiredException extends RuntimeException { - - -} diff --git a/src/main/java/com/wimdupont/personalweb/model/Hash.java b/src/main/java/com/wimdupont/personalweb/model/Hash.java @@ -0,0 +1,24 @@ +package com.wimdupont.personalweb.model; + +public enum Hash { + MD5("MD5"), + SHA_1("SHA-1"), + SHA_224("SHA-224"), + SHA_256("SHA-256"), + SHA_384("SHA-384"), + SHA_512("SHA-512"), + SHA3_224("SHA3-224"), + SHA3_256("SHA3-256"), + SHA3_384("SHA3-384"), + SHA3_512("SHA3-512"); + + private final String value; + + Hash(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/wimdupont/personalweb/model/InszGenerator.java b/src/main/java/com/wimdupont/personalweb/model/InszGenerator.java @@ -1,72 +0,0 @@ -package com.wimdupont.personalweb.model; - -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; - -public record InszGenerator( - @Min(value = 0) - @Max(value = 2999) - Integer year, - @Min(value = 1) - @Max(value = 12) - Integer month, - @Min(value = 1) - @Max(value = 31) - Integer day, - @Min(value = 1) - @Max(value = 999) - Integer birthCounter, - Gender gender) { - - private InszGenerator(Builder builder) { - this(builder.year, - builder.month, - builder.day, - builder.birthCounter, - builder.gender); - } - - public static final class Builder { - private Integer year; - private Integer month; - private Integer day; - private Integer birthCounter; - private Gender gender; - - private Builder() { - } - - public static Builder newBuilder() { - return new Builder(); - } - - public Builder year(Integer val) { - year = val; - return this; - } - - public Builder month(Integer val) { - month = val; - return this; - } - - public Builder day(Integer val) { - day = val; - return this; - } - - public Builder birthCounter(Integer val) { - birthCounter = val; - return this; - } - - public Builder gender(Gender val) { - gender = val; - return this; - } - - public InszGenerator build() { - return new InszGenerator(this); - } - } -} diff --git a/src/main/java/com/wimdupont/personalweb/model/InszModel.java b/src/main/java/com/wimdupont/personalweb/model/InszModel.java @@ -0,0 +1,55 @@ +package com.wimdupont.personalweb.model; + +import com.wimdupont.personalweb.model.validator.ValidInszModel; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; +import java.util.Random; + +@ValidInszModel +public record InszModel( + @NotNull + LocalDate birthDate, + @Min(value = 1) + @Max(value = 998) + @NotNull + Integer birthCounter, + @NotNull + Gender gender +) { + + public InszModel(LocalDate birthDate, Integer birthCounter, Gender gender) { + this.birthDate = generateBirthDateIfNull(birthDate); + this.birthCounter = getRandomIfNull(birthCounter, 1, 998); + this.gender = getRandomIfNull(gender); + } + + private Gender getRandomIfNull(Gender gender) { + return gender == null + ? Gender.values()[(new Random().nextInt(Gender.values().length))] + : gender; + } + + private int getRandomIfNull(Integer value, int min, int max) { + return value == null + ? generateRandom(min, max) + : value; + } + + private LocalDate generateBirthDateIfNull(LocalDate birthDate) { + if (birthDate == null) { + int minDay = (int) LocalDate.of(0, 1, 1).toEpochDay(); + int maxDay = (int) LocalDate.of(2999, 1, 1).toEpochDay(); + birthDate = LocalDate.ofEpochDay(generateRandom(minDay, maxDay)); + } + System.out.println("random date: " + birthDate); + return birthDate; + } + + private int generateRandom(int min, int max) { + return new Random().nextInt(max - min + 1) + min; + } + +} diff --git a/src/main/java/com/wimdupont/personalweb/model/validator/InszModelValidator.java b/src/main/java/com/wimdupont/personalweb/model/validator/InszModelValidator.java @@ -0,0 +1,24 @@ +package com.wimdupont.personalweb.model.validator; + +import com.wimdupont.personalweb.model.InszModel; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.LocalDate; + +public class InszModelValidator implements ConstraintValidator<ValidInszModel, InszModel> { + + @Override + public boolean isValid(InszModel value, ConstraintValidatorContext context) { + var minDate = LocalDate.of(0, 1, 1); + var maxDate = LocalDate.of(2999, 12, 31); + if (value.birthDate().isBefore(minDate) || value.birthDate().isAfter(maxDate)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate( + String.format("Please enter a birth date between %s and %s", minDate, maxDate)) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/wimdupont/personalweb/model/validator/ValidInszModel.java b/src/main/java/com/wimdupont/personalweb/model/validator/ValidInszModel.java @@ -0,0 +1,20 @@ +package com.wimdupont.personalweb.model.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = InszModelValidator.class) +@Target( { ElementType.TYPE, ElementType.RECORD_COMPONENT }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidInszModel { + String message() default "Invalid INSZ number"; + Class<?>[] groups() default {}; + Class<? extends Payload>[] payload() default {}; +} diff --git a/src/main/java/com/wimdupont/personalweb/service/InszService.java b/src/main/java/com/wimdupont/personalweb/service/InszService.java @@ -1,7 +1,12 @@ package com.wimdupont.personalweb.service; -import com.wimdupont.personalweb.model.InszGenerator; +import com.wimdupont.personalweb.exception.InvalidInszGenerated; +import com.wimdupont.personalweb.model.Gender; +import com.wimdupont.personalweb.model.InszModel; import jakarta.validation.Valid; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -9,14 +14,47 @@ import org.springframework.validation.annotation.Validated; @Validated public class InszService { - public String generate(@Valid InszGenerator insz) { - if (insz.gender() == null) { - return "Random!"; + private static final Logger LOGGER = LoggerFactory.getLogger(InszService.class); + + public String generate(@Valid InszModel insz) { + var year = StringUtils.leftPad(String.valueOf(insz.birthDate().getYear()), 4, "0").substring(2); + var month = StringUtils.leftPad(String.valueOf(insz.birthDate().getMonthValue()), 2, "0"); + var day = StringUtils.leftPad(String.valueOf(insz.birthDate().getDayOfMonth()), 2, "0"); + var birthCounterParam = insz.birthCounter(); + + if (hasNoValidBirthCounter(String.valueOf(insz.birthCounter()), insz.gender())) { + if (insz.birthCounter() == 998) { + --birthCounterParam; + } else { + ++birthCounterParam; + } + } + var birthCounter = StringUtils.leftPad(String.valueOf(birthCounterParam), 3, "0"); + + var baseNumber = year + month + day + birthCounter; + var bornAtOrAfter2000 = insz.birthDate().getYear() >= 2000; + + var result = baseNumber + getChecksum(baseNumber, bornAtOrAfter2000); + + if (!validate(result, bornAtOrAfter2000, insz.gender())) { + throw new InvalidInszGenerated(result); } - return insz.gender().name(); + + return result; } - public boolean validate(String insz) { + private String getChecksum(String baseNumber, boolean bornAtOrAfter2000) { + if (bornAtOrAfter2000) { + baseNumber = 2 + baseNumber; + } + LOGGER.info("getChecksum for: baseNumber: {}", baseNumber); + var checkSum = 97 - Long.parseLong(baseNumber) % 97; + LOGGER.info("Checksum = {}", checkSum); + return StringUtils.leftPad(String.valueOf(checkSum), 2, "0"); + } + + + public boolean validate(String insz, Boolean bornAtOrAfter2000, Gender gender) { if (insz == null) { return false; } @@ -26,17 +64,41 @@ public class InszService { return false; } + if (hasNoValidBirthCounter(insz.substring(6, 9), gender)) { + return false; + } + var baseNumber = insz.substring(0, 9); var checksum = insz.substring(9, 11); - return hasCorrectChecksum(baseNumber, checksum) || hasCorrectChecksum(2 + baseNumber, checksum); + if (bornAtOrAfter2000 == null) { + return hasCorrectChecksum(baseNumber, checksum) || hasCorrectChecksum(2 + baseNumber, checksum); + } else { + return bornAtOrAfter2000 + ? hasCorrectChecksum(2 + baseNumber, checksum) + : hasCorrectChecksum(baseNumber, checksum); + } } private boolean hasCorrectChecksum(String baseNumber, String checkSum) throws NumberFormatException { try { - return 97 - Integer.parseInt(baseNumber) % 97 == Integer.parseInt(checkSum); + return 97 - Long.parseLong(baseNumber) % 97 == Integer.parseInt(checkSum); } catch (NumberFormatException e) { return false; } } + + private boolean hasNoValidBirthCounter(String birthCounter, Gender gender) { + if (gender == null) { + return false; + } + try { + return !switch (gender) { + case MALE -> Integer.parseInt(birthCounter) % 2 != 0; + case FEMALE -> Integer.parseInt(birthCounter) % 2 == 0; + }; + } catch (NumberFormatException e) { + return true; + } + } } diff --git a/src/main/resources/api-documentation.md b/src/main/resources/api-documentation.md @@ -1,3 +1,5 @@ -Free of charge to use any amount of requests. +Use any amount of requests free of charge. + +Most of the endpoints offer functionality already provided by your OS, but I'd like to expose them anyway since people often navigate to their browser for these. *[Donations are accepted](https://wimdupont.com/donate)*