Compare commits

..

38 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
Elias Schneider
d1d3462056 release: 0.3.5 2022-12-11 12:23:46 +01:00
Elias Schneider
5b01108777 fix: zip doesn't contain file extension 2022-12-11 12:22:01 +01:00
Elias Schneider
3d1d4d0fc7 fix: only create zip if more than one file is in the share 2022-12-11 12:19:42 +01:00
Elias Schneider
7c0d62a429 Update FUNDING.yml 2022-12-10 23:26:57 +01:00
Elias Schneider
d010a8a2d3 feat: upload 3 files at same time 2022-12-10 23:16:10 +01:00
Elias Schneider
9798e26872 fix: setup wizard table doesn't take full width 2022-12-10 18:45:53 +01:00
Elias Schneider
0c10dc674f Merge pull request #37 from Neyxo/improvement-30-docker-image
Improved docker image (size & speed)
2022-12-10 18:33:45 +01:00
Elias Schneider
084e911eed fix: remove unnecessary port expose 2022-12-10 18:32:14 +01:00
Elias Schneider
797f8938ca fix: use node slim to fix arm builds 2022-12-10 18:31:39 +01:00
Elias Schneider
05cbb7b27e fix: jobs never get executed 2022-12-10 17:16:49 +01:00
Elias Schneider
905bab9c86 release: 0.3.4 2022-12-10 15:46:53 +01:00
Jean-Michel Carrel
8e38c5fed7 Improved docker image (size & speed) 2022-12-10 15:34:01 +01:00
Elias Schneider
7e877ce9f4 fix: show alternative to copy button if site is not using https 2022-12-10 13:16:23 +01:00
Elias Schneider
b1bfb09dfd fix: tables on mobile 2022-12-09 14:37:09 +01:00
Elias Schneider
c8a4521677 fix: sign up page available when registration is disabled 2022-12-09 12:05:43 +01:00
Elias Schneider
3c74cc14df release: 0.3.3 2022-12-08 23:22:59 +01:00
Elias Schneider
a165f8ec4d refactor: remove console log 2022-12-08 23:22:15 +01:00
Elias Schneider
d6a88f2a22 performance: reduce docker image size 2022-12-08 23:21:31 +01:00
Elias Schneider
b8172efd59 fix: allow empty strings in config variable 2022-12-08 23:21:16 +01:00
Elias Schneider
cbe37c6798 fix: obscured text length 2022-12-08 23:12:25 +01:00
Elias Schneider
a545c44426 fix: improve admin dashboard color and layout 2022-12-08 22:43:14 +01:00
Elias Schneider
08a2f60f72 chore: add migration for v0.3.3 2022-12-08 21:58:58 +01:00
Elias Schneider
907e56af0f fix: space character in email 2022-12-08 20:04:56 +01:00
Elias Schneider
888a0c5faf feat: add support for different email and user 2022-12-08 20:00:04 +01:00
Elias Schneider
bfb0d151ea fix: obscure critical config variables 2022-12-08 19:14:06 +01:00
Elias Schneider
1f63f22591 docs: add review to README 2022-12-08 17:30:12 +01:00
Elias Schneider
a2d5e0f72c test: fix system tests not await backend start 2022-12-07 13:44:02 +01:00
50 changed files with 842 additions and 252 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
# These are supported funding model platforms
github: stonith404

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,58 @@
### [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)
### Features
* upload 3 files at same time ([d010a8a](https://github.com/stonith404/pingvin-share/commit/d010a8a2d366708b1bb5088e9c1e9f9378d3e023))
### Bug Fixes
* jobs never get executed ([05cbb7b](https://github.com/stonith404/pingvin-share/commit/05cbb7b27ef98a3a80dd9edc318f1dcc9a8bd442))
* only create zip if more than one file is in the share ([3d1d4d0](https://github.com/stonith404/pingvin-share/commit/3d1d4d0fc7c0351724387c3721280c334ae94d98))
* remove unnecessary port expose ([084e911](https://github.com/stonith404/pingvin-share/commit/084e911eed95eb22fea0bf185803ba32c3eda1a9))
* setup wizard table doesn't take full width ([9798e26](https://github.com/stonith404/pingvin-share/commit/9798e26872064edc1049138cf73479b1354a43ed))
* use node slim to fix arm builds ([797f893](https://github.com/stonith404/pingvin-share/commit/797f8938cac9cc3bb788f632d97eba5c49fe98a5))
* zip doesn't contain file extension ([5b01108](https://github.com/stonith404/pingvin-share/commit/5b0110877745f1fcde4952737a93c07ef4a2a92d))
### [0.3.4](https://github.com/stonith404/pingvin-share/compare/v0.3.3...v0.3.4) (2022-12-10)
### Bug Fixes
* show alternative to copy button if site is not using https ([7e877ce](https://github.com/stonith404/pingvin-share/commit/7e877ce9f4b82d61c9b238e17def9f4c29e7aeb8))
* sign up page available when registration is disabled ([c8a4521](https://github.com/stonith404/pingvin-share/commit/c8a4521677280d6aba89d293a1fe0c38adf9f92c))
* tables on mobile ([b1bfb09](https://github.com/stonith404/pingvin-share/commit/b1bfb09dfd5c90cc18847470a9ce1ce8397c1476))
### [0.3.3](https://github.com/stonith404/pingvin-share/compare/v0.3.2...v0.3.3) (2022-12-08)
### Features
* add support for different email and user ([888a0c5](https://github.com/stonith404/pingvin-share/commit/888a0c5fafc51b6872ed71e37d4b40c9bf6a07f1))
### Bug Fixes
* allow empty strings in config variable ([b8172ef](https://github.com/stonith404/pingvin-share/commit/b8172efd59fb3271ab9b818b13a7003342b2cebd))
* improve admin dashboard color and layout ([a545c44](https://github.com/stonith404/pingvin-share/commit/a545c444261c90105dcb165ebcf4b26634e729ca))
* obscure critical config variables ([bfb0d15](https://github.com/stonith404/pingvin-share/commit/bfb0d151ea2ba125e536a16b1873e143a67e9f64))
* obscured text length ([cbe37c6](https://github.com/stonith404/pingvin-share/commit/cbe37c679853ecef1522ed213e4cac5defd5b45a))
* space character in email ([907e56a](https://github.com/stonith404/pingvin-share/commit/907e56af0faccdbc8d7f5ab3418a4ad71ff849f5))
### [0.3.2](https://github.com/stonith404/pingvin-share/compare/v0.3.1...v0.3.2) (2022-12-07) ### [0.3.2](https://github.com/stonith404/pingvin-share/compare/v0.3.1...v0.3.2) (2022-12-07)

View File

@@ -1,32 +1,51 @@
FROM node:18-alpine AS frontend-builder # Using node slim because prisma ORM needs libc for ARM builds
# Stage 1: on frontend dependency change
FROM node:18-slim AS frontend-dependencies
WORKDIR /opt/app WORKDIR /opt/app
COPY frontend/package.json frontend/package-lock.json ./ COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci RUN npm ci
# Stage 2: on frontend change
FROM node:18-slim AS frontend-builder
WORKDIR /opt/app
COPY ./frontend . COPY ./frontend .
COPY --from=frontend-dependencies /opt/app/node_modules ./node_modules
RUN npm run build RUN npm run build
FROM node:18 AS backend-builder # Stage 3: on backend dependency change
FROM node:18-slim AS backend-dependencies
WORKDIR /opt/app WORKDIR /opt/app
COPY backend/package.json backend/package-lock.json ./ COPY backend/package.json backend/package-lock.json ./
RUN npm ci RUN npm ci
COPY ./backend .
RUN npx prisma generate
RUN npm run build
FROM node:18 AS runner # Stage 4:on backend change
WORKDIR /opt/app/frontend FROM node:18-slim AS backend-builder
RUN apt-get update && apt-get install -y openssl
WORKDIR /opt/app
COPY ./backend .
COPY --from=backend-dependencies /opt/app/node_modules ./node_modules
RUN npx prisma generate
RUN npm run build && npm prune --production
# Stage 5: Final image
FROM node:18-slim AS runner
ENV NODE_ENV=production ENV NODE_ENV=production
COPY --from=frontend-builder /opt/app/next.config.js . RUN apt-get update && apt-get install -y openssl
WORKDIR /opt/app/frontend
COPY --from=frontend-builder /opt/app/public ./public COPY --from=frontend-builder /opt/app/public ./public
COPY --from=frontend-builder /opt/app/.next ./.next # Automatically leverage output traces to reduce image size
COPY --from=frontend-builder /opt/app/node_modules ./node_modules # https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=frontend-builder /opt/app/.next/standalone ./
COPY --from=frontend-builder /opt/app/.next/static ./.next/static
WORKDIR /opt/app/backend WORKDIR /opt/app/backend
COPY --from=backend-builder /opt/app/node_modules ./node_modules COPY --from=backend-builder /opt/app/node_modules ./node_modules
COPY --from=backend-builder /opt/app/dist ./dist COPY --from=backend-builder /opt/app/dist ./dist
COPY --from=backend-builder /opt/app/prisma ./prisma COPY --from=backend-builder /opt/app/prisma ./prisma
COPY --from=backend-builder /opt/app/package.json ./ COPY --from=backend-builder /opt/app/package.json ./
WORKDIR /opt/app
WORKDIR /opt/app
EXPOSE 3000 EXPOSE 3000
CMD cd frontend && node_modules/.bin/next start & cd backend && npm run prod CMD node frontend/server.js & cd backend && npm run prod

View File

@@ -2,12 +2,6 @@
Pingvin Share is self-hosted file sharing platform and an alternative for WeTransfer. Pingvin Share is self-hosted file sharing platform and an alternative for WeTransfer.
## 🎪 Showcase
Demo: https://pingvin-share.dev.eliasschneider.com
<img src="https://user-images.githubusercontent.com/58886915/167101708-b85032ad-f5b1-480a-b8d7-ec0096ea2a43.png" width="700"/>
## ✨ Features ## ✨ Features
- Spin up your instance within 2 minutes - Spin up your instance within 2 minutes
@@ -18,15 +12,28 @@ Demo: https://pingvin-share.dev.eliasschneider.com
- Email recepients - Email recepients
- Light & dark mode - Light & dark mode
## 🐧 Get to know Pingvin Share
- [Demo](https://pingvin-share.dev.eliasschneider.com)
- [Review by DB Tech](https://www.youtube.com/watch?v=rWwNeZCOPJA)
<img src="https://user-images.githubusercontent.com/58886915/167101708-b85032ad-f5b1-480a-b8d7-ec0096ea2a43.png" width="700"/>
## ⌨️ Setup ## ⌨️ Setup
> Pleas note that Pingvin Share is in early stage and could include some bugs > Pleas note that Pingvin Share is in early stage and could include some bugs
### Recommended installation
1. Download the `docker-compose.yml` file 1. Download the `docker-compose.yml` file
2. Run `docker-compose up -d` 2. Run `docker-compose up -d`
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧! 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 ### Upgrade to a new version
Run `docker compose pull && docker compose up -d` to update your docker container 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

@@ -60,7 +60,8 @@
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths": "4.1.1", "tsconfig-paths": "4.1.1",
"typescript": "^4.9.3" "typescript": "^4.9.3",
"wait-on": "^6.0.1"
} }
}, },
"node_modules/@angular-devkit/core": { "node_modules/@angular-devkit/core": {
@@ -390,6 +391,21 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true "dev": true
}, },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"dev": true
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.7", "version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
@@ -1027,6 +1043,27 @@
"integrity": "sha512-Bd4LZ+WAnUHOq31e9X/ihi5zPlr4SzTRwUZZYxvWOxlerIZ7HJlVa9zXpuKTKLpI9O1l8Ec4OYCKsivWCs5a3Q==", "integrity": "sha512-Bd4LZ+WAnUHOq31e9X/ihi5zPlr4SzTRwUZZYxvWOxlerIZ7HJlVa9zXpuKTKLpI9O1l8Ec4OYCKsivWCs5a3Q==",
"dev": true "dev": true
}, },
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
"dev": true
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"dev": true
},
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -1983,6 +2020,15 @@
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"dev": true "dev": true
}, },
"node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.7"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3518,6 +3564,26 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true "dev": true
}, },
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/forever-agent": { "node_modules/forever-agent": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -4264,6 +4330,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/joi": {
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz",
"integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0",
"@hapi/topo": "^5.0.0",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.0",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/js-sdsl": { "node_modules/js-sdsl": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
@@ -7134,6 +7213,25 @@
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true "dev": true
}, },
"node_modules/wait-on": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.1.tgz",
"integrity": "sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==",
"dev": true,
"dependencies": {
"axios": "^0.25.0",
"joi": "^17.6.0",
"lodash": "^4.17.21",
"minimist": "^1.2.5",
"rxjs": "^7.5.4"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@@ -7704,6 +7802,21 @@
} }
} }
}, },
"@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"dev": true
},
"@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dev": true,
"requires": {
"@hapi/hoek": "^9.0.0"
}
},
"@humanwhocodes/config-array": { "@humanwhocodes/config-array": {
"version": "0.11.7", "version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
@@ -8138,6 +8251,27 @@
"integrity": "sha512-Bd4LZ+WAnUHOq31e9X/ihi5zPlr4SzTRwUZZYxvWOxlerIZ7HJlVa9zXpuKTKLpI9O1l8Ec4OYCKsivWCs5a3Q==", "integrity": "sha512-Bd4LZ+WAnUHOq31e9X/ihi5zPlr4SzTRwUZZYxvWOxlerIZ7HJlVa9zXpuKTKLpI9O1l8Ec4OYCKsivWCs5a3Q==",
"dev": true "dev": true
}, },
"@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
"dev": true,
"requires": {
"@hapi/hoek": "^9.0.0"
}
},
"@sideway/formula": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
"dev": true
},
"@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"dev": true
},
"@tsconfig/node10": { "@tsconfig/node10": {
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -8925,6 +9059,15 @@
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"dev": true "dev": true
}, },
"axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.7"
}
},
"balanced-match": { "balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -10090,6 +10233,12 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true "dev": true
}, },
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true
},
"forever-agent": { "forever-agent": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -10637,6 +10786,19 @@
"resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz",
"integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q=="
}, },
"joi": {
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz",
"integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==",
"dev": true,
"requires": {
"@hapi/hoek": "^9.0.0",
"@hapi/topo": "^5.0.0",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.0",
"@sideway/pinpoint": "^2.0.0"
}
},
"js-sdsl": { "js-sdsl": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
@@ -12786,6 +12948,19 @@
} }
} }
}, },
"wait-on": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.1.tgz",
"integrity": "sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==",
"dev": true,
"requires": {
"axios": "^0.25.0",
"joi": "^17.6.0",
"lodash": "^4.17.21",
"minimist": "^1.2.5",
"rxjs": "^7.5.4"
}
},
"watchpack": { "watchpack": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

View File

@@ -7,7 +7,7 @@
"prod": "prisma migrate deploy && prisma db seed && node dist/src/main", "prod": "prisma migrate deploy && prisma db seed && node dist/src/main",
"lint": "eslint 'src/**/*.ts'", "lint": "eslint 'src/**/*.ts'",
"format": "prettier --write 'src/**/*.ts'", "format": "prettier --write 'src/**/*.ts'",
"test:system": "prisma migrate reset -f && nest start & sleep 10 && newman run ./test/system/newman-system-tests.json" "test:system": "prisma migrate reset -f && nest start & wait-on http://localhost:8080/api/configs && newman run ./test/system/newman-system-tests.json"
}, },
"prisma": { "prisma": {
"seed": "ts-node prisma/seed/config.seed.ts" "seed": "ts-node prisma/seed/config.seed.ts"
@@ -22,6 +22,7 @@
"@nestjs/platform-express": "^9.2.1", "@nestjs/platform-express": "^9.2.1",
"@nestjs/schedule": "^2.1.0", "@nestjs/schedule": "^2.1.0",
"@nestjs/throttler": "^3.1.0", "@nestjs/throttler": "^3.1.0",
"@prisma/client": "^4.7.1",
"archiver": "^5.3.1", "archiver": "^5.3.1",
"argon2": "^0.30.2", "argon2": "^0.30.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
@@ -36,13 +37,13 @@
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.6.0" "rxjs": "^7.6.0",
"ts-node": "^10.9.1"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^9.1.5", "@nestjs/cli": "^9.1.5",
"@nestjs/schematics": "^9.0.3", "@nestjs/schematics": "^9.0.3",
"@nestjs/testing": "^9.2.1", "@nestjs/testing": "^9.2.1",
"@prisma/client": "^4.7.1",
"@types/archiver": "^5.3.1", "@types/archiver": "^5.3.1",
"@types/cron": "^2.0.0", "@types/cron": "^2.0.0",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
@@ -63,8 +64,8 @@
"prisma": "^4.7.1", "prisma": "^4.7.1",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tsconfig-paths": "4.1.1", "tsconfig-paths": "4.1.1",
"typescript": "^4.9.3" "typescript": "^4.9.3",
"wait-on": "^6.0.1"
} }
} }

