Compare commits

...

11 Commits

Author SHA1 Message Date
Elias Schneider
bfb47ba6e8 release: 0.3.6 2022-12-13 18:45:52 +01:00
Elias Schneider
c1d87a1c29 test: improve tests for new feature 2022-12-13 18:44:17 +01:00
Elias Schneider
4c7e161217 chore: create prisma migration 2022-12-13 18:39:13 +01:00
Elias Schneider
844c47e129 fix: rerange accordion items 2022-12-13 09:57:48 +01:00
Elias Schneider
9b0c08d0cd fix: remove dot in email link 2022-12-13 09:06:18 +01:00
Elias Schneider
37fda220e9 Merge branch 'main' of https://github.com/stonith404/pingvin-share 2022-12-12 22:38:40 +01:00
Elias Schneider
3b7f5ddc52 Create close_inactive_issues.yml 2022-12-12 14:34:36 +01:00
Elias Schneider
8728fa5207 feat: add description field to share 2022-12-12 11:54:13 +01:00
Elias Schneider
c265129dcc Create SECURITY.md 2022-12-12 11:11:28 +01:00
Elias Schneider
78dd4a7e2a chore: add issue templates 2022-12-12 11:00:10 +01:00
Elias Schneider
3cad4dd487 docs: add synology nas installation by Marius 2022-12-11 12:38:58 +01:00
21 changed files with 295 additions and 81 deletions

45
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: "🐛 Bug Report"
description: "Submit a bug report to help us improve"
title: "🐛 Bug Report: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out our bug report form 🙏
- type: textarea
id: steps-to-reproduce
validations:
required: true
attributes:
label: "👟 Reproduction steps"
description: "How do you trigger this bug? Please walk us through it step by step."
placeholder: "When I ..."
- type: textarea
id: expected-behavior
validations:
required: true
attributes:
label: "👍 Expected behavior"
description: "What did you think would happen?"
placeholder: "It should ..."
- type: textarea
id: actual-behavior
validations:
required: true
attributes:
label: "👎 Actual Behavior"
description: "What did actually happen? Add screenshots, if applicable."
placeholder: "It actually ..."
- type: input
id: operating-system
attributes:
label: "🌐 Browser"
description: "Which browser do you use?"
placeholder: "Firefox"
validations:
required: true
- type: markdown
attributes:
value: |
Before submitting, please check if the issues hasn't been raised before.

29
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: 🚀 Feature
description: "Submit a proposal for a new feature"
title: "🚀 Feature: "
labels: [feature]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out our feature request form 🙏
- type: textarea
id: feature-description
validations:
required: true
attributes:
label: "🔖 Feature description"
description: "A clear and concise description of what the feature is."
placeholder: "You should add ..."
- type: textarea
id: pitch
validations:
required: true
attributes:
label: "🎤 Pitch"
description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable."
placeholder: "In my use-case, ..."
- type: markdown
attributes:
value: |
Before submitting, please check if the issues hasn't been raised before.

View File

