mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-24 18:26:20 +01:00
Merge branch 'dev' of https://github.com/umami-software/umami into dev
This commit is contained in:
commit
9a3e8921a7
@ -1,10 +1,14 @@
|
|||||||
import { Loading } from 'react-basics';
|
import { messages } from 'components/messages';
|
||||||
import useApi from 'hooks/useApi';
|
|
||||||
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
import { Loading, useToast } from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
export default function TeamMembers({ teamId, readOnly }) {
|
export default function TeamMembers({ teamId, readOnly }) {
|
||||||
|
const { toast, showToast } = useToast();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading } = useQuery(['teams:users', teamId], () =>
|
const { formatMessage } = useIntl();
|
||||||
|
const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () =>
|
||||||
get(`/teams/${teamId}/users`),
|
get(`/teams/${teamId}/users`),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -12,5 +16,15 @@ export default function TeamMembers({ teamId, readOnly }) {
|
|||||||
return <Loading icon="dots" position="block" />;
|
return <Loading icon="dots" position="block" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TeamMembersTable data={data} readOnly={readOnly} />;
|
const handleSave = async () => {
|
||||||
|
await refetch();
|
||||||
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{toast}
|
||||||
|
<TeamMembersTable onSave={handleSave} data={data} readOnly={readOnly} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,13 @@ import { useIntl } from 'react-intl';
|
|||||||
import { ROLES } from 'lib/constants';
|
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';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
|
||||||
export default function TeamMembersTable({ data = [], readOnly }) {
|
export default function TeamMembersTable({ data = [], onSave, readOnly }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
const { del, useMutation } = useApi();
|
||||||
|
const { mutate } = useMutation(data => del(`/teamUsers/${data.teamUserId}`));
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'username', label: formatMessage(labels.username), style: { flex: 2 } },
|
{ name: 'username', label: formatMessage(labels.username), style: { flex: 2 } },
|
||||||
@ -26,6 +29,17 @@ export default function TeamMembersTable({ data = [], readOnly }) {
|
|||||||
{ name: 'action', label: '', style: { flex: 1 } },
|
{ name: 'action', label: '', style: { flex: 1 } },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleRemoveTeamMember = teamUserId => {
|
||||||
|
mutate(
|
||||||
|
{ teamUserId },
|
||||||
|
{
|
||||||
|
onSuccess: async () => {
|
||||||
|
onSave();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table columns={columns} rows={data}>
|
<Table columns={columns} rows={data}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@ -46,7 +60,10 @@ export default function TeamMembersTable({ data = [], readOnly }) {
|
|||||||
),
|
),
|
||||||
action: !readOnly && (
|
action: !readOnly && (
|
||||||
<Flexbox flex={1} justifyContent="end">
|
<Flexbox flex={1} justifyContent="end">
|
||||||
<Button disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}>
|
<Button
|
||||||
|
onClick={() => handleRemoveTeamMember(row.id)}
|
||||||
|
disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}
|
||||||
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Close />
|
<Icons.Close />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
18
lib/auth.ts
18
lib/auth.ts
@ -3,7 +3,7 @@ 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, getTeamUserById } from 'queries';
|
||||||
import { getTeamWebsite, getTeamWebsiteByTeamMemberId } from 'queries/admin/teamWebsite';
|
import { getTeamWebsite, getTeamWebsiteByTeamMemberId } from 'queries/admin/teamWebsite';
|
||||||
import { validate } from 'uuid';
|
import { validate } from 'uuid';
|
||||||
import { Auth } from './types';
|
import { Auth } from './types';
|
||||||
@ -167,6 +167,22 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function canDeleteTeamUser({ user }: Auth, teamUserId: string) {
|
||||||
|
if (user.isAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validate(teamUserId)) {
|
||||||
|
const removeUser = await getTeamUserById(teamUserId);
|
||||||
|
|
||||||
|
const teamUser = await getTeamUser(removeUser.teamId, user.id);
|
||||||
|
|
||||||
|
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export async function canDeleteTeamWebsite({ user }: Auth, teamWebsiteId: string) {
|
export async function canDeleteTeamWebsite({ user }: Auth, teamWebsiteId: string) {
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
|
28
pages/api/teamUsers/[id].ts
Normal file
28
pages/api/teamUsers/[id].ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { canDeleteTeamUser } 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 { deleteTeamUser } from 'queries/admin/teamUser';
|
||||||
|
|
||||||
|
export interface TeamUserRequestQuery {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: NextApiRequestQueryBody<TeamUserRequestQuery>, res: NextApiResponse) => {
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
const { id: teamUserId } = req.query;
|
||||||
|
|
||||||
|
if (req.method === 'DELETE') {
|
||||||
|
if (!(await canDeleteTeamUser(req.auth, teamUserId))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const websites = await deleteTeamUser(teamUserId);
|
||||||
|
|
||||||
|
return ok(res, websites);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
@ -52,17 +52,17 @@ export async function updateTeam(
|
|||||||
export async function deleteTeam(
|
export async function deleteTeam(
|
||||||
teamId: string,
|
teamId: string,
|
||||||
): Promise<Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Team]>> {
|
): Promise<Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Team]>> {
|
||||||
const { client } = prisma;
|
const { client, transaction } = prisma;
|
||||||
|
|
||||||
return prisma.transaction([
|
return transaction([
|
||||||
client.teamWebsite.deleteMany({
|
client.teamWebsite.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
teamId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
client.teamUser.deleteMany({
|
client.teamUser.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
teamId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
client.team.delete({
|
client.team.delete({
|
||||||
|
@ -2,6 +2,14 @@ import { Prisma, TeamUser } from '@prisma/client';
|
|||||||
import { uuid } from 'lib/crypto';
|
import { uuid } from 'lib/crypto';
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
|
export async function getTeamUserById(teamUserId: string): Promise<TeamUser> {
|
||||||
|
return prisma.client.teamUser.findUnique({
|
||||||
|
where: {
|
||||||
|
id: teamUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getTeamUser(teamId: string, userId: string): Promise<TeamUser> {
|
export async function getTeamUser(teamId: string, userId: string): Promise<TeamUser> {
|
||||||
return prisma.client.teamUser.findFirst({
|
return prisma.client.teamUser.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -48,11 +56,25 @@ export async function updateTeamUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTeamUser(teamUserId: string): Promise<TeamUser> {
|
export async function deleteTeamUser(teamUserId: string): Promise<TeamUser> {
|
||||||
return prisma.client.teamUser.delete({
|
const { client, transaction } = prisma;
|
||||||
where: {
|
|
||||||
id: teamUserId,
|
const teamUser = await getTeamUserById(teamUserId);
|
||||||
},
|
|
||||||
});
|
return transaction([
|
||||||
|
client.teamWebsite.deleteMany({
|
||||||
|
where: {
|
||||||
|
teamId: teamUser.teamId,
|
||||||
|
website: {
|
||||||
|
userId: teamUser.userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
client.teamUser.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: teamUserId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTeamUserByUserId(
|
export async function deleteTeamUserByUserId(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Prisma, Team } from '@prisma/client';
|
import { Prisma, Team } from '@prisma/client';
|
||||||
import cache from 'lib/cache';
|
import cache from 'lib/cache';
|
||||||
|
import { ROLES } from 'lib/constants';
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import { Website, User, Roles } from 'lib/types';
|
import { Website, User, Roles } from 'lib/types';
|
||||||
|
|
||||||
@ -134,6 +135,19 @@ export async function deleteUser(
|
|||||||
websiteIds = websites.map(a => a.id);
|
websiteIds = websites.map(a => a.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const teams = await client.team.findMany({
|
||||||
|
where: {
|
||||||
|
teamUser: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
role: ROLES.teamOwner,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamIds = teams.map(a => a.id);
|
||||||
|
|
||||||
return prisma
|
return prisma
|
||||||
.transaction([
|
.transaction([
|
||||||
client.websiteEvent.deleteMany({
|
client.websiteEvent.deleteMany({
|
||||||
@ -144,21 +158,39 @@ export async function deleteUser(
|
|||||||
}),
|
}),
|
||||||
client.teamWebsite.deleteMany({
|
client.teamWebsite.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
website: {
|
OR: [
|
||||||
userId,
|
{
|
||||||
|
websiteId: {
|
||||||
|
in: websiteIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teamId: {
|
||||||
|
in: teamIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
client.teamWebsite.deleteMany({
|
||||||
|
where: {
|
||||||
|
teamId: {
|
||||||
|
in: teamIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
client.teamUser.deleteMany({
|
client.teamUser.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
team: {
|
teamId: {
|
||||||
userId,
|
in: teamIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
client.team.deleteMany({
|
client.team.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
id: {
|
||||||
|
in: teamIds,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
cloudMode
|
cloudMode
|
||||||
|
Loading…
Reference in New Issue
Block a user