View File

@@ -0,0 +1,17 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Config" (
"updatedAt" DATETIME NOT NULL,
"key" TEXT NOT NULL PRIMARY KEY,
"type" TEXT NOT NULL,
"value" TEXT NOT NULL,
"description" TEXT NOT NULL,
"obscured" BOOLEAN NOT NULL DEFAULT false,
"secret" BOOLEAN NOT NULL DEFAULT true,
"locked" BOOLEAN NOT NULL DEFAULT false
);
INSERT INTO "new_Config" ("description", "key", "locked", "secret", "type", "updatedAt", "value") SELECT "description", "key", "locked", "secret", "type", "updatedAt", "value" FROM "Config";
DROP TABLE "Config";
ALTER TABLE "new_Config" RENAME TO "Config";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

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) isZipReady Boolean @default(false)
views Int @default(0) views Int @default(0)
expiration DateTime expiration DateTime
description String?
creatorId String? creatorId String?
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade) creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)
@@ -84,6 +85,7 @@ model Config {
type String type String
value String value String
description String description String
obscured Boolean @default(false)
secret Boolean @default(true) secret Boolean @default(true)
locked Boolean @default(false) locked Boolean @default(false)
} }

View File

@@ -1,7 +1,7 @@
import { PrismaClient } from "@prisma/client"; import { Prisma, PrismaClient } from "@prisma/client";
import * as crypto from "crypto"; import * as crypto from "crypto";
const configVariables = [ const configVariables: Prisma.ConfigCreateInput[] = [
{ {
key: "SETUP_FINISHED", key: "SETUP_FINISHED",
description: "Whether the setup has been finished", description: "Whether the setup has been finished",
@@ -55,7 +55,7 @@ const configVariables = [
{ {
key: "ENABLE_EMAIL_RECIPIENTS", key: "ENABLE_EMAIL_RECIPIENTS",
description: description:
"Whether to send emails to recipients. Only set this to true if you entered the host, port, email and password of your SMTP server.", "Whether to send emails to recipients. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
type: "boolean", type: "boolean",
value: "false", value: "false",
secret: false, secret: false,
@@ -74,7 +74,13 @@ const configVariables = [
}, },
{ {
key: "SMTP_EMAIL", key: "SMTP_EMAIL",
description: "Email address of the SMTP server", description: "Email address which the emails get sent from",
type: "string",
value: "",
},
{
key: "SMTP_USERNAME",
description: "Username of the SMTP server",
type: "string", type: "string",
value: "", value: "",
}, },
@@ -83,6 +89,7 @@ const configVariables = [
description: "Password of the SMTP server", description: "Password of the SMTP server",
type: "string", type: "string",
value: "", value: "",
obscured: true,
}, },
]; ];
@@ -102,14 +109,34 @@ async function main() {
} }
} }
// Delete the config variable if it doesn't exist anymore
const configVariablesFromDatabase = await prisma.config.findMany(); const configVariablesFromDatabase = await prisma.config.findMany();
// Delete the config variable if it doesn't exist anymore
for (const configVariableFromDatabase of configVariablesFromDatabase) { for (const configVariableFromDatabase of configVariablesFromDatabase) {
if (!configVariables.find((v) => v.key == configVariableFromDatabase.key)) { const configVariable = configVariables.find(
(v) => v.key == configVariableFromDatabase.key
);
if (!configVariable) {
await prisma.config.delete({ await prisma.config.delete({
where: { key: configVariableFromDatabase.key }, where: { key: configVariableFromDatabase.key },
}); });
// Update the config variable if the metadata changed
} else if (
JSON.stringify({
...configVariable,
key: configVariableFromDatabase.key,
value: configVariableFromDatabase.value,
}) != JSON.stringify(configVariableFromDatabase)
) {
await prisma.config.update({
where: { key: configVariableFromDatabase.key },
data: {
...configVariable,
key: configVariableFromDatabase.key,
value: configVariableFromDatabase.value,
},
});
} }
} }
} }

