Skip to content

Commit

Permalink
feat: add api of auth domain (#1)
Browse files Browse the repository at this point in the history
* feat: add api of register user

* feat: add api of login

* feat: add validation about creating user

* feat: add api of reissuing tokens

* feat: separate method of parsing token
- add subject of token claim

* feat: add api of logout
- add argument resolver of logined user

* feat: add api of checking login id duplication
- with using strategy pattern

* test: add test of api checking login id

* feat: add api of checking nickname duplication

* style: reformat code

* build: set property of yml
  • Loading branch information
aiaiaiai1 authored Jun 2, 2024
1 parent b25995a commit 55964d2
Show file tree
Hide file tree
Showing 50 changed files with 1,762 additions and 10 deletions.
36 changes: 36 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,31 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
implementation 'com.mysql:mysql-connector-j:8.3.0'

runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'



implementation 'org.mindrot:jbcrypt:0.4'

implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

testImplementation 'io.rest-assured:rest-assured:5.3.0'

}

tasks.named('test') {
useJUnitPlatform()
}

bootJar{
archiveFileName = 'dev.jar'
}
6 changes: 3 additions & 3 deletions src/main/java/gymmi/GymmiApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
@SpringBootApplication
public class GymmiApplication {

public static void main(String[] args) {
SpringApplication.run(GymmiApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(GymmiApplication.class, args);
}

}
47 changes: 47 additions & 0 deletions src/main/java/gymmi/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package gymmi.controller;

import gymmi.entity.User;
import gymmi.global.Logined;
import gymmi.request.LoginRequest;
import gymmi.request.RegistrationRequest;
import gymmi.request.ReissueRequest;
import gymmi.response.TokensResponse;
import gymmi.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@PostMapping("/auth/join")
public ResponseEntity<Void> registerUser(@Validated @RequestBody RegistrationRequest request) {
authService.registerUser(request);
return ResponseEntity.ok().build();
}

@PostMapping("/auth/welcome")
public ResponseEntity<TokensResponse> login(@Validated @RequestBody LoginRequest request) {
TokensResponse response = authService.login(request);
return ResponseEntity.ok().body(response);
}

@PostMapping("/auth/reissue")
public ResponseEntity<TokensResponse> reissue(@Validated @RequestBody ReissueRequest request) {
TokensResponse response = authService.reissue(request);
return ResponseEntity.ok().body(response);
}

@PostMapping("/auth/goodbye")
public ResponseEntity<Void> logout(@Logined User user) {
authService.logout(user);
return ResponseEntity.ok().build();
}

}
26 changes: 26 additions & 0 deletions src/main/java/gymmi/controller/DuplicationCheckController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gymmi.controller;

import gymmi.global.DuplicationCheckType;
import gymmi.response.DuplicationResponse;
import gymmi.service.DuplicationCheckService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class DuplicationCheckController {

private final DuplicationCheckService duplicationCheckService;

@GetMapping("/check-duplication")
public ResponseEntity<DuplicationResponse> checkDuplication(
@RequestParam("type") DuplicationCheckType type,
@RequestParam("value") String value
) {
DuplicationResponse response = duplicationCheckService.checkDuplication(type, value);
return ResponseEntity.ok().body(response);
}
}
4 changes: 2 additions & 2 deletions src/main/java/gymmi/controller/WorkspaceController.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ public ResponseEntity<Void> createWorkspace() {
return null;
}

@GetMapping("/workspaces")
@GetMapping("/workspaces1")
public ResponseEntity<Void> seeJoinedWorkspaces() {
return null;
}

@GetMapping("/workspaces")
@GetMapping("/workspaces2")
public ResponseEntity<Void> seeAllWorkspaces() {
return null;
}
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/gymmi/entity/Logined.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package gymmi.entity;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Logined {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@JoinColumn(name = "user_id", nullable = false, unique = true, updatable = false)
@OneToOne(fetch = FetchType.LAZY)
private User user;

@Column
private String refreshToken;

public Logined(User user) {
this.user = user;
}

public void saveRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

public void destroyRefreshToken() {
this.refreshToken = null;
}

public boolean isActivatedRefreshToken(String refreshToken) {
if (this.refreshToken == null) {
return false;
}
return this.refreshToken.equals(refreshToken);
}

}
79 changes: 79 additions & 0 deletions src/main/java/gymmi/entity/User.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
package gymmi.entity;

import gymmi.exception.InvalidPatternException;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.NoArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;

import java.util.regex.Pattern;

@Entity
@Table(name = "uuser")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

private static final String SPECIAL_CHARACTER = "~!@#$%^&*()_+<>?:";
private static final Pattern REGEX_LOGIN_ID = Pattern.compile("^[a-zA-Z0-9]+$");
private static final Pattern REGEX_NICKNAME = Pattern.compile("^[ㄱ-ㅎ가-힣a-zA-Z0-9]+$");
private static final Pattern REGEX_EMAIL = Pattern.compile("^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]*$");
private static final Pattern REGEX_PASSWORD = Pattern.compile("^[a-zA-Z0-9" + SPECIAL_CHARACTER + "]+$");

