Feat/um 197 hook up teams (#1825)

* Link up teams UI.

* Fix auth order.

* PR touchups.
This commit is contained in:
Brian Cao 2023-03-09 12:42:12 -08:00 committed by GitHub
parent f908476e71
commit 8a9532f213
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 500 additions and 111 deletions

View File

@ -46,6 +46,7 @@ export const labels = defineMessages({
deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' }, deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' },
reset: { id: 'label.reset', defaultMessage: 'Reset' }, reset: { id: 'label.reset', defaultMessage: 'Reset' },
addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' }, addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' },
addWebsites: { id: 'label.add-websites', defaultMessage: 'Add websites' },
changePassword: { id: 'label.change-password', defaultMessage: 'Change password' }, changePassword: { id: 'label.change-password', defaultMessage: 'Change password' },
currentPassword: { id: 'label.current-password', defaultMessage: 'Current password' }, currentPassword: { id: 'label.current-password', defaultMessage: 'Current password' },
newPassword: { id: 'label.new-password', defaultMessage: 'New password' }, newPassword: { id: 'label.new-password', defaultMessage: 'New password' },
@ -145,6 +146,10 @@ export const messages = defineMessages({
id: 'message.reset-website', id: 'message.reset-website',
defaultMessage: 'To reset this website, type {confirmation} in the box below to confirm.', defaultMessage: 'To reset this website, type {confirmation} in the box below to confirm.',
}, },
websitesShared: {
id: 'message.shared-website',
defaultMessage: 'Websites can be viewed by the entire team.',
},
invalidDomain: { invalidDomain: {
id: 'message.invalid-domain', id: 'message.invalid-domain',
defaultMessage: 'Invalid domain. Do not include http/https.', defaultMessage: 'Invalid domain. Do not include http/https.',
@ -162,6 +167,14 @@ export const messages = defineMessages({
id: 'messages.no-websites', id: 'messages.no-websites',
defaultMessage: 'You do not have any websites configured.', defaultMessage: 'You do not have any websites configured.',
}, },
noTeamWebsites: {
id: 'messages.no-team-websites',
defaultMessage: 'This team does not have any websites.',
},
websitesAreShared: {
id: 'messages.websites-are-shared',
defaultMessage: 'Websites can be viewed by anyone on the team.',
},
noMatchPassword: { id: 'message.no-match-password', defaultMessage: 'Passwords do not match.' }, noMatchPassword: { id: 'message.no-match-password', defaultMessage: 'Passwords do not match.' },
goToSettings: { goToSettings: {
id: 'message.go-to-settings', id: 'message.go-to-settings',
@ -183,17 +196,6 @@ export const messages = defineMessages({
id: 'message.event-log', id: 'message.event-log',
defaultMessage: '{event} on {url}', defaultMessage: '{event} on {url}',
}, },
newVersionAvailable: {
id: 'new-version-available',
defaultMessage: 'A new version of Umami {version} is available!',
},
});
export const devices = defineMessages({
desktop: { id: 'metrics.device.desktop', defaultMessage: 'Desktop' },
laptop: { id: 'metrics.device.laptop', defaultMessage: 'Laptop' },
tablet: { id: 'metrics.device.tablet', defaultMessage: 'Tablet' },
mobile: { id: 'metrics.device.mobile', defaultMessage: 'Mobile' },
}); });
export function getMessage(id, formatMessage) { export function getMessage(id, formatMessage) {
@ -201,7 +203,3 @@ export function getMessage(id, formatMessage) {
return message ? formatMessage(message) : id; return message ? formatMessage(message) : id;
} }
export function getDeviceMessage(device) {
return devices[device] || labels.unknown;
}

View File

@ -1,13 +1,26 @@
import { Loading } from 'react-basics'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import { useIntl } from 'react-intl'; import { labels, messages } from 'components/messages';
import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import {
import { messages } from 'components/messages'; ActionForm,
Button,
Icon,
Icons,
Loading,
Modal,
ModalTrigger,
Text,
useToast,
} from 'react-basics';
import { useIntl } from 'react-intl';
import WebsiteAddTeamForm from 'components/pages/settings/teams/WebsiteAddTeamForm';
export default function TeamWebsites({ teamId }) { export default function TeamWebsites({ teamId }) {
const { toast, showToast } = useToast();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['teams:websites', teamId], () => const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () =>
get(`/teams/${teamId}/websites`), get(`/teams/${teamId}/websites`),
); );
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;
@ -16,10 +29,37 @@ export default function TeamWebsites({ teamId }) {
return <Loading icon="dots" position="block" />; return <Loading icon="dots" position="block" />;
} }
const handleSave = async () => {
await refetch();
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
const addButton = (
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal title={formatMessage(labels.addWebsite)}>
{close => <WebsiteAddTeamForm teamId={teamId} onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
);
return ( return (
<div> <div>
{hasData && <WebsitesTable data={data} />} {toast}
{!hasData && formatMessage(messages.noData)} {hasData && (
<ActionForm description={formatMessage(messages.websitesAreShared)}>{addButton}</ActionForm>
)}
{hasData && <TeamWebsitesTable teamId={teamId} data={data} onSave={handleSave} />}
{!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noTeamWebsites)}>
{addButton}
</EmptyPlaceholder>
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,103 @@
import Link from 'next/link';
import {
Table,
TableHeader,
TableBody,
TableRow,
TableCell,
TableColumn,
Button,
Text,
Icon,
Icons,
Flexbox,
} from 'react-basics';
import { useIntl } from 'react-intl';
import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
import useApi from 'hooks/useApi';
export default function TeamWebsitesTable({ teamId, data = [], onSave }) {
const { formatMessage } = useIntl();
const { user } = useUser();
const { del, useMutation } = useApi();
const { mutate } = useMutation(data => del(`/teamWebsites/${data.teamWebsiteId}`));
const columns = [
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
{ name: 'domain', label: formatMessage(labels.domain) },
{ name: 'action', label: ' ' },
];
const handleRemoveWebsite = teamWebsiteId => {
mutate(
{ teamWebsiteId },
{
onSuccess: async () => {
onSave();
},
},
);
};
return (
<Table columns={columns} rows={data}>
<TableHeader>
{(column, index) => {
return (
<TableColumn key={index} style={{ ...column.style }}>
{column.label}
</TableColumn>
);
}}
</TableHeader>
<TableBody>
{(row, keys, rowIndex) => {
const { id: teamWebsiteId } = row;
const { id: websiteId, name, domain, userId } = row.website;
const { teamUser } = row.team;
const owner = teamUser[0];
const canRemove = user.id === userId || user.id === owner.userId;
row.name = name;
row.domain = domain;
row.action = (
<Flexbox flex={1} justifyContent="end" gap={10}>
<Link href={`/websites/${websiteId}`} target="_blank">
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
{canRemove && (
<Button onClick={() => handleRemoveWebsite(teamWebsiteId)}>
<Icon>
<Icons.Trash />
</Icon>
<Text>{formatMessage(labels.remove)}</Text>
</Button>
)}
</Flexbox>
);
return (
<TableRow key={rowIndex} data={row} keys={keys}>
{(data, key, colIndex) => {
return (
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
<Flexbox flex={1} alignItems="center">
{data[key]}
</Flexbox>
</TableCell>
);
}}
</TableRow>
);
}}
</TableBody>
</Table>
);
}

View File

@ -76,7 +76,10 @@ export default function TeamsList() {
{hasData && <TeamsTable data={data} onDelete={handleDelete} />} {hasData && <TeamsTable data={data} onDelete={handleDelete} />}
{!hasData && ( {!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noTeams)}> <EmptyPlaceholder message={formatMessage(messages.noTeams)}>
<Flexbox gap={10}>
{joinButton}
{createButton} {createButton}
</Flexbox>
</EmptyPlaceholder> </EmptyPlaceholder>
)} )}
</Page> </Page>

View File

@ -1,26 +1,28 @@
import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
import { ROLES } from 'lib/constants';
import Link from 'next/link'; import Link from 'next/link';
import { import {
Button,
Flexbox,
Icon,
Icons,
Modal,
ModalTrigger,
Table, Table,
TableHeader,
TableBody, TableBody,
TableRow,
TableCell, TableCell,
TableColumn, TableColumn,
Button, TableHeader,
Icon, TableRow,
Flexbox,
Icons,
Text, Text,
ModalTrigger,
Modal,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { labels } from 'components/messages';
import { ROLES } from 'lib/constants';
import TeamDeleteForm from './TeamDeleteForm'; import TeamDeleteForm from './TeamDeleteForm';
export default function TeamsTable({ data = [], onDelete }) { export default function TeamsTable({ data = [], onDelete }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { user } = useUser();
const columns = [ const columns = [
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } }, { name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
@ -42,10 +44,12 @@ export default function TeamsTable({ data = [], onDelete }) {
<TableBody> <TableBody>
{(row, keys, rowIndex) => { {(row, keys, rowIndex) => {
const { id } = row; const { id } = row;
const owner = row.teamUser.find(({ role }) => role === ROLES.teamOwner);
const showDelete = user.id === owner?.userId;
const rowData = { const rowData = {
...row, ...row,
owner: row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username, owner: owner?.user?.username,
action: ( action: (
<Flexbox flex={1} gap={10} justifyContent="end"> <Flexbox flex={1} gap={10} justifyContent="end">
<Link href={`/settings/teams/${id}`}> <Link href={`/settings/teams/${id}`}>
@ -56,6 +60,7 @@ export default function TeamsTable({ data = [], onDelete }) {
<Text>{formatMessage(labels.edit)}</Text> <Text>{formatMessage(labels.edit)}</Text>
</Button> </Button>
</Link> </Link>
{showDelete && (
<ModalTrigger> <ModalTrigger>
<Button> <Button>
<Icon> <Icon>
@ -74,6 +79,7 @@ export default function TeamsTable({ data = [], onDelete }) {
)} )}
</Modal> </Modal>
</ModalTrigger> </ModalTrigger>
)}
</Flexbox> </Flexbox>
), ),
}; };

View File

@ -0,0 +1,62 @@
import { labels } from 'components/messages';
import useApi from 'hooks/useApi';
import { useRef, useState } from 'react';
import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from 'react-basics';
import { useIntl } from 'react-intl';
import WebsiteTags from './WebsiteTags';
export default function WebsiteAddTeamForm({ teamId, onSave, onClose }) {
const { formatMessage } = useIntl();
const { get, post, useQuery, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data));
const { data: websites } = useQuery(['websites'], () => get('/websites'));
const [newWebsites, setNewWebsites] = useState([]);
const formRef = useRef();
const handleSubmit = () => {
mutate(
{ websiteIds: newWebsites },
{
onSuccess: async () => {
onSave();
onClose();
},
},
);
};
const handleAddWebsite = value => {
if (!newWebsites.some(a => a === value)) {
const nextValue = [...newWebsites];
nextValue.push(value);
setNewWebsites(nextValue);
}
};
const handleRemoveWebsite = value => {
const newValue = newWebsites.filter(a => a !== value);
setNewWebsites(newValue);
};
return (
<>
<Form onSubmit={handleSubmit} error={error} ref={formRef}>
<FormRow label={formatMessage(labels.websites)}>
<Dropdown items={websites} onChange={handleAddWebsite}>
{({ id, name }) => <Item key={id}>{name}</Item>}
</Dropdown>
</FormRow>
<WebsiteTags items={websites} websites={newWebsites} onClick={handleRemoveWebsite} />
<FormButtons flex>
<SubmitButton disabled={newWebsites && newWebsites.length === 0}>
{formatMessage(labels.addWebsites)}
</SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons>
</Form>
</>
);
}

View File

@ -0,0 +1,29 @@
import { Button, Icon, Icons, Text } from 'react-basics';
import styles from './WebsiteTags.module.css';
export default function WebsiteTags({ items = [], websites = [], onClick }) {
if (websites.length === 0) {
return null;
}
return (
<div className={styles.filters}>
{websites.map(websiteId => {
const website = items.find(a => a.id === websiteId);
return (
<div key={websiteId} className={styles.tag}>
<Button onClick={() => onClick(websiteId)} variant="primary" size="sm">
<Text>
<b>{`${website.name}`}</b>
</Text>
<Icon>
<Icons.Close />
</Icon>
</Button>
</div>
);
})}
</div>
);
}

View File

@ -0,0 +1,11 @@
.filters {
display: flex;
justify-content: flex-start;
align-items: flex-start;
}
.tag {
text-align: center;
margin-bottom: 10px;
margin-right: 20px;
}

View File

@ -0,0 +1,19 @@
/*
Warnings:
- You are about to drop the column `user_id` on the `team` table. All the data in the column will be lost.
- You are about to drop the column `user_id` on the `team_website` table. All the data in the column will be lost.
*/
-- DropIndex
DROP INDEX "team_user_id_idx";
-- DropIndex
DROP INDEX "team_website_user_id_idx";
-- AlterTable
ALTER TABLE "team" DROP COLUMN "user_id";
-- AlterTable
ALTER TABLE "team_website" DROP COLUMN "user_id",
ADD COLUMN "userId" UUID;

View File

@ -19,7 +19,6 @@ model User {
Website Website[] Website Website[]
teamUser TeamUser[] teamUser TeamUser[]
teamWebsite TeamWebsite[]
@@map("user") @@map("user")
} }
@ -86,7 +85,6 @@ model WebsiteEvent {
model Team { model Team {
id String @id() @unique() @map("team_id") @db.Uuid id String @id() @unique() @map("team_id") @db.Uuid
name String @db.VarChar(50) name String @db.VarChar(50)
userId String @map("user_id") @db.Uuid
accessCode String? @unique @map("access_code") @db.VarChar(50) accessCode String? @unique @map("access_code") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) updatedAt DateTime? @map("updated_at") @db.Timestamptz(6)
@ -94,7 +92,6 @@ model Team {
teamUser TeamUser[] teamUser TeamUser[]
teamWebsite TeamWebsite[] teamWebsite TeamWebsite[]
@@index([userId])
@@index([accessCode]) @@index([accessCode])
@@map("team") @@map("team")
} }
@ -118,16 +115,13 @@ model TeamUser {
model TeamWebsite { model TeamWebsite {
id String @id() @unique() @map("team_website_id") @db.Uuid id String @id() @unique() @map("team_website_id") @db.Uuid
teamId String @map("team_id") @db.Uuid teamId String @map("team_id") @db.Uuid
userId String @map("user_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid websiteId String @map("website_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
team Team @relation(fields: [teamId], references: [id]) team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id])
website Website @relation(fields: [websiteId], references: [id]) website Website @relation(fields: [websiteId], references: [id])
@@index([teamId]) @@index([teamId])
@@index([userId])
@@index([websiteId]) @@index([websiteId])
@@map("team_website") @@map("team_website")
} }

View File

@ -1,10 +1,11 @@
import debug from 'debug'; import debug from 'debug';
import { validate } from 'uuid';
import cache from 'lib/cache'; import cache from 'lib/cache';
import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants';
import { secret } from 'lib/crypto'; import { secret } from 'lib/crypto';
import { ensureArray, parseSecureToken, parseToken } from 'next-basics'; import { ensureArray, parseSecureToken, parseToken } from 'next-basics';
import { getTeamUser } from 'queries'; import { getTeamUser } from 'queries';
import { getTeamWebsite, getTeamWebsiteByTeamMemberId } from 'queries/admin/teamWebsite';
import { validate } from 'uuid';
import { Auth } from './types'; import { Auth } from './types';
const log = debug('umami:auth'); const log = debug('umami:auth');
@ -59,6 +60,12 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
return true; return true;
} }
const teamWebsite = await getTeamWebsiteByTeamMemberId(websiteId, user.id);
if (teamWebsite) {
return true;
}
const website = await cache.fetchWebsite(websiteId); const website = await cache.fetchWebsite(websiteId);
if (website.userId) { if (website.userId) {
@ -160,6 +167,26 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
return false; return false;
} }
export async function canDeleteTeamWebsite({ user }: Auth, teamWebsiteId: string) {
if (user.isAdmin) {
return true;
}
if (validate(teamWebsiteId)) {
const teamWebsite = await getTeamWebsite(teamWebsiteId);
if (teamWebsite.website.userId === user.id) {
return true;
}
const teamUser = await getTeamUser(teamWebsite.teamId, user.id);
return hasPermission(teamUser.role, PERMISSIONS.teamDelete);
}
return false;
}
export async function canCreateUser({ user }: Auth) { export async function canCreateUser({ user }: Auth) {
return user.isAdmin; return user.isAdmin;
} }

View File

@ -0,0 +1,31 @@
import { canDeleteTeamWebsite } 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 { deleteTeamWebsite } from 'queries/admin/teamWebsite';
export interface TeamWebsiteRequestQuery {
id: string;
}
export default async (
req: NextApiRequestQueryBody<TeamWebsiteRequestQuery>,
res: NextApiResponse,
) => {
await useAuth(req, res);
const { id: teamWebsiteId } = req.query;
if (req.method === 'DELETE') {
if (!(await canDeleteTeamWebsite(req.auth, teamWebsiteId))) {
return unauthorized(res);
}
const websites = await deleteTeamWebsite(teamWebsiteId);
return ok(res, websites);
}
return methodNotAllowed(res);
};

View File

@ -1,17 +1,17 @@
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { NextApiRequestQueryBody } from 'lib/types';
import { canViewTeam } from 'lib/auth'; import { canViewTeam } from 'lib/auth';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { getTeamWebsites } from 'queries/admin/team'; import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeamWebsites, getTeamWebsites } from 'queries/admin/teamWebsite';
export interface TeamWebsiteRequestQuery { export interface TeamWebsiteRequestQuery {
id: string; id: string;
} }
export interface TeamWebsiteRequestBody { export interface TeamWebsiteRequestBody {
websiteId: string;
teamWebsiteId?: string; teamWebsiteId?: string;
websiteIds?: string[];
} }
export default async ( export default async (
@ -21,6 +21,9 @@ export default async (
await useAuth(req, res); await useAuth(req, res);
const { id: teamId } = req.query; const { id: teamId } = req.query;
const {
user: { id: userId },
} = req.auth;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await canViewTeam(req.auth, teamId))) { if (!(await canViewTeam(req.auth, teamId))) {
@ -32,5 +35,17 @@ export default async (
return ok(res, websites); return ok(res, websites);
} }
if (req.method === 'POST') {
if (!(await canViewTeam(req.auth, teamId))) {
return unauthorized(res);
}
const { websiteIds } = req.body;
const websites = await createTeamWebsites(teamId, websiteIds);
return ok(res, websites);
}
return methodNotAllowed(res); return methodNotAllowed(res);
}; };

View File

@ -34,12 +34,14 @@ export default async (
const { name } = req.body; const { name } = req.body;
const team = await createTeam({ const team = await createTeam(
{
id: uuid(), id: uuid(),
name, name,
userId,
accessCode: getRandomChars(16), accessCode: getRandomChars(16),
}); },
userId,
);
return ok(res, team); return ok(res, team);
} }

View File

@ -6,13 +6,13 @@ import { methodNotAllowed, ok, notFound } from 'next-basics';
import { createTeamUser, getTeam } from 'queries'; import { createTeamUser, getTeam } from 'queries';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
export interface TeamsRequestBody { export interface TeamsJoinRequestBody {
accessCode: string; accessCode: string;
} }
export default async ( export default async (
req: NextApiRequestQueryBody<any, TeamsRequestBody>, req: NextApiRequestQueryBody<any, TeamsJoinRequestBody>,
res: NextApiResponse<Team[] | Team>, res: NextApiResponse<Team>,
) => { ) => {
await useAuth(req, res); await useAuth(req, res);

View File

@ -18,26 +18,8 @@ export async function getTeams(where: Prisma.TeamWhereInput): Promise<Team[]> {
}); });
} }
export async function getTeamWebsites(teamId: string): Promise<TeamWebsite[]> { export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<Team> {
return prisma.client.teamWebsite.findMany({ const { id } = data;
where: {
teamId,
},
include: {
team: true,
},
orderBy: [
{
team: {
name: 'asc',
},
},
],
} as any);
}
export async function createTeam(data: Prisma.TeamCreateInput): Promise<Team> {
const { id, userId } = data;
return prisma.transaction([ return prisma.transaction([
prisma.client.team.create({ prisma.client.team.create({

View File

@ -1,43 +1,110 @@
import { TeamWebsite } from '@prisma/client'; import { TeamWebsite, Prisma, Website, Team, User } from '@prisma/client';
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(teamId: string, userId: string): Promise<TeamWebsite> { export async function getTeamWebsite(teamWebsiteId: string): Promise<
TeamWebsite & {
website: Website;
}
> {
return prisma.client.teamWebsite.findFirst({ return prisma.client.teamWebsite.findFirst({
where: { where: {
teamId, id: teamWebsiteId,
userId,
},
});
}
export async function getTeamWebsites(teamId: string): Promise<TeamWebsite[]> {
return prisma.client.teamWebsite.findMany({
where: {
teamId,
}, },
include: { include: {
user: true,
website: true, website: true,
}, },
}); });
} }
export async function createTeamWebsite( export async function getTeamWebsiteByTeamMemberId(
userId: string,
teamId: string,
websiteId: string, websiteId: string,
userId: string,
): Promise<TeamWebsite> { ): Promise<TeamWebsite> {
return prisma.client.teamWebsite.findFirst({
where: {
websiteId,
team: {
teamUser: {
some: {
userId,
},
},
},
},
});
}
export async function getTeamWebsites(teamId: string): Promise<
(TeamWebsite & {
team: Team;
website: Website & {
user: User;
};
})[]
> {
return prisma.client.teamWebsite.findMany({
where: {
teamId,
},
include: {
team: {
include: {
teamUser: {
where: {
role: ROLES.teamOwner,
},
},
},
},
website: {
include: {
user: true,
},
},
},
orderBy: [
{
team: {
name: 'asc',
},
},
],
});
}
export async function createTeamWebsite(teamId: string, websiteId: string): Promise<TeamWebsite> {
return prisma.client.teamWebsite.create({ return prisma.client.teamWebsite.create({
data: { data: {
id: uuid(), id: uuid(),
userId,
teamId, teamId,
websiteId, websiteId,
}, },
}); });
} }
export async function createTeamWebsites(teamId: string, websiteIds: string[]) {
const currentTeamWebsites = await getTeamWebsites(teamId);
// filter out websites that already exists on the team
const addWebsites = websiteIds.filter(
websiteId => !currentTeamWebsites.some(a => a.websiteId === websiteId),
);
const teamWebsites: Prisma.TeamWebsiteCreateManyInput[] = addWebsites.map(a => {
return {
id: uuid(),
teamId,
websiteId: a,
};
});
return prisma.client.teamWebsite.createMany({
data: teamWebsites,
});
}
export async function deleteTeamWebsite(teamWebsiteId: string): Promise<TeamWebsite> { export async function deleteTeamWebsite(teamWebsiteId: string): Promise<TeamWebsite> {
return prisma.client.teamWebsite.delete({ return prisma.client.teamWebsite.delete({
where: { where: {