Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

일반 회원 가입 API 개선 #83

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 54 additions & 42 deletions sql/titi.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,53 @@ use titi;

DELIMITER //

DROP FUNCTION IF EXISTS generate_unique_hash_code;

CREATE FUNCTION generate_unique_hash_code() RETURNS VARCHAR(8)
DETERMINISTIC
BEGIN
DECLARE new_hash_code VARCHAR(8);
DECLARE new_hash_code VARCHAR(8);

REPEAT
SET new_hash_code = LPAD(CONV(FLOOR(RAND() * 99999999), 10, 36), 8, '0');
UNTIL NOT EXISTS (SELECT 1 FROM members WHERE hashcode = new_hash_code) END REPEAT;
REPEAT
SET new_hash_code = LPAD(CONV(FLOOR(RAND() * 99999999), 10, 36), 8, '0');
UNTIL NOT EXISTS (SELECT 1 FROM members WHERE hashcode = new_hash_code) END REPEAT;

RETURN new_hash_code;
RETURN new_hash_code;
END //

DELIMITER ;

CREATE TABLE IF NOT EXISTS accounts
(
id BIGINT AUTO_INCREMENT,
username VARCHAR(30) NOT NULL UNIQUE COMMENT '아이디(이메일)',
password VARCHAR(255) COMMENT '비밀번호',
authority ENUM ('MEMBER', 'ADMIN') NOT NULL COMMENT '권한',
account_status ENUM ('ACTIVATED', 'DEACTIVATED', 'SUSPENDED', 'BLOCKED', 'DELETED') NOT NULL COMMENT '계정 상태',
created_at DATETIME(6) NOT NULL COMMENT '생성 일시',
updated_at DATETIME(6) NOT NULL COMMENT '수정 일시',
PRIMARY KEY (id)
) COMMENT '계정';

CREATE TABLE IF NOT EXISTS members
(
id BIGINT AUTO_INCREMENT,
username VARCHAR(30) NOT NULL UNIQUE COMMENT '아이디(이메일)',
password VARCHAR(255) NOT NULL COMMENT '비밀번호',
nickname VARCHAR(15) NOT NULL COMMENT '닉네임',
hashcode CHAR(8) NOT NULL COMMENT '해시코드',
authority ENUM('MEMBER', 'ADMIN') NOT NULL COMMENT '권한',
account_status ENUM('ACTIVATED', 'DEACTIVATED', 'SUSPENDED', 'BLOCKED', 'DELETED') NOT NULL COMMENT '계정 상태',
membership_type ENUM('NORMAL', 'PREMIUM') NOT NULL COMMENT '멤버십 유형',
profile_image_name VARCHAR(50) NOT NULL COMMENT '프로필 이미지 파일 이름',
profile_image_id CHAR(36) UNIQUE NOT NULL COMMENT '프로필 이미지 파일 ID',
profile_image_type ENUM('JPG', 'JPEG', 'PNG') NOT NULL COMMENT '프로필 이미지 파일 유형',
created_at DATETIME(6) NOT NULL COMMENT '생성 일시',
updated_at DATETIME(6) NOT NULL COMMENT '수정 일시',
id BIGINT AUTO_INCREMENT,
account_id BIGINT NOT NULL COMMENT '계정 PK',
nickname VARCHAR(15) NOT NULL COMMENT '닉네임',
hashcode CHAR(8) NOT NULL COMMENT '해시코드',
membership_type ENUM ('NORMAL', 'PREMIUM') NOT NULL COMMENT '멤버십 유형',
profile_image_name VARCHAR(50) NOT NULL COMMENT '프로필 이미지 파일 이름',
profile_image_id CHAR(36) UNIQUE NOT NULL COMMENT '프로필 이미지 파일 ID',
profile_image_type ENUM ('JPG', 'JPEG', 'PNG') NOT NULL COMMENT '프로필 이미지 파일 유형',
created_at DATETIME(6) NOT NULL COMMENT '생성 일시',
updated_at DATETIME(6) NOT NULL COMMENT '수정 일시',
PRIMARY KEY (id),
UNIQUE INDEX uix_members_nickname (nickname, hashcode)
) COMMENT '회원';

CREATE TRIGGER generate_hashcode
BEFORE INSERT ON members
BEFORE INSERT
ON members
FOR EACH ROW
SET NEW.hashcode = generate_unique_hash_code();

