Skip to content

Commit

Permalink
✨ : add users mgmt screen
Browse files Browse the repository at this point in the history
  • Loading branch information
juwit committed Aug 8, 2019
1 parent cfcf55c commit 83228e3
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/main/java/io/codeka/gaia/teams/bo/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public class User {
@DBRef
private Team team;

public User() {
}

public User(String username) {
this.username = username;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.codeka.gaia.teams.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@Secured("ROLE_ADMIN")
public class UsersMVCController {

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.codeka.gaia.teams.controller;

import io.codeka.gaia.teams.bo.User;
import io.codeka.gaia.teams.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
@Secured("ROLE_ADMIN")
public class UsersRestController {

private UserRepository userRepository;

@Autowired
public UsersRestController(UserRepository userRepository) {
this.userRepository = userRepository;
}

@GetMapping
public List<User> users(){
return this.userRepository.findAll();
}

@PutMapping("/{userId}")
public User saveUser(@RequestBody User user){
return this.userRepository.save(user);
}

}
1 change: 1 addition & 0 deletions src/main/resources/templates/layout/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ <h4>Menu</h4>
<li><a href="/modules"><i class="fa fa-object-group blue1_color"></i> <span>Modules</span></a></li>
<li><a href="/stacks"><i class="fas fa-layer-group blue2_color"></i> <span>Stacks</span></a></li>
<li sec:authorize="hasRole('ADMIN')"><a href="/settings"><i class="fa fa-cog yellow_color"></i> <span>Settings</span></a></li>
<li sec:authorize="hasRole('ADMIN')"><a href="/users"><i class="fas fa-user-friends yellow_color"></i> <span>Users</span></a></li>
</ul>
</div>
</nav>
Expand Down
140 changes: 140 additions & 0 deletions src/main/resources/templates/users.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head th:replace="layout/header :: header(~{::title})">

<title>Gaia - Users</title>

</head>
<body class="dashboard dashboard_2">
<div class="full_container">
<div class="inner_container">

<div th:replace="~{layout/sidebar}"></div>

<!-- right content -->
<div id="content">

<div th:replace="~{layout/topbar}"></div>

<div class="container-fluid">
<div class="row column_title">
<div class="col-md-12">
<div class="page_title">
<div id="navigation"></div>
</div>
</div>
</div>
<div class="row column1" id="placeholder"></div>
</div>

</div>
</div>
</div>

<template id="template-navigation">
<breadcrumb :page="page"></breadcrumb>
</template>

<template id="template">
<div class="block module_description">
<b-table :items="users" :fields="fields" stripped fixed >
<!-- A custom formatted column -->
<template slot="team" slot-scope="data">
<vue-multiselect
:multiple="false"
label="id"
track-by="id"
searchable
placeholder="Select user team"
v-model="data.item.team"
@input="changeTeam(data.item)"
:options="$teams"></vue-multiselect>
</template>
</b-table>
</div>
</template>

<!-- jQuery & bootstrap-->
<script src="/webjars/jquery/3.0.0/jquery.min.js"></script>
<script src="/webjars/popper.js/1.14.3/umd/popper.min.js"></script>
<script src="/webjars/bootstrap/4.3.1/js/bootstrap.min.js"></script>

<script src="/webjars/vue/2.5.16/vue.js"></script>
<script src="/webjars/bootstrap-vue/2.0.0-rc.26/dist/bootstrap-vue.js"></script>

<script src="/webjars/vue-multiselect/2.1.6/dist/vue-multiselect.min.js"></script>

<div th:replace="helpers/messenger"></div>
<div th:replace="vue_templates/breadcrumb"></div>

<script type="application/ecmascript">
Vue.component('vue-multiselect', window.VueMultiselect.default);

$(document).ready(function () {
/*-- sidebar js --*/
$('#sidebarCollapse').on('click', function () {
$('#sidebar').toggleClass('active');
});
$(function () {
$('[data-toggle="tooltip"]').tooltip()
});
});

new Vue({
el: "#navigation",
data: () => ({ page: 'users' }),
template: "#template-navigation",
});

let users;
let teams;
let options;

$.get("/api/teams").then(data => teams = data)
.then(_ => {
options = teams.map(team => {return {text: team.id, value: team}});
Vue.prototype.$teams = teams;
})
.then(_ => {
$.get("/api/users")
.then(data => {
users = data;

new Vue({
el: "#placeholder",
data: () => {return {
users,
fields: ["username", "team"]
}},
methods: {
changeTeam: function(user) {
const message = Messenger().post({
type: "info",
message: `Saving user ${user.username}.`
});
$.ajax({
url: `/api/users/${user.username}`,
data: JSON.stringify(user),
contentType: "application/json",
method: "PUT",
success: () => message.update({
type: "success",
message: `User ${user.username} saved.`
}),
error: () => message.update({
type: "error",
message: `Error saving user ${user.username}.`
})
});
}
},
template: "#template"
});

$('[data-toggle="tooltip"]').tooltip();
});
});
</script>

</body>
</html>
1 change: 1 addition & 0 deletions src/main/resources/templates/vue_templates/breadcrumb.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
module_description: [{text: 'Modules', href: '/modules'}, {text: 'Module description'},],
stacks: [{text: 'Stacks'}],
stack: [{text: 'Stacks', href: '/stacks'}, {text: 'Stack'}],
users: [{text: 'Users'}],
job: [{text: 'Stacks', href: '/stacks'}, {text: 'Stack', href: `/stacks/${stackId}`}, {text: 'Job'}],
settings: [{text: 'Settings'}],
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.codeka.gaia.teams.controller;

import io.codeka.gaia.config.SecurityConfig;
import io.codeka.gaia.teams.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
* Simple integration test that validates the security configuration of the UsersRestController, and its http routes
*/
@SpringBootTest(classes={UsersRestController.class, SecurityConfig.class})
@AutoConfigureWebMvc
@AutoConfigureMockMvc
@WithMockUser(value = "admin", roles = "ADMIN")
class UsersRestControllerIT {

@MockBean
private UserRepository userRepository;

@Autowired
private UsersRestController usersRestController;

@Autowired
private MockMvc mockMvc;

@Test
@WithMockUser("Matthew Bellamy")
void users_shouldNotBeAccessible_forStandardUsers(){
assertThrows(AccessDeniedException.class, () -> usersRestController.users());
}

@Test
void users_shouldBeAccessible_forAdminUser(){
assertDoesNotThrow(() -> usersRestController.users());
}

@Test
void users_shouldBeExposed_atSpecificUrl() throws Exception {
mockMvc.perform(get("/api/users")).andExpect(status().isOk());

verify(userRepository).findAll();
}

@Test
void saveUser_shouldBeExposed_atSpecificUrl() throws Exception {
mockMvc.perform(put("/api/users/test")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"Bob\"}")).andExpect(status().isOk());

verify(userRepository).save(any());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.codeka.gaia.teams.controller;

import io.codeka.gaia.teams.bo.User;
import io.codeka.gaia.teams.repository.TeamRepository;
import io.codeka.gaia.teams.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class UsersRestControllerTest {

@Mock
private UserRepository userRepository;

@InjectMocks
private UsersRestController usersRestController;

@Test
void users_shouldReturnAllTeams() {
usersRestController.users();

verify(userRepository).findAll();
}

@Test
void saveUser_shouldSaveTheUser() {
var john = new User();
usersRestController.saveUser(john);

verify(userRepository).save(john);
}
}

0 comments on commit 83228e3

Please sign in to comment.