mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
wip
This commit is contained in:
parent
06c84f0bf5
commit
8418f3645f
7 changed files with 86 additions and 276 deletions
|
|
@ -131,8 +131,6 @@ curl --request GET '$SOURCEBOT_URL/api/ee/audit' \
|
||||||
| `user.invite_accepted` | `user` | `invite` |
|
| `user.invite_accepted` | `user` | `invite` |
|
||||||
| `user.signed_in` | `user` | `user` |
|
| `user.signed_in` | `user` | `user` |
|
||||||
| `user.signed_out` | `user` | `user` |
|
| `user.signed_out` | `user` | `user` |
|
||||||
| `org.ownership_transfer_failed` | `user` | `org` |
|
|
||||||
| `org.ownership_transferred` | `user` | `org` |
|
|
||||||
|
|
||||||
|
|
||||||
## Response schema
|
## Response schema
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { createTransport } from "nodemailer";
|
||||||
import { Octokit } from "octokit";
|
import { Octokit } from "octokit";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { getOrgFromDomain } from "./data/org";
|
import { getOrgFromDomain } from "./data/org";
|
||||||
import { decrementOrgSeatCount, getSubscriptionForOrg } from "./ee/features/billing/serverUtils";
|
import { getSubscriptionForOrg } from "./ee/features/billing/serverUtils";
|
||||||
import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe";
|
import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe";
|
||||||
import InviteUserEmail from "./emails/inviteUserEmail";
|
import InviteUserEmail from "./emails/inviteUserEmail";
|
||||||
import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail";
|
import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail";
|
||||||
|
|
@ -1139,102 +1139,6 @@ export const getInviteInfo = async (inviteId: string) => sew(() =>
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const transferOwnership = async (newOwnerId: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
|
|
||||||
withAuth((userId) =>
|
|
||||||
withOrgMembership(userId, domain, async ({ org }) => {
|
|
||||||
const currentUserId = userId;
|
|
||||||
|
|
||||||
const failAuditCallback = async (error: string) => {
|
|
||||||
await auditService.createAudit({
|
|
||||||
action: "org.ownership_transfer_failed",
|
|
||||||
actor: {
|
|
||||||
id: currentUserId,
|
|
||||||
type: "user"
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
id: org.id.toString(),
|
|
||||||
type: "org"
|
|
||||||
},
|
|
||||||
orgId: org.id,
|
|
||||||
metadata: {
|
|
||||||
message: error
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (newOwnerId === currentUserId) {
|
|
||||||
await failAuditCallback("User is already the owner of this org");
|
|
||||||
return {
|
|
||||||
statusCode: StatusCodes.BAD_REQUEST,
|
|
||||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
|
||||||
message: "You're already the owner of this org",
|
|
||||||
} satisfies ServiceError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newOwner = await prisma.userToOrg.findUnique({
|
|
||||||
where: {
|
|
||||||
orgId_userId: {
|
|
||||||
userId: newOwnerId,
|
|
||||||
orgId: org.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!newOwner) {
|
|
||||||
await failAuditCallback("The user you're trying to make the owner doesn't exist");
|
|
||||||
return {
|
|
||||||
statusCode: StatusCodes.BAD_REQUEST,
|
|
||||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
|
||||||
message: "The user you're trying to make the owner doesn't exist",
|
|
||||||
} satisfies ServiceError;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction([
|
|
||||||
prisma.userToOrg.update({
|
|
||||||
where: {
|
|
||||||
orgId_userId: {
|
|
||||||
userId: newOwnerId,
|
|
||||||
orgId: org.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role: "OWNER",
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
prisma.userToOrg.update({
|
|
||||||
where: {
|
|
||||||
orgId_userId: {
|
|
||||||
userId: currentUserId,
|
|
||||||
orgId: org.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role: "MEMBER",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
await auditService.createAudit({
|
|
||||||
action: "org.ownership_transferred",
|
|
||||||
actor: {
|
|
||||||
id: currentUserId,
|
|
||||||
type: "user"
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
id: org.id.toString(),
|
|
||||||
type: "org"
|
|
||||||
},
|
|
||||||
orgId: org.id,
|
|
||||||
metadata: {
|
|
||||||
message: `Ownership transferred from ${currentUserId} to ${newOwnerId}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
}
|
|
||||||
}, /* minRequiredRole = */ OrgRole.OWNER)
|
|
||||||
));
|
|
||||||
|
|
||||||
export const checkIfOrgDomainExists = async (domain: string): Promise<boolean | ServiceError> => sew(() =>
|
export const checkIfOrgDomainExists = async (domain: string): Promise<boolean | ServiceError> => sew(() =>
|
||||||
withAuth(async () => {
|
withAuth(async () => {
|
||||||
const org = await prisma.org.findFirst({
|
const org = await prisma.org.findFirst({
|
||||||
|
|
@ -1246,81 +1150,6 @@ export const checkIfOrgDomainExists = async (domain: string): Promise<boolean |
|
||||||
return !!org;
|
return !!org;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const removeMemberFromOrg = async (memberId: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
|
|
||||||
withAuth(async (userId) =>
|
|
||||||
withOrgMembership(userId, domain, async ({ org }) => {
|
|
||||||
const targetMember = await prisma.userToOrg.findUnique({
|
|
||||||
where: {
|
|
||||||
orgId_userId: {
|
|
||||||
orgId: org.id,
|
|
||||||
userId: memberId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!targetMember) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
await tx.userToOrg.delete({
|
|
||||||
where: {
|
|
||||||
orgId_userId: {
|
|
||||||
orgId: org.id,
|
|
||||||
userId: memberId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED) {
|
|
||||||
const result = await decrementOrgSeatCount(org.id, tx);
|
|
||||||
if (isServiceError(result)) {
|
|
||||||
throw result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
}
|
|
||||||
}, /* minRequiredRole = */ OrgRole.OWNER)
|
|
||||||
));
|
|
||||||
|
|
||||||
export const leaveOrg = async (domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
|
|
||||||
withAuth(async (userId) =>
|
|
||||||
withOrgMembership(userId, domain, async ({ org, userRole }) => {
|
|
||||||
if (userRole === OrgRole.OWNER) {
|
|
||||||
return {
|
|
||||||
statusCode: StatusCodes.FORBIDDEN,
|
|
||||||
errorCode: ErrorCode.OWNER_CANNOT_LEAVE_ORG,
|
|
||||||
message: "Organization owners cannot leave their own organization",
|
|
||||||
} satisfies ServiceError;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
await tx.userToOrg.delete({
|
|
||||||
where: {
|
|
||||||
orgId_userId: {
|
|
||||||
orgId: org.id,
|
|
||||||
userId: userId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED) {
|
|
||||||
const result = await decrementOrgSeatCount(org.id, tx);
|
|
||||||
if (isServiceError(result)) {
|
|
||||||
throw result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
export const getOrgMembers = async (domain: string) => sew(() =>
|
export const getOrgMembers = async (domain: string) => sew(() =>
|
||||||
withAuth(async (userId) =>
|
withAuth(async (userId) =>
|
||||||
withOrgMembership(userId, domain, async ({ org }) => {
|
withOrgMembership(userId, domain, async ({ org }) => {
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ import { OrgRole } from "@prisma/client";
|
||||||
import placeholderAvatar from "@/public/placeholder_avatar.png";
|
import placeholderAvatar from "@/public/placeholder_avatar.png";
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { transferOwnership, removeMemberFromOrg, leaveOrg } from "@/actions";
|
|
||||||
import { isServiceError } from "@/lib/utils";
|
import { isServiceError } from "@/lib/utils";
|
||||||
import { useToast } from "@/components/hooks/use-toast";
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
|
import { removeMemberFromOrg } from "@/features/members/actions";
|
||||||
|
|
||||||
type Member = {
|
type Member = {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -30,20 +30,18 @@ export interface MembersListProps {
|
||||||
members: Member[],
|
members: Member[],
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
currentUserRole: OrgRole,
|
currentUserRole: OrgRole,
|
||||||
orgName: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MembersList = ({ members, currentUserId, currentUserRole, orgName }: MembersListProps) => {
|
export const MembersList = ({ members, currentUserId, currentUserRole }: MembersListProps) => {
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
const [roleFilter, setRoleFilter] = useState<"all" | OrgRole>("all")
|
const [roleFilter, setRoleFilter] = useState<"all" | OrgRole>("all")
|
||||||
const [dateSort, setDateSort] = useState<"newest" | "oldest">("newest")
|
const [dateSort, setDateSort] = useState<"newest" | "oldest">("newest")
|
||||||
const [memberToRemove, setMemberToRemove] = useState<Member | null>(null)
|
const [memberToRemove, setMemberToRemove] = useState<Member | null>(null)
|
||||||
const [memberToTransfer, setMemberToTransfer] = useState<Member | null>(null)
|
const [roleChangeData, setRoleChangeData] = useState<{ member: Member; newRole: OrgRole } | null>(null)
|
||||||
const domain = useDomain()
|
const domain = useDomain()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const [isRemoveDialogOpen, setIsRemoveDialogOpen] = useState(false)
|
const [isRemoveDialogOpen, setIsRemoveDialogOpen] = useState(false)
|
||||||
const [isTransferOwnershipDialogOpen, setIsTransferOwnershipDialogOpen] = useState(false)
|
const [isRoleChangeDialogOpen, setIsRoleChangeDialogOpen] = useState(false)
|
||||||
const [isLeaveOrgDialogOpen, setIsLeaveOrgDialogOpen] = useState(false)
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const captureEvent = useCaptureEvent();
|
const captureEvent = useCaptureEvent();
|
||||||
|
|
||||||
|
|
@ -83,45 +81,9 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName }
|
||||||
});
|
});
|
||||||
}, [domain, toast, router, captureEvent]);
|
}, [domain, toast, router, captureEvent]);
|
||||||
|
|
||||||
const onTransferOwnership = useCallback((memberId: string) => {
|
const onChangeMembership = useCallback((_memberId: string, _newRole: OrgRole) => {
|
||||||
transferOwnership(memberId, domain)
|
// @todo
|
||||||
.then((response) => {
|
}, []);
|
||||||
if (isServiceError(response)) {
|
|
||||||
toast({
|
|
||||||
description: `❌ Failed to transfer ownership. Reason: ${response.message}`
|
|
||||||
})
|
|
||||||
captureEvent('wa_members_list_transfer_ownership_fail', {
|
|
||||||
error: response.errorCode,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
description: `✅ Ownership transferred successfully.`
|
|
||||||
})
|
|
||||||
captureEvent('wa_members_list_transfer_ownership_success', {})
|
|
||||||
router.refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [domain, toast, router, captureEvent]);
|
|
||||||
|
|
||||||
const onLeaveOrg = useCallback(() => {
|
|
||||||
leaveOrg(domain)
|
|
||||||
.then((response) => {
|
|
||||||
if (isServiceError(response)) {
|
|
||||||
toast({
|
|
||||||
description: `❌ Failed to leave organization. Reason: ${response.message}`
|
|
||||||
})
|
|
||||||
captureEvent('wa_members_list_leave_org_fail', {
|
|
||||||
error: response.errorCode,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
description: `✅ You have left the organization.`
|
|
||||||
})
|
|
||||||
captureEvent('wa_members_list_leave_org_success', {})
|
|
||||||
router.push("/");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [domain, toast, router, captureEvent]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -181,7 +143,25 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-muted-foreground capitalize">{member.role.toLowerCase()}</span>
|
<Select
|
||||||
|
value={member.role}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newRole = value as OrgRole;
|
||||||
|
if (newRole !== member.role) {
|
||||||
|
setRoleChangeData({ member, newRole });
|
||||||
|
setIsRoleChangeDialogOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={member.id === currentUserId || currentUserRole !== OrgRole.OWNER}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder={member.role.toLowerCase()} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={OrgRole.OWNER}>Owner</SelectItem>
|
||||||
|
<SelectItem value={OrgRole.MEMBER}>Member</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
|
@ -207,17 +187,6 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName }
|
||||||
>
|
>
|
||||||
Copy email
|
Copy email
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{member.id !== currentUserId && currentUserRole === OrgRole.OWNER && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
setMemberToTransfer(member);
|
|
||||||
setIsTransferOwnershipDialogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Transfer ownership
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{member.id !== currentUserId && currentUserRole === OrgRole.OWNER && (
|
{member.id !== currentUserId && currentUserRole === OrgRole.OWNER && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer text-destructive"
|
className="cursor-pointer text-destructive"
|
||||||
|
|
@ -229,17 +198,6 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName }
|
||||||
Remove
|
Remove
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{member.id === currentUserId && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer text-destructive"
|
|
||||||
disabled={currentUserRole === OrgRole.OWNER}
|
|
||||||
onClick={() => {
|
|
||||||
setIsLeaveOrgDialogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Leave organization
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -273,46 +231,26 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName }
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={isTransferOwnershipDialogOpen}
|
open={isRoleChangeDialogOpen}
|
||||||
onOpenChange={setIsTransferOwnershipDialogOpen}
|
onOpenChange={setIsRoleChangeDialogOpen}
|
||||||
>
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Transfer Ownership</AlertDialogTitle>
|
<AlertDialogTitle>Change Member Role</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{`Are you sure you want to transfer ownership of ${orgName} to ${memberToTransfer?.name ?? memberToTransfer?.email}?`}
|
{roleChangeData && `Are you sure you want to change ${roleChangeData.member.name ?? roleChangeData.member.email}'s role to ${roleChangeData.newRole.toLowerCase()}?`}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onTransferOwnership(memberToTransfer?.id ?? "");
|
if (roleChangeData) {
|
||||||
|
onChangeMembership(roleChangeData.member.id, roleChangeData.newRole);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Transfer
|
Confirm
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
<AlertDialog
|
|
||||||
open={isLeaveOrgDialogOpen}
|
|
||||||
onOpenChange={setIsLeaveOrgDialogOpen}
|
|
||||||
>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Leave Organization</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{`Are you sure you want to leave ${orgName}?`}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
||||||
onClick={onLeaveOrg}
|
|
||||||
>
|
|
||||||
Leave
|
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,6 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp
|
||||||
members={members}
|
members={members}
|
||||||
currentUserId={me.id}
|
currentUserId={me.id}
|
||||||
currentUserRole={userRoleInOrg}
|
currentUserRole={userRoleInOrg}
|
||||||
orgName={org.name}
|
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ export function InviteLinkToggle({ inviteLinkEnabled, inviteLink }: InviteLinkTo
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-[var(--muted-foreground)]">
|
<p className="text-sm text-[var(--muted-foreground)]">
|
||||||
You can find this link again in the <strong>Settings → Members</strong> page.
|
You can find this link again in the <strong>Settings → Access</strong> page.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
50
packages/web/src/features/members/actions.ts
Normal file
50
packages/web/src/features/members/actions.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||||
|
import { decrementOrgSeatCount } from "@/ee/features/billing/serverUtils";
|
||||||
|
import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
|
||||||
|
import { notFound, ServiceError } from "@/lib/serviceError";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { prisma } from "@/prisma";
|
||||||
|
import { OrgRole } from "@sourcebot/db";
|
||||||
|
|
||||||
|
|
||||||
|
export const removeMemberFromOrg = async (memberId: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
|
||||||
|
withAuth(async (userId) =>
|
||||||
|
withOrgMembership(userId, domain, async ({ org }) => {
|
||||||
|
const targetMember = await prisma.userToOrg.findUnique({
|
||||||
|
where: {
|
||||||
|
orgId_userId: {
|
||||||
|
orgId: org.id,
|
||||||
|
userId: memberId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetMember) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.userToOrg.delete({
|
||||||
|
where: {
|
||||||
|
orgId_userId: {
|
||||||
|
orgId: org.id,
|
||||||
|
userId: memberId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (IS_BILLING_ENABLED) {
|
||||||
|
const result = await decrementOrgSeatCount(org.id, tx);
|
||||||
|
if (isServiceError(result)) {
|
||||||
|
throw result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}, /* minRequiredRole = */ OrgRole.OWNER)
|
||||||
|
));
|
||||||
|
|
@ -149,10 +149,6 @@ export type PosthogEventMap = {
|
||||||
wa_members_list_remove_member_fail: {
|
wa_members_list_remove_member_fail: {
|
||||||
error: string,
|
error: string,
|
||||||
},
|
},
|
||||||
wa_members_list_transfer_ownership_success: {},
|
|
||||||
wa_members_list_transfer_ownership_fail: {
|
|
||||||
error: string,
|
|
||||||
},
|
|
||||||
wa_members_list_leave_org_success: {},
|
wa_members_list_leave_org_success: {},
|
||||||
wa_members_list_leave_org_fail: {
|
wa_members_list_leave_org_fail: {
|
||||||
error: string,
|
error: string,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue