allow role management (closes #572)

This commit is contained in:
MiniDigger | Martin 2022-06-20 18:31:00 +02:00
parent 1edb8d9af9
commit c447bcb981
9 changed files with 93 additions and 12 deletions

View File

@ -835,7 +835,8 @@
"accepted": "Accepted",
"sidebar": "Other Administration",
"hangarAuth": "HangarAuth Profile",
"forum": "Forum Profile"
"forum": "Forum Profile",
"roles": "Roles"
},
"userActionLog": {
"title": "User Action Log",

View File

@ -4,21 +4,29 @@ import { useI18n } from "vue-i18n";
import Link from "~/components/design/Link.vue";
import Card from "~/components/design/Card.vue";
import { useApi, useInternalApi } from "~/composables/useApi";
import { PaginatedResult, Project } from "hangar-api";
import { useRoute } from "vue-router";
import { PaginatedResult, Project, User } from "hangar-api";
import { useRoute, useRouter } from "vue-router";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { OrganizationRoleTable } from "hangar-internal";
import { computed } from "vue";
import { computed, ref } from "vue";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import InputCheckbox from "~/components/ui/InputCheckbox.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { authUrl, forumUserUrl } from "~/composables/useUrlHelper";
import { useUser } from "~/composables/useApiHelper";
import Tag from "~/components/Tag.vue";
import InputSelect from "~/components/ui/InputSelect.vue";
import { useBackendDataStore } from "~/store/backendData";
import Button from "~/components/design/Button.vue";
import { AxiosError } from "axios";
const i18n = useI18n();
const route = useRoute();
const ctx = useContext();
const router = useRouter();
const backendData = useBackendDataStore();
const projects = await useApi<PaginatedResult<Project>>("projects", false, "get", {
owner: route.params.user,
@ -26,6 +34,7 @@ const projects = await useApi<PaginatedResult<Project>>("projects", false, "get"
const orgs = await useInternalApi<{ [key: string]: OrganizationRoleTable }>(`organizations/${route.params.user}/userOrganizations`, false).catch((e) =>
handleRequestError(e, ctx, i18n)
);
const user = await useUser(route.params.user as string).catch((e) => handleRequestError(e, ctx, i18n));
const projectsConfig = [
{ title: i18n.t("userAdmin.project"), name: "name" },
@ -52,6 +61,18 @@ const orgList = computed(() => {
const _forumUserUrl = computed(() => forumUserUrl(route.params.user as string));
const _authUrl = computed(() => authUrl(route.params.user as string));
const selectedRole = ref();
async function processRole(add: boolean) {
try {
await useInternalApi("/admin/user/" + route.params.user + "/" + selectedRole.value, true, add ? "POST" : "DELETE");
if (user) {
user.value = await useApi<User>(("users/" + route.params.user) as string);
}
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
}
}
useHead(useSeo(i18n.t("userAdmin.title") + " " + route.params.user, null, route, null));
</script>
@ -108,7 +129,24 @@ useHead(useSeo(i18n.t("userAdmin.title") + " " + route.params.user, null, route,
</template>
</SortableTable>
</Card>
<Card md="col-start-2 row-start-1 row-end-2">
<Card md="col-start-2 row-start-1">
<template #header>{{ i18n.t("userAdmin.roles") }}</template>
<Tag v-for="role in user.roles" :key="role.value" :color="{ background: role.color }" :name="role.title" />
<div class="flex mt-2">
<div class="flex-grow">
<InputSelect v-model="selectedRole" :values="backendData.globalRoles" item-text="title" item-value="value"></InputSelect>
</div>
<div>
<Button size="medium" @click="processRole(true)">{{ i18n.t("general.add") }}</Button>
</div>
<div class="ml-4">
<Button size="medium" @click="processRole(false)">{{ i18n.t("general.delete") }}</Button>
</div>
</div>
</Card>
<Card md="col-start-2 row-start-2">
<template #header>{{ i18n.t("userAdmin.sidebar") }}</template>
<ul>

View File

@ -44,6 +44,7 @@ export const useBackendDataStore = defineStore("backendData", () => {
const licenses = ref<string[]>([]);
const orgRoles = ref<Role[]>([]);
const projectRoles = ref<Role[]>([]);
const globalRoles = ref<Role[]>([]);
const channelColors = ref<Color[]>([]);
const flagReasons = ref<FlagReason[]>([]);
@ -86,6 +87,7 @@ export const useBackendDataStore = defineStore("backendData", () => {
fetchIfNeeded(async () => useInternalApi("data/orgRoles", false), orgRoles),
fetchIfNeeded(async () => useInternalApi("data/channelColors", false), channelColors),
fetchIfNeeded(async () => useInternalApi("data/projectRoles", false), projectRoles),
fetchIfNeeded(async () => useInternalApi("data/globalRoles", false), globalRoles),
fetchIfNeeded(async () => useInternalApi("data/flagReasons", false), flagReasons),
]);
} catch (e) {
@ -113,6 +115,7 @@ export const useBackendDataStore = defineStore("backendData", () => {
visibilities,
orgRoles,
projectRoles,
globalRoles,
channelColors,
flagReasons,
initBackendData,

View File

@ -86,6 +86,7 @@ public class WebConfig extends WebMvcConfigurationSupport {
} else {
corsRegistration.allowedOrigins(hangarConfig.getBaseUrl());
}
corsRegistration.allowedMethods("GET", "HEAD", "POST", "DELETE");
}
@Bean

View File

@ -17,6 +17,7 @@ import io.papermc.hangar.model.common.Prompt;
import io.papermc.hangar.model.common.projects.Category;
import io.papermc.hangar.model.common.projects.FlagReason;
import io.papermc.hangar.model.common.projects.Visibility;
import io.papermc.hangar.model.common.roles.GlobalRole;
import io.papermc.hangar.model.common.roles.OrganizationRole;
import io.papermc.hangar.model.common.roles.ProjectRole;
import io.papermc.hangar.security.annotations.Anyone;
@ -33,6 +34,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@ -132,6 +134,11 @@ public class BackendDataController {
return ResponseEntity.ok(ProjectRole.getAssignableRoles());
}
@GetMapping("/globalRoles")
public ResponseEntity<List<GlobalRole>> getGlobalRoles() {
return ResponseEntity.ok(Arrays.stream(GlobalRole.values()).toList());
}
@GetMapping("/orgRoles")
public ResponseEntity<List<OrganizationRole>> getAssignableOrganizationRoles() {
return ResponseEntity.ok(OrganizationRole.getAssignableRoles());

View File

@ -14,8 +14,10 @@ import io.papermc.hangar.controller.extras.pagination.filters.log.LogVersionFilt
import io.papermc.hangar.model.api.PaginatedResult;
import io.papermc.hangar.model.api.requests.RequestPagination;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.common.roles.GlobalRole;
import io.papermc.hangar.model.db.JobTable;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.roles.GlobalRoleTable;
import io.papermc.hangar.model.internal.admin.health.MissingFileCheck;
import io.papermc.hangar.model.internal.admin.health.UnhealthyProject;
import io.papermc.hangar.model.internal.api.requests.StringContent;
@ -28,6 +30,7 @@ import io.papermc.hangar.service.internal.JobService;
import io.papermc.hangar.service.internal.PlatformService;
import io.papermc.hangar.service.internal.admin.HealthService;
import io.papermc.hangar.service.internal.admin.StatService;
import io.papermc.hangar.service.internal.perms.roles.GlobalRoleService;
import io.papermc.hangar.service.internal.users.UserService;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
@ -36,6 +39,7 @@ import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@ -60,15 +64,17 @@ public class AdminController extends HangarComponent {
private final JobService jobService;
private final UserService userService;
private final ObjectMapper mapper;
private final GlobalRoleService globalRoleService;
@Autowired
public AdminController(PlatformService platformService, StatService statService, HealthService healthService, JobService jobService, UserService userService, ObjectMapper mapper) {
public AdminController(PlatformService platformService, StatService statService, HealthService healthService, JobService jobService, UserService userService, ObjectMapper mapper, GlobalRoleService globalRoleService) {
this.platformService = platformService;
this.statService = statService;
this.healthService = healthService;
this.jobService = jobService;
this.userService = userService;
this.mapper = mapper;
this.globalRoleService = globalRoleService;
}
@ResponseStatus(HttpStatus.OK)
@ -121,4 +127,18 @@ public class AdminController extends HangarComponent {
public PaginatedResult<HangarLoggedAction> getActionLog(@NotNull @ConfigurePagination(maxLimit = 50) RequestPagination pagination) {
return actionLogger.getLogs(pagination);
}
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(NamedPermission.EDIT_ALL_USER_SETTINGS)
@PostMapping(value = "/user/{user}/{role}")
public void addRole(@PathVariable UserTable user, @PathVariable String role) {
globalRoleService.addRole(new GlobalRoleTable(user.getUserId(), GlobalRole.byApiValue(role)));
}
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(NamedPermission.EDIT_ALL_USER_SETTINGS)
@DeleteMapping(value = "/user/{user}/{role}")
public void removeRole(@PathVariable UserTable user, @PathVariable String role) {
globalRoleService.deleteRole(new GlobalRoleTable(user.getUserId(), GlobalRole.byApiValue(role)));
}
}

View File

@ -1,10 +1,12 @@
package io.papermc.hangar.model.common.roles;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.papermc.hangar.db.customtypes.RoleCategory;
import io.papermc.hangar.model.common.Color;
import io.papermc.hangar.model.common.Permission;
import io.papermc.hangar.model.db.roles.GlobalRoleTable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -30,10 +32,10 @@ public enum GlobalRole implements Role<GlobalRoleTable> {
ADVISOR("Advisor", 13, Permission.None, "Advisor", Color.AQUA),
STONE_DONOR("Stone_Donor", 14, Permission.None, "Stone Donor", Color.GRAY, 5),
QUARTZ_DONOR("Quartz_Donor",15, Permission.None, "Quartz Donor", Color.QUARTZ, 4),
IRON_DONOR("Iron_Donor",16, Permission.None, "Iron Donor", Color.SILVER, 3),
GOLD_DONOR("Gold_Donor",17, Permission.None, "Gold Donor", Color.GOLD, 2),
DIAMOND_DONOR("Diamond_Donor",18, Permission.None, "Diamond Donor", Color.LIGHTBLUE, 1),
QUARTZ_DONOR("Quartz_Donor", 15, Permission.None, "Quartz Donor", Color.QUARTZ, 4),
IRON_DONOR("Iron_Donor", 16, Permission.None, "Iron Donor", Color.SILVER, 3),
GOLD_DONOR("Gold_Donor", 17, Permission.None, "Gold Donor", Color.GOLD, 2),
DIAMOND_DONOR("Diamond_Donor", 18, Permission.None, "Diamond Donor", Color.LIGHTBLUE, 1),
ORGANIZATION("Organization", 23, OrganizationRole.ORGANIZATION_OWNER.getPermissions(), "Organization", Color.PURPLE);
@ -108,4 +110,13 @@ public enum GlobalRole implements Role<GlobalRoleTable> {
public GlobalRoleTable create(@Nullable Long ignored, long userId, boolean ignoredToo) {
return new GlobalRoleTable(userId, this);
}
public static GlobalRole byApiValue(String apiValue) {
for (GlobalRole value : values()) {
if (value.value.endsWith(apiValue)) {
return value;
}
}
throw new IllegalArgumentException("No GlobalRole '" + apiValue + "'");
}
}

View File

@ -31,7 +31,7 @@ public enum OrganizationRole implements Role<OrganizationRoleTable> {
private final boolean isAssignable;
private static final OrganizationRole[] VALUES = values();
private static final List<OrganizationRole> ASSIGNABLE_ROLES = Arrays.stream(VALUES).filter(OrganizationRole::isAssignable).collect(Collectors.toList());
private static final List<OrganizationRole> ASSIGNABLE_ROLES = Arrays.stream(VALUES).filter(OrganizationRole::isAssignable).toList();
public static OrganizationRole[] getValues() { return VALUES; }
public static List<OrganizationRole> getAssignableRoles() { return ASSIGNABLE_ROLES; }

View File

@ -30,7 +30,7 @@ public enum ProjectRole implements Role<ProjectRoleTable> {
private final boolean isAssignable;
private static final ProjectRole[] VALUES = values();
private static final List<ProjectRole> ASSIGNABLE_ROLES = Arrays.stream(VALUES).filter(ProjectRole::isAssignable).collect(Collectors.toList());
private static final List<ProjectRole> ASSIGNABLE_ROLES = Arrays.stream(VALUES).filter(ProjectRole::isAssignable).toList();
public static ProjectRole[] getValues() {
return VALUES;