@@ -0,0 +1,22 @@
name: Close inactive issues
on:
schedule:
- cron: "00 00 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v4
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,3 +1,16 @@
### [0.3.6](https://github.com/stonith404/pingvin-share/compare/v0.3.5...v0.3.6) (2022-12-13)
### Features
* add description field to share ([8728fa5](https://github.com/stonith404/pingvin-share/commit/8728fa5207524e9aee26d68eafe1b6fff367d749))
### Bug Fixes
* remove dot in email link ([9b0c08d](https://github.com/stonith404/pingvin-share/commit/9b0c08d0cdeeeef217ccba57f593fea9d8858371))
* rerange accordion items ([844c47e](https://github.com/stonith404/pingvin-share/commit/844c47e1290fb0f7dedb41a18be59ed5ab83dabc))
### [0.3.5](https://github.com/stonith404/pingvin-share/compare/v0.3.4...v0.3.5) (2022-12-11)

View File

@@ -23,11 +23,17 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran
> Pleas note that Pingvin Share is in early stage and could include some bugs
### Recommended installation
1. Download the `docker-compose.yml` file
2. Run `docker-compose up -d`
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧!
### Additional resources
- [Synology NAS installation](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
### Upgrade to a new version
Run `docker compose pull && docker compose up -d` to update your docker container

7
SECURITY.md Normal file
View File

@@ -0,0 +1,7 @@
# Security Policy
## Supported Versions
As Pingvin Share is in beta, older versions don't get security updates. Please consider to update Pingvin Share regularly. Updates can be automated with e.g [Watchtower](https://github.com/containrrr/watchtower).
## Reporting a Vulnerability
Thank you for taking the time to report a vulnerability. Please DO NOT create an issue on GitHub because the vulnerability could get exploited. Instead please write an email to [elias@eliasschneider.com](mailto:elias@eliasschneider.com).

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Share" ADD COLUMN "description" TEXT;

View File

@@ -39,6 +39,7 @@ model Share {
isZipReady Boolean @default(false)
views Int @default(0)
expiration DateTime
description String?
creatorId String?
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)

View File

@@ -28,7 +28,7 @@ export class EmailService {
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
to: recipientEmail,
subject: "Files shared with you",
text: `Hey!\n${creator.username} shared some files with you. View or dowload the files with this link: ${shareUrl}.\nShared securely with Pingvin Share 🐧`,
text: `Hey!\n${creator.username} shared some files with you. View or dowload the files with this link: ${shareUrl}\nShared securely with Pingvin Share 🐧`,
});
}
}

View File

@@ -1,9 +1,11 @@
import { Type } from "class-transformer";
import {
IsEmail,
IsOptional,
IsString,
Length,
Matches,
MaxLength,
ValidateNested,
} from "class-validator";
import { ShareSecurityDTO } from "./shareSecurity.dto";
@@ -19,6 +21,10 @@ export class CreateShareDTO {
@IsString()
expiration: string;
@MaxLength(512)
@IsOptional()
description: string;
@IsEmail({}, { each: true })
recipients: string[];

View File

@@ -17,6 +17,9 @@ export class ShareDTO {
@Type(() => PublicUserDTO)
creator: PublicUserDTO;
@Expose()
description: string;
from(partial: Partial<ShareDTO>) {
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
}

View File

@@ -1,4 +1,4 @@
import { PickType } from "@nestjs/mapped-types";
import { UserDTO } from "./user.dto";
export class PublicUserDTO extends PickType(UserDTO, ["email"] as const) {}
export class PublicUserDTO extends PickType(UserDTO, ["username"] as const) {}

View File

@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "243b0832-3a6a-4389-bb71-4d988c0a86d9",
"_postman_id": "84a95987-2997-429a-aba6-d38289b0b76a",
"name": "Pingvin Share Testing",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "17822132"
@@ -431,7 +431,7 @@
" const responseBody = pm.response.json();",
" pm.expect(responseBody).to.have.property(\"id\")",
" pm.expect(responseBody).to.have.property(\"expiration\")",
" pm.expect(Object.keys(responseBody).length).be.equal(2)",
" pm.expect(Object.keys(responseBody).length).be.equal(3)",
"});",
""
],
@@ -517,6 +517,60 @@
},
"response": []
},
{
"name": "Upload file 2",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", () => {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Response body correct\", () => {",
" const responseBody = pm.response.json();",
" pm.expect(responseBody).to.have.property(\"id\")",
" pm.expect(Object.keys(responseBody).length).be.equal(1)",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "formdata",
"formdata": [
{
"key": "file",
"type": "file",
"src": "./test/system/test-file.txt"
}
]
},
"url": {
"raw": "{{API_URL}}/shares/:shareId/files",
"host": [
"{{API_URL}}"
],
"path": [
"shares",
":shareId",
"files"
],
"variable": [
{
"key": "shareId",
"value": "test-share"
}
]
}
},
"response": []
},
{
"name": "Complete share",
"event": [
@@ -532,7 +586,7 @@
" const responseBody = pm.response.json();",
" pm.expect(responseBody).to.have.property(\"id\")",
" pm.expect(responseBody).to.have.property(\"expiration\")",
" pm.expect(Object.keys(responseBody).length).be.equal(2)",
" pm.expect(Object.keys(responseBody).length).be.equal(3)",
"});",
""
],
@@ -942,9 +996,9 @@
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Response contains 1 file\", () => {",
"pm.test(\"Response contains 2 files\", () => {",
" const responseBody = pm.response.json();",
" pm.expect(responseBody.files.length).be.equal(1)",
" pm.expect(responseBody.files.length).be.equal(2)",
"});",
"",
"",

View File

@@ -9,35 +9,10 @@ const FileList = ({
shareId,
isLoading,
}: {
files: any[];
files?: any[];
shareId: string;
isLoading: boolean;
}) => {
const rows = files.map((file) => (
<tr key={file.name}>
<td>{file.name}</td>
<td>{byteStringToHumanSizeString(file.size)}</td>
<td>
{file.uploadingState ? (
file.uploadingState != "finished" ? (
<Loader size={22} />
) : (
<TbCircleCheck color="green" size={22} />
)
) : (
<ActionIcon
size={25}
onClick={async () => {
await shareService.downloadFile(shareId, file.id);
}}
>
<TbDownload />
</ActionIcon>
)}
</td>
</tr>
));
return (
<Table>
<thead>
@@ -47,7 +22,34 @@ const FileList = ({
<th></th>
</tr>
</thead>
<tbody>{isLoading ? skeletonRows : rows}</tbody>
<tbody>
{isLoading
? skeletonRows
: files!.map((file) => (
<tr key={file.name}>
<td>{file.name}</td>
<td>{byteStringToHumanSizeString(file.size)}</td>
<td>
{file.uploadingState ? (
file.uploadingState != "finished" ? (
<Loader size={22} />
) : (
<TbCircleCheck color="green" size={22} />
)
) : (
<ActionIcon
size={25}
onClick={async () => {
await shareService.downloadFile(shareId, file.id);
}}
>
<TbDownload />
</ActionIcon>
)}
</td>
</tr>
))}
</tbody>
</Table>
);
};

View File

@@ -12,6 +12,7 @@ import {
Select,
Stack,
Text,
Textarea,
TextInput,
Title,
} from "@mantine/core";
@@ -22,7 +23,7 @@ import { useState } from "react";
import { TbAlertCircle } from "react-icons/tb";
import * as yup from "yup";
import shareService from "../../../services/share.service";
import { ShareSecurity } from "../../../types/share.type";
import { CreateShare } from "../../../types/share.type";
import ExpirationPreview from "../ExpirationPreview";
const showCreateUploadModal = (
@@ -32,12 +33,7 @@ const showCreateUploadModal = (
allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean;
},
uploadCallback: (
id: string,
expiration: string,
recipients: string[],
security: ShareSecurity
) => void
uploadCallback: (createShare: CreateShare) => void
) => {
return modals.openModal({
title: <Title order={4}>Share</Title>,
@@ -54,12 +50,7 @@ const CreateUploadModalBody = ({
uploadCallback,
options,
}: {
uploadCallback: (
id: string,
expiration: string,
recipients: string[],
security: ShareSecurity
) => void;
uploadCallback: (createShare: CreateShare) => void;
options: {
isUserSignedIn: boolean;
allowUnauthenticatedShares: boolean;
@@ -88,6 +79,7 @@ const CreateUploadModalBody = ({
recipients: [] as string[],
password: undefined,
maxViews: undefined,
description: undefined,
expiration_num: 1,
expiration_unit: "-days",
never_expires: false,
@@ -116,9 +108,15 @@ const CreateUploadModalBody = ({
const expiration = form.values.never_expires
? "never"
: form.values.expiration_num + form.values.expiration_unit;
uploadCallback(values.link, expiration, values.recipients, {
password: values.password,
maxViews: values.maxViews,
uploadCallback({
id: values.link,
expiration: expiration,
recipients: values.recipients,
description: values.description,
security: {
password: values.password,
maxViews: values.maxViews,
},
});
modals.closeAll();
}
@@ -228,6 +226,18 @@ const CreateUploadModalBody = ({
{ExpirationPreview({ form })}
</Text>
<Accordion>
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
<Accordion.Control>Description</Accordion.Control>
<Accordion.Panel>
<Stack align="stretch">
<Textarea
variant="filled"
placeholder="Note for the recepients"
{...form.getInputProps("description")}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
{options.enableEmailRecepients && (
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
<Accordion.Control>Email recipients</Accordion.Control>
@@ -258,6 +268,7 @@ const CreateUploadModalBody = ({
</Accordion.Panel>
</Accordion.Item>
)}
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
<Accordion.Control>Security options</Accordion.Control>
<Accordion.Panel>

View File

@@ -73,7 +73,7 @@ function App({ Component, pageProps }: AppProps) {
<LoadingOverlay visible overlayOpacity={1} />
) : (
<ConfigContext.Provider value={configVariables}>
<UserContext.Provider value={user}>
<UserContext.Provider value={user} >
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
<Header />
<Container>

View File

@@ -1,4 +1,4 @@
import { Group } from "@mantine/core";
import { Box, Group, Text, Title } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { GetServerSidePropsContext } from "next";
import { useEffect, useState } from "react";
@@ -8,6 +8,7 @@ import FileList from "../../components/share/FileList";
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal";
import showErrorModal from "../../components/share/showErrorModal";
import shareService from "../../services/share.service";
import { Share as ShareType } from "../../types/share.type";
export function getServerSideProps(context: GetServerSidePropsContext) {
return {
@@ -17,7 +18,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
const Share = ({ shareId }: { shareId: string }) => {
const modals = useModals();
const [files, setFiles] = useState<any[]>([]);
const [share, setShare] = useState<ShareType>();
const getShareToken = async (password?: string) => {
await shareService
@@ -41,7 +42,7 @@ const Share = ({ shareId }: { shareId: string }) => {
shareService
.get(shareId)
.then((share) => {
setFiles(share.files);
setShare(share);
})
.catch((e) => {
const { error } = e.response.data;
@@ -77,12 +78,16 @@ const Share = ({ shareId }: { shareId: string }) => {
title={`Share ${shareId}`}
description="Look what I've shared with you."
/>
{files.length > 1 && (
<Group position="right" mb="lg">
<DownloadAllButton shareId={shareId} />
</Group>
)}
<FileList files={files} shareId={shareId} isLoading={files.length == 0} />
<Group position="apart" mb="lg">
<Box style={{ maxWidth: "70%" }}>
<Title order={3}>{share?.id}</Title>
<Text size="sm">{share?.description}</Text>
</Box>
{share?.files.length > 1 && <DownloadAllButton shareId={shareId} />}
</Group>
<FileList files={share?.files} shareId={shareId} isLoading={!share} />
</>
);
};

View File

@@ -13,10 +13,10 @@ import useConfig from "../hooks/config.hook";
import useUser from "../hooks/user.hook";
import shareService from "../services/share.service";
import { FileUpload } from "../types/File.type";
import { Share, ShareSecurity } from "../types/share.type";
import { CreateShare, Share } from "../types/share.type";
import toast from "../utils/toast.util";
let share: Share;
let createdShare: Share;
const promiseLimit = pLimit(3);
const Upload = () => {
@@ -28,12 +28,7 @@ const Upload = () => {
const [files, setFiles] = useState<FileUpload[]>([]);
const [isUploading, setisUploading] = useState(false);
const uploadFiles = async (
id: string,
expiration: string,
recipients: string[],
security: ShareSecurity
) => {
const uploadFiles = async (share: CreateShare) => {
setisUploading(true);
try {
setFiles((files) =>
@@ -42,7 +37,8 @@ const Upload = () => {
return file;
})
);
share = await shareService.create(id, expiration, recipients, security);
createdShare = await shareService.create(share);
const uploadPromises = files.map((file, i) => {
// Callback to indicate current upload progress
const progressCallBack = (progress: number) => {
@@ -91,9 +87,9 @@ const Upload = () => {
toast.error(`${fileErrorCount} file(s) failed to upload. Try again.`);
} else {
shareService
.completeShare(share.id)
.completeShare(createdShare.id)
.then(() => {
showCompletedUploadModal(modals, share);
showCompletedUploadModal(modals, createdShare);
setFiles([]);
})
.catch(() =>

View File

@@ -1,19 +1,22 @@
import {
CreateShare,
MyShare,
Share,
ShareMetaData,
ShareSecurity,
} from "../types/share.type";
import api from "./api.service";
const create = async (
id: string,
expiration: string,
recipients: string[],
security?: ShareSecurity
) => {
return (await api.post("shares", { id, expiration, recipients, security }))
.data;
const create = async (share: CreateShare) => {
const { id, expiration, recipients, security, description } = share;
return (
await api.post("shares", {
id,
expiration,
recipients,
security,
description,
})
).data;
};
const completeShare = async (id: string) => {

View File

@@ -4,9 +4,18 @@ export type Share = {
id: string;
files: any;
creator: User;
description?: string;
expiration: Date;
};
export type CreateShare = {
id: string;
description?: string;
recipients: string[];
expiration: string;
security: ShareSecurity;
};
export type ShareMetaData = {
id: string;
isZipReady: boolean;

View File

@@ -1,6 +1,6 @@
{
"name": "pingvin-share",
"version": "0.3.5",
"version": "0.3.6",
"scripts": {
"format": "cd frontend && npm run format && cd ../backend && npm run format",
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",