fix(project): hide mentor-only controls

This commit is contained in:
sechmachine
2026-08-15 00:30:22 +07:00
parent 7acd525205
commit a9ee99a6fe
6 changed files with 32 additions and 8 deletions
@@ -1,5 +1,6 @@
package com.lab.labtimesheet.feature.project.controller; package com.lab.labtimesheet.feature.project.controller;
import com.lab.labtimesheet.feature.project.exception.ProjectAccessDeniedException;
import com.lab.labtimesheet.feature.project.model.dto.ProjectCreateForm; import com.lab.labtimesheet.feature.project.model.dto.ProjectCreateForm;
import com.lab.labtimesheet.feature.project.model.dto.ProjectMemberForm; import com.lab.labtimesheet.feature.project.model.dto.ProjectMemberForm;
import com.lab.labtimesheet.feature.project.service.ProjectQueryService; import com.lab.labtimesheet.feature.project.service.ProjectQueryService;
@@ -34,7 +35,10 @@ public class ProjectController {
} }
@GetMapping("/new") @GetMapping("/new")
public String createForm(Model model) { public String createForm(Principal principal, Model model) {
if (!"MENTOR".equals(pages.authenticatedActor(principal.getName()).role())) {
throw new ProjectAccessDeniedException();
}
model.addAttribute("projectForm", new ProjectCreateForm()); model.addAttribute("projectForm", new ProjectCreateForm());
return "projects/form"; return "projects/form";
} }
@@ -10,5 +10,6 @@ public record ProjectDetail(
LocalDate startDate, LocalDate startDate,
LocalDate endDate, LocalDate endDate,
String mentorName, String mentorName,
String leaderName) { String leaderName,
boolean canManage) {
} }
@@ -64,7 +64,8 @@ public class ProjectQueryService {
project.startDate(), project.startDate(),
project.endDate(), project.endDate(),
displayName(project.mentorUserId()), displayName(project.mentorUserId()),
displayName(project.currentLeader().internUserId())); displayName(project.currentLeader().internUserId()),
project.mentorUserId() == actorUserId);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -7,7 +7,7 @@
<table><caption>Leadership history</caption><thead><tr><th scope="col">Leader</th><th scope="col">Started</th><th scope="col">Ended</th></tr></thead> <table><caption>Leadership history</caption><thead><tr><th scope="col">Leader</th><th scope="col">Started</th><th scope="col">Ended</th></tr></thead>
<tbody><tr th:each="term : ${leadership}"><td th:text="${term.leaderName}"></td><td th:text="${term.startedAt}"></td><td th:text="${term.endedAt}"></td></tr></tbody> <tbody><tr th:each="term : ${leadership}"><td th:text="${term.leaderName}"></td><td th:text="${term.startedAt}"></td><td th:text="${term.endedAt}"></td></tr></tbody>
</table> </table>
<form method="post" th:action="@{/projects/{id}/leadership(id=${project.id})}"><label for="leader">New Leader user ID</label><input id="leader" name="internUserId" type="number" min="1" required><button type="submit">Change Leader</button></form> <form th:if="${project.canManage}" method="post" th:action="@{/projects/{id}/leadership(id=${project.id})}"><label for="leader">New Leader user ID</label><input id="leader" name="internUserId" type="number" min="1" required><button type="submit">Change Leader</button></form>
</main> </main>
</body> </body>
</html> </html>
@@ -7,7 +7,7 @@
<table><caption>Membership history</caption><thead><tr><th scope="col">Intern</th><th scope="col">Joined</th><th scope="col">Left</th><th scope="col">Role</th></tr></thead> <table><caption>Membership history</caption><thead><tr><th scope="col">Intern</th><th scope="col">Joined</th><th scope="col">Left</th><th scope="col">Role</th></tr></thead>
<tbody><tr th:each="member : ${members}"><td th:text="${member.displayName}"></td><td th:text="${member.joinedAt}"></td><td th:text="${member.leftAt}"></td><td th:text="${member.currentLeader} ? 'Leader' : 'Member'"></td></tr></tbody> <tbody><tr th:each="member : ${members}"><td th:text="${member.displayName}"></td><td th:text="${member.joinedAt}"></td><td th:text="${member.leftAt}"></td><td th:text="${member.currentLeader} ? 'Leader' : 'Member'"></td></tr></tbody>
</table> </table>
<form method="post" th:action="@{/projects/{id}/members(id=${project.id})}"><label for="intern">Intern user ID</label><input id="intern" name="internUserId" type="number" min="1" required><button type="submit">Add member</button></form> <form th:if="${project.canManage}" method="post" th:action="@{/projects/{id}/members(id=${project.id})}"><label for="intern">Intern user ID</label><input id="intern" name="internUserId" type="number" min="1" required><button type="submit">Add member</button></form>
</main> </main>
</body> </body>
</html> </html>
@@ -3,6 +3,8 @@ package com.lab.labtimesheet.feature.project.controller;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -13,6 +15,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import com.lab.labtimesheet.feature.project.exception.ProjectAccessDeniedException; import com.lab.labtimesheet.feature.project.exception.ProjectAccessDeniedException;
import com.lab.labtimesheet.feature.project.model.dto.ProjectCreateCommand; import com.lab.labtimesheet.feature.project.model.dto.ProjectCreateCommand;
import com.lab.labtimesheet.feature.project.model.dto.ProjectActorView;
import com.lab.labtimesheet.feature.project.model.dto.ProjectDetail; import com.lab.labtimesheet.feature.project.model.dto.ProjectDetail;
import com.lab.labtimesheet.feature.project.model.dto.ProjectSummary; import com.lab.labtimesheet.feature.project.model.dto.ProjectSummary;
import com.lab.labtimesheet.feature.project.service.ProjectQueryService; import com.lab.labtimesheet.feature.project.service.ProjectQueryService;
@@ -79,16 +82,31 @@ class ProjectControllerTest {
LocalDate.of(2026, 8, 15), LocalDate.of(2026, 8, 15),
LocalDate.of(2026, 9, 30), LocalDate.of(2026, 9, 30),
"Mentor", "Mentor",
"Leader")); "Leader",
false));
when(pages.members(20L, 30L)).thenReturn(List.of()); when(pages.members(20L, 30L)).thenReturn(List.of());
when(pages.leadership(20L, 30L)).thenReturn(List.of()); when(pages.leadership(20L, 30L)).thenReturn(List.of());
mvc.perform(get("/projects/30/members")) mvc.perform(get("/projects/30/members"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(view().name("projects/members")); .andExpect(view().name("projects/members"))
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.content()
.string(not(containsString("Add member"))));
mvc.perform(get("/projects/30/leadership")) mvc.perform(get("/projects/30/leadership"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(view().name("projects/leadership")); .andExpect(view().name("projects/leadership"))
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.content()
.string(not(containsString("Change Leader"))));
}
@Test
@WithMockUser(username = "member@example.test")
void nonMentorCannotOpenProjectCreationForm() throws Exception {
when(pages.authenticatedActor("member@example.test"))
.thenReturn(new ProjectActorView(20L, "INTERN"));
mvc.perform(get("/projects/new"))
.andExpect(status().isNotFound());
} }
@Test @Test