diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44a8547 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# IDE +.idea/ +*.iws +*.iml +*.ipr +.vscode/ +.eclipse/ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +logs/ + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/README.md b/README.md index a6bbd59..2b4d2bb 100644 --- a/README.md +++ b/README.md @@ -1 +1,93 @@ -# coding-agent-output-comparison \ No newline at end of file +# User Management API + +Java REST APIユーザー管理システムです。管理者がユーザー情報を参照できる機能を提供します。 + +## 機能 + +- 管理者権限でのユーザー情報取得 +- ユーザーIDによる特定ユーザー情報の取得 +- 認証機能(Basic認証) +- エラーハンドリング(存在しないユーザーの場合) + +## API仕様 + +### GET /api/users/{id} + +指定されたIDのユーザー情報を取得します。 + +**認証**: Basic認証(管理者権限必須) + +**パラメータ**: +- `id` (path): ユーザーID(数値) + +**レスポンス**: +- 成功時 (200): ユーザー情報のJSON +- ユーザーが存在しない場合 (404): Not Found +- 認証失敗 (401): Unauthorized +- 権限不足 (403): Forbidden + +**レスポンス例**: +```json +{ + "id": 1, + "name": "田中太郎", + "email": "tanaka@example.com", + "phone": "090-1234-5678", + "department": "開発部" +} +``` + +## 使用方法 + +### アプリケーションの起動 + +```bash +mvn spring-boot:run +``` + +### APIの呼び出し例 + +```bash +# 正常なケース(ユーザーID 1の情報を取得) +curl -u admin:admin123 http://localhost:8080/api/users/1 + +# 存在しないユーザー(404エラー) +curl -u admin:admin123 http://localhost:8080/api/users/999 + +# 認証なし(401エラー) +curl http://localhost:8080/api/users/1 +``` + +### 認証情報 + +- ユーザー名: `admin` +- パスワード: `admin123` +- ロール: `ADMIN` + +## テストの実行 + +```bash +mvn test +``` + +## 技術仕様 + +- Java 17 +- Spring Boot 3.1.5 +- Spring Security (Basic認証) +- Spring Data JPA +- H2データベース(インメモリ) +- Maven + +## データベース + +開発用にH2インメモリデータベースを使用しています。 +初期データとして5件のサンプルユーザーが登録されています。 + +### サンプルデータ + +1. 田中太郎 (tanaka@example.com, 開発部) +2. 佐藤花子 (sato@example.com, 営業部) +3. 鈴木一郎 (suzuki@example.com, マーケティング部) +4. 高橋美穂 (takahashi@example.com, 人事部) +5. 山田次郎 (yamada@example.com, 総務部) \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a142458 --- /dev/null +++ b/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.1.5 + + + + com.example + user-management-api + 1.0.0 + User Management API + REST API for user management with admin authentication + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/src/main/java/com/example/usermanagement/UserManagementApplication.java b/src/main/java/com/example/usermanagement/UserManagementApplication.java new file mode 100644 index 0000000..711abe2 --- /dev/null +++ b/src/main/java/com/example/usermanagement/UserManagementApplication.java @@ -0,0 +1,12 @@ +package com.example.usermanagement; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserManagementApplication { + + public static void main(String[] args) { + SpringApplication.run(UserManagementApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/usermanagement/config/SecurityConfig.java b/src/main/java/com/example/usermanagement/config/SecurityConfig.java new file mode 100644 index 0000000..2790978 --- /dev/null +++ b/src/main/java/com/example/usermanagement/config/SecurityConfig.java @@ -0,0 +1,55 @@ +package com.example.usermanagement.config; + +import org.springframework.boot.autoconfigure.security.servlet.PathRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authz -> authz + .requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/users/**")).hasRole("ADMIN") + .anyRequest().authenticated() + ) + .httpBasic(basic -> {}) + .csrf(csrf -> csrf + .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")) + ) + .headers(headers -> headers.frameOptions().disable()); + + return http.build(); + } + + @Bean + public UserDetailsService userDetailsService() { + UserDetails admin = User.builder() + .username("admin") + .password(passwordEncoder().encode("admin123")) + .roles("ADMIN") + .build(); + + return new InMemoryUserDetailsManager(admin); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/usermanagement/controller/UserController.java b/src/main/java/com/example/usermanagement/controller/UserController.java new file mode 100644 index 0000000..d085f5e --- /dev/null +++ b/src/main/java/com/example/usermanagement/controller/UserController.java @@ -0,0 +1,30 @@ +package com.example.usermanagement.controller; + +import com.example.usermanagement.model.User; +import com.example.usermanagement.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.Optional; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getUserById(@PathVariable Long id) { + Optional user = userService.getUserById(id); + + if (user.isPresent()) { + return ResponseEntity.ok(user.get()); + } else { + return ResponseEntity.notFound().build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/usermanagement/model/User.java b/src/main/java/com/example/usermanagement/model/User.java new file mode 100644 index 0000000..b1b436a --- /dev/null +++ b/src/main/java/com/example/usermanagement/model/User.java @@ -0,0 +1,76 @@ +package com.example.usermanagement.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, unique = true) + private String email; + + @Column + private String phone; + + @Column + private String department; + + // Default constructor + public User() {} + + // Constructor + public User(String name, String email, String phone, String department) { + this.name = name; + this.email = email; + this.phone = phone; + this.department = department; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/usermanagement/repository/UserRepository.java b/src/main/java/com/example/usermanagement/repository/UserRepository.java new file mode 100644 index 0000000..d37832d --- /dev/null +++ b/src/main/java/com/example/usermanagement/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.example.usermanagement.repository; + +import com.example.usermanagement.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/example/usermanagement/service/UserService.java b/src/main/java/com/example/usermanagement/service/UserService.java new file mode 100644 index 0000000..41ad62e --- /dev/null +++ b/src/main/java/com/example/usermanagement/service/UserService.java @@ -0,0 +1,19 @@ +package com.example.usermanagement.service; + +import com.example.usermanagement.model.User; +import com.example.usermanagement.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + public Optional getUserById(Long id) { + return userRepository.findById(id); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..237d0ed --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,24 @@ +# Server configuration +server.port=8080 + +# Database configuration (H2 in-memory for demo) +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# JPA configuration +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.defer-datasource-initialization=true + +# H2 Console (for development only) +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# SQL initialization +spring.sql.init.mode=always + +# Application name +spring.application.name=user-management-api \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..a023f46 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,6 @@ +INSERT INTO users (name, email, phone, department) VALUES +('田中太郎', 'tanaka@example.com', '090-1234-5678', '開発部'), +('佐藤花子', 'sato@example.com', '090-2345-6789', '営業部'), +('鈴木一郎', 'suzuki@example.com', '090-3456-7890', 'マーケティング部'), +('高橋美穂', 'takahashi@example.com', '090-4567-8901', '人事部'), +('山田次郎', 'yamada@example.com', '090-5678-9012', '総務部'); \ No newline at end of file diff --git a/src/test/java/com/example/usermanagement/controller/UserControllerTest.java b/src/test/java/com/example/usermanagement/controller/UserControllerTest.java new file mode 100644 index 0000000..b02b088 --- /dev/null +++ b/src/test/java/com/example/usermanagement/controller/UserControllerTest.java @@ -0,0 +1,77 @@ +package com.example.usermanagement.controller; + +import com.example.usermanagement.model.User; +import com.example.usermanagement.service.UserService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(UserController.class) +@Import(com.example.usermanagement.config.SecurityConfig.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @WithMockUser(roles = "ADMIN") + void getUserById_WhenUserExists_ReturnsUser() throws Exception { + // Given + User user = new User("田中太郎", "tanaka@example.com", "090-1234-5678", "開発部"); + user.setId(1L); + when(userService.getUserById(1L)).thenReturn(Optional.of(user)); + + // When & Then + mockMvc.perform(get("/api/users/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("田中太郎")) + .andExpect(jsonPath("$.email").value("tanaka@example.com")) + .andExpect(jsonPath("$.phone").value("090-1234-5678")) + .andExpect(jsonPath("$.department").value("開発部")); + } + + @Test + @WithMockUser(roles = "ADMIN") + void getUserById_WhenUserNotExists_ReturnsNotFound() throws Exception { + // Given + when(userService.getUserById(anyLong())).thenReturn(Optional.empty()); + + // When & Then + mockMvc.perform(get("/api/users/999")) + .andExpect(status().isNotFound()); + } + + @Test + void getUserById_WhenNotAuthenticated_ReturnsUnauthorized() throws Exception { + // When & Then + mockMvc.perform(get("/api/users/1")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser(roles = "USER") + void getUserById_WhenNotAdmin_ReturnsForbidden() throws Exception { + // When & Then + mockMvc.perform(get("/api/users/1")) + .andExpect(status().isForbidden()); + } +} \ No newline at end of file