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

feat : 어드민 가입 기능 구현 #29

Merged
merged 9 commits into from
Jul 16, 2024
1 change: 1 addition & 0 deletions app/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ allprojects {
// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

implementation "org.springframework.boot:spring-boot-starter-web"
implementation 'org.springframework.boot:spring-boot-starter-validation'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package org.example.config;

import org.example.property.TokenProperty;
import org.example.property.UserAdminProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(TokenProperty.class)
@EnableConfigurationProperties({TokenProperty.class, UserAdminProperty.class})
@ComponentScan(basePackages = "org.example")
public class CommonApiConfig {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
Expand All @@ -29,11 +33,21 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(corsConfigurer -> corsConfigurer.configurationSource(
corsConfigurationSource()))
.formLogin(AbstractHttpConfigurer::disable)
corsConfigurationSource())
)
.formLogin(
(formLogin) -> formLogin
.loginPage("/admin/login")
.defaultSuccessUrl("/admin/home")
)
.logout((logout) -> logout
.logoutUrl("/admin/logout")
.logoutSuccessUrl("/admin/home")
.invalidateHttpSession(true)
)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(
configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
devmizz marked this conversation as resolved.
Show resolved Hide resolved
)
.authorizeHttpRequests(registry ->
registry
Expand Down Expand Up @@ -83,4 +97,15 @@ private CorsConfigurationSource corsConfigurationSource() {
return config;
};
}

