diff --git a/build.gradle b/build.gradle index a884401..5e11993 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,12 @@ dependencies { implementation 'com.github.loki4j:loki-logback-appender:1.4.1' implementation "org.springframework.boot:spring-boot-starter-actuator" runtimeOnly "io.micrometer:micrometer-registry-prometheus" + + // Bad Word Filtering + implementation 'io.github.vaneproject:badwordfiltering:1.0.0' + + // Mail + implementation 'org.springframework.boot:spring-boot-starter-mail' } tasks.named('test') { diff --git a/src/main/java/org/fontory/fontorybe/common/adapter/inbound/GlobalExceptionHandler.java b/src/main/java/org/fontory/fontorybe/common/adapter/inbound/GlobalExceptionHandler.java index e00df7e..f014499 100644 --- a/src/main/java/org/fontory/fontorybe/common/adapter/inbound/GlobalExceptionHandler.java +++ b/src/main/java/org/fontory/fontorybe/common/adapter/inbound/GlobalExceptionHandler.java @@ -4,6 +4,8 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.MalformedJwtException; import lombok.RequiredArgsConstructor; +import org.fontory.fontorybe.authentication.domain.exception.InvalidRefreshTokenException; +import org.fontory.fontorybe.authentication.domain.exception.TokenNotFoundException; import org.fontory.fontorybe.bookmark.domain.exception.BookmarkAlreadyException; import org.fontory.fontorybe.bookmark.domain.exception.BookmarkNotFoundException; import org.fontory.fontorybe.common.domain.BaseErrorResponse; @@ -12,14 +14,19 @@ import org.fontory.fontorybe.file.domain.exception.FileNotFoundException; import org.fontory.fontorybe.file.domain.exception.InvalidMultipartRequestException; import org.fontory.fontorybe.file.domain.exception.SingleFileRequiredException; +import org.fontory.fontorybe.font.domain.exception.FontContainsBadWordException; import org.fontory.fontorybe.font.domain.exception.FontDuplicateNameExistsException; import org.fontory.fontorybe.font.domain.exception.FontInvalidStatusException; import org.fontory.fontorybe.font.domain.exception.FontNotFoundException; import org.fontory.fontorybe.font.domain.exception.FontOwnerMismatchException; import org.fontory.fontorybe.font.domain.exception.FontSQSProduceExcepetion; -import org.fontory.fontorybe.member.domain.exception.*; -import org.fontory.fontorybe.authentication.domain.exception.InvalidRefreshTokenException; -import org.fontory.fontorybe.authentication.domain.exception.TokenNotFoundException; +import org.fontory.fontorybe.member.domain.exception.MemberAlreadyDisabledException; +import org.fontory.fontorybe.member.domain.exception.MemberAlreadyExistException; +import org.fontory.fontorybe.member.domain.exception.MemberAlreadyJoinedException; +import org.fontory.fontorybe.member.domain.exception.MemberContainsBadWordException; +import org.fontory.fontorybe.member.domain.exception.MemberDuplicateNameExistsException; +import org.fontory.fontorybe.member.domain.exception.MemberNotFoundException; +import org.fontory.fontorybe.member.domain.exception.MemberOwnerMismatchException; import org.fontory.fontorybe.provide.domain.exception.ProvideNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -156,4 +163,10 @@ public BaseErrorResponse fontInvalidStatusException(FontInvalidStatusException e public BaseErrorResponse fileNotFoundException(FileNotFoundException e) { return new BaseErrorResponse(e.getMessage()); } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({FontContainsBadWordException.class, MemberContainsBadWordException.class}) + public BaseErrorResponse containsBadWordException(Exception e) { + return new BaseErrorResponse(e.getMessage()); + } } diff --git a/src/main/java/org/fontory/fontorybe/config/BadWordFilteringConfig.java b/src/main/java/org/fontory/fontorybe/config/BadWordFilteringConfig.java new file mode 100644 index 0000000..fcd7a92 --- /dev/null +++ b/src/main/java/org/fontory/fontorybe/config/BadWordFilteringConfig.java @@ -0,0 +1,14 @@ +package org.fontory.fontorybe.config; + +import com.vane.badwordfiltering.BadWordFiltering; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BadWordFilteringConfig { + + @Bean + public BadWordFiltering badWordFiltering() { + return new BadWordFiltering(); + } +} diff --git a/src/main/java/org/fontory/fontorybe/font/domain/exception/FontContainsBadWordException.java b/src/main/java/org/fontory/fontorybe/font/domain/exception/FontContainsBadWordException.java new file mode 100644 index 0000000..1acad97 --- /dev/null +++ b/src/main/java/org/fontory/fontorybe/font/domain/exception/FontContainsBadWordException.java @@ -0,0 +1,10 @@ +package org.fontory.fontorybe.font.domain.exception; + +import org.fontory.fontorybe.common.domain.SkipDiscordNotification; + +@SkipDiscordNotification +public class FontContainsBadWordException extends RuntimeException { + public FontContainsBadWordException() { + super("Font contains bad word"); + } +} diff --git a/src/main/java/org/fontory/fontorybe/font/service/FontServiceImpl.java b/src/main/java/org/fontory/fontorybe/font/service/FontServiceImpl.java index 5e978b8..ea2029b 100644 --- a/src/main/java/org/fontory/fontorybe/font/service/FontServiceImpl.java +++ b/src/main/java/org/fontory/fontorybe/font/service/FontServiceImpl.java @@ -1,5 +1,6 @@ package org.fontory.fontorybe.font.service; +import com.vane.badwordfiltering.BadWordFiltering; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -9,9 +10,18 @@ import org.fontory.fontorybe.file.application.port.FileService; import org.fontory.fontorybe.file.domain.FileMetadata; import org.fontory.fontorybe.file.domain.FileUploadResult; -import org.fontory.fontorybe.font.controller.dto.*; +import org.fontory.fontorybe.font.controller.dto.FontCreateDTO; +import org.fontory.fontorybe.font.controller.dto.FontDeleteResponse; +import org.fontory.fontorybe.font.controller.dto.FontDownloadResponse; +import org.fontory.fontorybe.font.controller.dto.FontPageResponse; +import org.fontory.fontorybe.font.controller.dto.FontProgressResponse; +import org.fontory.fontorybe.font.controller.dto.FontProgressUpdateDTO; +import org.fontory.fontorybe.font.controller.dto.FontResponse; +import org.fontory.fontorybe.font.controller.dto.FontUpdateDTO; +import org.fontory.fontorybe.font.controller.dto.FontUpdateResponse; import org.fontory.fontorybe.font.controller.port.FontService; import org.fontory.fontorybe.font.domain.Font; +import org.fontory.fontorybe.font.domain.exception.FontContainsBadWordException; import org.fontory.fontorybe.font.domain.exception.FontDuplicateNameExistsException; import org.fontory.fontorybe.font.domain.exception.FontInvalidStatusException; import org.fontory.fontorybe.font.domain.exception.FontNotFoundException; @@ -39,6 +49,7 @@ public class FontServiceImpl implements FontService { private final MemberLookupService memberLookupService; private final FontRequestProducer fontRequestProducer; private final CloudStorageService cloudStorageService; + private final BadWordFiltering badWordFiltering; @Override @Transactional @@ -49,6 +60,9 @@ public Font create(Long memberId, FontCreateDTO fontCreateDTO, FileUploadResult if (isDuplicateNameExists(memberId, fontCreateDTO.getName())) { throw new FontDuplicateNameExistsException(); } + + checkContainsBadWord(fontCreateDTO.getName(), fontCreateDTO.getExample()); + FileMetadata fileMetadata = fileService.getOrThrowById(fileDetails.getId()); Font savedFont = fontRepository.save(Font.from(fontCreateDTO, member.getId(), fileMetadata.getKey())); @@ -82,6 +96,7 @@ public FontUpdateResponse update(Long memberId, Long fontId, FontUpdateDTO fontU Font targetFont = getOrThrowById(fontId); checkFontOwnership(member.getId(), targetFont.getMemberId()); + checkContainsBadWord(fontUpdateDTO.getName(), fontUpdateDTO.getExample()); Font updatedFont = fontRepository.save(targetFont.update(fontUpdateDTO)); String woff2Url = cloudStorageService.getWoff2Url(updatedFont.getKey()); @@ -340,4 +355,13 @@ private void checkFontStatusIsDone(Font targetFont) { throw new FontInvalidStatusException(); } } + + private void checkContainsBadWord(String name, String example) { + log.debug("Service detail: Checking bad word: name={}, example={}", name, example); + + if (badWordFiltering.blankCheck(name) || badWordFiltering.blankCheck(example)) { + log.warn("Service warning: Font contains bad word: name={}, example={}", name, example); + throw new FontContainsBadWordException(); + } + } } diff --git a/src/main/java/org/fontory/fontorybe/member/domain/exception/MemberContainsBadWordException.java b/src/main/java/org/fontory/fontorybe/member/domain/exception/MemberContainsBadWordException.java new file mode 100644 index 0000000..14dd5c2 --- /dev/null +++ b/src/main/java/org/fontory/fontorybe/member/domain/exception/MemberContainsBadWordException.java @@ -0,0 +1,10 @@ +package org.fontory.fontorybe.member.domain.exception; + +import org.fontory.fontorybe.common.domain.SkipDiscordNotification; + +@SkipDiscordNotification +public class MemberContainsBadWordException extends RuntimeException { + public MemberContainsBadWordException() { + super("Member contains bad word."); + } +} diff --git a/src/main/java/org/fontory/fontorybe/member/service/MemberOnboardServiceImpl.java b/src/main/java/org/fontory/fontorybe/member/service/MemberOnboardServiceImpl.java index f41ba2a..07c615f 100644 --- a/src/main/java/org/fontory/fontorybe/member/service/MemberOnboardServiceImpl.java +++ b/src/main/java/org/fontory/fontorybe/member/service/MemberOnboardServiceImpl.java @@ -1,5 +1,6 @@ package org.fontory.fontorybe.member.service; +import com.vane.badwordfiltering.BadWordFiltering; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.fontory.fontorybe.file.application.port.FileService; @@ -11,6 +12,7 @@ import org.fontory.fontorybe.member.controller.port.MemberOnboardService; import org.fontory.fontorybe.member.domain.Member; import org.fontory.fontorybe.member.domain.exception.MemberAlreadyJoinedException; +import org.fontory.fontorybe.member.domain.exception.MemberContainsBadWordException; import org.fontory.fontorybe.member.domain.exception.MemberDuplicateNameExistsException; import org.fontory.fontorybe.member.infrastructure.entity.MemberStatus; import org.fontory.fontorybe.member.service.port.MemberRepository; @@ -26,6 +28,7 @@ public class MemberOnboardServiceImpl implements MemberOnboardService { private final MemberLookupService memberLookupService; private final MemberCreationService memberCreationService; private final FileService fileService; + private final BadWordFiltering badWordFiltering; @Override @Transactional @@ -50,6 +53,14 @@ public Member initNewMemberInfo(Long requestMemberId, throw new MemberDuplicateNameExistsException(); } + checkContainsBadWord(initNewMemberInfoRequest.getNickname()); + return memberRepository.save(targetMember.initNewMemberInfo(initNewMemberInfoRequest, fileMetadata.getKey())); } + + private void checkContainsBadWord(String nickname) { + if (badWordFiltering.blankCheck(nickname)) { + throw new MemberContainsBadWordException(); + } + } } diff --git a/src/main/java/org/fontory/fontorybe/member/service/MemberUpdateServiceImpl.java b/src/main/java/org/fontory/fontorybe/member/service/MemberUpdateServiceImpl.java index 80a76a2..e40b866 100644 --- a/src/main/java/org/fontory/fontorybe/member/service/MemberUpdateServiceImpl.java +++ b/src/main/java/org/fontory/fontorybe/member/service/MemberUpdateServiceImpl.java @@ -1,5 +1,6 @@ package org.fontory.fontorybe.member.service; +import com.vane.badwordfiltering.BadWordFiltering; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.fontory.fontorybe.authentication.application.port.JwtTokenProvider; @@ -8,6 +9,7 @@ import org.fontory.fontorybe.member.domain.Member; import org.fontory.fontorybe.member.controller.dto.MemberUpdateRequest; import org.fontory.fontorybe.member.domain.exception.MemberAlreadyDisabledException; +import org.fontory.fontorybe.member.domain.exception.MemberContainsBadWordException; import org.fontory.fontorybe.member.domain.exception.MemberDuplicateNameExistsException; import org.fontory.fontorybe.member.service.port.MemberRepository; import org.fontory.fontorybe.provide.controller.port.ProvideService; @@ -23,6 +25,7 @@ public class MemberUpdateServiceImpl implements MemberUpdateService { private final JwtTokenProvider jwtTokenProvider; private final MemberRepository memberRepository; private final ProvideService provideService; + private final BadWordFiltering badWordFiltering; @Override @Transactional @@ -34,6 +37,8 @@ public Member update(Long requestMemberId, MemberUpdateRequest memberUpdateReque throw new MemberDuplicateNameExistsException(); } + checkContainsBadWord(memberUpdateRequest.getNickname()); + return memberRepository.save(targetMember.update(memberUpdateRequest)); } @@ -56,4 +61,10 @@ public Member disable(Long requestMemberId) { return memberRepository.save(targetMember); } + + private void checkContainsBadWord(String nickname) { + if (badWordFiltering.blankCheck(nickname)) { + throw new MemberContainsBadWordException(); + } + } } diff --git a/src/test/java/org/fontory/fontorybe/unit/mock/TestContainer.java b/src/test/java/org/fontory/fontorybe/unit/mock/TestContainer.java index 0618f3d..064f1a8 100644 --- a/src/test/java/org/fontory/fontorybe/unit/mock/TestContainer.java +++ b/src/test/java/org/fontory/fontorybe/unit/mock/TestContainer.java @@ -1,5 +1,6 @@ package org.fontory.fontorybe.unit.mock; +import com.vane.badwordfiltering.BadWordFiltering; import org.fontory.fontorybe.authentication.adapter.outbound.CookieUtilsImpl; import org.fontory.fontorybe.config.S3Config; import org.fontory.fontorybe.config.jwt.JwtProperties; @@ -82,6 +83,7 @@ public class TestContainer { public final MemberDefaults memberDefaults; public final CookieUtils cookieUtils; public final S3Config s3Config; + public final BadWordFiltering badWordFiltering; public TestContainer() { props = new JwtProperties( @@ -105,6 +107,8 @@ public TestContainer() { tokenStorage = new RedisTokenStorage(fakeRedisTemplate, props); + badWordFiltering = new BadWordFiltering(); + s3Config = new S3Config( TEST_AWS_REGION, TEST_CDN_URL, @@ -130,6 +134,7 @@ public TestContainer() { .memberRepository(memberRepository) .provideService(provideService) .jwtTokenProvider(jwtTokenProvider) + .badWordFiltering(badWordFiltering) .build(); memberDefaults = new MemberDefaults( @@ -166,6 +171,7 @@ public TestContainer() { .memberRepository(memberRepository) .memberLookupService(memberLookupService) .memberCreationService(memberCreationService) + .badWordFiltering(badWordFiltering) .build(); memberController = MemberController.builder()