View File

@@ -13,6 +13,7 @@ import { FileModule } from "./file/file.module";
import { PrismaModule } from "./prisma/prisma.module"; import { PrismaModule } from "./prisma/prisma.module";
import { ShareModule } from "./share/share.module"; import { ShareModule } from "./share/share.module";
import { UserModule } from "./user/user.module"; import { UserModule } from "./user/user.module";
import { JobsModule } from "./jobs/jobs.module";
@Module({ @Module({
imports: [ imports: [
@@ -22,6 +23,7 @@ import { UserModule } from "./user/user.module";
EmailModule, EmailModule,
PrismaModule, PrismaModule,
ConfigModule, ConfigModule,
JobsModule,
UserModule, UserModule,
MulterModule.registerAsync({ MulterModule.registerAsync({
useFactory: (config: ConfigService) => ({ useFactory: (config: ConfigService) => ({

View File

@@ -11,6 +11,9 @@ export class AdminConfigDTO extends ConfigDTO {
@Expose() @Expose()
description: string; description: string;
@Expose()
obscured: boolean;
from(partial: Partial<AdminConfigDTO>) { from(partial: Partial<AdminConfigDTO>) {
return plainToClass(AdminConfigDTO, partial, { return plainToClass(AdminConfigDTO, partial, {
excludeExtraneousValues: true, excludeExtraneousValues: true,

View File

@@ -1,7 +1,8 @@
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty, ValidateIf } from "class-validator";
class UpdateConfigDTO { class UpdateConfigDTO {
@IsNotEmpty() @IsNotEmpty()
@ValidateIf((dto) => dto.value !== "")
value: string | number | boolean; value: string | number | boolean;
} }

View File

@@ -14,7 +14,7 @@ export class EmailService {
port: parseInt(this.config.get("SMTP_PORT")), port: parseInt(this.config.get("SMTP_PORT")),
secure: parseInt(this.config.get("SMTP_PORT")) == 465, secure: parseInt(this.config.get("SMTP_PORT")) == 465,
auth: { auth: {
user: this.config.get("SMTP_EMAIL"), user: this.config.get("SMTP_USERNAME"),
pass: this.config.get("SMTP_PASSWORD"), pass: this.config.get("SMTP_PASSWORD"),
}, },
}); });
@@ -28,7 +28,7 @@ export class EmailService {
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`, from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
to: recipientEmail, to: recipientEmail,
subject: "Files shared with you", subject: "Files shared with you",
text: `Hey!\n${creator.username} shared some files with you. View or dowload the files with this link: ${shareUrl}.\n Shared 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

@@ -79,7 +79,7 @@ export class FileController {
const zip = this.fileService.getZip(shareId); const zip = this.fileService.getZip(shareId);
res.set({ res.set({
"Content-Type": "application/zip", "Content-Type": "application/zip",
"Content-Disposition": `attachment ; filename="pingvin-share-${shareId}"`, "Content-Disposition": `attachment ; filename="pingvin-share-${shareId}.zip"`,
}); });
return new StreamableFile(zip); return new StreamableFile(zip);

View File

@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { FileModule } from "src/file/file.module";
import { JobsService } from "./jobs.service";
@Module({
imports: [FileModule],
providers: [JobsService],
})
export class JobsModule {}

View File

@@ -1,5 +1,4 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
@Injectable() @Injectable()
@@ -8,7 +7,7 @@ export class PrismaService extends PrismaClient {
super({ super({
datasources: { datasources: {
db: { db: {
url: "file:../data/pingvin-share.db", url: "file:../data/pingvin-share.db?connection_limit=1",
}, },
}, },
}); });

View File

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

View File

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

View File

@@ -105,6 +105,7 @@ export class ShareService {
); );
// Asynchronously create a zip of all files // Asynchronously create a zip of all files
if (share.files.length > 1)
this.createZip(id).then(() => this.createZip(id).then(() =>
this.prisma.share.update({ where: { id }, data: { isZipReady: true } }) this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
); );

View File

@@ -1,4 +1,4 @@
import { PickType } from "@nestjs/mapped-types"; import { PickType } from "@nestjs/mapped-types";
import { UserDTO } from "./user.dto"; 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": { "info": {
"_postman_id": "243b0832-3a6a-4389-bb71-4d988c0a86d9", "_postman_id": "84a95987-2997-429a-aba6-d38289b0b76a",
"name": "Pingvin Share Testing", "name": "Pingvin Share Testing",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "17822132" "_exporter_id": "17822132"
@@ -431,7 +431,7 @@
" const responseBody = pm.response.json();", " const responseBody = pm.response.json();",
" pm.expect(responseBody).to.have.property(\"id\")", " pm.expect(responseBody).to.have.property(\"id\")",
" pm.expect(responseBody).to.have.property(\"expiration\")", " 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": [] "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", "name": "Complete share",
"event": [ "event": [
@@ -532,7 +586,7 @@
" const responseBody = pm.response.json();", " const responseBody = pm.response.json();",
" pm.expect(responseBody).to.have.property(\"id\")", " pm.expect(responseBody).to.have.property(\"id\")",
" pm.expect(responseBody).to.have.property(\"expiration\")", " 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.response.to.have.status(200);",
"});", "});",
"", "",
"pm.test(\"Response contains 1 file\", () => {", "pm.test(\"Response contains 2 files\", () => {",
" const responseBody = pm.response.json();", " const responseBody = pm.response.json();",
" pm.expect(responseBody.files.length).be.equal(1)", " pm.expect(responseBody.files.length).be.equal(2)",
"});", "});",
"", "",
"", "",

View File

@@ -1,10 +1,8 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const withPWA = require("next-pwa")({ const withPWA = require("next-pwa")({
dest: "public", dest: "public",
disable: process.env.NODE_ENV == "development" disable: process.env.NODE_ENV == "development",
}); });
module.exports = withPWA({ output: "standalone" });
module.exports = withPWA();

View File

@@ -26,6 +26,7 @@
"next-cookies": "^2.0.3", "next-cookies": "^2.0.3",
"next-http-proxy-middleware": "^1.2.5", "next-http-proxy-middleware": "^1.2.5",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"p-limit": "^4.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.7.1", "react-icons": "^4.7.1",
@@ -6161,6 +6162,35 @@
} }
}, },
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"dependencies": {
"yocto-queue": "^1.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate/node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
@@ -6175,14 +6205,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-locate": { "node_modules/p-locate/node_modules/yocto-queue": {
"version": "5.0.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true, "dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -8049,12 +8076,11 @@
} }
}, },
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
"dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=12.20"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -12482,12 +12508,11 @@
} }
}, },
"p-limit": { "p-limit": {
"version": "3.1.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"dev": true,
"requires": { "requires": {
"yocto-queue": "^0.1.0" "yocto-queue": "^1.0.0"
} }
}, },
"p-locate": { "p-locate": {
@@ -12497,6 +12522,23 @@
"dev": true, "dev": true,
"requires": { "requires": {
"p-limit": "^3.0.2" "p-limit": "^3.0.2"
},
"dependencies": {
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
}
} }
}, },
"p-map": { "p-map": {
@@ -13865,10 +13907,9 @@
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
}, },
"yocto-queue": { "yocto-queue": {
"version": "0.1.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g=="
"dev": true
}, },
"yup": { "yup": {
"version": "0.32.11", "version": "0.32.11",

View File

@@ -27,6 +27,7 @@
"next-cookies": "^2.0.3", "next-cookies": "^2.0.3",
"next-http-proxy-middleware": "^1.2.5", "next-http-proxy-middleware": "^1.2.5",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"p-limit": "^4.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.7.1", "react-icons": "^4.7.1",

View File

@@ -0,0 +1,16 @@
import { Stack, TextInput } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
const showShareLinkModal = (modals: ModalsContextProps, shareId: string) => {
const link = `${window.location.origin}/share/${shareId}`;
return modals.openModal({
title: "Share link",
children: (
<Stack align="stretch">
<TextInput variant="filled" value={link} />
</Stack>
),
});
};
export default showShareLinkModal;

View File

@@ -1,4 +1,12 @@
import { ActionIcon, Code, Group, Skeleton, Table, Text } from "@mantine/core"; import {
ActionIcon,
Box,
Code,
Group,
Skeleton,
Table,
Text,
} from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TbEdit, TbLock } from "react-icons/tb"; import { TbEdit, TbLock } from "react-icons/tb";
@@ -43,6 +51,7 @@ const AdminConfigTable = () => {
)); ));
return ( return (
<Box sx={{ display: "block", overflowX: "auto" }}>
<Table verticalSpacing="sm" horizontalSpacing="xl" withBorder> <Table verticalSpacing="sm" horizontalSpacing="xl" withBorder>
<thead> <thead>
<tr> <tr>
@@ -54,17 +63,20 @@ const AdminConfigTable = () => {
<tbody> <tbody>
{isLoading {isLoading
? skeletonRows ? skeletonRows
: configVariables.map((element) => ( : configVariables.map((configVariable) => (
<tr key={element.key}> <tr key={configVariable.key}>
<td style={{ maxWidth: "200px" }}> <td style={{ maxWidth: "200px" }}>
<Code>{element.key}</Code> {element.secret && <TbLock />}{" "} <Code>{configVariable.key}</Code>{" "}
<br /> {configVariable.secret && <TbLock />} <br />
<Text size="xs" color="dimmed"> <Text size="xs" color="dimmed">
{element.description} {configVariable.description}
</Text> </Text>
</td> </td>
<td>{element.value}</td> <td>
{configVariable.obscured
? "•".repeat(configVariable.value.length)
: configVariable.value}
</td>
<td> <td>
<Group position="right"> <Group position="right">
<ActionIcon <ActionIcon
@@ -74,7 +86,7 @@ const AdminConfigTable = () => {
onClick={() => onClick={() =>
showUpdateConfigVariableModal( showUpdateConfigVariableModal(
modals, modals,
element, configVariable,
getConfigVariables getConfigVariables
) )
} }
@@ -87,6 +99,7 @@ const AdminConfigTable = () => {
))} ))}
</tbody> </tbody>
</Table> </Table>
</Box>
); );
}; };

View File

@@ -18,8 +18,8 @@ const ManageUserTable = ({
const modals = useModals(); const modals = useModals();
return ( return (
<Box sx={{ display: "block", overflowX: "auto", whiteSpace: "nowrap" }}> <Box sx={{ display: "block", overflowX: "auto" }}>
<Table verticalSpacing="sm" highlightOnHover> <Table verticalSpacing="sm">
<thead> <thead>
<tr> <tr>
<th>Username</th> <th>Username</th>

View File

@@ -50,7 +50,6 @@ const Body = ({
<Stack> <Stack>
<form <form
onSubmit={form.onSubmit(async (values) => { onSubmit={form.onSubmit(async (values) => {
console.log(values);
userService userService
.create(values) .create(values)
.then(() => { .then(() => {
@@ -62,10 +61,7 @@ const Body = ({
> >
<Stack> <Stack>
<TextInput label="Username" {...form.getInputProps("username")} /> <TextInput label="Username" {...form.getInputProps("username")} />
<TextInput <TextInput label="Email" {...form.getInputProps("email")} />
label="Email"
{...form.getInputProps("email")}
/>
<PasswordInput <PasswordInput
label="New password" label="New password"
{...form.getInputProps("password")} {...form.getInputProps("password")}

View File

@@ -2,6 +2,7 @@ import {
Button, Button,
Code, Code,
NumberInput, NumberInput,
PasswordInput,
Select, Select,
Space, Space,
Stack, Stack,
@@ -53,9 +54,12 @@ const Body = ({
<Text> <Text>
Set <Code>{configVariable.key}</Code> to Set <Code>{configVariable.key}</Code> to
</Text> </Text>
{configVariable.type == "string" && ( {configVariable.type == "string" &&
(configVariable.obscured ? (
<PasswordInput label="Value" {...form.getInputProps("stringValue")} />
) : (
<TextInput label="Value" {...form.getInputProps("stringValue")} /> <TextInput label="Value" {...form.getInputProps("stringValue")} />
)} ))}
{configVariable.type == "number" && ( {configVariable.type == "number" && (
<NumberInput label="Value" {...form.getInputProps("numberValue")} /> <NumberInput label="Value" {...form.getInputProps("numberValue")} />
)} )}

View File

@@ -79,10 +79,7 @@ const Body = ({
label="Username" label="Username"
{...accountForm.getInputProps("username")} {...accountForm.getInputProps("username")}
/> />
<TextInput <TextInput label="Email" {...accountForm.getInputProps("email")} />
label="Email"
{...accountForm.getInputProps("email")}
/>
<Switch <Switch
mt="xs" mt="xs"
labelPosition="left" labelPosition="left"

View File

@@ -9,11 +9,23 @@ const FileList = ({
shareId, shareId,
isLoading, isLoading,
}: { }: {
files: any[]; files?: any[];
shareId: string; shareId: string;
isLoading: boolean; isLoading: boolean;
}) => { }) => {
const rows = files.map((file) => ( return (
<Table>
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th></th>
</tr>
</thead>
<tbody>
{isLoading
? skeletonRows
: files!.map((file) => (
<tr key={file.name}> <tr key={file.name}>
<td>{file.name}</td> <td>{file.name}</td>
<td>{byteStringToHumanSizeString(file.size)}</td> <td>{byteStringToHumanSizeString(file.size)}</td>
@@ -36,18 +48,8 @@ const FileList = ({
)} )}
</td> </td>
</tr> </tr>
)); ))}
</tbody>
return (
<Table>
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th></th>
</tr>
</thead>
<tbody>{isLoading ? skeletonRows : rows}</tbody>
</Table> </Table>
); );
}; };

View File

@@ -40,6 +40,7 @@ const Body = ({ share }: { share: Share }) => {
variant="filled" variant="filled"
value={link} value={link}
rightSection={ rightSection={
window.isSecureContext && (
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
clipboard.copy(link); clipboard.copy(link);
@@ -48,6 +49,7 @@ const Body = ({ share }: { share: Share }) => {
> >
<TbCopy /> <TbCopy />
</ActionIcon> </ActionIcon>
)
} }
/> />
<Text <Text

View File

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

View File

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

View File

@@ -80,10 +80,7 @@ const Account = () => {
label="Username" label="Username"
{...accountForm.getInputProps("username")} {...accountForm.getInputProps("username")}
/> />
<TextInput <TextInput label="Email" {...accountForm.getInputProps("email")} />
label="Email"
{...accountForm.getInputProps("email")}
/>
<Group position="right"> <Group position="right">
<Button type="submit">Save</Button> <Button type="submit">Save</Button>
</Group> </Group>

View File

@@ -17,6 +17,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TbLink, TbTrash } from "react-icons/tb"; import { TbLink, TbTrash } from "react-icons/tb";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import useUser from "../../hooks/user.hook"; import useUser from "../../hooks/user.hook";
import shareService from "../../services/share.service"; import shareService from "../../services/share.service";
@@ -83,12 +84,16 @@ const MyShares = () => {
variant="light" variant="light"
size={25} size={25}
onClick={() => { onClick={() => {
if (window.isSecureContext) {
clipboard.copy( clipboard.copy(
`${window.location.origin}/share/${share.id}` `${window.location.origin}/share/${share.id}`
); );
toast.success( toast.success(
"Your link was copied to the keyboard." "Your link was copied to the keyboard."
); );
} else {
showShareLinkModal(modals, share.id);
}
}} }}
> >
<TbLink /> <TbLink />

View File

@@ -1,4 +1,4 @@
import { Col, Container, createStyles, Grid, Paper, Text } from "@mantine/core"; import { Col, createStyles, Grid, Paper, Text } from "@mantine/core";
import Link from "next/link"; import Link from "next/link";
import { TbSettings, TbUsers } from "react-icons/tb"; import { TbSettings, TbUsers } from "react-icons/tb";
@@ -34,8 +34,7 @@ const Admin = () => {
const { classes, theme } = useStyles(); const { classes, theme } = useStyles();
return ( return (
<Container size="xl"> <Paper withBorder p={40}>
<Paper withBorder radius="md" p={40}>
<Grid mt="md"> <Grid mt="md">
{managementOptions.map((item) => { {managementOptions.map((item) => {
return ( return (
@@ -47,7 +46,7 @@ const Admin = () => {
key={item.title} key={item.title}
className={classes.item} className={classes.item}
> >
<item.icon color={theme.colors.victoria[5]} size={35} /> <item.icon color={theme.colors.victoria[8]} size={35} />
<Text mt={7}>{item.title}</Text> <Text mt={7}>{item.title}</Text>
</Paper> </Paper>
</Col> </Col>
@@ -55,7 +54,6 @@ const Admin = () => {
})} })}
</Grid> </Grid>
</Paper> </Paper>
</Container>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { Button, Stack, Text, Title } from "@mantine/core"; import { Box, Button, Stack, Text, Title } from "@mantine/core";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import AdminConfigTable from "../../components/admin/AdminConfigTable"; import AdminConfigTable from "../../components/admin/AdminConfigTable";
@@ -28,7 +28,9 @@ const Setup = () => {
<Logo height={80} width={80} /> <Logo height={80} width={80} />
<Title order={2}>Welcome to Pingvin Share</Title> <Title order={2}>Welcome to Pingvin Share</Title>
<Text>Let's customize Pingvin Share for you! </Text> <Text>Let's customize Pingvin Share for you! </Text>
<Box style={{ width: "100%" }}>
<AdminConfigTable /> <AdminConfigTable />
</Box>
<Button <Button
loading={isLoading} loading={isLoading}
onClick={async () => { onClick={async () => {

View File

@@ -10,7 +10,7 @@ const SignUp = () => {
const router = useRouter(); const router = useRouter();
if (user) { if (user) {
router.replace("/"); router.replace("/");
} else if (config.get("ALLOW_REGISTRATION") == "false") { } else if (!config.get("ALLOW_REGISTRATION")) {
router.replace("/auth/signIn"); router.replace("/auth/signIn");
} else { } else {
return ( return (

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 { useModals } from "@mantine/modals";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -8,6 +8,7 @@ import FileList from "../../components/share/FileList";
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal"; import showEnterPasswordModal from "../../components/share/showEnterPasswordModal";
import showErrorModal from "../../components/share/showErrorModal"; import showErrorModal from "../../components/share/showErrorModal";
import shareService from "../../services/share.service"; import shareService from "../../services/share.service";
import { Share as ShareType } from "../../types/share.type";
export function getServerSideProps(context: GetServerSidePropsContext) { export function getServerSideProps(context: GetServerSidePropsContext) {
return { return {
@@ -17,7 +18,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
const Share = ({ shareId }: { shareId: string }) => { const Share = ({ shareId }: { shareId: string }) => {
const modals = useModals(); const modals = useModals();
const [fileList, setFileList] = useState<any[]>([]); const [share, setShare] = useState<ShareType>();
const getShareToken = async (password?: string) => { const getShareToken = async (password?: string) => {
await shareService await shareService
@@ -41,7 +42,7 @@ const Share = ({ shareId }: { shareId: string }) => {
shareService shareService
.get(shareId) .get(shareId)
.then((share) => { .then((share) => {
setFileList(share.files); setShare(share);
}) })
.catch((e) => { .catch((e) => {
const { error } = e.response.data; const { error } = e.response.data;
@@ -77,14 +78,16 @@ const Share = ({ shareId }: { shareId: string }) => {
title={`Share ${shareId}`} title={`Share ${shareId}`}
description="Look what I've shared with you." description="Look what I've shared with you."
/> />
<Group position="right" mb="lg">
<DownloadAllButton shareId={shareId} /> <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> </Group>
<FileList
files={fileList} <FileList files={share?.files} shareId={shareId} isLoading={!share} />
shareId={shareId}
isLoading={fileList.length == 0}
/>
</> </>
); );
}; };

View File

@@ -2,6 +2,7 @@ import { Button, Group } from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import axios from "axios"; import axios from "axios";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import pLimit from "p-limit";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Meta from "../components/Meta"; import Meta from "../components/Meta";
import Dropzone from "../components/upload/Dropzone"; import Dropzone from "../components/upload/Dropzone";
@@ -12,10 +13,11 @@ import useConfig from "../hooks/config.hook";
import useUser from "../hooks/user.hook"; import useUser from "../hooks/user.hook";
import shareService from "../services/share.service"; import shareService from "../services/share.service";
import { FileUpload } from "../types/File.type"; import { FileUpload } from "../types/File.type";
import { ShareSecurity } from "../types/share.type"; import { CreateShare, Share } from "../types/share.type";
import toast from "../utils/toast.util"; import toast from "../utils/toast.util";
let share: any; let createdShare: Share;
const promiseLimit = pLimit(3);
const Upload = () => { const Upload = () => {
const router = useRouter(); const router = useRouter();
@@ -26,12 +28,7 @@ const Upload = () => {
const [files, setFiles] = useState<FileUpload[]>([]); const [files, setFiles] = useState<FileUpload[]>([]);
const [isUploading, setisUploading] = useState(false); const [isUploading, setisUploading] = useState(false);
const uploadFiles = async ( const uploadFiles = async (share: CreateShare) => {
id: string,
expiration: string,
recipients: string[],
security: ShareSecurity
) => {
setisUploading(true); setisUploading(true);
try { try {
setFiles((files) => setFiles((files) =>
@@ -40,8 +37,10 @@ const Upload = () => {
return file; return file;
}) })
); );
share = await shareService.create(id, expiration, recipients, security); createdShare = await shareService.create(share);
for (let i = 0; i < files.length; i++) {
const uploadPromises = files.map((file, i) => {
// Callback to indicate current upload progress
const progressCallBack = (progress: number) => { const progressCallBack = (progress: number) => {
setFiles((files) => { setFiles((files) => {
return files.map((file, callbackIndex) => { return files.map((file, callbackIndex) => {
@@ -54,11 +53,15 @@ const Upload = () => {
}; };
try { try {
await shareService.uploadFile(share.id, files[i], progressCallBack); return promiseLimit(() =>
shareService.uploadFile(share.id, file, progressCallBack)
);
} catch { } catch {
files[i].uploadingProgress = -1; file.uploadingProgress = -1;
}
} }
});
await Promise.all(uploadPromises);
} catch (e) { } catch (e) {
if (axios.isAxiosError(e)) { if (axios.isAxiosError(e)) {
toast.error(e.response?.data?.message ?? "An unkown error occured."); toast.error(e.response?.data?.message ?? "An unkown error occured.");
@@ -84,9 +87,9 @@ const Upload = () => {
toast.error(`${fileErrorCount} file(s) failed to upload. Try again.`); toast.error(`${fileErrorCount} file(s) failed to upload. Try again.`);
} else { } else {
shareService shareService
.completeShare(share.id) .completeShare(createdShare.id)
.then(() => { .then(() => {
showCompletedUploadModal(modals, share); showCompletedUploadModal(modals, createdShare);
setFiles([]); setFiles([]);
}) })
.catch(() => .catch(() =>

View File

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

View File

@@ -8,6 +8,7 @@ export type AdminConfig = Config & {
updatedAt: Date; updatedAt: Date;
secret: boolean; secret: boolean;
description: string; description: string;
obscured: boolean;
}; };
export default Config; export default Config;

View File

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

View File

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