@Bean
AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.example.property;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "admin")
public record UserAdminProperty(
String password
devmizz marked this conversation as resolved.
Show resolved Hide resolved
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.example.service;

import lombok.RequiredArgsConstructor;
import org.example.entity.User;
import org.example.property.UserAdminProperty;
import org.example.usecase.UserAdminUseCase;
import org.example.vo.UserRoleApiType;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserAdminSecurityService implements UserDetailsService {

private final UserAdminUseCase userAdminUseCase;
private final UserAdminProperty userAdminProperty;
private final PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User admin = userAdminUseCase.findByUsername(username);
admin.validateUserRole();

return new org.springframework.security.core.userdetails.User(
admin.getNickname(),
passwordEncoder.encode(userAdminProperty.password()),
UserRoleApiType.ADMIN.getAdminAuthority()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.example.vo;

import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

@Getter
public enum UserRoleApiType {
Expand All @@ -17,4 +21,11 @@ public enum UserRoleApiType {
public static UserRoleApiType from(UserRole userRole) {
return UserRoleApiType.valueOf(userRole.name());
}

public List<GrantedAuthority> getAdminAuthority() {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(ADMIN.authority));

return authorities;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/admin/home")
@RequestMapping("/admin")
@RequiredArgsConstructor
public class UserAdminController {

@GetMapping
@GetMapping("/home")
public String home() {
return "home";
}

@GetMapping("/login")
public String login() {
return "login_form";
}

}
11 changes: 6 additions & 5 deletions app/api/user-api/src/main/resources/templates/home.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<html lang="ko" layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<html lang="ko" layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<div layout:fragment="content" class="container my-3">
<div class="row">
<div class="col-md-6">
<div class="card text-center h-100">
<div class="card-body">
<h5 class="card-title">아티스트 추가</h5>
<p class="card-text">새로운 아티스트를 추가합니다.</p>
<a href="/admin/artists" class="btn btn-primary">추가하기</a>
<a sec:authorize="isAuthenticated()" href="/admin/artists" class="btn btn-primary">추가하기</a>
</div>
</div>
</div>
Expand All @@ -15,7 +16,7 @@ <h5 class="card-title">아티스트 추가</h5>
<div class="card-body">
<h5 class="card-title">아티스트 조회</h5>
<p class="card-text">등록된 아티스트를 조회합니다.</p>
<a href="/admin/artists/list" class="btn btn-primary">조회하기</a>
<a sec:authorize="isAuthenticated()" href="/admin/artists/list" class="btn btn-primary">조회하기</a>
</div>
</div>
</div>
Expand All @@ -26,7 +27,7 @@ <h5 class="card-title">아티스트 조회</h5>
<div class="card-body">
<h5 class="card-title">장르 추가</h5>
<p class="card-text">새로운 장르를 추가합니다.</p>
<a href="/admin/genres" class="btn btn-primary">추가하기</a>
<a sec:authorize="isAuthenticated()" href="/admin/genres" class="btn btn-primary">추가하기</a>
</div>
</div>
</div>
Expand All @@ -35,7 +36,7 @@ <h5 class="card-title">장르 추가</h5>
<div class="card-body">
<h5 class="card-title">장르 조회</h5>
<p class="card-text">등록된 장르를 조회합니다.</p>
<a href="/admin/genres/list" class="btn btn-primary">조회하기</a>
<a sec:authorize="isAuthenticated()" href="/admin/genres/list" class="btn btn-primary">조회하기</a>
</div>
</div>
</div>
Expand Down
20 changes: 20 additions & 0 deletions app/api/user-api/src/main/resources/templates/login_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<html lang="ko" layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<div layout:fragment="content" class="container my-3">
<form th:action="@{/admin/login}" method="post">
<div th:if="${param.error}">
<div class="alert alert-danger">
사용자ID 또는 비밀번호를 확인해 주세요.
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">어드민 ID</label>
<input type="text" name="username" id="username" class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호</label>
<input type="password" name="password" id="password" class="form-control">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
</html>
7 changes: 5 additions & 2 deletions app/api/user-api/src/main/resources/templates/navbar.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<html lang="ko" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="/admin/home">Show Pot Admin</a>
Expand All @@ -8,9 +9,11 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#">로그인</a>
<a class="nav-link" sec:authorize="isAnonymous()" th:href="@{/admin/login}">로그인</a>
<a class="nav-link" sec:authorize="isAuthenticated()" th:href="@{/admin/logout}">로그아웃</a>
</li>
</ul>
</div>
</div>
</nav>
</nav>
</html>
49 changes: 49 additions & 0 deletions app/domain/user-domain/src/main/java/org/error/UserAdminError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.error;

import org.example.exception.BusinessError;

public enum UserAdminError implements BusinessError {
ENTITY_NOT_FOUND_ERROR {
@Override
public int getHttpStatus() {
return 404;
}

@Override
public String getErrorCode() {
return "UAM-001";
}

@Override
public String getClientMessage() {
return "존재하지 않은 어드민입니다.";
}

@Override
public String getLogMessage() {
return "요청 값이 잘못 처리되었습니다.";
devmizz marked this conversation as resolved.
Show resolved Hide resolved
}
},
ADMIN_NOT_AUTHORITY_ERROR {
@Override
public int getHttpStatus() {
return 403;
}

@Override
public String getErrorCode() {
return "UAM-002";
}

@Override
public String getClientMessage() {
return "권한이 없는 사용자입니다.";
}

@Override
public String getLogMessage() {
return "요청 값이 잘못 처리되었습니다.";
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.error.UserAdminError;
import org.example.entity.credential.SocialLoginCredential;
import org.example.exception.BusinessException;
import org.example.vo.UserGender;
import org.example.vo.UserRole;

Expand Down Expand Up @@ -48,4 +50,10 @@ private User(
this.socialLoginCredential = socialLoginCredential;
this.userRole = UserRole.USER;
}

public void validateUserRole() {
devmizz marked this conversation as resolved.
Show resolved Hide resolved
if (!this.userRole.equals(UserRole.ADMIN)) {
throw new BusinessException(UserAdminError.ADMIN_NOT_AUTHORITY_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.example.repository.user;

import java.util.Optional;
import java.util.UUID;
import org.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, UUID>, UserQuerydslRepository {

Optional<User> findByNickname(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.example.usecase;

import lombok.RequiredArgsConstructor;
import org.error.UserAdminError;
import org.example.entity.User;
import org.example.exception.BusinessException;
import org.example.repository.user.UserRepository;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class UserAdminUseCase {

private final UserRepository userRepository;

public User findByUsername(String username) {
return userRepository.findByNickname(username)
.orElseThrow(() -> new BusinessException(UserAdminError.ENTITY_NOT_FOUND_ERROR));
}
}
3 changes: 3 additions & 0 deletions app/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ token:
secret-key: ahRhwlglftmrkwkfehlaussksmsdjraksrmadmfqjfrjtdlrhdkwnwlflsmswlqdptjgodqhrgkrptkftndlTDmfrjtdlek
access-token-expiration-seconds: 3600000 # 1hour = 1000(=1s) * 60 * 60
refresh-token-expiration-seconds: 1209600000 # 2weeks = 1000(=1s) * 60 * 60 * 24 * 14

admin:
password: showPot
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
- 'POSTGRES_USER=local'
- 'POSTGRES_PASSWORD=local'
ports:
- '5432'
- '5432:5432'
restart: always

redis:
Expand Down