mirror of
https://github.com/HangarMC/Hangar.git
synced 2024-11-21 01:21:54 +08:00
parent
ce91b3998b
commit
a91c8f684a
45
frontend/src/components/modals/MemberLeaveModal.vue
Normal file
45
frontend/src/components/modals/MemberLeaveModal.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<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 { useInternalApi } from "~/composables/useApi";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
author: string;
|
||||
organization?: boolean;
|
||||
slug?: string;
|
||||
}>(),
|
||||
{
|
||||
organization: false,
|
||||
slug: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const i18n = useI18n();
|
||||
const ctx = useContext();
|
||||
const router = useRouter();
|
||||
const name = props.organization ? props.author : props.slug;
|
||||
|
||||
async function leave() {
|
||||
const url = props.organization ? `organizations/org/${props.author}/members/leave` : `projects/project/${props.author}/${props.slug}/members/leave`;
|
||||
useInternalApi(url, true, "post")
|
||||
.then(() => router.go(0))
|
||||
.catch((e) => handleRequestError(e, ctx, i18n));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="i18n.t('form.memberList.leaveModal.title', [name])" window-classes="w-150">
|
||||
<template #default>
|
||||
<p>{{ i18n.t("form.memberList.leaveModal.description", [name]) }}</p>
|
||||
<Button class="mt-3" size="small" button-type="red" @click="leave()"> {{ i18n.t("form.memberList.leave") }} </Button>
|
||||
</template>
|
||||
<template #activator="{ on }">
|
||||
<Button class="text-base" size="small" button-type="red" v-on="on"> {{ i18n.t("form.memberList.leave") }} </Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
@ -18,6 +18,8 @@ import { useContext } from "vite-ssr/vue";
|
||||
import IconMdiClock from "~icons/mdi/clock";
|
||||
import Tooltip from "~/lib/components/design/Tooltip.vue";
|
||||
import InputAutocomplete from "~/lib/components/ui/InputAutocomplete.vue";
|
||||
import { useAuthStore } from "~/store/auth";
|
||||
import MemberLeaveModal from "~/components/modals/MemberLeaveModal.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -27,6 +29,7 @@ const props = withDefaults(
|
||||
organization?: boolean;
|
||||
author?: string;
|
||||
slug?: string;
|
||||
owner: number;
|
||||
}>(),
|
||||
{
|
||||
organization: false,
|
||||
@ -51,8 +54,16 @@ const store = useBackendDataStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const ctx = useContext();
|
||||
const authStore = useAuthStore();
|
||||
const roles: Role[] = props.organization ? store.orgRoles : store.projectRoles;
|
||||
|
||||
const canLeave = computed<boolean>(() => {
|
||||
if (!authStore.user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
@ -113,7 +124,7 @@ function convertMember(member: JoinableMember): EditableMember {
|
||||
}
|
||||
|
||||
async function doSearch(val: string) {
|
||||
result.value = ["Dum", "Dum2"];
|
||||
result.value = [];
|
||||
const users = await useApi<PaginatedResult<User>>("users", false, "get", {
|
||||
query: val,
|
||||
limit: 25,
|
||||
@ -133,6 +144,7 @@ interface EditableMember {
|
||||
<template #header>
|
||||
<div class="inline-flex w-full flex-cols space-between">
|
||||
<h3 class="flex-grow">{{ i18n.t("project.members") }}</h3>
|
||||
<MemberLeaveModal v-if="canLeave" :author="author" :organization="organization" :slug="slug" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -611,7 +611,12 @@
|
||||
"addUser": "Add User...",
|
||||
"create": "Create",
|
||||
"editUser": "Edit User",
|
||||
"invitedAs": "Pending invite as {0}"
|
||||
"invitedAs": "Pending invite as {0}",
|
||||
"leave": "Leave",
|
||||
"leaveModal": {
|
||||
"title": "Leave {0}",
|
||||
"description": "Are you sure you want to leave {0}? After you leave, you can only rejoin after receiving a new invitation."
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -131,7 +131,7 @@ function createPinnedVersionUrl(version: PinnedVersion): string {
|
||||
</ul>
|
||||
</Card>
|
||||
<ProjectPageList :project="project" :open="openProjectPages" />
|
||||
<MemberList :members="project.members" :author="project.owner.name" :slug="project.name" class="overflow-visible" />
|
||||
<MemberList :members="project.members" :author="project.owner.name" :slug="project.name" :owner="project.owner.userId" class="overflow-visible" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -390,7 +390,13 @@ useHead(
|
||||
</template>
|
||||
</Tabs>
|
||||
</Card>
|
||||
<MemberList :members="project.members" :author="project.owner.name" :slug="project.name" class="basis-full md:basis-3/12 h-max" />
|
||||
<MemberList
|
||||
:members="project.members"
|
||||
:author="project.owner.name"
|
||||
:slug="project.name"
|
||||
:owner="project.owner.userId"
|
||||
class="basis-full md:basis-3/12 h-max"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -162,7 +162,7 @@ useHead(useSeo(props.user.name, props.user.name + " is an author on Hangar. " +
|
||||
</ul>
|
||||
</Card>
|
||||
</template>
|
||||
<MemberList v-else :members="organization.members" :roles="orgRoles" organization :author="user.name" />
|
||||
<MemberList v-else :members="organization.members" :roles="orgRoles" organization :author="user.name" :owner="organization.owner.userId" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -78,12 +78,10 @@ async function markNotificationRead(notification: HangarNotification, router = t
|
||||
}
|
||||
}
|
||||
|
||||
async function updateInvite(invite: Invite, status: "accept" | "decline" | "unaccept") {
|
||||
async function updateInvite(invite: Invite, status: "accept" | "decline") {
|
||||
await useInternalApi(`invites/${invite.type}/${invite.roleTableId}/${status}`, true, "post").catch((e) => handleRequestError(e, ctx, i18n));
|
||||
if (status === "accept") {
|
||||
invite.accepted = true;
|
||||
} else if (status === "unaccept") {
|
||||
invite.accepted = false;
|
||||
} else {
|
||||
invites.value[invite.type] = invites.value[invite.type].filter((i) => i.roleTableId !== invite.roleTableId);
|
||||
}
|
||||
@ -151,10 +149,7 @@ function updateSelectedNotifications() {
|
||||
<Card v-for="(invite, index) in filteredInvites" :key="index">
|
||||
{{ i18n.t(!invite.accepted ? "notifications.invited" : "notifications.inviteAccepted", [invite.type]) }}:
|
||||
<router-link :to="invite.url" exact>{{ invite.name }}</router-link>
|
||||
<template v-if="invite.accepted">
|
||||
<Button class="ml-2" @click="updateInvite(invite, 'unaccept')">{{ i18n.t("notifications.invite.btns.unaccept") }}</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="!invite.accepted">
|
||||
<Button class="mr-2 ml-2" @click="updateInvite(invite, 'accept')">{{ i18n.t("notifications.invite.btns.accept") }}</Button>
|
||||
<Button @click="updateInvite(invite, 'decline')">{{ i18n.t("notifications.invite.btns.decline") }}</Button>
|
||||
</template>
|
||||
|
@ -245,13 +245,11 @@ public class HangarUserController extends HangarComponent {
|
||||
switch (status) {
|
||||
case DECLINE -> inviteService.declineInvite(table);
|
||||
case ACCEPT -> inviteService.acceptInvite(table);
|
||||
case UNACCEPT -> inviteService.unacceptInvite(table);
|
||||
}
|
||||
}
|
||||
|
||||
public enum InviteStatus {
|
||||
ACCEPT,
|
||||
DECLINE,
|
||||
UNACCEPT,
|
||||
DECLINE
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package io.papermc.hangar.controller.internal;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import io.papermc.hangar.HangarComponent;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.common.NamedPermission;
|
||||
@ -46,8 +45,6 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -125,6 +122,15 @@ public class OrganizationController extends HangarComponent {
|
||||
memberService.removeMember(member, organizationTable);
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@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) {
|
||||
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
|
||||
memberService.leave(organizationTable);
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@RateLimit(overdraft = 3, refillTokens = 1, refillSeconds = 60)
|
||||
|
@ -185,6 +185,15 @@ public class ProjectController extends HangarComponent {
|
||||
projectMemberService.removeMember(member, projectTable);
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@PermissionRequired(type = PermissionType.PROJECT, perms = NamedPermission.IS_SUBJECT_MEMBER, args = "{#author, #slug}")
|
||||
@PostMapping(path = "/project/{author}/{slug}/members/leave", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void leaveProject(@PathVariable String author, @PathVariable String slug) {
|
||||
ProjectTable projectTable = projectService.getProjectTable(author, slug);
|
||||
projectMemberService.leave(projectTable);
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@VisibilityRequired(type = Type.PROJECT, args = "{#projectId}")
|
||||
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 10)
|
||||
|
@ -65,7 +65,6 @@ public class LogAction<LC extends LogContext<? extends LoggedActionTable, LC>> {
|
||||
// TODO create organization
|
||||
public static final LogAction<OrganizationContext> ORGANIZATION_INVITES_SENT = new LogAction<>(PGLoggedAction.ORGANIZATION_INVITES_SENT, "Organization Invites Sent");
|
||||
public static final LogAction<OrganizationContext> ORGANIZATION_INVITE_DECLINED = new LogAction<>(PGLoggedAction.ORGANIZATION_INVITE_DECLINED, "Organization Invite Declined");
|
||||
public static final LogAction<OrganizationContext> ORGANIZATION_INVITE_UNACCEPTED = new LogAction<>(PGLoggedAction.ORGANIZATION_INVITE_UNACCEPTED, "Organization Invite Unaccepted");
|
||||
public static final LogAction<OrganizationContext> ORGANIZATION_MEMBER_ADDED = new LogAction<>(PGLoggedAction.ORGANIZATION_MEMBER_ADDED, "Organization Member Added");
|
||||
public static final LogAction<OrganizationContext> ORGANIZATION_MEMBERS_REMOVED = new LogAction<>(PGLoggedAction.ORGANIZATION_MEMBERS_REMOVED, "Organization Members Removed");
|
||||
public static final LogAction<OrganizationContext> ORGANIZATION_MEMBER_ROLES_CHANGED = new LogAction<>(PGLoggedAction.ORGANIZATION_MEMBER_ROLES_CHANGED, "Organization Member Roles Changed");
|
||||
|
@ -20,7 +20,6 @@ import io.papermc.hangar.service.internal.users.notifications.JoinableNotificati
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Transactional
|
||||
@ -80,14 +79,16 @@ public abstract class MemberService<
|
||||
membersDao.insert(constructor.create(roleTable.getUserId(), roleTable.getPrincipalId()));
|
||||
}
|
||||
|
||||
// TODO user's removing themselves from projects/organizations
|
||||
@Transactional
|
||||
public void removeMember(RT roleTable, String userName, boolean removeRole) {
|
||||
membersDao.delete(roleTable.getPrincipalId(), roleTable.getUserId());
|
||||
if (removeRole) {
|
||||
roleService.deleteRole(roleTable);
|
||||
public void leave(final J joinable) {
|
||||
final RT role = roleService.getRole(joinable.getId(), getHangarUserId());
|
||||
if (invalidRolesToChange().contains(role.getRole())) {
|
||||
throw new HangarApiException(this.errorPrefix + "invalidRole", role.getRole().getTitle());
|
||||
}
|
||||
logMemberRemoval(roleTable, "Removed:" + userName + " (" + roleTable.getRole().getTitle() + ")");
|
||||
|
||||
membersDao.delete(role.getPrincipalId(), role.getUserId());
|
||||
roleService.deleteRole(role);
|
||||
logMemberRemoval(role, "Left:" + getHangarPrincipal().getName() + " (" + role.getRole().getTitle() + ")");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
@ -19,7 +19,6 @@ import io.papermc.hangar.service.internal.users.NotificationService;
|
||||
import io.papermc.hangar.service.internal.users.notifications.JoinableNotificationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -112,22 +111,6 @@ public abstract class InviteService<LC extends LogContext<?, LC>, R extends Role
|
||||
roleTable.logAction(actionLogger, getInviteAcceptAction(), userTable.getName() + " accepted an invite for " + roleTable.getRole().getTitle(), roleTable.getCreatedAt().format(DateTimeFormatter.RFC_1123_DATE_TIME));
|
||||
}
|
||||
|
||||
public void unacceptInvite(RT roleTable) {
|
||||
if (!roleTable.isAccepted()) {
|
||||
throw new IllegalArgumentException("Cannot un-accept a non-accepted invite");
|
||||
}
|
||||
roleTable = roleService.changeAcceptance(roleTable, false);
|
||||
UserTable userTable = userDAO.getUserTable(roleTable.getUserId());
|
||||
memberService.removeMember(roleTable, userTable.getName(), false);
|
||||
logInviteUnaccepted(roleTable, userTable);
|
||||
}
|
||||
|
||||
abstract LogAction<LC> getInviteUnacceptAction();
|
||||
|
||||
protected void logInviteUnaccepted(RT roleTable, UserTable userTable) {
|
||||
roleTable.logAction(actionLogger, getInviteUnacceptAction(), userTable.getName() + " unaccepted an invite for " + roleTable.getRole().getTitle(), roleTable.getCreatedAt().format(DateTimeFormatter.RFC_1123_DATE_TIME));
|
||||
}
|
||||
|
||||
public void declineInvite(RT roleTable) {
|
||||
roleService.deleteRole(roleTable);
|
||||
logInviteDeclined(roleTable, userDAO.getUserTable(roleTable.getUserId()));
|
||||
|
@ -36,11 +36,6 @@ public class OrganizationInviteService extends InviteService<OrganizationContext
|
||||
return LogAction.ORGANIZATION_MEMBER_ADDED;
|
||||
}
|
||||
|
||||
@Override
|
||||
LogAction<OrganizationContext> getInviteUnacceptAction() {
|
||||
return LogAction.ORGANIZATION_INVITE_UNACCEPTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
LogAction<OrganizationContext> getInviteDeclineAction() {
|
||||
return LogAction.ORGANIZATION_INVITE_DECLINED;
|
||||
|
@ -11,7 +11,6 @@ import io.papermc.hangar.service.internal.perms.roles.ProjectRoleService;
|
||||
import io.papermc.hangar.service.internal.users.notifications.JoinableNotificationService.ProjectNotificationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@ -36,11 +35,6 @@ public class ProjectInviteService extends InviteService<ProjectContext, ProjectR
|
||||
return LogAction.PROJECT_MEMBER_ADDED;
|
||||
}
|
||||
|
||||
@Override
|
||||
LogAction<ProjectContext> getInviteUnacceptAction() {
|
||||
return LogAction.PROJECT_INVITE_UNACCEPTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
LogAction<ProjectContext> getInviteDeclineAction() {
|
||||
return LogAction.PROJECT_INVITE_DECLINED;
|
||||
|
Loading…
Reference in New Issue
Block a user