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 type { AuthedRequest } from "../../auth/middleware";
|
||||||
import { getOrganisationById, getUserById, updateOrganisationMemberRole } from "../../db/queries";
|
import {
|
||||||
|
getOrganisationById,
|
||||||
|
getOrganisationMemberRole,
|
||||||
|
getUserById,
|
||||||
|
updateOrganisationMemberRole,
|
||||||
|
} from "../../db/queries";
|
||||||
|
|
||||||
// /organisation/update-member-role?organisationId=1&userId=2&role=admin
|
// /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 url = new URL(req.url);
|
||||||
const organisationId = url.searchParams.get("organisationId");
|
const organisationId = url.searchParams.get("organisationId");
|
||||||
const userId = url.searchParams.get("userId");
|
const userId = url.searchParams.get("userId");
|
||||||
const role = url.searchParams.get("role");
|
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) {
|
if (!organisationId || !userId || !role) {
|
||||||
return new Response(
|
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 });
|
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);
|
return Response.json(member);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { OrganisationMemberResponse, OrganisationResponse } from "@issue/shared";
|
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 type { ReactNode } from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { AddMemberDialog } from "@/components/add-member-dialog";
|
import { AddMemberDialog } from "@/components/add-member-dialog";
|
||||||
@@ -30,9 +30,21 @@ function OrganisationsDialog({
|
|||||||
const [members, setMembers] = useState<OrganisationMemberResponse[]>([]);
|
const [members, setMembers] = useState<OrganisationMemberResponse[]>([]);
|
||||||
const [confirmDialog, setConfirmDialog] = useState<{
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
memberUserId: number;
|
title: string;
|
||||||
memberName: string;
|
message: string;
|
||||||
}>({ open: false, memberUserId: 0, memberName: "" });
|
confirmText: string;
|
||||||
|
processingText: string;
|
||||||
|
variant: "default" | "destructive";
|
||||||
|
onConfirm: () => Promise<void>;
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
title: "",
|
||||||
|
message: "",
|
||||||
|
confirmText: "",
|
||||||
|
processingText: "",
|
||||||
|
variant: "default",
|
||||||
|
onConfirm: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
const refetchMembers = useCallback(async () => {
|
const refetchMembers = useCallback(async () => {
|
||||||
if (!selectedOrganisation) return;
|
if (!selectedOrganisation) return;
|
||||||
@@ -60,34 +72,70 @@ function OrganisationsDialog({
|
|||||||
}
|
}
|
||||||
}, [selectedOrganisation]);
|
}, [selectedOrganisation]);
|
||||||
|
|
||||||
const handleRemoveMember = async (memberUserId: number, memberName: string) => {
|
const handleRoleChange = (memberUserId: number, memberName: string, currentRole: string) => {
|
||||||
setConfirmDialog({ open: true, memberUserId, memberName });
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmRemoveMember = async () => {
|
|
||||||
if (!selectedOrganisation) return;
|
if (!selectedOrganisation) return;
|
||||||
|
const action = currentRole === "admin" ? "demote" : "promote";
|
||||||
try {
|
const newRole = currentRole === "admin" ? "member" : "admin";
|
||||||
await organisation.removeMember({
|
setConfirmDialog({
|
||||||
organisationId: selectedOrganisation.Organisation.id,
|
open: true,
|
||||||
userId: confirmDialog.memberUserId,
|
title: action === "promote" ? "Promote Member" : "Demote Member",
|
||||||
onSuccess: () => {
|
message: `Are you sure you want to ${action} ${memberName} to ${newRole}?`,
|
||||||
setConfirmDialog({ open: false, memberUserId: 0, memberName: "" });
|
confirmText: action === "promote" ? "Promote" : "Demote",
|
||||||
void refetchMembers();
|
processingText: action === "promote" ? "Promoting..." : "Demoting...",
|
||||||
},
|
variant: action === "demote" ? "destructive" : "default",
|
||||||
onError: (error) => {
|
onConfirm: async () => {
|
||||||
console.error(error);
|
try {
|
||||||
},
|
await organisation.updateMemberRole({
|
||||||
});
|
organisationId: selectedOrganisation.Organisation.id,
|
||||||
} catch (err) {
|
userId: memberUserId,
|
||||||
console.error(err);
|
role: newRole,
|
||||||
}
|
onSuccess: () => {
|
||||||
|
closeConfirmDialog();
|
||||||
|
void refetchMembers();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// useEffect(() => {
|
const closeConfirmDialog = () => {
|
||||||
// if (!open) return;
|
setConfirmDialog((prev) => ({ ...prev, open: false }));
|
||||||
// void refetchOrganisations();
|
};
|
||||||
// }, [open, refetchOrganisations]);
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -168,24 +216,47 @@ function OrganisationsDialog({
|
|||||||
{member.OrganisationMember.role}
|
{member.OrganisationMember.role}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{(selectedOrganisation.OrganisationMember.role === "owner" ||
|
<div className="flex items-center gap-2">
|
||||||
selectedOrganisation.OrganisationMember.role ===
|
{(selectedOrganisation.OrganisationMember.role ===
|
||||||
"admin") &&
|
"owner" ||
|
||||||
member.OrganisationMember.role !== "owner" &&
|
selectedOrganisation.OrganisationMember.role ===
|
||||||
member.User.id !== user.id && (
|
"admin") &&
|
||||||
<Button
|
member.OrganisationMember.role !== "owner" &&
|
||||||
variant="dummy"
|
member.User.id !== user.id && (
|
||||||
size="none"
|
<>
|
||||||
onClick={() =>
|
<Button
|
||||||
handleRemoveMember(
|
variant="dummy"
|
||||||
member.User.id,
|
size="none"
|
||||||
member.User.name,
|
onClick={() =>
|
||||||
)
|
handleRoleChange(
|
||||||
}
|
member.User.id,
|
||||||
>
|
member.User.name,
|
||||||
<X className="size-5 text-destructive" />
|
member.OrganisationMember.role,
|
||||||
</Button>
|
)
|
||||||
)}
|
}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -211,13 +282,13 @@ function OrganisationsDialog({
|
|||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={confirmDialog.open}
|
open={confirmDialog.open}
|
||||||
onOpenChange={(open) => setConfirmDialog({ ...confirmDialog, open })}
|
onOpenChange={(open) => setConfirmDialog((prev) => ({ ...prev, open }))}
|
||||||
onConfirm={confirmRemoveMember}
|
onConfirm={confirmDialog.onConfirm}
|
||||||
title="Remove Member"
|
title={confirmDialog.title}
|
||||||
processingText="Removing..."
|
processingText={confirmDialog.processingText}
|
||||||
message={`Are you sure you want to remove ${confirmDialog.memberName} from this organisation?`}
|
message={confirmDialog.message}
|
||||||
confirmText="Remove"
|
confirmText={confirmDialog.confirmText}
|
||||||
variant="destructive"
|
variant={confirmDialog.variant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export { byUser } from "@/lib/server/organisation/byUser";
|
|||||||
export { create } from "@/lib/server/organisation/create";
|
export { create } from "@/lib/server/organisation/create";
|
||||||
export { members } from "@/lib/server/organisation/members";
|
export { members } from "@/lib/server/organisation/members";
|
||||||
export { removeMember } from "@/lib/server/organisation/removeMember";
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
todo.md
1
todo.md
@@ -15,4 +15,3 @@
|
|||||||
- time tracking (linked to issues or standalone)
|
- time tracking (linked to issues or standalone)
|
||||||
- user preferences
|
- user preferences
|
||||||
- "assign to me by default" option for new issues
|
- "assign to me by default" option for new issues
|
||||||
- org member role promotion/demotion
|
|
||||||
|
|||||||
Reference in New Issue
Block a user