diff --git a/components/messages.js b/components/messages.js
index 608d4fbe..c09fcad3 100644
--- a/components/messages.js
+++ b/components/messages.js
@@ -46,6 +46,7 @@ export const labels = defineMessages({
deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' },
reset: { id: 'label.reset', defaultMessage: 'Reset' },
addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' },
+ addWebsites: { id: 'label.add-websites', defaultMessage: 'Add websites' },
changePassword: { id: 'label.change-password', defaultMessage: 'Change password' },
currentPassword: { id: 'label.current-password', defaultMessage: 'Current password' },
newPassword: { id: 'label.new-password', defaultMessage: 'New password' },
@@ -145,6 +146,10 @@ export const messages = defineMessages({
id: 'message.reset-website',
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: {
id: 'message.invalid-domain',
defaultMessage: 'Invalid domain. Do not include http/https.',
@@ -162,6 +167,14 @@ export const messages = defineMessages({
id: 'messages.no-websites',
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.' },
goToSettings: {
id: 'message.go-to-settings',
@@ -183,17 +196,6 @@ export const messages = defineMessages({
id: 'message.event-log',
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) {
@@ -201,7 +203,3 @@ export function getMessage(id, formatMessage) {
return message ? formatMessage(message) : id;
}
-
-export function getDeviceMessage(device) {
- return devices[device] || labels.unknown;
-}
diff --git a/components/pages/settings/teams/TeamWebsites.js b/components/pages/settings/teams/TeamWebsites.js
index 1a04f490..494a6eb0 100644
--- a/components/pages/settings/teams/TeamWebsites.js
+++ b/components/pages/settings/teams/TeamWebsites.js
@@ -1,13 +1,26 @@
-import { Loading } from 'react-basics';
-import { useIntl } from 'react-intl';
+import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
+import { labels, messages } from 'components/messages';
+import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable';
import useApi from 'hooks/useApi';
-import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
-import { messages } from 'components/messages';
+import {
+ 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 }) {
+ const { toast, showToast } = useToast();
const { formatMessage } = useIntl();
const { get, useQuery } = useApi();
- const { data, isLoading } = useQuery(['teams:websites', teamId], () =>
+ const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () =>
get(`/teams/${teamId}/websites`),
);
const hasData = data && data.length !== 0;
@@ -16,10 +29,37 @@ export default function TeamWebsites({ teamId }) {
return ;
}
+ const handleSave = async () => {
+ await refetch();
+ showToast({ message: formatMessage(messages.saved), variant: 'success' });
+ };
+
+ const addButton = (
+
+
+
+ {close => }
+
+
+ );
+
return (
- {hasData &&
}
- {!hasData && formatMessage(messages.noData)}
+ {toast}
+ {hasData && (
+
{addButton}
+ )}
+ {hasData &&
}
+ {!hasData && (
+
+ {addButton}
+
+ )}
);
}
diff --git a/components/pages/settings/teams/TeamWebsitesTable.js b/components/pages/settings/teams/TeamWebsitesTable.js
new file mode 100644
index 00000000..a596204c
--- /dev/null
+++ b/components/pages/settings/teams/TeamWebsitesTable.js
@@ -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 (
+
+
+ {(column, index) => {
+ return (
+
+ {column.label}
+
+ );
+ }}
+
+
+ {(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 = (
+
+
+
+
+ {canRemove && (
+
+ )}
+
+ );
+
+ return (
+
+ {(data, key, colIndex) => {
+ return (
+
+
+ {data[key]}
+
+
+ );
+ }}
+
+ );
+ }}
+
+
+ );
+}
diff --git a/components/pages/settings/teams/TeamsList.js b/components/pages/settings/teams/TeamsList.js
index b48e9971..0e6ef9c2 100644
--- a/components/pages/settings/teams/TeamsList.js
+++ b/components/pages/settings/teams/TeamsList.js
@@ -76,7 +76,10 @@ export default function TeamsList() {
{hasData && }
{!hasData && (
- {createButton}
+
+ {joinButton}
+ {createButton}
+
)}
diff --git a/components/pages/settings/teams/TeamsTable.js b/components/pages/settings/teams/TeamsTable.js
index 7f22435c..4f70cf6d 100644
--- a/components/pages/settings/teams/TeamsTable.js
+++ b/components/pages/settings/teams/TeamsTable.js
@@ -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 {
+ Button,
+ Flexbox,
+ Icon,
+ Icons,
+ Modal,
+ ModalTrigger,
Table,
- TableHeader,
TableBody,
- TableRow,
TableCell,
TableColumn,
- Button,
- Icon,
- Flexbox,
- Icons,
+ TableHeader,
+ TableRow,
Text,
- ModalTrigger,
- Modal,
} from 'react-basics';
import { useIntl } from 'react-intl';
-import { labels } from 'components/messages';
-import { ROLES } from 'lib/constants';
import TeamDeleteForm from './TeamDeleteForm';
export default function TeamsTable({ data = [], onDelete }) {
const { formatMessage } = useIntl();
+ const { user } = useUser();
const columns = [
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
@@ -42,10 +44,12 @@ export default function TeamsTable({ data = [], onDelete }) {
{(row, keys, rowIndex) => {
const { id } = row;
+ const owner = row.teamUser.find(({ role }) => role === ROLES.teamOwner);
+ const showDelete = user.id === owner?.userId;
const rowData = {
...row,
- owner: row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username,
+ owner: owner?.user?.username,
action: (
@@ -56,24 +60,26 @@ export default function TeamsTable({ data = [], onDelete }) {
{formatMessage(labels.edit)}
-
-
-
- {close => (
-
- )}
-
-
+ {showDelete && (
+
+
+
+ {close => (
+
+ )}
+
+
+ )}
),
};
diff --git a/components/pages/settings/teams/WebsiteAddTeamForm.js b/components/pages/settings/teams/WebsiteAddTeamForm.js
new file mode 100644
index 00000000..5d85e5de
--- /dev/null
+++ b/components/pages/settings/teams/WebsiteAddTeamForm.js
@@ -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 (
+ <>
+
+ >
+ );
+}
diff --git a/components/pages/settings/teams/WebsiteTags.js b/components/pages/settings/teams/WebsiteTags.js
new file mode 100644
index 00000000..19179422
--- /dev/null
+++ b/components/pages/settings/teams/WebsiteTags.js
@@ -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 (
+
+ {websites.map(websiteId => {
+ const website = items.find(a => a.id === websiteId);
+
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/components/pages/settings/teams/WebsiteTags.module.css b/components/pages/settings/teams/WebsiteTags.module.css
new file mode 100644
index 00000000..50ae60a0
--- /dev/null
+++ b/components/pages/settings/teams/WebsiteTags.module.css
@@ -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;
+}
diff --git a/db/postgresql/migrations/07_remove_user_id/migration.sql b/db/postgresql/migrations/07_remove_user_id/migration.sql
new file mode 100644
index 00000000..63122f49
--- /dev/null
+++ b/db/postgresql/migrations/07_remove_user_id/migration.sql
@@ -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;
diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma
index c91d2a06..be3be8f1 100644
--- a/db/postgresql/schema.prisma
+++ b/db/postgresql/schema.prisma
@@ -17,9 +17,8 @@ model User {
updatedAt DateTime? @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
- Website Website[]
- teamUser TeamUser[]
- teamWebsite TeamWebsite[]
+ Website Website[]
+ teamUser TeamUser[]
@@map("user")
}
@@ -86,7 +85,6 @@ model WebsiteEvent {
model Team {
id String @id() @unique() @map("team_id") @db.Uuid
name String @db.VarChar(50)
- userId String @map("user_id") @db.Uuid
accessCode String? @unique @map("access_code") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @db.Timestamptz(6)
@@ -94,7 +92,6 @@ model Team {
teamUser TeamUser[]
teamWebsite TeamWebsite[]
- @@index([userId])
@@index([accessCode])
@@map("team")
}
@@ -118,16 +115,13 @@ model TeamUser {
model TeamWebsite {
id String @id() @unique() @map("team_website_id") @db.Uuid
teamId String @map("team_id") @db.Uuid
- userId String @map("user_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
team Team @relation(fields: [teamId], references: [id])
- user User @relation(fields: [userId], references: [id])
website Website @relation(fields: [websiteId], references: [id])
@@index([teamId])
- @@index([userId])
@@index([websiteId])
@@map("team_website")
}
diff --git a/lib/auth.ts b/lib/auth.ts
index 1722b15d..b602c3d2 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -1,10 +1,11 @@
import debug from 'debug';
-import { validate } from 'uuid';
import cache from 'lib/cache';
import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants';
import { secret } from 'lib/crypto';
import { ensureArray, parseSecureToken, parseToken } from 'next-basics';
import { getTeamUser } from 'queries';
+import { getTeamWebsite, getTeamWebsiteByTeamMemberId } from 'queries/admin/teamWebsite';
+import { validate } from 'uuid';
import { Auth } from './types';
const log = debug('umami:auth');
@@ -59,6 +60,12 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
return true;
}
+ const teamWebsite = await getTeamWebsiteByTeamMemberId(websiteId, user.id);
+
+ if (teamWebsite) {
+ return true;
+ }
+
const website = await cache.fetchWebsite(websiteId);
if (website.userId) {
@@ -160,6 +167,26 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
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) {
return user.isAdmin;
}
diff --git a/pages/api/teamWebsites/[id].ts b/pages/api/teamWebsites/[id].ts
new file mode 100644
index 00000000..96222e08
--- /dev/null
+++ b/pages/api/teamWebsites/[id].ts
@@ -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,
+ 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);
+};
diff --git a/pages/api/teams/[id]/websites.ts b/pages/api/teams/[id]/websites.ts
index 2c35600d..0db429d1 100644
--- a/pages/api/teams/[id]/websites.ts
+++ b/pages/api/teams/[id]/websites.ts
@@ -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 { 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 {
id: string;
}
export interface TeamWebsiteRequestBody {
- websiteId: string;
teamWebsiteId?: string;
+ websiteIds?: string[];
}
export default async (
@@ -21,6 +21,9 @@ export default async (
await useAuth(req, res);
const { id: teamId } = req.query;
+ const {
+ user: { id: userId },
+ } = req.auth;
if (req.method === 'GET') {
if (!(await canViewTeam(req.auth, teamId))) {
@@ -32,5 +35,17 @@ export default async (
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);
};
diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts
index 283f27ca..453f1ef3 100644
--- a/pages/api/teams/index.ts
+++ b/pages/api/teams/index.ts
@@ -34,12 +34,14 @@ export default async (
const { name } = req.body;
- const team = await createTeam({
- id: uuid(),
- name,
+ const team = await createTeam(
+ {
+ id: uuid(),
+ name,
+ accessCode: getRandomChars(16),
+ },
userId,
- accessCode: getRandomChars(16),
- });
+ );
return ok(res, team);
}
diff --git a/pages/api/teams/join.ts b/pages/api/teams/join.ts
index 99fa2a5c..95a41423 100644
--- a/pages/api/teams/join.ts
+++ b/pages/api/teams/join.ts
@@ -6,13 +6,13 @@ import { methodNotAllowed, ok, notFound } from 'next-basics';
import { createTeamUser, getTeam } from 'queries';
import { ROLES } from 'lib/constants';
-export interface TeamsRequestBody {
+export interface TeamsJoinRequestBody {
accessCode: string;
}
export default async (
- req: NextApiRequestQueryBody,
- res: NextApiResponse,
+ req: NextApiRequestQueryBody,
+ res: NextApiResponse,
) => {
await useAuth(req, res);
diff --git a/queries/admin/team.ts b/queries/admin/team.ts
index 19373eba..5f6e763e 100644
--- a/queries/admin/team.ts
+++ b/queries/admin/team.ts
@@ -18,26 +18,8 @@ export async function getTeams(where: Prisma.TeamWhereInput): Promise {
});
}
-export async function getTeamWebsites(teamId: string): Promise {
- return prisma.client.teamWebsite.findMany({
- where: {
- teamId,
- },
- include: {
- team: true,
- },
- orderBy: [
- {
- team: {
- name: 'asc',
- },
- },
- ],
- } as any);
-}
-
-export async function createTeam(data: Prisma.TeamCreateInput): Promise {
- const { id, userId } = data;
+export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise {
+ const { id } = data;
return prisma.transaction([
prisma.client.team.create({
diff --git a/queries/admin/teamWebsite.ts b/queries/admin/teamWebsite.ts
index affba285..bff1a9ee 100644
--- a/queries/admin/teamWebsite.ts
+++ b/queries/admin/teamWebsite.ts
@@ -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 prisma from 'lib/prisma';
-export async function getTeamWebsite(teamId: string, userId: string): Promise {
+export async function getTeamWebsite(teamWebsiteId: string): Promise<
+ TeamWebsite & {
+ website: Website;
+ }
+> {
return prisma.client.teamWebsite.findFirst({
where: {
- teamId,
- userId,
- },
- });
-}
-
-export async function getTeamWebsites(teamId: string): Promise {
- return prisma.client.teamWebsite.findMany({
- where: {
- teamId,
+ id: teamWebsiteId,
},
include: {
- user: true,
website: true,
},
});
}
-export async function createTeamWebsite(
- userId: string,
- teamId: string,
+export async function getTeamWebsiteByTeamMemberId(
websiteId: string,
+ userId: string,
): Promise {
+ 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 {
return prisma.client.teamWebsite.create({
data: {
id: uuid(),
- userId,
teamId,
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 {
return prisma.client.teamWebsite.delete({
where: {