mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Team delete functionality.
This commit is contained in:
parent
835289a1f8
commit
0ce2d1fbfc
@ -65,6 +65,7 @@ export const labels = defineMessages({
|
|||||||
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
|
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
|
||||||
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
|
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
|
||||||
viewDetails: { id: 'label.view-details', defaultMessage: 'View details' },
|
viewDetails: { id: 'label.view-details', defaultMessage: 'View details' },
|
||||||
|
deleteTeam: { id: 'label.delete-team', defaultMessage: 'Delete team' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
@ -132,6 +133,10 @@ export const messages = defineMessages({
|
|||||||
id: 'message.team-not-found',
|
id: 'message.team-not-found',
|
||||||
defaultMessage: 'Team not found.',
|
defaultMessage: 'Team not found.',
|
||||||
},
|
},
|
||||||
|
deleteTeam: {
|
||||||
|
id: 'message.delete-team',
|
||||||
|
defaultMessage: 'To delete this team, type {confirmation} in the box below to confirm.',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const devices = defineMessages({
|
export const devices = defineMessages({
|
||||||
|
44
components/pages/settings/teams/TeamDeleteForm.js
Normal file
44
components/pages/settings/teams/TeamDeleteForm.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormRow,
|
||||||
|
FormButtons,
|
||||||
|
FormInput,
|
||||||
|
SubmitButton,
|
||||||
|
TextField,
|
||||||
|
} from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
|
||||||
|
const CONFIRM_VALUE = 'DELETE';
|
||||||
|
|
||||||
|
export default function TeamDeleteForm({ teamId, onSave, onClose }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { del, useMutation } = useApi();
|
||||||
|
const { mutate, error } = useMutation(data => del(`/teams/${teamId}`, data));
|
||||||
|
|
||||||
|
const handleSubmit = async data => {
|
||||||
|
mutate(data, {
|
||||||
|
onSuccess: async () => {
|
||||||
|
onSave();
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit} error={error}>
|
||||||
|
<p>{formatMessage(messages.deleteTeam, { confirmation: CONFIRM_VALUE })}</p>
|
||||||
|
<FormRow label={formatMessage(labels.confirm)}>
|
||||||
|
<FormInput name="confirmation" rules={{ validate: value => value === CONFIRM_VALUE }}>
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormInput>
|
||||||
|
</FormRow>
|
||||||
|
<FormButtons flex>
|
||||||
|
<SubmitButton variant="danger">{formatMessage(labels.delete)}</SubmitButton>
|
||||||
|
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
@ -16,7 +16,7 @@ import { labels } from 'components/messages';
|
|||||||
|
|
||||||
const generateId = () => getRandomChars(16);
|
const generateId = () => getRandomChars(16);
|
||||||
|
|
||||||
export default function TeamEditForm({ teamId, data, onSave }) {
|
export default function TeamEditForm({ teamId, data, onSave, readOnly }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data));
|
const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data));
|
||||||
@ -47,19 +47,26 @@ export default function TeamEditForm({ teamId, data, onSave }) {
|
|||||||
<TextField value={teamId} readOnly allowCopy />
|
<TextField value={teamId} readOnly allowCopy />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.name)}>
|
<FormRow label={formatMessage(labels.name)}>
|
||||||
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
|
{!readOnly && (
|
||||||
<TextField />
|
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
|
||||||
</FormInput>
|
<TextField />
|
||||||
|
</FormInput>
|
||||||
|
)}
|
||||||
|
{readOnly && data.name}
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.accessCode)}>
|
<FormRow label={formatMessage(labels.accessCode)}>
|
||||||
<Flexbox gap={10}>
|
<Flexbox gap={10}>
|
||||||
<TextField value={accessCode} readOnly allowCopy />
|
<TextField value={accessCode} readOnly allowCopy />
|
||||||
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
|
{!readOnly && (
|
||||||
|
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
|
||||||
|
)}
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
{!readOnly && (
|
||||||
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
<FormButtons>
|
||||||
</FormButtons>
|
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
||||||
|
</FormButtons>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { Loading } from 'react-basics';
|
|||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
||||||
|
|
||||||
export default function TeamMembers({ teamId }) {
|
export default function TeamMembers({ teamId, readOnly }) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading } = useQuery(['teams:users', teamId], () =>
|
const { data, isLoading } = useQuery(['teams:users', teamId], () =>
|
||||||
get(`/teams/${teamId}/users`),
|
get(`/teams/${teamId}/users`),
|
||||||
@ -12,5 +12,5 @@ export default function TeamMembers({ teamId }) {
|
|||||||
return <Loading icon="dots" position="block" />;
|
return <Loading icon="dots" position="block" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TeamMembersTable data={data} />;
|
return <TeamMembersTable data={data} readOnly={readOnly} />;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import { ROLES } from 'lib/constants';
|
|||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
export default function TeamMembersTable({ data = [] }) {
|
export default function TeamMembersTable({ data = [], readOnly }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
@ -44,9 +44,9 @@ export default function TeamMembersTable({ data = [] }) {
|
|||||||
role: formatMessage(
|
role: formatMessage(
|
||||||
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
|
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
|
||||||
),
|
),
|
||||||
action: (
|
action: !readOnly && (
|
||||||
<Flexbox flex={1} justifyContent="end">
|
<Flexbox flex={1} justifyContent="end">
|
||||||
<Button disabled={user.id === row?.user?.id}>
|
<Button disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Close />
|
<Icons.Close />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
|
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
|
||||||
import useApi from 'hooks/useApi';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import TeamEditForm from 'components/pages/settings/teams/TeamEditForm';
|
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import TeamMembers from 'components/pages/settings/teams/TeamMembers';
|
|
||||||
import { labels, messages } from 'components/messages';
|
import { labels, messages } from 'components/messages';
|
||||||
|
import { ROLES } from 'lib/constants';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
import TeamEditForm from './TeamEditForm';
|
||||||
|
import TeamMembers from './TeamMembers';
|
||||||
import TeamWebsites from './TeamWebsites';
|
import TeamWebsites from './TeamWebsites';
|
||||||
|
|
||||||
export default function TeamSettings({ teamId }) {
|
export default function TeamSettings({ teamId }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const { user } = useUser();
|
||||||
const [values, setValues] = useState(null);
|
const [values, setValues] = useState(null);
|
||||||
const [tab, setTab] = useState('details');
|
const [tab, setTab] = useState('details');
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
@ -25,6 +28,9 @@ export default function TeamSettings({ teamId }) {
|
|||||||
},
|
},
|
||||||
{ cacheTime: 0 },
|
{ cacheTime: 0 },
|
||||||
);
|
);
|
||||||
|
const canEdit = data?.teamUser?.find(
|
||||||
|
({ userId, role }) => role === ROLES.teamOwner && userId === user.id,
|
||||||
|
);
|
||||||
|
|
||||||
const handleSave = data => {
|
const handleSave = data => {
|
||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
@ -55,9 +61,11 @@ export default function TeamSettings({ teamId }) {
|
|||||||
<Item key="members">{formatMessage(labels.members)}</Item>
|
<Item key="members">{formatMessage(labels.members)}</Item>
|
||||||
<Item key="websites">{formatMessage(labels.websites)}</Item>
|
<Item key="websites">{formatMessage(labels.websites)}</Item>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{tab === 'details' && <TeamEditForm teamId={teamId} data={values} onSave={handleSave} />}
|
{tab === 'details' && (
|
||||||
{tab === 'members' && <TeamMembers teamId={teamId} />}
|
<TeamEditForm teamId={teamId} data={values} onSave={handleSave} readOnly={!canEdit} />
|
||||||
{tab === 'websites' && <TeamWebsites teamId={teamId} />}
|
)}
|
||||||
|
{tab === 'members' && <TeamMembers teamId={teamId} readOnly={!canEdit} />}
|
||||||
|
{tab === 'websites' && <TeamWebsites teamId={teamId} readOnly={!canEdit} />}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,12 @@ export default function TeamsList() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleJoin = () => {
|
const handleJoin = () => {
|
||||||
|
setUpdate(state => state + 1);
|
||||||
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
setUpdate(state => state + 1);
|
||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,7 +73,7 @@ export default function TeamsList() {
|
|||||||
</Flexbox>
|
</Flexbox>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{hasData && <TeamsTable data={data} />}
|
{hasData && <TeamsTable data={data} onDelete={handleDelete} />}
|
||||||
{!hasData && (
|
{!hasData && (
|
||||||
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
||||||
{createButton}
|
{createButton}
|
||||||
|
@ -11,12 +11,15 @@ import {
|
|||||||
Flexbox,
|
Flexbox,
|
||||||
Icons,
|
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 { labels } from 'components/messages';
|
||||||
import { ROLES } from 'lib/constants';
|
import { ROLES } from 'lib/constants';
|
||||||
|
import TeamDeleteForm from './TeamDeleteForm';
|
||||||
|
|
||||||
export default function TeamsTable({ data = [] }) {
|
export default function TeamsTable({ data = [], onDelete }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@ -44,7 +47,7 @@ export default function TeamsTable({ data = [] }) {
|
|||||||
...row,
|
...row,
|
||||||
owner: row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username,
|
owner: row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username,
|
||||||
action: (
|
action: (
|
||||||
<Flexbox flex={1} justifyContent="end">
|
<Flexbox flex={1} gap={10} justifyContent="end">
|
||||||
<Link href={`/settings/teams/${id}`}>
|
<Link href={`/settings/teams/${id}`}>
|
||||||
<a>
|
<a>
|
||||||
<Button>
|
<Button>
|
||||||
@ -55,6 +58,17 @@ export default function TeamsTable({ data = [] }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
<ModalTrigger>
|
||||||
|
<Button>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Trash />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.delete)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal title={formatMessage(labels.deleteTeam)}>
|
||||||
|
{close => <TeamDeleteForm teamId={row.id} onSave={onDelete} onClose={close} />}
|
||||||
|
</Modal>
|
||||||
|
</ModalTrigger>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
37
lib/auth.ts
37
lib/auth.ts
@ -1,4 +1,5 @@
|
|||||||
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';
|
||||||
@ -60,10 +61,6 @@ export async function canViewWebsite({ user }: Auth, websiteId: string) {
|
|||||||
return user.id === website.userId;
|
return user.id === website.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (website.teamId) {
|
|
||||||
return getTeamUser(website.teamId, user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,18 +83,16 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validate(websiteId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const website = await cache.fetchWebsite(websiteId);
|
const website = await cache.fetchWebsite(websiteId);
|
||||||
|
|
||||||
if (website.userId) {
|
if (website.userId) {
|
||||||
return user.id === website.userId;
|
return user.id === website.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (website.teamId) {
|
|
||||||
const teamUser = await getTeamUser(website.teamId, user.id);
|
|
||||||
|
|
||||||
return hasPermission(teamUser.role, PERMISSIONS.websiteUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,12 +107,6 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
|||||||
return user.id === website.userId;
|
return user.id === website.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (website.teamId) {
|
|
||||||
const teamUser = await getTeamUser(website.teamId, user.id);
|
|
||||||
|
|
||||||
return hasPermission(teamUser.role, PERMISSIONS.websiteDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,9 +133,13 @@ export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamUser = await getTeamUser(teamId, user.id);
|
if (validate(teamId)) {
|
||||||
|
const teamUser = await getTeamUser(teamId, user.id);
|
||||||
|
|
||||||
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
|
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
||||||
@ -154,9 +147,13 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamUser = await getTeamUser(teamId, user.id);
|
if (validate(teamId)) {
|
||||||
|
const teamUser = await getTeamUser(teamId, user.id);
|
||||||
|
|
||||||
return hasPermission(teamUser.role, PERMISSIONS.teamDelete);
|
return hasPermission(teamUser.role, PERMISSIONS.teamDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canCreateUser({ user }: Auth) {
|
export async function canCreateUser({ user }: Auth) {
|
||||||
|
@ -33,7 +33,6 @@ export const ROLES = {
|
|||||||
user: 'user',
|
user: 'user',
|
||||||
teamOwner: 'team-owner',
|
teamOwner: 'team-owner',
|
||||||
teamMember: 'team-member',
|
teamMember: 'team-member',
|
||||||
teamGuest: 'team-guest',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PERMISSIONS = {
|
export const PERMISSIONS = {
|
||||||
@ -54,19 +53,8 @@ export const ROLE_PERMISSIONS = {
|
|||||||
PERMISSIONS.websiteDelete,
|
PERMISSIONS.websiteDelete,
|
||||||
PERMISSIONS.teamCreate,
|
PERMISSIONS.teamCreate,
|
||||||
],
|
],
|
||||||
[ROLES.teamOwner]: [
|
[ROLES.teamOwner]: [PERMISSIONS.teamUpdate, PERMISSIONS.teamDelete],
|
||||||
PERMISSIONS.teamUpdate,
|
[ROLES.teamMember]: [],
|
||||||
PERMISSIONS.teamDelete,
|
|
||||||
PERMISSIONS.websiteCreate,
|
|
||||||
PERMISSIONS.websiteUpdate,
|
|
||||||
PERMISSIONS.websiteDelete,
|
|
||||||
],
|
|
||||||
[ROLES.teamMember]: [
|
|
||||||
PERMISSIONS.websiteCreate,
|
|
||||||
PERMISSIONS.websiteUpdate,
|
|
||||||
PERMISSIONS.websiteDelete,
|
|
||||||
],
|
|
||||||
[ROLES.teamGuest]: [],
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const THEME_COLORS = {
|
export const THEME_COLORS = {
|
||||||
|
@ -6,6 +6,9 @@ import { ROLES } from 'lib/constants';
|
|||||||
export async function getTeam(where: Prisma.TeamWhereInput): Promise<Team> {
|
export async function getTeam(where: Prisma.TeamWhereInput): Promise<Team> {
|
||||||
return prisma.client.team.findFirst({
|
return prisma.client.team.findFirst({
|
||||||
where,
|
where,
|
||||||
|
include: {
|
||||||
|
teamUser: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user