mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-30 14:30:08 +08:00
Allow org transfer (half tested)
This commit is contained in:
parent
15a3111659
commit
b628a9afbd
66
frontend/src/components/modals/OrgTransferModal.vue
Normal file
66
frontend/src/components/modals/OrgTransferModal.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import Button from "~/lib/components/design/Button.vue";
|
||||
import Modal from "~/lib/components/modals/Modal.vue";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useApi, useInternalApi } from "~/composables/useApi";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import InputAutocomplete from "~/lib/components/ui/InputAutocomplete.vue";
|
||||
import { ref } from "vue";
|
||||
import { PaginatedResult, User } from "hangar-api";
|
||||
import { useNotificationStore } from "~/store/notification";
|
||||
|
||||
const props = defineProps<{
|
||||
organization: string;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const ctx = useContext();
|
||||
const router = useRouter();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const search = ref<string>("");
|
||||
const result = ref<string[]>([]);
|
||||
const loading = ref<boolean>(false);
|
||||
async function doSearch(val: string) {
|
||||
result.value = [];
|
||||
const users = await useApi<PaginatedResult<User>>("users", false, "get", {
|
||||
query: val,
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
});
|
||||
result.value = users.result.filter((u) => !u.isOrganization).map((u) => u.name);
|
||||
}
|
||||
|
||||
async function transfer() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await useInternalApi<string>(`organizations/org/${props.organization}/transfer`, true, "post", {
|
||||
content: search.value,
|
||||
});
|
||||
notificationStore.success(i18n.t("organization.settings.success.transferRequest", [search.value]));
|
||||
} catch (e) {
|
||||
handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="i18n.t('organization.settings.transferModal.title', [organization])" window-classes="w-150">
|
||||
<template #default>
|
||||
<p class="mb-2">{{ i18n.t("organization.settings.transferModal.description", [organization]) }}</p>
|
||||
<div class="flex items-center">
|
||||
<InputAutocomplete v-model="search" :values="result" :label="i18n.t('organization.settings.transferModal.transferTo')" @search="doSearch" />
|
||||
<Button :disabled="search.length === 0" :loading="loading" class="ml-2" @click="transfer">
|
||||
<IconMdiRenameBox class="mr-2" />
|
||||
{{ i18n.t("project.settings.transfer") }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #activator="{ on }">
|
||||
<Button size="small" class="mr-1" v-on="on"><IconMdiCogTransfer /></Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
@ -65,9 +65,7 @@ const canLeave = computed<boolean>(() => {
|
||||
|
||||
return props.members.some((member) => member.user.id === authStore.user?.id && member.user.id !== props.owner);
|
||||
});
|
||||
const canEdit = computed<boolean>(() => {
|
||||
return hasPerms(NamedPermission.EDIT_SUBJECT_SETTINGS);
|
||||
});
|
||||
const canEdit = computed<boolean>(() => hasPerms(NamedPermission.EDIT_SUBJECT_SETTINGS));
|
||||
const saving = ref<boolean>(false);
|
||||
const search = ref<string>("");
|
||||
const addErrors = ref<string[]>([]);
|
||||
|
@ -52,7 +52,7 @@
|
||||
"noProjects": "There are no projects. 😢",
|
||||
"noProjectsFound": "Found 0 projects. 😢",
|
||||
"title": "Find your favorite plugins",
|
||||
"subTitle": "Hangar allows you to find the best Velocity, Waterfall or Paper plugins for your Minecraft Servers.",
|
||||
"subTitle": "Hangar allows you to find the best Paper, Velocity, or Waterfall plugins for your Minecraft server.",
|
||||
"categories": "Categories",
|
||||
"licenses": "Licenses",
|
||||
"versions": "Minecraft versions",
|
||||
@ -609,6 +609,14 @@
|
||||
"notMember": "{0} is not a member of the organization, therefore you cannot edit their role",
|
||||
"invalidRole": "{0} cannot be added/removed from the organization",
|
||||
"pendingTransfer": "You already have a pending transfer"
|
||||
},
|
||||
"transferModal": {
|
||||
"title": "Transfer {0} to another user",
|
||||
"description": "After you click the transfer button, the selected user will receive an invitation to join {0} as the new owner. If they accept, you will be demoted to the secondary owner rank.",
|
||||
"transferTo": "User"
|
||||
},
|
||||
"success": {
|
||||
"transferRequest": "Successfully sent a transfer request to {0}. You can cancel this via the member list."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -712,6 +720,7 @@
|
||||
"watching": "Watching",
|
||||
"stars": "Stars",
|
||||
"orgs": "Organizations",
|
||||
"management": "Management",
|
||||
"viewOnForums": "View on forums ",
|
||||
"taglineLabel": "User Tagline",
|
||||
"editTagline": "Edit Tagline",
|
||||
@ -729,7 +738,8 @@
|
||||
"unlock": "Unlock Account",
|
||||
"apiKeys": "API Keys",
|
||||
"activity": "User Activity",
|
||||
"admin": "User Admin"
|
||||
"admin": "User Admin",
|
||||
"transfer": "Transfer organization to another user"
|
||||
},
|
||||
"lock": {
|
||||
"confirmLock": "Lock {0}'s account?",
|
||||
|
@ -119,7 +119,6 @@ watch(selectedTab, (val) => history.replaceState({}, "", route.path + "#" + val)
|
||||
const search = ref<string>("");
|
||||
const result = ref<string[]>([]);
|
||||
async function doSearch(val: string) {
|
||||
//TODO also include orgs
|
||||
result.value = [];
|
||||
const users = await useApi<PaginatedResult<User>>("users", false, "get", {
|
||||
query: val,
|
||||
|
@ -28,6 +28,7 @@ import IconMdiEyeOffOutline from "~icons/mdi/eye-off-outline";
|
||||
import OrgVisibilityModal from "~/components/modals/OrgVisibilityModal.vue";
|
||||
import LockUserModal from "~/components/modals/LockUserModal.vue";
|
||||
import ProjectCard from "~/components/projects/ProjectCard.vue";
|
||||
import OrgTransferModal from "~/components/modals/OrgTransferModal.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
user: User;
|
||||
@ -61,10 +62,10 @@ const buttons = computed<UserButton[]>(() => {
|
||||
if (hasPerms(NamedPermission.EDIT_ALL_USER_SETTINGS)) {
|
||||
list.push({ icon: IconMdiKey, attr: { to: "/" + props.user.name + "/settings/api-keys" }, name: "apiKeys" });
|
||||
}
|
||||
}
|
||||
if ((hasPerms(NamedPermission.MOD_NOTES_AND_FLAGS) || hasPerms(NamedPermission.REVIEWER)) && !props.user.isOrganization) {
|
||||
if (hasPerms(NamedPermission.MOD_NOTES_AND_FLAGS) || hasPerms(NamedPermission.REVIEWER)) {
|
||||
list.push({ icon: IconMdiCalendar, attr: { to: `/admin/activities/${props.user.name}` }, name: "activity" });
|
||||
}
|
||||
}
|
||||
if (hasPerms(NamedPermission.EDIT_ALL_USER_SETTINGS)) {
|
||||
list.push({ icon: IconMdiWrench, attr: { to: "/admin/user/" + props.user.name }, name: "admin" });
|
||||
}
|
||||
@ -91,7 +92,10 @@ useHead(useSeo(props.user.name, props.user.name + " is an author on Hangar. " +
|
||||
</div>
|
||||
<div class="flex-basis-full flex-grow md:max-w-1/3 md:min-w-1/3">
|
||||
<Card v-if="buttons.length !== 0" class="mb-4 border-solid border-top-4 border-top-red-500 dark:border-top-red-500">
|
||||
<template #header> Admin actions </template>
|
||||
<template #header>{{ i18n.t("author.management") }}</template>
|
||||
<Tooltip v-if="hasPerms(NamedPermission.IS_SUBJECT_OWNER)" :content="i18n.t('author.tooltips.transfer')">
|
||||
<OrgTransferModal :organization="user.name" />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip v-for="btn in buttons" :key="btn.name">
|
||||
<template #content>
|
||||
|
@ -94,7 +94,7 @@ public class OrganizationController extends HangarComponent {
|
||||
@RateLimit(overdraft = 7, refillTokens = 2, refillSeconds = 10)
|
||||
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#name}")
|
||||
@PostMapping(path = "/org/{name}/members/add", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void addProjectMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
|
||||
public void addOrganizationMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
|
||||
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
|
||||
if (organizationTable == null) {
|
||||
throw new HangarApiException("Org " + name + " doesn't exist");
|
||||
@ -107,7 +107,7 @@ public class OrganizationController extends HangarComponent {
|
||||
@RateLimit(overdraft = 5, refillTokens = 2, refillSeconds = 10)
|
||||
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#name}")
|
||||
@PostMapping(path = "/org/{name}/members/edit", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void editProjectMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
|
||||
public void editOrganizationMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
|
||||
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
|
||||
memberService.editMember(member, organizationTable);
|
||||
}
|
||||
@ -117,7 +117,7 @@ public class OrganizationController extends HangarComponent {
|
||||
@RateLimit(overdraft = 7, refillTokens = 2, refillSeconds = 10)
|
||||
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#name}")
|
||||
@PostMapping(path = "/org/{name}/members/remove", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void removeProjectMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
|
||||
public void removeOrganizationMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
|
||||
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
|
||||
memberService.removeMember(member, organizationTable);
|
||||
}
|
||||
@ -126,11 +126,30 @@ public class OrganizationController extends HangarComponent {
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_MEMBER, args = "{#name}")
|
||||
@PostMapping(path = "/org/{name}/members/leave", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void leaveProject(@PathVariable String name) {
|
||||
public void leaveOrganization(@PathVariable String name) {
|
||||
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
|
||||
memberService.leave(organizationTable);
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_OWNER, args = "{#name}")
|
||||
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 60)
|
||||
@PostMapping(path = "/org/{name}/transfer", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void transferProject(@PathVariable String name, @Valid @RequestBody StringContent nameContent) {
|
||||
final OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
|
||||
inviteService.sendTransferRequest(nameContent.getContent(), organizationTable);
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_OWNER, args = "{#name}")
|
||||
@PostMapping(path = "/org/{name}/canceltransfer", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void transferProject(@PathVariable String name) {
|
||||
final OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
|
||||
inviteService.cancelTransferRequest(organizationTable);
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@RateLimit(overdraft = 3, refillTokens = 1, refillSeconds = 60)
|
||||
|
@ -169,7 +169,6 @@ public class ProjectController extends HangarComponent {
|
||||
@Unlocked
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@PermissionRequired(type = PermissionType.PROJECT, perms = NamedPermission.IS_SUBJECT_OWNER, args = "{#author, #slug}")
|
||||
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 60)
|
||||
@PostMapping(path = "/project/{author}/{slug}/canceltransfer", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void transferProject(@PathVariable String author, @PathVariable String slug) {
|
||||
final ProjectTable projectTable = projectService.getProjectTable(author, slug);
|
||||
|
Loading…
Reference in New Issue
Block a user