Skip to content

Commit

Permalink
both: create user and refactor admin panel
Browse files Browse the repository at this point in the history
  • Loading branch information
while1618 committed Dec 6, 2024
1 parent 3476641 commit e9afc3a
Show file tree
Hide file tree
Showing 28 changed files with 590 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.bootstrapbugz.backend.admin.service.UserService;
import org.bootstrapbugz.backend.auth.jwt.service.AccessTokenService;
import org.bootstrapbugz.backend.auth.jwt.service.RefreshTokenService;
import org.bootstrapbugz.backend.shared.error.exception.BadRequestException;
import org.bootstrapbugz.backend.shared.error.exception.ConflictException;
import org.bootstrapbugz.backend.shared.error.exception.ResourceNotFoundException;
import org.bootstrapbugz.backend.shared.payload.dto.PageableDTO;
Expand Down Expand Up @@ -157,6 +158,8 @@ private void setLock(User user, Boolean lock) {
}

private void setRoles(User user, Set<RoleName> roleNames) {
if (roleNames.isEmpty()) throw new BadRequestException("user.rolesEmpty");

final var roles = Set.copyOf(roleRepository.findAllByNameIn(roleNames));
if (user.getRoles().equals(roles)) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public class Path {
public static final String AUTH = BACKEND_VERSION + "/auth";
public static final String PROFILE = BACKEND_VERSION + "/profile";
public static final String USERS = BACKEND_VERSION + "/users";
public static final String ROLES = BACKEND_VERSION + "/roles";
public static final String ADMIN_USERS = BACKEND_VERSION + "/admin/users";

private Path() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.bootstrapbugz.backend.user.controller;

import java.util.List;
import org.bootstrapbugz.backend.shared.constants.Path;
import org.bootstrapbugz.backend.user.payload.dto.RoleDTO;
import org.bootstrapbugz.backend.user.service.RoleService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(Path.ROLES)
public class RoleController {
private final RoleService roleService;

public RoleController(RoleService roleService) {
this.roleService = roleService;
}

@GetMapping
public ResponseEntity<List<RoleDTO>> findAll() {
return ResponseEntity.ok(roleService.findAll());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public interface UserMapper {

Set<RoleDTO> rolesToRoleDTOs(Set<Role> roles);

RoleDTO roleToRoleDTO(Role role);

@Mapping(source = "roles", target = "roleDTOs")
UserDTO userToAdminUserDTO(User user);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.bootstrapbugz.backend.user.service;

import java.util.List;
import org.bootstrapbugz.backend.user.payload.dto.RoleDTO;

public interface RoleService {
List<RoleDTO> findAll();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.bootstrapbugz.backend.user.service.impl;

import java.util.List;
import org.bootstrapbugz.backend.user.mapper.UserMapper;
import org.bootstrapbugz.backend.user.payload.dto.RoleDTO;
import org.bootstrapbugz.backend.user.repository.RoleRepository;
import org.bootstrapbugz.backend.user.service.RoleService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
@PreAuthorize("hasAuthority('ADMIN')")
public class RoleServiceImpl implements RoleService {
private final RoleRepository roleRepository;

public RoleServiceImpl(RoleRepository roleRepository) {
this.roleRepository = roleRepository;
}

@Override
public List<RoleDTO> findAll() {
return roleRepository.findAll().stream().map(UserMapper.INSTANCE::roleToRoleDTO).toList();
}
}
25 changes: 25 additions & 0 deletions backend/spring-boot/src/main/resources/static/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ tags:
description: Profile endpoints
- name: users
description: Users endpoints
- name: roles
description: Roles endpoints
- name: admin
description: Admin endpoints
paths:
Expand Down Expand Up @@ -357,6 +359,29 @@ paths:
available: true
400:
$ref: "#/components/responses/BadRequest"
/roles:
get:
tags:
- roles
summary: Get roles
security:
- BearerAuth: []
responses:
200:
description: Roles retrieved successfully
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RoleDTO"
example:
- name: ADMIN
- name: USER
401:
$ref: "#/components/responses/Unauthorized"
403:
$ref: "#/components/responses/Forbidden"
/admin/users:
post:
tags:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,24 @@ void deleteUserById_throwForbidden_userNotAdmin() throws Exception {
.andExpect(status().isForbidden())
.andExpect(content().string(containsString(forbidden)));
}

@Test
void getRoles_throwUnauthorized_userNotSignedIn() throws Exception {
mockMvc
.perform(get(Path.ROLES).contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized())
.andExpect(content().string(containsString(unauthorized)));
}

@Test
void getRoles_throwForbidden_userNotAdmin() throws Exception {
final var authTokens = IntegrationTestUtil.authTokens(mockMvc, objectMapper, "user");
mockMvc
.perform(
get(Path.ROLES)
.contentType(MediaType.APPLICATION_JSON)
.headers(IntegrationTestUtil.authHeader(authTokens.accessToken())))
.andExpect(status().isForbidden())
.andExpect(content().string(containsString(forbidden)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.bootstrapbugz.backend.user.integration;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.bootstrapbugz.backend.shared.config.DatabaseContainers;
import org.bootstrapbugz.backend.shared.constants.Path;
import org.bootstrapbugz.backend.shared.util.IntegrationTestUtil;
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.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

@DirtiesContext
@AutoConfigureMockMvc
@ActiveProfiles("test")
@SpringBootTest
public class RoleControllerIT extends DatabaseContainers {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;

@Test
void findAllRoles() throws Exception {
final var authTokens = IntegrationTestUtil.authTokens(mockMvc, objectMapper, "admin");
mockMvc
.perform(
get(Path.ROLES)
.contentType(MediaType.APPLICATION_JSON)
.headers(IntegrationTestUtil.authHeader(authTokens.accessToken())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}
}
3 changes: 3 additions & 0 deletions frontend/svelte-kit/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"general_save": "Save",
"general_cancel": "Cancel",
"general_delete": "Delete",
"general_status": "Status",

"home_getStarted": "Get Started",
"home_greetings": "Hello there",
Expand Down Expand Up @@ -68,6 +69,8 @@
"auth_resetPassword": "Reset password",
"auth_tokenInvalid": "Invalid token",

"admin_userCreate": "Create user",
"admin_userCreatedSuccess": "User created successfully",
"admin_users": "Users",
"admin_userId": "User ID",
"admin_userUsername": "Username",
Expand Down
3 changes: 3 additions & 0 deletions frontend/svelte-kit/messages/sr.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"general_save": "Sačuvaj",
"general_cancel": "Otkaži",
"general_delete": "Obriši",
"general_status": "Status",

"home_getStarted": "Počni",
"home_greetings": "Zdravo",
Expand Down Expand Up @@ -68,6 +69,8 @@
"auth_resetPassword": "Resetuj lozinku",
"auth_tokenInvalid": "Nevalidan token",

"admin_userCreate": "Kreiraj korisnika",
"admin_userCreatedSuccess": "Korisnik uspešno kreiran",
"admin_users": "Korisnici",
"admin_userId": "Korisničko ime",
"admin_userUsername": "Korisničko ime",
Expand Down
2 changes: 1 addition & 1 deletion frontend/svelte-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@types/eslint": "9.6.1",
"@types/jsonwebtoken": "9.0.7",
"autoprefixer": "10.4.20",
"bits-ui": "1.0.0-next.63",
"bits-ui": "1.0.0-next.66",
"clsx": "2.1.1",
"eslint": "9.16.0",
"eslint-config-prettier": "9.1.0",
Expand Down
10 changes: 5 additions & 5 deletions frontend/svelte-kit/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import X from 'lucide-svelte/icons/x';
import type { Snippet } from 'svelte';
import * as Dialog from './index.js';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>

<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
class={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...restProps}
>
{@render children?.()}
<DialogPrimitive.Close
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>

<DialogPrimitive.Description
bind:ref
class={cn('text-sm text-muted-foreground', className)}
{...restProps}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div
bind:this={ref}
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...restProps}
>
{@render children?.()}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import type { WithElementRef } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div
bind:this={ref}
class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
{...restProps}
>
{@render children?.()}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>

<DialogPrimitive.Overlay
bind:ref
class={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...restProps}
/>
Loading

0 comments on commit e9afc3a

Please sign in to comment.