diff --git a/docs/spring_boot_coding_test.postman_collection.json b/docs/spring_boot_coding_test.postman_collection.json new file mode 100644 index 0000000..4e64425 --- /dev/null +++ b/docs/spring_boot_coding_test.postman_collection.json @@ -0,0 +1,454 @@ +{ + "info": { + "_postman_id": "88a0d749-6f0e-4000-abc9-4a87de218946", + "name": "spring_boot_coding_test", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "27536794" + }, + "item": [ + { + "name": "Users", + "item": [ + { + "name": "GetAllUsers", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "users" + ] + } + }, + "response": [] + }, + { + "name": "CreateUser", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "29207796-400d-4653-a5ca-5e5b5f8f6540", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"username\": \"Ram\",\r\n \"password\": \"abcd\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/api/v1/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "users" + ] + } + }, + "response": [] + }, + { + "name": "UpdateUserById", + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8080/api/v1/users/0f2f0a79-0640-4cca-8b07-e683c70e33fc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "users", + "0f2f0a79-0640-4cca-8b07-e683c70e33fc" + ] + } + }, + "response": [] + }, + { + "name": "FetchUserById", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/users/192a2472-9d91-4016-99fb-9b0882d916b3", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "users", + "192a2472-9d91-4016-99fb-9b0882d916b3" + ] + } + }, + "response": [] + }, + { + "name": "PatchUserById", + "request": { + "method": "PATCH", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/users/192a2472-9d91-4016-99fb-9b0882d916b3", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "users", + "192a2472-9d91-4016-99fb-9b0882d916b3" + ] + } + }, + "response": [] + }, + { + "name": "DeleteUser", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/users/192a2472-9d91-4016-99fb-9b0882d916b3", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "users", + "192a2472-9d91-4016-99fb-9b0882d916b3" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Projects", + "item": [ + { + "name": "GetAllProjects", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/projects", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "projects" + ] + } + }, + "response": [] + }, + { + "name": "CreateProject", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "0e9a0d44-f14c-4515-ba03-449b0223dcce", + "type": "string" + }, + { + "key": "username", + "value": "product_owner", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\":\"Project1\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/api/v1/projects", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "projects" + ] + } + }, + "response": [] + }, + { + "name": "UpdateOrPatchProjectById", + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8080/api/v1/projects/0f2f0a79-0640-4cca-8b07-e683c70e33fc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "projects", + "0f2f0a79-0640-4cca-8b07-e683c70e33fc" + ] + } + }, + "response": [] + }, + { + "name": "FetchProjectById", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/projects/192a2472-9d91-4016-99fb-9b0882d916b3", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "projects", + "192a2472-9d91-4016-99fb-9b0882d916b3" + ] + } + }, + "response": [] + }, + { + "name": "DeleteProject", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/projects/192a2472-9d91-4016-99fb-9b0882d916b3", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "projects", + "192a2472-9d91-4016-99fb-9b0882d916b3" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Tasks", + "item": [ + { + "name": "GetAllTasks", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/tasks", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "tasks" + ] + } + }, + "response": [] + }, + { + "name": "CreateTask", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "0e9a0d44-f14c-4515-ba03-449b0223dcce", + "type": "string" + }, + { + "key": "username", + "value": "product_owner", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"title\" : \"Eating\",\r\n \"description\" : \"Eating with Teeth\",\r\n \"status\" : \"Work In Progress\",\r\n \"project\":{\r\n \"id\" : \"e8c57a39-fe41-4523-977c-2b97e21e936c\"\r\n },\r\n \"user\" : {\r\n \"id\" : \"e960b50f-1f95-4dcf-b8f6-1522dcf051bb\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/api/v1/tasks", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "tasks" + ] + } + }, + "response": [] + }, + { + "name": "UpdateTaskById", + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8080/api/v1/tasks/0f2f0a79-0640-4cca-8b07-e683c70e33fc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "tasks", + "0f2f0a79-0640-4cca-8b07-e683c70e33fc" + ] + } + }, + "response": [] + }, + { + "name": "FetchTaskById", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/tasks/192a2472-9d91-4016-99fb-9b0882d916b3", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "tasks", + "192a2472-9d91-4016-99fb-9b0882d916b3" + ] + } + }, + "response": [] + }, + { + "name": "PatchTaskById", + "request": { + "method": "PATCH", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/tasks/192a2472-9d91-4016-99fb-9b0882d916b3", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "tasks", + "192a2472-9d91-4016-99fb-9b0882d916b3" + ] + } + }, + "response": [] + }, + { + "name": "DeleteTask", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "localhost:8080/api/v1/tasks/192a2472-9d91-4016-99fb-9b0882d916b3", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "api", + "v1", + "tasks", + "192a2472-9d91-4016-99fb-9b0882d916b3" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index b0a6389..bfb1ae6 100644 --- a/pom.xml +++ b/pom.xml @@ -21,13 +21,37 @@ org.springframework.boot spring-boot-starter - + + org.springframework.boot + spring-boot-starter-web + org.springframework.boot spring-boot-starter-test test - + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + org.springframework.boot + spring-boot-starter-security + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + @@ -35,7 +59,15 @@ org.springframework.boot spring-boot-maven-plugin - + + org.apache.maven.plugins + maven-compiler-plugin + + 15 + 15 + + + diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/SpringBootCodingTestApplication.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/SpringBootCodingTestApplication.java index 14e2bbf..e79cc63 100644 --- a/src/main/java/com/accenture/codingtest/springbootcodingtest/SpringBootCodingTestApplication.java +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/SpringBootCodingTestApplication.java @@ -1,13 +1,23 @@ package com.accenture.codingtest.springbootcodingtest; +import com.accenture.codingtest.springbootcodingtest.entity.Project; +import com.accenture.codingtest.springbootcodingtest.service.ProjectService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + @SpringBootApplication public class SpringBootCodingTestApplication { - public static void main(String[] args) { - SpringApplication.run(SpringBootCodingTestApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(SpringBootCodingTestApplication.class, args); + + + + } } diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/config/SecurityConfig.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/config/SecurityConfig.java new file mode 100644 index 0000000..a1679eb --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/config/SecurityConfig.java @@ -0,0 +1,72 @@ +package com.accenture.codingtest.springbootcodingtest.config; + +import com.accenture.codingtest.springbootcodingtest.model.Role; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final UserDetailsService userDetailsService; + + public SecurityConfig(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .antMatchers(HttpMethod.GET, "/api/v1/users").hasRole(Role.ADMIN.name()) + .antMatchers(HttpMethod.GET, "/api/v1/users/{user_id}").hasRole(Role.ADMIN.name()) + .antMatchers(HttpMethod.POST, "/api/v1/users").hasRole(Role.ADMIN.name()) + .antMatchers(HttpMethod.PUT, "/api/v1/users/{user_id}").hasRole(Role.ADMIN.name()) + .antMatchers(HttpMethod.PATCH, "/api/v1/users/{user_id}").hasRole(Role.ADMIN.name()) + .antMatchers(HttpMethod.DELETE, "/api/v1/users/{user_id}").hasRole(Role.ADMIN.name()) + + .antMatchers(HttpMethod.GET, "/api/v1/projects").hasAnyRole(Role.ADMIN.name(),Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.GET, "/api/v1/projects/{project_id}").hasAnyRole(Role.ADMIN.name(),Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.POST, "/api/v1/projects").hasRole(Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.PUT, "/api/v1/projects/{project_id}").hasAnyRole(Role.ADMIN.name(),Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.DELETE, "/api/v1/projects/{project_id}").hasRole(Role.ADMIN.name()) + + .antMatchers(HttpMethod.GET, "/api/v1/tasks").hasAnyRole(Role.ADMIN.name(),Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.GET, "/api/v1/tasks/{task_id}").hasAnyRole(Role.ADMIN.name(),Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.POST, "/api/v1/tasks").hasRole(Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.PUT, "/api/v1/tasks/{task_id}").hasRole(Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.PATCH, "/api/v1/tasks/{task_id}").hasRole(Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.DELETE, "/api/v1/tasks/{task_id}").hasAnyRole(Role.ADMIN.name(),Role.PROJECT_OWNER.name()) + .antMatchers(HttpMethod.PATCH, "/api/v1/tasks/{task_id}/status").hasRole(Role.TEAM_MEMBER.name()) + + .anyRequest().authenticated() + .and() + .httpBasic(); + } + + + @Override + public void configure(WebSecurity web) throws Exception { + web + .ignoring() + .antMatchers("/h2-console/**"); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/config/UserDetailsServiceImpl.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/config/UserDetailsServiceImpl.java new file mode 100644 index 0000000..4a2f595 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/config/UserDetailsServiceImpl.java @@ -0,0 +1,44 @@ +package com.accenture.codingtest.springbootcodingtest.config; + +import com.accenture.codingtest.springbootcodingtest.entity.User; +import com.accenture.codingtest.springbootcodingtest.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + private final UserRepository userRepository; + + @Autowired + public UserDetailsServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + + Set authorities = user.getRoles().stream() + .map(role -> new SimpleGrantedAuthority(role.getAuthority())) + .collect(Collectors.toSet()); + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), + authorities + ); + } + +} + + diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/ProjectController.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/ProjectController.java new file mode 100644 index 0000000..e74b2c2 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/ProjectController.java @@ -0,0 +1,68 @@ +package com.accenture.codingtest.springbootcodingtest.controller; + +import com.accenture.codingtest.springbootcodingtest.entity.Project; +import com.accenture.codingtest.springbootcodingtest.service.ProjectService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping(value = "api/v1/projects") +public class ProjectController { + + private final ProjectService projectService; + + @Autowired + public ProjectController(ProjectService projectService) { + this.projectService = projectService; + } + + + @GetMapping + public ResponseEntity> getAllProjects( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "5") int size, + @RequestParam(defaultValue = "name") String sortBy + ) { + Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy)); + Page projects = projectService.getAllProjects(pageable); + return ResponseEntity.ok(projects); + } + + + @GetMapping("/{project_id}") + public ResponseEntity getProjectById (@PathVariable("project_id") UUID projectId){ + Project project = projectService.findById(projectId); + return ResponseEntity.ok(project); + } + + @PostMapping + public ResponseEntity createProject (@RequestBody Project project){ + Project savedProject = projectService.save(project); + return ResponseEntity.status(HttpStatus.CREATED).body(savedProject); + } + + @PutMapping("/{project_id}") + public ResponseEntity updateProjectById (@PathVariable("project_id") UUID projectId, + @RequestBody Project project){ + Project updated = projectService.updateProject(projectId, project); + + return ResponseEntity.ok(updated); + } + + @DeleteMapping("/{project_id}") + public ResponseEntity deleteProject (@PathVariable("project_id") UUID projectId){ + projectService.delete(projectId); + return ResponseEntity.noContent().build(); + } + + + } diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/TaskController.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/TaskController.java new file mode 100644 index 0000000..dd082ce --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/TaskController.java @@ -0,0 +1,78 @@ +package com.accenture.codingtest.springbootcodingtest.controller; + +import com.accenture.codingtest.springbootcodingtest.entity.Task; +import com.accenture.codingtest.springbootcodingtest.model.TaskStatus; +import com.accenture.codingtest.springbootcodingtest.service.TaskService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping(value = "/api/v1/tasks") +public class TaskController { + private final TaskService taskService; + + @Autowired + public TaskController(TaskService taskService) { + this.taskService = taskService; + } + + @GetMapping + public ResponseEntity> getAllTasks() { + List taskList = taskService.findAll(); + return ResponseEntity.ok(taskList); + } + + @GetMapping("/{task_id}") + public ResponseEntity getTaskById(@PathVariable("task_id") UUID taskId) { + Task task = taskService.findById(taskId); + return ResponseEntity.ok(task); + } + + @PostMapping + public ResponseEntity createTask(@RequestBody Task task) { + task.setStatus(TaskStatus.NOT_STARTED); + Task savedTask = taskService.save(task); + return ResponseEntity.status(HttpStatus.CREATED).body(savedTask); + } + + @PutMapping("/{task_id}") + public ResponseEntity updateTask(@PathVariable("task_id") UUID taskId, + @RequestBody Task updatedTask) { + Task task = taskService.updateTask(taskId,updatedTask); + return ResponseEntity.ok(task); + } + + + @PatchMapping("/{task_id}") + public ResponseEntity patchTask(@PathVariable("task_id") UUID taskId, + @RequestBody Task updatedTask) { + Task task = taskService.patchTask(taskId,updatedTask); + + return ResponseEntity.ok(task); + } + + @DeleteMapping("/{task_id}") + public ResponseEntity deleteTask(@PathVariable("task_id") UUID taskId) { + + taskService.delete(taskId); + return ResponseEntity.noContent().build(); + } + + + @PatchMapping("/{task_id}/status") + public ResponseEntity updateTaskStatus(@PathVariable("task_id") UUID taskId, + @RequestParam("status") TaskStatus status) { + Task task = taskService.updateTaskStatus(taskId,status); + return ResponseEntity.ok(task); + } + + + + +} + diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/UserController.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/UserController.java new file mode 100644 index 0000000..c213d7a --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/controller/UserController.java @@ -0,0 +1,65 @@ +package com.accenture.codingtest.springbootcodingtest.controller; + + +import com.accenture.codingtest.springbootcodingtest.entity.User; +import com.accenture.codingtest.springbootcodingtest.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/users") +public class UserController { + + private final UserService userService; + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public ResponseEntity> getAllUsers(){ + List userList = userService.findAll(); + return ResponseEntity.ok(userList); + } + + @GetMapping("/{user_id}") + public ResponseEntity getUserById(@PathVariable("user_id") UUID userId) { + User user = userService.findById(userId); + return ResponseEntity.ok(user); + } + + @PostMapping + public ResponseEntity createUser(@RequestBody User user){ + User savedUser = userService.save(user); + return ResponseEntity.status(HttpStatus.CREATED).body(savedUser); + } + + @PutMapping("/{user_id}") + public ResponseEntity updateUser(@PathVariable("user_id") UUID userId, + @RequestBody User user){ + + User updatedUser = userService.updateUser(userId,user); + return ResponseEntity.ok(updatedUser); + } + + @PatchMapping("/{user_id}") + public ResponseEntity patchUser(@PathVariable("user_id") UUID userId, + @RequestBody User user){ + User patchedUser = userService.patchUser(userId,user); + return ResponseEntity.ok(patchedUser); + + } + + @DeleteMapping("/{user_id}") + public ResponseEntity deleteUser(@PathVariable("user_id") UUID userId){ + userService.delete(userId); + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/dataInitializer/ProjectInitializer.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/dataInitializer/ProjectInitializer.java new file mode 100644 index 0000000..46cc19d --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/dataInitializer/ProjectInitializer.java @@ -0,0 +1,83 @@ +package com.accenture.codingtest.springbootcodingtest.dataInitializer; + +import com.accenture.codingtest.springbootcodingtest.entity.Project; +import com.accenture.codingtest.springbootcodingtest.repository.ProjectRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class ProjectInitializer implements CommandLineRunner { + private final ProjectRepository projectRepository; + + @Autowired + public ProjectInitializer(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + @Override + public void run(String... args) { + + Project project1 = new Project(); + project1.setName("project1"); + projectRepository.save(project1); + + Project project2 = new Project(); + project2.setName("project2"); + projectRepository.save(project2); + + Project project3 = new Project(); + project3.setName("project3"); + projectRepository.save(project3); + + Project project4 = new Project(); + project4.setName("project4"); + projectRepository.save(project4); + + Project project5 = new Project(); + project5.setName("project5"); + projectRepository.save(project5); + + Project project6 = new Project(); + project6.setName("project6"); + projectRepository.save(project6); + + Project project7 = new Project(); + project7.setName("project7"); + projectRepository.save(project7); + + Project project8 = new Project(); + project8.setName("project8"); + projectRepository.save(project8); + + Project project9 = new Project(); + project9.setName("project9"); + projectRepository.save(project9); + + Project project10 = new Project(); + project10.setName("project10"); + projectRepository.save(project10); + + Project project11 = new Project(); + project11.setName("project11"); + projectRepository.save(project11); + + Project project12 = new Project(); + project12.setName("project12"); + projectRepository.save(project12); + + Project project13 = new Project(); + project13.setName("project13"); + projectRepository.save(project13); + + Project project14 = new Project(); + project14.setName("project14"); + projectRepository.save(project14); + + Project project15 = new Project(); + project15.setName("project15"); + projectRepository.save(project15); + + + } +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/dataInitializer/UserInitializer.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/dataInitializer/UserInitializer.java new file mode 100644 index 0000000..3650441 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/dataInitializer/UserInitializer.java @@ -0,0 +1,46 @@ +package com.accenture.codingtest.springbootcodingtest.dataInitializer; + +import com.accenture.codingtest.springbootcodingtest.entity.User; +import com.accenture.codingtest.springbootcodingtest.model.Role; +import com.accenture.codingtest.springbootcodingtest.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class UserInitializer implements CommandLineRunner { + + private final UserRepository userRepository; + + @Autowired + public UserInitializer(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public void run(String... args) throws Exception { + User user1 = new User(); + user1.setUsername("projectOwner"); + user1.setPassword(passwordEncoder().encode("1234")); + user1.getRoles().add(Role.PROJECT_OWNER); + userRepository.save(user1); + + User user2 = new User(); + user2.setUsername("admin"); + user2.setPassword(passwordEncoder().encode("1234")); + user2.getRoles().add(Role.ADMIN); + userRepository.save(user2); + + User user3 = new User(); + user3.setUsername("teamMember"); + user3.setPassword(passwordEncoder().encode("1234")); + user3.getRoles().add(Role.TEAM_MEMBER); + userRepository.save(user3); + } + + private PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/Project.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/Project.java new file mode 100644 index 0000000..0875f85 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/Project.java @@ -0,0 +1,39 @@ +package com.accenture.codingtest.springbootcodingtest.entity; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +@Table(name = "projects") +public class Project { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + @Column(name = "name",nullable = false,unique = true) + private String name; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Project { " + + "id= " + id + + " , name='" + name + + "' }"; + } +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/Task.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/Task.java new file mode 100644 index 0000000..6bf6fa7 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/Task.java @@ -0,0 +1,77 @@ +package com.accenture.codingtest.springbootcodingtest.entity; + + +import com.accenture.codingtest.springbootcodingtest.model.TaskStatus; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +@Table(name = "tasks") +public class Task { + + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + @Column(nullable = false) + private String title; + private String description; + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private TaskStatus status; + @ManyToOne + @JoinColumn(name = "project_id",nullable = false) + private Project project; + @ManyToOne + @JoinColumn(name = "user_id",nullable = false) + private User user; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public TaskStatus getStatus() { + return status; + } + + public void setStatus(TaskStatus status) { + this.status = status; + } + + public Project getProject() { + return project; + } + + public void setProject(Project project) { + this.project = project; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/User.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/User.java new file mode 100644 index 0000000..34c81fd --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/entity/User.java @@ -0,0 +1,59 @@ +package com.accenture.codingtest.springbootcodingtest.entity; + +import com.accenture.codingtest.springbootcodingtest.model.Role; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + + @Column(nullable = false,unique = true) + private String username; + @Column(nullable = false) + private String password; + + @ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER) + @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) + @Enumerated(EnumType.STRING) + private Set roles = new HashSet<>(); + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/model/Role.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/model/Role.java new file mode 100644 index 0000000..8bc2f47 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/model/Role.java @@ -0,0 +1,23 @@ +package com.accenture.codingtest.springbootcodingtest.model; + +public enum Role { + PROJECT_OWNER("Project Owner", "ROLE_PROJECT_OWNER"), + ADMIN("Admin", "ROLE_ADMIN"), + TEAM_MEMBER("Team Member", "ROLE_TEAM_MEMBER"); + + private final String roleName; + private final String authority; + + Role(String roleName, String authority) { + this.roleName = roleName; + this.authority = authority; + } + + public String getRoleName() { + return roleName; + } + + public String getAuthority() { + return authority; + } +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/model/TaskStatus.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/model/TaskStatus.java new file mode 100644 index 0000000..21e1af7 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/model/TaskStatus.java @@ -0,0 +1,8 @@ +package com.accenture.codingtest.springbootcodingtest.model; + +public enum TaskStatus { + NOT_STARTED, + IN_PROGRESS, + READY_FOR_TEST, + COMPLETED +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/ProjectRepository.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/ProjectRepository.java new file mode 100644 index 0000000..d2839ce --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/ProjectRepository.java @@ -0,0 +1,13 @@ +package com.accenture.codingtest.springbootcodingtest.repository; + +import com.accenture.codingtest.springbootcodingtest.entity.Project; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface ProjectRepository extends JpaRepository { + + Page findByNameContainingIgnoreCase(String q, PageRequest pageRequest); +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/TaskRepository.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/TaskRepository.java new file mode 100644 index 0000000..deb954b --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/TaskRepository.java @@ -0,0 +1,10 @@ +package com.accenture.codingtest.springbootcodingtest.repository; + +import com.accenture.codingtest.springbootcodingtest.entity.Task; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface TaskRepository extends JpaRepository { + +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/UserRepository.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/UserRepository.java new file mode 100644 index 0000000..14c3767 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.accenture.codingtest.springbootcodingtest.repository; + +import com.accenture.codingtest.springbootcodingtest.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String userName); + +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/service/ProjectService.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/service/ProjectService.java new file mode 100644 index 0000000..9ffd250 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/service/ProjectService.java @@ -0,0 +1,53 @@ +package com.accenture.codingtest.springbootcodingtest.service; + +import com.accenture.codingtest.springbootcodingtest.entity.Project; +import com.accenture.codingtest.springbootcodingtest.repository.ProjectRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +@Service("projectService") +public class ProjectService { + + private final ProjectRepository projectRepository; + + @Autowired + public ProjectService(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + + public Project findById(UUID projectId) { + return projectRepository.findById(projectId) + .orElseThrow(() -> new RuntimeException("Project not found with Id: " + projectId)); + } + + public Page getAllProjects(Pageable pageable) { + return projectRepository.findAll(pageable); + } + + + public Project save(Project project) { + return projectRepository.save(project); + } + + public Project updateProject(UUID projectId, Project project) { + Project fetch = findById(projectId); + fetch.setName(project.getName()); + return projectRepository.save(fetch); + } + + public void delete(UUID projectId) { + Project fetch = findById(projectId); + projectRepository.delete(fetch); + } + + + +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/service/TaskService.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/service/TaskService.java new file mode 100644 index 0000000..0c9757b --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/service/TaskService.java @@ -0,0 +1,103 @@ +package com.accenture.codingtest.springbootcodingtest.service; + +import com.accenture.codingtest.springbootcodingtest.entity.Project; +import com.accenture.codingtest.springbootcodingtest.entity.Task; +import com.accenture.codingtest.springbootcodingtest.entity.User; +import com.accenture.codingtest.springbootcodingtest.model.TaskStatus; +import com.accenture.codingtest.springbootcodingtest.repository.TaskRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +public class TaskService { + private final TaskRepository taskRepository; + private final UserService userService; + private final ProjectService projectService; + + @Autowired + public TaskService(TaskRepository taskRepository, UserService userService, ProjectService projectService) { + this.taskRepository = taskRepository; + this.userService = userService; + this.projectService = projectService; + } + + public Task findById(UUID taskId) { + return taskRepository.findById(taskId) + .orElseThrow(() -> new RuntimeException("Task not found with Id: " + taskId)); + } + + public List findAll() { + return taskRepository.findAll(); + } + + public Task save(Task task) { + + assignProject(task); + + assignUser(task); + + return taskRepository.save(task); + } + + private void assignUser(Task task) { + UUID userId = task.getUser().getId(); + User user = userService.findById(userId); + task.setUser(user); + } + + private void assignProject(Task task) { + UUID projectId = task.getProject().getId(); + Project project = projectService.findById(projectId); + task.setProject(project); + } + + public Task updateTask(UUID taskId, Task task) { + Task fetch = findById(taskId); + + + fetch.setTitle(task.getTitle()); + fetch.setDescription(task.getDescription()); + fetch.setStatus(task.getStatus()); + assignProject(task); + assignUser(task); + + return taskRepository.save(fetch); + } + + public Task patchTask(UUID taskId, Task task) { + Task fetch = findById(taskId); + + if (task.getTitle() != null) { + fetch.setTitle(task.getTitle()); + } + if (task.getDescription() != null) { + fetch.setDescription(task.getDescription()); + } + if (task.getStatus() != null) { + fetch.setStatus(task.getStatus()); + } + if (task.getProject() != null) { + assignProject(task); + } + if (task.getUser() != null) { + assignUser(task); + } + + return taskRepository.save(fetch); + } + + public void delete(UUID taskId) { + Task fetch = findById(taskId); + taskRepository.delete(fetch); + } + + + public Task updateTaskStatus(UUID taskId, TaskStatus status) { + Task fetch = findById(taskId); + patchTask(taskId,fetch).setStatus(status); + return null; + } +} diff --git a/src/main/java/com/accenture/codingtest/springbootcodingtest/service/UserService.java b/src/main/java/com/accenture/codingtest/springbootcodingtest/service/UserService.java new file mode 100644 index 0000000..eb1d6a6 --- /dev/null +++ b/src/main/java/com/accenture/codingtest/springbootcodingtest/service/UserService.java @@ -0,0 +1,60 @@ +package com.accenture.codingtest.springbootcodingtest.service; + +import com.accenture.codingtest.springbootcodingtest.entity.User; +import com.accenture.codingtest.springbootcodingtest.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +public class UserService { + + + private final UserRepository userRepository; + + @Autowired + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User save(User user) { + userRepository.save(user); + return user; + } + + public User findById(UUID userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found with userId: " + userId)); + } + + public List findAll() { + return userRepository.findAll(); + } + + public User updateUser(UUID userId, User user) { + User fetch = findById(userId); + fetch.setUsername(user.getUsername()); + fetch.setPassword(user.getPassword()); + return userRepository.save(fetch); + } + + public User patchUser(UUID userId, User user){ + User fetch = findById(userId); + if (user.getUsername() != null){ + fetch.setUsername(user.getUsername()); + } + if (user.getPassword() != null){ + fetch.setPassword(user.getPassword()); + } + + return userRepository.save(fetch); + } + + public void delete(UUID userId) { + User fetch = findById(userId); + userRepository.deleteById(userId); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..f6f4510 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,8 @@ +# H2 Database +spring.h2.console.enabled=true +spring.datasource.url=jdbc:h2:mem:dcbapp +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect diff --git a/src/test/java/com/accenture/codingtest/springbootcodingtest/UserControllerTest.java b/src/test/java/com/accenture/codingtest/springbootcodingtest/UserControllerTest.java new file mode 100644 index 0000000..f3408ea --- /dev/null +++ b/src/test/java/com/accenture/codingtest/springbootcodingtest/UserControllerTest.java @@ -0,0 +1,51 @@ +package com.accenture.codingtest.springbootcodingtest; + +import com.accenture.codingtest.springbootcodingtest.entity.User; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.*; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class UserControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void testCreateUser() { + String createUserUrl = "/api/v1/users"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBasicAuth("admin", "admin"); + + String requestBody = """ + { + "username":"Ram", + "password":"1234" + } + """; + HttpEntity requestEntity = new HttpEntity<>(requestBody, headers); + + URI uri = UriComponentsBuilder.fromUriString(createUserUrl).build().toUri(); + + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.POST, + requestEntity, + User.class + ); + + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + User createdUser = response.getBody(); + assertEquals("Ram", createdUser.getUsername()); + } + + +}