mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
org member promotion/demotion
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
import type { BunRequest } from "bun";
|
||||
import { getOrganisationById, getUserById, updateOrganisationMemberRole } from "../../db/queries";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import {
|
||||
getOrganisationById,
|
||||
getOrganisationMemberRole,
|
||||
getUserById,
|
||||
updateOrganisationMemberRole,
|
||||
} from "../../db/queries";
|
||||
|
||||
// /organisation/update-member-role?organisationId=1&userId=2&role=admin
|
||||
export default async function organisationUpdateMemberRole(req: BunRequest) {
|
||||
export default async function organisationUpdateMemberRole(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const organisationId = url.searchParams.get("organisationId");
|
||||
const userId = url.searchParams.get("userId");
|
||||
const role = url.searchParams.get("role");
|
||||
if (!role || !["admin", "member"].includes(role)) {
|
||||
return new Response("Invalid role: must be either 'admin' or 'member'", { status: 400 });
|
||||
}
|
||||
|
||||
if (!organisationId || !userId || !role) {
|
||||
return new Response(
|
||||
@@ -32,7 +40,21 @@ export default async function organisationUpdateMemberRole(req: BunRequest) {
|
||||
return new Response(`user with id ${userId} not found`, { status: 404 });
|
||||
}
|
||||
|
||||
const member = await updateOrganisationMemberRole(orgIdNumber, userIdNumber, role);
|
||||
const requesterMember = await getOrganisationMemberRole(orgIdNumber, req.userId);
|
||||
if (!requesterMember) {
|
||||
return new Response("You are not a member of this organisation", { status: 403 });
|
||||
}
|
||||
|
||||
let member = await getOrganisationMemberRole(orgIdNumber, userIdNumber);
|
||||
if (!member) {
|
||||
return new Response(`User with id ${userId} is not a member of this organisation`, { status: 404 });
|
||||
}
|
||||
|
||||
if (requesterMember.role !== "owner" && requesterMember.role !== "admin") {
|
||||
return new Response("Only owners and admins can update member roles", { status: 403 });
|
||||
}
|
||||
|
||||
member = await updateOrganisationMemberRole(orgIdNumber, userIdNumber, role);
|
||||
|
||||
return Response.json(member);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OrganisationMemberResponse, OrganisationResponse } from "@issue/shared";
|
||||
import { Plus, X } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, Plus, X } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { AddMemberDialog } from "@/components/add-member-dialog";
|
||||
@@ -30,9 +30,21 @@ function OrganisationsDialog({
|
||||
const [members, setMembers] = useState<OrganisationMemberResponse[]>([]);
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
open: boolean;
|
||||
memberUserId: number;
|
||||
memberName: string;
|
||||
}>({ open: false, memberUserId: 0, memberName: "" });
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText: string;
|
||||
processingText: string;
|
||||
variant: "default" | "destructive";
|
||||
onConfirm: () => Promise<void>;
|
||||
}>({
|
||||
open: false,
|
||||
title: "",
|
||||
message: "",
|
||||
confirmText: "",
|
||||
processingText: "",
|
||||
variant: "default",
|
||||
onConfirm: async () => {},
|
||||
});
|
||||
|
||||
const refetchMembers = useCallback(async () => {
|
||||
if (!selectedOrganisation) return;
|
||||
@@ -60,34 +72,70 @@ function OrganisationsDialog({
|
||||
}
|
||||
}, [selectedOrganisation]);
|
||||
|
||||
const handleRemoveMember = async (memberUserId: number, memberName: string) => {
|
||||
setConfirmDialog({ open: true, memberUserId, memberName });
|
||||
};
|
||||
|
||||
const confirmRemoveMember = async () => {
|
||||
const handleRoleChange = (memberUserId: number, memberName: string, currentRole: string) => {
|
||||
if (!selectedOrganisation) return;
|
||||
|
||||
try {
|
||||
await organisation.removeMember({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
userId: confirmDialog.memberUserId,
|
||||
onSuccess: () => {
|
||||
setConfirmDialog({ open: false, memberUserId: 0, memberName: "" });
|
||||
void refetchMembers();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
const action = currentRole === "admin" ? "demote" : "promote";
|
||||
const newRole = currentRole === "admin" ? "member" : "admin";
|
||||
setConfirmDialog({
|
||||
open: true,
|
||||
title: action === "promote" ? "Promote Member" : "Demote Member",
|
||||
message: `Are you sure you want to ${action} ${memberName} to ${newRole}?`,
|
||||
confirmText: action === "promote" ? "Promote" : "Demote",
|
||||
processingText: action === "promote" ? "Promoting..." : "Demoting...",
|
||||
variant: action === "demote" ? "destructive" : "default",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await organisation.updateMemberRole({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
userId: memberUserId,
|
||||
role: newRole,
|
||||
onSuccess: () => {
|
||||
closeConfirmDialog();
|
||||
void refetchMembers();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!open) return;
|
||||
// void refetchOrganisations();
|
||||
// }, [open, refetchOrganisations]);
|
||||
const closeConfirmDialog = () => {
|
||||
setConfirmDialog((prev) => ({ ...prev, open: false }));
|
||||
};
|
||||
|
||||
const handleRemoveMember = (memberUserId: number, memberName: string) => {
|
||||
if (!selectedOrganisation) return;
|
||||
setConfirmDialog({
|
||||
open: true,
|
||||
title: "Remove Member",
|
||||
message: `Are you sure you want to remove ${memberName} from this organisation?`,
|
||||
confirmText: "Remove",
|
||||
processingText: "Removing...",
|
||||
variant: "destructive",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await organisation.removeMember({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
userId: memberUserId,
|
||||
onSuccess: () => {
|
||||
closeConfirmDialog();
|
||||
void refetchMembers();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -168,24 +216,47 @@ function OrganisationsDialog({
|
||||
{member.OrganisationMember.role}
|
||||
</span>
|
||||
</div>
|
||||
{(selectedOrganisation.OrganisationMember.role === "owner" ||
|
||||
selectedOrganisation.OrganisationMember.role ===
|
||||
"admin") &&
|
||||
member.OrganisationMember.role !== "owner" &&
|
||||
member.User.id !== user.id && (
|
||||
<Button
|
||||
variant="dummy"
|
||||
size="none"
|
||||
onClick={() =>
|
||||
handleRemoveMember(
|
||||
member.User.id,
|
||||
member.User.name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<X className="size-5 text-destructive" />
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{(selectedOrganisation.OrganisationMember.role ===
|
||||
"owner" ||
|
||||
selectedOrganisation.OrganisationMember.role ===
|
||||
"admin") &&
|
||||
member.OrganisationMember.role !== "owner" &&
|
||||
member.User.id !== user.id && (
|
||||
<>
|
||||
<Button
|
||||
variant="dummy"
|
||||
size="none"
|
||||
onClick={() =>
|
||||
handleRoleChange(
|
||||
member.User.id,
|
||||
member.User.name,
|
||||
member.OrganisationMember.role,
|
||||
)
|
||||
}
|
||||
>
|
||||
{member.OrganisationMember.role ===
|
||||
"admin" ? (
|
||||
<ChevronDown className="size-5 text-yellow-500" />
|
||||
) : (
|
||||
<ChevronUp className="size-5 text-green-500" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="dummy"
|
||||
size="none"
|
||||
onClick={() =>
|
||||
handleRemoveMember(
|
||||
member.User.id,
|
||||
member.User.name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<X className="size-5 text-destructive" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -211,13 +282,13 @@ function OrganisationsDialog({
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmDialog.open}
|
||||
onOpenChange={(open) => setConfirmDialog({ ...confirmDialog, open })}
|
||||
onConfirm={confirmRemoveMember}
|
||||
title="Remove Member"
|
||||
processingText="Removing..."
|
||||
message={`Are you sure you want to remove ${confirmDialog.memberName} from this organisation?`}
|
||||
confirmText="Remove"
|
||||
variant="destructive"
|
||||
onOpenChange={(open) => setConfirmDialog((prev) => ({ ...prev, open }))}
|
||||
onConfirm={confirmDialog.onConfirm}
|
||||
title={confirmDialog.title}
|
||||
processingText={confirmDialog.processingText}
|
||||
message={confirmDialog.message}
|
||||
confirmText={confirmDialog.confirmText}
|
||||
variant={confirmDialog.variant}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -3,3 +3,4 @@ export { byUser } from "@/lib/server/organisation/byUser";
|
||||
export { create } from "@/lib/server/organisation/create";
|
||||
export { members } from "@/lib/server/organisation/members";
|
||||
export { removeMember } from "@/lib/server/organisation/removeMember";
|
||||
export { updateMemberRole } from "@/lib/server/organisation/updateMemberRole";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { getCsrfToken, getServerURL } from "@/lib/utils";
|
||||
import type { ServerQueryInput } from "..";
|
||||
|
||||
export async function updateMemberRole({
|
||||
organisationId,
|
||||
userId,
|
||||
role,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
organisationId: number;
|
||||
userId: number;
|
||||
role: string;
|
||||
} & ServerQueryInput) {
|
||||
const url = new URL(`${getServerURL()}/organisation/update-member-role`);
|
||||
url.searchParams.set("organisationId", `${organisationId}`);
|
||||
url.searchParams.set("userId", `${userId}`);
|
||||
url.searchParams.set("role", role);
|
||||
|
||||
const csrfToken = getCsrfToken();
|
||||
const headers: HeadersInit = {};
|
||||
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text();
|
||||
onError?.(error || `failed to update member role (${res.status})`);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
onSuccess?.(data, res);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user