Fix teamWebsite / teamUser.

This commit is contained in:
Brian Cao 2023-04-09 16:04:28 -07:00
parent 53b23420a4
commit 9eff565e7a
14 changed files with 83 additions and 52 deletions

View File

@ -4,7 +4,7 @@ import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from
import WebsiteTags from './WebsiteTags'; import WebsiteTags from './WebsiteTags';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function WebsiteAddTeamForm({ teamId, onSave, onClose }) { export default function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { get, post, useQuery, useMutation } = useApi(); const { get, post, useQuery, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data)); const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data));

View File

@ -2,18 +2,21 @@ import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function TeamLeaveForm({ teamUserId, teamName, onSave, onClose }) { export default function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => del(`/teamUsers/${teamUserId}`, data)); const { mutate, error, isLoading } = useMutation(() => del(`/team/${teamId}/users/${userId}`));
const handleSubmit = async data => { const handleSubmit = async () => {
mutate(data, { mutate(
onSuccess: async () => { {},
onSave(); {
onClose(); onSuccess: async () => {
onSave();
onClose();
},
}, },
}); );
}; };
return ( return (

View File

@ -2,14 +2,14 @@ import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import { Icon, Icons, LoadingButton, Text } from 'react-basics'; import { Icon, Icons, LoadingButton, Text } from 'react-basics';
export default function TeamMemberRemoveButton({ teamUserId, disabled, onSave }) { export default function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, isLoading } = useMutation(() => del(`/teamUsers/${teamUserId}`)); const { mutate, isLoading } = useMutation(() => del(`/team/${teamId}/users/${userId}`));
const handleRemoveTeamMember = () => { const handleRemoveTeamMember = () => {
mutate( mutate(
{ teamUserId }, {},
{ {
onSuccess: () => { onSuccess: () => {
onSave(); onSave();

View File

@ -43,7 +43,8 @@ export default function TeamMembersTable({ data = [], onSave, readOnly }) {
action: !readOnly && ( action: !readOnly && (
<Flexbox flex={1} justifyContent="end"> <Flexbox flex={1} justifyContent="end">
<TeamMemberRemoveButton <TeamMemberRemoveButton
teamUserId={row.id} teamId={row.teamId}
userId={row.userId}
disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner} disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}
onSave={onSave} onSave={onSave}
></TeamMemberRemoveButton> ></TeamMemberRemoveButton>

View File

@ -2,14 +2,14 @@ import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import { Icon, Icons, LoadingButton, Text } from 'react-basics'; import { Icon, Icons, LoadingButton, Text } from 'react-basics';
export default function TeamWebsiteRemoveButton({ teamWebsiteId, onSave }) { export default function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, isLoading } = useMutation(() => del(`/teamWebsites/${teamWebsiteId}`)); const { mutate, isLoading } = useMutation(() => del(`/teams/${teamId}/websites/${websiteId}`));
const handleRemoveTeamMember = () => { const handleRemoveTeamMember = () => {
mutate( mutate(
{ teamWebsiteId }, {},
{ {
onSuccess: () => { onSuccess: () => {
onSave(); onSave();

View File

@ -11,7 +11,7 @@ import {
} from 'react-basics'; } from 'react-basics';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable'; import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable';
import WebsiteAddTeamForm from 'components/pages/settings/teams/WebsiteAddTeamForm'; import TeamAddWebsiteForm from 'components/pages/settings/teams/TeamAddWebsiteForm';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
@ -42,7 +42,7 @@ export default function TeamWebsites({ teamId }) {
<Text>{formatMessage(labels.addWebsite)}</Text> <Text>{formatMessage(labels.addWebsite)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.addWebsite)}> <Modal title={formatMessage(labels.addWebsite)}>
{close => <WebsiteAddTeamForm teamId={teamId} onSave={handleSave} onClose={close} />} {close => <TeamAddWebsiteForm teamId={teamId} onSave={handleSave} onClose={close} />}
</Modal> </Modal>
</ModalTrigger> </ModalTrigger>
); );

View File

@ -38,7 +38,7 @@ export default function TeamWebsitesTable({ data = [], onSave }) {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{(row, keys, rowIndex) => { {(row, keys, rowIndex) => {
const { id: teamWebsiteId } = row; const { teamId } = row;
const { id: websiteId, name, domain, userId } = row.website; const { id: websiteId, name, domain, userId } = row.website;
const { teamUser } = row.team; const { teamUser } = row.team;
const owner = teamUser[0]; const owner = teamUser[0];
@ -59,7 +59,8 @@ export default function TeamWebsitesTable({ data = [], onSave }) {
</Link> </Link>
{canRemove && ( {canRemove && (
<TeamWebsiteRemoveButton <TeamWebsiteRemoveButton
teamWebsiteId={teamWebsiteId} teamId={teamId}
websiteId={websiteId}
onSave={onSave} onSave={onSave}
></TeamWebsiteRemoveButton> ></TeamWebsiteRemoveButton>
)} )}

View File

@ -92,7 +92,8 @@ export default function TeamsTable({ data = [], onDelete }) {
<Modal title={formatMessage(labels.leaveTeam)}> <Modal title={formatMessage(labels.leaveTeam)}>
{close => ( {close => (
<TeamLeaveForm <TeamLeaveForm
teamUserId={teamUserId} teamId={id}
userId={user.id}
teamName={row.name} teamName={row.name}
onSave={onDelete} onSave={onDelete}
onClose={close} onClose={close}

View File

@ -205,13 +205,13 @@ export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUs
return false; return false;
} }
export async function canDeleteTeamWebsite({ user }: Auth, teamWebsiteId: string) { export async function canDeleteTeamWebsite({ user }: Auth, teamId: string, websiteId: string) {
if (user.isAdmin) { if (user.isAdmin) {
return true; return true;
} }
if (validate(teamWebsiteId)) { if (validate(teamId) && validate(websiteId)) {
const teamWebsite = await getTeamWebsite(teamWebsiteId); const teamWebsite = await getTeamWebsite(teamId, websiteId);
if (teamWebsite.website.userId === user.id) { if (teamWebsite.website.userId === user.id) {
return true; return true;
@ -219,7 +219,7 @@ export async function canDeleteTeamWebsite({ user }: Auth, teamWebsiteId: string
const teamUser = await getTeamUser(teamWebsite.teamId, user.id); const teamUser = await getTeamUser(teamWebsite.teamId, user.id);
return hasPermission(teamUser.role, PERMISSIONS.teamDelete); return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
} }
return false; return false;

View File

@ -0,0 +1,29 @@
import { canDeleteTeamUser } from 'lib/auth';
import { useAuth } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { deleteTeamUser } from 'queries';
export interface TeamUserRequestQuery {
id: string;
userId: string;
}
export default async (req: NextApiRequestQueryBody<TeamUserRequestQuery>, res: NextApiResponse) => {
await useAuth(req, res);
if (req.method === 'DELETE') {
const { id: teamId, userId } = req.query;
if (!(await canDeleteTeamUser(req.auth, teamId, userId))) {
return unauthorized(res, 'You must be the owner of this team.');
}
await deleteTeamUser(teamId, userId);
return ok(res);
}
return methodNotAllowed(res);
};

View File

@ -1,9 +1,9 @@
import { NextApiRequestQueryBody } from 'lib/types'; import { canUpdateTeam, canViewTeam } from 'lib/auth';
import { canDeleteTeamUser, canUpdateTeam, canViewTeam } from 'lib/auth';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeamUser, deleteTeamUser, getUser, getTeamUsers } from 'queries'; import { createTeamUser, getTeamUsers, getUser } from 'queries';
export interface TeamUserRequestQuery { export interface TeamUserRequestQuery {
id: string; id: string;
@ -12,7 +12,6 @@ export interface TeamUserRequestQuery {
export interface TeamUserRequestBody { export interface TeamUserRequestBody {
email: string; email: string;
roleId: string; roleId: string;
userId?: string;
} }
export default async ( export default async (
@ -52,17 +51,5 @@ export default async (
return ok(res, updated); return ok(res, updated);
} }
if (req.method === 'DELETE') {
const { userId } = req.body;
if (await canDeleteTeamUser(req.auth, teamId, userId)) {
return unauthorized(res, 'You must be the owner of this team.');
}
await deleteTeamUser(teamId, userId);
return ok(res);
}
return methodNotAllowed(res); return methodNotAllowed(res);
}; };

View File

@ -5,24 +5,25 @@ import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { deleteTeamWebsite } from 'queries/admin/teamWebsite'; import { deleteTeamWebsite } from 'queries/admin/teamWebsite';
export interface TeamWebsiteRequestQuery { export interface TeamWebsitesRequestQuery {
id: string; id: string;
websiteId: string;
} }
export default async ( export default async (
req: NextApiRequestQueryBody<TeamWebsiteRequestQuery>, req: NextApiRequestQueryBody<TeamWebsitesRequestQuery>,
res: NextApiResponse, res: NextApiResponse,
) => { ) => {
await useAuth(req, res); await useAuth(req, res);
const { id: teamWebsiteId } = req.query; const { id: teamId, websiteId } = req.query;
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
if (!(await canDeleteTeamWebsite(req.auth, teamWebsiteId))) { if (!(await canDeleteTeamWebsite(req.auth, teamId, websiteId))) {
return unauthorized(res); return unauthorized(res);
} }
const websites = await deleteTeamWebsite(teamWebsiteId); const websites = await deleteTeamWebsite(teamId, websiteId);
return ok(res, websites); return ok(res, websites);
} }

View File

@ -1,16 +1,20 @@
import { TeamWebsite, Prisma, Website, Team, User, TeamUser } from '@prisma/client'; import { Prisma, Team, TeamUser, TeamWebsite, Website } from '@prisma/client';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto'; import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
export async function getTeamWebsite(teamWebsiteId: string): Promise< export async function getTeamWebsite(
teamId: string,
websiteId: string,
): Promise<
TeamWebsite & { TeamWebsite & {
website: Website; website: Website;
} }
> { > {
return prisma.client.teamWebsite.findFirst({ return prisma.client.teamWebsite.findFirst({
where: { where: {
id: teamWebsiteId, teamId,
websiteId,
}, },
include: { include: {
website: true, website: true,
@ -110,10 +114,14 @@ export async function createTeamWebsites(teamId: string, websiteIds: string[]) {
}); });
} }
export async function deleteTeamWebsite(teamWebsiteId: string): Promise<TeamWebsite> { export async function deleteTeamWebsite(
return prisma.client.teamWebsite.delete({ teamId: string,
websiteId: string,
): Promise<Prisma.BatchPayload> {
return prisma.client.teamWebsite.deleteMany({
where: { where: {
id: teamWebsiteId, teamId,
websiteId,
}, },
}); });
} }