Compare commits
10
Commits
d0f7ecdd7c
...
4bcdbfd1b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bcdbfd1b1 | ||
|
|
d69281aeff | ||
|
|
27414e6d97 | ||
|
|
57704e5943 | ||
|
|
3042f86adc | ||
|
|
48c9f571f0 | ||
|
|
be15cc828f | ||
|
|
5611db86a4 | ||
|
|
68ebf191fe | ||
|
|
9af800f2a6 |
@@ -27,6 +27,8 @@ jobs:
|
||||
type=semver,pattern={{version}},prefix=v
|
||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
||||
type=semver,pattern={{major}},prefix=v
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && !github.event.release.prerelease) }}
|
||||
type=raw,value=beta,enable=${{ github.event_name == 'release' && github.event.release.prerelease }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -11,26 +11,38 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Environment Setup
|
||||
- name: Swap PR labels
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
IS_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||
run: |
|
||||
# 1. Find all merged PRs with the 'awaiting release' label
|
||||
pr_numbers=$(gh pr list --state merged --label "awaiting release" --json number --jq '.[].number')
|
||||
if [ "$IS_PRERELEASE" = "true" ]; then
|
||||
echo "Processing pre-release..."
|
||||
FROM_LABEL="awaiting release"
|
||||
TO_LABEL="in beta"
|
||||
else
|
||||
echo "Processing regular release..."
|
||||
FROM_LABEL="in beta"
|
||||
TO_LABEL="released"
|
||||
fi
|
||||
|
||||
# 1. Find all merged PRs with the source label
|
||||
pr_numbers=$(gh pr list --state merged --label "$FROM_LABEL" --limit 1000 --json number --jq '.[].number')
|
||||
|
||||
if [ -z "$pr_numbers" ]; then
|
||||
echo "No PRs found with 'awaiting release' label."
|
||||
echo "No PRs found with '$FROM_LABEL' label."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2. Loop through each PR to swap the labels
|
||||
for pr in $pr_numbers; do
|
||||
echo "Updating labels for PR #$pr"
|
||||
echo "Updating labels for PR #$pr (moving from '$FROM_LABEL' to '$TO_LABEL')"
|
||||
|
||||
# Remove the old label
|
||||
gh pr edit $pr --remove-label "awaiting release"
|
||||
gh pr edit $pr --remove-label "$FROM_LABEL"
|
||||
|
||||
# Add the new label
|
||||
gh pr edit $pr --add-label "released"
|
||||
gh pr edit $pr --add-label "$TO_LABEL"
|
||||
done
|
||||
|
||||
|
||||
+6
-5
@@ -13,11 +13,12 @@ If you do end up using AI tools, we ask that you only do so **assistively** (lik
|
||||
|
||||
## Guidelines for Using AI Tools
|
||||
|
||||
1. **Understand fully:** You must be able to explain every line of code you submit
|
||||
2. **Test thoroughly:** Review and test all code (manually by a human) before submission
|
||||
3. **Take responsibility:** You are accountable for bugs, issues, or problems with your contribution
|
||||
4. **Disclose usage:** Note which AI tools you used in your PR description
|
||||
5. **Follow reasonable coding practices:** Use multiple commits to break down changes,
|
||||
1. **Assistance, not Generation:** Do not just copy-paste/save AI output. AI use should assist human development, not replace.
|
||||
2. **Understand fully:** You must be able to explain every line of code you submit
|
||||
3. **Test thoroughly:** Review and test all code (manually by a human) before submission
|
||||
4. **Take responsibility:** You are accountable for bugs, issues, or problems with your contribution
|
||||
5. **Disclose usage:** Note which AI tools you used in your PR description
|
||||
6. **Follow reasonable coding practices:** Use multiple commits to break down changes,
|
||||
a single large commit makes review and tracking code changes difficult for maintainers.
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
## [1.21.2](https://github.com/smp46/pingvin-share-x/compare/v1.21.1...v1.21.2) (2026-07-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** resolve security vulns via npm audit ([945c5e5](https://github.com/smp46/pingvin-share-x/commit/945c5e572dbf08021a15d510c9f12f1fc6ee9630))
|
||||
* **deps:** update backend sharp to non-vuln version ([9af800f](https://github.com/smp46/pingvin-share-x/commit/9af800f2a66f9ffd8fbf2926adce490723a77a05))
|
||||
* **deps:** update sharp & brace-expansion to non-vuln version ([d0f7ecd](https://github.com/smp46/pingvin-share-x/commit/d0f7ecdd7cb61eda264a61a3eeab346f33e2bf5a))
|
||||
|
||||
## [1.21.1](https://github.com/smp46/pingvin-share-x/compare/v1.21.0...v1.21.1) (2026-07-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Generated
+177
-137
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.787.0",
|
||||
"@keyv/redis": "^4.4.0",
|
||||
@@ -50,7 +50,7 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"serialize-javascript": "^7.0.3",
|
||||
"sharp": "^0.34.1",
|
||||
"sharp": "^0.35.3",
|
||||
"uuid": "^11.1.1",
|
||||
"yaml": "^2.7.1"
|
||||
},
|
||||
@@ -1901,9 +1901,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1913,19 +1913,19 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1935,19 +1935,38 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-freebsd-wasm32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1961,9 +1980,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1977,9 +1996,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
|
||||
"integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1993,9 +2012,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2009,9 +2028,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
|
||||
"integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2025,9 +2044,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
|
||||
"integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2041,9 +2060,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
|
||||
"integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2057,9 +2076,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2073,9 +2092,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2089,9 +2108,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2105,9 +2124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
|
||||
"integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2117,19 +2136,19 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
"@img/sharp-libvips-linux-arm": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2139,19 +2158,19 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
|
||||
"integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2161,19 +2180,19 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
|
||||
"integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2183,19 +2202,19 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
|
||||
"integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2205,19 +2224,19 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2227,19 +2246,19 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
"@img/sharp-libvips-linux-x64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2249,19 +2268,19 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2271,38 +2290,54 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
"@emnapi/runtime": "^1.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-webcontainers-wasm32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2312,16 +2347,16 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
|
||||
"integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2331,16 +2366,16 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": "^20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2350,7 +2385,7 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
@@ -9480,9 +9515,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -9568,47 +9603,52 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"hasInstallScript": true,
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
|
||||
"integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"@img/colour": "^1.1.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
"semver": "^7.8.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
"@img/sharp-darwin-arm64": "0.35.3",
|
||||
"@img/sharp-darwin-x64": "0.35.3",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.3",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.2",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.2",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.2",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.2",
|
||||
"@img/sharp-linux-arm": "0.35.3",
|
||||
"@img/sharp-linux-arm64": "0.35.3",
|
||||
"@img/sharp-linux-ppc64": "0.35.3",
|
||||
"@img/sharp-linux-riscv64": "0.35.3",
|
||||
"@img/sharp-linux-s390x": "0.35.3",
|
||||
"@img/sharp-linux-x64": "0.35.3",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.3",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.3",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.3",
|
||||
"@img/sharp-win32-arm64": "0.35.3",
|
||||
"@img/sharp-win32-ia32": "0.35.3",
|
||||
"@img/sharp-win32-x64": "0.35.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "cross-env NODE_ENV=development nest start --watch",
|
||||
@@ -62,7 +62,7 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"serialize-javascript": "^7.0.3",
|
||||
"sharp": "^0.34.1",
|
||||
"sharp": "^0.35.3",
|
||||
"uuid": "^11.1.1",
|
||||
"yaml": "^2.7.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "ShareUserRecipient" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" TEXT NOT NULL,
|
||||
"shareId" TEXT NOT NULL,
|
||||
CONSTRAINT "ShareUserRecipient_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "ShareUserRecipient_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "Share" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_ShareSecurity" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"password" TEXT,
|
||||
"maxViews" INTEGER,
|
||||
"restrictToRecipients" BOOLEAN NOT NULL DEFAULT false,
|
||||
"shareId" TEXT,
|
||||
CONSTRAINT "ShareSecurity_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "Share" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_ShareSecurity" ("createdAt", "id", "maxViews", "password", "shareId") SELECT "createdAt", "id", "maxViews", "password", "shareId" FROM "ShareSecurity";
|
||||
DROP TABLE "ShareSecurity";
|
||||
ALTER TABLE "new_ShareSecurity" RENAME TO "ShareSecurity";
|
||||
CREATE UNIQUE INDEX "ShareSecurity_shareId_key" ON "ShareSecurity"("shareId");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ShareUserRecipient_userId_shareId_key" ON "ShareUserRecipient"("userId", "shareId");
|
||||
@@ -19,10 +19,11 @@ model User {
|
||||
ldapDN String? @unique
|
||||
shareSizeLimit String?
|
||||
|
||||
shares Share[]
|
||||
refreshTokens RefreshToken[]
|
||||
loginTokens LoginToken[]
|
||||
reverseShares ReverseShare[]
|
||||
shares Share[]
|
||||
refreshTokens RefreshToken[]
|
||||
loginTokens LoginToken[]
|
||||
reverseShares ReverseShare[]
|
||||
shareUserRecipients ShareUserRecipient[]
|
||||
|
||||
totpEnabled Boolean @default(false)
|
||||
totpVerified Boolean @default(false)
|
||||
@@ -98,9 +99,10 @@ model Share {
|
||||
reverseShareId String?
|
||||
reverseShare ReverseShare? @relation(fields: [reverseShareId], references: [id], onDelete: Cascade)
|
||||
|
||||
security ShareSecurity?
|
||||
recipients ShareRecipient[]
|
||||
files File[]
|
||||
security ShareSecurity?
|
||||
recipients ShareRecipient[]
|
||||
userRecipients ShareUserRecipient[]
|
||||
files File[]
|
||||
storageProvider String @default("LOCAL")
|
||||
}
|
||||
|
||||
@@ -145,13 +147,27 @@ model ShareSecurity {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
password String?
|
||||
maxViews Int?
|
||||
password String?
|
||||
maxViews Int?
|
||||
restrictToRecipients Boolean @default(false)
|
||||
|
||||
shareId String? @unique
|
||||
share Share? @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model ShareUserRecipient {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
shareId String
|
||||
share Share @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, shareId])
|
||||
}
|
||||
|
||||
model Config {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@ export const configVariables = {
|
||||
defaultValue: "",
|
||||
secret: false,
|
||||
},
|
||||
uploadProgressStyle: {
|
||||
type: "string",
|
||||
defaultValue: "circle",
|
||||
secret: false,
|
||||
},
|
||||
},
|
||||
share: {
|
||||
allowRegistration: {
|
||||
@@ -122,6 +127,11 @@ export const configVariables = {
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
enableUserRecipients: {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
fileRetentionPeriod: {
|
||||
type: "timespan",
|
||||
defaultValue: "0 days",
|
||||
@@ -198,6 +208,11 @@ export const configVariables = {
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
shareRecipientsReplyToCreator: {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
shareDownloadNotificationSubject: {
|
||||
type: "string",
|
||||
defaultValue: "Your file was downloaded",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import * as fs from "fs";
|
||||
import * as sharp from "sharp";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const sharp = require("sharp");
|
||||
|
||||
const IMAGES_PATH = "../frontend/public/img";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export class EmailService {
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
private readonly i18n: I18nService,
|
||||
) { }
|
||||
) {}
|
||||
private readonly logger = new Logger(EmailService.name);
|
||||
|
||||
getTransporter() {
|
||||
@@ -38,23 +38,28 @@ export class EmailService {
|
||||
});
|
||||
}
|
||||
|
||||
private async sendMail(email: string, subject: string, text: string) {
|
||||
const isHtml = this.config.get("email.sendHtmlEmails");
|
||||
private async sendMail(
|
||||
email: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
replyTo?: string,
|
||||
) {
|
||||
const isHtml = this.config.get("email.sendHtmlEmails");
|
||||
|
||||
await this.getTransporter()
|
||||
.sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email",
|
||||
)}>`,
|
||||
to: email,
|
||||
subject: subject,
|
||||
[isHtml ? "html" : "text"]: text,
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error(e);
|
||||
throw new InternalServerErrorException(this.i18n.t("email.sendFailed"));
|
||||
});
|
||||
|
||||
await this.getTransporter()
|
||||
.sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email",
|
||||
)}>`,
|
||||
to: email,
|
||||
subject: subject,
|
||||
[isHtml ? "html" : "text"]: text,
|
||||
...(replyTo && { replyTo }),
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error(e);
|
||||
throw new InternalServerErrorException(this.i18n.t("email.sendFailed"));
|
||||
});
|
||||
}
|
||||
|
||||
async sendMailToShareRecipients(
|
||||
@@ -76,6 +81,14 @@ export class EmailService {
|
||||
const lang = this.config.get("general.defaultLanguage");
|
||||
const locale = this.i18n.translate("email.locale", { lang });
|
||||
|
||||
let replyTo: string | undefined = undefined;
|
||||
if (
|
||||
this.config.get("email.shareRecipientsReplyToCreator") &&
|
||||
creator?.email
|
||||
) {
|
||||
replyTo = `"${creator.username}" <${creator.email}>`;
|
||||
}
|
||||
|
||||
await this.sendMail(
|
||||
recipientEmail,
|
||||
this.config.get("email.shareRecipientsSubject"),
|
||||
@@ -85,7 +98,7 @@ export class EmailService {
|
||||
.replaceAll(
|
||||
"{creator}",
|
||||
creator?.username ??
|
||||
this.i18n.t("email.shareRecipientsCreatorFallback"),
|
||||
this.i18n.t("email.shareRecipientsCreatorFallback"),
|
||||
)
|
||||
.replaceAll("{creatorEmail}", creator?.email ?? "")
|
||||
.replaceAll("{shareUrl}", shareUrl)
|
||||
@@ -99,6 +112,7 @@ export class EmailService {
|
||||
? moment(expiration).locale(locale).fromNow()
|
||||
: this.i18n.t("email.shareRecipientsExpiresNeverFallback"),
|
||||
),
|
||||
replyTo,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,11 @@ export class FileSecurityGuard extends ShareSecurityGuard {
|
||||
|
||||
const share = await this._prisma.share.findUnique({
|
||||
where: { id: shareId },
|
||||
include: { security: true },
|
||||
include: {
|
||||
security: true,
|
||||
userRecipients: { select: { userId: true } },
|
||||
recipients: { select: { email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// If there is no share token the user requests a file directly
|
||||
@@ -70,6 +74,19 @@ export class FileSecurityGuard extends ShareSecurityGuard {
|
||||
throw new NotFoundException(this._i18n.t("file.notFound"));
|
||||
}
|
||||
|
||||
if (share.security?.restrictToRecipients) {
|
||||
const user = await this.authenticateUser(context);
|
||||
const isCreator = user && share.creatorId === user.id;
|
||||
const isRecipient = await this.isRecipient(share, user);
|
||||
|
||||
if (!isCreator && !isRecipient) {
|
||||
throw new ForbiddenException(
|
||||
this._i18n.t("share.restrictedToRecipients"),
|
||||
"share_restricted_to_recipients",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (share.security?.password)
|
||||
throw new ForbiddenException(this._i18n.t("file.passwordProtected"));
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
"wrongPassword": "Wrong password",
|
||||
"tokenRequired": "Share token required",
|
||||
"privateShare": "Only reverse share creator can access this share",
|
||||
"maxViewsExceeded": "Maximum views exceeded"
|
||||
"maxViewsExceeded": "Maximum views exceeded",
|
||||
"restrictedToRecipients": "This share is restricted to specific recipients. Please log in to access it."
|
||||
}
|
||||
|
||||
@@ -27,19 +27,20 @@ export class ReverseShareService {
|
||||
)
|
||||
.toDate();
|
||||
|
||||
const creator = await this.prisma.user.findUnique({
|
||||
where: { id: creatorId },
|
||||
});
|
||||
|
||||
const parsedExpiration = parseRelativeDateToAbsolute(data.shareExpiration);
|
||||
const maxExpiration = this.config.get("share.maxExpiration");
|
||||
if (
|
||||
!creator?.isAdmin &&
|
||||
maxExpiration.value !== 0 &&
|
||||
parsedExpiration >
|
||||
moment().add(maxExpiration.value, maxExpiration.unit).toDate()
|
||||
) {
|
||||
throw new BadRequestException(this.i18n.t("share.maxExpirationExceeded"));
|
||||
}
|
||||
|
||||
const creator = await this.prisma.user.findUnique({
|
||||
where: { id: creatorId },
|
||||
});
|
||||
const userMaxShareSize = creator?.shareSizeLimit
|
||||
? parseInt(creator.shareSizeLimit)
|
||||
: parseInt(this.config.get("share.maxSize"));
|
||||
|
||||
@@ -6,4 +6,7 @@ export class MyShareSecurityDTO {
|
||||
|
||||
@Expose()
|
||||
maxViews: number;
|
||||
|
||||
@Expose()
|
||||
restrictToRecipients: boolean;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { IsNumber, IsOptional, IsString, Length } from "class-validator";
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Length,
|
||||
} from "class-validator";
|
||||
|
||||
export class ShareSecurityDTO {
|
||||
@IsString()
|
||||
@@ -9,4 +15,8 @@ export class ShareSecurityDTO {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
maxViews: number;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
restrictToRecipients: boolean;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,46 @@ export class ShareSecurityGuard extends JwtGuard {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
protected async authenticateUser(
|
||||
context: ExecutionContext,
|
||||
): Promise<User | undefined> {
|
||||
await super.canActivate(context);
|
||||
const request: Request = context.switchToHttp().getRequest();
|
||||
return request.user as User;
|
||||
}
|
||||
|
||||
protected async isRecipient(
|
||||
share: {
|
||||
id: string;
|
||||
userRecipients: { userId: string }[];
|
||||
recipients: { email: string }[];
|
||||
},
|
||||
user?: User,
|
||||
): Promise<boolean> {
|
||||
if (!user) return false;
|
||||
if (!this.configService.get("share.enableUserRecipients")) return false;
|
||||
|
||||
// Already linked as a recipient of this share.
|
||||
const isLinked = share.userRecipients.some((r) => r.userId === user.id);
|
||||
if (isLinked) return true;
|
||||
|
||||
// Otherwise, if the user's (account-verified) email matches one of the
|
||||
// share's recipients, grant access and link them
|
||||
const userEmail = user.email?.toLowerCase();
|
||||
const isEmailRecipient =
|
||||
!!userEmail &&
|
||||
share.recipients.some((r) => r.email.toLowerCase() === userEmail);
|
||||
if (isEmailRecipient) {
|
||||
await this.prisma.shareUserRecipient.upsert({
|
||||
where: { userId_shareId: { userId: user.id, shareId: share.id } },
|
||||
create: { userId: user.id, shareId: share.id },
|
||||
update: {},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request: Request = context.switchToHttp().getRequest();
|
||||
|
||||
@@ -38,14 +78,17 @@ export class ShareSecurityGuard extends JwtGuard {
|
||||
|
||||
const share = await this.prisma.share.findUnique({
|
||||
where: { id: shareId },
|
||||
include: { security: true, reverseShare: true },
|
||||
include: {
|
||||
security: true,
|
||||
reverseShare: true,
|
||||
userRecipients: { select: { userId: true } },
|
||||
recipients: { select: { email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!share) throw new NotFoundException(this.i18n.t("share.notFound"));
|
||||
|
||||
// Run the JWTGuard to set the user
|
||||
await super.canActivate(context);
|
||||
const user = request.user as User;
|
||||
const user = await this.authenticateUser(context);
|
||||
|
||||
// If admin access is enabled and user is admin, allow access
|
||||
if (
|
||||
@@ -62,6 +105,22 @@ export class ShareSecurityGuard extends JwtGuard {
|
||||
throw new NotFoundException(this.i18n.t("share.notFound"));
|
||||
}
|
||||
|
||||
// If user sharing is enabled, check if the authenticated user is a recipient
|
||||
if (await this.isRecipient(share, user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If share is restricted to named recipients, block everyone else (excluding the creator)
|
||||
if (
|
||||
share.security?.restrictToRecipients &&
|
||||
(!user || share.creatorId !== user.id)
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
this.i18n.t("share.restrictedToRecipients"),
|
||||
"share_restricted_to_recipients",
|
||||
);
|
||||
}
|
||||
|
||||
if (share.security?.password && !shareToken)
|
||||
throw new ForbiddenException(
|
||||
this.i18n.t("file.passwordProtected"),
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
@@ -19,6 +20,7 @@ import * as moment from "moment";
|
||||
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 { ConfigService } from "src/config/config.service";
|
||||
import { AdminShareDTO } from "./dto/adminShare.dto";
|
||||
import { CreateShareDTO } from "./dto/createShare.dto";
|
||||
import { MyShareDTO } from "./dto/myShare.dto";
|
||||
@@ -40,6 +42,7 @@ export class ShareController {
|
||||
constructor(
|
||||
private shareService: ShareService,
|
||||
private jwtService: JwtService,
|
||||
private config: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get("all")
|
||||
@@ -56,6 +59,14 @@ export class ShareController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get("received")
|
||||
@UseGuards(JwtGuard)
|
||||
async getReceivedShares(@GetUser() user: User) {
|
||||
if (!this.config.get("share.enableUserRecipients"))
|
||||
throw new ForbiddenException("User recipients are not enabled");
|
||||
return this.shareService.getReceivedShares(user.id);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@UseGuards(IdValidation, ShareSecurityGuard)
|
||||
async get(@Param("id") id: string) {
|
||||
|
||||
@@ -53,10 +53,32 @@ export class ShareService {
|
||||
if (!share.security || Object.keys(share.security).length == 0)
|
||||
share.security = undefined;
|
||||
|
||||
if (share.security?.restrictToRecipients && share.security?.password) {
|
||||
throw new BadRequestException(
|
||||
"Cannot set a password on a share restricted to recipients.",
|
||||
);
|
||||
}
|
||||
|
||||
if (share.security?.password) {
|
||||
share.security.password = await argon.hash(share.security.password);
|
||||
}
|
||||
|
||||
if (
|
||||
this.configService.get("share.enableUserRecipients") &&
|
||||
share.security?.restrictToRecipients
|
||||
) {
|
||||
if (!share.recipients?.length) {
|
||||
throw new BadRequestException(
|
||||
"A share restricted to recipients must have at least one recipient.",
|
||||
);
|
||||
}
|
||||
// Note: we intentionally do NOT check whether the recipient emails belong
|
||||
// to registered accounts. Doing so would leak which emails have an account
|
||||
// (account enumeration). Recipients who aren't registered yet simply sign
|
||||
// up to gain access (see ShareSecurityGuard); if signups are disabled they
|
||||
// can't access it.
|
||||
}
|
||||
|
||||
let expirationDate: Date;
|
||||
|
||||
// If share is created by a reverse share token override the expiration date
|
||||
@@ -66,7 +88,9 @@ export class ShareService {
|
||||
expirationDate = reverseShare.shareExpiration;
|
||||
} else {
|
||||
expirationDate = this.parseExpiration(share.expiration);
|
||||
this.validateExpiration(expirationDate);
|
||||
if (!user?.isAdmin) {
|
||||
this.validateExpiration(expirationDate);
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(`${SHARE_DIRECTORY}/${share.id}`, {
|
||||
@@ -163,12 +187,32 @@ export class ShareService {
|
||||
recipient.email,
|
||||
recipient.id,
|
||||
share.id,
|
||||
share.creator,
|
||||
share.creator || share.reverseShare?.creator,
|
||||
share.description,
|
||||
share.expiration,
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-link email recipients who are registered users so the share appears in their dashboard
|
||||
if (this.configService.get("share.enableUserRecipients")) {
|
||||
const emails = share.recipients.map((r) => r.email);
|
||||
if (emails.length > 0) {
|
||||
const matchedUsers = await this.prisma.user.findMany({
|
||||
where: { email: { in: emails } },
|
||||
select: { id: true },
|
||||
});
|
||||
for (const matchedUser of matchedUsers) {
|
||||
await this.prisma.shareUserRecipient.upsert({
|
||||
where: {
|
||||
userId_shareId: { userId: matchedUser.id, shareId: share.id },
|
||||
},
|
||||
create: { userId: matchedUser.id, shareId: share.id },
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifyReverseShareCreator = share.reverseShare
|
||||
? this.config.get("smtp.enabled") &&
|
||||
share.reverseShare.sendEmailNotification
|
||||
@@ -328,7 +372,9 @@ export class ShareService {
|
||||
let expirationDate: Date | undefined;
|
||||
if (body.expiration !== undefined) {
|
||||
expirationDate = this.parseExpiration(body.expiration);
|
||||
this.validateExpiration(expirationDate);
|
||||
if (!user?.isAdmin) {
|
||||
this.validateExpiration(expirationDate);
|
||||
}
|
||||
}
|
||||
|
||||
const data: Prisma.ShareUpdateInput = {
|
||||
@@ -395,6 +441,22 @@ export class ShareService {
|
||||
return (await this.prisma.share.findUnique({ where: { id } })).uploadLocked;
|
||||
}
|
||||
|
||||
async getReceivedShares(userId: string) {
|
||||
return this.prisma.shareUserRecipient.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
share: {
|
||||
include: {
|
||||
creator: true,
|
||||
files: true,
|
||||
security: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
private transformShare(share: any) {
|
||||
return {
|
||||
...share,
|
||||
@@ -404,6 +466,7 @@ export class ShareService {
|
||||
security: {
|
||||
maxViews: share.security?.maxViews,
|
||||
passwordProtected: !!share.security?.password,
|
||||
restrictToRecipients: !!share.security?.restrictToRecipients,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ email:
|
||||
Pingvin Share 🐧
|
||||
#Whether to send an email to the share creator when an email recipient downloads a file. This requires SMTP and email recipient sharing.
|
||||
enableShareDownloadNotifications: "false"
|
||||
#Whether to set the Reply-To header to the email address of the user who created the share.
|
||||
shareRecipientsReplyToCreator: "false"
|
||||
#Subject of the email which gets sent to the share creator when a recipient downloads a file.
|
||||
shareDownloadNotificationSubject: Your file was downloaded
|
||||
#Message which gets sent to the share creator when a recipient downloads a file. Available variables:
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/server": "^11.11.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { stringToTimespan, timespanToString } from "../../../utils/date.util";
|
||||
import FileSizeInput from "../../core/FileSizeInput";
|
||||
import TimespanInput from "../../core/TimespanInput";
|
||||
import { LOCALES } from "../../../i18n/locales";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
|
||||
const AdminConfigInput = ({
|
||||
configVariable,
|
||||
@@ -37,6 +38,7 @@ const AdminConfigInput = ({
|
||||
updatedConfigVariables?: UpdateConfig[];
|
||||
optionalConfigVariables?: AdminConfig[];
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
const isCustomCssConfig = configVariable.key === "appearance.customCss";
|
||||
const isThemePrimaryColorConfig =
|
||||
configVariable.key === "appearance.themePrimaryColor";
|
||||
@@ -47,6 +49,8 @@ const AdminConfigInput = ({
|
||||
configVariable.key === "appearance.themeColorScheme";
|
||||
const isDefaultLanguageConfig =
|
||||
configVariable.key === "general.defaultLanguage";
|
||||
const isUploadProgressStyleConfig =
|
||||
configVariable.key === "appearance.uploadProgressStyle";
|
||||
const isEmailShareConfig =
|
||||
configVariable.key === "email.enableShareEmailRecipients";
|
||||
const isEmailVerificationConfig =
|
||||
@@ -246,6 +250,37 @@ const AdminConfigInput = ({
|
||||
value={form.values.stringValue}
|
||||
onChange={(value) => onValueChange(configVariable, value)}
|
||||
/>
|
||||
) : isUploadProgressStyleConfig ? (
|
||||
<Select
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
disabled={!configVariable.allowEdit}
|
||||
data={[
|
||||
{
|
||||
value: "circle",
|
||||
label: t(
|
||||
"admin.config.appearance.upload-progress-style.circle",
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "circle-percentage",
|
||||
label: t(
|
||||
"admin.config.appearance.upload-progress-style.circle-percentage",
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "percentage-time",
|
||||
label: t(
|
||||
"admin.config.appearance.upload-progress-style.percentage-time",
|
||||
),
|
||||
},
|
||||
]}
|
||||
value={form.values.stringValue}
|
||||
placeholder={configVariable.defaultValue}
|
||||
onChange={(value) => onValueChange(configVariable, value ?? "")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
style={{
|
||||
|
||||
@@ -124,7 +124,7 @@ const ManageShareTable = ({
|
||||
parseInt(config.get("share.maxSize")),
|
||||
config.get("general.appUrl"),
|
||||
config.get("general.appUrl", true),
|
||||
config.get("share.maxExpiration"),
|
||||
{ value: 0, unit: "days" },
|
||||
updateShare,
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -217,6 +217,14 @@ const Header = () => {
|
||||
link: "/account/reverseShares",
|
||||
label: t("navbar.links.reverse"),
|
||||
},
|
||||
...(config.get("share.enableUserRecipients")
|
||||
? [
|
||||
{
|
||||
link: "/account/received",
|
||||
label: t("navbar.links.received"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const mobileProfileLinks: NavLink[] = [
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ActionIcon, Menu } from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { TbArrowLoopLeft, TbLink } from "react-icons/tb";
|
||||
import { TbArrowLoopLeft, TbLink, TbMailbox } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HoverTip } from "../../components/core/HoverTip";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import { useState } from "react";
|
||||
|
||||
const NavbarShareMneu = () => {
|
||||
const t = useTranslate();
|
||||
const config = useConfig();
|
||||
const [menuOpened, setMenuOpened] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -32,6 +34,15 @@ const NavbarShareMneu = () => {
|
||||
>
|
||||
<FormattedMessage id="navbar.links.reverse" />
|
||||
</Menu.Item>
|
||||
{config.get("share.enableUserRecipients") && (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href="/account/received"
|
||||
icon={<TbMailbox />}
|
||||
>
|
||||
<FormattedMessage id="navbar.links.received" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,19 @@ import showFilePreviewModal from "./modals/showFilePreviewModal";
|
||||
import { HoverTip } from "../core/HoverTip";
|
||||
import api from "../../services/api.service";
|
||||
|
||||
const renderFileName = (name: string) => {
|
||||
const parts = name.split("/");
|
||||
if (parts.length === 1) return name;
|
||||
const fileName = parts.pop();
|
||||
const folderPath = parts.join("/");
|
||||
return (
|
||||
<span>
|
||||
<span style={{ opacity: 0.5 }}>{folderPath}/</span>
|
||||
<span style={{ fontWeight: 600 }}>{fileName}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const FileList = ({
|
||||
files,
|
||||
setShare,
|
||||
@@ -118,7 +131,7 @@ const FileList = ({
|
||||
? skeletonRows
|
||||
: files!.map((file) => (
|
||||
<tr key={file.name}>
|
||||
<td>{file.name}</td>
|
||||
<td>{renderFileName(file.name)}</td>
|
||||
<td>{byteToHumanSizeString(parseInt(file.size))}</td>
|
||||
<td>
|
||||
<Group position="right" noWrap>
|
||||
|
||||
@@ -92,7 +92,9 @@ const Body = ({
|
||||
sendEmailNotification: false,
|
||||
expiration_num: defaultTimespan.value,
|
||||
expiration_unit: `-${defaultTimespan.unit}` as string,
|
||||
simplified: !reverseShareSimpleOnly ? false : !!(getCookie("reverse-share.simplified") ?? false),
|
||||
simplified: !reverseShareSimpleOnly
|
||||
? false
|
||||
: !!(getCookie("reverse-share.simplified") ?? false),
|
||||
publicAccess: !!(getCookie("reverse-share.public-access") ?? true),
|
||||
},
|
||||
validate: yupResolver(
|
||||
@@ -273,7 +275,7 @@ const Body = ({
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{!reverseShareSimpleOnly &&
|
||||
{!reverseShareSimpleOnly && (
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
@@ -285,7 +287,7 @@ const Body = ({
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
@@ -301,8 +303,8 @@ const Body = ({
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form >
|
||||
</Group >
|
||||
</form>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -88,9 +88,8 @@ const Body = ({
|
||||
? parseInt(currentShare.creator.shareSizeLimit)
|
||||
: maxShareSize;
|
||||
|
||||
const shareSizeRatio = resolvedMaxShareSize > 0
|
||||
? currentShare.size / resolvedMaxShareSize
|
||||
: 0;
|
||||
const shareSizeRatio =
|
||||
resolvedMaxShareSize > 0 ? currentShare.size / resolvedMaxShareSize : 0;
|
||||
|
||||
const formattedShareSize = byteToHumanSizeString(currentShare.size);
|
||||
const formattedMaxShareSize = byteToHumanSizeString(resolvedMaxShareSize);
|
||||
@@ -139,6 +138,15 @@ const Body = ({
|
||||
{currentShare.description || "-"}
|
||||
</Text>
|
||||
|
||||
{currentShare.recipients && currentShare.recipients.length > 0 && (
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="upload.modal.accordion.email.title" />:{" "}
|
||||
</b>
|
||||
{currentShare.recipients.join(", ")}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.createdAt" />:{" "}
|
||||
@@ -174,14 +182,9 @@ const Body = ({
|
||||
)}
|
||||
<Progress
|
||||
value={shareSizeProgress}
|
||||
label={
|
||||
shareSizeRatio >= 0.1
|
||||
? formattedShareSize
|
||||
: ""
|
||||
}
|
||||
label={shareSizeRatio >= 0.1 ? formattedShareSize : ""}
|
||||
style={{
|
||||
width:
|
||||
shareSizeRatio < 0.1 ? "70%" : "80%",
|
||||
width: shareSizeRatio < 0.1 ? "70%" : "80%",
|
||||
}}
|
||||
size="xl"
|
||||
radius="xl"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Center, createStyles, Group, Text } from "@mantine/core";
|
||||
import { Button, Center, createStyles, Group, Text, Menu } from "@mantine/core";
|
||||
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
|
||||
import { ForwardedRef, useRef } from "react";
|
||||
import { TbCloudUpload, TbUpload } from "react-icons/tb";
|
||||
import React, { ForwardedRef, useRef } from "react";
|
||||
import { TbCloudUpload, TbUpload, TbFolder } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
@@ -32,6 +32,74 @@ const useStyles = createStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const traverseDirectory = async (entry: any, path = ""): Promise<File[]> => {
|
||||
if (entry.isFile) {
|
||||
return new Promise((resolve) => {
|
||||
entry.file((file: File) => {
|
||||
const relativePath = path ? `${path}/${file.name}` : file.name;
|
||||
Object.defineProperty(file, "webkitRelativePath", {
|
||||
value: relativePath,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
resolve([file]);
|
||||
});
|
||||
});
|
||||
} else if (entry.isDirectory) {
|
||||
const dirReader = entry.createReader();
|
||||
const readEntries = (): Promise<any[]> => {
|
||||
return new Promise((resolve) => {
|
||||
dirReader.readEntries(
|
||||
(entries: any[]) => resolve(entries),
|
||||
() => resolve([]),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
let entries: any[] = [];
|
||||
let readBatch = await readEntries();
|
||||
while (readBatch.length > 0) {
|
||||
entries = entries.concat(readBatch);
|
||||
readBatch = await readEntries();
|
||||
}
|
||||
|
||||
const promises = entries.map((e) =>
|
||||
traverseDirectory(e, path ? `${path}/${entry.name}` : entry.name)
|
||||
);
|
||||
const results = await Promise.all(promises);
|
||||
return results.flat();
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getFilesFromEvent = async (event: any): Promise<any[]> => {
|
||||
const items = event.dataTransfer ? event.dataTransfer.items : event.target.files;
|
||||
if (!items) return [];
|
||||
|
||||
const filePromises: Promise<File[]>[] = [];
|
||||
|
||||
if (event.dataTransfer) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.kind === "file") {
|
||||
const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
|
||||
if (entry) {
|
||||
filePromises.push(traverseDirectory(entry));
|
||||
} else {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
filePromises.push(Promise.resolve([file]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const fileArrays = await Promise.all(filePromises);
|
||||
return fileArrays.flat();
|
||||
} else {
|
||||
return Array.from(items) as File[];
|
||||
}
|
||||
};
|
||||
|
||||
const Dropzone = ({
|
||||
title,
|
||||
isUploading,
|
||||
@@ -46,17 +114,60 @@ const Dropzone = ({
|
||||
onFilesChanged: (files: FileUpload[]) => void;
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const { classes } = useStyles();
|
||||
const openRef = useRef<() => void>();
|
||||
const folderInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isFolderUploadSupported =
|
||||
typeof window !== "undefined" &&
|
||||
typeof HTMLInputElement !== "undefined" &&
|
||||
"webkitdirectory" in HTMLInputElement.prototype;
|
||||
|
||||
const handleFolderSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const filesList = event.target.files;
|
||||
if (!filesList) return;
|
||||
const filesArray = Array.from(filesList) as FileUpload[];
|
||||
|
||||
const files = filesArray.map((newFile) => {
|
||||
newFile.uploadingProgress = 0;
|
||||
return newFile;
|
||||
});
|
||||
|
||||
const fileSizeSum = files.reduce((n, { size }) => n + size, 0);
|
||||
|
||||
if (fileSizeSum + currentFilesSize > maxShareSize) {
|
||||
toast.error(
|
||||
t("upload.dropzone.notify.file-too-big", {
|
||||
maxSize: byteToHumanSizeString(maxShareSize),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
onFilesChanged(files);
|
||||
}
|
||||
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<input
|
||||
type="file"
|
||||
ref={folderInputRef}
|
||||
style={{ display: "none" }}
|
||||
{...({
|
||||
webkitdirectory: "",
|
||||
directory: "",
|
||||
} as any)}
|
||||
multiple
|
||||
onChange={handleFolderSelect}
|
||||
/>
|
||||
<MantineDropzone
|
||||
onReject={(e) => {
|
||||
toast.error(e[0].errors[0].message);
|
||||
}}
|
||||
disabled={isUploading}
|
||||
openRef={openRef as ForwardedRef<() => void>}
|
||||
getFilesFromEvent={getFilesFromEvent}
|
||||
onDrop={(files: FileUpload[]) => {
|
||||
const fileSizeSum = files.reduce((n, { size }) => n + size, 0);
|
||||
|
||||
@@ -93,16 +204,21 @@ const Dropzone = ({
|
||||
</div>
|
||||
</MantineDropzone>
|
||||
<Center>
|
||||
<Button
|
||||
className={classes.control}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="xl"
|
||||
disabled={isUploading}
|
||||
onClick={() => openRef.current && openRef.current()}
|
||||
>
|
||||
{<TbUpload />}
|
||||
</Button>
|
||||
{isFolderUploadSupported && (
|
||||
<Button
|
||||
className={classes.control}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="xl"
|
||||
disabled={isUploading}
|
||||
onClick={() => folderInputRef.current?.click()}
|
||||
>
|
||||
<TbFolder style={{ marginRight: 6 }} />
|
||||
<FormattedMessage
|
||||
id={currentFilesSize > 0 ? "upload.button.folder.append" : "upload.button.folder"}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Center>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import useUser from "../../hooks/user.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { FileListItem, FileMetaData, FileUpload } from "../../types/File.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { getNormalizedFileName, filterDuplicateFiles } from "../../utils/file.util";
|
||||
|
||||
const promiseLimit = pLimit(3);
|
||||
let errorToastShown = false;
|
||||
@@ -109,7 +110,7 @@ const EditableUpload = ({
|
||||
blob,
|
||||
{
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
name: getNormalizedFileName(file),
|
||||
},
|
||||
chunkIndex,
|
||||
chunks,
|
||||
@@ -194,7 +195,14 @@ const EditableUpload = ({
|
||||
};
|
||||
|
||||
const appendFiles = (appendingFiles: FileUpload[]) => {
|
||||
setUploadingFiles([...appendingFiles, ...uploadingFiles]);
|
||||
const combinedExisting = [...existingFiles, ...uploadingFiles];
|
||||
const filtered = filterDuplicateFiles(
|
||||
appendingFiles,
|
||||
combinedExisting,
|
||||
(normalizedName) => toast.error(t("upload.notify.duplicate-skipped", { name: normalizedName }))
|
||||
);
|
||||
if (filtered.length === 0) return;
|
||||
setUploadingFiles([...filtered, ...uploadingFiles]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -11,6 +11,27 @@ import { HoverTip } from "../core/HoverTip";
|
||||
import showTextEditorModal from "./modals/showTextEditorModal";
|
||||
import shareService from "../../services/share.service";
|
||||
|
||||
const renderFileName = (name: string) => {
|
||||
const parts = name.split("/");
|
||||
if (parts.length === 1) return name;
|
||||
const fileName = parts.pop();
|
||||
const folderPath = parts.join("/");
|
||||
return (
|
||||
<span>
|
||||
<span style={{ opacity: 0.5 }}>{folderPath}/</span>
|
||||
<span style={{ fontWeight: 600 }}>{fileName}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getFileNameOrPath = (file: FileListItem) => {
|
||||
const pathName =
|
||||
"webkitRelativePath" in file && file.webkitRelativePath
|
||||
? file.webkitRelativePath
|
||||
: file.name;
|
||||
return pathName.replace(/\\/g, "/").replace(/^\//, "");
|
||||
};
|
||||
|
||||
const FileListRow = ({
|
||||
file,
|
||||
onRemove,
|
||||
@@ -31,7 +52,8 @@ const FileListRow = ({
|
||||
const restorable = onRestore && !uploadable && !!file.deleted;
|
||||
const deleted = !uploadable && !!file.deleted;
|
||||
|
||||
const isTextFile = shareService.isShareTextFile(file.name);
|
||||
const fileNameOrPath = getFileNameOrPath(file);
|
||||
const isTextFile = shareService.isShareTextFile(fileNameOrPath);
|
||||
const editable = isTextFile && uploadable && file.uploadingProgress === 0;
|
||||
|
||||
const t = useTranslate();
|
||||
@@ -43,7 +65,7 @@ const FileListRow = ({
|
||||
textDecoration: deleted ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
<td>{file.name}</td>
|
||||
<td>{renderFileName(fileNameOrPath)}</td>
|
||||
<td>{byteToHumanSizeString(+file.size)}</td>
|
||||
<td>
|
||||
<Group position="right" spacing="xs" noWrap>
|
||||
|
||||
@@ -1,17 +1,145 @@
|
||||
import { Loader, RingProgress } from "@mantine/core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Loader, RingProgress, Text } from "@mantine/core";
|
||||
import { TbCircleCheck } from "react-icons/tb";
|
||||
import { HoverTip } from "../core/HoverTip";
|
||||
import { useIntl } from "react-intl";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
const UploadProgressIndicator = ({ progress }: { progress: number }) => {
|
||||
if (progress > 0 && progress < 100) {
|
||||
return (
|
||||
<RingProgress
|
||||
sections={[{ value: progress, color: "victoria" }]}
|
||||
thickness={3}
|
||||
size={25}
|
||||
/>
|
||||
const intl = useIntl();
|
||||
const config = useConfig();
|
||||
const progressStyle =
|
||||
config.get("appearance.uploadProgressStyle") ?? "circle";
|
||||
const startTimeRef = useRef<number | null>(null);
|
||||
const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (progress <= 0 || progress >= 100) {
|
||||
startTimeRef.current = null;
|
||||
setRemainingSeconds(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startTimeRef.current) {
|
||||
startTimeRef.current = Date.now();
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedMs = Date.now() - startTimeRef.current;
|
||||
if (elapsedMs > 500) {
|
||||
const rate = progress / 100;
|
||||
const totalMs = elapsedMs / rate;
|
||||
const remainingMs = totalMs - elapsedMs;
|
||||
setRemainingSeconds(remainingMs / 1000);
|
||||
}
|
||||
}, [progress]);
|
||||
|
||||
const formatRemainingTime = (seconds: number | null): string => {
|
||||
if (seconds === null || !isFinite(seconds) || seconds < 0) {
|
||||
return intl.formatMessage({
|
||||
id: "upload.filelist.estimating",
|
||||
});
|
||||
}
|
||||
|
||||
let timeStr = "";
|
||||
if (seconds < 60) {
|
||||
timeStr = `${Math.round(seconds)}s`;
|
||||
} else {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.round(seconds % 60);
|
||||
if (minutes < 60) {
|
||||
timeStr = `${minutes}m ${remainingSeconds}s`;
|
||||
} else {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
timeStr = `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "upload.filelist.remaining",
|
||||
},
|
||||
{ time: timeStr },
|
||||
);
|
||||
};
|
||||
|
||||
if (progress > 0 && progress < 100) {
|
||||
const tooltipLabel = formatRemainingTime(remainingSeconds);
|
||||
if (progressStyle === "circle") {
|
||||
return (
|
||||
<HoverTip label={tooltipLabel}>
|
||||
<RingProgress
|
||||
sections={[{ value: progress, color: "victoria" }]}
|
||||
thickness={3}
|
||||
size={25}
|
||||
/>
|
||||
</HoverTip>
|
||||
);
|
||||
} else if (progressStyle === "circle-percentage") {
|
||||
return (
|
||||
<HoverTip label={tooltipLabel}>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 40,
|
||||
height: 40,
|
||||
}}
|
||||
>
|
||||
<RingProgress
|
||||
sections={[{ value: progress, color: "victoria" }]}
|
||||
thickness={3}
|
||||
size={40}
|
||||
label={
|
||||
<Text size="xs" color="victoria" weight={500} align="center">
|
||||
{Math.min(Math.round(progress), 99)}%
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</HoverTip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Text size="sm" color="dimmed" style={{ whiteSpace: "nowrap" }}>
|
||||
{Math.min(Math.round(progress), 99)}% • {tooltipLabel}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
} else if (progress >= 100) {
|
||||
if (progressStyle === "circle-percentage") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 40,
|
||||
height: 40,
|
||||
}}
|
||||
>
|
||||
<TbCircleCheck color="green" size={35} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <TbCircleCheck color="green" size={22} />;
|
||||
} else {
|
||||
if (progressStyle === "circle-percentage") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 40,
|
||||
height: 40,
|
||||
}}
|
||||
>
|
||||
<Loader color="red" size={30} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Loader color="red" size={19} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ const showCreateUploadModal = (
|
||||
defaultAppUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
enableUserRecipients: boolean;
|
||||
maxExpiration: Timespan;
|
||||
defaultExpiration: Timespan;
|
||||
shareIdLength: number;
|
||||
@@ -121,6 +122,7 @@ const CreateUploadModalBody = ({
|
||||
defaultAppUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
enableUserRecipients: boolean;
|
||||
maxExpiration: Timespan;
|
||||
defaultExpiration: Timespan;
|
||||
shareIdLength: number;
|
||||
@@ -174,10 +176,19 @@ const CreateUploadModalBody = ({
|
||||
expiration_num: defaultTimespan.value,
|
||||
expiration_unit: `-${defaultTimespan.unit}` as string,
|
||||
never_expires: false,
|
||||
restrictToRecipients: false,
|
||||
},
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const handleRestrictToggle = (checked: boolean) => {
|
||||
form.setFieldValue("restrictToRecipients", checked);
|
||||
if (checked) {
|
||||
// A share can't be both password-protected and restricted to recipients.
|
||||
form.setFieldValue("password", undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
if (!(await shareService.isShareIdAvailable(values.link))) {
|
||||
form.setFieldError("link", t("upload.modal.link.error.taken"));
|
||||
@@ -223,8 +234,11 @@ const CreateUploadModalBody = ({
|
||||
recipients: values.recipients,
|
||||
description: values.description,
|
||||
security: {
|
||||
password: values.password || undefined,
|
||||
password: values.restrictToRecipients
|
||||
? undefined
|
||||
: values.password || undefined,
|
||||
maxViews: values.maxViews || undefined,
|
||||
restrictToRecipients: values.restrictToRecipients || undefined,
|
||||
},
|
||||
},
|
||||
files,
|
||||
@@ -417,9 +431,18 @@ const CreateUploadModalBody = ({
|
||||
return undefined;
|
||||
}
|
||||
form.setFieldError("recipients", null);
|
||||
const newRecipients = form.values.recipients.includes(
|
||||
query,
|
||||
)
|
||||
? form.values.recipients
|
||||
: [...form.values.recipients, query];
|
||||
form.setFieldValue("recipients", newRecipients);
|
||||
return query;
|
||||
}}
|
||||
{...form.getInputProps("recipients")}
|
||||
onChange={(value: string[]) => {
|
||||
form.setFieldValue("recipients", value);
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" || e.key === "," || e.key === ";") {
|
||||
e.preventDefault();
|
||||
@@ -428,10 +451,11 @@ const CreateUploadModalBody = ({
|
||||
inputValue.match(/^\S+@\S+\.\S+$/) &&
|
||||
!form.values.recipients.includes(inputValue)
|
||||
) {
|
||||
form.setFieldValue("recipients", [
|
||||
const newRecipients = [
|
||||
...form.values.recipients,
|
||||
inputValue,
|
||||
]);
|
||||
];
|
||||
form.setFieldValue("recipients", newRecipients);
|
||||
}
|
||||
setEmailSearch("");
|
||||
} else if (e.key === " ") {
|
||||
@@ -440,6 +464,18 @@ const CreateUploadModalBody = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{options.enableUserRecipients && (
|
||||
<Checkbox
|
||||
mt="sm"
|
||||
label={t(
|
||||
"upload.modal.accordion.email.restrict-to-recipients",
|
||||
)}
|
||||
checked={form.values.restrictToRecipients}
|
||||
onChange={(e) =>
|
||||
handleRestrictToggle(e.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
@@ -450,15 +486,19 @@ const CreateUploadModalBody = ({
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack align="stretch">
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.security.password.placeholder",
|
||||
)}
|
||||
label={t("upload.modal.accordion.security.password.label")}
|
||||
autoComplete="new-password"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
{!form.values.restrictToRecipients && (
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.security.password.placeholder",
|
||||
)}
|
||||
label={t(
|
||||
"upload.modal.accordion.security.password.label",
|
||||
)}
|
||||
autoComplete="new-password"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
)}
|
||||
<NumberInput
|
||||
min={1}
|
||||
type="number"
|
||||
|
||||
@@ -7,6 +7,7 @@ export default {
|
||||
|
||||
"navbar.links.shares": "My shares",
|
||||
"navbar.links.reverse": "Reverse shares",
|
||||
"navbar.links.received": "Received shares",
|
||||
|
||||
"navbar.avatar.account": "My account",
|
||||
"navbar.avatar.admin": "Administration",
|
||||
@@ -174,6 +175,9 @@ export default {
|
||||
"account.shares.table.createdAt": "Created on",
|
||||
"account.shares.table.size": "Size",
|
||||
"account.shares.table.password-protected": "Password protected",
|
||||
"account.shares.table.recipients": "Recipients",
|
||||
"account.shares.table.restricted-to-recipients": "Restricted to recipients only",
|
||||
"account.shares.table.shared-with-recipients": "Shared with recipients",
|
||||
"account.shares.table.visitor-count": "{count} of {max}",
|
||||
"account.shares.table.expiry-never": "Never",
|
||||
|
||||
@@ -189,6 +193,15 @@ export default {
|
||||
|
||||
// END /account/shares
|
||||
|
||||
// /account/received
|
||||
"account.received-shares.title": "Received shares",
|
||||
"account.received-shares.title.empty": "No shares received yet",
|
||||
"account.received-shares.description.empty":
|
||||
"Shares sent to your email address will appear here.",
|
||||
"account.received-shares.table.from": "From",
|
||||
"account.received-shares.button.open": "Open",
|
||||
// END /account/received
|
||||
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Reverse shares",
|
||||
"account.reverseShares.description":
|
||||
@@ -321,6 +334,7 @@ export default {
|
||||
"upload.notify.generic-error":
|
||||
"An error occurred while finishing your share.",
|
||||
"upload.notify.count-failed": "{count} files failed to upload. Trying again.",
|
||||
"upload.notify.duplicate-skipped": "Skipped duplicate file: {name}",
|
||||
"upload.reverse-share.error.invalid.title": "Invalid reverse share link",
|
||||
"upload.reverse-share.error.invalid.description":
|
||||
"This link has no remaining uses or is invalid.",
|
||||
@@ -328,13 +342,18 @@ export default {
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Upload files",
|
||||
"upload.dropzone.description":
|
||||
"Drag'n'drop files here to start your share or 'Ctrl+V' to upload text content from the clipboard. We only accept files up to {maxSize} in total.",
|
||||
"Drag'n'drop files or folders here to start your share or 'Ctrl+V' to upload text content from the clipboard. We only accept files up to {maxSize} in total.",
|
||||
"upload.dropzone.notify.file-too-big":
|
||||
"Your files exceed the maximum share size of {maxSize}.",
|
||||
"upload.button.folder": "Upload folder",
|
||||
"upload.button.folder.append": "Append folder",
|
||||
"upload.button.add": "Add to upload",
|
||||
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Name",
|
||||
"upload.filelist.size": "Size",
|
||||
"upload.filelist.estimating": "Estimating...",
|
||||
"upload.filelist.remaining": "{time} remaining",
|
||||
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Create Share",
|
||||
@@ -373,6 +392,8 @@ export default {
|
||||
"upload.modal.accordion.email.title": "Email recipients",
|
||||
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||
"upload.modal.accordion.email.restrict-to-recipients":
|
||||
"Restrict access to these recipients only (they must sign in to access it)",
|
||||
|
||||
"upload.modal.accordion.security.title": "Security options",
|
||||
"upload.modal.accordion.security.password.label": "Password protection",
|
||||
@@ -406,6 +427,10 @@ export default {
|
||||
"share.error.access-denied.title": "Private share",
|
||||
"share.error.access-denied.description":
|
||||
"The current account does not have permission to access this share",
|
||||
"share.error.restricted.title": "Restricted share",
|
||||
"share.error.restricted.description":
|
||||
"This share is restricted to specific recipients. Please log in to access it.",
|
||||
"share.error.restricted.button": "Log in",
|
||||
|
||||
"share.modal.password.title": "Password required",
|
||||
"share.modal.password.description":
|
||||
@@ -484,6 +509,12 @@ export default {
|
||||
"admin.config.appearance.custom-css": "Custom CSS",
|
||||
"admin.config.appearance.custom-css.description":
|
||||
"Global CSS applied to the frontend. Use carefully, as invalid CSS may affect the UI.",
|
||||
"admin.config.appearance.upload-progress-style": "Upload progress style",
|
||||
"admin.config.appearance.upload-progress-style.description":
|
||||
"Choose how upload progress is displayed in the file list.",
|
||||
"admin.config.appearance.upload-progress-style.circle": "Circle indicator",
|
||||
"admin.config.appearance.upload-progress-style.circle-percentage": "Circle with percentage",
|
||||
"admin.config.appearance.upload-progress-style.percentage-time": "Percentage and time remaining",
|
||||
"admin.config.general.app-url": "App URL",
|
||||
"admin.config.general.app-url.description":
|
||||
"On which URL Pingvin Share is available",
|
||||
@@ -557,6 +588,10 @@ export default {
|
||||
"admin.config.email.invite-message": "Invite message",
|
||||
"admin.config.email.invite-message.description":
|
||||
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL, {email} with the email and {password} with the users password.",
|
||||
"admin.config.email.share-recipients-reply-to-creator":
|
||||
"Set Reply-To to creator's email",
|
||||
"admin.config.email.share-recipients-reply-to-creator.description":
|
||||
"Whether to set the Reply-To header to the email address of the user who created the share.",
|
||||
"admin.config.email.enable-share-download-notifications":
|
||||
"Enable download notifications",
|
||||
"admin.config.email.enable-share-download-notifications.description":
|
||||
@@ -612,6 +647,9 @@ export default {
|
||||
"Force reverse shares to be created in simple mode. If disabled, the creator of the reverse share can choose between simple and advanced mode.",
|
||||
"admin.config.share.allow-admin-access-all-shares.description":
|
||||
"Allow administrators to access all shares, even if they are password protected, expired or deleted.",
|
||||
"admin.config.share.enable-user-recipients": "Enable sharing with registered users",
|
||||
"admin.config.share.enable-user-recipients.description":
|
||||
"When enabled, shares sent to a registered user's email address will automatically appear in their account. Users can also restrict share access to named recipients only.",
|
||||
"admin.config.share.file-retention-period": "File retention period",
|
||||
"admin.config.share.file-retention-period.description":
|
||||
"How long files are kept after a share expires or gets deleted. Only useful if the 'Allow admin access to all shares' is also enabled. Set to -1 to keep files forever.",
|
||||
|
||||
@@ -218,7 +218,9 @@ function App({ Component, pageProps }: AppProps) {
|
||||
if (!pageProps.language) return;
|
||||
const cookieLanguage = getCookie("language");
|
||||
if (!cookieLanguage) {
|
||||
i18nUtil.setLanguageCookie(pageProps.language);
|
||||
if (!pageProps.isConfigFallback) {
|
||||
i18nUtil.setLanguageCookie(pageProps.language);
|
||||
}
|
||||
} else if (pageProps.language !== cookieLanguage) {
|
||||
location.reload();
|
||||
}
|
||||
@@ -227,7 +229,7 @@ function App({ Component, pageProps }: AppProps) {
|
||||
|
||||
document.documentElement.dir = current.direction ?? "ltr";
|
||||
document.documentElement.lang = current.code;
|
||||
}, [pageProps.language]);
|
||||
}, [pageProps.language, pageProps.isConfigFallback]);
|
||||
|
||||
useEffect(() => {
|
||||
const userColorPreference = userPreferences.get("colorScheme");
|
||||
@@ -333,6 +335,7 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
route?: string;
|
||||
colorScheme: ColorScheme;
|
||||
language?: string;
|
||||
isConfigFallback?: boolean;
|
||||
} = {
|
||||
route: ctx.resolvedUrl,
|
||||
colorScheme:
|
||||
@@ -357,6 +360,7 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
).data;
|
||||
} catch (e) {
|
||||
pageProps.configVariables = getDefaultConfig();
|
||||
pageProps.isConfigFallback = true;
|
||||
}
|
||||
|
||||
pageProps.route = ctx.req.url;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Space,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import moment from "moment";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import CenterLoader from "../../components/core/CenterLoader";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
|
||||
const ReceivedShares = () => {
|
||||
const t = useTranslate();
|
||||
const router = useRouter();
|
||||
const config = useConfig();
|
||||
const [receivedShares, setReceivedShares] = useState<any[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.get("share.enableUserRecipients")) {
|
||||
router.replace("/");
|
||||
return;
|
||||
}
|
||||
shareService.getReceivedShares().then((data) => setReceivedShares(data));
|
||||
}, []);
|
||||
|
||||
if (!receivedShares) return <CenterLoader />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("account.received-shares.title")} />
|
||||
<Title mb={30} order={3}>
|
||||
<FormattedMessage id="account.received-shares.title" />
|
||||
</Title>
|
||||
{receivedShares.length === 0 ? (
|
||||
<Center style={{ height: "70vh" }}>
|
||||
<Stack align="center" spacing={10}>
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="account.received-shares.title.empty" />
|
||||
</Title>
|
||||
<Text>
|
||||
<FormattedMessage id="account.received-shares.description.empty" />
|
||||
</Text>
|
||||
<Space h={5} />
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<Box sx={{ display: "block", overflowX: "auto" }}>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.id" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.name" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.received-shares.table.from" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.expiresAt" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{receivedShares.map(({ share }) => (
|
||||
<tr key={share.id}>
|
||||
<td>{share.id}</td>
|
||||
<td>{share.name}</td>
|
||||
<td>{share.creator?.username ?? "—"}</td>
|
||||
<td>
|
||||
{moment(share.expiration).unix() === 0 ? (
|
||||
<FormattedMessage id="account.shares.table.expiry-never" />
|
||||
) : (
|
||||
moment(share.expiration).format("LLL")
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<Button
|
||||
component={Link}
|
||||
href={`/share/${share.id}`}
|
||||
variant="light"
|
||||
size="xs"
|
||||
color="victoria"
|
||||
>
|
||||
<FormattedMessage id="account.received-shares.button.open" />
|
||||
</Button>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReceivedShares;
|
||||
@@ -77,7 +77,9 @@ const MyShares = () => {
|
||||
showCreateReverseShareModal(
|
||||
modals,
|
||||
config.get("smtp.enabled"),
|
||||
config.get("share.maxExpiration"),
|
||||
user?.isAdmin
|
||||
? { value: 0, unit: "days" }
|
||||
: config.get("share.maxExpiration"),
|
||||
config.get("share.defaultExpiration"),
|
||||
config.get("share.reverseShareSimpleOnly"),
|
||||
appUrl,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
useMantineTheme,
|
||||
Group,
|
||||
Space,
|
||||
Stack,
|
||||
@@ -21,7 +22,9 @@ import {
|
||||
TbLink,
|
||||
TbLock,
|
||||
TbTrash,
|
||||
TbUsers,
|
||||
} from "react-icons/tb";
|
||||
import { FaUserLock } from "react-icons/fa";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import showShareInformationsModal from "../../components/share/showShareInformationsModal";
|
||||
@@ -29,6 +32,7 @@ import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||
import { HoverTip } from "../../components/core/HoverTip";
|
||||
import CenterLoader from "../../components/core/CenterLoader";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { MyShare } from "../../types/share.type";
|
||||
@@ -38,7 +42,9 @@ const MyShares = () => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const config = useConfig();
|
||||
const { user } = useUser();
|
||||
const t = useTranslate();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const [shares, setShares] = useState<MyShare[]>();
|
||||
|
||||
@@ -80,6 +86,7 @@ const MyShares = () => {
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.name" />
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.visitors" />
|
||||
</th>
|
||||
@@ -90,16 +97,46 @@ const MyShares = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{shares.map((share) => (
|
||||
{shares.map((share) => {
|
||||
return (
|
||||
<tr key={share.id}>
|
||||
<td>
|
||||
<Group spacing="xs">
|
||||
{share.id}{" "}
|
||||
{share.security?.passwordProtected && (
|
||||
<TbLock
|
||||
color="orange"
|
||||
title={t("account.shares.table.password-protected")}
|
||||
/>
|
||||
<HoverTip label="Password Protected">
|
||||
<span style={{ display: "inline-flex" }}>
|
||||
<TbLock
|
||||
color="orange"
|
||||
title={t("account.shares.table.password-protected")}
|
||||
/>
|
||||
</span>
|
||||
</HoverTip>
|
||||
)}
|
||||
{config.get("share.enableUserRecipients") && (
|
||||
share.security?.restrictToRecipients ? (
|
||||
<HoverTip label="Recipients Only">
|
||||
<span style={{ display: "inline-flex" }}>
|
||||
<FaUserLock
|
||||
color={theme.colors.gray[6]}
|
||||
title={t(
|
||||
"account.shares.table.restricted-to-recipients",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</HoverTip>
|
||||
) : share.recipients?.length ? (
|
||||
<HoverTip label="Sent to Recipients">
|
||||
<span style={{ display: "inline-flex" }}>
|
||||
<TbUsers
|
||||
color={theme.colors.gray[6]}
|
||||
title={t(
|
||||
"account.shares.table.shared-with-recipients",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</HoverTip>
|
||||
) : null
|
||||
)}
|
||||
</Group>
|
||||
</td>
|
||||
@@ -145,7 +182,9 @@ const MyShares = () => {
|
||||
parseInt(config.get("share.maxSize")),
|
||||
config.get("general.appUrl"),
|
||||
config.get("general.appUrl", true),
|
||||
config.get("share.maxExpiration"),
|
||||
user?.isAdmin
|
||||
? { value: 0, unit: "days" }
|
||||
: config.get("share.maxExpiration"),
|
||||
(updatedShare) =>
|
||||
setShares(
|
||||
shares.map((item) =>
|
||||
@@ -221,7 +260,8 @@ const MyShares = () => {
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
@@ -33,11 +33,8 @@ const Intro = () => {
|
||||
</Anchor>{" "}
|
||||
</Text>
|
||||
<Text>
|
||||
You can also support development via {" "}
|
||||
<Anchor
|
||||
target="_blank"
|
||||
href="https://github.com/sponsors/smp46"
|
||||
>
|
||||
You can also support development via{" "}
|
||||
<Anchor target="_blank" href="https://github.com/sponsors/smp46">
|
||||
GitHub Sponsors ❤️
|
||||
</Anchor>{" "}
|
||||
</Text>
|
||||
|
||||
@@ -49,27 +49,21 @@ export default function VerifyAccount() {
|
||||
{status === "success" && (
|
||||
<>
|
||||
<Text align="center">
|
||||
<FormattedMessage
|
||||
id="verify.success"
|
||||
/>
|
||||
<FormattedMessage id="verify.success" />
|
||||
</Text>
|
||||
<Button
|
||||
fullWidth
|
||||
mt="xl"
|
||||
onClick={() => router.replace("/auth/signIn")}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="verify.button.signin"
|
||||
/>
|
||||
<FormattedMessage id="verify.button.signin" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{status === "error" && (
|
||||
<>
|
||||
<Text align="center" color="red">
|
||||
<FormattedMessage
|
||||
id="verify.error"
|
||||
/>
|
||||
<FormattedMessage id="verify.error" />
|
||||
</Text>
|
||||
<Button
|
||||
fullWidth
|
||||
|
||||
@@ -32,16 +32,12 @@ export default function VerificationInfo() {
|
||||
<Meta title={t("verify.info.title")} />
|
||||
<Container size={420} my={40}>
|
||||
<Title order={2} align="center" weight={900}>
|
||||
<FormattedMessage
|
||||
id="verify.info.title"
|
||||
/>
|
||||
<FormattedMessage id="verify.info.title" />
|
||||
</Title>
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<Stack align="center">
|
||||
<Text align="center">
|
||||
<FormattedMessage
|
||||
id="verify.info.description"
|
||||
/>
|
||||
<FormattedMessage id="verify.info.description" />
|
||||
</Text>
|
||||
{email && (
|
||||
<Text weight={700} size="sm">
|
||||
@@ -49,9 +45,7 @@ export default function VerificationInfo() {
|
||||
</Text>
|
||||
)}
|
||||
<Text align="center" size="sm" color="dimmed">
|
||||
<FormattedMessage
|
||||
id="verify.info.note"
|
||||
/>
|
||||
<FormattedMessage id="verify.info.note" />
|
||||
</Text>
|
||||
<Stack w="100%" mt="xl">
|
||||
<Button
|
||||
@@ -61,14 +55,10 @@ export default function VerificationInfo() {
|
||||
disabled={!email}
|
||||
fullWidth
|
||||
>
|
||||
<FormattedMessage
|
||||
id="verify.info.resend.button"
|
||||
/>
|
||||
<FormattedMessage id="verify.info.resend.button" />
|
||||
</Button>
|
||||
<Button fullWidth onClick={() => router.replace("/auth/signIn")}>
|
||||
<FormattedMessage
|
||||
id="verify.button.signin"
|
||||
/>
|
||||
<FormattedMessage id="verify.button.signin" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { ActionIcon, Box, Group, Text, Title } from "@mantine/core";
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
@@ -32,6 +41,7 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
const modals = useModals();
|
||||
const router = useRouter();
|
||||
const [share, setShare] = useState<ShareType>();
|
||||
const [isRestricted, setIsRestricted] = useState(false);
|
||||
const { user } = useUser();
|
||||
const config = useConfig();
|
||||
const t = useTranslate();
|
||||
@@ -53,7 +63,9 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
parseInt(config.get("share.maxSize")),
|
||||
config.get("general.appUrl"),
|
||||
config.get("general.appUrl", true),
|
||||
config.get("share.maxExpiration"),
|
||||
user?.isAdmin
|
||||
? { value: 0, unit: "days" }
|
||||
: config.get("share.maxExpiration"),
|
||||
(updatedShare: MyShare) => {
|
||||
setShare((prev) =>
|
||||
prev
|
||||
@@ -124,6 +136,11 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
"go-home",
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
e.response.status == 403 &&
|
||||
error == "share_restricted_to_recipients"
|
||||
) {
|
||||
setIsRestricted(true);
|
||||
} else if (e.response.status == 403 && error == "private_share") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
@@ -149,6 +166,27 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
getFiles();
|
||||
}, []);
|
||||
|
||||
if (isRestricted) {
|
||||
return (
|
||||
<Center style={{ height: "70vh" }}>
|
||||
<Stack align="center" spacing="md">
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="share.error.restricted.title" />
|
||||
</Title>
|
||||
<Text color="dimmed" align="center">
|
||||
<FormattedMessage id="share.error.restricted.description" />
|
||||
</Text>
|
||||
<Button
|
||||
component={Link}
|
||||
href={`/auth/signIn?redirect=/share/${shareId}`}
|
||||
>
|
||||
<FormattedMessage id="share.error.restricted.button" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
|
||||
@@ -19,6 +19,10 @@ import { FileUpload } from "../../types/File.type";
|
||||
import { CreateShare, Share } from "../../types/share.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
getNormalizedFileName,
|
||||
filterDuplicateFiles,
|
||||
} from "../../utils/file.util";
|
||||
|
||||
const promiseLimit = pLimit(3);
|
||||
let errorToastShown = false;
|
||||
@@ -109,10 +113,23 @@ const Upload = ({
|
||||
blob,
|
||||
{
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
name: getNormalizedFileName(file),
|
||||
},
|
||||
chunkIndex,
|
||||
chunks,
|
||||
(progressEvent) => {
|
||||
if (progressEvent.total && file.size > 0) {
|
||||
const chunkProgress =
|
||||
progressEvent.loaded / progressEvent.total;
|
||||
const uploadedBytesBeforeThisChunk =
|
||||
chunkIndex * chunkSize.current;
|
||||
const uploadedBytesInThisChunk = blob.size * chunkProgress;
|
||||
const totalUploaded =
|
||||
uploadedBytesBeforeThisChunk + uploadedBytesInThisChunk;
|
||||
const overallPercent = (totalUploaded / file.size) * 100;
|
||||
setFileProgress(Math.min(overallPercent, 99.9));
|
||||
}
|
||||
},
|
||||
)
|
||||
.then((response) => {
|
||||
fileId = response.id;
|
||||
@@ -155,7 +172,10 @@ const Upload = ({
|
||||
"share.allowUnauthenticatedShares",
|
||||
),
|
||||
enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
|
||||
maxExpiration: config.get("share.maxExpiration"),
|
||||
enableUserRecipients: config.get("share.enableUserRecipients"),
|
||||
maxExpiration: user?.isAdmin
|
||||
? { value: 0, unit: "days" }
|
||||
: config.get("share.maxExpiration"),
|
||||
defaultExpiration: config.get("share.defaultExpiration"),
|
||||
shareIdLength: config.get("share.shareIdLength"),
|
||||
simplified,
|
||||
@@ -165,12 +185,19 @@ const Upload = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleDropzoneFilesChanged = (files: FileUpload[]) => {
|
||||
const handleDropzoneFilesChanged = (newFiles: FileUpload[]) => {
|
||||
const filtered = filterDuplicateFiles(newFiles, files, (normalizedName) =>
|
||||
toast.error(
|
||||
t("upload.notify.duplicate-skipped", { name: normalizedName }),
|
||||
),
|
||||
);
|
||||
if (filtered.length === 0) return;
|
||||
|
||||
if (autoOpenCreateUploadModal) {
|
||||
setFiles(files);
|
||||
showCreateUploadModalCallback(files);
|
||||
setFiles(filtered);
|
||||
showCreateUploadModalCallback(filtered);
|
||||
} else {
|
||||
setFiles((oldArr) => [...oldArr, ...files]);
|
||||
setFiles((oldArr) => [...oldArr, ...filtered]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -205,11 +232,21 @@ const Upload = ({
|
||||
const fileUpload = file as FileUpload;
|
||||
fileUpload.uploadingProgress = 0;
|
||||
|
||||
const filtered = filterDuplicateFiles(
|
||||
[fileUpload],
|
||||
files,
|
||||
(normalizedName) =>
|
||||
toast.error(
|
||||
t("upload.notify.duplicate-skipped", { name: normalizedName }),
|
||||
),
|
||||
);
|
||||
if (filtered.length === 0) return;
|
||||
|
||||
if (autoOpenCreateUploadModal) {
|
||||
setFiles([fileUpload]);
|
||||
showCreateUploadModalCallback([fileUpload]);
|
||||
setFiles(filtered);
|
||||
showCreateUploadModalCallback(filtered);
|
||||
} else {
|
||||
setFiles((oldArr) => [...oldArr, fileUpload]);
|
||||
setFiles((oldArr) => [...oldArr, ...filtered]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,6 +73,10 @@ const getMyShares = async (): Promise<MyShare[]> => {
|
||||
return (await api.get("shares")).data;
|
||||
};
|
||||
|
||||
const getReceivedShares = async (): Promise<any[]> => {
|
||||
return (await api.get("shares/received")).data;
|
||||
};
|
||||
|
||||
const getShareToken = async (id: string, password?: string) => {
|
||||
if (!isValidId(id)) throw new Error("Invalid ID");
|
||||
await api.post(`/shares/${id}/token`, { password });
|
||||
@@ -132,6 +136,7 @@ const uploadFile = async (
|
||||
},
|
||||
chunkIndex: number,
|
||||
totalChunks: number,
|
||||
onUploadProgress?: (progressEvent: any) => void,
|
||||
): Promise<FileUploadResponse> => {
|
||||
if (!isValidId(shareId)) throw new Error("Invalid Share ID");
|
||||
return (
|
||||
@@ -143,6 +148,7 @@ const uploadFile = async (
|
||||
chunkIndex,
|
||||
totalChunks,
|
||||
},
|
||||
onUploadProgress,
|
||||
})
|
||||
).data;
|
||||
};
|
||||
@@ -199,6 +205,7 @@ export default {
|
||||
doesFileSupportPreview,
|
||||
isShareTextFile,
|
||||
getMyShares,
|
||||
getReceivedShares,
|
||||
isShareIdAvailable,
|
||||
downloadFile,
|
||||
removeFile,
|
||||
|
||||
@@ -49,7 +49,8 @@ export type ShareMetaData = {
|
||||
export type MyShare = Omit<Share, "hasPassword"> & {
|
||||
views: number;
|
||||
createdAt: Date;
|
||||
security?: MyShareSecurity;
|
||||
recipients: string[];
|
||||
security: MyShareSecurity;
|
||||
};
|
||||
|
||||
export type MyReverseShare = {
|
||||
@@ -64,9 +65,11 @@ export type MyReverseShare = {
|
||||
export type ShareSecurity = {
|
||||
maxViews?: number;
|
||||
password?: string;
|
||||
restrictToRecipients?: boolean;
|
||||
};
|
||||
|
||||
export type MyShareSecurity = {
|
||||
passwordProtected: boolean;
|
||||
maxViews?: number;
|
||||
restrictToRecipients: boolean;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
export const getNormalizedFileName = (file: File): string => {
|
||||
const pathName = file.webkitRelativePath || file.name;
|
||||
return pathName.replace(/\\/g, "/").replace(/^\//, "");
|
||||
};
|
||||
|
||||
export const filterDuplicateFiles = <T extends File>(
|
||||
newFiles: T[],
|
||||
existingFilesList: Array<{ name: string; webkitRelativePath?: string; deleted?: boolean }>,
|
||||
onDuplicateDetected: (name: string) => void
|
||||
): T[] => {
|
||||
const existingNames = new Set(
|
||||
existingFilesList
|
||||
.filter((file) => !file.deleted)
|
||||
.map((file) => {
|
||||
const pathName = file.webkitRelativePath || file.name;
|
||||
return pathName.replace(/\\/g, "/").replace(/^\//, "");
|
||||
})
|
||||
);
|
||||
|
||||
const filtered: T[] = [];
|
||||
const seenInBatch = new Set<string>();
|
||||
|
||||
for (const file of newFiles) {
|
||||
const normalizedName = getNormalizedFileName(file);
|
||||
if (existingNames.has(normalizedName) || seenInBatch.has(normalizedName)) {
|
||||
onDuplicateDetected(normalizedName);
|
||||
} else {
|
||||
seenInBatch.add(normalizedName);
|
||||
filtered.push(file);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pingvin-share-x",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share-x",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"devDependencies": {
|
||||
"conventional-changelog": "^7.2.0",
|
||||
"conventional-changelog-conventionalcommits": "^9.3.1"
|
||||
|
||||
+3
-1
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"name": "pingvin-share-x",
|
||||
"version": "1.21.1",
|
||||
"version": "1.21.2",
|
||||
"scripts": {
|
||||
"format": "cd frontend && npm run format && cd ../backend && npm run format",
|
||||
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",
|
||||
"version": "conventional-changelog -p conventionalcommits -i CHANGELOG.md -o CHANGELOG.md && git add CHANGELOG.md",
|
||||
"release:patch": "cd backend && npm version patch --commit-hooks false && cd ../frontend && npm version patch --commit-hooks false && cd .. && git add . && npm version patch --force -m 'release: %s' && git push && git push --tags",
|
||||
"release:minor": "cd backend && npm version minor --commit-hooks false && cd ../frontend && npm version minor --commit-hooks false && cd .. && git add . && npm version minor --force -m 'release: %s' && git push && git push --tags",
|
||||
"release:beta:patch": "cd backend && npm version prerelease --preid beta --commit-hooks false && cd ../frontend && npm version prerelease --preid beta --commit-hooks false && cd .. && git add . && npm version prerelease --preid beta --force -m 'release: %s' && git push && git push --tags",
|
||||
"release:beta:minor": "cd backend && npm version preminor --preid beta --commit-hooks false && cd ../frontend && npm version preminor --preid beta --commit-hooks false && cd .. && git add . && npm version preminor --preid beta --force -m 'release: %s' && git push && git push --tags",
|
||||
"deploy:dev": "docker buildx build --push --tag smp46/pingvin-share-x:development --platform linux/amd64,linux/arm64 ."
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
Reference in New Issue
Block a user