private static final Pattern REGEX_ENGLISH = Pattern.compile("[a-zA-Z]");
private static final Pattern REGEX_NUMBER = Pattern.compile("[0-9]");

private static final Pattern REGEX_SPECIAL_CHARACTER = Pattern.compile("[" + SPECIAL_CHARACTER + "]");


@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -22,4 +42,63 @@ public class User {
@Column
private String email;

@Builder
public User(String loginId, String plainPassword, String nickname, String email) {
validateLoginId(loginId);
validatePassword(plainPassword);
validateNickname(nickname);
this.loginId = loginId;
this.password = encryptPassword(plainPassword);
this.nickname = nickname;
this.email = email;
}

private void validatePassword(String plainPassword) {
if (!REGEX_PASSWORD.matcher(plainPassword).matches()) {
throw new InvalidPatternException("비밀번호는 영문+숫자+특수문자 조합으로 구성해주세요.");
}
if (!REGEX_ENGLISH.matcher(plainPassword).find()) {
throw new InvalidPatternException("영문을 포함해주세요");
}
if (!REGEX_NUMBER.matcher(plainPassword).find()) {
throw new InvalidPatternException("숫자를 포함해주세요.");
}
if (!REGEX_SPECIAL_CHARACTER.matcher(plainPassword).find()) {
throw new InvalidPatternException("특수문자를 포함해주세요.");
}
}

private String encryptPassword(String plainPassword) {
return BCrypt.hashpw(plainPassword, BCrypt.gensalt());
}


private void validateLoginId(String loginId) {
if (!REGEX_LOGIN_ID.matcher(loginId).matches()) {
throw new InvalidPatternException("아이디는 영문+숫자 조합으로 구성해주세요.");
}
if (!REGEX_ENGLISH.matcher(loginId).find()) {
throw new InvalidPatternException("영문을 포함해주세요");
}
if (!REGEX_NUMBER.matcher(loginId).find()) {
throw new InvalidPatternException("숫자를 포함해주세요.");
}
}

private void validateNickname(String nickname) {
if (!REGEX_NICKNAME.matcher(nickname).matches()) {
throw new InvalidPatternException("닉네임은 한글(초성), 영문, 숫자만 가능합니다.");
}
}

public boolean canAuthenticate(String loginId, String plainPassword) {
if (this.loginId.equals(loginId) && BCrypt.checkpw(plainPassword, password)) {
return true;
}
return false;
}

public Long getId() {
return id;
}
}
11 changes: 11 additions & 0 deletions src/main/java/gymmi/exception/AlreadyExistException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gymmi.exception;

public class AlreadyExistException extends BadRequestException {

public static final String ERROR_CODE = "ALREADY_EXIST";
public static final String ERROR_DESCRIPTION = "(어떠한 것이든) 이미 존재하는 경우";

public AlreadyExistException(String message) {
super(message, ERROR_CODE, ERROR_DESCRIPTION);
}
}
15 changes: 15 additions & 0 deletions src/main/java/gymmi/exception/AuthenticationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package gymmi.exception;

public class AuthenticationException extends BadRequestException {

public static final String ERROR_CODE = "AUTHENTICATION";
public static final String ERROR_DESCRIPTION = "인증에 실패한 경우";

public AuthenticationException(String message) {
super(message, ERROR_CODE, ERROR_DESCRIPTION);
}

public AuthenticationException(String message, Throwable cause) {
super(message, cause, ERROR_CODE, ERROR_DESCRIPTION);
}
}
29 changes: 29 additions & 0 deletions src/main/java/gymmi/exception/BadRequestException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package gymmi.exception;

public class BadRequestException extends GymmiException {

public static final String ERROR_CODE = "INVALID_REQUEST";
public static final String ERROR_DESCRIPTION = "요청이 잘못 된 경우";

public BadRequestException(String message, String errorCode, String errorDescription) {
super(message, errorCode, errorDescription);
}

public BadRequestException(String message, Throwable cause, String errorCode, String errorDescription) {
super(message, cause, errorCode, errorDescription);
}

public BadRequestException(String message) {
this(message, ERROR_CODE, ERROR_DESCRIPTION);
}

@Override
public String getErrorCode() {
return super.getErrorCode();
}

@Override
public String getErrorDescription() {
return super.getErrorDescription();
}
}
16 changes: 16 additions & 0 deletions src/main/java/gymmi/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gymmi.exception;

import lombok.Getter;

@Getter
public class ErrorResponse {

private final String errorCode;
private final String message;

public ErrorResponse(String errorCode, String message) {
this.errorCode = errorCode;
this.message = message;
}
}

Loading

0 comments on commit 55964d2

Please sign in to comment.