mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-24 18:26:20 +01:00
Feat/um 197 hook up teams (#1825)
* Link up teams UI. * Fix auth order. * PR touchups.
This commit is contained in:
parent
f908476e71
commit
8a9532f213
@ -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;
|
||||
}
|
||||
|
@ -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 <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 (
|
||||
<div>
|
||||
{hasData && <WebsitesTable data={data} />}
|
||||
{!hasData && formatMessage(messages.noData)}
|
||||
{toast}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
103
components/pages/settings/teams/TeamWebsitesTable.js
Normal file
103
components/pages/settings/teams/TeamWebsitesTable.js
Normal 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>
|
||||
);
|
||||
}
|
@ -76,7 +76,10 @@ export default function TeamsList() {
|
||||
{hasData && <TeamsTable data={data} onDelete={handleDelete} />}
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
||||
{createButton}
|
||||
<Flexbox gap={10}>
|
||||
{joinButton}
|
||||
{createButton}
|
||||
</Flexbox>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
</Page>
|
||||
|
@ -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 }) {
|
||||
<TableBody>
|
||||
{(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: (
|
||||
<Flexbox flex={1} gap={10} justifyContent="end">
|
||||
<Link href={`/settings/teams/${id}`}>
|
||||
@ -56,24 +60,26 @@ export default function TeamsTable({ data = [], onDelete }) {
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
<ModalTrigger>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Trash />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.deleteTeam)}>
|
||||
{close => (
|
||||
<TeamDeleteForm
|
||||
teamId={row.id}
|
||||
teamName={row.name}
|
||||
onSave={onDelete}
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
{showDelete && (
|
||||
<ModalTrigger>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Trash />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.deleteTeam)}>
|
||||
{close => (
|
||||
<TeamDeleteForm
|
||||
teamId={row.id}
|
||||
teamName={row.name}
|
||||
onSave={onDelete}
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
)}
|
||||
</Flexbox>
|
||||
),
|
||||
};
|
||||
|
62
components/pages/settings/teams/WebsiteAddTeamForm.js
Normal file
62
components/pages/settings/teams/WebsiteAddTeamForm.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
29
components/pages/settings/teams/WebsiteTags.js
Normal file
29
components/pages/settings/teams/WebsiteTags.js
Normal 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>
|
||||
);
|
||||
}
|
11
components/pages/settings/teams/WebsiteTags.module.css
Normal file
11
components/pages/settings/teams/WebsiteTags.module.css
Normal 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;
|
||||
}
|
19
db/postgresql/migrations/07_remove_user_id/migration.sql
Normal file
19
db/postgresql/migrations/07_remove_user_id/migration.sql
Normal 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;
|
@ -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")
|
||||
}
|
||||
|
29
lib/auth.ts
29
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;
|
||||
}
|
||||
|
31
pages/api/teamWebsites/[id].ts
Normal file
31
pages/api/teamWebsites/[id].ts
Normal 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);
|
||||
};
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<any, TeamsRequestBody>,
|
||||
res: NextApiResponse<Team[] | Team>,
|
||||
req: NextApiRequestQueryBody<any, TeamsJoinRequestBody>,
|
||||
res: NextApiResponse<Team>,
|
||||
) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
|
@ -18,26 +18,8 @@ export async function getTeams(where: Prisma.TeamWhereInput): Promise<Team[]> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTeamWebsites(teamId: string): Promise<TeamWebsite[]> {
|
||||
return prisma.client.teamWebsite.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
team: {
|
||||
name: 'asc',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
}
|
||||
|
||||
export async function createTeam(data: Prisma.TeamCreateInput): Promise<Team> {
|
||||
const { id, userId } = data;
|
||||
export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<Team> {
|
||||
const { id } = data;
|
||||
|
||||
return prisma.transaction([
|
||||
prisma.client.team.create({
|
||||
|
@ -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<TeamWebsite> {
|
||||
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<TeamWebsite[]> {
|
||||
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<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({
|
||||
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<TeamWebsite> {
|
||||
return prisma.client.teamWebsite.delete({
|
||||
where: {
|
||||
|
Loading…
Reference in New Issue
Block a user