Expand All @@ -56,40 +68,40 @@ CREATE TABLE IF NOT EXISTS devices

CREATE TABLE IF NOT EXISTS oauth2_infos
(
id BIGINT AUTO_INCREMENT,
member_id BIGINT NOT NULL COMMENT '회원 PK',
provider_id VARCHAR(100) NOT NULL COMMENT 'OAuth2 서비스 회원 식별자',
provider ENUM('GOOGLE', 'APPLE') NOT NULL COMMENT 'OAuth2 서비스 제공 업체',
access_token VARCHAR(100) NOT NULL COMMENT 'OAuth2 인증 토큰',
refresh_token VARCHAR(100) NOT NULL COMMENT 'OAuth2 인증 갱신 토큰',
created_at DATETIME(6) NOT NULL COMMENT '생성 일시',
updated_at DATETIME(6) NOT NULL COMMENT '수정 일시',
id BIGINT AUTO_INCREMENT,
account_id BIGINT NOT NULL COMMENT '계정 PK',
provider_id VARCHAR(100) NOT NULL COMMENT 'OAuth2 서비스 회원 식별자',
provider ENUM ('GOOGLE', 'APPLE') NOT NULL COMMENT 'OAuth2 서비스 제공 업체',
access_token VARCHAR(100) NOT NULL COMMENT 'OAuth2 인증 토큰',
refresh_token VARCHAR(100) NOT NULL COMMENT 'OAuth2 인증 갱신 토큰',
created_at DATETIME(6) NOT NULL COMMENT '생성 일시',
updated_at DATETIME(6) NOT NULL COMMENT '수정 일시',
PRIMARY KEY (id),
CONSTRAINT fk_oauth2_infos_member_id FOREIGN KEY (member_id) REFERENCES members(id),
CONSTRAINT fk_oauth2_infos_account_id FOREIGN KEY (account_id) REFERENCES accounts (id),
UNIQUE INDEX uix_oauth2_infos_provider_id (provider, provider_id)
) COMMENT 'OAuth2 정보';

CREATE TABLE IF NOT EXISTS notifications
(
uid BIGINT NOT NULL,
notification_category ENUM ('INFORMATION', 'NOTICE', 'MARKETING', 'AUTHENTICATION') NOT NULL COMMENT '알림 종류',
notification_type ENUM ('EMAIL') NOT NULL COMMENT '알림 유형',
notification_status ENUM ('CREATED', 'COMPLETED', 'FAILED') NOT NULL COMMENT '알림 상태',
target_type ENUM ('EMAIL') NOT NULL COMMENT '수신 유형',
target_value VARCHAR(255) NOT NULL COMMENT '수신자 정보',
service_name ENUM ('AUTH') NOT NULL COMMENT '서비스명',
service_request_id VARCHAR(255) NOT NULL COMMENT '서비스 요청 ID',
notified_at DATETIME(6) NULL COMMENT '발송 일시',
created_at DATETIME(6) NOT NULL COMMENT '생성 일시',
updated_at DATETIME(6) NOT NULL COMMENT '수정 일시',
uid BIGINT NOT NULL,
notification_category ENUM ('INFORMATION', 'NOTICE', 'MARKETING', 'AUTHENTICATION') NOT NULL COMMENT '알림 종류',
notification_type ENUM ('EMAIL') NOT NULL COMMENT '알림 유형',
notification_status ENUM ('CREATED', 'COMPLETED', 'FAILED') NOT NULL COMMENT '알림 상태',
target_type ENUM ('EMAIL') NOT NULL COMMENT '수신 유형',
target_value VARCHAR(255) NOT NULL COMMENT '수신자 정보',
service_name ENUM ('AUTH') NOT NULL COMMENT '서비스명',
service_request_id VARCHAR(255) NOT NULL COMMENT '서비스 요청 ID',
notified_at DATETIME(6) NULL COMMENT '발송 일시',
created_at DATETIME(6) NOT NULL COMMENT '생성 일시',
updated_at DATETIME(6) NOT NULL COMMENT '수정 일시',
PRIMARY KEY (uid)
) COMMENT '알림';

