Skip to content

Commit

Permalink
Merge pull request #47 from KSIUJ/user-manager
Browse files Browse the repository at this point in the history
Implement user management, closes #12
  • Loading branch information
apardyl authored Dec 13, 2018
2 parents fb63df7 + 105eac1 commit 3378d2b
Show file tree
Hide file tree
Showing 14 changed files with 418 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ class UserRegistrationController(
fun registerPost(
@Valid @ModelAttribute("form") user: UserRegistrationForm,
result: BindingResult
): String {
): ModelAndView {
if (!registrationEnabled) {
return "registration/registration_disabled"
return ModelAndView("registration/registration_disabled", HttpStatus.FORBIDDEN)
}

if (!user.userName.isBlank() && userRepository.findByUserName(user.userName) != null) {
Expand All @@ -56,14 +56,14 @@ class UserRegistrationController(
}

if (result.hasErrors()) {
return "registration/create_account"
return ModelAndView("registration/create_account", HttpStatus.BAD_REQUEST)
}

val newUser = User(userName = user.userName, email = user.email, firstName = user.firstName,
lastName = user.lastName, enabled = true)
userRepository.save(newUser)
eventPublisher.publishEvent(OnEmailVerificationRequestedEvent(newUser))
return "registration/verify_email"
return ModelAndView("registration/verify_email")
}

@GetMapping(value = ["/register/activate/"], params = ["token"])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package pl.edu.uj.ii.ksi.mordor.controllers.admin

import org.springframework.security.access.annotation.Secured
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.servlet.View
import org.springframework.web.servlet.view.RedirectView
import pl.edu.uj.ii.ksi.mordor.persistence.entities.Permission

@Controller
class AdminController {
@Secured(Permission.ACCESS_ADMIN_PANEL_STR)
@GetMapping("/admin/")
fun adminPage(): View {
return RedirectView("/admin/users/")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package pl.edu.uj.ii.ksi.mordor.controllers.admin

import javax.validation.Valid
import org.springframework.data.domain.PageRequest
import org.springframework.http.HttpStatus
import org.springframework.security.access.annotation.Secured
import org.springframework.stereotype.Controller
import org.springframework.validation.BindingResult
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.View
import org.springframework.web.servlet.view.RedirectView
import pl.edu.uj.ii.ksi.mordor.exceptions.BadRequestException
import pl.edu.uj.ii.ksi.mordor.forms.UserForm
import pl.edu.uj.ii.ksi.mordor.persistence.entities.Permission
import pl.edu.uj.ii.ksi.mordor.persistence.entities.Role
import pl.edu.uj.ii.ksi.mordor.persistence.entities.User
import pl.edu.uj.ii.ksi.mordor.persistence.repositories.UserRepository
import pl.edu.uj.ii.ksi.mordor.services.UserManagerService

@Controller
class UsersController(private val userRepository: UserRepository, private val userManagerService: UserManagerService) {
companion object {
private const val usersPerPage = 100
}

@Secured(Permission.ACCESS_ADMIN_PANEL_STR)
@GetMapping("/admin/users/")
fun userList(): View {
return RedirectView("/admin/users/0/")
}

@Secured(Permission.ACCESS_ADMIN_PANEL_STR)
@GetMapping("/admin/users/{num}/")
fun userList(@PathVariable("num") pageNumber: Int): ModelAndView {
val users = userRepository.findAll(PageRequest.of(pageNumber, usersPerPage))
return ModelAndView("admin/user_list", "users", users)
}

@Secured(Permission.MANAGE_USERS_STR)
@GetMapping("/admin/user/{id}/")
fun userProfile(@PathVariable("id") userId: Long): ModelAndView {
val user = userRepository.findById(userId)
if (user.isPresent) {
val u = user.get()
val userForm = UserForm(u.id!!, u.userName, u.email, "", "", u.firstName, u.lastName, u.enabled, u.role)
return ModelAndView("admin/user_profile", "user", userForm)
} else {
throw BadRequestException("No user for id: $userId")
}
}

@Secured(Permission.MANAGE_USERS_STR)
@PostMapping("/admin/user/{id}/")
fun userProfileEdit(
@PathVariable("id") userId: Long,
@Valid @ModelAttribute("user") form: UserForm,
result: BindingResult
): ModelAndView {
if (form.id != userId) {
throw BadRequestException("User id mismatch")
}
if (form.password != form.password2) {
result.rejectValue("password2", "password2.notMatch", "passwords do not match")
}
if (result.hasErrors()) {
return ModelAndView("admin/user_profile", HttpStatus.BAD_REQUEST)
}
val user = userRepository.findById(form.id).orElseThrow { BadRequestException("unknown user") }
user.email = form.email
user.firstName = form.firstName
user.lastName = form.lastName
user.enabled = form.enabled
user.role = form.role
if (!form.password.isNullOrBlank()) {
user.password = userManagerService.hashPassword(form.password)
}
userRepository.save(user)
return ModelAndView(RedirectView("/admin/users/"))
}

@Secured(Permission.MANAGE_USERS_STR)
@PostMapping("/admin/user/{id}/delete/")
fun userProfileDelete(
@PathVariable("id") userId: Long
): ModelAndView {
val user = userRepository.findById(userId).orElseThrow { BadRequestException("unknown user") }
userRepository.delete(user)
return ModelAndView(RedirectView("/admin/users/"))
}

@Secured(Permission.MANAGE_USERS_STR)
@GetMapping("/admin/user/create/")
fun userProfileCreate(): ModelAndView {
return ModelAndView("admin/user_create", "user", UserForm(null, "", "", "", "", "", "", true, Role.USER))
}

@Secured(Permission.MANAGE_USERS_STR)
@PostMapping("/admin/user/create/")
fun userProfileCreatePost(@Valid @ModelAttribute("user") form: UserForm, result: BindingResult): ModelAndView {
if (!form.userName.isBlank() && userRepository.findByUserName(form.userName) != null) {
result.rejectValue("userName", "username.exists", "username unavailable")
}

if (form.email != null && !form.email.isBlank() && userRepository.findByEmail(form.email) != null) {
result.rejectValue("email", "email.exists", "email already in use")
}
if (form.password != form.password2) {
result.rejectValue("password2", "password2.notMatch", "passwords do not match")
}
if (result.hasErrors()) {
return ModelAndView("admin/user_create", HttpStatus.BAD_REQUEST)
}
val user = User(null, form.userName, form.password, form.email,
form.firstName, form.lastName, form.enabled, form.role)
userRepository.save(user)
return ModelAndView(RedirectView("/admin/users/"))
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/pl/edu/uj/ii/ksi/mordor/forms/UserForm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pl.edu.uj.ii.ksi.mordor.forms

import pl.edu.uj.ii.ksi.mordor.persistence.entities.Role

data class UserForm(
val id: Long?,

val userName: String,

val email: String?,

val password: String?,

val password2: String?,

val firstName: String?,

val lastName: String?,

val enabled: Boolean,

val role: Role
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ enum class Permission : GrantedAuthority {
ROLE_UPLOAD,
ROLE_WRITE,
ROLE_LIST_HIDDEN_FILES,
ROLE_ACCESS_ADMIN_PANEL,
ROLE_MANAGE_USERS;

companion object {
// Compile time constants for annotations
const val READ_STR = "ROLE_READ"
const val UPLOAD_STR = "ROLE_UPLOAD"
const val WRITE_STR = "ROLE_WRITE"
const val ACCESS_ADMIN_PANEL_STR = "ROLE_ACCESS_ADMIN_PANEL"
const val LIST_HIDDENFILES_STR = "ROLE_LIST_HIDDEN_FILES"
const val MANAGE_USERS_STR = "ROLE_MANAGE_USERS"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package pl.edu.uj.ii.ksi.mordor.persistence.entities
enum class Role(val permissions: List<Permission>) {
NOBODY(listOf()),
USER(listOf(Permission.ROLE_READ, Permission.ROLE_UPLOAD)),
MOD(listOf(Permission.ROLE_READ, Permission.ROLE_UPLOAD, Permission.ROLE_WRITE, Permission.ROLE_LIST_HIDDEN_FILES)),
MOD(listOf(Permission.ROLE_READ, Permission.ROLE_UPLOAD, Permission.ROLE_WRITE, Permission.ROLE_LIST_HIDDEN_FILES,
Permission.ROLE_ACCESS_ADMIN_PANEL)),
ADMIN(listOf(Permission.ROLE_READ, Permission.ROLE_UPLOAD, Permission.ROLE_WRITE, Permission.ROLE_LIST_HIDDEN_FILES,
Permission.ROLE_MANAGE_USERS)),
Permission.ROLE_ACCESS_ADMIN_PANEL, Permission.ROLE_MANAGE_USERS)),
EXTERNAL(listOf())
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class UserManagerService(
private val userRepository: UserRepository,
private val tokenRepository: EmailVerificationTokenRepository
) {
private fun hashPassword(password: String): String = "{bcrypt}" + BCryptPasswordEncoder().encode(password)
fun hashPassword(password: String): String = "{bcrypt}" + BCryptPasswordEncoder().encode(password)

fun resetPassword(tokenId: String, password: String): Boolean {
val token = tokenRepository.findByToken(tokenId)
Expand Down
21 changes: 21 additions & 0 deletions src/main/resources/templates/admin/layout_admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en" layout:decorate="~{layout}" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Mordor - Admin</title>
</head>
<body>
<div layout:fragment="content">
<ul class="nav nav-tabs mb-1">
<li class="nav-item">
<a class="nav-link"
th:classappend="${#httpServletRequest.getRequestURI().startsWith('/admin/user') ? 'active':''}"
href="/admin/users/">Users</a>
</li>
</ul>
<div layout:fragment="content-admin">
<!--CONTENT-->
</div>
</div>
</body>
</html>
60 changes: 60 additions & 0 deletions src/main/resources/templates/admin/user_create.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en" layout:decorate="~{admin/layout_admin}" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Create user</title>
</head>
<body>
<div layout:fragment="content-admin">
<form th:action="@{/admin/user/create/}" th:object="${user}" method="post" style="display: inline">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" class="form-control" th:field="*{userName}" id="username"/>
<div class="alert alert-danger" th:if="${#fields.hasErrors('userName')}" th:errors="*{email}">error</div>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="text" class="form-control" th:field="*{email}" id="email"/>
<div class="alert alert-danger" th:if="${#fields.hasErrors('email')}" th:errors="*{email}">error</div>
</div>
<div class="form-group">
<label for="firstName">First Name:</label>
<input type="text" class="form-control" th:field="*{firstName}" id="firstName"/>
<div class="alert alert-danger" th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}">error
</div>
</div>
<div class="form-group">
<label for="lastName">Last Name:</label>
<input type="text" class="form-control" th:field="*{lastName}" id="lastName"/>
<div class="alert alert-danger" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}">error</div>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" class="form-control" th:field="*{password}" id="password"/>
<div class="alert alert-danger" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">error</div>
</div>
<div class="form-group">
<label for="password2">Repeat password:</label>
<input type="password" class="form-control" th:field="*{password2}" id="password2"/>
<div class="alert alert-danger" th:if="${#fields.hasErrors('password2')}" th:errors="*{password2}">error
</div>
</div>
<div class="form-group">
<input type="checkbox" class="form-check-input" th:field="*{enabled}" id="enabled">
<label for="enabled" class="form-check-label">Local login enabled</label>
</div>
<div class="form-group">
<label for="role">Role:</label>
<select class="form-control" th:field="*{role}" id="role">
<option th:each="r : ${T(pl.edu.uj.ii.ksi.mordor.persistence.entities.Role).values()}"
th:value="${r}" th:text="${r.name()}">

</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</body>
</html>

43 changes: 43 additions & 0 deletions src/main/resources/templates/admin/user_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en" layout:decorate="~{admin/layout_admin}" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Users</title>
</head>
<body>
<div layout:fragment="content-admin">
<table class="table">
<thead>
<tr class="table-active">
<td scope="col">Id</td>
<td scope="col">Username</td>
<td scope="col">Email</td>
<td scope="col">First name</td>
<td scope="col">Last name</td>
<td scope="col">Local login</td>
<td scope="col">Role</td>
<td scope="col"></td>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td scope="row" th:text="${user.id}"></td>
<td th:text="${user.userName}"></td>
<td th:text="${user.email}"></td>
<td th:text="${user.firstName}"></td>
<td th:text="${user.lastName}"></td>
<td><i class="fas fa-check" th:if="${user.enabled}"></i></td>
<td th:text="${user.role.name()}"></td>
<td><a class="btn btn-light" href="#" th:href="@{'/admin/user/' + ${user.id} + '/'}"
sec:authorize="hasRole('ROLE_MANAGE_USERS')"><i class="fas fa-edit"></i></a></td>
</tr>
</tbody>
</table>
<div th:replace="fragments/pagination :: pagination(${users}, '/admin/users/')">
</div>
<a sec:authorize="hasRole('ROLE_MANAGE_USERS')" class="btn btn-success"
href="#" th:href="@{'/admin/user/create/'}">Add</a>
</div>
</body>
</html>

Loading

0 comments on commit 3378d2b

Please sign in to comment.