mirror of
https://github.com/kremalicious/umami.git
synced 2025-01-11 13:44:01 +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' },
|
||||
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
|
||||
viewDetails: { id: 'label.view-details', defaultMessage: 'View details' },
|
||||
deleteTeam: { id: 'label.delete-team', defaultMessage: 'Delete team' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
@ -132,6 +133,10 @@ export const messages = defineMessages({
|
||||
id: 'message.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({
|
||||
|
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);
|
||||
|
||||
export default function TeamEditForm({ teamId, data, onSave }) {
|
||||
export default function TeamEditForm({ teamId, data, onSave, readOnly }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { post, useMutation } = useApi();
|
||||
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 />
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.name)}>
|
||||
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
|
||||
<TextField />
|
||||
</FormInput>
|
||||
{!readOnly && (
|
||||
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
|
||||
<TextField />
|
||||
</FormInput>
|
||||
)}
|
||||
{readOnly && data.name}
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.accessCode)}>
|
||||
<Flexbox gap={10}>
|
||||
<TextField value={accessCode} readOnly allowCopy />
|
||||
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
|
||||
{!readOnly && (
|
||||
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
|
||||
)}
|
||||
</Flexbox>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
||||
</FormButtons>
|
||||
{!readOnly && (
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
||||
</FormButtons>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Loading } from 'react-basics';
|
||||
import useApi from 'hooks/useApi';
|
||||
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
||||
|
||||
export default function TeamMembers({ teamId }) {
|
||||
export default function TeamMembers({ teamId, readOnly }) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading } = useQuery(['teams:users', teamId], () =>
|
||||
get(`/teams/${teamId}/users`),
|
||||
@ -12,5 +12,5 @@ export default function TeamMembers({ teamId }) {
|
||||
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 useUser from 'hooks/useUser';
|
||||
|
||||
export default function TeamMembersTable({ data = [] }) {
|
||||
export default function TeamMembersTable({ data = [], readOnly }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { user } = useUser();
|
||||
|
||||
@ -44,9 +44,9 @@ export default function TeamMembersTable({ data = [] }) {
|
||||
role: formatMessage(
|
||||
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
|
||||
),
|
||||
action: (
|
||||
action: !readOnly && (
|
||||
<Flexbox flex={1} justifyContent="end">
|
||||
<Button disabled={user.id === row?.user?.id}>
|
||||
<Button disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}>
|
||||
<Icon>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
|
@ -1,17 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
|
||||
import useApi from 'hooks/useApi';
|
||||
import Link from 'next/link';
|
||||
import Page from 'components/layout/Page';
|
||||
import TeamEditForm from 'components/pages/settings/teams/TeamEditForm';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import TeamMembers from 'components/pages/settings/teams/TeamMembers';
|
||||
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';
|
||||
|
||||
export default function TeamSettings({ teamId }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { user } = useUser();
|
||||
const [values, setValues] = useState(null);
|
||||
const [tab, setTab] = useState('details');
|
||||
const { get, useQuery } = useApi();
|
||||
@ -25,6 +28,9 @@ export default function TeamSettings({ teamId }) {
|
||||
},
|
||||
{ cacheTime: 0 },
|
||||
);
|
||||
const canEdit = data?.teamUser?.find(
|
||||
({ userId, role }) => role === ROLES.teamOwner && userId === user.id,
|
||||
);
|
||||
|
||||
const handleSave = data => {
|
||||
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="websites">{formatMessage(labels.websites)}</Item>
|
||||
</Tabs>
|
||||
{tab === 'details' && <TeamEditForm teamId={teamId} data={values} onSave={handleSave} />}
|
||||
{tab === 'members' && <TeamMembers teamId={teamId} />}
|
||||
{tab === 'websites' && <TeamWebsites teamId={teamId} />}
|
||||
{tab === 'details' && (
|
||||
<TeamEditForm teamId={teamId} data={values} onSave={handleSave} readOnly={!canEdit} />
|
||||
)}
|
||||
{tab === 'members' && <TeamMembers teamId={teamId} readOnly={!canEdit} />}
|
||||
{tab === 'websites' && <TeamWebsites teamId={teamId} readOnly={!canEdit} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
@ -25,6 +25,12 @@ export default function TeamsList() {
|
||||
};
|
||||
|
||||
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' });
|
||||
};
|
||||
|
||||
@ -67,7 +73,7 @@ export default function TeamsList() {
|
||||
</Flexbox>
|
||||
)}
|
||||
</PageHeader>
|
||||
{hasData && <TeamsTable data={data} />}
|
||||
{hasData && <TeamsTable data={data} onDelete={handleDelete} />}
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
||||
{createButton}
|
||||
|
@ -11,12 +11,15 @@ import {
|
||||
Flexbox,
|
||||
Icons,
|
||||
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 = [] }) {
|
||||
export default function TeamsTable({ data = [], onDelete }) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const columns = [
|
||||
@ -44,7 +47,7 @@ export default function TeamsTable({ data = [] }) {
|
||||
...row,
|
||||
owner: row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username,
|
||||
action: (
|
||||
<Flexbox flex={1} justifyContent="end">
|
||||
<Flexbox flex={1} gap={10} justifyContent="end">
|
||||
<Link href={`/settings/teams/${id}`}>
|
||||
<a>
|
||||
<Button>
|
||||
@ -55,6 +58,17 @@ export default function TeamsTable({ data = [] }) {
|
||||
</Button>
|
||||
</a>
|
||||
</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>
|
||||
),
|
||||
};
|
||||
|
37
lib/auth.ts
37
lib/auth.ts
@ -1,4 +1,5 @@
|
||||
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';
|
||||
@ -60,10 +61,6 @@ export async function canViewWebsite({ user }: Auth, websiteId: string) {
|
||||
return user.id === website.userId;
|
||||
}
|
||||
|
||||
if (website.teamId) {
|
||||
return getTeamUser(website.teamId, user.id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -86,18 +83,16 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!validate(websiteId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
|
||||
if (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;
|
||||
}
|
||||
|
||||
@ -112,12 +107,6 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
||||
return user.id === website.userId;
|
||||
}
|
||||
|
||||
if (website.teamId) {
|
||||
const teamUser = await getTeamUser(website.teamId, user.id);
|
||||
|
||||
return hasPermission(teamUser.role, PERMISSIONS.websiteDelete);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -144,9 +133,13 @@ export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
||||
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) {
|
||||
@ -154,9 +147,13 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
||||
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) {
|
||||
|
@ -33,7 +33,6 @@ export const ROLES = {
|
||||
user: 'user',
|
||||
teamOwner: 'team-owner',
|
||||
teamMember: 'team-member',
|
||||
teamGuest: 'team-guest',
|
||||
} as const;
|
||||
|
||||
export const PERMISSIONS = {
|
||||
@ -54,19 +53,8 @@ export const ROLE_PERMISSIONS = {
|
||||
PERMISSIONS.websiteDelete,
|
||||
PERMISSIONS.teamCreate,
|
||||
],
|
||||
[ROLES.teamOwner]: [
|
||||
PERMISSIONS.teamUpdate,
|
||||
PERMISSIONS.teamDelete,
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
],
|
||||
[ROLES.teamMember]: [
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
],
|
||||
[ROLES.teamGuest]: [],
|
||||
[ROLES.teamOwner]: [PERMISSIONS.teamUpdate, PERMISSIONS.teamDelete],
|
||||
[ROLES.teamMember]: [],
|
||||
} as const;
|
||||
|
||||
export const THEME_COLORS = {
|
||||
|
@ -6,6 +6,9 @@ import { ROLES } from 'lib/constants';
|
||||
export async function getTeam(where: Prisma.TeamWhereInput): Promise<Team> {
|
||||
return prisma.client.team.findFirst({
|
||||
where,
|
||||
include: {
|
||||
teamUser: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user