mirror of
https://github.com/stonith404/pingvin-share.git
synced 2024-11-18 21:30:12 +01:00
feat: add admin-exclusive share-management page (#461)
* testing with all_shares * share table * share table * change icon on admin page * add share size to list --------- Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
parent
a45184995f
commit
3b1c9f1efb
27
backend/src/share/dto/adminShare.dto.ts
Normal file
27
backend/src/share/dto/adminShare.dto.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { OmitType } from "@nestjs/swagger";
|
||||||
|
import { Expose, plainToClass } from "class-transformer";
|
||||||
|
import { ShareDTO } from "./share.dto";
|
||||||
|
|
||||||
|
export class AdminShareDTO extends OmitType(ShareDTO, [
|
||||||
|
"files",
|
||||||
|
"from",
|
||||||
|
"fromList",
|
||||||
|
] as const) {
|
||||||
|
@Expose()
|
||||||
|
views: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
from(partial: Partial<AdminShareDTO>) {
|
||||||
|
return plainToClass(AdminShareDTO, partial, {
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fromList(partial: Partial<AdminShareDTO>[]) {
|
||||||
|
return partial.map((part) =>
|
||||||
|
plainToClass(AdminShareDTO, part, { excludeExtraneousValues: true }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ import { Throttle } from "@nestjs/throttler";
|
|||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { GetUser } from "src/auth/decorator/getUser.decorator";
|
import { GetUser } from "src/auth/decorator/getUser.decorator";
|
||||||
|
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
||||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||||
import { CreateShareDTO } from "./dto/createShare.dto";
|
import { CreateShareDTO } from "./dto/createShare.dto";
|
||||||
import { MyShareDTO } from "./dto/myShare.dto";
|
import { MyShareDTO } from "./dto/myShare.dto";
|
||||||
@ -25,10 +26,17 @@ import { ShareOwnerGuard } from "./guard/shareOwner.guard";
|
|||||||
import { ShareSecurityGuard } from "./guard/shareSecurity.guard";
|
import { ShareSecurityGuard } from "./guard/shareSecurity.guard";
|
||||||
import { ShareTokenSecurity } from "./guard/shareTokenSecurity.guard";
|
import { ShareTokenSecurity } from "./guard/shareTokenSecurity.guard";
|
||||||
import { ShareService } from "./share.service";
|
import { ShareService } from "./share.service";
|
||||||
|
import { AdminShareDTO } from "./dto/adminShare.dto";
|
||||||
@Controller("shares")
|
@Controller("shares")
|
||||||
export class ShareController {
|
export class ShareController {
|
||||||
constructor(private shareService: ShareService) {}
|
constructor(private shareService: ShareService) {}
|
||||||
|
|
||||||
|
@Get("all")
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async getAllShares() {
|
||||||
|
return new AdminShareDTO().fromList(await this.shareService.getShares());
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
async getMyShares(@GetUser() user: User) {
|
async getMyShares(@GetUser() user: User) {
|
||||||
|
@ -194,6 +194,22 @@ export class ShareService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getShares() {
|
||||||
|
const shares = await this.prisma.share.findMany({
|
||||||
|
orderBy: {
|
||||||
|
expiration: "desc",
|
||||||
|
},
|
||||||
|
include: { files: true, creator: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return shares.map((share) => {
|
||||||
|
return {
|
||||||
|
...share,
|
||||||
|
size: share.files.reduce((acc, file) => acc + parseInt(file.size), 0),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getSharesByUser(userId: string) {
|
async getSharesByUser(userId: string) {
|
||||||
const shares = await this.prisma.share.findMany({
|
const shares = await this.prisma.share.findMany({
|
||||||
where: {
|
where: {
|
||||||
@ -214,7 +230,6 @@ export class ShareService {
|
|||||||
return shares.map((share) => {
|
return shares.map((share) => {
|
||||||
return {
|
return {
|
||||||
...share,
|
...share,
|
||||||
size: share.files.reduce((acc, file) => acc + parseInt(file.size), 0),
|
|
||||||
recipients: share.recipients.map((recipients) => recipients.email),
|
recipients: share.recipients.map((recipients) => recipients.email),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
142
frontend/src/components/admin/shares/ManageShareTable.tsx
Normal file
142
frontend/src/components/admin/shares/ManageShareTable.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Group,
|
||||||
|
MediaQuery,
|
||||||
|
Skeleton,
|
||||||
|
Table,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useClipboard } from "@mantine/hooks";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import moment from "moment";
|
||||||
|
import { TbLink, TbTrash } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import useConfig from "../../../hooks/config.hook";
|
||||||
|
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||||
|
import { MyShare } from "../../../types/share.type";
|
||||||
|
import { byteToHumanSizeString } from "../../../utils/fileSize.util";
|
||||||
|
import toast from "../../../utils/toast.util";
|
||||||
|
import showShareLinkModal from "../../account/showShareLinkModal";
|
||||||
|
|
||||||
|
const ManageShareTable = ({
|
||||||
|
shares,
|
||||||
|
deleteShare,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
shares: MyShare[];
|
||||||
|
deleteShare: (share: MyShare) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}) => {
|
||||||
|
const modals = useModals();
|
||||||
|
const clipboard = useClipboard();
|
||||||
|
const config = useConfig();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "block", overflowX: "auto" }}>
|
||||||
|
<Table verticalSpacing="sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="account.shares.table.id" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="account.shares.table.name" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="admin.shares.table.username" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="account.shares.table.visitors" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="account.shares.table.size" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="account.shares.table.expiresAt" />
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading
|
||||||
|
? skeletonRows
|
||||||
|
: shares.map((share) => (
|
||||||
|
<tr key={share.id}>
|
||||||
|
<td>{share.id}</td>
|
||||||
|
<td>{share.name}</td>
|
||||||
|
<td>{share.creator.username}</td>
|
||||||
|
<td>{share.views}</td>
|
||||||
|
<td>{byteToHumanSizeString(share.size)}</td>
|
||||||
|
<td>
|
||||||
|
{moment(share.expiration).unix() === 0
|
||||||
|
? "Never"
|
||||||
|
: moment(share.expiration).format("LLL")}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Group position="right">
|
||||||
|
<ActionIcon
|
||||||
|
color="victoria"
|
||||||
|
variant="light"
|
||||||
|
size={25}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.isSecureContext) {
|
||||||
|
clipboard.copy(
|
||||||
|
`${config.get("general.appUrl")}/s/${share.id}`,
|
||||||
|
);
|
||||||
|
toast.success(t("common.notify.copied"));
|
||||||
|
} else {
|
||||||
|
showShareLinkModal(
|
||||||
|
modals,
|
||||||
|
share.id,
|
||||||
|
config.get("general.appUrl"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TbLink />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deleteShare(share)}
|
||||||
|
>
|
||||||
|
<TbTrash />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const skeletonRows = [...Array(10)].map((v, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
</MediaQuery>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
export default ManageShareTable;
|
@ -224,6 +224,7 @@ export default {
|
|||||||
// /admin
|
// /admin
|
||||||
"admin.title": "Administration",
|
"admin.title": "Administration",
|
||||||
"admin.button.users": "User management",
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.shares": "Share management",
|
||||||
"admin.button.config": "Configuration",
|
"admin.button.config": "Configuration",
|
||||||
"admin.version": "Version",
|
"admin.version": "Version",
|
||||||
// END /admin
|
// END /admin
|
||||||
@ -260,6 +261,19 @@ export default {
|
|||||||
|
|
||||||
// END /admin/users
|
// END /admin/users
|
||||||
|
|
||||||
|
// /admin/shares
|
||||||
|
"admin.shares.title": "Share management",
|
||||||
|
"admin.shares.table.id": "Share ID",
|
||||||
|
"admin.shares.table.username": "Creator",
|
||||||
|
"admin.shares.table.visitors": "Visitors",
|
||||||
|
"admin.shares.table.expires": "Expires At",
|
||||||
|
|
||||||
|
"admin.shares.edit.delete.title": "Delete share {id}",
|
||||||
|
"admin.shares.edit.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /admin/shares
|
||||||
|
|
||||||
// /upload
|
// /upload
|
||||||
"upload.title": "Upload",
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbRefresh, TbSettings, TbUsers } from "react-icons/tb";
|
import { TbLink, TbRefresh, TbSettings, TbUsers } from "react-icons/tb";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
import useTranslate from "../../hooks/useTranslate.hook";
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
@ -41,6 +41,11 @@ const Admin = () => {
|
|||||||
icon: TbUsers,
|
icon: TbUsers,
|
||||||
route: "/admin/users",
|
route: "/admin/users",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("admin.button.shares"),
|
||||||
|
icon: TbLink,
|
||||||
|
route: "/admin/shares",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("admin.button.config"),
|
title: t("admin.button.config"),
|
||||||
icon: TbSettings,
|
icon: TbSettings,
|
||||||
|
74
frontend/src/pages/admin/shares.tsx
Normal file
74
frontend/src/pages/admin/shares.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { Group, Space, Text, Title } from "@mantine/core";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import Meta from "../../components/Meta";
|
||||||
|
import ManageShareTable from "../../components/admin/shares/ManageShareTable";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
|
import shareService from "../../services/share.service";
|
||||||
|
import { MyShare } from "../../types/share.type";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
const Shares = () => {
|
||||||
|
const [shares, setShares] = useState<MyShare[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const modals = useModals();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
|
const getShares = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
shareService.list().then((shares) => {
|
||||||
|
setShares(shares);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteShare = (share: MyShare) => {
|
||||||
|
modals.openConfirmModal({
|
||||||
|
title: t("admin.shares.edit.delete.title", {
|
||||||
|
id: share.id,
|
||||||
|
}),
|
||||||
|
children: (
|
||||||
|
<Text size="sm">
|
||||||
|
<FormattedMessage id="admin.shares.edit.delete.description" />
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
labels: {
|
||||||
|
confirm: t("common.button.delete"),
|
||||||
|
cancel: t("common.button.cancel"),
|
||||||
|
},
|
||||||
|
confirmProps: { color: "red" },
|
||||||
|
onConfirm: async () => {
|
||||||
|
shareService
|
||||||
|
.remove(share.id)
|
||||||
|
.then(() => setShares(shares.filter((v) => v.id != share.id)))
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getShares();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta title={t("admin.shares.title")} />
|
||||||
|
<Group position="apart" align="baseline" mb={20}>
|
||||||
|
<Title mb={30} order={3}>
|
||||||
|
<FormattedMessage id="admin.shares.title" />
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ManageShareTable
|
||||||
|
shares={shares}
|
||||||
|
deleteShare={deleteShare}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
<Space h="xl" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Shares;
|
@ -11,6 +11,10 @@ import {
|
|||||||
} from "../types/share.type";
|
} from "../types/share.type";
|
||||||
import api from "./api.service";
|
import api from "./api.service";
|
||||||
|
|
||||||
|
const list = async (): Promise<MyShare[]> => {
|
||||||
|
return (await api.get(`shares/all`)).data;
|
||||||
|
};
|
||||||
|
|
||||||
const create = async (share: CreateShare) => {
|
const create = async (share: CreateShare) => {
|
||||||
return (await api.post("shares", share)).data;
|
return (await api.post("shares", share)).data;
|
||||||
};
|
};
|
||||||
@ -131,6 +135,7 @@ const removeReverseShare = async (id: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
list,
|
||||||
create,
|
create,
|
||||||
completeShare,
|
completeShare,
|
||||||
revertComplete,
|
revertComplete,
|
||||||
|
Loading…
Reference in New Issue
Block a user