Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
8 changes: 6 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-session-jdbc'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-starter-session-jdbc-test'
testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-security'
}

tasks.named('test') {
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/com/example/user/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.user.controller;

import com.example.user.dto.request.SignUpRequest;
import com.example.user.dto.request.UserUpdateRequest;
import com.example.user.dto.response.SignUpResponse;
import com.example.user.dto.response.UserSearchResponse;
import com.example.user.global.dto.ApiResponse;
import com.example.user.global.security.UserPrincipal;
import com.example.user.global.util.HttpUtil;
import com.example.user.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

private final UserService userService;

@PostMapping("/signup")
public ResponseEntity<ApiResponse<SignUpResponse>> signup(
@Valid @RequestBody SignUpRequest request
) {
SignUpResponse response = userService.signup(request);

ApiResponse<SignUpResponse> apiResponse = HttpUtil.success("create user", response);

return ResponseEntity.status(201).body(apiResponse);
}

@GetMapping("/me")
public ResponseEntity<ApiResponse<UserSearchResponse>> getMyProfile(
@AuthenticationPrincipal UserPrincipal principal
) {
UserSearchResponse response = userService.getMyProfile(principal.getUserId());

ApiResponse<UserSearchResponse> apiResponse = HttpUtil.success("select my profile", response);

return ResponseEntity.ok(apiResponse);
}

@PatchMapping("/me")
public ResponseEntity<Void> update(
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody UserUpdateRequest request
) {
userService.update(principal.getUserId(), request);

return ResponseEntity.noContent().build();
}

@DeleteMapping("/me")
public ResponseEntity<Void> delete(
@AuthenticationPrincipal UserPrincipal principal
) {
userService.delete(principal.getUserId());

return ResponseEntity.noContent().build();
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/example/user/dto/request/SignUpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.user.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record SignUpRequest(
@NotBlank
@Email
String email,

@NotBlank
String password,

@NotBlank
String name
) {
}
11 changes: 11 additions & 0 deletions src/main/java/com/example/user/dto/request/UserUpdateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.user.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;

public record UserUpdateRequest(
String name,

@JsonProperty("image_url")
String imageUrl
) {
}
13 changes: 13 additions & 0 deletions src/main/java/com/example/user/dto/response/SignUpResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.user.dto.response;

import com.example.user.entity.User;
import com.fasterxml.jackson.annotation.JsonProperty;

public record SignUpResponse(
@JsonProperty("user_id")
String userId
) {
public static SignUpResponse from(User user) {
return new SignUpResponse(user.getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.example.user.dto.response;

import com.example.user.entity.User;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.time.LocalDateTime;

public record UserSearchResponse(
@JsonProperty("image_url")
String imageUrl,

String email,

String name,

@JsonProperty("created_at")
LocalDateTime createdAt
) {
public static UserSearchResponse from(User user) {
return new UserSearchResponse(
user.getImageUrl(),
user.getEmail(),
user.getName(),
user.getCreatedAt()
);
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/example/user/entity/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.user.entity;

public enum Role {
USER,
ADMIN
}
77 changes: 77 additions & 0 deletions src/main/java/com/example/user/entity/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.example.user.entity;

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
@Getter
@Table(name = "users")
public class User {
@Id
private String id;

@Column(nullable = false, unique = true)
private String email;

private String password;

@Column(nullable = false)
private String name;

@Column(name = "image_url")
private String imageUrl;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;

@Column(name = "auth_provider")
private String authProvider;

@Column(name = "provider_id")
private String providerId;

@CreationTimestamp
@Column(nullable = false, name = "created_at")
private LocalDateTime createdAt;

@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;

@Column(name = "deleted_at")
private LocalDateTime deletedAt;

public static User create(String email, String password, String name) {
return User.builder()
.id(UUID.randomUUID().toString())
.email(email)
.password(password)
.name(name)
.role(Role.USER)
.build();
}

public void update(String name, String imageUrl) {
if (this.deletedAt != null) {
throw new IllegalStateException("삭제된 유저");
}
if (name != null) this.name = name;
if (imageUrl != null) this.imageUrl = imageUrl;
}

public void delete() {
if (this.deletedAt != null) {
throw new IllegalStateException("삭제된 유저");
}
this.deletedAt = LocalDateTime.now();
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/example/user/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.user.global.config;

import com.example.user.global.security.JwtFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtFilter jwtFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/users/signup").permitAll()
.requestMatchers("/users/me").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/example/user/global/dto/ApiResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.user.global.dto;

public record ApiResponse<T>(
String message,
T data
) {
}
55 changes: 55 additions & 0 deletions src/main/java/com/example/user/global/security/JwtFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.example.user.global.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;

@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

private final JwtProvider jwtProvider;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = request.getHeader("Authorization");

if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);

try {
if (jwtProvider.validateToken(token)) {
String userId = jwtProvider.getUserId(token);

UserPrincipal principal = new UserPrincipal(userId);

Authentication authentication =
new UsernamePasswordAuthenticationToken(
principal,
null,
Collections.emptyList()
);

SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
SecurityContextHolder.clearContext();
}
}

filterChain.doFilter(request, response);
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/example/user/global/security/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.user.global.security;


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
public class JwtProvider {

private final SecretKey secretKey;

public JwtProvider(@Value("${jwt.secret}") String secret) {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}

public String getUserId(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();

return claims.getSubject();
}

public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);

return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
Loading