CREATE TABLE IF NOT EXISTS notification_histories
(
uid BIGINT NOT NULL,
notification_id BIGINT NOT NULL COMMENT '알림 PK',
uid BIGINT NOT NULL,
notification_id BIGINT NOT NULL COMMENT '알림 PK',
notification_status ENUM ('CREATED', 'COMPLETED', 'FAILED') NOT NULL COMMENT '알림 상태',
created_at datetime(6) NOT NULL COMMENT '생성 일시',
created_at datetime(6) NOT NULL COMMENT '생성 일시',
PRIMARY KEY (uid)
) COMMENT '알림 이력';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.titi.titi_user.adapter.in.web.api;
package com.titi.titi_auth.adapter.in.web.api;

import org.hibernate.validator.constraints.Length;
import org.springframework.http.MediaType;
Expand All @@ -16,22 +16,22 @@
import lombok.Builder;
import lombok.RequiredArgsConstructor;

import com.titi.titi_user.application.port.in.CheckUsernameUseCase;
import com.titi.titi_user.common.TiTiUserBusinessCodes;
import com.titi.titi_auth.application.port.in.CheckUsernameUseCase;
import com.titi.titi_auth.common.TiTiAuthBusinessCodes;

@Validated
@RestController
@RequiredArgsConstructor
class CheckUsernameController implements UserApi {
public class CheckUsernameController implements AuthApi {

private final CheckUsernameUseCase checkUsernameUseCase;

@Operation(summary = "checkUsername API", description = "Check if the username exists.")
@Parameter(name = "username", description = "Username in Email format.", required = true)
@GetMapping(value = "/members/check", produces = MediaType.APPLICATION_JSON_VALUE)
@GetMapping(value = "/accounts/check", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<CheckUsernameResponseBody> checkUsername(@NotBlank @Email @Length(max = 30) @RequestParam String username) {
final CheckUsernameUseCase.Result result = this.checkUsernameUseCase.invoke(CheckUsernameUseCase.Command.builder().username(username).build());
final TiTiUserBusinessCodes businessCodes = result.isPresent() ? TiTiUserBusinessCodes.ALREADY_EXISTS_USERNAME : TiTiUserBusinessCodes.DOES_NOT_EXIST_USERNAME;
final TiTiAuthBusinessCodes businessCodes = result.isPresent() ? TiTiAuthBusinessCodes.ALREADY_EXISTS_USERNAME : TiTiAuthBusinessCodes.DOES_NOT_EXIST_USERNAME;
return ResponseEntity.status(businessCodes.getStatus())
.body(
CheckUsernameResponseBody.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.titi.titi_auth.adapter.out.persistence;

import java.util.Optional;

import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

import com.titi.titi_auth.application.port.out.persistence.FindAccountPort;
import com.titi.titi_auth.data.jpa.entity.mapper.EntityMapper;
import com.titi.titi_auth.data.jpa.repository.AccountEntityRepository;
import com.titi.titi_auth.domain.Account;

@Component
@RequiredArgsConstructor
public class FindAccountPortAdapter implements FindAccountPort {

private final AccountEntityRepository accountEntityRepository;

@Override
public Optional<Account> invoke(Account account) {
return this.accountEntityRepository.findByEntity(EntityMapper.INSTANCE.toEntity(account))
.map(EntityMapper.INSTANCE::toDomain);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.titi.titi_auth.adapter.out.persistence;

import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

import com.titi.titi_auth.application.port.out.persistence.SaveAccountPort;
import com.titi.titi_auth.data.jpa.entity.AccountEntity;
import com.titi.titi_auth.data.jpa.entity.mapper.EntityMapper;
import com.titi.titi_auth.data.jpa.repository.AccountEntityRepository;
import com.titi.titi_auth.domain.Account;

@Component
@RequiredArgsConstructor
public class SaveAccountPortAdapter implements SaveAccountPort {

private final AccountEntityRepository accountEntityRepository;

@Override
public Account invoke(Account account) {
if (account.id() != null) {
throw new IllegalArgumentException("account.id() must be null.");
}
final AccountEntity accountEntity = accountEntityRepository.save(EntityMapper.INSTANCE.toEntity(account));
return EntityMapper.INSTANCE.toDomain(accountEntity);
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.titi.titi_user.application.port.in;
package com.titi.titi_auth.application.port.in;

import lombok.Builder;

Expand All @@ -20,4 +20,4 @@ record Result(

}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.titi.titi_auth.application.port.in;

import lombok.Builder;

public interface CreateAccountUseCase {

Result invoke(Command command);

@Builder
record Command(
String username,
String encodedEncryptedPassword
) {

}

@Builder
record Result(
Long accountId
) {

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.titi.titi_auth.application.port.out.persistence;

import java.util.Optional;

import com.titi.titi_auth.domain.Account;

public interface FindAccountPort {

Optional<Account> invoke(Account account);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.titi.titi_auth.application.port.out.persistence;

import com.titi.titi_auth.domain.Account;

public interface SaveAccountPort {

Account invoke(Account account);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.titi.titi_auth.application.service;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

import com.titi.titi_auth.application.port.in.CheckUsernameUseCase;
import com.titi.titi_auth.application.port.out.persistence.FindAccountPort;
import com.titi.titi_auth.domain.Account;

@Service
@RequiredArgsConstructor
public class CheckUsernameService implements CheckUsernameUseCase {

private final FindAccountPort findAccountPort;

@Override
public Result invoke(Command command) {
final Account account = Account.builder().username(command.username()).build();
return CheckUsernameUseCase.Result.builder()
.isPresent(this.findAccountPort.invoke(account).isPresent())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.titi.titi_auth.application.service;

import java.nio.charset.StandardCharsets;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

import com.titi.titi_auth.application.port.in.CreateAccountUseCase;
import com.titi.titi_auth.application.port.out.persistence.FindAccountPort;
import com.titi.titi_auth.application.port.out.persistence.SaveAccountPort;
import com.titi.titi_auth.common.TiTiAuthBusinessCodes;
import com.titi.titi_auth.common.TiTiAuthException;
import com.titi.titi_auth.domain.Account;
import com.titi.titi_auth.domain.AccountStatus;
import com.titi.titi_auth.domain.Authority;
import com.titi.titi_auth.domain.EncodedEncryptedPassword;

@Service
@RequiredArgsConstructor
public class CreateAccountService implements CreateAccountUseCase {

private final PasswordEncoder passwordEncoder;
private final FindAccountPort findAccountPort;
private final SaveAccountPort saveAccountPort;
@Value("${crypto.secret-key}")
private String secretKey;

@Override
public Result invoke(Command command) {
this.validateUsername(command.username());
final String rawPassword = EncodedEncryptedPassword.builder()
.value(command.encodedEncryptedPassword())
.build()
.getRawPassword(this.secretKey.getBytes(StandardCharsets.UTF_8));
final String encodedEncryptedPassword = this.passwordEncoder.encode(rawPassword);
final Account account = this.saveAccountPort.invoke(
Account.builder()
.username(command.username())
.encodedEncryptedPassword(encodedEncryptedPassword)
.accountStatus(AccountStatus.ACTIVATED)
.authority(Authority.MEMBER)
.build()
);
return Result.builder()
.accountId(account.id())
.build();
}

private void validateUsername(String username) {
final Account account = Account.builder().username(username).build();
if (this.findAccountPort.invoke(account).isPresent()) {
throw new TiTiAuthException(TiTiAuthBusinessCodes.UNAVAILABLE_USERNAME);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ public enum TiTiAuthBusinessCodes {
MISMATCHED_AUTH_CODE(200, "AU1003", "The authentication code does not match."),
REISSUE_ACCESS_TOKEN_SUCCESS(200, "AU1004", "Successfully reissued the Access Token."),
REISSUE_ACCESS_TOKEN_FAILURE_INVALID_REFRESH_TOKEN(200, "AU1005", "The Refresh Token is invalid, so the reissuance of the Access Token has failed."),
DOES_NOT_EXIST_USERNAME(200, "AU1006", "The username does not exist."),
ALREADY_EXISTS_USERNAME(200, "AU1007", "The username already exists."),

GENERATE_AUTH_CODE_FAILURE(500, "AU7000", "Failed to generate and transmit the authentication code. Please try again later."),
VERIFY_AUTH_CODE_FAILURE(500, "AU7001", "Authentication code verification failed. Please try again later."),
UNAVAILABLE_USERNAME(400, "AU7002", "The username is unavailable."),
CACHE_SERVER_ERROR(500, "AU9000", "Cache server error. Please try again later"),
;

Expand Down
Loading