Skip to content

Commit

Permalink
✨ Adapt phrosfire's PR364
Browse files Browse the repository at this point in the history
  • Loading branch information
MathiasGruber committed Feb 21, 2025
1 parent bfd5cd4 commit a234f76
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 100 deletions.
137 changes: 72 additions & 65 deletions app/src/layout/Clan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import type { MutateContentSchema } from "@/validators/comments";
import type { UserNindo } from "@/drizzle/schema";
import type { ArrayElement } from "@/utils/typeutils";
import type { ClanRouter } from "@/routers/clan";
import { canEditClans } from "@/utils/permissions";

export const ClansOverview: React.FC = () => {
// Must be in allied village
Expand Down Expand Up @@ -1209,73 +1210,79 @@ export const ClanMembers: React.FC<ClanMembersProps> = (props) => {
// Derived
const isColeader = checkCoLeader(userId, clanData);
const isLeader = userId === clanData.leaderId;
const canEdit = userData ? canEditClans(userData.role) : false;

// Adjust members for table
const members = clanData.members
.map((member) => {
const memberIsLeader = member.userId === clanData.leaderId;
const memberIsColeader = checkCoLeader(member.userId, clanData);
const memberLeaderLike = memberIsLeader || memberIsColeader;
return {
...member,
rank: memberIsLeader
? "Leader"
: memberIsColeader
? "Coleader"
: showUserRank(member),
actions: (
<div className="flex flex-row gap-1">
{member.userId !== userId && (
<>
{(isLeader || isColeader) && !memberLeaderLike && (
<Confirm
title="Kick Member"
proceed_label="Submit"
button={
<Button id={`kick-${member.userId}`}>
<DoorOpen className="mr-2 h-5 w-5" />
Kick
</Button>
}
onAccept={() => kick({ clanId, memberId: member.userId })}
>
Confirm that you want to kick this member from the clan.
</Confirm>
)}
{isLeader && memberLeaderLike && (
<Confirm
title="Demote Member"
button={
<Button id={`demote-${member.userId}`}>
<ArrowBigDownDash className="mr-2 h-5 w-5" />
Demote
</Button>
}
onAccept={() => demote({ clanId, memberId: member.userId })}
>
Confirm that you want to demote this member.
</Confirm>
)}
{(isLeader || (isColeader && !memberLeaderLike)) && (
<Confirm
title="Promote Member"
button={
<Button id={`promote-${member.userId}`}>
<ArrowBigUpDash className="mr-2 h-5 w-5" />
Promote
</Button>
}
onAccept={() => promote({ clanId, memberId: member.userId })}
>
Confirm that you want to promote this member to leader of the clan.
</Confirm>
)}
</>
)}
</div>
),
};
})
const members = clanData.members.map((member) => {
const memberIsLeader = member.userId === clanData.leaderId;
const memberIsColeader = checkCoLeader(member.userId, clanData);
const canKick =
canEdit || // canEdit role can kick anyone
(isLeader && !memberIsLeader) || // Leader can kick anyone except other leaders
(isColeader && !memberIsLeader && !memberIsColeader); // Co-leaders can kick normal members only
return {
...member,
rank: memberIsLeader ? "Leader" : memberIsColeader ? "Coleader" : showUserRank(member),
actions: (
<div className="flex flex-row gap-1">
{member.userId !== userId && (
<>
{/* KICK BUTTON (Now allows kicking leaders if canEdit is true) */}
{canKick && (
<Confirm
title="Kick Member"
proceed_label="Submit"
button={
<Button id={`kick-${member.userId}`}>
<DoorOpen className="mr-2 h-5 w-5" />
Kick
</Button>
}
onAccept={() => kick({ clanId, memberId: member.userId })}
>
{memberIsLeader
? "You are about to kick the leader. Ensure leadership transition is planned."
: "Confirm that you want to kick this member from the clan."}
</Confirm>
)}

{/* DEMOTE BUTTON */}
{(isLeader || canEdit) && (
<Confirm
title="Demote Member"
button={
<Button id={`demote-${member.userId}`}>
<ArrowBigDownDash className="mr-2 h-5 w-5" />
Demote
</Button>
}
onAccept={() => demote({ clanId, memberId: member.userId })}
>
Confirm that you want to demote this member.
</Confirm>
)}

{/* PROMOTE BUTTON */}
{(isLeader || (isColeader && !memberIsLeader && !memberIsColeader) || canEdit) && (
<Confirm
title="Promote Member"
button={
<Button id={`promote-${member.userId}`}>
<ArrowBigUpDash className="mr-2 h-5 w-5" />
Promote
</Button>
}
onAccept={() => promote({ clanId, memberId: member.userId })}
>
Confirm that you want to promote this member.
</Confirm>
)}
</>
)}
</div>
),
};
})
.sort((a, b) => {
if (a.rank === "Leader") return -1;
if (b.rank === "Leader") return 1;
Expand Down
98 changes: 63 additions & 35 deletions app/src/server/api/routers/clan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { fetchUser, updateNindo } from "@/routers/profile";
import { getServerPusher } from "@/libs/pusher";
import { clanCreateSchema, checkCoLeader } from "@/validators/clan";
import { hasRequiredRank } from "@/libs/train";
import { canEditClans } from "@/utils/permissions";
import { checkIfSectorIsAvailable } from "@/libs/clan";
import {
fetchRequest,
Expand Down Expand Up @@ -553,39 +554,58 @@ export const clanRouter = createTRPCRouter({
const isLeader = user.userId === fetchedClan?.leaderId;
const isColeader = checkCoLeader(user.userId, fetchedClan);
const isMemberColeader = checkCoLeader(input.memberId, fetchedClan);
const updateData = (() => {
if (isMemberColeader && isLeader)
return {
leaderId: input.memberId,
coLeader1:
fetchedClan.coLeader1 === input.memberId ? null : fetchedClan.coLeader1,
coLeader2:
fetchedClan.coLeader2 === input.memberId ? null : fetchedClan.coLeader2,
coLeader3:
fetchedClan.coLeader3 === input.memberId ? null : fetchedClan.coLeader3,
coLeader4:
fetchedClan.coLeader4 === input.memberId ? null : fetchedClan.coLeader4,
};
if (!fetchedClan?.coLeader1) return { coLeader1: input.memberId };
if (!fetchedClan?.coLeader2) return { coLeader2: input.memberId };
if (!fetchedClan?.coLeader3) return { coLeader3: input.memberId };
if (!fetchedClan?.coLeader4) return { coLeader4: input.memberId };
return null;
})();
const canEdit = canEditClans(user.role);
const isMemberLeader = input.memberId === fetchedClan?.leaderId;
// Guards
if (!user) return errorResponse("User not found");
if (!member) return errorResponse("Member not found");
if (!isLeader && !isColeader)
if (member.userId === user.userId) return errorResponse("Not yourself");
if (isMemberLeader)
return errorResponse(`${groupLabel} leader cannot be promoted`);
if (!isLeader && !isColeader && !canEdit)
return errorResponse(`Only ${groupLabel} leaders can promote`);
if (isMemberColeader && !isLeader)
if (isMemberColeader && !isLeader && !canEdit)
return errorResponse(`Only for ${groupLabel} leader`);
if (member.userId === user.userId) return errorResponse("Not yourself");
if (member.clanId !== fetchedClan.id)
return errorResponse(`Not in ${groupLabel}`);
if (!updateData)
return errorResponse(`No more co-leaders can be added to ${groupLabel}`);
if (!hasRequiredRank(member.rank, CLAN_RANK_REQUIREMENT)) {
if (!hasRequiredRank(member.rank, CLAN_RANK_REQUIREMENT))
return errorResponse("Leader rank too low");
let updateData = null;
// If member is a co-leader and the user is either a leader or has canEdit, promote them to leader
if (isMemberColeader && (isLeader || canEdit)) {
updateData = {
leaderId: input.memberId, // Promote co-leader to leader
coLeader1:
fetchedClan.coLeader1 === input.memberId
? fetchedClan.leaderId
: fetchedClan.coLeader1, // Demote leader to first available co-leader slot
coLeader2:
fetchedClan.coLeader2 === input.memberId
? fetchedClan.coLeader1
: fetchedClan.coLeader2,
coLeader3:
fetchedClan.coLeader3 === input.memberId
? fetchedClan.coLeader2
: fetchedClan.coLeader3,
coLeader4:
fetchedClan.coLeader4 === input.memberId
? fetchedClan.coLeader3
: fetchedClan.coLeader4,
};
}
// If member is NOT a co-leader, allow promotion to co-leader instead
else if (!isMemberColeader) {
if (!fetchedClan.coLeader1) updateData = { coLeader1: input.memberId };
else if (!fetchedClan.coLeader2) updateData = { coLeader2: input.memberId };
else if (!fetchedClan.coLeader3) updateData = { coLeader3: input.memberId };
else if (!fetchedClan.coLeader4) updateData = { coLeader4: input.memberId };
else return errorResponse(`No more co-leader slots available in ${groupLabel}`);
}
// If the member is already a co-leader but the user isn't a leader or can't edit, deny the promotion attempt
else {
return errorResponse(
`Only ${groupLabel} leader or canEdit users can promote to leader`,
);
}
// Mutate
await ctx.drizzle.update(clan).set(updateData).where(eq(clan.id, fetchedClan.id));
Expand All @@ -607,22 +627,19 @@ export const clanRouter = createTRPCRouter({
const isColeader = checkCoLeader(user.userId, clanData);
const isMemberLeader = input.memberId === clanData?.leaderId;
const isMemberColeader = checkCoLeader(input.memberId, clanData);
const canEdit = canEditClans(user.role);
const isYourself = ctx.userId === input.memberId;
const groupLabel = user?.isOutlaw ? "faction" : "clan";
// Guards
if (!clanData) return errorResponse(`${groupLabel} not found`);
if (!user) return errorResponse("User not found");
if (!member) return errorResponse("Member not found");
if (!isLeader && !isColeader)
return errorResponse(`Only ${groupLabel} leaders can demote`);
if (!isLeader && !isColeader && !canEdit) return errorResponse(`Not allowed`);
if (isMemberLeader)
return errorResponse(`New ${groupLabel} leader must be promoted first`);
if (member.clanId !== clanData.id) return errorResponse(`Not in ${groupLabel}`);
if (!hasRequiredRank(member.rank, CLAN_RANK_REQUIREMENT)) {
return errorResponse("Leader rank too low");
}
if (isMemberColeader && !isLeader && !isYourself) {
return errorResponse(`Only for ${groupLabel} leader`);
if (isMemberColeader && !isLeader && !canEdit && !isYourself) {
return errorResponse(`Only ${groupLabel} leader can demote`);
}
// Mutate
await ctx.drizzle
Expand Down Expand Up @@ -652,17 +669,28 @@ export const clanRouter = createTRPCRouter({
const isColeader = checkCoLeader(user.userId, fetchedClan);
const isMemberColeader = checkCoLeader(input.memberId, fetchedClan);
const isMemberLeader = input.memberId === fetchedClan?.leaderId;
const canEdit = canEditClans(user.role);
const groupLabel = user?.isOutlaw ? "faction" : "clan";
// Guards
if (!fetchedClan) return errorResponse(`${groupLabel} not found`);
if (!user) return errorResponse("User not found");
if (!member) return errorResponse("Member not found");
if (isMemberLeader) return errorResponse(`Cannot kick ${groupLabel} leader`);
if (isMemberLeader && !canEdit)
return errorResponse(`Cannot kick ${groupLabel} leader`);
if (fetchedClan.villageId !== user.villageId)
return errorResponse(user.isOutlaw ? "!= syndicate" : "!= village");
if (!isLeader && !isColeader) return errorResponse("Not allowed");
if (!isLeader && isMemberColeader)
if (!isLeader && !isColeader && !canEdit) return errorResponse("Not allowed");
if (!isLeader && isMemberColeader && !canEdit)
return errorResponse(`Only ${groupLabel} leader can kick`);
// If the leader is being kicked, promote the kicker to leader
if (isMemberLeader && canEdit) {
await ctx.drizzle
.update(clan)
.set({
leaderId: user.userId, // Promote the kicker to leader
})
.where(eq(clan.id, fetchedClan.id));
}
// Mutate
await removeFromClan(ctx.drizzle, fetchedClan, member, [
`Kicked by ${user.username}`,
Expand Down
5 changes: 5 additions & 0 deletions app/src/utils/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const canPlayHiddenQuests = (role: UserRole) => {

export const canSubmitNotification = (role: UserRole) => {
return [
"CODER",
"CONTENT",
"EVENT",
"HEAD_MODERATOR",
Expand Down Expand Up @@ -211,3 +212,7 @@ export const canChangeCombatBgScheme = (role: UserRole) => {
export const canReviewLinkPromotions = (role: UserRole) => {
return ["CODING-ADMIN"].includes(role);
};

export const canEditClans = (role: UserRole) => {
return ["CONTENT-ADMIN", "CODING-ADMIN", "MODERATOR-ADMIN"].includes(role);
};

0 comments on commit a234f76

Please sign in to comment.