Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a4c3bf58f | ||
|
|
64efac5b68 | ||
|
|
8c5c696c51 | ||
|
|
01da83cdf6 | ||
|
|
cfcc5cebac | ||
|
|
b96878b6b1 | ||
|
|
9c381a2ed6 | ||
|
|
4f9b4f38f6 | ||
|
|
c98b237259 | ||
|
|
17d593a794 | ||
|
|
ac580b79b4 | ||
|
|
4186a768b3 | ||
|
|
4924f76394 | ||
|
|
f1f514dff7 | ||
|
|
94e2a6110d | ||
|
|
7716f5c0ce | ||
|
|
0a963bfaf1 | ||
|
|
472c93d548 | ||
|
|
93aacca9b4 | ||
|
|
3505669135 | ||
|
|
fe735f9704 | ||
|
|
3563715f57 | ||
|
|
14c2185e6f | ||
|
|
27ee9fb6cb | ||
|
|
601772d2f4 | ||
|
|
0e66be5f08 | ||
|
|
4cabcfb715 | ||
|
|
e5e9d85d39 | ||
|
|
70fd2d94be | ||
|
|
e5a0c649e3 | ||
|
|
414bcecbb5 | ||
|
|
968352cb6c | ||
|
|
355f860387 | ||
|
|
083d82c28b | ||
|
|
046c630abf | ||
|
|
d2bfb9a55f | ||
|
|
fccf57e9e4 | ||
|
|
e1a68f75f7 | ||
|
|
9d9cc7b4ab | ||
|
|
d1cde75a66 | ||
|
|
bbc81d8dd0 | ||
|
|
0cdc04bfb5 | ||
|
|
367f804a49 | ||
|
|
9193a79b9a | ||
|
|
31366d961f | ||
|
|
2dac38560b | ||
|
|
db2720ab7b | ||
|
|
6d6b9e81ff | ||
|
|
f9ddd7bacd | ||
|
|
3773432eb5 | ||
|
|
46783ce463 | ||
|
|
c0cc16fa43 | ||
|
|
4fd29037a0 | ||
|
|
1c7832ad1f | ||
|
|
962ec27df4 | ||
|
|
9268e35141 | ||
|
|
e8be0d60e6 | ||
|
|
0eabf78f13 | ||
|
|
4136bf5778 | ||
|
|
42b3604e2a | ||
|
|
84f4c39c1e | ||
|
|
bfef246d98 | ||
|
|
3b89fb950a | ||
|
|
7afda85f03 | ||
|
|
a3a7a5d9ab | ||
|
|
74cd520cb8 | ||
|
|
a511f24a6b | ||
|
|
b3862f3f3e | ||
|
|
d147614f76 | ||
|
|
c999df15e0 | ||
|
|
908d6e298f | ||
|
|
44c4a2e269 | ||
|
|
dc060f258b | ||
|
|
3b1c9f1efb | ||
|
|
a45184995f | ||
|
|
b717663b5c | ||
|
|
0e12ba87bc | ||
|
|
ec1feadee9 | ||
|
|
2e0d8d4fed | ||
|
|
b7f0f9d3ee | ||
|
|
c303454db3 | ||
|
|
3972589f76 | ||
|
|
3c5e0ad513 | ||
|
|
384fd19203 | ||
|
|
9d1a12b0d1 | ||
|
|
24e100bd7b | ||
|
|
1da4feeb89 | ||
|
|
c0a245e11b | ||
|
|
7a15fbb465 | ||
|
|
0bfbaea49a | ||
|
|
82871ce5dc | ||
|
|
593a65dac1 | ||
|
|
92ee1ab527 | ||
|
|
e71f6cd159 | ||
|
|
0b07bfbc14 | ||
|
|
63842cd0cc | ||
|
|
9f686c6ee3 | ||
|
|
c6d8188e4e | ||
|
|
6d87e20e29 | ||
|
|
b8efb9f54b | ||
|
|
013b9886af | ||
|
|
43bff91db2 | ||
|
|
1aa3d8e5e8 | ||
|
|
4dae7e250a | ||
|
|
7e91d83f9a | ||
|
|
e11dbfe893 | ||
|
|
ea83cf3876 |
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💬 Discord
|
||||
url: https://discord.gg/wHRQ9nFRcK
|
||||
about: For help and chatting with the community
|
||||
17
.github/ISSUE_TEMPLATE/question.yml
vendored
17
.github/ISSUE_TEMPLATE/question.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: ❓ Question
|
||||
description: "Submit a question"
|
||||
title: "❓ Question:"
|
||||
labels: [question]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "🙋♂️ Question"
|
||||
description: "A clear question. Please provide as much detail as possible."
|
||||
placeholder: "How do I ...?"
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before submitting, please check if the question hasn't been asked before.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -38,6 +38,9 @@ yarn-error.log*
|
||||
# project specific
|
||||
/backend/data/
|
||||
/data/
|
||||
/docs/build/
|
||||
/docs/.docusaurus
|
||||
/docs/.cache-loader
|
||||
|
||||
# Jetbrains specific (webstorm)
|
||||
.idea/**/**
|
||||
|
||||
143
CHANGELOG.md
143
CHANGELOG.md
@@ -1,3 +1,146 @@
|
||||
## [1.0.1](https://github.com/stonith404/pingvin-share/compare/v1.0.0...v1.0.1) (2024-08-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **email:** add {email} placeholder to user invitation email ([#564](https://github.com/stonith404/pingvin-share/issues/564)) ([8c5c696](https://github.com/stonith404/pingvin-share/commit/8c5c696c514a5fb450462184240b21553d7f1532))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **translations:** add missing string for ldap group ([64efac5](https://github.com/stonith404/pingvin-share/commit/64efac5b685bf2de9d65c6a4f8890d45afe6476d))
|
||||
|
||||
## [1.0.0](https://github.com/stonith404/pingvin-share/compare/v0.29.0...v1.0.0) (2024-08-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ldap:** Adding support for LDAP authentication ([#554](https://github.com/stonith404/pingvin-share/issues/554)) ([4186a76](https://github.com/stonith404/pingvin-share/commit/4186a768b310855282bc4876d1f294700963b8f5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* get started button on home page not working when sign-up is disabled ([4924f76](https://github.com/stonith404/pingvin-share/commit/4924f763947c9a6b79ba0d85887f104ed9545c78))
|
||||
* internal server error if user has no password when trying to sign in ([9c381a2](https://github.com/stonith404/pingvin-share/commit/9c381a2ed6b3b7dfd95d4278889b937ffb85e01b))
|
||||
|
||||
## [0.29.0](https://github.com/stonith404/pingvin-share/compare/v0.28.0...v0.29.0) (2024-07-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add more options to reverse shares ([#495](https://github.com/stonith404/pingvin-share/issues/495)) ([fe735f9](https://github.com/stonith404/pingvin-share/commit/fe735f9704c9d96398f3127a559e17848b08d140)), closes [#155](https://github.com/stonith404/pingvin-share/issues/155)
|
||||
* sort share files by name by default ([27ee9fb](https://github.com/stonith404/pingvin-share/commit/27ee9fb6cb98177661bed20a0baa399b27e70b7e))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "fix: set max age of access token cookie to 15 minutes" ([14c2185](https://github.com/stonith404/pingvin-share/commit/14c2185e6f1a81d63e25fbeec3e30a54cf6a44c5))
|
||||
|
||||
## [0.28.0](https://github.com/stonith404/pingvin-share/compare/v0.27.0...v0.28.0) (2024-07-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Add role-based access management from OpenID Connect ([#535](https://github.com/stonith404/pingvin-share/issues/535)) ([70fd2d9](https://github.com/stonith404/pingvin-share/commit/70fd2d94be3411cc430f5c56e522028398127efb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* store only 10 share tokens in the cookies and clear the expired ones ([e5a0c64](https://github.com/stonith404/pingvin-share/commit/e5a0c649e36e0db419d04446affe2564c45cf321))
|
||||
|
||||
## [0.27.0](https://github.com/stonith404/pingvin-share/compare/v0.26.0...v0.27.0) (2024-07-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add logs for successful registration, successful login and failed login ([d2bfb9a](https://github.com/stonith404/pingvin-share/commit/d2bfb9a55fdad6a05377b8552471cf1151304c90))
|
||||
* **auth:** Allow to hide username / password login form when OAuth is enabled ([#518](https://github.com/stonith404/pingvin-share/issues/518)) ([e1a68f7](https://github.com/stonith404/pingvin-share/commit/e1a68f75f7b034f1ef9e45f26de584f13e355589)), closes [#489](https://github.com/stonith404/pingvin-share/issues/489)
|
||||
* **smtp:** allow unauthorized mail server certificates ([#525](https://github.com/stonith404/pingvin-share/issues/525)) ([083d82c](https://github.com/stonith404/pingvin-share/commit/083d82c28b835c178f076e89ef8f5885e8ea31cb))
|
||||
|
||||
## [0.26.0](https://github.com/stonith404/pingvin-share/compare/v0.25.0...v0.26.0) (2024-07-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **backend:** Make session duration configurable ([#512](https://github.com/stonith404/pingvin-share/issues/512)) ([367f804](https://github.com/stonith404/pingvin-share/commit/367f804a494c85b4caf879d51982339fb6b86ba1)), closes [#507](https://github.com/stonith404/pingvin-share/issues/507)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **oauth:** provider username is ignored when signing up using OAuth ([#511](https://github.com/stonith404/pingvin-share/issues/511)) ([31366d9](https://github.com/stonith404/pingvin-share/commit/31366d961f5827c200038b65ec9de5d4ddc8b898)), closes [#505](https://github.com/stonith404/pingvin-share/issues/505)
|
||||
* set max age of access token cookie to 15 minutes ([2dac385](https://github.com/stonith404/pingvin-share/commit/2dac38560b6c54b6e7676dcd4682bfa57973292f))
|
||||
|
||||
## [0.25.0](https://github.com/stonith404/pingvin-share/compare/v0.24.2...v0.25.0) (2024-06-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add auto open share modal config for global. ([#474](https://github.com/stonith404/pingvin-share/issues/474)) ([4fd2903](https://github.com/stonith404/pingvin-share/commit/4fd29037a08dbe505bdd8cf20f6f114cbade8483))
|
||||
* **frontend:** locale for dates and tooltip for copy link button ([#492](https://github.com/stonith404/pingvin-share/issues/492)) ([1c7832a](https://github.com/stonith404/pingvin-share/commit/1c7832ad1fb445fd1dbe1c111be5a331eaa4b797))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* share size not displayed on my shares page ([c0cc16f](https://github.com/stonith404/pingvin-share/commit/c0cc16fa430bc64afb024c19d5faf24456bd417c))
|
||||
|
||||
## [0.24.2](https://github.com/stonith404/pingvin-share/compare/v0.24.1...v0.24.2) (2024-05-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* admin couldn't delete shares created by anonymous users ([7afda85](https://github.com/stonith404/pingvin-share/commit/7afda85f03d410a6c611860d0c3fb2b88a2e3679))
|
||||
* whitespace in title on homepage ([74cd520](https://github.com/stonith404/pingvin-share/commit/74cd520cb8c4ea87822ab6d54c0bf010455f401b))
|
||||
|
||||
## [0.24.1](https://github.com/stonith404/pingvin-share/compare/v0.24.0...v0.24.1) (2024-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* error on admin share management page if a share was created by an anonymous user ([c999df1](https://github.com/stonith404/pingvin-share/commit/c999df15e04a927f6e952db3c807b9591fb14894))
|
||||
|
||||
## [0.24.0](https://github.com/stonith404/pingvin-share/compare/v0.23.1...v0.24.0) (2024-05-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add admin-exclusive share-management page ([#461](https://github.com/stonith404/pingvin-share/issues/461)) ([3b1c9f1](https://github.com/stonith404/pingvin-share/commit/3b1c9f1efb7d02469e92537a2d1378b6cb412878))
|
||||
* add name property to share ([#462](https://github.com/stonith404/pingvin-share/issues/462)) ([b717663](https://github.com/stonith404/pingvin-share/commit/b717663b5c3a4a98e361e7e39b680f4852537c59))
|
||||
|
||||
## [0.23.1](https://github.com/stonith404/pingvin-share/compare/v0.23.0...v0.23.1) (2024-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** crash on unhandled promise rejections ([1da4fee](https://github.com/stonith404/pingvin-share/commit/1da4feeb895a13d0a0ae754bd716a84e8186d081))
|
||||
* changing the chunk size needed an app restart ([24e100b](https://github.com/stonith404/pingvin-share/commit/24e100bd7be8bf20778bdf2767aa35cae8d7e502))
|
||||
* disable js execution on raw file view ([9d1a12b](https://github.com/stonith404/pingvin-share/commit/9d1a12b0d1812214f1fe6fa56e3848091ce4945c))
|
||||
* incorrect layout on 404 page ([3c5e0ad](https://github.com/stonith404/pingvin-share/commit/3c5e0ad5134ee2d405ac420152b5825102f65bfc))
|
||||
* normal shares were added to the previous reverse share ([3972589](https://github.com/stonith404/pingvin-share/commit/3972589f76519b03074d916fb2460c795b1f0737))
|
||||
* redirect vulnerability on error, sign in and totp page ([384fd19](https://github.com/stonith404/pingvin-share/commit/384fd19203b63eeb4b952f83a9e1eaab1b19b90d))
|
||||
|
||||
## [0.23.0](https://github.com/stonith404/pingvin-share/compare/v0.22.2...v0.23.0) (2024-04-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add config variable to adjust chunk size ([0bfbaea](https://github.com/stonith404/pingvin-share/commit/0bfbaea49aad0c695fee6558c89c661687912e4f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* delete share files if user gets deleted ([e71f6cd](https://github.com/stonith404/pingvin-share/commit/e71f6cd1598ed87366074398042a6b88675587ca))
|
||||
* error in logs if "allow unauthenticated shares" is enabled ([c6d8188](https://github.com/stonith404/pingvin-share/commit/c6d8188e4e33ba682551a3ca79205ff5a6d7ead5))
|
||||
* memory leak while uploading files by disabling base64 encoding of chunks ([7a15fbb](https://github.com/stonith404/pingvin-share/commit/7a15fbb4651c2fee32fb4c1ee2c9d7f12323feb0))
|
||||
|
||||
## [0.22.2](https://github.com/stonith404/pingvin-share/compare/v0.22.1...v0.22.2) (2024-02-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* extend access token cookie expiration ([013b988](https://github.com/stonith404/pingvin-share/commit/013b9886af5629b2ead6000b962267afc761c612))
|
||||
* reduce refresh access token calls ([1aa3d8e](https://github.com/stonith404/pingvin-share/commit/1aa3d8e5e89b3696cc9554f41e9ce13806dde406))
|
||||
* replace Nginx with Caddy to fix "premature close" error while downloading larger files ([43bff91](https://github.com/stonith404/pingvin-share/commit/43bff91db2ba4ec68d76e601f7bc42cb7a506bc5))
|
||||
|
||||
## [0.22.1](https://github.com/stonith404/pingvin-share/compare/v0.22.0...v0.22.1) (2024-02-18)
|
||||
|
||||
|
||||
|
||||
15
Caddyfile
Normal file
15
Caddyfile
Normal file
@@ -0,0 +1,15 @@
|
||||
:3000 {
|
||||
# Reverse proxy for /api
|
||||
reverse_proxy /api/* http://localhost:8080 {
|
||||
header_up X-Forwarded-Host {host}:{server_port}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
|
||||
# Reverse proxy for all other requests
|
||||
reverse_proxy http://localhost:3333 {
|
||||
header_up X-Forwarded-Host {host}:{server_port}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
17
Dockerfile
17
Dockerfile
@@ -30,12 +30,9 @@ RUN npm run build && npm prune --production
|
||||
FROM node:20-alpine AS runner
|
||||
ENV NODE_ENV=docker
|
||||
|
||||
# Alpine specific dependencies
|
||||
RUN apk update --no-cache
|
||||
RUN apk upgrade --no-cache
|
||||
RUN apk add --no-cache curl nginx
|
||||
|
||||
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
RUN apk update --no-cache \
|
||||
&& apk upgrade --no-cache \
|
||||
&& apk add --no-cache curl caddy
|
||||
|
||||
WORKDIR /opt/app/frontend
|
||||
COPY --from=frontend-builder /opt/app/public ./public
|
||||
@@ -49,13 +46,13 @@ COPY --from=backend-builder /opt/app/dist ./dist
|
||||
COPY --from=backend-builder /opt/app/prisma ./prisma
|
||||
COPY --from=backend-builder /opt/app/package.json ./
|
||||
|
||||
COPY ./Caddyfile /etc/caddy/Caddyfile
|
||||
COPY ./scripts/docker-entrypoint.sh /opt/app/docker-entrypoint.sh
|
||||
|
||||
WORKDIR /opt/app
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Add a health check to ensure the container is healthy
|
||||
HEALTHCHECK --interval=10s --timeout=3s CMD curl -f http://localhost:3000/api/health || exit 1
|
||||
|
||||
# Application startup
|
||||
# HOSTNAME=0.0.0.0 fixes https://github.com/vercel/next.js/issues/51684. It can be removed as soon as the issue is fixed
|
||||
CMD cp -rn /tmp/img/* /opt/app/frontend/public/img && nginx && PORT=3333 HOSTNAME=0.0.0.0 node frontend/server.js & cd backend && npm run prod
|
||||
CMD ["sh", "/opt/app/docker-entrypoint.sh"]
|
||||
137
README.md
137
README.md
@@ -1,8 +1,6 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_Read this in another language: [Spanish](/docs/README.es.md), [English](/README.md), [Simplified Chinese](/docs/README.zh-cn.md), [日本語](/docs/README.ja-jp.md)_
|
||||
[](https://discord.gg/wHRQ9nFRcK) [](https://crowdin.com/project/pingvin-share) [](https://github.com/sponsors/stonith404)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,140 +24,13 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran
|
||||
|
||||
## ⌨️ Setup
|
||||
|
||||
> Note: Pingvin Share is in its early stages and may contain bugs.
|
||||
|
||||
### Installation with Docker (recommended)
|
||||
|
||||
1. Download the `docker-compose.yml` file
|
||||
2. Run `docker-compose up -d`
|
||||
2. Run `docker compose up -d`
|
||||
|
||||
The website is now listening on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
||||
|
||||
### Stand-alone Installation
|
||||
## 📚 Documentation
|
||||
|
||||
Required tools:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) for running Pingvin Share in the background
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stonith404/pingvin-share
|
||||
cd pingvin-share
|
||||
|
||||
# Checkout the latest version
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Start the backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
# Start the frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
API_URL=http://localhost:8080 # Set the URL of the backend, default: http://localhost:8080
|
||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||
```
|
||||
|
||||
**Uploading Large Files**: By default, Pingvin Share uses a built-in reverse proxy to reduce the installation steps. However, this reverse proxy is not optimized for uploading large files. If you wish to upload larger files, you can either use the Docker installation or set up your own reverse proxy. An example configuration for Nginx can be found in `/nginx/nginx.conf`.
|
||||
|
||||
The website is now listening on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
||||
|
||||
### Integrations
|
||||
|
||||
#### ClamAV (Docker only)
|
||||
|
||||
ClamAV is used to scan shares for malicious files and remove them if found.
|
||||
|
||||
1. Add the ClamAV container to the Docker Compose stack (see `docker-compose.yml`) and start the container.
|
||||
2. Docker will wait for ClamAV to start before starting Pingvin Share. This may take a minute or two.
|
||||
3. The Pingvin Share logs should now log "ClamAV is active"
|
||||
|
||||
Please note that ClamAV needs a lot of [ressources](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements).
|
||||
|
||||
#### OAuth 2 Login
|
||||
|
||||
View the [OAuth 2 guide](/docs/oauth2-guide.md) for more information.
|
||||
|
||||
### Additional resources
|
||||
|
||||
- [Synology NAS installation](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
### Upgrade to a new version
|
||||
|
||||
As Pingvin Share is in early stage, see the release notes for breaking changes before upgrading.
|
||||
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Stand-alone
|
||||
|
||||
1. Stop the running app
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. Repeat the steps from the [installation guide](#stand-alone-installation) except the `git clone` step.
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# Checkout the latest version
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Start the backend
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
# Start the frontend
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
You can customize Pingvin Share by going to the configuration page in your admin dashboard.
|
||||
|
||||
#### Environment variables
|
||||
|
||||
For installation specific configuration, you can use environment variables. The following variables are available:
|
||||
|
||||
##### Backend
|
||||
|
||||
| Variable | Default Value | Description |
|
||||
| ---------------- | -------------------------------------------------- | -------------------------------------- |
|
||||
| `PORT` | `8080` | The port on which the backend listens. |
|
||||
| `DATABASE_URL` | `file:../data/pingvin-share.db?connection_limit=1` | The URL of the SQLite database. |
|
||||
| `DATA_DIRECTORY` | `./data` | The directory where data is stored. |
|
||||
| `CLAMAV_HOST` | `127.0.0.1` | The IP address of the ClamAV server. |
|
||||
| `CLAMAV_PORT` | `3310` | The port number of the ClamAV server. |
|
||||
|
||||
##### Frontend
|
||||
|
||||
| Variable | Default Value | Description |
|
||||
| --------- | ----------------------- | ---------------------------------------- |
|
||||
| `PORT` | `3000` | The port on which the frontend listens. |
|
||||
| `API_URL` | `http://localhost:8080` | The URL of the backend for the frontend. |
|
||||
|
||||
## 🖤 Contribute
|
||||
|
||||
### Translations
|
||||
|
||||
You can help to translate Pingvin Share into your language.
|
||||
On [Crowdin](https://crowdin.com/project/pingvin-share) you can easily translate Pingvin Share online.
|
||||
|
||||
Is your language not on Crowdin? Feel free to [Request it](https://github.com/stonith404/pingvin-share/issues/new?assignees=&labels=language-request&projects=&template=language-request.yml&title=%F0%9F%8C%90+Language+request%3A+%3Clanguage+name+in+english%3E).
|
||||
|
||||
Any issues while translating? Feel free to participate in the [Localization discussion](https://github.com/stonith404/pingvin-share/discussions/198).
|
||||
|
||||
### Project
|
||||
|
||||
You're very welcome to contribute to Pingvin Share! Please follow the [contribution guide](/CONTRIBUTING.md) to get started.
|
||||
For more installation options and advanced configurations, please refer to the [documentation](https://stonith404.github.io/pingvin-share).
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
As Pingvin Share is in beta, older versions don't get security updates. Please consider to update Pingvin Share regularly. Updates can be automated with e.g [Watchtower](https://github.com/containrrr/watchtower).
|
||||
|
||||
Older versions of Pingvin Share do not receive security updates. To ensure your system remains secure, we strongly recommend updating Pingvin Share regularly. You can automate these updates using tools like [Watchtower](https://github.com/containrrr/watchtower).
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Thank you for taking the time to report a vulnerability. Please DO NOT create an issue on GitHub because the vulnerability could get exploited. Instead please write an email to [elias@eliasschneider.com](mailto:elias@eliasschneider.com).
|
||||
|
||||
10938
backend/package-lock.json
generated
10938
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.22.1",
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "cross-env NODE_ENV=development nest start --watch",
|
||||
@@ -13,73 +13,75 @@
|
||||
"seed": "ts-node prisma/seed/config.seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^2.1.0",
|
||||
"@nestjs/common": "^10.1.2",
|
||||
"@nestjs/config": "^3.0.0",
|
||||
"@nestjs/core": "^10.1.2",
|
||||
"@nestjs/jwt": "^10.1.0",
|
||||
"@nestjs/passport": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.1.2",
|
||||
"@nestjs/schedule": "^3.0.1",
|
||||
"@nestjs/swagger": "^7.1.4",
|
||||
"@nestjs/throttler": "^4.2.1",
|
||||
"@prisma/client": "^5.0.0",
|
||||
"archiver": "^5.3.1",
|
||||
"argon2": "^0.30.3",
|
||||
"@nestjs/cache-manager": "^2.2.2",
|
||||
"@nestjs/common": "^10.3.9",
|
||||
"@nestjs/config": "^3.2.2",
|
||||
"@nestjs/core": "^10.3.9",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.3.9",
|
||||
"@nestjs/schedule": "^4.0.2",
|
||||
"@nestjs/swagger": "^7.3.1",
|
||||
"@nestjs/throttler": "^5.2.0",
|
||||
"@prisma/client": "^5.16.1",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/ldapjs": "^3.0.6",
|
||||
"archiver": "^7.0.1",
|
||||
"argon2": "^0.40.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"cache-manager": "^5.2.4",
|
||||
"clamscan": "^2.1.2",
|
||||
"cache-manager": "^5.6.1",
|
||||
"clamscan": "^2.2.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"class-validator": "^0.14.1",
|
||||
"content-disposition": "^0.5.4",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"jmespath": "^0.16.0",
|
||||
"ldapjs": "^3.0.7",
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-fetch": "^2.7.0",
|
||||
"nodemailer": "^6.9.4",
|
||||
"moment": "^2.30.1",
|
||||
"nanoid": "^3.3.7",
|
||||
"nodemailer": "^6.9.14",
|
||||
"otplib": "^12.0.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"qrcode-svg": "^1.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^5.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rimraf": "^5.0.7",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.32.4",
|
||||
"ts-node": "^10.9.1"
|
||||
"sharp": "^0.33.4",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.1.10",
|
||||
"@nestjs/schematics": "^10.0.1",
|
||||
"@nestjs/testing": "^10.1.2",
|
||||
"@types/archiver": "^5.3.2",
|
||||
"@types/clamscan": "^2.0.4",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@nestjs/cli": "^10.3.2",
|
||||
"@nestjs/schematics": "^10.1.1",
|
||||
"@nestjs/testing": "^10.3.9",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/clamscan": "^2.0.8",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/cron": "^2.0.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/node-fetch": "^2.6.6",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/passport-jwt": "^3.0.9",
|
||||
"@types/qrcode-svg": "^1.1.1",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/qrcode-svg": "^1.1.4",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||
"@typescript-eslint/parser": "^6.2.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.14.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"newman": "^5.3.2",
|
||||
"prettier": "^3.0.0",
|
||||
"prisma": "^5.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"newman": "^6.1.3",
|
||||
"prettier": "^3.3.2",
|
||||
"prisma": "^5.16.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wait-on": "^7.0.1"
|
||||
"typescript": "^5.5.2",
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Share" ADD COLUMN "name" TEXT;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_ReverseShare" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"token" TEXT NOT NULL,
|
||||
"shareExpiration" DATETIME NOT NULL,
|
||||
"maxShareSize" TEXT NOT NULL,
|
||||
"sendEmailNotification" BOOLEAN NOT NULL,
|
||||
"remainingUses" INTEGER NOT NULL,
|
||||
"simplified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
CONSTRAINT "ReverseShare_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_ReverseShare" ("createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "token") SELECT "createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "token" FROM "ReverseShare";
|
||||
DROP TABLE "ReverseShare";
|
||||
ALTER TABLE "new_ReverseShare" RENAME TO "ReverseShare";
|
||||
CREATE UNIQUE INDEX "ReverseShare_token_key" ON "ReverseShare"("token");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_ReverseShare" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"token" TEXT NOT NULL,
|
||||
"shareExpiration" DATETIME NOT NULL,
|
||||
"maxShareSize" TEXT NOT NULL,
|
||||
"sendEmailNotification" BOOLEAN NOT NULL,
|
||||
"remainingUses" INTEGER NOT NULL,
|
||||
"simplified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"publicAccess" BOOLEAN NOT NULL DEFAULT true,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
CONSTRAINT "ReverseShare_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_ReverseShare" ("createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "simplified", "token") SELECT "createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "simplified", "token" FROM "ReverseShare";
|
||||
DROP TABLE "ReverseShare";
|
||||
ALTER TABLE "new_ReverseShare" RENAME TO "ReverseShare";
|
||||
CREATE UNIQUE INDEX "ReverseShare_token_key" ON "ReverseShare"("token");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[ldapDN]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "ldapDN" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_ldapDN_key" ON "User"("ldapDN");
|
||||
@@ -16,6 +16,7 @@ model User {
|
||||
email String @unique
|
||||
password String?
|
||||
isAdmin Boolean @default(false)
|
||||
ldapDN String? @unique
|
||||
|
||||
shares Share[]
|
||||
refreshTokens RefreshToken[]
|
||||
@@ -75,6 +76,7 @@ model Share {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
name String?
|
||||
uploadLocked Boolean @default(false)
|
||||
isZipReady Boolean @default(false)
|
||||
views Int @default(0)
|
||||
@@ -102,6 +104,8 @@ model ReverseShare {
|
||||
maxShareSize String
|
||||
sendEmailNotification Boolean
|
||||
remainingUses Int
|
||||
simplified Boolean @default(false)
|
||||
publicAccess Boolean @default(true)
|
||||
|
||||
creatorId String
|
||||
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -25,6 +25,11 @@ const configVariables: ConfigVariables = {
|
||||
defaultValue: "true",
|
||||
secret: false,
|
||||
},
|
||||
sessionDuration: {
|
||||
type: "number",
|
||||
defaultValue: "2160",
|
||||
secret: false,
|
||||
},
|
||||
},
|
||||
share: {
|
||||
allowRegistration: {
|
||||
@@ -51,12 +56,21 @@ const configVariables: ConfigVariables = {
|
||||
type: "number",
|
||||
defaultValue: "9",
|
||||
},
|
||||
chunkSize: {
|
||||
type: "number",
|
||||
defaultValue: "10000000",
|
||||
secret: false,
|
||||
},
|
||||
autoOpenShareModal: {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
},
|
||||
email: {
|
||||
enableShareEmailRecipients: {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
|
||||
secret: false,
|
||||
},
|
||||
shareRecipientsSubject: {
|
||||
@@ -93,7 +107,7 @@ const configVariables: ConfigVariables = {
|
||||
inviteMessage: {
|
||||
type: "text",
|
||||
defaultValue:
|
||||
"Hey!\n\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\n\nYour password is: {password}\n\nPingvin Share 🐧",
|
||||
'Hey!\n\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\n\nYou can use the email "{email}" and the password "{password}" to sign in.\n\nPingvin Share 🐧',
|
||||
},
|
||||
},
|
||||
smtp: {
|
||||
@@ -102,6 +116,12 @@ const configVariables: ConfigVariables = {
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
allowUnauthorizedCertificates: {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
|
||||
secret: false,
|
||||
},
|
||||
host: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
@@ -124,6 +144,42 @@ const configVariables: ConfigVariables = {
|
||||
obscured: true,
|
||||
},
|
||||
},
|
||||
ldap: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
|
||||
url: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
|
||||
bindDn: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
bindPassword: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
|
||||
searchBase: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
searchQuery: {
|
||||
type: "string",
|
||||
defaultValue: ""
|
||||
},
|
||||
|
||||
adminGroups: {
|
||||
type: "string",
|
||||
defaultValue: ""
|
||||
}
|
||||
},
|
||||
oauth: {
|
||||
"allowRegistration": {
|
||||
type: "boolean",
|
||||
@@ -133,6 +189,11 @@ const configVariables: ConfigVariables = {
|
||||
type: "boolean",
|
||||
defaultValue: "true",
|
||||
},
|
||||
"disablePassword": {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
"github-enabled": {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
@@ -205,6 +266,18 @@ const configVariables: ConfigVariables = {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"oidc-rolePath": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"oidc-roleGeneralAccess": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"oidc-roleAdminAccess": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"oidc-clientId": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
@@ -214,7 +287,7 @@ const configVariables: ConfigVariables = {
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
type ConfigVariables = {
|
||||
@@ -266,12 +339,15 @@ async function seedConfigVariables() {
|
||||
|
||||
async function migrateConfigVariables() {
|
||||
const existingConfigVariables = await prisma.config.findMany();
|
||||
const orderMap: { [category: string]: number } = {};
|
||||
|
||||
for (const existingConfigVariable of existingConfigVariables) {
|
||||
const configVariable =
|
||||
configVariables[existingConfigVariable.category]?.[
|
||||
existingConfigVariable.name
|
||||
];
|
||||
|
||||
// Delete the config variable if it doesn't exist in the seed
|
||||
if (!configVariable) {
|
||||
await prisma.config.delete({
|
||||
where: {
|
||||
@@ -282,15 +358,11 @@ async function migrateConfigVariables() {
|
||||
},
|
||||
});
|
||||
|
||||
// Update the config variable if the metadata changed
|
||||
} else if (
|
||||
JSON.stringify({
|
||||
...configVariable,
|
||||
name: existingConfigVariable.name,
|
||||
category: existingConfigVariable.category,
|
||||
value: existingConfigVariable.value,
|
||||
}) != JSON.stringify(existingConfigVariable)
|
||||
) {
|
||||
// Update the config variable if it exists in the seed
|
||||
} else {
|
||||
const variableOrder = Object.keys(
|
||||
configVariables[existingConfigVariable.category]
|
||||
).indexOf(existingConfigVariable.name);
|
||||
await prisma.config.update({
|
||||
where: {
|
||||
name_category: {
|
||||
@@ -303,8 +375,10 @@ async function migrateConfigVariables() {
|
||||
name: existingConfigVariable.name,
|
||||
category: existingConfigVariable.category,
|
||||
value: existingConfigVariable.value,
|
||||
order: variableOrder,
|
||||
},
|
||||
});
|
||||
orderMap[existingConfigVariable.category] = variableOrder + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@ import { Module } from "@nestjs/common";
|
||||
import { ScheduleModule } from "@nestjs/schedule";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
|
||||
import { CacheModule } from "@nestjs/cache-manager";
|
||||
import { APP_GUARD } from "@nestjs/core";
|
||||
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { AppController } from "./app.controller";
|
||||
import { ClamScanModule } from "./clamscan/clamscan.module";
|
||||
import { ConfigModule } from "./config/config.module";
|
||||
import { EmailModule } from "./email/email.module";
|
||||
import { FileModule } from "./file/file.module";
|
||||
import { JobsModule } from "./jobs/jobs.module";
|
||||
import { OAuthModule } from "./oauth/oauth.module";
|
||||
import { PrismaModule } from "./prisma/prisma.module";
|
||||
import { ReverseShareModule } from "./reverseShare/reverseShare.module";
|
||||
import { ShareModule } from "./share/share.module";
|
||||
import { UserModule } from "./user/user.module";
|
||||
import { ClamScanModule } from "./clamscan/clamscan.module";
|
||||
import { ReverseShareModule } from "./reverseShare/reverseShare.module";
|
||||
import { AppController } from "./app.controller";
|
||||
import { OAuthModule } from "./oauth/oauth.module";
|
||||
import { CacheModule } from "@nestjs/cache-manager";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,10 +28,12 @@ import { CacheModule } from "@nestjs/cache-manager";
|
||||
ConfigModule,
|
||||
JobsModule,
|
||||
UserModule,
|
||||
ThrottlerModule.forRoot({
|
||||
ttl: 60,
|
||||
limit: 100,
|
||||
}),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60,
|
||||
limit: 100,
|
||||
},
|
||||
]),
|
||||
ScheduleModule.forRoot(),
|
||||
ClamScanModule,
|
||||
ReverseShareModule,
|
||||
|
||||
@@ -37,15 +37,21 @@ export class AuthController {
|
||||
) {}
|
||||
|
||||
@Post("signUp")
|
||||
@Throttle(10, 5 * 60)
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 20,
|
||||
ttl: 5 * 60,
|
||||
},
|
||||
})
|
||||
async signUp(
|
||||
@Body() dto: AuthRegisterDTO,
|
||||
@Req() { ip }: Request,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
if (!this.config.get("share.allowRegistration"))
|
||||
throw new ForbiddenException("Registration is not allowed");
|
||||
|
||||
const result = await this.authService.signUp(dto);
|
||||
const result = await this.authService.signUp(dto, ip);
|
||||
|
||||
this.authService.addTokensToResponse(
|
||||
response,
|
||||
@@ -57,13 +63,19 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post("signIn")
|
||||
@Throttle(10, 5 * 60)
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 20,
|
||||
ttl: 5 * 60,
|
||||
},
|
||||
})
|
||||
@HttpCode(200)
|
||||
async signIn(
|
||||
@Body() dto: AuthSignInDTO,
|
||||
@Req() { ip }: Request,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
const result = await this.authService.signIn(dto);
|
||||
const result = await this.authService.signIn(dto, ip);
|
||||
|
||||
if (result.accessToken && result.refreshToken) {
|
||||
this.authService.addTokensToResponse(
|
||||
@@ -77,7 +89,12 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post("signIn/totp")
|
||||
@Throttle(10, 5 * 60)
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 20,
|
||||
ttl: 5 * 60,
|
||||
},
|
||||
})
|
||||
@HttpCode(200)
|
||||
async signInTotp(
|
||||
@Body() dto: AuthSignInTotpDTO,
|
||||
@@ -95,14 +112,24 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post("resetPassword/:email")
|
||||
@Throttle(5, 5 * 60)
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 20,
|
||||
ttl: 5 * 60,
|
||||
},
|
||||
})
|
||||
@HttpCode(202)
|
||||
async requestResetPassword(@Param("email") email: string) {
|
||||
this.authService.requestResetPassword(email);
|
||||
}
|
||||
|
||||
@Post("resetPassword")
|
||||
@Throttle(5, 5 * 60)
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 20,
|
||||
ttl: 5 * 60,
|
||||
},
|
||||
})
|
||||
@HttpCode(204)
|
||||
async resetPassword(@Body() dto: ResetPasswordDTO) {
|
||||
return await this.authService.resetPassword(dto.token, dto.password);
|
||||
|
||||
@@ -5,6 +5,8 @@ import { AuthController } from "./auth.controller";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { AuthTotpService } from "./authTotp.service";
|
||||
import { JwtStrategy } from "./strategy/jwt.strategy";
|
||||
import { LdapService } from "./ldap.service";
|
||||
import { UserModule } from "../user/user.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -12,9 +14,10 @@ import { JwtStrategy } from "./strategy/jwt.strategy";
|
||||
global: true,
|
||||
}),
|
||||
EmailModule,
|
||||
UserModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, AuthTotpService, JwtStrategy],
|
||||
providers: [AuthService, AuthTotpService, JwtStrategy, LdapService],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
export class AuthModule { }
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
Logger,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
@@ -15,6 +16,9 @@ import { EmailService } from "src/email/email.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
||||
import { AuthSignInDTO } from "./dto/authSignIn.dto";
|
||||
import { LdapService } from "./ldap.service";
|
||||
import { inspect } from "util";
|
||||
import { UserSevice } from "../user/user.service";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -23,9 +27,12 @@ export class AuthService {
|
||||
private jwtService: JwtService,
|
||||
private config: ConfigService,
|
||||
private emailService: EmailService,
|
||||
) {}
|
||||
private ldapService: LdapService,
|
||||
private userService: UserSevice,
|
||||
) { }
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
|
||||
async signUp(dto: AuthRegisterDTO) {
|
||||
async signUp(dto: AuthRegisterDTO, ip: string, isAdmin?: boolean) {
|
||||
const isFirstUser = (await this.prisma.user.count()) == 0;
|
||||
|
||||
const hash = dto.password ? await argon.hash(dto.password) : null;
|
||||
@@ -35,7 +42,7 @@ export class AuthService {
|
||||
email: dto.email,
|
||||
username: dto.username,
|
||||
password: hash,
|
||||
isAdmin: isFirstUser,
|
||||
isAdmin: isAdmin ?? isFirstUser,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,6 +51,7 @@ export class AuthService {
|
||||
);
|
||||
const accessToken = await this.createAccessToken(user, refreshTokenId);
|
||||
|
||||
this.logger.log(`User ${user.email} signed up from IP ${ip}`);
|
||||
return { accessToken, refreshToken, user };
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
@@ -57,20 +65,37 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async signIn(dto: AuthSignInDTO) {
|
||||
async signIn(dto: AuthSignInDTO, ip: string) {
|
||||
if (!dto.email && !dto.username)
|
||||
throw new BadRequestException("Email or username is required");
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email: dto.email }, { username: dto.username }],
|
||||
},
|
||||
});
|
||||
if (!this.config.get("oauth.disablePassword")) {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email: dto.email }, { username: dto.username }],
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !(await argon.verify(user.password, dto.password)))
|
||||
throw new UnauthorizedException("Wrong email or password");
|
||||
if (user?.password && await argon.verify(user.password, dto.password)) {
|
||||
this.logger.log(`Successful password login for user ${user.email} from IP ${ip}`);
|
||||
return this.generateToken(user);
|
||||
}
|
||||
}
|
||||
|
||||
return this.generateToken(user);
|
||||
if (this.config.get("ldap.enabled")) {
|
||||
this.logger.debug(`Trying LDAP login for user ${dto.username}`);
|
||||
const ldapUser = await this.ldapService.authenticateUser(dto.username, dto.password);
|
||||
if (ldapUser) {
|
||||
const user = await this.userService.findOrCreateFromLDAP(dto.username, ldapUser);
|
||||
this.logger.log(`Successful LDAP login for user ${user.email} from IP ${ip}`);
|
||||
return this.generateToken(user);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Failed login attempt for user ${dto.email || dto.username} from IP ${ip}`,
|
||||
);
|
||||
throw new UnauthorizedException("Wrong email or password");
|
||||
}
|
||||
|
||||
async generateToken(user: User, isOAuth = false) {
|
||||
@@ -94,12 +119,15 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async requestResetPassword(email: string) {
|
||||
if (this.config.get("oauth.disablePassword"))
|
||||
throw new ForbiddenException("Password sign in is disabled");
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { email },
|
||||
include: { resetPasswordToken: true },
|
||||
});
|
||||
|
||||
if (!user) throw new BadRequestException("User not found");
|
||||
if (!user) return;
|
||||
|
||||
// Delete old reset password token
|
||||
if (user.resetPasswordToken) {
|
||||
@@ -119,6 +147,9 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async resetPassword(token: string, newPassword: string) {
|
||||
if (this.config.get("oauth.disablePassword"))
|
||||
throw new ForbiddenException("Password sign in is disabled");
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { resetPasswordToken: { token } },
|
||||
});
|
||||
@@ -205,7 +236,12 @@ export class AuthService {
|
||||
|
||||
async createRefreshToken(userId: string) {
|
||||
const { id, token } = await this.prisma.refreshToken.create({
|
||||
data: { userId, expiresAt: moment().add(3, "months").toDate() },
|
||||
data: {
|
||||
userId,
|
||||
expiresAt: moment()
|
||||
.add(this.config.get("general.sessionDuration"), "hours")
|
||||
.toDate(),
|
||||
},
|
||||
});
|
||||
|
||||
return { refreshTokenId: id, refreshToken: token };
|
||||
@@ -227,13 +263,16 @@ export class AuthService {
|
||||
accessToken?: string,
|
||||
) {
|
||||
if (accessToken)
|
||||
response.cookie("access_token", accessToken, { sameSite: "lax" });
|
||||
response.cookie("access_token", accessToken, {
|
||||
sameSite: "lax",
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30 * 3, // 3 months
|
||||
});
|
||||
if (refreshToken)
|
||||
response.cookie("refresh_token", refreshToken, {
|
||||
path: "/api/auth/token",
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30 * 3,
|
||||
maxAge: 1000 * 60 * 60 * this.config.get("general.sessionDuration"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
154
backend/src/auth/ldap.service.ts
Normal file
154
backend/src/auth/ldap.service.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Inject, Injectable, Logger } from "@nestjs/common";
|
||||
import * as ldap from "ldapjs";
|
||||
import { AttributeJson, InvalidCredentialsError, SearchCallbackResponse, SearchOptions } from "ldapjs";
|
||||
import { inspect } from "node:util";
|
||||
import { ConfigService } from "../config/config.service";
|
||||
|
||||
type LdapSearchEntry = {
|
||||
objectName: string,
|
||||
attributes: AttributeJson[],
|
||||
};
|
||||
|
||||
async function ldapExecuteSearch(client: ldap.Client, base: string, options: SearchOptions): Promise<LdapSearchEntry[]> {
|
||||
const searchResponse = await new Promise<SearchCallbackResponse>((resolve, reject) => {
|
||||
client.search(base, options, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return await new Promise<any[]>((resolve, reject) => {
|
||||
const entries: LdapSearchEntry[] = [];
|
||||
searchResponse.on("searchEntry", entry => entries.push({ attributes: entry.pojo.attributes, objectName: entry.pojo.objectName }));
|
||||
searchResponse.once("error", reject);
|
||||
searchResponse.once("end", () => resolve(entries));
|
||||
});
|
||||
}
|
||||
|
||||
async function ldapBindUser(client: ldap.Client, dn: string, password: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.bind(dn, password, error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async function ldapCreateConnection(logger: Logger, url: string): Promise<ldap.Client> {
|
||||
const ldapClient = ldap.createClient({
|
||||
url: url.split(","),
|
||||
connectTimeout: 10_000,
|
||||
timeout: 10_000
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
ldapClient.once("error", reject);
|
||||
ldapClient.on("setupError", reject);
|
||||
ldapClient.on("socketTimeout", reject);
|
||||
ldapClient.on("connectRefused", () => reject(new Error("connection has been refused")));
|
||||
ldapClient.on("connectTimeout", () => reject(new Error("connect timed out")));
|
||||
ldapClient.on("connectError", reject);
|
||||
|
||||
ldapClient.on("connect", resolve);
|
||||
}).catch(error => {
|
||||
logger.error(`Connect error: ${inspect(error)}`);
|
||||
ldapClient.destroy();
|
||||
throw error;
|
||||
});
|
||||
|
||||
return ldapClient;
|
||||
}
|
||||
|
||||
export type LdapAuthenticateResult = {
|
||||
userDn: string,
|
||||
attributes: Record<string, string[]>
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class LdapService {
|
||||
private readonly logger = new Logger(LdapService.name);
|
||||
constructor(
|
||||
@Inject(ConfigService)
|
||||
private readonly serviceConfig: ConfigService,
|
||||
) { }
|
||||
|
||||
private async createLdapConnection(): Promise<ldap.Client> {
|
||||
const ldapUrl = this.serviceConfig.get("ldap.url");
|
||||
if (!ldapUrl) {
|
||||
throw new Error("LDAP server URL is not defined");
|
||||
}
|
||||
|
||||
const ldapClient = await ldapCreateConnection(this.logger, ldapUrl);
|
||||
try {
|
||||
const bindDn = this.serviceConfig.get("ldap.bindDn") || null;
|
||||
if (bindDn) {
|
||||
try {
|
||||
await ldapBindUser(ldapClient, bindDn, this.serviceConfig.get("ldap.bindPassword"))
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to bind to default user: ${error}`);
|
||||
throw new Error("failed to bind to default user");
|
||||
}
|
||||
}
|
||||
|
||||
return ldapClient;
|
||||
} catch (error) {
|
||||
ldapClient.destroy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async authenticateUser(username: string, password: string): Promise<LdapAuthenticateResult | null> {
|
||||
if (!username.match(/^[a-zA-Z0-0]+$/)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const searchBase = this.serviceConfig.get("ldap.searchBase");
|
||||
const searchQuery = this.serviceConfig.get("ldap.searchQuery")
|
||||
.replaceAll("%username%", username);
|
||||
|
||||
const ldapClient = await this.createLdapConnection();
|
||||
try {
|
||||
const [result] = await ldapExecuteSearch(ldapClient, searchBase, {
|
||||
filter: searchQuery,
|
||||
scope: "sub"
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
/* user not found */
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
await ldapBindUser(ldapClient, result.objectName, password);
|
||||
|
||||
/*
|
||||
* In theory we could query the user attributes now,
|
||||
* but as we must query the user attributes for validation anyways
|
||||
* we'll create a second ldap server connection.
|
||||
*/
|
||||
return {
|
||||
userDn: result.objectName,
|
||||
attributes: Object.fromEntries(result.attributes.map(attribute => [attribute.type, attribute.values])),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidCredentialsError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.warn(`LDAP user bind failure: ${inspect(error)}`);
|
||||
return null;
|
||||
} finally {
|
||||
ldapClient.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`LDAP connect error: ${inspect(error)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,11 @@ export class EmailService {
|
||||
user: this.config.get("smtp.username"),
|
||||
pass: this.config.get("smtp.password"),
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: !this.config.get(
|
||||
"smtp.allowUnauthorizedCertificates",
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,7 +116,8 @@ export class EmailService {
|
||||
this.config
|
||||
.get("email.inviteMessage")
|
||||
.replaceAll("{url}", loginUrl)
|
||||
.replaceAll("{password}", password),
|
||||
.replaceAll("{password}", password)
|
||||
.replaceAll("{email}", recipientEmail),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,18 +26,21 @@ export class FileController {
|
||||
@SkipThrottle()
|
||||
@UseGuards(CreateShareGuard, ShareOwnerGuard)
|
||||
async create(
|
||||
@Query() query: any,
|
||||
|
||||
@Query()
|
||||
query: {
|
||||
id: string;
|
||||
name: string;
|
||||
chunkIndex: string;
|
||||
totalChunks: string;
|
||||
},
|
||||
@Body() body: string,
|
||||
@Param("shareId") shareId: string,
|
||||
) {
|
||||
const { id, name, chunkIndex, totalChunks } = query;
|
||||
|
||||
// Data can be empty if the file is empty
|
||||
const data = body.toString().split(",")[1] ?? "";
|
||||
|
||||
return await this.fileService.create(
|
||||
data,
|
||||
body,
|
||||
{ index: parseInt(chunkIndex), total: parseInt(totalChunks) },
|
||||
{ id, name },
|
||||
shareId,
|
||||
@@ -72,6 +75,7 @@ export class FileController {
|
||||
const headers = {
|
||||
"Content-Type": file.metaData.mimeType,
|
||||
"Content-Length": file.metaData.size,
|
||||
"Content-Security-Policy": "script-src 'none'",
|
||||
};
|
||||
|
||||
if (download === "true") {
|
||||
|
||||
@@ -47,7 +47,7 @@ export class FileService {
|
||||
}
|
||||
|
||||
// If the sent chunk index and the expected chunk index doesn't match throw an error
|
||||
const chunkSize = 10 * 1024 * 1024; // 10MB
|
||||
const chunkSize = this.config.get("share.chunkSize");
|
||||
const expectedChunkIndex = Math.ceil(diskFileSize / chunkSize);
|
||||
|
||||
if (expectedChunkIndex != chunk.index)
|
||||
|
||||
@@ -9,14 +9,16 @@ import * as moment from "moment";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
|
||||
import { ShareService } from "src/share/share.service";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class FileSecurityGuard extends ShareSecurityGuard {
|
||||
constructor(
|
||||
private _shareService: ShareService,
|
||||
private _prisma: PrismaService,
|
||||
_config: ConfigService,
|
||||
) {
|
||||
super(_shareService, _prisma);
|
||||
super(_shareService, _prisma, _config);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { ClassSerializerInterceptor, ValidationPipe } from "@nestjs/common";
|
||||
import {
|
||||
ClassSerializerInterceptor,
|
||||
Logger,
|
||||
ValidationPipe,
|
||||
} from "@nestjs/common";
|
||||
import { NestFactory, Reflector } from "@nestjs/core";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
import * as bodyParser from "body-parser";
|
||||
import * as cookieParser from "cookie-parser";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import * as fs from "fs";
|
||||
import { AppModule } from "./app.module";
|
||||
import { ConfigService } from "./config/config.service";
|
||||
import { DATA_DIRECTORY } from "./constants";
|
||||
|
||||
async function bootstrap() {
|
||||
@@ -13,7 +19,16 @@ async function bootstrap() {
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
||||
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
||||
|
||||
app.use(bodyParser.raw({ type: "application/octet-stream", limit: "20mb" }));
|
||||
const config = app.get<ConfigService>(ConfigService);
|
||||
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
const chunkSize = config.get("share.chunkSize");
|
||||
bodyParser.raw({
|
||||
type: "application/octet-stream",
|
||||
limit: `${chunkSize}B`,
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
app.use(cookieParser());
|
||||
app.set("trust proxy", true);
|
||||
|
||||
@@ -34,5 +49,8 @@ async function bootstrap() {
|
||||
}
|
||||
|
||||
await app.listen(parseInt(process.env.PORT) || 8080);
|
||||
|
||||
const logger = new Logger("UnhandledAsyncError");
|
||||
process.on("unhandledRejection", (e) => logger.error(e));
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -3,4 +3,5 @@ export interface OAuthSignInDto {
|
||||
providerId: string;
|
||||
providerUsername: string;
|
||||
email: string;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export class OAuthController {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
loginToken?: string;
|
||||
} = await this.oauthService.signIn(user);
|
||||
} = await this.oauthService.signIn(user, request.ip);
|
||||
if (token.accessToken) {
|
||||
this.authService.addTokensToResponse(
|
||||
response,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { Inject, Injectable, Logger } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import { nanoid } from "nanoid";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
@@ -15,6 +15,7 @@ export class OAuthService {
|
||||
private auth: AuthService,
|
||||
@Inject("OAUTH_PLATFORMS") private platforms: string[],
|
||||
) {}
|
||||
private readonly logger = new Logger(OAuthService.name);
|
||||
|
||||
available(): string[] {
|
||||
return this.platforms
|
||||
@@ -39,21 +40,25 @@ export class OAuthService {
|
||||
return Object.fromEntries(oauthUsers.map((u) => [u.provider, u]));
|
||||
}
|
||||
|
||||
async signIn(user: OAuthSignInDto) {
|
||||
async signIn(user: OAuthSignInDto, ip: string) {
|
||||
const oauthUser = await this.prisma.oAuthUser.findFirst({
|
||||
where: {
|
||||
provider: user.provider,
|
||||
providerUserId: user.providerId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
if (oauthUser) {
|
||||
return this.auth.generateToken(oauthUser.user, true);
|
||||
await this.updateIsAdmin(user);
|
||||
const updatedUser = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
this.logger.log(`Successful login for user ${user.email} from IP ${ip}`);
|
||||
return this.auth.generateToken(updatedUser, true);
|
||||
}
|
||||
|
||||
return this.signUp(user);
|
||||
return this.signUp(user, ip);
|
||||
}
|
||||
|
||||
async link(
|
||||
@@ -102,9 +107,9 @@ export class OAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
private async getAvailableUsername(email: string) {
|
||||
// only remove + and - from email for now (maybe not enough)
|
||||
let username = email.split("@")[0].replace(/[+-]/g, "").substring(0, 20);
|
||||
private async getAvailableUsername(preferredUsername: string) {
|
||||
// only remove + and - from preferred username for now (maybe not enough)
|
||||
let username = preferredUsername.replace(/[+-]/g, "").substring(0, 20);
|
||||
while (true) {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
@@ -119,7 +124,7 @@ export class OAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
private async signUp(user: OAuthSignInDto) {
|
||||
private async signUp(user: OAuthSignInDto, ip: string) {
|
||||
// register
|
||||
if (!this.config.get("oauth.allowRegistration")) {
|
||||
throw new ErrorPageException("no_user", "/auth/signIn", [
|
||||
@@ -148,14 +153,19 @@ export class OAuthService {
|
||||
userId: existingUser.id,
|
||||
},
|
||||
});
|
||||
await this.updateIsAdmin(user);
|
||||
return this.auth.generateToken(existingUser, true);
|
||||
}
|
||||
|
||||
const result = await this.auth.signUp({
|
||||
email: user.email,
|
||||
username: await this.getAvailableUsername(user.email),
|
||||
password: null,
|
||||
});
|
||||
const result = await this.auth.signUp(
|
||||
{
|
||||
email: user.email,
|
||||
username: await this.getAvailableUsername(user.providerUsername),
|
||||
password: null,
|
||||
},
|
||||
ip,
|
||||
user.isAdmin,
|
||||
);
|
||||
|
||||
await this.prisma.oAuthUser.create({
|
||||
data: {
|
||||
@@ -168,4 +178,16 @@ export class OAuthService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async updateIsAdmin(user: OAuthSignInDto) {
|
||||
if ("isAdmin" in user)
|
||||
await this.prisma.user.update({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
data: {
|
||||
isAdmin: user.isAdmin,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import fetch from "node-fetch";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { OAuthCallbackDto } from "../dto/oauthCallback.dto";
|
||||
import { OAuthSignInDto } from "../dto/oauthSignIn.dto";
|
||||
@@ -52,7 +51,7 @@ export class DiscordProvider implements OAuthProvider<DiscordToken> {
|
||||
this.config.get("general.appUrl") + "/api/oauth/callback/discord",
|
||||
}),
|
||||
});
|
||||
const token: DiscordToken = await res.json();
|
||||
const token = (await res.json()) as DiscordToken;
|
||||
return {
|
||||
accessToken: token.access_token,
|
||||
refreshToken: token.refresh_token,
|
||||
@@ -102,10 +101,10 @@ export class DiscordProvider implements OAuthProvider<DiscordToken> {
|
||||
});
|
||||
const guilds = (await res.json()) as DiscordPartialGuild[];
|
||||
if (!guilds.some((guild) => guild.id === guildId)) {
|
||||
throw new ErrorPageException("discord_guild_permission_denied");
|
||||
throw new ErrorPageException("user_not_allowed");
|
||||
}
|
||||
} catch {
|
||||
throw new ErrorPageException("discord_guild_permission_denied");
|
||||
throw new ErrorPageException("user_not_allowed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Logger } from "@nestjs/common";
|
||||
import fetch from "node-fetch";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Cache } from "cache-manager";
|
||||
import * as jmespath from "jmespath";
|
||||
import { nanoid } from "nanoid";
|
||||
import { OAuthCallbackDto } from "../dto/oauthCallback.dto";
|
||||
import { OAuthProvider, OAuthToken } from "./oauthProvider.interface";
|
||||
@@ -25,7 +25,7 @@ export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
protected cache: Cache,
|
||||
) {
|
||||
this.discoveryUri = this.getDiscoveryUri();
|
||||
this.config.addListener("update", (key: string, _: unknown) => {
|
||||
this.config.addListener("update", (key: string) => {
|
||||
if (this.keyOfConfigUpdateEvents.includes(key)) {
|
||||
this.deinit();
|
||||
this.discoveryUri = this.getDiscoveryUri();
|
||||
@@ -94,7 +94,7 @@ export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
redirect_uri: this.getRedirectUri(),
|
||||
}).toString(),
|
||||
});
|
||||
const token: OidcToken = await res.json();
|
||||
const token = (await res.json()) as OidcToken;
|
||||
return {
|
||||
accessToken: token.access_token,
|
||||
expiresIn: token.expires_in,
|
||||
@@ -109,6 +109,11 @@ export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
token: OAuthToken<OidcToken>,
|
||||
query: OAuthCallbackDto,
|
||||
claim?: string,
|
||||
roleConfig?: {
|
||||
path?: string;
|
||||
generalAccess?: string;
|
||||
adminAccess?: string;
|
||||
},
|
||||
): Promise<OAuthSignInDto> {
|
||||
const idTokenData = this.decodeIdToken(token.idToken);
|
||||
// maybe it's not necessary to verify the id token since it's directly obtained from the provider
|
||||
@@ -125,9 +130,47 @@ export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
|
||||
const username = claim
|
||||
? idTokenData[claim]
|
||||
: idTokenData.name ||
|
||||
idTokenData.nickname ||
|
||||
idTokenData.preferred_username;
|
||||
: idTokenData.preferred_username ||
|
||||
idTokenData.name ||
|
||||
idTokenData.nickname;
|
||||
|
||||
let isAdmin: boolean;
|
||||
|
||||
if (roleConfig?.path) {
|
||||
// A path to read roles from the token is configured
|
||||
let roles: string[] | null;
|
||||
try {
|
||||
roles = jmespath.search(idTokenData, roleConfig.path);
|
||||
} catch (e) {
|
||||
roles = null;
|
||||
}
|
||||
if (Array.isArray(roles)) {
|
||||
// Roles are found in the token
|
||||
if (
|
||||
roleConfig.generalAccess &&
|
||||
!roles.includes(roleConfig.generalAccess)
|
||||
) {
|
||||
// Role for general access is configured and the user does not have it
|
||||
this.logger.error(
|
||||
`User roles ${roles} do not include ${roleConfig.generalAccess}`,
|
||||
);
|
||||
throw new ErrorPageException("user_not_allowed");
|
||||
}
|
||||
if (roleConfig.adminAccess) {
|
||||
// Role for admin access is configured
|
||||
isAdmin = roles.includes(roleConfig.adminAccess);
|
||||
}
|
||||
} else {
|
||||
this.logger.error(
|
||||
`Roles not found at path ${roleConfig.path} in ID Token ${JSON.stringify(
|
||||
idTokenData,
|
||||
undefined,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
throw new ErrorPageException("user_not_allowed");
|
||||
}
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
this.logger.error(
|
||||
@@ -147,6 +190,7 @@ export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
email: idTokenData.email,
|
||||
providerId: idTokenData.sub,
|
||||
providerUsername: username,
|
||||
...(isAdmin !== undefined && { isAdmin }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,7 +203,7 @@ export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
: Date.now() + 1000 * 60 * 60 * 24;
|
||||
this.configuration = {
|
||||
expires,
|
||||
data: await res.json(),
|
||||
data: (await res.json()) as OidcConfiguration,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import fetch from "node-fetch";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { OAuthCallbackDto } from "../dto/oauthCallback.dto";
|
||||
import { OAuthSignInDto } from "../dto/oauthSignIn.dto";
|
||||
@@ -38,7 +37,7 @@ export class GitHubProvider implements OAuthProvider<GitHubToken> {
|
||||
},
|
||||
},
|
||||
);
|
||||
const token: GitHubToken = await res.json();
|
||||
const token = (await res.json()) as GitHubToken;
|
||||
return {
|
||||
accessToken: token.access_token,
|
||||
tokenType: token.token_type,
|
||||
|
||||
@@ -34,6 +34,15 @@ export class OidcProvider extends GenericOidcProvider {
|
||||
_?: string,
|
||||
): Promise<OAuthSignInDto> {
|
||||
const claim = this.config.get("oauth.oidc-usernameClaim") || undefined;
|
||||
return super.getUserInfo(token, query, claim);
|
||||
const rolePath = this.config.get("oauth.oidc-rolePath") || undefined;
|
||||
const roleGeneralAccess =
|
||||
this.config.get("oauth.oidc-roleGeneralAccess") || undefined;
|
||||
const roleAdminAccess =
|
||||
this.config.get("oauth.oidc-roleAdminAccess") || undefined;
|
||||
return super.getUserInfo(token, query, claim, {
|
||||
path: rolePath,
|
||||
generalAccess: roleGeneralAccess,
|
||||
adminAccess: roleAdminAccess,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,10 @@ export class CreateReverseShareDTO {
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
maxUseCount: number;
|
||||
|
||||
@IsBoolean()
|
||||
simplified: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
publicAccess: boolean;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ export class ReverseShareDTO {
|
||||
@Expose()
|
||||
token: string;
|
||||
|
||||
@Expose()
|
||||
simplified: boolean;
|
||||
|
||||
from(partial: Partial<ReverseShareDTO>) {
|
||||
return plainToClass(ReverseShareDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
|
||||
@@ -13,7 +13,7 @@ export class ReverseShareTokenWithShares extends OmitType(ReverseShareDTO, [
|
||||
@Type(() => OmitType(MyShareDTO, ["recipients", "hasPassword"] as const))
|
||||
shares: Omit<
|
||||
MyShareDTO,
|
||||
"recipients" | "files" | "from" | "fromList" | "hasPassword"
|
||||
"recipients" | "files" | "from" | "fromList" | "hasPassword" | "size"
|
||||
>[];
|
||||
|
||||
@Expose()
|
||||
|
||||
@@ -36,7 +36,12 @@ export class ReverseShareController {
|
||||
return { token, link };
|
||||
}
|
||||
|
||||
@Throttle(20, 60)
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 20,
|
||||
ttl: 60,
|
||||
},
|
||||
})
|
||||
@Get(":reverseShareToken")
|
||||
async getByToken(@Param("reverseShareToken") reverseShareToken: string) {
|
||||
const isValid = await this.reverseShareService.isValid(reverseShareToken);
|
||||
|
||||
@@ -49,6 +49,8 @@ export class ReverseShareService {
|
||||
remainingUses: data.maxUseCount,
|
||||
maxShareSize: data.maxShareSize,
|
||||
sendEmailNotification: data.sendEmailNotification,
|
||||
simplified: data.simplified,
|
||||
publicAccess: data.publicAccess,
|
||||
creatorId,
|
||||
},
|
||||
});
|
||||
|
||||
27
backend/src/share/dto/adminShare.dto.ts
Normal file
27
backend/src/share/dto/adminShare.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
import { Expose, plainToClass } from "class-transformer";
|
||||
import { ShareDTO } from "./share.dto";
|
||||
|
||||
export class AdminShareDTO extends OmitType(ShareDTO, [
|
||||
"files",
|
||||
"from",
|
||||
"fromList",
|
||||
] as const) {
|
||||
@Expose()
|
||||
views: number;
|
||||
|
||||
@Expose()
|
||||
createdAt: Date;
|
||||
|
||||
from(partial: Partial<AdminShareDTO>) {
|
||||
return plainToClass(AdminShareDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
});
|
||||
}
|
||||
|
||||
fromList(partial: Partial<AdminShareDTO>[]) {
|
||||
return partial.map((part) =>
|
||||
plainToClass(AdminShareDTO, part, { excludeExtraneousValues: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ export class CreateShareDTO {
|
||||
@Length(3, 50)
|
||||
id: string;
|
||||
|
||||
@Length(3, 30)
|
||||
@IsOptional()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
expiration: string;
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ export class ShareDTO {
|
||||
@Expose()
|
||||
id: string;
|
||||
|
||||
@Expose()
|
||||
name?: string;
|
||||
|
||||
@Expose()
|
||||
expiration: Date;
|
||||
|
||||
@@ -23,6 +26,9 @@ export class ShareDTO {
|
||||
@Expose()
|
||||
hasPassword: boolean;
|
||||
|
||||
@Expose()
|
||||
size: number;
|
||||
|
||||
from(partial: Partial<ShareDTO>) {
|
||||
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
|
||||
}
|
||||
|
||||
19
backend/src/share/dto/shareComplete.dto.ts
Normal file
19
backend/src/share/dto/shareComplete.dto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Expose, plainToClass } from "class-transformer";
|
||||
import { ShareDTO } from "./share.dto";
|
||||
|
||||
export class CompletedShareDTO extends ShareDTO {
|
||||
@Expose()
|
||||
notifyReverseShareCreator?: boolean;
|
||||
|
||||
from(partial: Partial<CompletedShareDTO>) {
|
||||
return plainToClass(CompletedShareDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
});
|
||||
}
|
||||
|
||||
fromList(partial: Partial<CompletedShareDTO>[]) {
|
||||
return partial.map((part) =>
|
||||
plainToClass(CompletedShareDTO, part, { excludeExtraneousValues: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,8 @@ export class CreateShareGuard extends JwtGuard {
|
||||
|
||||
if (!reverseShareTokenId) return false;
|
||||
|
||||
const isReverseShareTokenValid = await this.reverseShareService.isValid(
|
||||
reverseShareTokenId,
|
||||
);
|
||||
const isReverseShareTokenValid =
|
||||
await this.reverseShareService.isValid(reverseShareTokenId);
|
||||
|
||||
return isReverseShareTokenValid;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { JwtGuard } from "../../auth/guard/jwt.guard";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class ShareOwnerGuard extends JwtGuard {
|
||||
@@ -34,10 +34,20 @@ export class ShareOwnerGuard extends JwtGuard {
|
||||
|
||||
if (!share) throw new NotFoundException("Share not found");
|
||||
|
||||
// Run the JWTGuard to set the user
|
||||
await super.canActivate(context);
|
||||
const user = request.user as User;
|
||||
|
||||
// If the user is an admin, allow access
|
||||
if (user?.isAdmin) return true;
|
||||
|
||||
// If it's a anonymous share, allow access
|
||||
if (!share.creatorId) return true;
|
||||
|
||||
if (!(await super.canActivate(context))) return false;
|
||||
// If not signed in, deny access
|
||||
if (!user) return false;
|
||||
|
||||
return share.creatorId == (request.user as User).id;
|
||||
// If the user is the creator of the share, allow access
|
||||
return share.creatorId == user.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
@@ -9,13 +8,19 @@ import { Request } from "express";
|
||||
import * as moment from "moment";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShareService } from "src/share/share.service";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class ShareSecurityGuard implements CanActivate {
|
||||
export class ShareSecurityGuard extends JwtGuard {
|
||||
constructor(
|
||||
private shareService: ShareService,
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request: Request = context.switchToHttp().getRequest();
|
||||
@@ -31,7 +36,7 @@ export class ShareSecurityGuard implements CanActivate {
|
||||
|
||||
const share = await this.prisma.share.findUnique({
|
||||
where: { id: shareId },
|
||||
include: { security: true },
|
||||
include: { security: true, reverseShare: true },
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -53,6 +58,22 @@ export class ShareSecurityGuard implements CanActivate {
|
||||
"share_token_required",
|
||||
);
|
||||
|
||||
// Run the JWTGuard to set the user
|
||||
await super.canActivate(context);
|
||||
const user = request.user as User;
|
||||
|
||||
// Only the creator and reverse share creator can access the reverse share if it's not public
|
||||
if (
|
||||
share.reverseShare &&
|
||||
!share.reverseShare.publicAccess &&
|
||||
share.creatorId !== user?.id &&
|
||||
share.reverseShare.creatorId !== user?.id
|
||||
)
|
||||
throw new ForbiddenException(
|
||||
"Only reverse share creator can access this share",
|
||||
"private_share",
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,15 @@ import {
|
||||
Res,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Throttle } from "@nestjs/throttler";
|
||||
import { User } from "@prisma/client";
|
||||
import { Request, Response } from "express";
|
||||
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 { AdminShareDTO } from "./dto/adminShare.dto";
|
||||
import { CreateShareDTO } from "./dto/createShare.dto";
|
||||
import { MyShareDTO } from "./dto/myShare.dto";
|
||||
import { ShareDTO } from "./dto/share.dto";
|
||||
@@ -25,9 +29,19 @@ import { ShareOwnerGuard } from "./guard/shareOwner.guard";
|
||||
import { ShareSecurityGuard } from "./guard/shareSecurity.guard";
|
||||
import { ShareTokenSecurity } from "./guard/shareTokenSecurity.guard";
|
||||
import { ShareService } from "./share.service";
|
||||
import { CompletedShareDTO } from "./dto/shareComplete.dto";
|
||||
@Controller("shares")
|
||||
export class ShareController {
|
||||
constructor(private shareService: ShareService) {}
|
||||
constructor(
|
||||
private shareService: ShareService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
@Get("all")
|
||||
@UseGuards(JwtGuard, AdministratorGuard)
|
||||
async getAllShares() {
|
||||
return new AdminShareDTO().fromList(await this.shareService.getShares());
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(JwtGuard)
|
||||
@@ -73,7 +87,7 @@ export class ShareController {
|
||||
@UseGuards(CreateShareGuard, ShareOwnerGuard)
|
||||
async complete(@Param("id") id: string, @Req() request: Request) {
|
||||
const { reverse_share_token } = request.cookies;
|
||||
return new ShareDTO().from(
|
||||
return new CompletedShareDTO().from(
|
||||
await this.shareService.complete(id, reverse_share_token),
|
||||
);
|
||||
}
|
||||
@@ -86,26 +100,40 @@ export class ShareController {
|
||||
|
||||
@Delete(":id")
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async remove(@Param("id") id: string) {
|
||||
await this.shareService.remove(id);
|
||||
async remove(@Param("id") id: string, @GetUser() user: User) {
|
||||
const isDeleterAdmin = user?.isAdmin === true;
|
||||
await this.shareService.remove(id, isDeleterAdmin);
|
||||
}
|
||||
|
||||
@Throttle(10, 60)
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 10,
|
||||
ttl: 60,
|
||||
},
|
||||
})
|
||||
@Get("isShareIdAvailable/:id")
|
||||
async isShareIdAvailable(@Param("id") id: string) {
|
||||
return this.shareService.isShareIdAvailable(id);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Throttle(20, 5 * 60)
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 20,
|
||||
ttl: 5 * 60,
|
||||
},
|
||||
})
|
||||
@UseGuards(ShareTokenSecurity)
|
||||
@Post(":id/token")
|
||||
async getShareToken(
|
||||
@Param("id") id: string,
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@Body() body: SharePasswordDto,
|
||||
) {
|
||||
const token = await this.shareService.getShareToken(id, body.password);
|
||||
|
||||
this.clearShareTokenCookies(request, response);
|
||||
response.cookie(`share_${id}_token`, token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
@@ -113,4 +141,32 @@ export class ShareController {
|
||||
|
||||
return { token };
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the 10 most recent share token cookies and deletes the rest and all expired ones
|
||||
*/
|
||||
private clearShareTokenCookies(request: Request, response: Response) {
|
||||
const shareTokenCookies = Object.entries(request.cookies)
|
||||
.filter(([key]) => key.startsWith("share_") && key.endsWith("_token"))
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
payload: this.jwtService.decode(value),
|
||||
}));
|
||||
|
||||
const expiredTokens = shareTokenCookies.filter(
|
||||
(cookie) => cookie.payload.exp < moment().unix(),
|
||||
);
|
||||
const validTokens = shareTokenCookies.filter(
|
||||
(cookie) => cookie.payload.exp >= moment().unix(),
|
||||
);
|
||||
|
||||
expiredTokens.forEach((cookie) => response.clearCookie(cookie.key));
|
||||
|
||||
if (validTokens.length > 10) {
|
||||
validTokens
|
||||
.sort((a, b) => a.payload.exp - b.payload.exp)
|
||||
.slice(0, -10)
|
||||
.forEach((cookie) => response.clearCookie(cookie.key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ShareService } from "./share.service";
|
||||
imports: [
|
||||
JwtModule.register({}),
|
||||
EmailModule,
|
||||
ClamScanModule,
|
||||
forwardRef(() => ClamScanModule),
|
||||
ReverseShareModule,
|
||||
forwardRef(() => FileModule),
|
||||
],
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { JwtService, JwtSignOptions } from "@nestjs/jwt";
|
||||
import { Share, User } from "@prisma/client";
|
||||
import * as archiver from "archiver";
|
||||
import * as argon from "argon2";
|
||||
@@ -46,9 +46,8 @@ export class ShareService {
|
||||
let expirationDate: Date;
|
||||
|
||||
// If share is created by a reverse share token override the expiration date
|
||||
const reverseShare = await this.reverseShareService.getByToken(
|
||||
reverseShareToken,
|
||||
);
|
||||
const reverseShare =
|
||||
await this.reverseShareService.getByToken(reverseShareToken);
|
||||
if (reverseShare) {
|
||||
expirationDate = reverseShare.shareExpiration;
|
||||
} else {
|
||||
@@ -160,11 +159,12 @@ export class ShareService {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
share.reverseShare &&
|
||||
this.config.get("smtp.enabled") &&
|
||||
share.reverseShare.sendEmailNotification
|
||||
) {
|
||||
const notifyReverseShareCreator = share.reverseShare
|
||||
? this.config.get("smtp.enabled") &&
|
||||
share.reverseShare.sendEmailNotification
|
||||
: undefined;
|
||||
|
||||
if (notifyReverseShareCreator) {
|
||||
await this.emailService.sendMailToReverseShareCreator(
|
||||
share.reverseShare.creator.email,
|
||||
share.id,
|
||||
@@ -181,10 +181,15 @@ export class ShareService {
|
||||
});
|
||||
}
|
||||
|
||||
return this.prisma.share.update({
|
||||
const updatedShare = await this.prisma.share.update({
|
||||
where: { id },
|
||||
data: { uploadLocked: true },
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedShare,
|
||||
notifyReverseShareCreator,
|
||||
};
|
||||
}
|
||||
|
||||
async revertComplete(id: string) {
|
||||
@@ -194,6 +199,22 @@ export class ShareService {
|
||||
});
|
||||
}
|
||||
|
||||
async getShares() {
|
||||
const shares = await this.prisma.share.findMany({
|
||||
orderBy: {
|
||||
expiration: "desc",
|
||||
},
|
||||
include: { files: true, creator: true },
|
||||
});
|
||||
|
||||
return shares.map((share) => {
|
||||
return {
|
||||
...share,
|
||||
size: share.files.reduce((acc, file) => acc + parseInt(file.size), 0),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getSharesByUser(userId: string) {
|
||||
const shares = await this.prisma.share.findMany({
|
||||
where: {
|
||||
@@ -214,6 +235,7 @@ export class ShareService {
|
||||
return shares.map((share) => {
|
||||
return {
|
||||
...share,
|
||||
size: share.files.reduce((acc, file) => acc + parseInt(file.size), 0),
|
||||
recipients: share.recipients.map((recipients) => recipients.email),
|
||||
};
|
||||
});
|
||||
@@ -223,7 +245,11 @@ export class ShareService {
|
||||
const share = await this.prisma.share.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
files: true,
|
||||
files: {
|
||||
orderBy: {
|
||||
name: "asc",
|
||||
},
|
||||
},
|
||||
creator: true,
|
||||
security: true,
|
||||
},
|
||||
@@ -251,13 +277,14 @@ export class ShareService {
|
||||
return share;
|
||||
}
|
||||
|
||||
async remove(shareId: string) {
|
||||
async remove(shareId: string, isDeleterAdmin = false) {
|
||||
const share = await this.prisma.share.findUnique({
|
||||
where: { id: shareId },
|
||||
});
|
||||
|
||||
if (!share) throw new NotFoundException("Share not found");
|
||||
if (!share.creatorId)
|
||||
|
||||
if (!share.creatorId && !isDeleterAdmin)
|
||||
throw new ForbiddenException("Anonymous shares can't be deleted");
|
||||
|
||||
await this.fileService.deleteAllFiles(shareId);
|
||||
@@ -311,15 +338,21 @@ export class ShareService {
|
||||
const { expiration } = await this.prisma.share.findUnique({
|
||||
where: { id: shareId },
|
||||
});
|
||||
return this.jwtService.sign(
|
||||
{
|
||||
shareId,
|
||||
},
|
||||
{
|
||||
expiresIn: moment(expiration).diff(new Date(), "seconds") + "s",
|
||||
secret: this.config.get("internal.jwtSecret"),
|
||||
},
|
||||
);
|
||||
|
||||
const tokenPayload = {
|
||||
shareId,
|
||||
iat: moment().unix(),
|
||||
};
|
||||
|
||||
const tokenOptions: JwtSignOptions = {
|
||||
secret: this.config.get("internal.jwtSecret"),
|
||||
};
|
||||
|
||||
if (!moment(expiration).isSame(0)) {
|
||||
tokenOptions.expiresIn = moment(expiration).diff(new Date(), "seconds");
|
||||
}
|
||||
|
||||
return this.jwtService.sign(tokenPayload, tokenOptions);
|
||||
}
|
||||
|
||||
async verifyShareToken(shareId: string, token: string) {
|
||||
|
||||
@@ -25,16 +25,21 @@ export class UserDTO {
|
||||
@Expose()
|
||||
isAdmin: boolean;
|
||||
|
||||
@Expose()
|
||||
isLdap: boolean;
|
||||
|
||||
ldapDN?: string;
|
||||
|
||||
@Expose()
|
||||
totpVerified: boolean;
|
||||
|
||||
from(partial: Partial<UserDTO>) {
|
||||
return plainToClass(UserDTO, partial, { excludeExtraneousValues: true });
|
||||
const result = plainToClass(UserDTO, partial, { excludeExtraneousValues: true });
|
||||
result.isLdap = partial.ldapDN?.length > 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
fromList(partial: Partial<UserDTO>[]) {
|
||||
return partial.map((part) =>
|
||||
plainToClass(UserDTO, part, { excludeExtraneousValues: true }),
|
||||
);
|
||||
return partial.map((part) => this.from(part));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ export class UserController {
|
||||
// Own user operations
|
||||
@Get("me")
|
||||
@UseGuards(JwtGuard)
|
||||
async getCurrentUser(@GetUser() user: User) {
|
||||
async getCurrentUser(@GetUser() user?: User) {
|
||||
if (!user) return null;
|
||||
const userDTO = new UserDTO().from(user);
|
||||
userDTO.hasPassword = !!user.password;
|
||||
return userDTO;
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Module } from "@nestjs/common";
|
||||
import { EmailModule } from "src/email/email.module";
|
||||
import { UserController } from "./user.controller";
|
||||
import { UserSevice } from "./user.service";
|
||||
import { FileModule } from "src/file/file.module";
|
||||
|
||||
@Module({
|
||||
imports: [EmailModule],
|
||||
imports: [EmailModule, FileModule],
|
||||
providers: [UserSevice],
|
||||
controllers: [UserController],
|
||||
exports: [UserSevice]
|
||||
})
|
||||
export class UserModule {}
|
||||
export class UserModule { }
|
||||
|
||||
@@ -4,15 +4,20 @@ import * as argon from "argon2";
|
||||
import * as crypto from "crypto";
|
||||
import { EmailService } from "src/email/email.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { FileService } from "../file/file.service";
|
||||
import { CreateUserDTO } from "./dto/createUser.dto";
|
||||
import { UpdateUserDto } from "./dto/updateUser.dto";
|
||||
import { ConfigService } from "../config/config.service";
|
||||
import { LdapAuthenticateResult } from "../auth/ldap.service";
|
||||
|
||||
@Injectable()
|
||||
export class UserSevice {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private emailService: EmailService,
|
||||
) {}
|
||||
private fileService: FileService,
|
||||
private configService: ConfigService,
|
||||
) { }
|
||||
|
||||
async list() {
|
||||
return await this.prisma.user.findMany();
|
||||
@@ -74,6 +79,53 @@ export class UserSevice {
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: { shares: true },
|
||||
});
|
||||
if (!user) throw new BadRequestException("User not found");
|
||||
|
||||
await Promise.all(
|
||||
user.shares.map((share) => this.fileService.deleteAllFiles(share.id)),
|
||||
);
|
||||
|
||||
return await this.prisma.user.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async findOrCreateFromLDAP(username: string, ldap: LdapAuthenticateResult) {
|
||||
const passwordHash = await argon.hash(crypto.randomUUID());
|
||||
const userEmail = ldap.attributes["userPrincipalName"]?.at(0) ?? `${crypto.randomUUID()}@ldap.local`;
|
||||
const adminGroup = this.configService.get("ldap.adminGroups");
|
||||
const isAdmin = ldap.attributes["memberOf"]?.includes(adminGroup) ?? false;
|
||||
try {
|
||||
return await this.prisma.user.upsert({
|
||||
create: {
|
||||
username,
|
||||
email: userEmail,
|
||||
password: passwordHash,
|
||||
isAdmin,
|
||||
ldapDN: ldap.userDn,
|
||||
},
|
||||
update: {
|
||||
username,
|
||||
email: userEmail,
|
||||
|
||||
isAdmin,
|
||||
ldapDN: ldap.userDn,
|
||||
},
|
||||
where: {
|
||||
ldapDN: ldap.userDn
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e.code == "P2002") {
|
||||
const duplicatedField: string = e.meta.target[0];
|
||||
throw new BadRequestException(
|
||||
`A user with this ${duplicatedField} already exists`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@
|
||||
" const responseBody = pm.response.json();",
|
||||
" pm.expect(responseBody).to.have.property(\"id\")",
|
||||
" pm.expect(responseBody).to.have.property(\"expiration\")",
|
||||
" pm.expect(Object.keys(responseBody).length).be.equal(3)",
|
||||
" pm.expect(Object.keys(responseBody).length).be.equal(4)",
|
||||
"});",
|
||||
""
|
||||
],
|
||||
@@ -626,7 +626,7 @@
|
||||
" const responseBody = pm.response.json();",
|
||||
" pm.expect(responseBody).to.have.property(\"id\")",
|
||||
" pm.expect(responseBody).to.have.property(\"expiration\")",
|
||||
" pm.expect(Object.keys(responseBody).length).be.equal(3)",
|
||||
" pm.expect(Object.keys(responseBody).length).be.equal(4)",
|
||||
"});",
|
||||
""
|
||||
],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
clamav:
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
pingvin-share:
|
||||
image: stonith404/pingvin-share
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
_Leer esto en otro idioma: [Inglés](/CONTRIBUTING.md), [Español](/docs/CONTRIBUTING.es.md), [Chino Simplificado](/docs/CONTRIBUTING.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
# Contribuyendo
|
||||
|
||||
¡Nos ❤️ encantaría que contribuyas a Pingvin Share y nos ayudes a hacerlo mejor! Todas las contribuciones son bienvenidas, incluyendo problemas, sugerencias, _pull requests_ y más.
|
||||
|
||||
## Para comenzar
|
||||
|
||||
Si encontraste un error, tienes una sugerencia o algo más, simplemente crea un problema (issue) en GitHub y nos pondremos en contacto contigo 😊.
|
||||
|
||||
## Para hacer una Pull Request
|
||||
|
||||
Antes de enviar la pull request para su revisión, asegúrate de que:
|
||||
|
||||
- El nombre de la pull request sigue las [especificaciones de Commits Convencionales](https://www.conventionalcommits.org/):
|
||||
|
||||
`<tipo>[ámbito opcional]: <descripción>`
|
||||
|
||||
ejemplo:
|
||||
|
||||
```
|
||||
feat(share): agregar protección con contraseña
|
||||
```
|
||||
|
||||
Donde `tipo` puede ser:
|
||||
|
||||
- **feat** - es una nueva función
|
||||
- **doc** - cambios solo en la documentación
|
||||
- **fix** - una corrección de error
|
||||
- **refactor** - cambios en el código que no solucionan un error ni agregan una función
|
||||
|
||||
- Tu pull requests tiene una descripción detallada.
|
||||
|
||||
- Ejecutaste `npm run format` para formatear el código.
|
||||
|
||||
<details>
|
||||
<summary>¿No sabes como crear una pull request? Aprende cómo crear una pull request</summary>
|
||||
|
||||
1. Crea un fork del repositorio haciendo clic en el botón `Fork` en el repositorio de Pingvin Share.
|
||||
|
||||
2. Clona tu fork en tu máquina con `git clone`.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/[your_username]/pingvin-share
|
||||
```
|
||||
|
||||
3. Trabajar - hacer commit - repetir
|
||||
|
||||
4. Haz un `push` de tus cambios a GitHub.
|
||||
|
||||
```
|
||||
$ git push origin [nombre_de_tu_nueva_rama]
|
||||
```
|
||||
|
||||
5. Envía tus cambios para su revisión. Si vas a tu repositorio en GitHub, verás un botón `Comparar y crear pull requests`. Haz clic en ese botón.
|
||||
6. Inicia una Pull Request
|
||||
7. Ahora envía la pull requests y haz clic en `Crear pull requests`
|
||||
8. Espera a que alguien revise tu solicitud y apruebe o rechace tus cambios. Puedes ver los comentarios en la página de la solicitud en GitHub.
|
||||
|
||||
</details>
|
||||
|
||||
## Instalación del proyecto
|
||||
|
||||
Pingvin Share consiste de un frontend y un backend.
|
||||
|
||||
### Backend
|
||||
|
||||
El backend está hecho con [Nest.js](https://nestjs.com) y usa Typescript.
|
||||
|
||||
#### Instalación
|
||||
|
||||
1. Abrimos la carpeta `backend`
|
||||
2. Instalamos las dependencias con `npm install`
|
||||
3. Haz un `push` del esquema de la base de datos a la base de datos ejecutando `npx prisma db push`
|
||||
4. Rellena la base de datos ejecutando `npx prisma db seed`
|
||||
5. Inicia el backend con `npm run dev`
|
||||
|
||||
### Frontend
|
||||
|
||||
El frontend está hecho con [Next.js](https://nextjs.org) y usa Typescript.
|
||||
|
||||
#### Instalación
|
||||
|
||||
1. Primero inicia el backend
|
||||
2. Abre la carpeta `frontend`
|
||||
3. Instala las dependencias con `npm install`
|
||||
4. Inicia el frontend con `npm run dev`
|
||||
|
||||
¡Ya está todo listo!
|
||||
|
||||
### Testing
|
||||
|
||||
Por el momento, solo tenemos pruebas para el backend. Para ejecutar estas pruebas, debes ejecutar el comando `npm run test:system` en la carpeta del backend.
|
||||
@@ -1,97 +0,0 @@
|
||||
_选择合适的语言阅读: [西班牙语](/docs/CONTRIBUTING.es.md), [英语](/CONTRIBUTING.md), [简体中文](/docs/CONTRIBUTING.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
# 提交贡献
|
||||
|
||||
我们非常感谢你 ❤️ 为 Pingvin Share 提交贡献使其变得更棒! 欢迎任何形式的贡献,包括 issues, 建议, PRs 和其他形式
|
||||
|
||||
## 小小的开始
|
||||
|
||||
你找到了一个 bug,有新特性建议或者其他提议,请在 GitHub 建立一个 issue 以便我和你联络 😊
|
||||
|
||||
## 提交一个 Pull Request
|
||||
|
||||
在你提交 PR 前请确保
|
||||
|
||||
- PR 的名字遵守 [Conventional Commits specification](https://www.conventionalcommits.org):
|
||||
|
||||
`<type>[optional scope]: <description>`
|
||||
|
||||
例如:
|
||||
|
||||
```
|
||||
feat(share): add password protection
|
||||
```
|
||||
|
||||
`TYPE` 可以是:
|
||||
|
||||
- **feat** - 这是一个新特性 feature
|
||||
- **doc** - 仅仅改变了文档部分 documentation
|
||||
- **fix** - 修复了一个 bug
|
||||
- **refactor** - 更新了代码,但是并非出于增加新特性 feature 或修复 bug 的目的
|
||||
|
||||
- 请在 PR 中附详细的解释说明
|
||||
- 使用 `npm run format` 格式化你的代码
|
||||
|
||||
<details>
|
||||
<summary>不知道怎么发起一个 PR? 点开了解怎么发起一个 PR </summary>
|
||||
|
||||
1. 点击 Pingvin Share 仓库的 `Fork` 按钮,复制一份你的仓库
|
||||
|
||||
2. 通过 `git clone` 将你的仓库克隆到本地
|
||||
|
||||
```
|
||||
$ git clone https://github.com/[你的用户名]/pingvin-share
|
||||
```
|
||||
|
||||
3. 进行你的修改 - 提交 commit 你的修改 - 重复直到完成
|
||||
|
||||
4. 将你的修改提交到 GitHub
|
||||
|
||||
```
|
||||
$ git push origin [你的新分支的名字]
|
||||
```
|
||||
|
||||
5. 提交你的代码以便代码审查
|
||||
|
||||
如果你进入你 fork 的 Github 仓库,你会看到一个 `Compare & pull request` 按钮,点击该按钮
|
||||
|
||||
6. 发起一个 PR
|
||||
7. 点击 `Create pull request` 来提交你的 PR
|
||||
8. 等待代码审查,通过或以某些原因拒绝
|
||||
|
||||
</details>
|
||||
|
||||
## 配置开发项目
|
||||
|
||||
Pingvin Share 包括前端和后端部分
|
||||
|
||||
### 后端
|
||||
|
||||
后端使用 [Nest.js](https://nestjs.com) 建立,使用 Typescript
|
||||
|
||||
#### 搭建
|
||||
|
||||
1. 打开 `backend` 文件夹
|
||||
2. 使用 `npm install` 安装依赖
|
||||
3. 通过 `npx prisma db push` 配置数据库结构
|
||||
4. 通过 `npx prisma db seed` 初始化数据库数据
|
||||
5. 通过 `npm run dev` 启动后端
|
||||
|
||||
### 前端
|
||||
|
||||
后端使用 [Next.js](https://nextjs.org) 建立,使用 Typescript
|
||||
|
||||
#### 搭建
|
||||
|
||||
1. 首先启动后端
|
||||
2. 打开 `frontend` 文件夹
|
||||
3. 通过 `npm install` 安装依赖
|
||||
4. 通过 `npm run dev` 启动前端
|
||||
|
||||
开发项目配置完成
|
||||
|
||||
### 测试
|
||||
|
||||
目前阶段我们只有后端的系统测试,在 `backend` 文件夹运行 `npm run test:system` 来执行系统测试
|
||||
@@ -1,129 +0,0 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_Leer esto en otro idioma: [Inglés](/README.md), [Español](/docs/README.es.md), [Chino Simplificado](/docs/README.zh-cn.md), [日本語](/docs/README.ja-jp.md)_
|
||||
|
||||
---
|
||||
|
||||
Pingvin Share es una plataforma de intercambio de archivos autoalojada y una alternativa a WeTransfer.
|
||||
|
||||
## ✨ Características
|
||||
|
||||
- Compartir archivos utilizando un enlace
|
||||
- Tamaño de archivo ilimitado (unicamente restringido por el espacio en disco)
|
||||
- Establecer una fecha de caducidad para los recursos compartidos
|
||||
- Uso compartido seguro con límites de visitantes y contraseñas
|
||||
- Destinatarios de correo electrónico
|
||||
- Integración con ClamAV para escaneos de seguridad
|
||||
|
||||
## 🐧 Conoce Pingvin Share
|
||||
|
||||
- [Demo](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [Reseña realizada por No Solo Hacking (español)](https://www.youtube.com/watch?v=ocd4EpLTYkU)
|
||||
- [Reseña por DB Tech (inglés)](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
|
||||
## ⌨️ Instalación
|
||||
|
||||
> Nota: Pingvin Share está en sus primeras etapas y puede contener errores.
|
||||
|
||||
### Instalación con Docker (recomendada)
|
||||
|
||||
1. Descarge el archivo `docker-compose.yml`
|
||||
2. Ejecute `docker-compose up -d`
|
||||
|
||||
El sitio web ahora está esperando conexiones en `http://localhost:3000`, ¡diviértase usando Pingvin Share 🐧!
|
||||
|
||||
### Instalación autónoma
|
||||
|
||||
Herramientas requeridas:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) para ejecutar Pingvin Share en segundo plano
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stonith404/pingvin-share
|
||||
cd pingvin-share
|
||||
|
||||
# Consultar la última versión
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Iniciar el backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
# Iniciar el frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||
```
|
||||
|
||||
El sitio web ahora está esperando conexiones en `http://localhost:3000`, ¡diviértase usando Pingvin Share 🐧!
|
||||
|
||||
### Integraciones
|
||||
|
||||
#### ClamAV (Unicamente con Docker)
|
||||
|
||||
ClamAV se utiliza para escanear los recursos compartidos en busca de archivos maliciosos y eliminarlos si los encuentra.
|
||||
|
||||
1. Añade el contenedor ClamAV al stack de Docker Compose (ver `docker-compose.yml`) e inicie el contenedor.
|
||||
2. Docker esperará a que ClamAV se inicie antes de iniciar Pingvin Share. Esto puede tardar uno o dos minutos.
|
||||
3. Los registros de Pingvin Share ahora deberían decir "ClamAV está activo".
|
||||
|
||||
Por favor, ten en cuenta que ClamAV necesita muchos [recursos](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements).
|
||||
|
||||
### Recursos adicionales
|
||||
|
||||
- [Instalación en Synology NAS (Inglés)](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
### Actualizar a una nueva versión
|
||||
|
||||
Dado que Pingvin Share se encuentra en una fase inicial, consulte las notas de la versión para conocer los cambios de última hora antes de actualizar.
|
||||
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Instalación autónoma
|
||||
|
||||
1. Deten la aplicación en ejecución
|
||||
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
|
||||
2. Repite los pasos de la [guía de instalación](#instalación-autonoma) excepto el paso de `git clone`.
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# Consultar la última versión
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Iniciar el backend
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
# Iniciar frontend
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### Marca personalizada
|
||||
|
||||
Puedes cambiar el nombre y el logotipo de la aplicación visitando la página de configuración de administrador.
|
||||
|
||||
## 🖤 Contribuye
|
||||
|
||||
¡Eres bienvenido a contribuir a Pingvin Share! Sige la [guía de contribución](/CONTRIBUTING.md) para empezar.
|
||||
@@ -1,158 +0,0 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_READMEを別の言語で読む: [Spanish](/docs/README.es.md), [English](/README.md), [Simplified Chinese](/docs/README.zh-cn.md), [日本語](/docs/README.ja-jp.md)_
|
||||
|
||||
---
|
||||
|
||||
Pingvin Share は、セルフホスト型のファイル共有プラットフォームであり、WeTransfer、ギガファイル便などの代替プラットフォームです。
|
||||
|
||||
## ✨ 特徴的な機能
|
||||
|
||||
- リンクを用いたファイル共有
|
||||
- ファイルサイズ無制限 (ストレージスペースの範囲内で)
|
||||
- 共有への有効期限の設定
|
||||
- 訪問回数の制限とパスワードの設定により共有を安全に保つ
|
||||
- メールでリンクを共有
|
||||
- ClamAVと連携して、ウイルスチェックが可能
|
||||
|
||||
## 🐧 Pingvin Shareについて知る
|
||||
|
||||
- [デモ](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [DB Techによるレビュー](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
|
||||
## ⌨️ セットアップ
|
||||
|
||||
> 注意: Pingvin Shareは、早期段階であり、バグが含まれている場合があります。
|
||||
|
||||
### Dockerでインストール (おすすめ)
|
||||
|
||||
1. `docker-compose.yml`ファイルをダウンロード
|
||||
2. `docker-compose up -d`を実行
|
||||
|
||||
Webサイトは、`http://localhost:3000`でリッスンされます。これでPingvin Shareをお使い頂けます🐧!
|
||||
|
||||
### スタンドアローンインストール
|
||||
|
||||
必要なツール:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) Pingvin Shareをバックグラウンドで動作させるために必要
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stonith404/pingvin-share
|
||||
cd pingvin-share
|
||||
|
||||
# 最新バージョンをチェックアウト
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# バックエンドを開始
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
#フロントエンドを開始
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||
```
|
||||
|
||||
Webサイトは、`http://localhost:3000`でリッスンされます。これでPingvin Shareをお使い頂けます🐧!
|
||||
|
||||
### 連携機能
|
||||
|
||||
#### ClamAV (Dockerのみ)
|
||||
|
||||
ClamAVは、共有されたファイルをスキャンし、感染したファイルを見つけた場合に削除するために使用されます。
|
||||
|
||||
1. ClamAVコンテナをDocker Composeの定義ファイル(`docker-compose.yml`を確認)に追加し、コンテナを開始してください。
|
||||
2. Dockerは、Pingvin Shareを開始する前に、ClamAVの準備が整うまで待機します。これには、1分から2分ほどかかります。
|
||||
3. Pingvin Shareのログに"ClamAV is active"というログが記録されます。
|
||||
|
||||
ClamAVは、非常に多くのリソースを必要とします、詳しくは[リソース](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements)をご確認ください。
|
||||
|
||||
### 追加情報
|
||||
|
||||
- [Synology NASへのインストール方法](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
### 新しいバージョンへのアップグレード
|
||||
|
||||
Pingvin Shareは早期段階のため、アップグレード前に必ずリリースノートを確認して、アップグレードしても問題ないかどうかご確認ください。
|
||||
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### スタンドアローン
|
||||
|
||||
1. アプリを停止する
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. `git clone`のステップを除いて、[インストールガイド](#stand-alone-installation)をくり返してください。
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# 最新バージョンをチェックアウト
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# バックエンドを開始
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
#フロントエンドを開始
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### 設定
|
||||
|
||||
管理者のダッシュボード内の「設定」ページから、Pingvin Shareをカスタマイズできます。
|
||||
|
||||
#### 環境変数
|
||||
|
||||
インストール時の特定の設定で、環境変数を使用できます。次の環境変数が使用可能です:
|
||||
|
||||
##### バックエンド
|
||||
|
||||
| 変数名 | デフォルト値 | 説明 |
|
||||
| ---------------- | -------------------------------------------------- | -------------------------------------- |
|
||||
| `PORT` | `8080` | バックエンドがリッスンするポート番号 |
|
||||
| `DATABASE_URL` | `file:../data/pingvin-share.db?connection_limit=1` | SQLiteのURL |
|
||||
| `DATA_DIRECTORY` | `./data` | データを保管するディレクトリ |
|
||||
| `CLAMAV_HOST` | `127.0.0.1` | ClamAVサーバーのIPアドレス |
|
||||
| `CLAMAV_PORT` | `3310` | ClamAVサーバーのポート番号 |
|
||||
|
||||
##### フロントエンド
|
||||
|
||||
| 変数名 | デフォルト値 | 説明 |
|
||||
| --------- | ----------------------- | ---------------------------------------- |
|
||||
| `PORT` | `3000` | フロントエンドがリッスンするポート番号 |
|
||||
| `API_URL` | `http://localhost:8080` | フロントエンドからアクセスするバックエンドへのURL |
|
||||
|
||||
## 🖤 コントリビュート
|
||||
|
||||
### 翻訳
|
||||
|
||||
Pingvin Shareをあなたが使用している言語に翻訳するお手伝いを募集しています。
|
||||
[Crowdin](https://crowdin.com/project/pingvin-share)上で、簡単にPingvin Shareの翻訳作業への参加が可能です。
|
||||
|
||||
あなたの言語がありませんか? 気軽に[リクエスト](https://github.com/stonith404/pingvin-share/issues/new?assignees=&labels=language-request&projects=&template=language-request.yml&title=%F0%9F%8C%90+Language+request%3A+%3Clanguage+name+in+english%3E)してください。
|
||||
|
||||
翻訳中に問題がありましたか? [ローカライズに関するディスカッション](https://github.com/stonith404/pingvin-share/discussions/198)に是非参加してください。
|
||||
|
||||
### プロジェクト
|
||||
|
||||
Pingvin Shareへのコントリビュートをいつでもお待ちしています! [コントリビューションガイド](/CONTRIBUTING.md)を確認して、是非参加してください。
|
||||
@@ -1,126 +0,0 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_选择合适的语言阅读: [西班牙语](/docs/README.es.md), [英语](/README.md), [简体中文](/docs/README.zh-cn.md), [日本语](/docs/README.ja-jp.md)_
|
||||
|
||||
---
|
||||
|
||||
Pingvin Share 是一个可自建的文件分享平台,是 WeTransfer 的一个替代品
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 通过可自定义后缀的链接分享文件
|
||||
- 可自定义任意大小的文件上传限制 (受制于托管所在的硬盘大小)
|
||||
- 对共享链接设置有效期限
|
||||
- 对共享链接设置访问次数和访问密码
|
||||
- 通过邮件自动发送共享链接
|
||||
- 整合 ClamAV 进行反病毒检查
|
||||
|
||||
## 🐧 了解一下 Pingvin Share
|
||||
|
||||
- [示例网站](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [DB Tech 推荐视频](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
|
||||
## ⌨️ 自建指南
|
||||
|
||||
> 注意:Pingvin Share 仍处于开发阶段并且可能存在 bugs
|
||||
|
||||
### Docker 部署 (推荐)
|
||||
|
||||
1. 下载 `docker-compose.yml`
|
||||
2. 运行命令 `docker-compose up -d`
|
||||
|
||||
现在网站运行在 `http://localhost:3000`,尝试一下你本地的 Pingvin Share 🐧!
|
||||
|
||||
### Stand-alone 部署
|
||||
|
||||
必须的依赖:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) 用于后台运行 Pingvin Share
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stonith404/pingvin-share
|
||||
cd pingvin-share
|
||||
|
||||
# 获取最新的版本
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# 启动后端 backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
# 启动前端 frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||
```
|
||||
|
||||
现在网站运行在 `http://localhost:3000`,尝试一下你本地的 Pingvin Share 🐧!
|
||||
|
||||
### 整合组件
|
||||
|
||||
#### ClamAV (仅限 Docker 部署)
|
||||
|
||||
扫描上传文件中是否存在可疑文件,如果存在 ClamAV 会自动移除
|
||||
|
||||
1. 在 docker-compose 配置中添加 ClamAV 容器 (见 `docker-compose.yml` 注释部分) 并启动容器
|
||||
2. Docker 会在启动 Pingvin Share 前启动 ClamAV,也许会花费 1-2 分钟
|
||||
3. Pingvin Share 日志中应该有 "ClamAV is active"
|
||||
|
||||
请注意 ClamAV 会消耗很多 [系统资源(特别是内存)](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements)
|
||||
|
||||
### 更多资源
|
||||
|
||||
- [群晖 NAS 配置](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
### 升级
|
||||
|
||||
因为 Pingvin Share 仍处在开发阶段,在升级前请务必阅读 release notes 避免不可逆的改变
|
||||
|
||||
#### Docker 升级
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Stand-alone 升级
|
||||
|
||||
1. 停止正在运行的 app
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. 重复 [installation guide](#stand-alone-installation) 中的步骤,除了 `git clone` 这一步
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# 获取最新的版本
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# 启动后端 backend
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
# 启动前端 frontend
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### 自定义品牌
|
||||
|
||||
你可以在管理员配置页面改变网站的名字和 logo
|
||||
|
||||
## 🖤 提交贡献
|
||||
|
||||
非常欢迎向 Pingvin Share 提交贡献! 请阅读 [contribution guide](/CONTRIBUTING.md) 来提交你的贡献
|
||||
3
docs/babel.config.js
Normal file
3
docs/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
91
docs/docs/help-out/contribute.md
Normal file
91
docs/docs/help-out/contribute.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Contributing
|
||||
|
||||
We would ❤️ for you to contribute to Pingvin Share and help make it better! All contributions are welcome, including issues, suggestions, pull requests and more.
|
||||
|
||||
## Getting started
|
||||
|
||||
You've found a bug, have suggestion or something else, just create an issue on GitHub and we can get in touch 😊.
|
||||
|
||||
## Submit a Pull Request
|
||||
|
||||
Before you submit the pull request for review please ensure that
|
||||
|
||||
- The pull request naming follows the [Conventional Commits specification](https://www.conventionalcommits.org):
|
||||
|
||||
`<type>[optional scope]: <description>`
|
||||
|
||||
example:
|
||||
|
||||
```
|
||||
feat(share): add password protection
|
||||
```
|
||||
|
||||
When `TYPE` can be:
|
||||
|
||||
- **feat** - is a new feature
|
||||
- **doc** - documentation only changes
|
||||
- **fix** - a bug fix
|
||||
- **refactor** - code change that neither fixes a bug nor adds a feature
|
||||
|
||||
- Your pull request has a detailed description
|
||||
- You run `npm run format` to format the code
|
||||
|
||||
<details>
|
||||
<summary>Don't know how to create a pull request? Learn how to create a pull request</summary>
|
||||
|
||||
1. Create a fork of the repository by clicking on the `Fork` button in the Pingvin Share repository
|
||||
|
||||
2. Clone your fork to your machine with `git clone`
|
||||
|
||||
```
|
||||
$ git clone https://github.com/[your_username]/pingvin-share
|
||||
```
|
||||
|
||||
3. Work - commit - repeat
|
||||
|
||||
4. Push changes to GitHub
|
||||
|
||||
```
|
||||
$ git push origin [name_of_your_new_branch]
|
||||
```
|
||||
|
||||
5. Submit your changes for review
|
||||
If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button.
|
||||
6. Start a Pull Request
|
||||
7. Now submit the pull request and click on `Create pull request`.
|
||||
8. Get a code review approval/reject
|
||||
|
||||
</details>
|
||||
|
||||
## Setup project
|
||||
|
||||
Pingvin Share consists of a frontend and a backend.
|
||||
|
||||
### Backend
|
||||
|
||||
The backend is built with [Nest.js](https://nestjs.com) and uses Typescript.
|
||||
|
||||
#### Setup
|
||||
|
||||
1. Open the `backend` folder
|
||||
2. Install the dependencies with `npm install`
|
||||
3. Push the database schema to the database by running `npx prisma db push`
|
||||
4. Seed the database with `npx prisma db seed`
|
||||
5. Start the backend with `npm run dev`
|
||||
|
||||
### Frontend
|
||||
|
||||
The frontend is built with [Next.js](https://nextjs.org) and uses Typescript.
|
||||
|
||||
#### Setup
|
||||
|
||||
1. Start the backend first
|
||||
2. Open the `frontend` folder
|
||||
3. Install the dependencies with `npm install`
|
||||
4. Start the frontend with `npm run dev`
|
||||
|
||||
You're all set!
|
||||
|
||||
### Testing
|
||||
|
||||
At the moment we only have system tests for the backend. To run these tests, run `npm run test:system` in the backend folder.
|
||||
8
docs/docs/help-out/translate.md
Normal file
8
docs/docs/help-out/translate.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Translating
|
||||
|
||||
You can help to translate Pingvin Share into your language.
|
||||
On [Crowdin](https://crowdin.com/project/pingvin-share) you can easily translate Pingvin Share online.
|
||||
|
||||
Is your language not on Crowdin? Feel free to [Request it](https://github.com/stonith404/pingvin-share/issues/new?assignees=&labels=language-request&projects=&template=language-request.yml&title=%F0%9F%8C%90+Language+request%3A+%3Clanguage+name+in+english%3E).
|
||||
|
||||
Any issues while translating? Feel free to participate in the [Localization discussion](https://github.com/stonith404/pingvin-share/discussions/198).
|
||||
24
docs/docs/introduction.md
Normal file
24
docs/docs/introduction.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
id: introduction
|
||||
---
|
||||
|
||||
# Introduction
|
||||
Pingvin Share is self-hosted file sharing platform and an alternative for WeTransfer.
|
||||
|
||||
## Features
|
||||
|
||||
- Share files using a link
|
||||
- Unlimited file size (restricted only by disk space)
|
||||
- Set an expiration date for shares
|
||||
- Secure shares with visitor limits and passwords
|
||||
- Email recipients
|
||||
- Integration with ClamAV for security scans
|
||||
|
||||
And more!
|
||||
|
||||
## Get to know Pingvin Share
|
||||
|
||||
- [Demo](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [Review by DB Tech](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
28
docs/docs/setup/configuration.md
Normal file
28
docs/docs/setup/configuration.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
id: configuration
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
You can customize Pingvin Share like changing your domain by going to the configuration page in your admin dashboard `/admin/config`.
|
||||
|
||||
#### Environment variables
|
||||
|
||||
For installation specific configuration, you can use environment variables. The following variables are available:
|
||||
|
||||
##### Backend
|
||||
|
||||
| Variable | Default Value | Description |
|
||||
| ---------------- | -------------------------------------------------- | -------------------------------------- |
|
||||
| `PORT` | `8080` | The port on which the backend listens. |
|
||||
| `DATABASE_URL` | `file:../data/pingvin-share.db?connection_limit=1` | The URL of the SQLite database. |
|
||||
| `DATA_DIRECTORY` | `./data` | The directory where data is stored. |
|
||||
| `CLAMAV_HOST` | `127.0.0.1` | The IP address of the ClamAV server. |
|
||||
| `CLAMAV_PORT` | `3310` | The port number of the ClamAV server. |
|
||||
|
||||
##### Frontend
|
||||
|
||||
| Variable | Default Value | Description |
|
||||
| --------- | ----------------------- | ---------------------------------------- |
|
||||
| `PORT` | `3000` | The port on which the frontend listens. |
|
||||
| `API_URL` | `http://localhost:8080` | The URL of the backend for the frontend. |
|
||||
45
docs/docs/setup/installation.md
Normal file
45
docs/docs/setup/installation.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: installation
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
### Installation with Docker (recommended)
|
||||
|
||||
1. Download the `docker-compose.yml` file
|
||||
2. Run `docker compose up -d`
|
||||
|
||||
The website is now listening on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
||||
|
||||
### Stand-alone Installation
|
||||
|
||||
Required tools:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) for running Pingvin Share in the background
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stonith404/pingvin-share
|
||||
cd pingvin-share
|
||||
|
||||
# Checkout the latest version
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Start the backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
# Start the frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
API_URL=http://localhost:8080 # Set the URL of the backend, default: http://localhost:8080
|
||||
pm2 start --name="pingvin-share-frontend" .next/standalone/server.js
|
||||
```
|
||||
|
||||
**Uploading Large Files**: By default, Pingvin Share uses a built-in reverse proxy to reduce the installation steps. However, this reverse proxy is not optimized for uploading large files. If you wish to upload larger files, you can either use the Docker installation or set up your own reverse proxy. An example configuration for Caddy can be found in `./Caddyfile`.
|
||||
|
||||
The website is now listening on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
||||
15
docs/docs/setup/integrations.md
Normal file
15
docs/docs/setup/integrations.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: integrations
|
||||
---
|
||||
|
||||
# Integrations
|
||||
|
||||
#### ClamAV (Docker only)
|
||||
|
||||
ClamAV is used to scan shares for malicious files and remove them if found.
|
||||
|
||||
1. Add the ClamAV container to the Docker Compose stack (see `docker-compose.yml`) and start the container.
|
||||
2. Docker will wait for ClamAV to start before starting Pingvin Share. This may take a minute or two.
|
||||
3. The Pingvin Share logs should now log "ClamAV is active"
|
||||
|
||||
Please note that ClamAV needs a lot of [ressources](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements).
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
id: oauth2login
|
||||
---
|
||||
|
||||
# OAuth 2 Login Guide
|
||||
|
||||
## Config Built-in OAuth 2 Providers
|
||||
@@ -24,8 +28,7 @@ Redirect URL: `https://<your-domain>/api/oauth/callback/google`
|
||||
|
||||
Please follow the [official guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) to register an application.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Microsoft Tenant** you set in the admin panel must match the **supported account types** you set in the Microsoft Entra admin center, otherwise the OAuth login will not work. Refer to the [official documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri) for more details.
|
||||
> [!IMPORTANT] > **Microsoft Tenant** you set in the admin panel must match the **supported account types** you set in the Microsoft Entra admin center, otherwise the OAuth login will not work. Refer to the [official documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri) for more details.
|
||||
|
||||
Redirect URL: `https://<your-domain>/api/oauth/callback/microsoft`
|
||||
|
||||
@@ -67,8 +70,8 @@ const configVariables: ConfigVariables = {
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Create provider class
|
||||
@@ -106,23 +109,29 @@ Register your provider in [`OAuthModule`](../backend/src/oauth/oauth.module.ts)
|
||||
// your provider
|
||||
{
|
||||
provide: "OAUTH_PROVIDERS",
|
||||
useFactory(github: GitHubProvider, /* your provider */): Record<string, OAuthProvider<unknown>> {
|
||||
useFactory(
|
||||
github: GitHubProvider /* your provider */
|
||||
): Record<string, OAuthProvider<unknown>> {
|
||||
return {
|
||||
github,
|
||||
/* your provider */
|
||||
};
|
||||
},
|
||||
inject: [GitHubProvider, /* your provider */],
|
||||
inject: [GitHubProvider /* your provider */],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class OAuthModule {
|
||||
}
|
||||
export class OAuthModule {}
|
||||
```
|
||||
|
||||
```ts
|
||||
export interface OAuthSignInDto {
|
||||
provider: 'github' | 'google' | 'microsoft' | 'discord' | 'oidc' /* your provider*/;
|
||||
provider:
|
||||
| "github"
|
||||
| "google"
|
||||
| "microsoft"
|
||||
| "discord"
|
||||
| "oidc" /* your provider*/;
|
||||
providerId: string;
|
||||
providerUsername: string;
|
||||
email: string;
|
||||
@@ -136,10 +145,10 @@ Add an icon in [`oauth.util.tsx`](../frontend/src/utils/oauth.util.tsx).
|
||||
```tsx
|
||||
const getOAuthIcon = (provider: string) => {
|
||||
return {
|
||||
'github': <SiGithub />,
|
||||
github: <SiGithub />,
|
||||
/* your provider */
|
||||
}[provider];
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Add i18n text
|
||||
44
docs/docs/setup/upgrading.md
Normal file
44
docs/docs/setup/upgrading.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
id: upgrading
|
||||
---
|
||||
|
||||
# Upgrading
|
||||
|
||||
### Upgrade to a new version
|
||||
|
||||
As Pingvin Share is in early stage, see the release notes for breaking changes before upgrading.
|
||||
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Stand-alone
|
||||
|
||||
1. Stop the running app
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. Repeat the steps from the [installation guide](#stand-alone-installation) except the `git clone` step.
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# Checkout the latest version
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Start the backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
# Start the frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
API_URL=http://localhost:8080 # Set the URL of the backend, default: http://localhost:8080
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
64
docs/docusaurus.config.ts
Normal file
64
docs/docusaurus.config.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type * as Preset from "@docusaurus/preset-classic";
|
||||
import type { Config } from "@docusaurus/types";
|
||||
import { themes as prismThemes } from "prism-react-renderer";
|
||||
|
||||
const config: Config = {
|
||||
title: "Pingvin Share",
|
||||
tagline:
|
||||
"Pingvin Share is self-hosted file sharing platform and an alternative for WeTransfer.",
|
||||
favicon: "img/pingvinshare.svg",
|
||||
|
||||
url: "https://stonith404.github.io",
|
||||
baseUrl: "/pingvin-share/",
|
||||
organizationName: "stonith404",
|
||||
projectName: "pingvin-share",
|
||||
|
||||
onBrokenLinks: "warn",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en"],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
"classic",
|
||||
{
|
||||
docs: {
|
||||
routeBasePath: "/",
|
||||
sidebarPath: "./sidebars.ts",
|
||||
editUrl: "https://github.com/stonith404/pingvin-share/edit/main/docs",
|
||||
},
|
||||
blog: false,
|
||||
} satisfies Preset.Options,
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
image: "img/pingvinshare.svg",
|
||||
colorMode:{
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
navbar: {
|
||||
title: "Pingvin Share",
|
||||
logo: {
|
||||
alt: "Pingvin Share Logo",
|
||||
src: "img/pingvinshare.svg",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
href: "https://github.com/stonith404/pingvin-share",
|
||||
label: "GitHub",
|
||||
position: "right",
|
||||
},
|
||||
],
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
export default config;
|
||||
14529
docs/package-lock.json
generated
Normal file
14529
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
docs/package.json
Normal file
47
docs/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "pingvindocs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.4.0",
|
||||
"@docusaurus/preset-classic": "3.4.0",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.4.0",
|
||||
"@docusaurus/tsconfig": "3.4.0",
|
||||
"@docusaurus/types": "3.4.0",
|
||||
"typescript": "~5.2.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
||||
67
docs/sidebars.ts
Normal file
67
docs/sidebars.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
|
||||
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
const sidebars: SidebarsConfig = {
|
||||
docsSidebar: [
|
||||
{
|
||||
type: "doc",
|
||||
id : "introduction",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Getting Started",
|
||||
items: [
|
||||
{
|
||||
type: "doc",
|
||||
id: "setup/installation",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "setup/configuration",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "setup/integrations",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "setup/oauth2login",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "setup/upgrading",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Helping Out",
|
||||
items: [
|
||||
{
|
||||
type: "doc",
|
||||
id: "help-out/translate",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "help-out/contribute",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
label: "Discord",
|
||||
href: "https://discord.gg/HutpbfB59Q",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
6
docs/src/pages/index.tsx
Normal file
6
docs/src/pages/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
export default function Home() {
|
||||
return <Redirect to='/pingvin-share/introduction' />;
|
||||
}
|
||||
BIN
docs/static/img/image.png
vendored
Normal file
BIN
docs/static/img/image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
1
docs/static/img/pingvinshare.svg
vendored
Normal file
1
docs/static/img/pingvinshare.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 943.11 911.62"><ellipse cx="471.56" cy="454.28" rx="471.56" ry="454.28" fill="#46509e"/><ellipse cx="471.56" cy="390.28" rx="233.66" ry="207" fill="#37474f"/><path d="M705.22,849c-36.69,21.14-123.09,64.32-240.64,62.57A469.81,469.81,0,0,1,237.89,849V394.76H705.22Z" fill="#37474f"/><path d="M658.81,397.7V873.49a478.12,478.12,0,0,1-374.19,0V397.7c0-95.55,83.78-173,187.1-173S658.81,302.15,658.81,397.7Z" fill="#fff"/><polygon points="565.02 431.68 471.56 514.49 378.09 431.68 565.02 431.68" fill="#46509e"/><ellipse cx="378.09" cy="369.58" rx="23.37" ry="20.7" fill="#37474f"/><ellipse cx="565.02" cy="369.58" rx="23.37" ry="20.7" fill="#37474f"/><path d="M658.49,400.63c0-40-36.6-72.45-81.79-72.45s-81.78,32.41-81.78,72.45a64.79,64.79,0,0,0,7.9,31.05H440.29a64.79,64.79,0,0,0,7.9-31.05c0-40-36.59-72.45-81.78-72.45s-81.79,32.41-81.79,72.45l-46.73-10.35c0-114.31,104.64-207,233.67-207s233.66,92.69,233.66,207Z" fill="#37474f"/></svg>
|
||||
|
After Width: | Height: | Size: 1018 B |
7
docs/tsconfig.json
Normal file
7
docs/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
5587
frontend/package-lock.json
generated
5587
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "0.22.1",
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
@@ -9,46 +9,46 @@
|
||||
"format": "prettier --end-of-line=auto --write \"src/**/*.ts*\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@mantine/core": "^6.0.17",
|
||||
"@mantine/dropzone": "^6.0.17",
|
||||
"@mantine/form": "^6.0.17",
|
||||
"@mantine/hooks": "^6.0.17",
|
||||
"@mantine/modals": "^6.0.17",
|
||||
"@mantine/next": "^6.0.17",
|
||||
"@mantine/notifications": "^6.0.17",
|
||||
"axios": "^1.4.0",
|
||||
"@mantine/core": "^6.0.21",
|
||||
"@mantine/dropzone": "^6.0.21",
|
||||
"@mantine/form": "^6.0.21",
|
||||
"@mantine/hooks": "^6.0.21",
|
||||
"@mantine/modals": "^6.0.21",
|
||||
"@mantine/next": "^6.0.21",
|
||||
"@mantine/notifications": "^6.0.21",
|
||||
"axios": "^1.7.2",
|
||||
"cookies-next": "^2.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"jose": "^4.14.4",
|
||||
"jose": "^4.15.5",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"markdown-to-jsx": "^7.4.1",
|
||||
"markdown-to-jsx": "^7.4.7",
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.29.4",
|
||||
"next": "^13.4.12",
|
||||
"next-cookies": "^2.0.3",
|
||||
"next-http-proxy-middleware": "^1.2.5",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^14.2.3",
|
||||
"next-http-proxy-middleware": "^1.2.6",
|
||||
"next-pwa": "^5.6.0",
|
||||
"p-limit": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.10.1",
|
||||
"react-intl": "^6.4.4",
|
||||
"sharp": "^0.32.4",
|
||||
"yup": "^1.2.0"
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-intl": "^6.6.8",
|
||||
"sharp": "^0.33.4",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/node": "20.4.5",
|
||||
"@types/react": "18.2.17",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"axios": "^1.4.0",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-next": "^13.4.12",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"prettier": "^3.0.0",
|
||||
"tar": "^6.1.15",
|
||||
"typescript": "^5.1.6"
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/react": "18.3.2",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/parser": "^7.10.0",
|
||||
"axios": "^1.7.2",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"prettier": "^3.2.5",
|
||||
"tar": "^6.2.1",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ const CreateEnableTotpModal = ({
|
||||
</span>
|
||||
</Center>
|
||||
|
||||
<Tooltip label={t("account.modal.totp.clickToCopy")}>
|
||||
<Tooltip label={t("common.button.clickToCopy")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(options.secret);
|
||||
|
||||
@@ -17,13 +17,9 @@ const showShareInformationsModal = (
|
||||
const t = translateOutsideContext();
|
||||
const link = `${appUrl}/s/${share.id}`;
|
||||
|
||||
let shareSize: number = 0;
|
||||
for (let file of share.files as FileMetaData[])
|
||||
shareSize += parseInt(file.size);
|
||||
|
||||
const formattedShareSize = byteToHumanSizeString(shareSize);
|
||||
const formattedShareSize = byteToHumanSizeString(share.size);
|
||||
const formattedMaxShareSize = byteToHumanSizeString(maxShareSize);
|
||||
const shareSizeProgress = (shareSize / maxShareSize) * 100;
|
||||
const shareSizeProgress = (share.size / maxShareSize) * 100;
|
||||
|
||||
const formattedCreatedAt = moment(share.createdAt).format("LLL");
|
||||
const formattedExpiration =
|
||||
@@ -42,12 +38,18 @@ const showShareInformationsModal = (
|
||||
</b>
|
||||
{share.id}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.name" />:{" "}
|
||||
</b>
|
||||
{share.name || "-"}
|
||||
</Text>
|
||||
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.description" />:{" "}
|
||||
</b>
|
||||
{share.description || "No description"}
|
||||
{share.description || "-"}
|
||||
</Text>
|
||||
|
||||
<Text size="sm">
|
||||
@@ -75,15 +77,15 @@ const showShareInformationsModal = (
|
||||
</Text>
|
||||
|
||||
<Flex align="center" justify="center">
|
||||
{shareSize / maxShareSize < 0.1 && (
|
||||
{share.size / maxShareSize < 0.1 && (
|
||||
<Text size="xs" style={{ marginRight: "4px" }}>
|
||||
{formattedShareSize}
|
||||
</Text>
|
||||
)}
|
||||
<Progress
|
||||
value={shareSizeProgress}
|
||||
label={shareSize / maxShareSize >= 0.1 ? formattedShareSize : ""}
|
||||
style={{ width: shareSize / maxShareSize < 0.1 ? "70%" : "80%" }}
|
||||
label={share.size / maxShareSize >= 0.1 ? formattedShareSize : ""}
|
||||
style={{ width: share.size / maxShareSize < 0.1 ? "70%" : "80%" }}
|
||||
size="xl"
|
||||
radius="xl"
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,14 @@ import {
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbAt, TbMail, TbShare, TbSocial, TbSquare } from "react-icons/tb";
|
||||
import {
|
||||
TbAt,
|
||||
TbMail,
|
||||
TbShare,
|
||||
TbSocial,
|
||||
TbSquare,
|
||||
TbBinaryTree,
|
||||
} from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const categories = [
|
||||
@@ -20,6 +27,7 @@ const categories = [
|
||||
{ name: "Share", icon: <TbShare /> },
|
||||
{ name: "SMTP", icon: <TbAt /> },
|
||||
{ name: "OAuth", icon: <TbSocial /> },
|
||||
{ name: "LDAP", icon: <TbBinaryTree /> },
|
||||
];
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
|
||||
@@ -33,6 +33,7 @@ const LogoConfigInput = ({
|
||||
value={logo}
|
||||
onChange={(v) => setLogo(v)}
|
||||
accept=".png"
|
||||
// @ts-ignore (https://github.com/mantinedev/mantine/issues/5401)
|
||||
placeholder={t("admin.config.general.logo.placeholder")}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
149
frontend/src/components/admin/shares/ManageShareTable.tsx
Normal file
149
frontend/src/components/admin/shares/ManageShareTable.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Group,
|
||||
MediaQuery,
|
||||
Skeleton,
|
||||
Table,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import moment from "moment";
|
||||
import { TbLink, TbTrash } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import { MyShare } from "../../../types/share.type";
|
||||
import { byteToHumanSizeString } from "../../../utils/fileSize.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import showShareLinkModal from "../../account/showShareLinkModal";
|
||||
|
||||
const ManageShareTable = ({
|
||||
shares,
|
||||
deleteShare,
|
||||
isLoading,
|
||||
}: {
|
||||
shares: MyShare[];
|
||||
deleteShare: (share: MyShare) => void;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const config = useConfig();
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "block", overflowX: "auto" }}>
|
||||
<Table verticalSpacing="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.id" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.name" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="admin.shares.table.username" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.visitors" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.size" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.expiresAt" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading
|
||||
? skeletonRows
|
||||
: shares.map((share) => (
|
||||
<tr key={share.id}>
|
||||
<td>{share.id}</td>
|
||||
<td>{share.name}</td>
|
||||
<td>
|
||||
{share.creator ? (
|
||||
share.creator.username
|
||||
) : (
|
||||
<Text color="dimmed">Anonymous</Text>
|
||||
)}
|
||||
</td>
|
||||
<td>{share.views}</td>
|
||||
<td>{byteToHumanSizeString(share.size)}</td>
|
||||
<td>
|
||||
{moment(share.expiration).unix() === 0
|
||||
? "Never"
|
||||
: moment(share.expiration).format("LLL")}
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("general.appUrl")}/s/${share.id}`,
|
||||
);
|
||||
toast.success(t("common.notify.copied"));
|
||||
} else {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
share.id,
|
||||
config.get("general.appUrl"),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TbLink />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => deleteShare(share)}
|
||||
>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const skeletonRows = [...Array(10)].map((v, i) => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<Skeleton key={i} height={20} />
|
||||
</td>
|
||||
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
|
||||
<td>
|
||||
<Skeleton key={i} height={20} />
|
||||
</td>
|
||||
</MediaQuery>
|
||||
<td>
|
||||
<Skeleton key={i} height={20} />
|
||||
</td>
|
||||
<td>
|
||||
<Skeleton key={i} height={20} />
|
||||
</td>
|
||||
<td>
|
||||
<Skeleton key={i} height={20} />
|
||||
</td>
|
||||
<td>
|
||||
<Skeleton key={i} height={20} />
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
export default ManageShareTable;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionIcon, Box, Group, Skeleton, Table } from "@mantine/core";
|
||||
import { ActionIcon, Badge, Box, Group, Skeleton, Table } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
||||
import User from "../../../types/user.type";
|
||||
@@ -40,21 +40,28 @@ const ManageUserTable = ({
|
||||
? skeletonRows
|
||||
: users.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>{user.username}</td>
|
||||
<td>
|
||||
{user.username}{" "}
|
||||
{user.isLdap ? (
|
||||
<Badge style={{ marginLeft: "1em" }}>LDAP</Badge>
|
||||
) : null}
|
||||
</td>
|
||||
<td>{user.email}</td>
|
||||
<td>{user.isAdmin && <TbCheck />}</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
showUpdateUserModal(modals, user, getUsers)
|
||||
}
|
||||
>
|
||||
<TbEdit />
|
||||
</ActionIcon>
|
||||
{user.isLdap ? null : (
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
showUpdateUserModal(modals, user, getUsers)
|
||||
}
|
||||
>
|
||||
<TbEdit />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="red"
|
||||
|
||||
@@ -25,8 +25,22 @@ import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
import { getOAuthIcon, getOAuthUrl } from "../../utils/oauth.util";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { safeRedirectPath } from "../../utils/router.util";
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
signInWith: {
|
||||
fontWeight: 500,
|
||||
"&:before": {
|
||||
content: "''",
|
||||
flex: 1,
|
||||
display: "block",
|
||||
},
|
||||
"&:after": {
|
||||
content: "''",
|
||||
flex: 1,
|
||||
display: "block",
|
||||
},
|
||||
},
|
||||
or: {
|
||||
"&:before": {
|
||||
content: "''",
|
||||
@@ -98,7 +112,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
);
|
||||
} else {
|
||||
await refreshUser();
|
||||
router.replace(redirectPath);
|
||||
router.replace(safeRedirectPath(redirectPath));
|
||||
}
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
@@ -127,49 +141,58 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
</Text>
|
||||
)}
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
signIn(values.emailOrUsername, values.password);
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
label={t("signin.input.email-or-username")}
|
||||
placeholder={t("signin.input.email-or-username.placeholder")}
|
||||
{...form.getInputProps("emailOrUsername")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t("signin.input.password")}
|
||||
placeholder={t("signin.input.password.placeholder")}
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
{config.get("smtp.enabled") && (
|
||||
<Group position="right" mt="xs">
|
||||
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
||||
<FormattedMessage id="resetPassword.title" />
|
||||
</Anchor>
|
||||
</Group>
|
||||
)}
|
||||
<Button fullWidth mt="xl" type="submit">
|
||||
<FormattedMessage id="signin.button.submit" />
|
||||
</Button>
|
||||
</form>
|
||||
{config.get("oauth.disablePassword") || (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
signIn(values.emailOrUsername, values.password);
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
label={t("signin.input.email-or-username")}
|
||||
placeholder={t("signin.input.email-or-username.placeholder")}
|
||||
{...form.getInputProps("emailOrUsername")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t("signin.input.password")}
|
||||
placeholder={t("signin.input.password.placeholder")}
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
{config.get("smtp.enabled") && (
|
||||
<Group position="right" mt="xs">
|
||||
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
||||
<FormattedMessage id="resetPassword.title" />
|
||||
</Anchor>
|
||||
</Group>
|
||||
)}
|
||||
<Button fullWidth mt="xl" type="submit">
|
||||
<FormattedMessage id="signin.button.submit" />
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
{oauth.length > 0 && (
|
||||
<Stack mt="xl">
|
||||
<Group align="center" className={classes.or}>
|
||||
<Text>{t("signIn.oauth.or")}</Text>
|
||||
</Group>
|
||||
<Stack mt={config.get("oauth.disablePassword") ? undefined : "xl"}>
|
||||
{config.get("oauth.disablePassword") ? (
|
||||
<Group align="center" className={classes.signInWith}>
|
||||
<Text>{t("signIn.oauth.signInWith")}</Text>
|
||||
</Group>
|
||||
) : (
|
||||
<Group align="center" className={classes.or}>
|
||||
<Text>{t("signIn.oauth.or")}</Text>
|
||||
</Group>
|
||||
)}
|
||||
<Group position="center">
|
||||
{oauth.map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
component="a"
|
||||
target="_blank"
|
||||
title={t(`signIn.oauth.${provider}`)}
|
||||
href={getOAuthUrl(config.get("general.appUrl"), provider)}
|
||||
variant="light"
|
||||
fullWidth
|
||||
>
|
||||
{getOAuthIcon(provider)}
|
||||
{"\u2002" + t(`signIn.oauth.${provider}`)}
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
PinInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useState } from "react";
|
||||
import authService from "../../services/auth.service";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { useRouter } from "next/router";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
import { safeRedirectPath } from "../../utils/router.util";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
function TotpForm({ redirectPath }: { redirectPath: string }) {
|
||||
const t = useTranslate();
|
||||
@@ -46,7 +47,7 @@ function TotpForm({ redirectPath }: { redirectPath: string }) {
|
||||
router.query.loginToken as string,
|
||||
);
|
||||
await refreshUser();
|
||||
await router.replace(redirectPath);
|
||||
await router.replace(safeRedirectPath(redirectPath));
|
||||
} catch (e) {
|
||||
toast.axiosError(e);
|
||||
form.setFieldError("code", "error");
|
||||
|
||||
@@ -39,7 +39,7 @@ const FileList = ({
|
||||
const t = useTranslate();
|
||||
|
||||
const [sort, setSort] = useState<TableSort>({
|
||||
property: undefined,
|
||||
property: "name",
|
||||
direction: "desc",
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getExpirationPreview } from "../../../utils/date.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import FileSizeInput from "../FileSizeInput";
|
||||
import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
|
||||
const showCreateReverseShareModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -61,10 +62,16 @@ const Body = ({
|
||||
sendEmailNotification: false,
|
||||
expiration_num: 1,
|
||||
expiration_unit: "-days",
|
||||
simplified: !!(getCookie("reverse-share.simplified") ?? false),
|
||||
publicAccess: !!(getCookie("reverse-share.public-access") ?? true),
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
// remember simplified and publicAccess in cookies
|
||||
setCookie("reverse-share.simplified", values.simplified);
|
||||
setCookie("reverse-share.public-access", values.publicAccess);
|
||||
|
||||
const expirationDate = moment().add(
|
||||
form.values.expiration_num,
|
||||
form.values.expiration_unit.replace(
|
||||
@@ -91,6 +98,8 @@ const Body = ({
|
||||
values.maxShareSize,
|
||||
values.maxUseCount,
|
||||
values.sendEmailNotification,
|
||||
values.simplified,
|
||||
values.publicAccess,
|
||||
)
|
||||
.then(({ link }) => {
|
||||
modals.closeAll();
|
||||
@@ -210,7 +219,28 @@ const Body = ({
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label={t("account.reverseShares.modal.simplified")}
|
||||
description={t(
|
||||
"account.reverseShares.modal.simplified.description",
|
||||
)}
|
||||
{...form.getInputProps("simplified", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label={t("account.reverseShares.modal.public-access")}
|
||||
description={t(
|
||||
"account.reverseShares.modal.public-access.description",
|
||||
)}
|
||||
{...form.getInputProps("publicAccess", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Button mt="md" type="submit">
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
|
||||
@@ -53,10 +53,18 @@ function CopyTextField(props: { link: string }) {
|
||||
</ActionIcon>
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
{window.isSecureContext && (
|
||||
<ActionIcon onClick={copyLink}>
|
||||
{checkState ? <TbCheck /> : <TbCopy />}
|
||||
</ActionIcon>
|
||||
<Tooltip
|
||||
label={t("common.button.clickToCopy")}
|
||||
position="top"
|
||||
offset={-2}
|
||||
openDelay={200}
|
||||
>
|
||||
<ActionIcon onClick={copyLink}>
|
||||
{checkState ? <TbCheck /> : <TbCopy />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -36,12 +36,12 @@ const Dropzone = ({
|
||||
title,
|
||||
isUploading,
|
||||
maxShareSize,
|
||||
showCreateUploadModalCallback,
|
||||
onFilesChanged,
|
||||
}: {
|
||||
title?: string;
|
||||
isUploading: boolean;
|
||||
maxShareSize: number;
|
||||
showCreateUploadModalCallback: (files: FileUpload[]) => void;
|
||||
onFilesChanged: (files: FileUpload[]) => void;
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
@@ -69,7 +69,7 @@ const Dropzone = ({
|
||||
newFile.uploadingProgress = 0;
|
||||
return newFile;
|
||||
});
|
||||
showCreateUploadModalCallback(files);
|
||||
onFilesChanged(files);
|
||||
}
|
||||
}}
|
||||
className={classes.dropzone}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { cleanNotifications } from "@mantine/notifications";
|
||||
import { AxiosError } from "axios";
|
||||
import { useRouter } from "next/router";
|
||||
import pLimit from "p-limit";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Dropzone from "../../components/upload/Dropzone";
|
||||
import FileList from "../../components/upload/FileList";
|
||||
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { FileListItem, FileMetaData, FileUpload } from "../../types/File.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const promiseLimit = pLimit(3);
|
||||
const chunkSize = 10 * 1024 * 1024; // 10MB
|
||||
let errorToastShown = false;
|
||||
|
||||
const EditableUpload = ({
|
||||
@@ -33,6 +30,8 @@ const EditableUpload = ({
|
||||
const router = useRouter();
|
||||
const config = useConfig();
|
||||
|
||||
const chunkSize = useRef(parseInt(config.get("share.chunkSize")));
|
||||
|
||||
const [existingFiles, setExistingFiles] =
|
||||
useState<Array<FileMetaData & { deleted?: boolean }>>(savedFiles);
|
||||
const [uploadingFiles, setUploadingFiles] = useState<FileUpload[]>([]);
|
||||
@@ -66,7 +65,7 @@ const EditableUpload = ({
|
||||
const fileUploadPromises = files.map(async (file, fileIndex) =>
|
||||
// Limit the number of concurrent uploads to 3
|
||||
promiseLimit(async () => {
|
||||
let fileId: string;
|
||||
let fileId: string | undefined;
|
||||
|
||||
const setFileProgress = (progress: number) => {
|
||||
setUploadingFiles((files) =>
|
||||
@@ -81,38 +80,30 @@ const EditableUpload = ({
|
||||
|
||||
setFileProgress(1);
|
||||
|
||||
let chunks = Math.ceil(file.size / chunkSize);
|
||||
let chunks = Math.ceil(file.size / chunkSize.current);
|
||||
|
||||
// If the file is 0 bytes, we still need to upload 1 chunk
|
||||
if (chunks == 0) chunks++;
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) {
|
||||
const from = chunkIndex * chunkSize;
|
||||
const to = from + chunkSize;
|
||||
const from = chunkIndex * chunkSize.current;
|
||||
const to = from + chunkSize.current;
|
||||
const blob = file.slice(from, to);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) =>
|
||||
await shareService
|
||||
.uploadFile(
|
||||
shareId,
|
||||
event,
|
||||
{
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
},
|
||||
chunkIndex,
|
||||
chunks,
|
||||
)
|
||||
.then((response) => {
|
||||
fileId = response.id;
|
||||
resolve(response);
|
||||
})
|
||||
.catch(reject);
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
await shareService
|
||||
.uploadFile(
|
||||
shareId,
|
||||
blob,
|
||||
{
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
},
|
||||
chunkIndex,
|
||||
chunks,
|
||||
)
|
||||
.then((response) => {
|
||||
fileId = response.id;
|
||||
});
|
||||
|
||||
setFileProgress(((chunkIndex + 1) / chunks) * 100);
|
||||
} catch (e) {
|
||||
@@ -226,7 +217,7 @@ const EditableUpload = ({
|
||||
<Dropzone
|
||||
title={t("share.edit.append-upload")}
|
||||
maxShareSize={maxShareSize}
|
||||
showCreateUploadModalCallback={appendFiles}
|
||||
onFilesChanged={appendFiles}
|
||||
isUploading={isUploading}
|
||||
/>
|
||||
{existingAndUploadedFiles.length > 0 && (
|
||||
|
||||
@@ -7,12 +7,12 @@ import { FormattedMessage } from "react-intl";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../../hooks/useTranslate.hook";
|
||||
import { Share } from "../../../types/share.type";
|
||||
import { CompletedShare } from "../../../types/share.type";
|
||||
import CopyTextField from "../CopyTextField";
|
||||
|
||||
const showCompletedUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
share: Share,
|
||||
share: CompletedShare,
|
||||
appUrl: string,
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
@@ -25,7 +25,7 @@ const showCompletedUploadModal = (
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
const Body = ({ share, appUrl }: { share: CompletedShare; appUrl: string }) => {
|
||||
const modals = useModals();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
@@ -35,6 +35,19 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
return (
|
||||
<Stack align="stretch">
|
||||
<CopyTextField link={link} />
|
||||
{share.notifyReverseShareCreator === true && (
|
||||
<Text
|
||||
size="sm"
|
||||
sx={(theme) => ({
|
||||
color:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.gray[3]
|
||||
: theme.colors.dark[4],
|
||||
})}
|
||||
>
|
||||
{t("upload.modal.completed.notified-reverse-share-creator")}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
|
||||
@@ -30,6 +30,8 @@ import shareService from "../../../services/share.service";
|
||||
import { FileUpload } from "../../../types/File.type";
|
||||
import { CreateShare } from "../../../types/share.type";
|
||||
import { getExpirationPreview } from "../../../utils/date.util";
|
||||
import React from "react";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
const showCreateUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -40,12 +42,26 @@ const showCreateUploadModal = (
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
simplified: boolean;
|
||||
},
|
||||
files: FileUpload[],
|
||||
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void,
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
|
||||
if (options.simplified) {
|
||||
return modals.openModal({
|
||||
title: t("upload.modal.title"),
|
||||
children: (
|
||||
<SimplifiedCreateUploadModalModal
|
||||
options={options}
|
||||
files={files}
|
||||
uploadCallback={uploadCallback}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return modals.openModal({
|
||||
title: t("upload.modal.title"),
|
||||
children: (
|
||||
@@ -58,6 +74,23 @@ const showCreateUploadModal = (
|
||||
});
|
||||
};
|
||||
|
||||
const generateLink = () =>
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substring(10, 17);
|
||||
|
||||
const generateAvailableLink = async (times = 10): Promise<string> => {
|
||||
if (times <= 0) {
|
||||
throw new Error("Could not generate available link");
|
||||
}
|
||||
const _link = generateLink();
|
||||
if (!(await shareService.isShareIdAvailable(_link))) {
|
||||
return await generateAvailableLink(times - 1);
|
||||
} else {
|
||||
return _link;
|
||||
}
|
||||
};
|
||||
|
||||
const CreateUploadModalBody = ({
|
||||
uploadCallback,
|
||||
files,
|
||||
@@ -77,9 +110,7 @@ const CreateUploadModalBody = ({
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7);
|
||||
const generatedLink = generateLink();
|
||||
|
||||
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
|
||||
|
||||
@@ -92,11 +123,16 @@ const CreateUploadModalBody = ({
|
||||
.matches(new RegExp("^[a-zA-Z0-9_-]*$"), {
|
||||
message: t("upload.modal.link.error.invalid"),
|
||||
}),
|
||||
name: yup
|
||||
.string()
|
||||
.transform((value) => value || undefined)
|
||||
.min(3, t("common.error.too-short", { length: 3 }))
|
||||
.max(30, t("common.error.too-long", { length: 30 })),
|
||||
password: yup
|
||||
.string()
|
||||
.transform((value) => value || undefined)
|
||||
.min(3)
|
||||
.max(30),
|
||||
.min(3, t("common.error.too-short", { length: 3 }))
|
||||
.max(30, t("common.error.too-long", { length: 30 })),
|
||||
maxViews: yup
|
||||
.number()
|
||||
.transform((value) => value || undefined)
|
||||
@@ -105,6 +141,7 @@ const CreateUploadModalBody = ({
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: undefined,
|
||||
link: generatedLink,
|
||||
recipients: [] as string[],
|
||||
password: undefined,
|
||||
@@ -154,6 +191,7 @@ const CreateUploadModalBody = ({
|
||||
uploadCallback(
|
||||
{
|
||||
id: values.link,
|
||||
name: values.name,
|
||||
expiration: expirationString,
|
||||
recipients: values.recipients,
|
||||
description: values.description,
|
||||
@@ -194,14 +232,7 @@ const CreateUploadModalBody = ({
|
||||
<Button
|
||||
style={{ flex: "0 0 auto" }}
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
"link",
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7),
|
||||
)
|
||||
}
|
||||
onClick={() => form.setFieldValue("link", generateLink())}
|
||||
>
|
||||
<FormattedMessage id="common.button.generate" />
|
||||
</Button>
|
||||
@@ -235,7 +266,6 @@ const CreateUploadModalBody = ({
|
||||
disabled={form.values.never_expires}
|
||||
{...form.getInputProps("expiration_unit")}
|
||||
data={[
|
||||
// Set the label to singular if the number is 1, else plural
|
||||
{
|
||||
value: "-minutes",
|
||||
label:
|
||||
@@ -308,14 +338,21 @@ const CreateUploadModalBody = ({
|
||||
<Accordion>
|
||||
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
|
||||
<Accordion.Control>
|
||||
<FormattedMessage id="upload.modal.accordion.description.title" />
|
||||
<FormattedMessage id="upload.modal.accordion.name-and-description.title" />
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack align="stretch">
|
||||
<TextInput
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.name-and-description.name.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Textarea
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.description.placeholder",
|
||||
"upload.modal.accordion.name-and-description.description.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
@@ -333,7 +370,9 @@ const CreateUploadModalBody = ({
|
||||
placeholder={t("upload.modal.accordion.email.placeholder")}
|
||||
searchable
|
||||
creatable
|
||||
autoComplete="email-recipients"
|
||||
id="recipient_email"
|
||||
autoComplete="email"
|
||||
type="email"
|
||||
getCreateLabel={(query) => `+ ${query}`}
|
||||
onCreate={(query) => {
|
||||
if (!query.match(/^\S+@\S+\.\S+$/)) {
|
||||
@@ -351,6 +390,25 @@ const CreateUploadModalBody = ({
|
||||
}
|
||||
}}
|
||||
{...form.getInputProps("recipients")}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
// Add email on comma or semicolon
|
||||
if (e.key === "," || e.key === ";") {
|
||||
e.preventDefault();
|
||||
const inputValue = (
|
||||
e.target as HTMLInputElement
|
||||
).value.trim();
|
||||
if (inputValue.match(/^\S+@\S+\.\S+$/)) {
|
||||
form.setFieldValue("recipients", [
|
||||
...form.values.recipients,
|
||||
inputValue,
|
||||
]);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
}
|
||||
} else if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
@@ -394,4 +452,108 @@ const CreateUploadModalBody = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SimplifiedCreateUploadModalModal = ({
|
||||
uploadCallback,
|
||||
files,
|
||||
options,
|
||||
}: {
|
||||
files: FileUpload[];
|
||||
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void;
|
||||
options: {
|
||||
isUserSignedIn: boolean;
|
||||
isReverseShare: boolean;
|
||||
appUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
};
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
name: yup
|
||||
.string()
|
||||
.transform((value) => value || undefined)
|
||||
.min(3, t("common.error.too-short", { length: 3 }))
|
||||
.max(30, t("common.error.too-long", { length: 30 })),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
},
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
const link = await generateAvailableLink().catch(() => {
|
||||
toast.error(t("upload.modal.link.error.taken"));
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadCallback(
|
||||
{
|
||||
id: link,
|
||||
name: values.name,
|
||||
expiration: "never",
|
||||
recipients: [],
|
||||
description: values.description,
|
||||
security: {
|
||||
password: undefined,
|
||||
maxViews: undefined,
|
||||
},
|
||||
},
|
||||
files,
|
||||
);
|
||||
modals.closeAll();
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{showNotSignedInAlert && !options.isUserSignedIn && (
|
||||
<Alert
|
||||
withCloseButton
|
||||
onClose={() => setShowNotSignedInAlert(false)}
|
||||
icon={<TbAlertCircle size={16} />}
|
||||
title={t("upload.modal.not-signed-in")}
|
||||
color="yellow"
|
||||
>
|
||||
<FormattedMessage id="upload.modal.not-signed-in-description" />
|
||||
</Alert>
|
||||
)}
|
||||
<form onSubmit={onSubmit}>
|
||||
<Stack align="stretch">
|
||||
<Stack align="stretch">
|
||||
<TextInput
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.name-and-description.name.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Textarea
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.name-and-description.description.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
</Stack>
|
||||
<Button type="submit" data-autofocus>
|
||||
<FormattedMessage id="common.button.share" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default showCreateUploadModal;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import arabic from "./translations/ar-EG";
|
||||
import danish from "./translations/da-DK";
|
||||
import german from "./translations/de-DE";
|
||||
import greek from "./translations/el-GR";
|
||||
@@ -5,8 +6,10 @@ import english from "./translations/en-US";
|
||||
import spanish from "./translations/es-ES";
|
||||
import finnish from "./translations/fi-FI";
|
||||
import french from "./translations/fr-FR";
|
||||
import hungarian from "./translations/hu-HU";
|
||||
import italian from "./translations/it-IT";
|
||||
import japanese from "./translations/ja-JP";
|
||||
import korean from "./translations/ko-KR";
|
||||
import dutch from "./translations/nl-BE";
|
||||
import polish from "./translations/pl-PL";
|
||||
import portuguese from "./translations/pt-BR";
|
||||
@@ -15,8 +18,11 @@ import slovenian from "./translations/sl-SI";
|
||||
import serbian from "./translations/sr-SP";
|
||||
import swedish from "./translations/sv-SE";
|
||||
import thai from "./translations/th-TH";
|
||||
import ukrainian from "./translations/uk-UA";
|
||||
import chineseSimplified from "./translations/zh-CN";
|
||||
import chineseTraditional from "./translations/zh-TW";
|
||||
import turkish from "./translations/tr-TR";
|
||||
import czech from "./translations/cs-CZ";
|
||||
|
||||
export const LOCALES = {
|
||||
ENGLISH: {
|
||||
@@ -69,6 +75,11 @@ export const LOCALES = {
|
||||
code: "ru-RU",
|
||||
messages: russian,
|
||||
},
|
||||
UKRAINIAN: {
|
||||
name: "Українська",
|
||||
code: "uk-UA",
|
||||
messages: ukrainian,
|
||||
},
|
||||
THAI: {
|
||||
name: "ไทย",
|
||||
code: "th-TH",
|
||||
@@ -114,4 +125,29 @@ export const LOCALES = {
|
||||
code: "sl-SI",
|
||||
messages: slovenian,
|
||||
},
|
||||
ARABIC: {
|
||||
name: "العربية",
|
||||
code: "ar-EG",
|
||||
messages: arabic,
|
||||
},
|
||||
HUNGARIAN: {
|
||||
name: "Hungarian",
|
||||
code: "hu-HU",
|
||||
messages: hungarian,
|
||||
},
|
||||
KOREAN: {
|
||||
name: "한국어",
|
||||
code: "ko-KR",
|
||||
messages: korean,
|
||||
},
|
||||
TURKISH: {
|
||||
name: "Türkçe",
|
||||
code: "tr-TR",
|
||||
messages: turkish,
|
||||
},
|
||||
CZECH: {
|
||||
name: "Čeština",
|
||||
code: "cs-CZ",
|
||||
messages: czech,
|
||||
},
|
||||
};
|
||||
|
||||
470
frontend/src/i18n/translations/ar-EG.ts
Normal file
470
frontend/src/i18n/translations/ar-EG.ts
Normal file
@@ -0,0 +1,470 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "رفع",
|
||||
"navbar.signin": "تسجيل الدخول",
|
||||
"navbar.home": "الصفحة الرئيسية",
|
||||
"navbar.signup": "إنشاء حساب",
|
||||
"navbar.links.shares": "مشاركاتي",
|
||||
"navbar.links.reverse": "مشاركاتي العكسية",
|
||||
"navbar.avatar.account": "حسابي",
|
||||
"navbar.avatar.admin": "الإدارة",
|
||||
"navbar.avatar.signout": "تسجيل الخروج",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "منصة لمشاركة الملفات <h>باستضافة ذاتية</h>.",
|
||||
"home.description": "أحقًا تريد تسليم ملفاتك الشخصية لطرف ثالث مثل WeTransfer؟",
|
||||
"home.bullet.a.name": "استضافة ذاتية",
|
||||
"home.bullet.a.description": "قم باستضافة Pingvin Share على جهازك.",
|
||||
"home.bullet.b.name": "الخصوصية",
|
||||
"home.bullet.b.description": "ملفاتك تخصّك وحدك فقط، ولا ينبغي أبدًا أن تقع بأيدي طرفٍ ثالث.",
|
||||
"home.bullet.c.name": "ليس هناك أية قيود على حجم الملفات",
|
||||
"home.bullet.c.description": "ارفع أي ملف تريده مهما كان حجمه كبيرًا. إن مساحة قرصك الصلب هي المحدد الوحيد هنا.",
|
||||
"home.button.start": "ابدأ",
|
||||
"home.button.source": "النص البرمجي المصدري",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "أهلًا بعودتك",
|
||||
"signin.description": "ليس لديك حساب؟",
|
||||
"signin.button.signup": "إنشاء حساب",
|
||||
"signin.input.email-or-username": "البريد أو اسم المستخدم",
|
||||
"signin.input.email-or-username.placeholder": "بريدك أو اسم المستخدم",
|
||||
"signin.input.password": "كلمة السر",
|
||||
"signin.input.password.placeholder": "كلمة السر",
|
||||
"signin.button.submit": "تسجيل الدخول",
|
||||
"signIn.notify.totp-required.title": "إن المصادقة الثنائية ضرورية",
|
||||
"signIn.notify.totp-required.description": "فضلًا أدخل رمز المصادقة الثنائية",
|
||||
"signIn.oauth.or": "أو",
|
||||
"signIn.oauth.signInWith": "تسجيل الدخول بواسطة تطبيق",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "أنشئ حسابًا",
|
||||
"signup.description": "لديك حساب بالفعل؟",
|
||||
"signup.button.signin": "تسجيل الدخول",
|
||||
"signup.input.username": "اسم المستخدم",
|
||||
"signup.input.username.placeholder": "اسم المستخدم",
|
||||
"signup.input.email": "البريد",
|
||||
"signup.input.email.placeholder": "بريدك",
|
||||
"signup.button.submit": "لنبدأ",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "كلمة المرور لمرة واحدة المؤقتة TOTP",
|
||||
"totp.button.signIn": "تسجيل الدخول",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "نسيت كلمة سرّك؟",
|
||||
"resetPassword.description": "اكتب بريدك لتعيد تعيين كلمة السر.",
|
||||
"resetPassword.notify.success": "إذا كان هذا البريد مسجلًا لدينا فستصله الآن رسالة فيها رابط لإعادة تعيين كلمة السرّ.",
|
||||
"resetPassword.button.back": "العودة لصفحة تسجيل الدخول",
|
||||
"resetPassword.text.resetPassword": "إعادة تعيين كلمة السر",
|
||||
"resetPassword.text.enterNewPassword": "أدخل كلمة السر الجديدة",
|
||||
"resetPassword.input.password": "كلمة السر الجديدة",
|
||||
"resetPassword.notify.passwordReset": "أعدتَ تعيين كلمة السر بنجاح.",
|
||||
// /account
|
||||
"account.title": "حسابي",
|
||||
"account.card.info.title": "معلومات الحساب",
|
||||
"account.card.info.username": "اسم المستخدم",
|
||||
"account.card.info.email": "البريد",
|
||||
"account.notify.info.success": "تم تحديث الحساب بنجاح",
|
||||
"account.card.password.title": "كلمة السر",
|
||||
"account.card.password.old": "كلمة السر القديمة",
|
||||
"account.card.password.new": "كلمة السر الجديدة",
|
||||
"account.card.password.noPasswordSet": "ليس لحسابك كلمة سر. إذا أردت تسجيل الدخول باستخدام البريد وكلمة سر، فعليك أن تُعيِّن كلمة سر.",
|
||||
"account.notify.password.success": "غيرت كلمة السر بنجاح",
|
||||
"account.card.oauth.title": "الدخول بحساب تواصل اجتماعي",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "ربط",
|
||||
"account.card.oauth.unlink": "فك الربط",
|
||||
"account.card.oauth.unlinked": "تم فك الربط",
|
||||
"account.modal.unlink.title": "فك ربط الحساب",
|
||||
"account.modal.unlink.description": "قد يؤدي إلغاء ربط حساباتك الاجتماعية إلى فقدان وصولك لحسابك إذا كنت لا تتذكر اسم المستخدم وكلمة السر الخاصة بك.",
|
||||
"account.notify.oauth.unlinked.success": "تم فك الربط بنجاح",
|
||||
"account.card.security.title": "الأمان",
|
||||
"account.card.security.totp.enable.description": "اكتب كلمة سرّك لبدء تمكين TOTP",
|
||||
"account.card.security.totp.disable.description": "اكتب كلمة سرّك لتعطيل TOTP",
|
||||
"account.card.security.totp.button.start": "ابدأ",
|
||||
"account.modal.totp.title": "تمكين TOTP",
|
||||
"account.modal.totp.step1": "الخطوة 1: أضف تطبيق المصادقة",
|
||||
"account.modal.totp.step2": "الخطوة 2: تحقّق من صحة رمزك",
|
||||
"account.modal.totp.enterManually": "أدخل يدوياً",
|
||||
"account.modal.totp.code": "الرمز",
|
||||
"common.button.clickToCopy": "انقر للنسخ",
|
||||
"account.modal.totp.verify": "تحقق",
|
||||
"account.notify.totp.disable": "تم تعطيل TOTP بنجاح",
|
||||
"account.notify.totp.enable": "تم تمكين TOTP بنجاح",
|
||||
"account.card.language.title": "اللغة",
|
||||
"account.card.language.description": "يقوم المجتمع بترجمة هذا المشروع. ربما بعض اللغات لم تكتمل ترجمتها بعد.",
|
||||
"account.card.color.title": "نظام الألوان",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "داكن",
|
||||
"account.theme.light": "فاتح",
|
||||
"account.theme.system": "حسب النظام",
|
||||
"account.button.delete": "حذف الحساب",
|
||||
"account.modal.delete.title": "حذف الحساب",
|
||||
"account.modal.delete.description": "هل تريد حقاً حذف حسابك بما في ذلك جميع مشاركاتك النشطة؟",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "مشاركاتي",
|
||||
"account.shares.title.empty": "المكان خالٍ هنا 👀",
|
||||
"account.shares.description.empty": "ليس لديك أي مشاركات.",
|
||||
"account.shares.button.create": "أنشئ واحدًا",
|
||||
"account.shares.info.title": "معلومات المشاركة",
|
||||
"account.shares.table.id": "الرقم التعريفي",
|
||||
"account.shares.table.name": "الاسم",
|
||||
"account.shares.table.description": "الوصف",
|
||||
"account.shares.table.visitors": "الزوار",
|
||||
"account.shares.table.expiresAt": "تاريخ انتهاء الصلاحية",
|
||||
"account.shares.table.createdAt": "تاريخ الإنشاء",
|
||||
"account.shares.table.size": "الحجم",
|
||||
"account.shares.modal.share-informations": "معلومات المشاركة",
|
||||
"account.shares.modal.share-link": "رابط المشاركة",
|
||||
"account.shares.modal.delete.title": "حذف المشاركة {share}",
|
||||
"account.shares.modal.delete.description": "هل تريد حذف هذه المشاركة حقاً؟",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "المشاركات العكسية",
|
||||
"account.reverseShares.description": "تسمح لك المشاركة العكسية بإنشاء رابط فريد يسمح للمستخدمين الخارجيين بإنشاء مشاركة.",
|
||||
"account.reverseShares.title.empty": "المكان خالٍ هنا 👀",
|
||||
"account.reverseShares.description.empty": "ليس لديك أي مشاركات عكسية.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "إنشاء مشاركة عكسية",
|
||||
"account.reverseShares.modal.expiration.label": "انتهاء الصلاحية",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "دقيقة",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "دقائق",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "ساعة",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "ساعات",
|
||||
"account.reverseShares.modal.expiration.day-singular": "يوم",
|
||||
"account.reverseShares.modal.expiration.day-plural": "أيام",
|
||||
"account.reverseShares.modal.expiration.week-singular": "أسبوع",
|
||||
"account.reverseShares.modal.expiration.week-plural": "أسابيع",
|
||||
"account.reverseShares.modal.expiration.month-singular": "شهر",
|
||||
"account.reverseShares.modal.expiration.month-plural": "أشهر",
|
||||
"account.reverseShares.modal.expiration.year-singular": "سنة",
|
||||
"account.reverseShares.modal.expiration.year-plural": "سنوات",
|
||||
"account.reverseShares.modal.max-size.label": "الحد الأقصى لحجم المشاركة",
|
||||
"account.reverseShares.modal.send-email": "أرسل إشعارًا بالبريد",
|
||||
"account.reverseShares.modal.send-email.description": "إرسال إشعار بالبريد الإلكتروني عند إنشاء مشاركة باستخدام رابط المشاركة العكسي هذا.",
|
||||
"account.reverseShares.modal.simplified": "Simple mode",
|
||||
"account.reverseShares.modal.simplified.description": "Make it easy for the person uploading the file to share it with you. They will be able to customize only the name and description of the share.",
|
||||
"account.reverseShares.modal.public-access": "Public access",
|
||||
"account.reverseShares.modal.public-access.description": "Make the created shares with this reverse share public. If disabled, only you and the creator of the share can view it.",
|
||||
"account.reverseShares.modal.max-use.label": "الحد الأقصى لعدد الاستخدامات",
|
||||
"account.reverseShares.modal.max-use.description": "أقصى عدد من المرّات التي يمكن فيها استخدام هذا الرابط لإنشاء مشاركة.",
|
||||
"account.reverseShare.never-expires": "لن تنتهي صلاحية هذه المشاركة العكسية أبدًا.",
|
||||
"account.reverseShare.expires-on": "هذه المشاركة العكسية ستنتهي صلاحيتها في {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "لم يتم إنشاء أي مشاركة بعد",
|
||||
"account.reverseShares.table.count.singular": "مشاركة",
|
||||
"account.reverseShares.table.count.plural": "مشاركات",
|
||||
"account.reverseShares.table.shares": "مشاركات",
|
||||
"account.reverseShares.table.remaining": "الاستخدامات المتبقية",
|
||||
"account.reverseShares.table.max-size": "الحد الأقصى لحجم المشاركة",
|
||||
"account.reverseShares.table.expires": "تاريخ انتهاء الصلاحية",
|
||||
"account.reverseShares.modal.reverse-share-link": "رابط المشاركة العكسية",
|
||||
"account.reverseShares.modal.delete.title": "حذف المشاركة العكسية",
|
||||
"account.reverseShares.modal.delete.description": "هل تريد حقاً حذف هذه المشاركة العكسية؟ إذا قمت بذلك، فسيتم حذف المشاركات المرتبطة بها أيضاً.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "الإدارة",
|
||||
"admin.button.users": "إدارة المستخدم",
|
||||
"admin.button.shares": "إدارة المشاركة",
|
||||
"admin.button.config": "الإعدادات",
|
||||
"admin.version": "الإصدار",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "إدارة المستخدم",
|
||||
"admin.users.table.username": "اسم المستخدم",
|
||||
"admin.users.table.email": "البريد",
|
||||
"admin.users.table.admin": "المدير",
|
||||
"admin.users.edit.update.title": "تحديث المستخدم {username}",
|
||||
"admin.users.edit.update.admin-privileges": "صلاحيات المدير",
|
||||
"admin.users.edit.update.change-password.title": "تغيير كلمة السر",
|
||||
"admin.users.edit.update.change-password.field": "كلمة السر الجديدة",
|
||||
"admin.users.edit.update.change-password.button": "حفظ كلمة السر الجديدة",
|
||||
"admin.users.edit.update.notify.password.success": "غيرت كلمة السر بنجاح",
|
||||
"admin.users.edit.delete.title": "حذف المستخدم {username}",
|
||||
"admin.users.edit.delete.description": "هل تريد حقاً حذف هذا المستخدم وكل مشاركاته؟",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "أنشئ مستخدمًا",
|
||||
"admin.users.modal.create.username": "اسم المستخدم",
|
||||
"admin.users.modal.create.email": "البريد",
|
||||
"admin.users.modal.create.password": "كلمة السر",
|
||||
"admin.users.modal.create.manual-password": "تعيين كلمة السر يدوياً",
|
||||
"admin.users.modal.create.manual-password.description": "بدون هذا الخيار، سيتلقى المستخدم رسالة بريد إلكتروني فيها رابط لتعيين كلمة السر الخاصة به.",
|
||||
"admin.users.modal.create.admin": "صلاحيات المدير",
|
||||
"admin.users.modal.create.admin.description": "مع هذا الخيار، سيتمكن المستخدم من الدخول إلى لوحة الإدارة.",
|
||||
// END /admin/users
|
||||
// /admin/shares
|
||||
"admin.shares.title": "إدارة المشاركة",
|
||||
"admin.shares.table.id": "معرّف المشاركة",
|
||||
"admin.shares.table.username": "المُنشئ",
|
||||
"admin.shares.table.visitors": "الزوار",
|
||||
"admin.shares.table.expires": "تاريخ انتهاء الصلاحية",
|
||||
"admin.shares.edit.delete.title": "حذف المشاركة {id}",
|
||||
"admin.shares.edit.delete.description": "هل تريد حذف هذه المشاركة حقاً؟",
|
||||
// END /admin/shares
|
||||
// /upload
|
||||
"upload.title": "رفع",
|
||||
"upload.notify.generic-error": "حدث خطأ أثناء إنهاء مشاركتك.",
|
||||
"upload.notify.count-failed": "فشل رفع {count} ملفات. تجري المحاولة مجددًا.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "رفع الملفات",
|
||||
"upload.dropzone.description": "اسحب الملفات إلى هنا لبدء مشاركتك. يمكننا فقط قبول الملفات التي لا يزيد حجمها عن {maxSize} بالمجمل.",
|
||||
"upload.dropzone.notify.file-too-big": "تتجاوز ملفاتك الحجم الأقصى للمشاركة والذي هو {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "الاسم",
|
||||
"upload.filelist.size": "الحجم",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "إنشاء مشاركة",
|
||||
"upload.modal.link.error.invalid": "يمكن أن يحتوي فقط على الأحرف والأرقام والشرطات السفلية والواصلات",
|
||||
"upload.modal.link.error.taken": "هذا الرابط مستخدم مسبقاً",
|
||||
"upload.modal.not-signed-in": "لم تقم بتسجيل الدخول",
|
||||
"upload.modal.not-signed-in-description": "لن تتمكن من حذف مشاركتك يدوياً أو عرض عدد الزوار.",
|
||||
"upload.modal.expires.never": "أبدًا",
|
||||
"upload.modal.expires.never-long": "لا تنتهي الصلاحية أبداً",
|
||||
"upload.modal.expires.error.too-long": "انتهاء الصلاحية يتجاوز الحد الأقصى لتاريخ انتهاء الصلاحية والذي هو {max}.",
|
||||
"upload.modal.link.label": "الرابط",
|
||||
"upload.modal.expires.label": "انتهاء الصلاحية",
|
||||
"upload.modal.expires.minute-singular": "دقيقة",
|
||||
"upload.modal.expires.minute-plural": "دقائق",
|
||||
"upload.modal.expires.hour-singular": "ساعة",
|
||||
"upload.modal.expires.hour-plural": "ساعات",
|
||||
"upload.modal.expires.day-singular": "يوم",
|
||||
"upload.modal.expires.day-plural": "أيام",
|
||||
"upload.modal.expires.week-singular": "أسبوع",
|
||||
"upload.modal.expires.week-plural": "أسابيع",
|
||||
"upload.modal.expires.month-singular": "شهر",
|
||||
"upload.modal.expires.month-plural": "أشهر",
|
||||
"upload.modal.expires.year-singular": "سنة",
|
||||
"upload.modal.expires.year-plural": "سنوات",
|
||||
"upload.modal.accordion.name-and-description.title": "الاسم والوصف",
|
||||
"upload.modal.accordion.name-and-description.name.placeholder": "الاسم",
|
||||
"upload.modal.accordion.name-and-description.description.placeholder": "ملاحظة لمستقبلي هذه المشاركة",
|
||||
"upload.modal.accordion.email.title": "مستلمو البريد الإلكتروني",
|
||||
"upload.modal.accordion.email.placeholder": "أدخل مستلمي البريد",
|
||||
"upload.modal.accordion.email.invalid-email": "عنوان البريد غير صحيح",
|
||||
"upload.modal.accordion.security.title": "خيارات الأمان",
|
||||
"upload.modal.accordion.security.password.label": "الحماية بكلمة السر",
|
||||
"upload.modal.accordion.security.password.placeholder": "لا توجد كلمة سر",
|
||||
"upload.modal.accordion.security.max-views.label": "الحد الأقصى للمشاهدات",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "لا يوجد حد",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "لن تنتهي صلاحية هذه المشاركة أبدًا.",
|
||||
"upload.modal.completed.expires-on": "هذه المشاركة ستنتهي صلاحيتها في {expiration}.",
|
||||
"upload.modal.completed.share-ready": "المشاركة جاهزة",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "We have notified the creator of the reverse share. You can also manually share this link with them through other means.",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "المشاركة {shareId}",
|
||||
"share.description": "انظر ما الذي شاركته معك!",
|
||||
"share.error.visitor-limit-exceeded.title": "تم تجاوز حد المشاهدات",
|
||||
"share.error.visitor-limit-exceeded.description": "تم تجاوز الحد الأقصى لزوار هذه المشاركة.",
|
||||
"share.error.removed.title": "تمت إزالة المشاركة",
|
||||
"share.error.not-found.title": "المشاركة غير موجودة",
|
||||
"share.error.not-found.description": "المشاركة التي تبحث عنها غير موجودة.",
|
||||
"share.error.access-denied.title": "Private share",
|
||||
"share.error.access-denied.description": "The current account does not have permission to access this share",
|
||||
"share.modal.password.title": "كلمة السر مطلوبة",
|
||||
"share.modal.password.description": "للوصول إلى هذه المشاركة الرجاء إدخال كلمة سر المشاركة.",
|
||||
"share.modal.password": "كلمة السر",
|
||||
"share.modal.error.invalid-password": "كلمة السر غير صحيحة",
|
||||
"share.button.download-all": "تنزيل الكل",
|
||||
"share.notify.download-all-preparing": "يتم تحضير المشاركة. حاول مرة أخرى في بضع دقائق.",
|
||||
"share.modal.file-link": "رابط الملف",
|
||||
"share.table.name": "الاسم",
|
||||
"share.table.size": "الحجم",
|
||||
"share.modal.file-preview.error.not-supported.title": "المعاينة غير مدعومة",
|
||||
"share.modal.file-preview.error.not-supported.description": "معاينة هذا النوع من الملفات غير مدعومة. الرجاء تنزيل الملف لعرضه.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "تحرير {shareId}",
|
||||
"share.edit.append-upload": "إضافة ملف",
|
||||
"share.edit.notify.generic-error": "حدث خطأ أثناء إنهاء مشاركتك.",
|
||||
"share.edit.notify.save-success": "تم تحديث المشاركة بنجاح",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "الإعدادات",
|
||||
"admin.config.category.general": "عام",
|
||||
"admin.config.category.share": "مشاركة",
|
||||
"admin.config.category.email": "البريد",
|
||||
"admin.config.category.smtp": "بروتوكول نقل البريد البسيط SMTP",
|
||||
"admin.config.category.oauth": "الدخول بحساب تواصل اجتماعي",
|
||||
"admin.config.general.app-name": "اسم التطبيق",
|
||||
"admin.config.general.app-name.description": "اسم التطبيق",
|
||||
"admin.config.general.app-url": "رابط التطبيق",
|
||||
"admin.config.general.app-url.description": "الرابط الذي تكون مشاركة Pingvin صالحة عليه",
|
||||
"admin.config.general.show-home-page": "إظهار الصفحة الرئيسية",
|
||||
"admin.config.general.show-home-page.description": "تحديد ما إذا كان سيتم عرض الصفحة الرئيسية",
|
||||
"admin.config.general.session-duration": "مدة الجلسة",
|
||||
"admin.config.general.session-duration.description": "الوقت بالساعات الذي يجب على المستخدم بعده إعادة تسجيل الدخول (الافتراضي: 3 أشهر).",
|
||||
"admin.config.general.logo": "الشعار",
|
||||
"admin.config.general.logo.description": "يمكنك تغيير شعارك عن طريق تحميل صورة جديدة. يجب أن تكون الصورة PNG ويجب أن يكون تنسيقها 1:1.",
|
||||
"admin.config.general.logo.placeholder": "اختر صورة",
|
||||
"admin.config.email.enable-share-email-recipients": "تفعيل مستلمي البريد الإلكتروني لهذه المشاركة",
|
||||
"admin.config.email.enable-share-email-recipients.description": "السماح لرسائل البريد بأن تُشارك المستلمين. لا تفعّل هذا الخيار ما لم تفعّل SMTP مسبقًا.",
|
||||
"admin.config.email.share-recipients-subject": "عنوان الرسالة لمستلمي المشاركة",
|
||||
"admin.config.email.share-recipients-subject.description": "عنوان البريد الذي سيُرسَل لمستقبِلي المشاركة.",
|
||||
"admin.config.email.share-recipients-message": "رسالتك لمستقبِلي المشاركة",
|
||||
"admin.config.email.share-recipients-message.description": "الرسالة التي ستُرسل لمستقبِلي المشاركة. يمكنك استخدام هذه المتغيرات:\n{creator} - اسم المستخدم الذي أنشأ المشاركة\n{shareUrl} - رابط المشاركة\n{desc} - وصف المشاركة\n{expires} - تاريخ انتهاء صلاحية المشاركة\nستتم كتابة قيم هذه المتغيرات تلقائيًا.",
|
||||
"admin.config.email.reverse-share-subject": "عنوان المشاركة العكسية",
|
||||
"admin.config.email.reverse-share-subject.description": "عنوان البريد الذي سيُرسل عندما يُنشئ شخص ما مشاركةً باستخدام رابط المشاركة العكسية الخاص بك.",
|
||||
"admin.config.email.reverse-share-message": "رسالة المشاركة العكسية",
|
||||
"admin.config.email.reverse-share-message.description": "الرسالة التي ستُرسل عندما يُنشئ شخص ما مشاركة باستخدام رابط المشاركة الخاص بك. سيُوضع اسم المُنشِئ ورابط المشاركة مكان {shareUrl}.",
|
||||
"admin.config.email.reset-password-subject": "رسالة إعادة تعيين كلمة السر",
|
||||
"admin.config.email.reset-password-subject.description": "عنوان البريد الذي سيُرسل حين يطلب مستخدم ما إعادة تعيين كلمة سرّه.",
|
||||
"admin.config.email.reset-password-message": "رسالة إعادة تعيين كلمة السر",
|
||||
"admin.config.email.reset-password-message.description": "الرسالة التي ستُرسل عندما يطلب المستخدم إعادة تعيين كلمة سرّه. سيُوضع رابط إعادة تعيين كلمة السر مكان {url}.",
|
||||
"admin.config.email.invite-subject": "عنوان الدعوة",
|
||||
"admin.config.email.invite-subject.description": "عنوان البريد الذي سيُرسل عندما يقوم المشرف بدعوة مستخدم ما.",
|
||||
"admin.config.email.invite-message": "رسالة الدعوة",
|
||||
"admin.config.email.invite-message.description": "الرسالة التي ستُرسل عندما يدعو مشرفٌ مستخدمًا. سيُوضع رابط الدعوة مكان {url} وكلمة السر مكان {password}.",
|
||||
"admin.config.share.allow-registration": "السماح بالتسجيل",
|
||||
"admin.config.share.allow-registration.description": "إتاحة تسجيل حساب جديد",
|
||||
"admin.config.share.allow-unauthenticated-shares": "السماح بالمشاركات غير المصادق عليها",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "إتاحة إنشاء المشاركات للمستخدمين غير الموثقين",
|
||||
"admin.config.share.max-expiration": "أبعد زمن لانتهاء الصلاحية",
|
||||
"admin.config.share.max-expiration.description": "أطول زمن لانتهاء صلاحية المشاركات بالساعات. الصفر يعني أن المشاركة لن تنتهي صلاحيتها.",
|
||||
"admin.config.share.max-size": "أكبر حجم",
|
||||
"admin.config.share.max-size.description": "أكبر حجم للمشاركة مقيسًا بالبايت",
|
||||
"admin.config.share.zip-compression-level": "مستوى ضغط الZip",
|
||||
"admin.config.share.zip-compression-level.description": "ضبط الميزان بين حجم الملف وسرعة الضغط. يمكنك إدخال قيم بين 0 إلى 9، حيث 0 تعني بدون ضغط و9 تعني أقصى ضغط. ",
|
||||
"admin.config.share.chunk-size": "حجم القطعة",
|
||||
"admin.config.share.chunk-size.description": "ضبط حجم القطعة (بالبايت) لملفاتك المرفوعة للموازنة بين الكفاءة والفعالية حسب قوة اتصالك بالإنترنت. القطع الأصغر يمكن أن ترفع معدل النجاح في حال كان اتصالك بالإنترنت غير مستقر، بينما القطع الأكبر يمكنها أن تُسرّع رفع الملفات في حال كان الاتصال بالإنترنت مستقرًا.",
|
||||
"admin.config.share.auto-open-share-modal": "Auto open create share modal",
|
||||
"admin.config.share.auto-open-share-modal.description": "The share creation modal automatically appears when a user selects files, eliminating the need to manually click the button.",
|
||||
"admin.config.smtp.enabled": "مفعل",
|
||||
"admin.config.smtp.enabled.description": "تفعيل الـSMTP. لا تفعّله إلا إذا قمت بإدخال المضيف، والمنفذ، والبريد الإلكتروني، واسم المستخدم، وكلمة السر لخادم الـSMTP.",
|
||||
"admin.config.smtp.host": "المُضيف",
|
||||
"admin.config.smtp.host.description": "مضيف خادم الـSMTP",
|
||||
"admin.config.smtp.port": "المنفذ",
|
||||
"admin.config.smtp.port.description": "منفذ خادم الـSMTP",
|
||||
"admin.config.smtp.email": "البريد الإلكتروني",
|
||||
"admin.config.smtp.email.description": "عنوان البريد الذي ستُرسَل الرسائل منه",
|
||||
"admin.config.smtp.username": "اسم المستخدم",
|
||||
"admin.config.smtp.username.description": "اسم المستخدم لخادم الـSMTP",
|
||||
"admin.config.smtp.password": "كلمة السر",
|
||||
"admin.config.smtp.password.description": "كلمة السر لخادم الـSMTP",
|
||||
"admin.config.smtp.button.test": "إرسال رسالة بريد تجريبية",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "Trust unauthorized SMTP server certificates",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Only set this to true if you need to trust self signed certificates.",
|
||||
"admin.config.oauth.allow-registration": "السماح بتسجيل الحسابات الجديدة",
|
||||
"admin.config.oauth.allow-registration.description": "السماح للمستخدمين بالدخول بواسطة حساباتهم الاجتماعية",
|
||||
"admin.config.oauth.ignore-totp": "تجاهل TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "تجاهل TOTP إذا دخل المستخدم بحسابه الاجتماعي",
|
||||
"admin.config.oauth.disable-password": "تعطيل تسجيل الدخول باستخدام كلمة السر",
|
||||
"admin.config.oauth.disable-password.description": "Whether to disable password login\nMake sure that an OAuth provider is properly configured before activating this configuration to avoid being locked out.",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "تفعيل خيار الدخول بحساب GitHub",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "معرف العميل لتطبيق GitHub OAuth",
|
||||
"admin.config.oauth.github-client-secret": "الرمز السرّي لـGitHub Client",
|
||||
"admin.config.oauth.github-client-secret.description": "الرّمز السرّي للعميل لتطبيق GitHub OAuth",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "تفعيل خيار الدخول بحساب Google",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "معرف العميل لتطبيق Google OAuth",
|
||||
"admin.config.oauth.google-client-secret": "الرمز السرّي لـ Google Client",
|
||||
"admin.config.oauth.google-client-secret.description": "الرّمز السرّي للعميل لتطبيق Google OAuth",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "تفعيل خيار الدخول بحساب Microsoft",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "معرف Tenant لتطبيق مايكروسوفت OAuth\nالشائع: يمكن للمستخدمين الذين لديهم حساب مايكروسوفت شخصي وحساب عمل أو مدرسة من معرف Microsoft Entra أن يسجلوا الدخول إلى التطبيق. بالنسبة المؤسسات: يمكن فقط للمستخدمين الذين لديهم حسابات عمل أو مدرسة من Microsoft Entra ID تسجيل الدخول إلى التطبيق.\nالمستهلكين: يمكن فقط للمستخدمين الذين لديهم حساب مايكروسوفت الشخصي تسجيل الدخول إلى التطبيق.\nاسم نطاق مستأجر Microsoft Entra أو معرف المستأجر بتنسيق GUID: يمكن فقط للمستخدمين من مستأجر Microsoft Entra محدد (أعضاء الإدارة الذين لديهم حساب عمل أو مدرسة أو ضيوف الإدارة الذين لديهم حساب شخصي لمايكروسوفت) تسجيل الدخول إلى التطبيق.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "معرف العميل لتطبيق Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-secret": "الرمز السرّي لـMicrosoft Client",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "الرّمز السرّي للعميل لتطبيق Microsoft OAuth",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "تفعيل خيار الدخول بحساب Discord",
|
||||
"admin.config.oauth.discord-limited-guild": "مُعرِّف خادم Discord المحدود",
|
||||
"admin.config.oauth.discord-limited-guild.description": "حصر تسجيل الدخول على المستخدمين الموجودين في خادم محدّد. اترك هذا الخيار فارغًا لتعطيله.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "معرف العميل لتطبيق Discord OAuth",
|
||||
"admin.config.oauth.discord-client-secret": "الرمز السرّي لـDiscord Client",
|
||||
"admin.config.oauth.discord-client-secret.description": "الرّمز السرّي للعميل لتطبيق Discord OAuth",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "تفعيل الدخول باستخدام OpenID Connect",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "رابط الاستكشاف لتطبيق OpenID Connect OAuth",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "طلب اسم المستخدم في رمز معرف OpenID Connect. إذا كنت لا تعرف معنى هذا الإعداد، اتركه فارغًا.",
|
||||
"admin.config.oauth.oidc-role-path": "Path to roles in OpenID Connect token",
|
||||
"admin.config.oauth.oidc-role-path.description": "Must be a valid JMES path referencing an array of roles. " + "Managing access rights using OpenID Connect roles is only recommended if no other identity provider is configured and password login is disabled. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect role for general access",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Role required for general access. Must be present in a user’s roles for them to log in. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect role for admin access",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "Role required for administrative access. Must be present in a user’s roles for them to access the admin panel. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "معرف العميل لتطبيق OpenID Connect OAuth",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "الرّمز السرّي للعميل لتطبيق OpenID Connect OAuth",
|
||||
"admin.config.category.ldap": "LDAP",
|
||||
"admin.config.ldap.enabled": "Enabled LDAP",
|
||||
"admin.config.ldap.enabled.description": "Use LDAP authentication for user login",
|
||||
"admin.config.ldap.url": "Server URL",
|
||||
"admin.config.ldap.url.description": "URL of the LDAP server",
|
||||
"admin.config.ldap.bind-dn": "Bind DN",
|
||||
"admin.config.ldap.bind-dn.description": "Default user which will be used to execute the user search",
|
||||
"admin.config.ldap.bind-password": "Bind password",
|
||||
"admin.config.ldap.bind-password.description": "Password for the user search user",
|
||||
"admin.config.ldap.search-base": "User base",
|
||||
"admin.config.ldap.search-base.description": "Base location, where the user search will be performed",
|
||||
"admin.config.ldap.search-query": "User query",
|
||||
"admin.config.ldap.search-query.description": "The user query will be used to search the 'User base' for the LDAP user. %username% can be used as the placeholder for the user given input.",
|
||||
"admin.config.ldap.admin-groups": "Admin group",
|
||||
// 404
|
||||
"404.description": "هذه الصفحة غير موجودة.",
|
||||
"404.button.home": "أعدني للصفحة الرئيسية",
|
||||
// error
|
||||
"error.title": "خطأ",
|
||||
"error.description": "عذرًا!",
|
||||
"error.button.back": "العودة",
|
||||
"error.msg.default": "حَدث خطأ ما.",
|
||||
"error.msg.access_denied": "قمت بإلغاء عملية المصادقة، الرجاء المحاولة مرة أخرى.",
|
||||
"error.msg.expired_token": "استغرقت عملية المصادقة وقتًا طويلًا، يرجى المحاولة مرة أخرى.",
|
||||
"error.msg.invalid_token": "خطأ داخلي",
|
||||
"error.msg.no_user": "المستخدم المرتبط بهذا الحساب {0} غير موجود.",
|
||||
"error.msg.no_email": "لا يمكن الحصول على عنوان البريد الإلكتروني من هذا الحساب {0}.",
|
||||
"error.msg.already_linked": "حساب {0} هذا مرتبط بالفعل بحساب آخر.",
|
||||
"error.msg.not_linked": "لم يتم ربط حساب {0} هذا بأي حساب حتى الآن.",
|
||||
"error.msg.unverified_account": "لم يتم التحقق من حساب {0} هذا، يرجى المحاولة مرة أخرى بعد التحقق.",
|
||||
"error.msg.user_not_allowed": "غير مسموح لك بتسجيل الدخول.",
|
||||
"error.msg.cannot_get_user_info": "فشلت عملية جلب معلومات المستخدم الخاصة بك من حساب {0} هذا.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "حفظ",
|
||||
"common.button.create": "إنشاء",
|
||||
"common.button.submit": "إرسال",
|
||||
"common.button.delete": "حذف",
|
||||
"common.button.cancel": "إلغاء",
|
||||
"common.button.confirm": "تأكيد",
|
||||
"common.button.disable": "إيقاف",
|
||||
"common.button.share": "مشاركة",
|
||||
"common.button.generate": "توليد",
|
||||
"common.button.done": "تم",
|
||||
"common.text.link": "الرابط",
|
||||
"common.text.navigate-to-link": "الذهاب إلى الرابط",
|
||||
"common.text.or": "أو",
|
||||
"common.button.go-back": "العودة",
|
||||
"common.button.go-home": "العودة للصفحة الرئيسية",
|
||||
"common.notify.copied": "تم نسخ الرابط إلى الحافظة",
|
||||
"common.success": "تم",
|
||||
"common.error": "خطأ",
|
||||
"common.error.unknown": "حدث خطأ غير معروف",
|
||||
"common.error.invalid-email": "عنوان البريد غير صحيح",
|
||||
"common.error.too-short": "يجب أن يكون على الأقل {length} حرفًا",
|
||||
"common.error.too-long": "يجب أن يكون على الأكثر {length} حرفًا",
|
||||
"common.error.exact-length": "يجب أن يكون بالضبط {length} حرفًا",
|
||||
"common.error.invalid-number": "يجب أن يكون رقماً",
|
||||
"common.error.field-required": "هذا الحقل مطلوب"
|
||||
};
|
||||
646
frontend/src/i18n/translations/cs-CZ.ts
Normal file
646
frontend/src/i18n/translations/cs-CZ.ts
Normal file
@@ -0,0 +1,646 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Upload",
|
||||
"navbar.signin": "Sign in",
|
||||
"navbar.home": "Home",
|
||||
"navbar.signup": "Sign Up",
|
||||
|
||||
"navbar.links.shares": "My shares",
|
||||
"navbar.links.reverse": "Reverse shares",
|
||||
|
||||
"navbar.avatar.account": "My account",
|
||||
"navbar.avatar.admin": "Administration",
|
||||
"navbar.avatar.signout": "Sign out",
|
||||
// END navbar
|
||||
|
||||
// /
|
||||
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||
|
||||
"home.description":
|
||||
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||
"home.bullet.a.name": "Self-Hosted",
|
||||
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||
"home.bullet.b.name": "Privacy",
|
||||
"home.bullet.b.description":
|
||||
"Your files are your files and should never get into the hands of third parties.",
|
||||
"home.bullet.c.name": "No annoying file size limit",
|
||||
"home.bullet.c.description":
|
||||
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||
|
||||
"home.button.start": "Get started",
|
||||
"home.button.source": "Source code",
|
||||
// END /
|
||||
|
||||
// /auth/signin
|
||||
"signin.title": "Welcome back",
|
||||
"signin.description": "You don't have an account yet?",
|
||||
"signin.button.signup": "Sign up",
|
||||
"signin.input.email-or-username": "Email or username",
|
||||
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||
"signin.input.password": "Password",
|
||||
"signin.input.password.placeholder": "Your password",
|
||||
"signin.button.submit": "Sign in",
|
||||
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||
"signIn.notify.totp-required.description":
|
||||
"Please enter your two-factor authentication code",
|
||||
"signIn.oauth.or": "OR",
|
||||
"signIn.oauth.signInWith": "Sign in with",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
|
||||
// END /auth/signin
|
||||
|
||||
// /auth/signup
|
||||
"signup.title": "Create an account",
|
||||
"signup.description": "Already have an account?",
|
||||
"signup.button.signin": "Sign in",
|
||||
"signup.input.username": "Username",
|
||||
"signup.input.username.placeholder": "Your username",
|
||||
"signup.input.email": "Email",
|
||||
"signup.input.email.placeholder": "Your email",
|
||||
"signup.button.submit": "Let's get started",
|
||||
|
||||
// END /auth/signup
|
||||
|
||||
// /auth/totp
|
||||
"totp.title": "TOTP Authentication",
|
||||
"totp.button.signIn": "Sign in",
|
||||
|
||||
// END /auth/totp
|
||||
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Forgot your password?",
|
||||
"resetPassword.description": "Enter your email to reset your password.",
|
||||
"resetPassword.notify.success":
|
||||
"A message with a link to reset your password has been sent if the email exists.",
|
||||
"resetPassword.button.back": "Back to sign in page",
|
||||
"resetPassword.text.resetPassword": "Reset password",
|
||||
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||
"resetPassword.input.password": "New password",
|
||||
"resetPassword.notify.passwordReset":
|
||||
"Your password has been reset successfully.",
|
||||
|
||||
// /account
|
||||
"account.title": "My account",
|
||||
|
||||
"account.card.info.title": "Account info",
|
||||
"account.card.info.username": "Username",
|
||||
"account.card.info.email": "Email",
|
||||
"account.notify.info.success": "Account updated successfully",
|
||||
|
||||
"account.card.password.title": "Password",
|
||||
"account.card.password.old": "Old password",
|
||||
"account.card.password.new": "New password",
|
||||
"account.card.password.noPasswordSet":
|
||||
"You don't have a password set. If you want to sign in with email and password you need to set a password.",
|
||||
"account.notify.password.success": "Password changed successfully",
|
||||
|
||||
"account.card.oauth.title": "Social login",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Link",
|
||||
"account.card.oauth.unlink": "Unlink",
|
||||
"account.card.oauth.unlinked": "Unlinked",
|
||||
"account.modal.unlink.title": "Unlink account",
|
||||
"account.modal.unlink.description":
|
||||
"Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
|
||||
"account.notify.oauth.unlinked.success": "Unlinked successfully",
|
||||
|
||||
"account.card.security.title": "Security",
|
||||
"account.card.security.totp.enable.description":
|
||||
"Enter your current password to start enabling TOTP",
|
||||
"account.card.security.totp.disable.description":
|
||||
"Enter your current password to disable TOTP",
|
||||
"account.card.security.totp.button.start": "Start",
|
||||
"account.modal.totp.title": "Enable TOTP",
|
||||
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||
"account.modal.totp.enterManually": "Enter manually",
|
||||
"account.modal.totp.code": "Code",
|
||||
"common.button.clickToCopy": "Click to copy",
|
||||
"account.modal.totp.verify": "Verify",
|
||||
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||
|
||||
"account.card.language.title": "Language",
|
||||
"account.card.language.description":
|
||||
"The project is translated by the community. Some languages might be incomplete.",
|
||||
"account.card.color.title": "Color scheme",
|
||||
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Dark",
|
||||
"account.theme.light": "Light",
|
||||
"account.theme.system": "System",
|
||||
|
||||
"account.button.delete": "Delete Account",
|
||||
"account.modal.delete.title": "Delete Account",
|
||||
"account.modal.delete.description":
|
||||
"Do you really want to delete your account including all your active shares?",
|
||||
// END /account
|
||||
|
||||
// /account/shares
|
||||
"account.shares.title": "My shares",
|
||||
"account.shares.title.empty": "It's empty here 👀",
|
||||
"account.shares.description.empty": "You don't have any shares.",
|
||||
"account.shares.button.create": "Create one",
|
||||
|
||||
"account.shares.info.title": "Share informations",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Name",
|
||||
"account.shares.table.description": "Description",
|
||||
"account.shares.table.visitors": "Visitors",
|
||||
"account.shares.table.expiresAt": "Expires at",
|
||||
"account.shares.table.createdAt": "Created at",
|
||||
"account.shares.table.size": "Size",
|
||||
|
||||
"account.shares.modal.share-informations": "Share informations",
|
||||
"account.shares.modal.share-link": "Share link",
|
||||
|
||||
"account.shares.modal.delete.title": "Delete share {share}",
|
||||
"account.shares.modal.delete.description":
|
||||
"Do you really want to delete this share?",
|
||||
|
||||
// END /account/shares
|
||||
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Reverse shares",
|
||||
"account.reverseShares.description":
|
||||
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||
|
||||
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||
"account.reverseShares.description.empty":
|
||||
"You don't have any reverse shares.",
|
||||
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Create reverse share",
|
||||
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||
|
||||
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||
|
||||
"account.reverseShares.modal.send-email": "Send email notification",
|
||||
"account.reverseShares.modal.send-email.description":
|
||||
"Send an email notification when a share is created with this reverse share link.",
|
||||
|
||||
"account.reverseShares.modal.simplified": "Simple mode",
|
||||
"account.reverseShares.modal.simplified.description":
|
||||
"Make it easy for the person uploading the file to share it with you. They will be able to customize only the name and description of the share.",
|
||||
|
||||
"account.reverseShares.modal.public-access": "Public access",
|
||||
"account.reverseShares.modal.public-access.description":
|
||||
"Make the created shares with this reverse share public. If disabled, only you and the creator of the share can view it.",
|
||||
|
||||
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||
"account.reverseShares.modal.max-use.description":
|
||||
"The maximum amount of times this URL can be used to create a share.",
|
||||
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||
"account.reverseShare.expires-on":
|
||||
"This reverse share will expire on {expiration}.",
|
||||
|
||||
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||
"account.reverseShares.table.count.singular": "share",
|
||||
"account.reverseShares.table.count.plural": "shares",
|
||||
"account.reverseShares.table.shares": "Shares",
|
||||
"account.reverseShares.table.remaining": "Remaining uses",
|
||||
"account.reverseShares.table.max-size": "Max share size",
|
||||
"account.reverseShares.table.expires": "Expires at",
|
||||
|
||||
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||
|
||||
"account.reverseShares.modal.delete.title": "Delete reverse share",
|
||||
"account.reverseShares.modal.delete.description":
|
||||
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||
|
||||
// END /account/reverseShares
|
||||
|
||||
// /admin
|
||||
"admin.title": "Administration",
|
||||
"admin.button.users": "User management",
|
||||
"admin.button.shares": "Share management",
|
||||
"admin.button.config": "Configuration",
|
||||
"admin.version": "Version",
|
||||
// END /admin
|
||||
|
||||
// /admin/users
|
||||
"admin.users.title": "User management",
|
||||
"admin.users.table.username": "Username",
|
||||
"admin.users.table.email": "Email",
|
||||
"admin.users.table.admin": "Admin",
|
||||
|
||||
"admin.users.edit.update.title": "Update user {username}",
|
||||
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||
"admin.users.edit.update.change-password.title": "Change password",
|
||||
"admin.users.edit.update.change-password.field": "New password",
|
||||
"admin.users.edit.update.change-password.button": "Save new password",
|
||||
"admin.users.edit.update.notify.password.success":
|
||||
"Password changed successfully",
|
||||
|
||||
"admin.users.edit.delete.title": "Delete user {username}",
|
||||
"admin.users.edit.delete.description":
|
||||
"Do you really want to delete this user and all his shares?",
|
||||
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Create user",
|
||||
"admin.users.modal.create.username": "Username",
|
||||
"admin.users.modal.create.email": "Email",
|
||||
"admin.users.modal.create.password": "Password",
|
||||
"admin.users.modal.create.manual-password": "Set password manually",
|
||||
"admin.users.modal.create.manual-password.description":
|
||||
"If not checked, the user will receive an email with a link to set their password.",
|
||||
"admin.users.modal.create.admin": "Admin privileges",
|
||||
"admin.users.modal.create.admin.description":
|
||||
"If checked, the user will be able to access the admin panel.",
|
||||
|
||||
// END /admin/users
|
||||
|
||||
// /admin/shares
|
||||
"admin.shares.title": "Share management",
|
||||
"admin.shares.table.id": "Share ID",
|
||||
"admin.shares.table.username": "Creator",
|
||||
"admin.shares.table.visitors": "Visitors",
|
||||
"admin.shares.table.expires": "Expires At",
|
||||
|
||||
"admin.shares.edit.delete.title": "Delete share {id}",
|
||||
"admin.shares.edit.delete.description":
|
||||
"Do you really want to delete this share?",
|
||||
|
||||
// END /admin/shares
|
||||
|
||||
// /upload
|
||||
"upload.title": "Upload",
|
||||
|
||||
"upload.notify.generic-error":
|
||||
"An error occurred while finishing your share.",
|
||||
"upload.notify.count-failed": "{count} files failed to upload. Trying again.",
|
||||
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Upload files",
|
||||
"upload.dropzone.description":
|
||||
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||
"upload.dropzone.notify.file-too-big":
|
||||
"Your files exceed the maximum share size of {maxSize}.",
|
||||
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Name",
|
||||
"upload.filelist.size": "Size",
|
||||
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Create Share",
|
||||
"upload.modal.link.error.invalid":
|
||||
"Can only contain letters, numbers, underscores, and hyphens",
|
||||
"upload.modal.link.error.taken": "This link is already in use",
|
||||
"upload.modal.not-signed-in": "You're not signed in",
|
||||
"upload.modal.not-signed-in-description":
|
||||
"You will be unable to delete your share manually and view the visitor count.",
|
||||
|
||||
"upload.modal.expires.never": "never",
|
||||
"upload.modal.expires.never-long": "Never Expires",
|
||||
"upload.modal.expires.error.too-long":
|
||||
"Expiration exceeds maximum expiration date of {max}.",
|
||||
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Expiration",
|
||||
"upload.modal.expires.minute-singular": "Minute",
|
||||
"upload.modal.expires.minute-plural": "Minutes",
|
||||
"upload.modal.expires.hour-singular": "Hour",
|
||||
"upload.modal.expires.hour-plural": "Hours",
|
||||
"upload.modal.expires.day-singular": "Day",
|
||||
"upload.modal.expires.day-plural": "Days",
|
||||
"upload.modal.expires.week-singular": "Week",
|
||||
"upload.modal.expires.week-plural": "Weeks",
|
||||
"upload.modal.expires.month-singular": "Month",
|
||||
"upload.modal.expires.month-plural": "Months",
|
||||
"upload.modal.expires.year-singular": "Year",
|
||||
"upload.modal.expires.year-plural": "Years",
|
||||
|
||||
"upload.modal.accordion.name-and-description.title": "Name and description",
|
||||
"upload.modal.accordion.name-and-description.name.placeholder": "Name",
|
||||
"upload.modal.accordion.name-and-description.description.placeholder":
|
||||
"Note for the recipients of this share",
|
||||
|
||||
"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.security.title": "Security options",
|
||||
"upload.modal.accordion.security.password.label": "Password protection",
|
||||
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||
"upload.modal.completed.expires-on":
|
||||
"This share will expire on {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Share ready",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "We have notified the creator of the reverse share. You can also manually share this link with them through other means.",
|
||||
|
||||
// END /upload
|
||||
|
||||
// /share/[id]
|
||||
"share.title": "Share {shareId}",
|
||||
"share.description": "Look what I've shared with you!",
|
||||
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||
"share.error.visitor-limit-exceeded.description":
|
||||
"The visitor limit from this share has been exceeded.",
|
||||
"share.error.removed.title": "Share removed",
|
||||
"share.error.not-found.title": "Share not found",
|
||||
"share.error.not-found.description":
|
||||
"The share you're looking for doesn't exist.",
|
||||
"share.error.access-denied.title": "Private share",
|
||||
"share.error.access-denied.description": "The current account does not have permission to access this share",
|
||||
|
||||
"share.modal.password.title": "Password required",
|
||||
"share.modal.password.description":
|
||||
"To access this share please enter the password for the share.",
|
||||
"share.modal.password": "Password",
|
||||
"share.modal.error.invalid-password": "Invalid password",
|
||||
|
||||
"share.button.download-all": "Download all",
|
||||
"share.notify.download-all-preparing":
|
||||
"The share is preparing. Try again in a few minutes.",
|
||||
|
||||
"share.modal.file-link": "File link",
|
||||
"share.table.name": "Name",
|
||||
"share.table.size": "Size",
|
||||
|
||||
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||
"share.modal.file-preview.error.not-supported.description":
|
||||
"A preview for this file type is unsupported. Please download the file to view it.",
|
||||
|
||||
// END /share/[id]
|
||||
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error":
|
||||
"An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
|
||||
// /admin/config
|
||||
"admin.config.title": "Configuration",
|
||||
"admin.config.category.general": "General",
|
||||
"admin.config.category.share": "Share",
|
||||
"admin.config.category.email": "Email",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "Social Login",
|
||||
|
||||
"admin.config.general.app-name": "App name",
|
||||
"admin.config.general.app-name.description": "Name of the application",
|
||||
"admin.config.general.app-url": "App URL",
|
||||
"admin.config.general.app-url.description":
|
||||
"On which URL Pingvin Share is available",
|
||||
"admin.config.general.show-home-page": "Show home page",
|
||||
"admin.config.general.show-home-page.description":
|
||||
"Whether to show the home page",
|
||||
"admin.config.general.session-duration": "Session Duration",
|
||||
"admin.config.general.session-duration.description":
|
||||
"Time in hours after which a user must log in again (default: 3 months).",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description":
|
||||
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Pick image",
|
||||
|
||||
"admin.config.email.enable-share-email-recipients":
|
||||
"Enable share email recipients",
|
||||
"admin.config.email.enable-share-email-recipients.description":
|
||||
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||
"admin.config.email.share-recipients-subject.description":
|
||||
"Subject of the email which gets sent to the share recipients.",
|
||||
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||
"admin.config.email.share-recipients-message.description":
|
||||
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual value.",
|
||||
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||
"admin.config.email.reverse-share-subject.description":
|
||||
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||
"admin.config.email.reverse-share-message.description":
|
||||
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||
"admin.config.email.reset-password-subject.description":
|
||||
"Subject of the email which gets sent when a user requests a password reset.",
|
||||
"admin.config.email.reset-password-message": "Reset password message",
|
||||
"admin.config.email.reset-password-message.description":
|
||||
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||
"admin.config.email.invite-subject": "Invite subject",
|
||||
"admin.config.email.invite-subject.description":
|
||||
"Subject of the email which gets sent when an admin invites a user.",
|
||||
"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 and {password} with the password.",
|
||||
|
||||
"admin.config.share.allow-registration": "Allow registration",
|
||||
"admin.config.share.allow-registration.description":
|
||||
"Whether registration is allowed",
|
||||
"admin.config.share.allow-unauthenticated-shares":
|
||||
"Allow unauthenticated shares",
|
||||
"admin.config.share.allow-unauthenticated-shares.description":
|
||||
"Whether unauthenticated users can create shares",
|
||||
"admin.config.share.max-expiration": "Max expiration",
|
||||
"admin.config.share.max-expiration.description":
|
||||
"Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"admin.config.share.max-size": "Max size",
|
||||
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||
"admin.config.share.zip-compression-level": "Zip compression level",
|
||||
"admin.config.share.zip-compression-level.description":
|
||||
"Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
|
||||
"admin.config.share.chunk-size": "Chunk size",
|
||||
"admin.config.share.chunk-size.description":
|
||||
"Adjust the chunk size (in bytes) for your uploads to balance efficiency and reliability according to your internet connection. Smaller chunks can enhance success rates for unstable connections, while larger chunks speed up uploads for stable connections.",
|
||||
"admin.config.share.auto-open-share-modal": "Auto open create share modal",
|
||||
"admin.config.share.auto-open-share-modal.description":
|
||||
"The share creation modal automatically appears when a user selects files, eliminating the need to manually click the button.",
|
||||
|
||||
"admin.config.smtp.enabled": "Enabled",
|
||||
"admin.config.smtp.enabled.description":
|
||||
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||
"admin.config.smtp.host": "Host",
|
||||
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||
"admin.config.smtp.port": "Port",
|
||||
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||
"admin.config.smtp.email": "Email",
|
||||
"admin.config.smtp.email.description":
|
||||
"Email address which the emails get sent from",
|
||||
"admin.config.smtp.username": "Username",
|
||||
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||
"admin.config.smtp.password": "Password",
|
||||
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||
"admin.config.smtp.button.test": "Send test email",
|
||||
"admin.config.smtp.allow-unauthorized-certificates":
|
||||
"Trust unauthorized SMTP server certificates",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description":
|
||||
"Only set this to true if you need to trust self signed certificates.",
|
||||
|
||||
"admin.config.oauth.allow-registration": "Allow registration",
|
||||
"admin.config.oauth.allow-registration.description":
|
||||
"Allow users to register via social login",
|
||||
"admin.config.oauth.ignore-totp": "Ignore TOTP",
|
||||
"admin.config.oauth.ignore-totp.description":
|
||||
"Whether to ignore TOTP when user using social login",
|
||||
"admin.config.oauth.disable-password": "Disable password login",
|
||||
"admin.config.oauth.disable-password.description":
|
||||
"Whether to disable password login\nMake sure that an OAuth provider is properly configured before activating this configuration to avoid being locked out.",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description":
|
||||
"Whether GitHub login is enabled",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description":
|
||||
"Client ID of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description":
|
||||
"Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description":
|
||||
"Whether Google login is enabled",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description":
|
||||
"Client ID of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description":
|
||||
"Client secret of the Google OAuth app",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description":
|
||||
"Whether Microsoft login is enabled",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description":
|
||||
"Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description":
|
||||
"Client ID of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description":
|
||||
"Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description":
|
||||
"Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description":
|
||||
"Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description":
|
||||
"Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description":
|
||||
"Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description":
|
||||
"Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description":
|
||||
"Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description":
|
||||
"Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-path": "Path to roles in OpenID Connect token",
|
||||
"admin.config.oauth.oidc-role-path.description":
|
||||
"Must be a valid JMES path referencing an array of roles. " +
|
||||
"Managing access rights using OpenID Connect roles is only recommended if no other identity provider is configured and password login is disabled. " +
|
||||
"Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect role for general access",
|
||||
"admin.config.oauth.oidc-role-general-access.description":
|
||||
"Role required for general access. Must be present in a user’s roles for them to log in. " +
|
||||
"Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect role for admin access",
|
||||
"admin.config.oauth.oidc-role-admin-access.description":
|
||||
"Role required for administrative access. Must be present in a user’s roles for them to access the admin panel. " +
|
||||
"Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description":
|
||||
"Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description":
|
||||
"Client secret of the OpenID Connect OAuth app",
|
||||
|
||||
"admin.config.category.ldap": "LDAP",
|
||||
"admin.config.ldap.enabled": "Enabled LDAP",
|
||||
"admin.config.ldap.enabled.description": "Use LDAP authentication for user login",
|
||||
"admin.config.ldap.url": "Server URL",
|
||||
"admin.config.ldap.url.description": "URL of the LDAP server",
|
||||
"admin.config.ldap.bind-dn": "Bind DN",
|
||||
"admin.config.ldap.bind-dn.description": "Default user which will be used to execute the user search",
|
||||
"admin.config.ldap.bind-password": "Bind password",
|
||||
"admin.config.ldap.bind-password.description": "Password for the user search user",
|
||||
"admin.config.ldap.search-base": "User base",
|
||||
"admin.config.ldap.search-base.description": "Base location, where the user search will be performed",
|
||||
"admin.config.ldap.search-query": "User query",
|
||||
"admin.config.ldap.search-query.description": "The user query will be used to search the 'User base' for the LDAP user. %username% can be used as the placeholder for the user given input.",
|
||||
"admin.config.ldap.admin-groups": "Admin group",
|
||||
|
||||
// 404
|
||||
"404.description": "Oops this page doesn't exist.",
|
||||
"404.button.home": "Bring me back home",
|
||||
|
||||
// error
|
||||
"error.title": "Error",
|
||||
"error.description": "Oops!",
|
||||
"error.button.back": "Go back",
|
||||
"error.msg.default": "Something went wrong.",
|
||||
"error.msg.access_denied":
|
||||
"You canceled the authentication process, please try again.",
|
||||
"error.msg.expired_token":
|
||||
"The authentication process took too long, please try again.",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
|
||||
"error.msg.no_email": "Can't get email address from this {0} account.",
|
||||
"error.msg.already_linked":
|
||||
"This {0} account is already linked to another account.",
|
||||
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
|
||||
"error.msg.unverified_account":
|
||||
"This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.user_not_allowed":
|
||||
"You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info":
|
||||
"Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
|
||||
// Common translations
|
||||
"common.button.save": "Save",
|
||||
"common.button.create": "Create",
|
||||
"common.button.submit": "Submit",
|
||||
"common.button.delete": "Delete",
|
||||
"common.button.cancel": "Cancel",
|
||||
"common.button.confirm": "Confirm",
|
||||
"common.button.disable": "Disable",
|
||||
"common.button.share": "Share",
|
||||
"common.button.generate": "Generate",
|
||||
"common.button.done": "Done",
|
||||
"common.text.link": "Link",
|
||||
"common.text.navigate-to-link": "Go to the link",
|
||||
"common.text.or": "or",
|
||||
"common.button.go-back": "Go back",
|
||||
"common.button.go-home": "Go home",
|
||||
"common.notify.copied": "Your link was copied to the clipboard",
|
||||
"common.success": "Success",
|
||||
|
||||
"common.error": "Error",
|
||||
"common.error.unknown": "An unknown error occurred",
|
||||
"common.error.invalid-email": "Invalid email address",
|
||||
"common.error.too-short": "Must be at least {length} characters",
|
||||
"common.error.too-long": "Must be at most {length} characters",
|
||||
"common.error.exact-length": "Must be exactly {length} characters",
|
||||
"common.error.invalid-number": "Must be a number",
|
||||
"common.error.field-required": "This field is required",
|
||||
};
|
||||
@@ -12,16 +12,13 @@ export default {
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "En <h>self-hosted</h> fildelingsplatform.",
|
||||
"home.description":
|
||||
"Er du sikker på, at du vil overlade dine personlige filer til tredjeparter som WeTransfer?",
|
||||
"home.description": "Er du sikker på, at du vil overlade dine personlige filer til tredjeparter som WeTransfer?",
|
||||
"home.bullet.a.name": "Self-Hosted",
|
||||
"home.bullet.a.description": "Host Pingvin Share på din egen maskine.",
|
||||
"home.bullet.b.name": "Privatliv",
|
||||
"home.bullet.b.description":
|
||||
"Dine filer er dine filer og bør ikke komme i hænderne på tredjeparter.",
|
||||
"home.bullet.b.description": "Dine filer er dine filer og bør ikke komme i hænderne på tredjeparter.",
|
||||
"home.bullet.c.name": "Ingen irriterende grænse for filstørrelse",
|
||||
"home.bullet.c.description":
|
||||
"Upload så store filer, som du vil. Kun din harddisk sætter grænsen.",
|
||||
"home.bullet.c.description": "Upload så store filer, som du vil. Kun din harddisk sætter grænsen.",
|
||||
"home.button.start": "Kom i gang",
|
||||
"home.button.source": "Source code",
|
||||
// END /
|
||||
@@ -30,15 +27,14 @@ export default {
|
||||
"signin.description": "Har du ikke en bruger endnu?",
|
||||
"signin.button.signup": "Opret bruger",
|
||||
"signin.input.email-or-username": "E-mail eller brugernavn",
|
||||
"signin.input.email-or-username.placeholder":
|
||||
"Din e-mail eller dit brugernavn",
|
||||
"signin.input.email-or-username.placeholder": "Din e-mail eller dit brugernavn",
|
||||
"signin.input.password": "Adgangskode",
|
||||
"signin.input.password.placeholder": "Din adgangskode",
|
||||
"signin.button.submit": "Log ind",
|
||||
"signIn.notify.totp-required.title": "2-faktor login påkrævet",
|
||||
"signIn.notify.totp-required.description":
|
||||
"Indtast den aktuelle engangskode fra din 2-faktor Authenticator",
|
||||
"signIn.notify.totp-required.description": "Indtast den aktuelle engangskode fra din 2-faktor Authenticator",
|
||||
"signIn.oauth.or": "OR",
|
||||
"signIn.oauth.signInWith": "Sign in with",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
@@ -61,10 +57,8 @@ export default {
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Glemt din adgangskode?",
|
||||
"resetPassword.description":
|
||||
"Indtast din e-mail for at nulstille din adgangskode.",
|
||||
"resetPassword.notify.success":
|
||||
"A message with a link to reset your password has been sent if the email exists.",
|
||||
"resetPassword.description": "Indtast din e-mail for at nulstille din adgangskode.",
|
||||
"resetPassword.notify.success": "A message with a link to reset your password has been sent if the email exists.",
|
||||
"resetPassword.button.back": "Tilbage til login",
|
||||
"resetPassword.text.resetPassword": "Nulstil adgangskode",
|
||||
"resetPassword.text.enterNewPassword": "Indtast din nye adgangskode",
|
||||
@@ -79,8 +73,7 @@ export default {
|
||||
"account.card.password.title": "Adgangskode",
|
||||
"account.card.password.old": "Gammel adgangskode",
|
||||
"account.card.password.new": "Ny adgangskode",
|
||||
"account.card.password.noPasswordSet":
|
||||
"You don't have a password set. If you want to sign in with email and password you need to set a password.",
|
||||
"account.card.password.noPasswordSet": "You don't have a password set. If you want to sign in with email and password you need to set a password.",
|
||||
"account.notify.password.success": "Adgangskoden er ændret",
|
||||
"account.card.oauth.title": "Social login",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
@@ -92,27 +85,23 @@ export default {
|
||||
"account.card.oauth.unlink": "Unlink",
|
||||
"account.card.oauth.unlinked": "Unlinked",
|
||||
"account.modal.unlink.title": "Unlink account",
|
||||
"account.modal.unlink.description":
|
||||
"Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
|
||||
"account.modal.unlink.description": "Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
|
||||
"account.notify.oauth.unlinked.success": "Unlinked successfully",
|
||||
"account.card.security.title": "Sikkerhed",
|
||||
"account.card.security.totp.enable.description":
|
||||
"Indtast din nuværende adgangskode for at begynde opsætningen af 2-faktor login",
|
||||
"account.card.security.totp.disable.description":
|
||||
"Indtast din nuværende adgangskode for at begynde opsætningen af 2-faktor login",
|
||||
"account.card.security.totp.enable.description": "Indtast din nuværende adgangskode for at begynde opsætningen af 2-faktor login",
|
||||
"account.card.security.totp.disable.description": "Indtast din nuværende adgangskode for at begynde opsætningen af 2-faktor login",
|
||||
"account.card.security.totp.button.start": "Start",
|
||||
"account.modal.totp.title": "Aktiver 2-faktor login",
|
||||
"account.modal.totp.step1": "Trin 1: Tilføj din 2-faktor Authenticator",
|
||||
"account.modal.totp.step2": "Trin 2: Valider din kode",
|
||||
"account.modal.totp.enterManually": "Indtast manuelt",
|
||||
"account.modal.totp.code": "Kode",
|
||||
"account.modal.totp.clickToCopy": "Klik for at kopiere",
|
||||
"common.button.clickToCopy": "Klik for at kopiere",
|
||||
"account.modal.totp.verify": "Bekræft",
|
||||
"account.notify.totp.disable": "2-faktor blev deaktiveret",
|
||||
"account.notify.totp.enable": "2-faktor blev deaktiveret",
|
||||
"account.card.language.title": "Sprog",
|
||||
"account.card.language.description":
|
||||
"The project is translated by the community. Some languages might be incomplete.",
|
||||
"account.card.language.description": "The project is translated by the community. Some languages might be incomplete.",
|
||||
"account.card.color.title": "Farveskema",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Mørkt",
|
||||
@@ -120,8 +109,7 @@ export default {
|
||||
"account.theme.system": "System",
|
||||
"account.button.delete": "Slet bruger",
|
||||
"account.modal.delete.title": "Slet bruger",
|
||||
"account.modal.delete.description":
|
||||
"Er du sikker på at du vil slette din bruger, herunder alle dine aktive delinger?",
|
||||
"account.modal.delete.description": "Er du sikker på at du vil slette din bruger, herunder alle dine aktive delinger?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Mine delte filer",
|
||||
@@ -139,16 +127,13 @@ export default {
|
||||
"account.shares.modal.share-informations": "Share informations",
|
||||
"account.shares.modal.share-link": "Del link",
|
||||
"account.shares.modal.delete.title": "Slet share {share}",
|
||||
"account.shares.modal.delete.description":
|
||||
"Ønsker du virkelig at slette denne deling?",
|
||||
"account.shares.modal.delete.description": "Ønsker du virkelig at slette denne deling?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Omvendt deling",
|
||||
"account.reverseShares.description":
|
||||
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||
"account.reverseShares.description": "A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||
"account.reverseShares.title.empty": "Der er tomt her 👀",
|
||||
"account.reverseShares.description.empty":
|
||||
"You don't have any reverse shares.",
|
||||
"account.reverseShares.description.empty": "You don't have any reverse shares.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Create reverse share",
|
||||
"account.reverseShares.modal.expiration.label": "Udløb",
|
||||
@@ -166,16 +151,16 @@ export default {
|
||||
"account.reverseShares.modal.expiration.year-plural": "År",
|
||||
"account.reverseShares.modal.max-size.label": "Maksimal størrelse for deling",
|
||||
"account.reverseShares.modal.send-email": "Send e-mail notifikation",
|
||||
"account.reverseShares.modal.send-email.description":
|
||||
"Send en e-mail notifikation, når der oprettes en deling med dette omvendte delingslink.",
|
||||
"account.reverseShares.modal.send-email.description": "Send en e-mail notifikation, når der oprettes en deling med dette omvendte delingslink.",
|
||||
"account.reverseShares.modal.simplified": "Simple mode",
|
||||
"account.reverseShares.modal.simplified.description": "Make it easy for the person uploading the file to share it with you. They will be able to customize only the name and description of the share.",
|
||||
"account.reverseShares.modal.public-access": "Public access",
|
||||
"account.reverseShares.modal.public-access.description": "Make the created shares with this reverse share public. If disabled, only you and the creator of the share can view it.",
|
||||
"account.reverseShares.modal.max-use.label": "Maksimal anvendelser",
|
||||
"account.reverseShares.modal.max-use.description":
|
||||
"Det maksimale antal gange, denne URL kan bruges til at oprette en deling.",
|
||||
"account.reverseShares.modal.max-use.description": "Det maksimale antal gange, denne URL kan bruges til at oprette en deling.",
|
||||
"account.reverseShare.never-expires": "Denne omvendte deling udløber aldrig.",
|
||||
"account.reverseShare.expires-on":
|
||||
"Denne omvendte deling udløber den {expiration}.",
|
||||
"account.reverseShares.table.no-shares":
|
||||
"Der er ikke oprettet nogle delinger endnu",
|
||||
"account.reverseShare.expires-on": "Denne omvendte deling udløber den {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Der er ikke oprettet nogle delinger endnu",
|
||||
"account.reverseShares.table.count.singular": "del",
|
||||
"account.reverseShares.table.count.plural": "shares",
|
||||
"account.reverseShares.table.shares": "Delinger",
|
||||
@@ -184,12 +169,12 @@ export default {
|
||||
"account.reverseShares.table.expires": "Udløber d",
|
||||
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||
"account.reverseShares.modal.delete.title": "Delete reverse share",
|
||||
"account.reverseShares.modal.delete.description":
|
||||
"Ønsker du virkelig at slette denne omvendte deling? Hvis du gør det, vil de tilknyttede delinger også blive slettet.",
|
||||
"account.reverseShares.modal.delete.description": "Ønsker du virkelig at slette denne omvendte deling? Hvis du gør det, vil de tilknyttede delinger også blive slettet.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Administration",
|
||||
"admin.button.users": "Brugeradministration",
|
||||
"admin.button.shares": "Share management",
|
||||
"admin.button.config": "Konfiguration",
|
||||
"admin.version": "Version",
|
||||
// END /admin
|
||||
@@ -205,46 +190,46 @@ export default {
|
||||
"admin.users.edit.update.change-password.button": "Gem ny adgangskode",
|
||||
"admin.users.edit.update.notify.password.success": "Adgangskoden er ændret",
|
||||
"admin.users.edit.delete.title": "Slet bruger {username}",
|
||||
"admin.users.edit.delete.description":
|
||||
"Er du sikker på du vil slette denne bruger og tilhørende delinger?",
|
||||
"admin.users.edit.delete.description": "Er du sikker på du vil slette denne bruger og tilhørende delinger?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Opret bruger",
|
||||
"admin.users.modal.create.username": "Brugernavn",
|
||||
"admin.users.modal.create.email": "E-mail",
|
||||
"admin.users.modal.create.password": "Adgangskode",
|
||||
"admin.users.modal.create.manual-password": "Angiv adgangskode manuelt",
|
||||
"admin.users.modal.create.manual-password.description":
|
||||
"If not checked, the user will receive an email with a link to set their password.",
|
||||
"admin.users.modal.create.manual-password.description": "If not checked, the user will receive an email with a link to set their password.",
|
||||
"admin.users.modal.create.admin": "Admin rettigheder",
|
||||
"admin.users.modal.create.admin.description":
|
||||
"If checked, the user will be able to access the admin panel.",
|
||||
"admin.users.modal.create.admin.description": "If checked, the user will be able to access the admin panel.",
|
||||
// END /admin/users
|
||||
// /admin/shares
|
||||
"admin.shares.title": "Share management",
|
||||
"admin.shares.table.id": "Share ID",
|
||||
"admin.shares.table.username": "Creator",
|
||||
"admin.shares.table.visitors": "Besøgende",
|
||||
"admin.shares.table.expires": "Expires At",
|
||||
"admin.shares.edit.delete.title": "Delete share {id}",
|
||||
"admin.shares.edit.delete.description": "Do you really want to delete this share?",
|
||||
// END /admin/shares
|
||||
// /upload
|
||||
"upload.title": "Upload",
|
||||
"upload.notify.generic-error":
|
||||
"Der opstod en fejl under afslutningen af din deling.",
|
||||
"upload.notify.generic-error": "Der opstod en fejl under afslutningen af din deling.",
|
||||
"upload.notify.count-failed": "{count} files failed to upload. Trying again.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Upload filer",
|
||||
"upload.dropzone.description":
|
||||
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||
"upload.dropzone.notify.file-too-big":
|
||||
"Your files exceed the maximum share size of {maxSize}.",
|
||||
"upload.dropzone.description": "Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||
"upload.dropzone.notify.file-too-big": "Your files exceed the maximum share size of {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Navn",
|
||||
"upload.filelist.size": "Størrelse",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Opret Deling",
|
||||
"upload.modal.link.error.invalid":
|
||||
"Can only contain letters, numbers, underscores, and hyphens",
|
||||
"upload.modal.link.error.invalid": "Can only contain letters, numbers, underscores, and hyphens",
|
||||
"upload.modal.link.error.taken": "Det valgte link er allerede i brug",
|
||||
"upload.modal.not-signed-in": "Du er ikke logget ind",
|
||||
"upload.modal.not-signed-in-description":
|
||||
"Du vil ikke være i stand til at slette din deling manuelt og se antallet af besøgende.",
|
||||
"upload.modal.not-signed-in-description": "Du vil ikke være i stand til at slette din deling manuelt og se antallet af besøgende.",
|
||||
"upload.modal.expires.never": "aldrig",
|
||||
"upload.modal.expires.never-long": "Udløber aldrig",
|
||||
"upload.modal.expires.error.too-long":
|
||||
"Udløbsdatoen overskrider den maksimalt tilladte udløbsdato på {max}.",
|
||||
"upload.modal.expires.error.too-long": "Udløbsdatoen overskrider den maksimalt tilladte udløbsdato på {max}.",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Udløb",
|
||||
"upload.modal.expires.minute-singular": "Minut",
|
||||
@@ -259,9 +244,9 @@ export default {
|
||||
"upload.modal.expires.month-plural": "Måneder",
|
||||
"upload.modal.expires.year-singular": "År",
|
||||
"upload.modal.expires.year-plural": "År",
|
||||
"upload.modal.accordion.description.title": "Beskrivelse",
|
||||
"upload.modal.accordion.description.placeholder":
|
||||
"Bemærkning til modtagerne af dette share",
|
||||
"upload.modal.accordion.name-and-description.title": "Name and description",
|
||||
"upload.modal.accordion.name-and-description.name.placeholder": "Navn",
|
||||
"upload.modal.accordion.name-and-description.description.placeholder": "Note for the recipients of this share",
|
||||
"upload.modal.accordion.email.title": "E-mail modtagere",
|
||||
"upload.modal.accordion.email.placeholder": "Indtast e-mail modtagere",
|
||||
"upload.modal.accordion.email.invalid-email": "Ugyldig e-mailadresse",
|
||||
@@ -272,42 +257,36 @@ export default {
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Ingen begrænsning",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Denne deling vil aldrig udløbe.",
|
||||
"upload.modal.completed.expires-on":
|
||||
"Denne omvendte deling udløber den {expiration}.",
|
||||
"upload.modal.completed.expires-on": "Denne omvendte deling udløber den {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Delingen er klar",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "We have notified the creator of the reverse share. You can also manually share this link with them through other means.",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Del {shareId}",
|
||||
"share.description": "Se hvad jeg har delt med dig!",
|
||||
"share.error.visitor-limit-exceeded.title":
|
||||
"Grænsen for besøgende overskredet",
|
||||
"share.error.visitor-limit-exceeded.description":
|
||||
"Besøgsgrænsen for denne deling er blevet overskredet.",
|
||||
"share.error.visitor-limit-exceeded.title": "Grænsen for besøgende overskredet",
|
||||
"share.error.visitor-limit-exceeded.description": "Besøgsgrænsen for denne deling er blevet overskredet.",
|
||||
"share.error.removed.title": "Deling fjernet",
|
||||
"share.error.not-found.title": "Delingen blev ikke fundet",
|
||||
"share.error.not-found.description":
|
||||
"Den deling, du leder efter, eksisterer ikke.",
|
||||
"share.error.not-found.description": "Den deling, du leder efter, eksisterer ikke.",
|
||||
"share.error.access-denied.title": "Private share",
|
||||
"share.error.access-denied.description": "The current account does not have permission to access this share",
|
||||
"share.modal.password.title": "Adgangskode påkrævet",
|
||||
"share.modal.password.description":
|
||||
"For at få adgang til denne deling, indtast venligst adgangskoden til delingen.",
|
||||
"share.modal.password.description": "For at få adgang til denne deling, indtast venligst adgangskoden til delingen.",
|
||||
"share.modal.password": "Adgangskode",
|
||||
"share.modal.error.invalid-password": "Ugyldig adgangskode",
|
||||
"share.button.download-all": "Download alle",
|
||||
"share.notify.download-all-preparing":
|
||||
"Delingen forberedes. Prøv igen om et par minutter.",
|
||||
"share.notify.download-all-preparing": "Delingen forberedes. Prøv igen om et par minutter.",
|
||||
"share.modal.file-link": "Fil link",
|
||||
"share.table.name": "Navn",
|
||||
"share.table.size": "Størrelse",
|
||||
"share.modal.file-preview.error.not-supported.title":
|
||||
"Forhåndsvisning ikke understøttet",
|
||||
"share.modal.file-preview.error.not-supported.description":
|
||||
"A preview for this file type is unsupported. Please download the file to view it.",
|
||||
"share.modal.file-preview.error.not-supported.title": "Forhåndsvisning ikke understøttet",
|
||||
"share.modal.file-preview.error.not-supported.description": "A preview for this file type is unsupported. Please download the file to view it.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.title": "Rediger {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error":
|
||||
"An error occurred while finishing your share.",
|
||||
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
@@ -320,135 +299,125 @@ export default {
|
||||
"admin.config.general.app-name": "App-navn",
|
||||
"admin.config.general.app-name.description": "Navnet på applikationen",
|
||||
"admin.config.general.app-url": "App URL",
|
||||
"admin.config.general.app-url.description":
|
||||
"På hvilken URL Pingvin Share er tilgængelig",
|
||||
"admin.config.general.app-url.description": "På hvilken URL Pingvin Share er tilgængelig",
|
||||
"admin.config.general.show-home-page": "Vis forside",
|
||||
"admin.config.general.show-home-page.description": "Om forsiden skal vises",
|
||||
"admin.config.general.session-duration": "Session Duration",
|
||||
"admin.config.general.session-duration.description": "Time in hours after which a user must log in again (default: 3 months).",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description":
|
||||
"Skift dit logo ved at uploade et nyt billede. Billedet skal være PNG og skal have formatet 1:1.",
|
||||
"admin.config.general.logo.description": "Skift dit logo ved at uploade et nyt billede. Billedet skal være PNG og skal have formatet 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Vælg billede",
|
||||
"admin.config.email.enable-share-email-recipients":
|
||||
"Aktiver deling til e-mail modtagere",
|
||||
"admin.config.email.enable-share-email-recipients.description":
|
||||
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||
"admin.config.email.enable-share-email-recipients": "Aktiver deling til e-mail modtagere",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||
"admin.config.email.share-recipients-subject.description":
|
||||
"Subject of the email which gets sent to the share recipients.",
|
||||
"admin.config.email.share-recipients-subject.description": "Subject of the email which gets sent to the share recipients.",
|
||||
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||
"admin.config.email.share-recipients-message.description":
|
||||
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual value.",
|
||||
"admin.config.email.share-recipients-message.description": "Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual value.",
|
||||
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||
"admin.config.email.reverse-share-subject.description":
|
||||
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||
"admin.config.email.reverse-share-subject.description": "Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||
"admin.config.email.reverse-share-message.description":
|
||||
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||
"admin.config.email.reverse-share-message.description": "Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||
"admin.config.email.reset-password-subject.description":
|
||||
"Subject of the email which gets sent when a user requests a password reset.",
|
||||
"admin.config.email.reset-password-subject.description": "Subject of the email which gets sent when a user requests a password reset.",
|
||||
"admin.config.email.reset-password-message": "Nulstil adgangskode besked",
|
||||
"admin.config.email.reset-password-message.description":
|
||||
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||
"admin.config.email.reset-password-message.description": "Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||
"admin.config.email.invite-subject": "Invitations emne",
|
||||
"admin.config.email.invite-subject.description":
|
||||
"Emne for den e-mail, der sendes, når en administrator inviterer en ny bruger.",
|
||||
"admin.config.email.invite-subject.description": "Emne for den e-mail, der sendes, når en administrator inviterer en ny bruger.",
|
||||
"admin.config.email.invite-message": "Invitations besked",
|
||||
"admin.config.email.invite-message.description":
|
||||
"Besked som bliver sendt, når en administrator inviterer en bruger. {url} vil blive erstattet med invitations-URL'en og {password} med adgangskoden.",
|
||||
"admin.config.email.invite-message.description": "Besked som bliver sendt, når en administrator inviterer en bruger. {url} vil blive erstattet med invitations-URL'en og {password} med adgangskoden.",
|
||||
"admin.config.share.allow-registration": "Tillad oprettelser",
|
||||
"admin.config.share.allow-registration.description":
|
||||
"Om alle skal kunne oprette en bruger",
|
||||
"admin.config.share.allow-unauthenticated-shares":
|
||||
"Tillad uautoriserede delinger",
|
||||
"admin.config.share.allow-unauthenticated-shares.description":
|
||||
"Whether unauthenticated users can create shares",
|
||||
"admin.config.share.allow-registration.description": "Om alle skal kunne oprette en bruger",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Tillad uautoriserede delinger",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Whether unauthenticated users can create shares",
|
||||
"admin.config.share.max-expiration": "Maks. udløb",
|
||||
"admin.config.share.max-expiration.description":
|
||||
"Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"admin.config.share.max-size": "Maks. størrelse",
|
||||
"admin.config.share.max-size.description": "Maksimal filstørrelse i bytes",
|
||||
"admin.config.share.zip-compression-level": "Zip compression level",
|
||||
"admin.config.share.zip-compression-level.description":
|
||||
"Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
|
||||
"admin.config.share.zip-compression-level.description": "Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
|
||||
"admin.config.share.chunk-size": "Chunk size",
|
||||
"admin.config.share.chunk-size.description": "Adjust the chunk size (in bytes) for your uploads to balance efficiency and reliability according to your internet connection. Smaller chunks can enhance success rates for unstable connections, while larger chunks speed up uploads for stable connections.",
|
||||
"admin.config.share.auto-open-share-modal": "Auto open create share modal",
|
||||
"admin.config.share.auto-open-share-modal.description": "The share creation modal automatically appears when a user selects files, eliminating the need to manually click the button.",
|
||||
"admin.config.smtp.enabled": "Aktiveret",
|
||||
"admin.config.smtp.enabled.description":
|
||||
"Om SMTP er aktiveret. Aktiver kun SMTP, hvis du har indtastet SMTP-server vært, port, e-mail, bruger og adgangskode.",
|
||||
"admin.config.smtp.enabled.description": "Om SMTP er aktiveret. Aktiver kun SMTP, hvis du har indtastet SMTP-server vært, port, e-mail, bruger og adgangskode.",
|
||||
"admin.config.smtp.host": "Vært",
|
||||
"admin.config.smtp.host.description": "Vært for SMTP serveren",
|
||||
"admin.config.smtp.port": "Port",
|
||||
"admin.config.smtp.port.description": "Porten til SMTP serveren",
|
||||
"admin.config.smtp.email": "E-mail",
|
||||
"admin.config.smtp.email.description":
|
||||
"E-mail adressen som der skal afsendes fra",
|
||||
"admin.config.smtp.email.description": "E-mail adressen som der skal afsendes fra",
|
||||
"admin.config.smtp.username": "Brugernavn",
|
||||
"admin.config.smtp.username.description": "Brugernavnet til SMTP serveren",
|
||||
"admin.config.smtp.password": "Adgangskode",
|
||||
"admin.config.smtp.password.description": "Adgangskoden til SMTP serveren",
|
||||
"admin.config.smtp.button.test": "Send test e-mail",
|
||||
"admin.config.oauth.allow-registration": "Allow registration",
|
||||
"admin.config.oauth.allow-registration.description":
|
||||
"Allow users to register via social login",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "Trust unauthorized SMTP server certificates",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Only set this to true if you need to trust self signed certificates.",
|
||||
"admin.config.oauth.allow-registration": "Tillad registrering",
|
||||
"admin.config.oauth.allow-registration.description": "Allow users to register via social login",
|
||||
"admin.config.oauth.ignore-totp": "Ignore TOTP",
|
||||
"admin.config.oauth.ignore-totp.description":
|
||||
"Whether to ignore TOTP when user using social login",
|
||||
"admin.config.oauth.ignore-totp.description": "Whether to ignore TOTP when user using social login",
|
||||
"admin.config.oauth.disable-password": "Disable password login",
|
||||
"admin.config.oauth.disable-password.description": "Whether to disable password login\nMake sure that an OAuth provider is properly configured before activating this configuration to avoid being locked out.",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description":
|
||||
"Whether GitHub login is enabled",
|
||||
"admin.config.oauth.github-enabled.description": "Whether GitHub login is enabled",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description":
|
||||
"Client ID of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-id.description": "Client ID of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description":
|
||||
"Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description":
|
||||
"Whether Google login is enabled",
|
||||
"admin.config.oauth.google-enabled.description": "Whether Google login is enabled",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description":
|
||||
"Client ID of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-id.description": "Client ID of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description":
|
||||
"Client secret of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret of the Google OAuth app",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description":
|
||||
"Whether Microsoft login is enabled",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description":
|
||||
"Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description":
|
||||
"Client ID of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description":
|
||||
"Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description":
|
||||
"Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description":
|
||||
"Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description":
|
||||
"Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description":
|
||||
"Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description":
|
||||
"Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description":
|
||||
"Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description":
|
||||
"Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-path": "Path to roles in OpenID Connect token",
|
||||
"admin.config.oauth.oidc-role-path.description": "Must be a valid JMES path referencing an array of roles. " + "Managing access rights using OpenID Connect roles is only recommended if no other identity provider is configured and password login is disabled. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect role for general access",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Role required for general access. Must be present in a user’s roles for them to log in. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect role for admin access",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "Role required for administrative access. Must be present in a user’s roles for them to access the admin panel. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description":
|
||||
"Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description":
|
||||
"Client secret of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
"admin.config.category.ldap": "LDAP",
|
||||
"admin.config.ldap.enabled": "Enabled LDAP",
|
||||
"admin.config.ldap.enabled.description": "Use LDAP authentication for user login",
|
||||
"admin.config.ldap.url": "Server URL",
|
||||
"admin.config.ldap.url.description": "URL of the LDAP server",
|
||||
"admin.config.ldap.bind-dn": "Bind DN",
|
||||
"admin.config.ldap.bind-dn.description": "Default user which will be used to execute the user search",
|
||||
"admin.config.ldap.bind-password": "Bind password",
|
||||
"admin.config.ldap.bind-password.description": "Password for the user search user",
|
||||
"admin.config.ldap.search-base": "User base",
|
||||
"admin.config.ldap.search-base.description": "Base location, where the user search will be performed",
|
||||
"admin.config.ldap.search-query": "User query",
|
||||
"admin.config.ldap.search-query.description": "The user query will be used to search the 'User base' for the LDAP user. %username% can be used as the placeholder for the user given input.",
|
||||
"admin.config.ldap.admin-groups": "Admin group",
|
||||
// 404
|
||||
"404.description": "Ups! Denne side findes ikke.",
|
||||
"404.button.home": "Gå tilbage",
|
||||
@@ -457,22 +426,16 @@ export default {
|
||||
"error.description": "Hovsa!",
|
||||
"error.button.back": "Gå tilbage",
|
||||
"error.msg.default": "Something went wrong.",
|
||||
"error.msg.access_denied":
|
||||
"You canceled the authentication process, please try again.",
|
||||
"error.msg.expired_token":
|
||||
"The authentication process took too long, please try again.",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.access_denied": "You canceled the authentication process, please try again.",
|
||||
"error.msg.expired_token": "The authentication process took too long, please try again.",
|
||||
"error.msg.invalid_token": "Intern Fejl",
|
||||
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
|
||||
"error.msg.no_email": "Can't get email address from this {0} account.",
|
||||
"error.msg.already_linked":
|
||||
"This {0} account is already linked to another account.",
|
||||
"error.msg.already_linked": "This {0} account is already linked to another account.",
|
||||
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
|
||||
"error.msg.unverified_account":
|
||||
"This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied":
|
||||
"Du har ikke tilladelse til at logge ind.",
|
||||
"error.msg.cannot_get_user_info":
|
||||
"Can not get your user info from this {0} account.",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.user_not_allowed": "Du har ikke tilladelse til at logge ind.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
@@ -503,5 +466,5 @@ export default {
|
||||
"common.error.too-long": "Må højst være {length} tegn",
|
||||
"common.error.exact-length": "Skal være præcis {length} tegn",
|
||||
"common.error.invalid-number": "Skal være et tal",
|
||||
"common.error.field-required": "Dette felt er påkrævet",
|
||||
};
|
||||
"common.error.field-required": "Dette felt er påkrævet"
|
||||
};
|
||||
@@ -12,17 +12,13 @@ export default {
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "Eine <h>selbst gehostete</h> Dateifreigabe-Plattform.",
|
||||
"home.description":
|
||||
"Möchtest du wirklich deine persönlichen Dateien an Drittanbieter wie WeTransfer weitergeben?",
|
||||
"home.description": "Möchtest du wirklich deine persönlichen Dateien an Drittanbieter wie WeTransfer weitergeben?",
|
||||
"home.bullet.a.name": "Selbst gehostet",
|
||||
"home.bullet.a.description":
|
||||
"Betreibe Pingvin Share auf deinem eigenen Server.",
|
||||
"home.bullet.a.description": "Betreibe Pingvin Share auf deinem eigenen Server.",
|
||||
"home.bullet.b.name": "Privatsphäre",
|
||||
"home.bullet.b.description":
|
||||
"Deine Dateien gehören dir und sollten niemals in die Hände Dritter gelangen.",
|
||||
"home.bullet.b.description": "Deine Dateien gehören dir und sollten niemals in die Hände Dritter gelangen.",
|
||||
"home.bullet.c.name": "Keine lästige Dateigrößenbegrenzung",
|
||||
"home.bullet.c.description":
|
||||
"Lade Dateien beliebiger Größe hoch. Nur dein Festplattenspeicher stellt die Grenze dar.",
|
||||
"home.bullet.c.description": "Lade Dateien beliebiger Größe hoch. Nur dein Festplattenspeicher stellt die Grenze dar.",
|
||||
"home.button.start": "Lege los",
|
||||
"home.button.source": "Quellcode",
|
||||
// END /
|
||||
@@ -31,15 +27,14 @@ export default {
|
||||
"signin.description": "Du hast noch kein Konto?",
|
||||
"signin.button.signup": "Registrieren",
|
||||
"signin.input.email-or-username": "Email oder Benutzername",
|
||||
"signin.input.email-or-username.placeholder":
|
||||
"Deine Email Adresse oder Benutzername",
|
||||
"signin.input.email-or-username.placeholder": "Deine Email Adresse oder Benutzername",
|
||||
"signin.input.password": "Passwort",
|
||||
"signin.input.password.placeholder": "Dein Passwort",
|
||||
"signin.button.submit": "Anmelden",
|
||||
"signIn.notify.totp-required.title": "Zwei-Faktor-Authentifizierung benötigt",
|
||||
"signIn.notify.totp-required.description":
|
||||
"Bitte füge deinen Zwei-Faktor-Authentifizierungscode ein",
|
||||
"signIn.notify.totp-required.description": "Bitte füge deinen Zwei-Faktor-Authentifizierungscode ein",
|
||||
"signIn.oauth.or": "ODER",
|
||||
"signIn.oauth.signInWith": "Anmelden mit",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
@@ -62,16 +57,13 @@ export default {
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Passwort vergessen?",
|
||||
"resetPassword.description":
|
||||
"Gib deine Email Adresse ein, um dein Passwort zurückzusetzen.",
|
||||
"resetPassword.notify.success":
|
||||
"A message with a link to reset your password has been sent if the email exists.",
|
||||
"resetPassword.description": "Gib deine Email Adresse ein, um dein Passwort zurückzusetzen.",
|
||||
"resetPassword.notify.success": "Wir haben dir einen Link gesendet, unter dem du dein Passwort zurücksetzen kannst.",
|
||||
"resetPassword.button.back": "Zurück zur Anmeldeseite",
|
||||
"resetPassword.text.resetPassword": "Passwort zurücksetzen",
|
||||
"resetPassword.text.enterNewPassword": "Gib dein neues Passwort ein",
|
||||
"resetPassword.input.password": "Neues Passwort",
|
||||
"resetPassword.notify.passwordReset":
|
||||
"Dein Passwort wurde erfolgreich zurückgesetzt.",
|
||||
"resetPassword.notify.passwordReset": "Dein Passwort wurde erfolgreich zurückgesetzt.",
|
||||
// /account
|
||||
"account.title": "Mein Konto",
|
||||
"account.card.info.title": "Kontoinformationen",
|
||||
@@ -81,8 +73,7 @@ export default {
|
||||
"account.card.password.title": "Passwort",
|
||||
"account.card.password.old": "Altes Passwort",
|
||||
"account.card.password.new": "Neues Passwort",
|
||||
"account.card.password.noPasswordSet":
|
||||
"Du hast kein Passwort erstellt. Wenn Du Dich mit E-Mail und Passwort anmelden möchtest, musst Du ein Passwort festlegen.",
|
||||
"account.card.password.noPasswordSet": "Du hast kein Passwort erstellt. Wenn du dich mit E-Mail und Passwort anmelden möchtest, musst du ein Passwort festlegen.",
|
||||
"account.notify.password.success": "Passwort erfolgreich geändert",
|
||||
"account.card.oauth.title": "Anmeldung über soziale Netzwerke",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
@@ -94,27 +85,23 @@ export default {
|
||||
"account.card.oauth.unlink": "Verknüpfung aufheben",
|
||||
"account.card.oauth.unlinked": "Verknüpfung aufgehoben",
|
||||
"account.modal.unlink.title": "Kontoverknüpfung aufheben",
|
||||
"account.modal.unlink.description":
|
||||
"Das Entfernen der Verknüpfung mit Deinem sozialen Konten kann dazu führen, dass Du Dein Konto verlierst, wenn Du Dich nicht an Deinen Benutzernamen und Dein Passwort erinnerst.",
|
||||
"account.modal.unlink.description": "Das Entfernen der Verknüpfung mit deinem sozialen Konten kann dazu führen, dass du dein Konto verlierst, wenn du dich nicht an deinen Benutzernamen und dein Passwort erinnerst.",
|
||||
"account.notify.oauth.unlinked.success": "Verknüpfung erfolgreich aufgehoben",
|
||||
"account.card.security.title": "Sicherheit",
|
||||
"account.card.security.totp.enable.description":
|
||||
"Gib dein aktuelles Passwort ein, um TOTP zu aktivieren",
|
||||
"account.card.security.totp.disable.description":
|
||||
"Gib dein aktuelles Passwort ein, um TOTP zu deaktivieren",
|
||||
"account.card.security.totp.enable.description": "Gib dein aktuelles Passwort ein, um TOTP zu aktivieren",
|
||||
"account.card.security.totp.disable.description": "Gib dein aktuelles Passwort ein, um TOTP zu deaktivieren",
|
||||
"account.card.security.totp.button.start": "Starten",
|
||||
"account.modal.totp.title": "TOTP aktivieren",
|
||||
"account.modal.totp.step1": "Schritt 1: Füge deinen Authenticator hinzu",
|
||||
"account.modal.totp.step2": "Schritt 2: Bestätige deinen Code",
|
||||
"account.modal.totp.enterManually": "Manuell eingeben",
|
||||
"account.modal.totp.code": "Code",
|
||||
"account.modal.totp.clickToCopy": "Klicken zum Kopieren",
|
||||
"common.button.clickToCopy": "Klicken zum Kopieren",
|
||||
"account.modal.totp.verify": "Überprüfen",
|
||||
"account.notify.totp.disable": "TOTP erfolgreich deaktiviert",
|
||||
"account.notify.totp.enable": "TOTP erfolgreich aktiviert",
|
||||
"account.card.language.title": "Sprache",
|
||||
"account.card.language.description":
|
||||
"Das Projekt wird von der Community übersetzt. Einige Sprachen könnten unvollständig sein.",
|
||||
"account.card.language.description": "Das Projekt wird von der Community übersetzt. Einige Sprachen könnten unvollständig sein.",
|
||||
"account.card.color.title": "Farbschema",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Dunkel",
|
||||
@@ -122,8 +109,7 @@ export default {
|
||||
"account.theme.system": "System",
|
||||
"account.button.delete": "Konto löschen",
|
||||
"account.modal.delete.title": "Konto löschen",
|
||||
"account.modal.delete.description":
|
||||
"Möchtest du wirklich dein Konto inklusive aller aktiven Freigaben löschen?",
|
||||
"account.modal.delete.description": "Möchtest du wirklich dein Konto inklusive aller aktiven Freigaben löschen?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Meine Freigaben",
|
||||
@@ -141,16 +127,13 @@ export default {
|
||||
"account.shares.modal.share-informations": "Teile deine Information",
|
||||
"account.shares.modal.share-link": "Freigabe teilen",
|
||||
"account.shares.modal.delete.title": "Lösche Freigabe {share}",
|
||||
"account.shares.modal.delete.description":
|
||||
"Möchtest du wirklich diese Freigabe löschen?",
|
||||
"account.shares.modal.delete.description": "Möchtest du wirklich diese Freigabe löschen?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Externe Freigaben",
|
||||
"account.reverseShares.description":
|
||||
"Eine externe Freigabe erlaubt dir eine einzigartige URL zu erstellen, die externen Benutzern erlaubt Dateien hochzuladen.",
|
||||
"account.reverseShares.description": "Eine externe Freigabe erlaubt dir eine einzigartige URL zu erstellen, die externen Benutzern erlaubt Dateien hochzuladen.",
|
||||
"account.reverseShares.title.empty": "Es ist leer hier 👀",
|
||||
"account.reverseShares.description.empty":
|
||||
"Du hast keine externen Freigaben erstellt.",
|
||||
"account.reverseShares.description.empty": "Du hast keine externen Freigaben erstellt.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Externe Freigabe erstellen",
|
||||
"account.reverseShares.modal.expiration.label": "Gültig bis",
|
||||
@@ -168,15 +151,15 @@ export default {
|
||||
"account.reverseShares.modal.expiration.year-plural": "Jahre",
|
||||
"account.reverseShares.modal.max-size.label": "Max. Freigabengröße",
|
||||
"account.reverseShares.modal.send-email": "Email Benachrichtigung senden",
|
||||
"account.reverseShares.modal.send-email.description":
|
||||
"Sendet eine Email Benachrichtigung, wenn eine Datei auf einer externen Freigabe hochgeladen wurde.",
|
||||
"account.reverseShares.modal.send-email.description": "Sendet eine Email Benachrichtigung, wenn eine Datei auf einer externen Freigabe hochgeladen wurde.",
|
||||
"account.reverseShares.modal.simplified": "Einfacher Modus",
|
||||
"account.reverseShares.modal.simplified.description": "Mache es der Person einfach, die die Datei hochlädt, sie mit Dir zu teilen. Sie können nur den Namen und die Beschreibung der Freigabe ändern.",
|
||||
"account.reverseShares.modal.public-access": "Öffentlicher Zugriff",
|
||||
"account.reverseShares.modal.public-access.description": "Make the created shares with this reverse share public. If disabled, only you and the creator of the share can view it.",
|
||||
"account.reverseShares.modal.max-use.label": "Maximale Nutzungen",
|
||||
"account.reverseShares.modal.max-use.description":
|
||||
"Die maximale Anzahl von Verwendungen der URL, um Dateien hochzuladen.",
|
||||
"account.reverseShare.never-expires":
|
||||
"Diese externe Freigabe wird nicht ablaufen.",
|
||||
"account.reverseShare.expires-on":
|
||||
"Diese externe Freigabe wird am {expiration} ablaufen.",
|
||||
"account.reverseShares.modal.max-use.description": "Die maximale Anzahl von Verwendungen der URL, um Dateien hochzuladen.",
|
||||
"account.reverseShare.never-expires": "Diese externe Freigabe wird nicht ablaufen.",
|
||||
"account.reverseShare.expires-on": "Diese externe Freigabe wird am {expiration} ablaufen.",
|
||||
"account.reverseShares.table.no-shares": "Noch keine Freigaben erstellt",
|
||||
"account.reverseShares.table.count.singular": "Freigabe",
|
||||
"account.reverseShares.table.count.plural": "Freigaben",
|
||||
@@ -186,12 +169,12 @@ export default {
|
||||
"account.reverseShares.table.expires": "Läuft ab am",
|
||||
"account.reverseShares.modal.reverse-share-link": "Link zu externer Freigabe",
|
||||
"account.reverseShares.modal.delete.title": "Lösche externe Freigabe",
|
||||
"account.reverseShares.modal.delete.description":
|
||||
"Möchtest du wirklich diese externe Freigabe löschen? In diesem Falle werden auch hiermit verbundene Freigaben gelöscht.",
|
||||
"account.reverseShares.modal.delete.description": "Möchtest du wirklich diese externe Freigabe löschen? In diesem Falle werden auch hiermit verbundene Freigaben gelöscht.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Verwaltung",
|
||||
"admin.button.users": "Benutzerverwaltung",
|
||||
"admin.button.shares": "Freigabenverwaltung",
|
||||
"admin.button.config": "Konfiguration",
|
||||
"admin.version": "Version",
|
||||
// END /admin
|
||||
@@ -205,50 +188,48 @@ export default {
|
||||
"admin.users.edit.update.change-password.title": "Passwort ändern",
|
||||
"admin.users.edit.update.change-password.field": "Neues Passwort",
|
||||
"admin.users.edit.update.change-password.button": "Neues Passwort speichern",
|
||||
"admin.users.edit.update.notify.password.success":
|
||||
"Passwort erfolgreich geändert",
|
||||
"admin.users.edit.update.notify.password.success": "Passwort erfolgreich geändert",
|
||||
"admin.users.edit.delete.title": "Löschen des Nutzers {username}",
|
||||
"admin.users.edit.delete.description":
|
||||
"Möchtest du wirklich diesen Benutzer und all seine Freigaben löschen?",
|
||||
"admin.users.edit.delete.description": "Möchtest du wirklich diesen Benutzer und all seine Freigaben löschen?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Benutzer erstellen",
|
||||
"admin.users.modal.create.username": "Benutzername",
|
||||
"admin.users.modal.create.email": "Email",
|
||||
"admin.users.modal.create.password": "Passwort",
|
||||
"admin.users.modal.create.manual-password": "Passwort manuell festlegen",
|
||||
"admin.users.modal.create.manual-password.description":
|
||||
"Wenn nicht aktiviert, erhält der Benutzer eine Email mit einem Link, um sein Passwort festzulegen.",
|
||||
"admin.users.modal.create.manual-password.description": "Wenn nicht aktiviert, erhält der Benutzer eine Email mit einem Link, um sein Passwort festzulegen.",
|
||||
"admin.users.modal.create.admin": "Administratorrechte",
|
||||
"admin.users.modal.create.admin.description":
|
||||
"Wenn aktiviert, kann der Benutzer auf das Administrator-Panel zugreifen.",
|
||||
"admin.users.modal.create.admin.description": "Wenn aktiviert, kann der Benutzer auf das Administrator-Panel zugreifen.",
|
||||
// END /admin/users
|
||||
// /admin/shares
|
||||
"admin.shares.title": "Freigabenverwaltung",
|
||||
"admin.shares.table.id": "ID teilen",
|
||||
"admin.shares.table.username": "Ersteller",
|
||||
"admin.shares.table.visitors": "Besucher",
|
||||
"admin.shares.table.expires": "Läuft ab am",
|
||||
"admin.shares.edit.delete.title": "Lösche Freigabe {id}",
|
||||
"admin.shares.edit.delete.description": "Möchtest du wirklich diese Freigabe löschen?",
|
||||
// END /admin/shares
|
||||
// /upload
|
||||
"upload.title": "Upload",
|
||||
"upload.notify.generic-error":
|
||||
"Während der Erstellung der Freigabe ist ein Fehler aufgetreten.",
|
||||
"upload.notify.count-failed":
|
||||
"{count} Dateien konnten nicht hochgeladen werden. Wird erneut versucht.",
|
||||
"upload.notify.generic-error": "Während der Erstellung der Freigabe ist ein Fehler aufgetreten.",
|
||||
"upload.notify.count-failed": "{count} Dateien konnten nicht hochgeladen werden. Wird erneut versucht.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Dateien hochladen",
|
||||
"upload.dropzone.description":
|
||||
"Ziehe Dateien hierher, um deine Freigabe zu starten. Wir können nur Dateien akzeptieren, die insgesamt weniger als {maxSize} groß sind.",
|
||||
"upload.dropzone.notify.file-too-big":
|
||||
"Ihre Dateien überschreiten die maximale Freigabegröße von {maxSize}.",
|
||||
"upload.dropzone.description": "Ziehe Dateien hierher, um deine Freigabe zu starten. Wir können nur Dateien akzeptieren, die insgesamt weniger als {maxSize} groß sind.",
|
||||
"upload.dropzone.notify.file-too-big": "Ihre Dateien überschreiten die maximale Freigabegröße von {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Name",
|
||||
"upload.filelist.size": "Größe",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Erstelle Freigabe",
|
||||
"upload.modal.link.error.invalid":
|
||||
"Darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten",
|
||||
"upload.modal.link.error.invalid": "Darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten",
|
||||
"upload.modal.link.error.taken": "Dieser Link wird bereits verwendet",
|
||||
"upload.modal.not-signed-in": "Du bist nicht angemeldet",
|
||||
"upload.modal.not-signed-in-description":
|
||||
"Du wirst deine Freigabe nicht löschen können oder die Besucheranzahl sehen.",
|
||||
"upload.modal.not-signed-in-description": "Du wirst deine Freigabe nicht löschen können oder die Besucheranzahl sehen.",
|
||||
"upload.modal.expires.never": "niemals",
|
||||
"upload.modal.expires.never-long": "Läuft nicht ab",
|
||||
"upload.modal.expires.error.too-long":
|
||||
"Ablauf überschreitet das maximale Ablaufdatum von {max}.",
|
||||
"upload.modal.expires.error.too-long": "Ablauf überschreitet das maximale Ablaufdatum von {max}.",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Gültig bis",
|
||||
"upload.modal.expires.minute-singular": "Minute",
|
||||
@@ -263,9 +244,9 @@ export default {
|
||||
"upload.modal.expires.month-plural": "Monate",
|
||||
"upload.modal.expires.year-singular": "Jahr",
|
||||
"upload.modal.expires.year-plural": "Year",
|
||||
"upload.modal.accordion.description.title": "Beschreibung",
|
||||
"upload.modal.accordion.description.placeholder":
|
||||
"Hinweis für die Empfänger dieser Freigabe",
|
||||
"upload.modal.accordion.name-and-description.title": "Name und Beschreibung",
|
||||
"upload.modal.accordion.name-and-description.name.placeholder": "Name",
|
||||
"upload.modal.accordion.name-and-description.description.placeholder": "Hinweis für die Empfänger dieser Freigabe",
|
||||
"upload.modal.accordion.email.title": "Email Empfänger",
|
||||
"upload.modal.accordion.email.placeholder": "Email der Empfänger eingeben",
|
||||
"upload.modal.accordion.email.invalid-email": "Ungültige Emailadresse",
|
||||
@@ -276,40 +257,36 @@ export default {
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Unbegrenzt",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Diese Freigabe läuft niemals ab.",
|
||||
"upload.modal.completed.expires-on":
|
||||
"Diese Freigabe wird am {expiration} ablaufen.",
|
||||
"upload.modal.completed.expires-on": "Diese Freigabe wird am {expiration} ablaufen.",
|
||||
"upload.modal.completed.share-ready": "Freigabe bereit",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "Wir haben den Ersteller der externen Freigabe benachrichtigt. Du kannst den Link auch auf andere Wege teilen.",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Freigabe {shareId}",
|
||||
"share.description": "Schau, was ich mit dir geteilt habe!",
|
||||
"share.error.visitor-limit-exceeded.title": "Besucher Limit erreicht",
|
||||
"share.error.visitor-limit-exceeded.description":
|
||||
"Die maximale Besucheranzahl für diese Freigabe ist überschritten.",
|
||||
"share.error.visitor-limit-exceeded.description": "Die maximale Besucheranzahl für diese Freigabe ist überschritten.",
|
||||
"share.error.removed.title": "Freigabe entfernt",
|
||||
"share.error.not-found.title": "Freigabe nicht gefunden",
|
||||
"share.error.not-found.description": "Die gesuchte Freigabe existiert nicht.",
|
||||
"share.error.access-denied.title": "Private Freigabe",
|
||||
"share.error.access-denied.description": "Das aktuelle Konto hat keine Berechtigung, um auf diese Freigabe zuzugreifen",
|
||||
"share.modal.password.title": "Passwort erforderlich",
|
||||
"share.modal.password.description":
|
||||
"Um auf diese Freigabe zuzugreifen, gib bitte das Passwort für die Freigabe ein.",
|
||||
"share.modal.password.description": "Um auf diese Freigabe zuzugreifen, gib bitte das Passwort für die Freigabe ein.",
|
||||
"share.modal.password": "Passwort",
|
||||
"share.modal.error.invalid-password": "Ungültiges Passwort",
|
||||
"share.button.download-all": "Alles herunterladen",
|
||||
"share.notify.download-all-preparing":
|
||||
"Die Freigabe wird vorbereitet. Versuche es in ein paar Minuten erneut.",
|
||||
"share.notify.download-all-preparing": "Die Freigabe wird vorbereitet. Versuche es in ein paar Minuten erneut.",
|
||||
"share.modal.file-link": "Dateilink",
|
||||
"share.table.name": "Name",
|
||||
"share.table.size": "Größe",
|
||||
"share.modal.file-preview.error.not-supported.title":
|
||||
"Vorschau wird nicht unterstützt",
|
||||
"share.modal.file-preview.error.not-supported.description":
|
||||
"A preview for this file type is unsupported. Please download the file to view it.",
|
||||
"share.modal.file-preview.error.not-supported.title": "Vorschau wird nicht unterstützt",
|
||||
"share.modal.file-preview.error.not-supported.description": "Eine Vorschau für diesen Dateityp wird nicht unterstützt. Bitte lade die Datei herunter, um sie anzusehen.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "{shareId} bearbeiten",
|
||||
"share.edit.append-upload": "Datei anfügen",
|
||||
"share.edit.notify.generic-error":
|
||||
"Während der Erstellung der Freigabe ist ein Fehler aufgetreten.",
|
||||
"share.edit.notify.generic-error": "Während der Erstellung der Freigabe ist ein Fehler aufgetreten.",
|
||||
"share.edit.notify.save-success": "Freigabe erfolgreich aktualisiert",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
@@ -322,138 +299,125 @@ export default {
|
||||
"admin.config.general.app-name": "App-Name",
|
||||
"admin.config.general.app-name.description": "Name der Applikation",
|
||||
"admin.config.general.app-url": "App-URL",
|
||||
"admin.config.general.app-url.description":
|
||||
"Auf welcher URL Pingvin Share verfügbar ist",
|
||||
"admin.config.general.app-url.description": "Auf welcher URL Pingvin Share verfügbar ist",
|
||||
"admin.config.general.show-home-page": "Startseite anzeigen",
|
||||
"admin.config.general.show-home-page.description":
|
||||
"Ob die Startseite angezeigt werden soll",
|
||||
"admin.config.general.show-home-page.description": "Ob die Startseite angezeigt werden soll",
|
||||
"admin.config.general.session-duration": "Session-Dauer",
|
||||
"admin.config.general.session-duration.description": "Zeit in Stunden, nach der ein Benutzer sich erneut anmelden muss (Voreinstellung: 3 Monate).",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description":
|
||||
"Ändere dein Logo durch Hochladen eines Bildes. Das Bild muss im PNG-Format vorliegen und sollte mit Seitenverhältnis 1:1 sein.",
|
||||
"admin.config.general.logo.description": "Ändere dein Logo durch Hochladen eines Bildes. Das Bild muss im PNG-Format vorliegen und sollte mit Seitenverhältnis 1:1 sein.",
|
||||
"admin.config.general.logo.placeholder": "Bild auswählen",
|
||||
"admin.config.email.enable-share-email-recipients":
|
||||
"Erlaube das Teilen der Freigabe via Email",
|
||||
"admin.config.email.enable-share-email-recipients.description":
|
||||
"Gibt an, ob Emails an Freigabe-Empfänger ermöglicht werden sollen. Aktiviere dies nur, wenn Du SMTP aktivierst hast.",
|
||||
"admin.config.email.share-recipients-subject":
|
||||
"Betreff für Freigabe-Empfänger",
|
||||
"admin.config.email.share-recipients-subject.description":
|
||||
"Betreff der E-Mail, die an die Freigabe-Empfänger gesendet wird.",
|
||||
"admin.config.email.share-recipients-message":
|
||||
"Nachricht für Freigabe-Empfänger",
|
||||
"admin.config.email.share-recipients-message.description":
|
||||
"Nachricht, die an die Freigabe-Empfänger gesendet wird. Verfügbare Variablen:\n- {creator} - Der Benutzername des Erstellers der Freigabe\n- {shareUrl} - Die URL der Freigabe\n- {desc} - Die Beschreibung der Freigabe\n- {expires} - Das Ablaufdatum der Freigabe\nVariablen werden durch die tatsächlichen Werte ersetzt.",
|
||||
"admin.config.email.enable-share-email-recipients": "Erlaube das Teilen der Freigabe via Email",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Gibt an, ob Emails an Freigabe-Empfänger ermöglicht werden sollen. Aktiviere dies nur, wenn du SMTP aktivierst hast.",
|
||||
"admin.config.email.share-recipients-subject": "Betreff für Freigabe-Empfänger",
|
||||
"admin.config.email.share-recipients-subject.description": "Betreff der E-Mail, die an die Freigabe-Empfänger gesendet wird.",
|
||||
"admin.config.email.share-recipients-message": "Nachricht für Freigabe-Empfänger",
|
||||
"admin.config.email.share-recipients-message.description": "Nachricht, die an die Freigabe-Empfänger gesendet wird. Verfügbare Variablen:\n- {creator} - Der Benutzername des Erstellers der Freigabe\n- {shareUrl} - Die URL der Freigabe\n- {desc} - Die Beschreibung der Freigabe\n- {expires} - Das Ablaufdatum der Freigabe\nVariablen werden durch die tatsächlichen Werte ersetzt.",
|
||||
"admin.config.email.reverse-share-subject": "Name der externen Freigabe",
|
||||
"admin.config.email.reverse-share-subject.description":
|
||||
"Betreff der Email, die gesendet wird, wenn jemand eine Datei mit deinem externen Freigabe-Link hochlädt.",
|
||||
"admin.config.email.reverse-share-subject.description": "Betreff der Email, die gesendet wird, wenn jemand eine Datei mit deinem externen Freigabe-Link hochlädt.",
|
||||
"admin.config.email.reverse-share-message": "Nachricht für externe Freigabe",
|
||||
"admin.config.email.reverse-share-message.description":
|
||||
"Nachricht, die gesendet wird, wenn jemand eine Freigabe mit deinem externen Freigabe-Link erstellt. {shareUrl} wird durch den Namen des Erstellers und die Freigabe-URL ersetzt.",
|
||||
"admin.config.email.reset-password-subject":
|
||||
"Betreff für Passwortzurücksetzung",
|
||||
"admin.config.email.reset-password-subject.description":
|
||||
"Betreff der E-Mail, die gesendet wird, wenn ein Benutzer eine Passwortzurücksetzung anfordert.",
|
||||
"admin.config.email.reset-password-message":
|
||||
"Nachricht für Passwortzurücksetzung",
|
||||
"admin.config.email.reset-password-message.description":
|
||||
"Nachricht, die gesendet wird, wenn ein Benutzer eine Passwortzurücksetzung anfordert. {url} wird durch die URL für das Zurücksetzen des Passworts ersetzt.",
|
||||
"admin.config.email.reverse-share-message.description": "Nachricht, die gesendet wird, wenn jemand eine Freigabe mit deinem externen Freigabe-Link erstellt. {shareUrl} wird durch den Namen des Erstellers und die Freigabe-URL ersetzt.",
|
||||
"admin.config.email.reset-password-subject": "Betreff für Passwortzurücksetzung",
|
||||
"admin.config.email.reset-password-subject.description": "Betreff der E-Mail, die gesendet wird, wenn ein Benutzer eine Passwortzurücksetzung anfordert.",
|
||||
"admin.config.email.reset-password-message": "Nachricht für Passwortzurücksetzung",
|
||||
"admin.config.email.reset-password-message.description": "Nachricht, die gesendet wird, wenn ein Benutzer eine Passwortzurücksetzung anfordert. {url} wird durch die URL für das Zurücksetzen des Passworts ersetzt.",
|
||||
"admin.config.email.invite-subject": "Betreff für Einladung",
|
||||
"admin.config.email.invite-subject.description":
|
||||
"Betreff der E-Mail, die gesendet wird, wenn ein Administrator einen Benutzer einlädt.",
|
||||
"admin.config.email.invite-subject.description": "Betreff der E-Mail, die gesendet wird, wenn ein Administrator einen Benutzer einlädt.",
|
||||
"admin.config.email.invite-message": "Nachricht für Einladung",
|
||||
"admin.config.email.invite-message.description":
|
||||
"Nachricht, die gesendet wird, wenn ein Administrator einen Benutzer einlädt. {url} wird durch die Einladungs-URL und {password} durch das Passwort ersetzt.",
|
||||
"admin.config.email.invite-message.description": "Nachricht, die gesendet wird, wenn ein Administrator einen Benutzer einlädt. {url} wird durch die Einladungs-URL und {password} durch das Passwort ersetzt.",
|
||||
"admin.config.share.allow-registration": "Registrierung erlauben",
|
||||
"admin.config.share.allow-registration.description":
|
||||
"Gibt an, ob eine Registrierung erlaubt ist",
|
||||
"admin.config.share.allow-unauthenticated-shares":
|
||||
"Nicht authentifizierte Freigaben erlauben",
|
||||
"admin.config.share.allow-unauthenticated-shares.description":
|
||||
"Gibt an, ob nicht authentifizierte Benutzer Freigaben erstellen können",
|
||||
"admin.config.share.allow-registration.description": "Gibt an, ob eine Registrierung erlaubt ist",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Nicht authentifizierte Freigaben erlauben",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Gibt an, ob nicht authentifizierte Benutzer Freigaben erstellen können",
|
||||
"admin.config.share.max-expiration": "Max. Ablaufdatum",
|
||||
"admin.config.share.max-expiration.description":
|
||||
"Maximale Ablaufzeit in Stunden. Auf 0 setzen, um kein Ablaufdatum zu definieren.",
|
||||
"admin.config.share.max-expiration.description": "Maximale Ablaufzeit in Stunden. Auf 0 setzen, um kein Ablaufdatum zu definieren.",
|
||||
"admin.config.share.max-size": "Maximale Größe",
|
||||
"admin.config.share.max-size.description":
|
||||
"Maximale Größe einer Freigabe in Bytes",
|
||||
"admin.config.share.max-size.description": "Maximale Größe einer Freigabe in Bytes",
|
||||
"admin.config.share.zip-compression-level": "Zip Komprimierungsstufe",
|
||||
"admin.config.share.zip-compression-level.description":
|
||||
"Passe den Wert an, um ein Gleichgewicht zwischen Dateigröße und Komprimierungsgeschwindigkeit herzustellen. Gültige Werte liegen zwischen 0 und 9, wobei 0 für keine Komprimierung und 9 für maximale Komprimierung steht.",
|
||||
"admin.config.share.zip-compression-level.description": "Passe den Wert an, um ein Gleichgewicht zwischen Dateigröße und Komprimierungsgeschwindigkeit herzustellen. Gültige Werte liegen zwischen 0 und 9, wobei 0 für keine Komprimierung und 9 für maximale Komprimierung steht.",
|
||||
"admin.config.share.chunk-size": "Chunkgröße",
|
||||
"admin.config.share.chunk-size.description": "Passe die Chunkgröße (in Bytes) für deine Uploads an, um die Zuverlässigkeit deiner Internetverbindung auszugleichen. Kleinere Chunks können die Erfolgsraten für instabile Verbindungen verbessern, während größere Chunks Uploads für stabile Verbindungen beschleunigen können.",
|
||||
"admin.config.share.auto-open-share-modal": "Freigabe-Fenster automatisch öffnen",
|
||||
"admin.config.share.auto-open-share-modal.description": "Das Freigabe-Fenster erscheint automatisch, sobald ein Benutzer Dateien ausgewählt hat, ohne extra auf den Button klicken zu müssen.",
|
||||
"admin.config.smtp.enabled": "Aktiviert",
|
||||
"admin.config.smtp.enabled.description":
|
||||
"Gibt an, ob SMTP aktiviert ist. Aktiviere dies nur, wenn Du den Host, den Port, die Email, den Benutzernamen und das Passwort deines SMTP-Servers eingegeben hast.",
|
||||
"admin.config.smtp.enabled.description": "Gibt an, ob SMTP aktiviert ist. Aktiviere dies nur, wenn du den Host, den Port, die Email, den Benutzernamen und das Passwort deines SMTP-Servers eingegeben hast.",
|
||||
"admin.config.smtp.host": "Host",
|
||||
"admin.config.smtp.host.description": "Host des SMTP-Servers",
|
||||
"admin.config.smtp.port": "Port",
|
||||
"admin.config.smtp.port.description": "Port des SMTP-Servers",
|
||||
"admin.config.smtp.email": "E-Mail",
|
||||
"admin.config.smtp.email.description":
|
||||
"E-Mail-Adresse, von der die E-Mails gesendet werden",
|
||||
"admin.config.smtp.email.description": "E-Mail-Adresse, von der die E-Mails gesendet werden",
|
||||
"admin.config.smtp.username": "Benutzername",
|
||||
"admin.config.smtp.username.description": "Benutzername des SMTP-Servers",
|
||||
"admin.config.smtp.password": "Passwort",
|
||||
"admin.config.smtp.password.description": "Passwort des SMTP-Servers",
|
||||
"admin.config.smtp.button.test": "Test-E-Mail senden",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "Vertrauen von nicht authentifizierten SMTP-Server-Zertifikaten",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Verwenden Sie diese Option nur, wenn Sie selbst signierten Zertifikaten vertrauen müssen.",
|
||||
"admin.config.oauth.allow-registration": "Registrierung erlauben",
|
||||
"admin.config.oauth.allow-registration.description":
|
||||
"Benutzern erlauben, sich über Soziale Netzwerke zu registrieren",
|
||||
"admin.config.oauth.allow-registration.description": "Benutzern erlauben, sich über Soziale Netzwerke zu registrieren",
|
||||
"admin.config.oauth.ignore-totp": "TOTP ignorieren",
|
||||
"admin.config.oauth.ignore-totp.description":
|
||||
"Gibt an, ob TOTP ignoriert werden soll, wenn sich der Benutzer über Soziale Netzwerke anmeldet",
|
||||
"admin.config.oauth.ignore-totp.description": "Gibt an, ob TOTP ignoriert werden soll, wenn sich der Benutzer über Soziale Netzwerke anmeldet",
|
||||
"admin.config.oauth.disable-password": "Anmelden mit Passwort deaktivieren",
|
||||
"admin.config.oauth.disable-password.description": "Deaktiviert das Anmelden mit Passwort\nStelle vor Aktivierung dieser Konfiguration sicher, dass ein OAuth-Provider korrekt konfiguriert ist, um nicht ausgesperrt zu werden.",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "GitHub Anmeldung erlaubt",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client-ID",
|
||||
"admin.config.oauth.github-client-id.description":
|
||||
"Client-ID der GitHub OAuth-App",
|
||||
"admin.config.oauth.github-client-id.description": "Client-ID der GitHub OAuth-App",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client-Secret",
|
||||
"admin.config.oauth.github-client-secret.description":
|
||||
"Client-Secret der GitHub OAuth-App",
|
||||
"admin.config.oauth.github-client-secret.description": "Client-Secret der GitHub OAuth-App",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Google Anmeldung erlaubt",
|
||||
"admin.config.oauth.google-client-id": "Google Client-ID",
|
||||
"admin.config.oauth.google-client-id.description":
|
||||
"Client-ID der Google OAuth-App",
|
||||
"admin.config.oauth.google-client-id.description": "Client-ID der Google OAuth-App",
|
||||
"admin.config.oauth.google-client-secret": "Google Client-Secret",
|
||||
"admin.config.oauth.google-client-secret.description":
|
||||
"Client-Secret der Google OAuth-App",
|
||||
"admin.config.oauth.google-client-secret.description": "Client-Secret der Google OAuth-App",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description":
|
||||
"Microsoft Anmeldung erlaubt",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Microsoft Anmeldung erlaubt",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Mandant",
|
||||
"admin.config.oauth.microsoft-tenant.description":
|
||||
"Mandant-ID der Microsoft OAuth App\ncommon: Benutzer mit einem persönlichen Microsoft-Konto und einem Arbeits- oder Schulkonto von Microsoft Entra ID können sich in der Anwendung anmelden.\norganizations: Nur Benutzer mit Arbeits- oder Schulkonten von Microsoft Entra ID können sich in der Anwendung anmelden.\nconsumers: Nur Benutzer mit einem persönlichen Microsoft-Konto können sich in der Anwendung anmelden.\nDomänenname des Microsoft Entra Mandanten oder die Mandanten-ID im GUID-Format: Nur Benutzer eines bestimmten Microsoft Entra Mandanten (Verzeichnismitglieder mit einem Arbeits- oder Schulkonto oder Verzeichnis Gäste mit einem persönlichen Microsoft-Konto) können sich anmelden.",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Mandant-ID der Microsoft OAuth App\ncommon: Benutzer mit einem persönlichen Microsoft-Konto und einem Arbeits- oder Schulkonto von Microsoft Entra ID können sich in der Anwendung anmelden.\norganizations: Nur Benutzer mit Arbeits- oder Schulkonten von Microsoft Entra ID können sich in der Anwendung anmelden.\nconsumers: Nur Benutzer mit einem persönlichen Microsoft-Konto können sich in der Anwendung anmelden.\nDomänenname des Microsoft Entra Mandanten oder die Mandanten-ID im GUID-Format: Nur Benutzer eines bestimmten Microsoft Entra Mandanten (Verzeichnismitglieder mit einem Arbeits- oder Schulkonto oder Verzeichnis Gäste mit einem persönlichen Microsoft-Konto) können sich anmelden.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client-ID",
|
||||
"admin.config.oauth.microsoft-client-id.description":
|
||||
"Client-ID der Microsoft OAuth-App",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client-ID der Microsoft OAuth-App",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client-Secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description":
|
||||
"Client-Secret der Microsoft OAuth-App",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client-Secret der Microsoft OAuth-App",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Discord Anmeldung erlaubt",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description":
|
||||
"Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord Server-ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Die Anmeldung auf Benutzer in einem bestimmten Server beschränken. Leer lassen, um zu deaktivieren.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client-ID",
|
||||
"admin.config.oauth.discord-client-id.description":
|
||||
"Client-ID der Discord OAuth-App",
|
||||
"admin.config.oauth.discord-client-id.description": "Client-ID der Discord OAuth-App",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client-Secret",
|
||||
"admin.config.oauth.discord-client-secret.description":
|
||||
"Client-Secret der Discord OAuth-App",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client-Secret der Discord OAuth-App",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description":
|
||||
"OpenID Connect Anmeldung erlaubt",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description":
|
||||
"Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description":
|
||||
"Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-enabled.description": "OpenID Connect Anmeldung erlaubt",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Verbindung Discovery URL",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery-URL der OpenID OAuth App",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect Benutzername anfordern",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Benutzername im OpenID Token. Leer lassen, wenn du nicht weißt, was diese Konfiguration bedeutet.",
|
||||
"admin.config.oauth.oidc-role-path": "Path to roles in OpenID Connect token",
|
||||
"admin.config.oauth.oidc-role-path.description": "Muss ein valider JMES-Pfad sein, der zu einem Array an Rollen führt. " + "Die Zugangsverwaltung über Rollen in OpenID Connect ist nur empfohlen, wenn kein anderer Identitätsprovider konfiguriert und die Anmeldung per Password deaktiviert ist. " + "Leer lassen, wenn du nicht weißt, was diese Konfiguration bedeutet.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect role for general access",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Rolle für generellen Zugriff. Muss Teil der Rollen eines Benutzers sein, damit dieser sich anmelden kann. " + "Leer lassen, wenn du nicht weißt, was diese Konfiguration bedeutet.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect Rolle für Admin-Zugriff",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "Rolle für administrativen Zugriff. Muss Teil der Rollen eines Benutzers sein, damit dieser auf das Administrator-Panel zugreifen kann. " + "Leer lassen, wenn du nicht weißt, was diese Konfiguration bedeutet.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client-ID",
|
||||
"admin.config.oauth.oidc-client-id.description":
|
||||
"Client-ID der OpenID Connect OAuth-App",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client-ID der OpenID Connect OAuth-App",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client-Secret",
|
||||
"admin.config.oauth.oidc-client-secret.description":
|
||||
"Client-Secret der OpenID Connect OAuth-App",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client-Secret der OpenID Connect OAuth-App",
|
||||
"admin.config.category.ldap": "LDAP",
|
||||
"admin.config.ldap.enabled": "Enabled LDAP",
|
||||
"admin.config.ldap.enabled.description": "Use LDAP authentication for user login",
|
||||
"admin.config.ldap.url": "Server URL",
|
||||
"admin.config.ldap.url.description": "URL of the LDAP server",
|
||||
"admin.config.ldap.bind-dn": "Bind DN",
|
||||
"admin.config.ldap.bind-dn.description": "Default user which will be used to execute the user search",
|
||||
"admin.config.ldap.bind-password": "Bind password",
|
||||
"admin.config.ldap.bind-password.description": "Password for the user search user",
|
||||
"admin.config.ldap.search-base": "User base",
|
||||
"admin.config.ldap.search-base.description": "Base location, where the user search will be performed",
|
||||
"admin.config.ldap.search-query": "User query",
|
||||
"admin.config.ldap.search-query.description": "The user query will be used to search the 'User base' for the LDAP user. %username% can be used as the placeholder for the user given input.",
|
||||
"admin.config.ldap.admin-groups": "Admin group",
|
||||
// 404
|
||||
"404.description": "Ups, diese Seite existiert nicht.",
|
||||
"404.button.home": "Zurück zur Startseite",
|
||||
@@ -462,25 +426,16 @@ export default {
|
||||
"error.description": "Ups!",
|
||||
"error.button.back": "Zurück",
|
||||
"error.msg.default": "Etwas ist schief gelaufen.",
|
||||
"error.msg.access_denied":
|
||||
"Du hast den Authentifizierungsprozess abgebrochen, bitte versuche es erneut.",
|
||||
"error.msg.expired_token":
|
||||
"Der Authentifizierungsprozess hat zu lange gedauert, bitte versuche es erneut.",
|
||||
"error.msg.access_denied": "Du hast den Authentifizierungsprozess abgebrochen, bitte versuche es erneut.",
|
||||
"error.msg.expired_token": "Der Authentifizierungsprozess hat zu lange gedauert, bitte versuche es erneut.",
|
||||
"error.msg.invalid_token": "Interner Fehler",
|
||||
"error.msg.no_user":
|
||||
"Der mit diesem {0} Konto verknüpfte Benutzer existiert nicht.",
|
||||
"error.msg.no_email":
|
||||
"Kann die E-Mail-Adresse von dem Konto {0} nicht abrufen.",
|
||||
"error.msg.already_linked":
|
||||
"Das Konto {0} ist bereits mit einem anderen Konto verknüpft.",
|
||||
"error.msg.not_linked":
|
||||
"Das Konto {0} wurde noch nicht mit einem Konto verknüpft.",
|
||||
"error.msg.unverified_account":
|
||||
"Dieses Konto {0} wurde noch nicht verifiziert, bitte versuchen Sie es nach der Verifikation erneut.",
|
||||
"error.msg.discord_guild_permission_denied":
|
||||
"Du bist nicht berechtigt, Dich anzumelden.",
|
||||
"error.msg.cannot_get_user_info":
|
||||
"Deine Benutzerinformationen können nicht von diesem Konto {0} abgerufen werden.",
|
||||
"error.msg.no_user": "Der mit diesem {0} Konto verknüpfte Benutzer existiert nicht.",
|
||||
"error.msg.no_email": "Kann die E-Mail-Adresse von dem Konto {0} nicht abrufen.",
|
||||
"error.msg.already_linked": "Das Konto {0} ist bereits mit einem anderen Konto verknüpft.",
|
||||
"error.msg.not_linked": "Das Konto {0} wurde noch nicht mit einem Konto verknüpft.",
|
||||
"error.msg.unverified_account": "Dieses Konto {0} wurde noch nicht verifiziert, bitte versuche es nach der Verifikation erneut.",
|
||||
"error.msg.user_not_allowed": "Du bist nicht berechtigt, dich anzumelden.",
|
||||
"error.msg.cannot_get_user_info": "Deine Benutzerinformationen können nicht von diesem Konto {0} abgerufen werden.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
@@ -501,7 +456,7 @@ export default {
|
||||
"common.text.navigate-to-link": "Link öffnen",
|
||||
"common.text.or": "oder",
|
||||
"common.button.go-back": "Zurück",
|
||||
"common.button.go-home": "Go home",
|
||||
"common.button.go-home": "Zur Startseite",
|
||||
"common.notify.copied": "Dein Link wurde in die Zwischenablage kopiert",
|
||||
"common.success": "Erfolg",
|
||||
"common.error": "Fehler",
|
||||
@@ -511,5 +466,5 @@ export default {
|
||||
"common.error.too-long": "Muss maximal {length} Zeichen enthalten",
|
||||
"common.error.exact-length": "Muss genau {length} Zeichen lang sein",
|
||||
"common.error.invalid-number": "Muss eine Zahl sein",
|
||||
"common.error.field-required": "Dieses Feld ist erforderlich",
|
||||
};
|
||||
"common.error.field-required": "Dieses Feld ist erforderlich"
|
||||
};
|
||||
@@ -11,19 +11,14 @@ export default {
|
||||
"navbar.avatar.signout": "Αποσύνδεση",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title":
|
||||
"Μια πλατφόρμα κοινής χρήσης αρχείων <h>σε ιδιωτική εγκατάσταση</h>.",
|
||||
"home.description":
|
||||
"Θέλετε πραγματικά να δώσετε τα προσωπικά σας αρχεία στο χέρι τρίτων όπως WeTransfer?",
|
||||
"home.title": "Μια πλατφόρμα κοινής χρήσης αρχείων <h>σε ιδιωτική εγκατάσταση</h>.",
|
||||
"home.description": "Θέλετε πραγματικά να δώσετε τα προσωπικά σας αρχεία στο χέρι τρίτων όπως WeTransfer?",
|
||||
"home.bullet.a.name": "Ιδιωτική εγκατάσταση",
|
||||
"home.bullet.a.description":
|
||||
"Φιλοξενήστε το Pingvin Share στο δικό σας μηχάνημα.",
|
||||
"home.bullet.a.description": "Φιλοξενήστε το Pingvin Share στο δικό σας μηχάνημα.",
|
||||
"home.bullet.b.name": "Απόρρητο",
|
||||
"home.bullet.b.description":
|
||||
"Τα αρχεία σας είναι αρχεία σας και δεν πρέπει ποτέ να μπείτε στα χέρια τρίτων.",
|
||||
"home.bullet.b.description": "Τα αρχεία σας είναι αρχεία σας και δεν πρέπει ποτέ να μπείτε στα χέρια τρίτων.",
|
||||
"home.bullet.c.name": "Χωρίς όριο μεγέθους αρχείου",
|
||||
"home.bullet.c.description":
|
||||
"Ανεβάστε όσο μεγάλα αρχεία θέλετε. Μόνο ο σκληρός σας δίσκος θα είναι το όριό σας.",
|
||||
"home.bullet.c.description": "Ανεβάστε όσο μεγάλα αρχεία θέλετε. Μόνο ο σκληρός σας δίσκος θα είναι το όριό σας.",
|
||||
"home.button.start": "Ας αρχίσουμε",
|
||||
"home.button.source": "Πηγαίος κώδικας",
|
||||
// END /
|
||||
@@ -36,11 +31,10 @@ export default {
|
||||
"signin.input.password": "Κωδικόs πρόσβασης",
|
||||
"signin.input.password.placeholder": "Ο κωδικός πρόσβασής σας",
|
||||
"signin.button.submit": "Είσοδος",
|
||||
"signIn.notify.totp-required.title":
|
||||
"Απαιτείται έλεγχος ταυτότητας δύο παραγόντων.",
|
||||
"signIn.notify.totp-required.description":
|
||||
"Παρακαλώ εισάγετε τον κωδικό 2FA.",
|
||||
"signIn.notify.totp-required.title": "Απαιτείται έλεγχος ταυτότητας δύο παραγόντων.",
|
||||
"signIn.notify.totp-required.description": "Παρακαλώ εισάγετε τον κωδικό 2FA.",
|
||||
"signIn.oauth.or": "Ή",
|
||||
"signIn.oauth.signInWith": "Sign in with",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
@@ -63,16 +57,13 @@ export default {
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Ξεχάσατε τον κωδικό σας;",
|
||||
"resetPassword.description":
|
||||
"A message with a link to reset your password has been sent if the email exists.",
|
||||
"resetPassword.notify.success":
|
||||
"Εισάγετε το email σας για επαναφορά κωδικού.",
|
||||
"resetPassword.description": "A message with a link to reset your password has been sent if the email exists.",
|
||||
"resetPassword.notify.success": "Εισάγετε το email σας για επαναφορά κωδικού.",
|
||||
"resetPassword.button.back": "Πίσω στη σελίδα εισόδου",
|
||||
"resetPassword.text.resetPassword": "Επαναφορά κωδικού πρόσβασης",
|
||||
"resetPassword.text.enterNewPassword": "Εισάγετε το νέο σας κωδικό",
|
||||
"resetPassword.input.password": "Νέος κωδικός",
|
||||
"resetPassword.notify.passwordReset":
|
||||
"Η επαναφορά του κωδικού πρόσβασης σας ολοκληρώθηκε με επιτυχία!",
|
||||
"resetPassword.notify.passwordReset": "Η επαναφορά του κωδικού πρόσβασης σας ολοκληρώθηκε με επιτυχία!",
|
||||
// /account
|
||||
"account.title": "Ο λογαριασμός μου",
|
||||
"account.card.info.title": "Πληροφορίες λογαριασμού",
|
||||
@@ -82,8 +73,7 @@ export default {
|
||||
"account.card.password.title": "Κωδικόs πρόσβασης",
|
||||
"account.card.password.old": "Παλιός κωδικός",
|
||||
"account.card.password.new": "Νέος κωδικός",
|
||||
"account.card.password.noPasswordSet":
|
||||
"Δεν έχετε ορίσει κωδικό πρόσβασης. Αν θέλετε να συνδεθείτε με email και κωδικό πρόσβασης, πρέπει να ορίσετε έναν κωδικό πρόσβασης.",
|
||||
"account.card.password.noPasswordSet": "Δεν έχετε ορίσει κωδικό πρόσβασης. Αν θέλετε να συνδεθείτε με email και κωδικό πρόσβασης, πρέπει να ορίσετε έναν κωδικό πρόσβασης.",
|
||||
"account.notify.password.success": "Ο κωδικός πρόσβασης άλλαξε επιτυχώς",
|
||||
"account.card.oauth.title": "Σύνδεση με λογαριασμό μέσων κοινωνικού δικτύου",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
@@ -95,27 +85,23 @@ export default {
|
||||
"account.card.oauth.unlink": "Αποσύνδεση",
|
||||
"account.card.oauth.unlinked": "Αποσυνδεδεμένο",
|
||||
"account.modal.unlink.title": "Αποσύνδεση Λογαριασμού",
|
||||
"account.modal.unlink.description":
|
||||
"Η αποσύνδεση των κοινωνικών λογαριασμών σας μπορεί να προκαλέσει απώλεια του λογαριασμού σας αν δε θυμάστε το όνομα χρήστη και τον κωδικό πρόσβασης.",
|
||||
"account.modal.unlink.description": "Η αποσύνδεση των κοινωνικών λογαριασμών σας μπορεί να προκαλέσει απώλεια του λογαριασμού σας αν δε θυμάστε το όνομα χρήστη και τον κωδικό πρόσβασης.",
|
||||
"account.notify.oauth.unlinked.success": "Επιτυχής αποσύνδεση",
|
||||
"account.card.security.title": "Ασφάλεια",
|
||||
"account.card.security.totp.enable.description":
|
||||
"Εισάγετε τον τρέχοντα κωδικό σας για να ξεκινήσετε την ενεργοποίηση του TOTP",
|
||||
"account.card.security.totp.disable.description":
|
||||
"Εισάγετε τον τρέχοντα κωδικό πρόσβασης για να απενεργοποιήσετε το TOTP",
|
||||
"account.card.security.totp.enable.description": "Εισάγετε τον τρέχοντα κωδικό σας για να ξεκινήσετε την ενεργοποίηση του TOTP",
|
||||
"account.card.security.totp.disable.description": "Εισάγετε τον τρέχοντα κωδικό πρόσβασης για να απενεργοποιήσετε το TOTP",
|
||||
"account.card.security.totp.button.start": "Έναρξη",
|
||||
"account.modal.totp.title": "Ενεργοποίηση TOTP",
|
||||
"account.modal.totp.step1": "Βήμα 1: Προσθέστε τον έλεγχο ταυτότητας",
|
||||
"account.modal.totp.step2": "Βήμα 2: Επικυρώστε τον κωδικό σας",
|
||||
"account.modal.totp.enterManually": "Χειροκίνητη εισαγωγή",
|
||||
"account.modal.totp.code": "Κώδικας",
|
||||
"account.modal.totp.clickToCopy": "Κάνε κλικ για αντιγραφή",
|
||||
"common.button.clickToCopy": "Κάνε κλικ για αντιγραφή",
|
||||
"account.modal.totp.verify": "Επαλήθευση",
|
||||
"account.notify.totp.disable": "Το TOTP απενεργοποιήθηκε επιτυχώς",
|
||||
"account.notify.totp.enable": "Το TOTP ενεργοποιήθηκε επιτυχώς",
|
||||
"account.card.language.title": "Γλώσσα",
|
||||
"account.card.language.description":
|
||||
"Η μετάφραση της εφαρμογής γίνεται από την εθελοντές της κοινότητας. Μερικές γλώσσες μπορεί να είναι ελλιπείς.",
|
||||
"account.card.language.description": "Η μετάφραση της εφαρμογής γίνεται από την εθελοντές της κοινότητας. Μερικές γλώσσες μπορεί να είναι ελλιπείς.",
|
||||
"account.card.color.title": "Συνδυασμός χρωμάτων",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Σκοτεινό",
|
||||
@@ -123,8 +109,7 @@ export default {
|
||||
"account.theme.system": "Σύστημα",
|
||||
"account.button.delete": "Διαγραφή Λογαριασμού",
|
||||
"account.modal.delete.title": "Διαγραφή Λογαριασμού",
|
||||
"account.modal.delete.description":
|
||||
"Θέλετε πραγματικά να διαγράψετε το λογαριασμό σας, συμπεριλαμβανομένων όλων των ενεργών μετοχών σας?",
|
||||
"account.modal.delete.description": "Θέλετε πραγματικά να διαγράψετε το λογαριασμό σας, συμπεριλαμβανομένων όλων των ενεργών μετοχών σας?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Οι κοινοποιήσεις μου",
|
||||
@@ -142,16 +127,13 @@ export default {
|
||||
"account.shares.modal.share-informations": "Πληροφορίες διαμοιρασμού",
|
||||
"account.shares.modal.share-link": "Κοινοποίηση συνδέσμου",
|
||||
"account.shares.modal.delete.title": "Διαγραφή κοινοποίησης {share}",
|
||||
"account.shares.modal.delete.description":
|
||||
"Θέλετε πραγματικά να διαγράψετε αυτό το διαμοιρασμό;",
|
||||
"account.shares.modal.delete.description": "Θέλετε πραγματικά να διαγράψετε αυτό το διαμοιρασμό;",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Αντίστροφες κοινοποιήσεις",
|
||||
"account.reverseShares.description":
|
||||
"Μια αντίστροφη κοινοποίηση σάς επιτρέπει να δημιουργήσετε μια μοναδική διεύθυνση URL που επιτρέπει σε εξωτερικούς χρήστες να δημιουργήσουν μια κοινή χρήση.",
|
||||
"account.reverseShares.description": "Μια αντίστροφη κοινοποίηση σάς επιτρέπει να δημιουργήσετε μια μοναδική διεύθυνση URL που επιτρέπει σε εξωτερικούς χρήστες να δημιουργήσουν μια κοινή χρήση.",
|
||||
"account.reverseShares.title.empty": "Είναι κενό εδώ 👀",
|
||||
"account.reverseShares.description.empty":
|
||||
"Δεν έχετε καμία αντίστροφη μετοχή.",
|
||||
"account.reverseShares.description.empty": "Δεν έχετε καμία αντίστροφη μετοχή.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Δημιουργία αντίστροφης κοινοποίησης",
|
||||
"account.reverseShares.modal.expiration.label": "Λήξη",
|
||||
@@ -169,33 +151,30 @@ export default {
|
||||
"account.reverseShares.modal.expiration.year-plural": "Έτη",
|
||||
"account.reverseShares.modal.max-size.label": "Μέγιστο μέγεθος κοινοποίησης",
|
||||
"account.reverseShares.modal.send-email": "Αποστολή ειδοποιήσεων με email",
|
||||
"account.reverseShares.modal.send-email.description":
|
||||
"Στείλτε μια ειδοποίηση μέσω ηλεκτρονικού ταχυδρομείου όταν δημιουργείται ένας διαμοιρασμός με αυτόν τον σύνδεσμο ανάστροφης κοινοποίησης.",
|
||||
"account.reverseShares.modal.send-email.description": "Στείλτε μια ειδοποίηση μέσω ηλεκτρονικού ταχυδρομείου όταν δημιουργείται ένας διαμοιρασμός με αυτόν τον σύνδεσμο ανάστροφης κοινοποίησης.",
|
||||
"account.reverseShares.modal.simplified": "Simple mode",
|
||||
"account.reverseShares.modal.simplified.description": "Make it easy for the person uploading the file to share it with you. They will be able to customize only the name and description of the share.",
|
||||
"account.reverseShares.modal.public-access": "Public access",
|
||||
"account.reverseShares.modal.public-access.description": "Make the created shares with this reverse share public. If disabled, only you and the creator of the share can view it.",
|
||||
"account.reverseShares.modal.max-use.label": "Μέγιστες χρήσεις",
|
||||
"account.reverseShares.modal.max-use.description":
|
||||
"Ο μέγιστος αριθμός που μπορεί να χρησιμοποιηθεί αυτό το URL για τη δημιουργία ενός διαμοιρασμού.",
|
||||
"account.reverseShare.never-expires":
|
||||
"Αυτός ο αντίστροφος διαμοιρασμός δε λήγει.",
|
||||
"account.reverseShare.expires-on":
|
||||
"Αυτός ο αντίστροφος διαμοιρασμός θα λήξει {expiration}.",
|
||||
"account.reverseShares.table.no-shares":
|
||||
"Δε δημιουργήθηκαν κοινοποιήσεις ακόμα",
|
||||
"account.reverseShares.modal.max-use.description": "Ο μέγιστος αριθμός που μπορεί να χρησιμοποιηθεί αυτό το URL για τη δημιουργία ενός διαμοιρασμού.",
|
||||
"account.reverseShare.never-expires": "Αυτός ο αντίστροφος διαμοιρασμός δε λήγει.",
|
||||
"account.reverseShare.expires-on": "Αυτός ο αντίστροφος διαμοιρασμός θα λήξει {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Δε δημιουργήθηκαν κοινοποιήσεις ακόμα",
|
||||
"account.reverseShares.table.count.singular": "διαμοιρασμός",
|
||||
"account.reverseShares.table.count.plural": "διαμοιρασμοί",
|
||||
"account.reverseShares.table.shares": "Διαμοιρασμοί",
|
||||
"account.reverseShares.table.remaining": "Υπόλοιπες χρήσεις",
|
||||
"account.reverseShares.table.max-size": "Μέγιστο μέγεθος κοινοποίησης",
|
||||
"account.reverseShares.table.expires": "Λήγει στις",
|
||||
"account.reverseShares.modal.reverse-share-link":
|
||||
"Αντίστροφος σύνδεσμος κοινής χρήσης",
|
||||
"account.reverseShares.modal.delete.title":
|
||||
"Διαγραφή αντίστροφης κοινοποίησης",
|
||||
"account.reverseShares.modal.delete.description":
|
||||
"Θέλετε πραγματικά να διαγράψετε αυτή την αντίστροφη κοινοποίηση; Εάν το κάνετε, οι συνδεδεμένες κοινοποιήσεις θα διαγραφούν επίσης.",
|
||||
"account.reverseShares.modal.reverse-share-link": "Αντίστροφος σύνδεσμος κοινής χρήσης",
|
||||
"account.reverseShares.modal.delete.title": "Διαγραφή αντίστροφης κοινοποίησης",
|
||||
"account.reverseShares.modal.delete.description": "Θέλετε πραγματικά να διαγράψετε αυτή την αντίστροφη κοινοποίηση; Εάν το κάνετε, οι συνδεδεμένες κοινοποιήσεις θα διαγραφούν επίσης.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Διαχείριση",
|
||||
"admin.button.users": "Διαχείριση χρηστών",
|
||||
"admin.button.shares": "Share management",
|
||||
"admin.button.config": "Διαμόρφωση",
|
||||
"admin.version": "Έκδοση",
|
||||
// END /admin
|
||||
@@ -208,53 +187,49 @@ export default {
|
||||
"admin.users.edit.update.admin-privileges": "Δικαιώματα διαχειριστή",
|
||||
"admin.users.edit.update.change-password.title": "Αλλαγή κωδικού πρόσβασής",
|
||||
"admin.users.edit.update.change-password.field": "Νέος κωδικός πρόσβασης",
|
||||
"admin.users.edit.update.change-password.button":
|
||||
"Αποθήκευση νέου κωδικού πρόσβασης",
|
||||
"admin.users.edit.update.notify.password.success":
|
||||
"Ο κωδικός πρόσβασης άλλαξε επιτυχώς",
|
||||
"admin.users.edit.update.change-password.button": "Αποθήκευση νέου κωδικού πρόσβασης",
|
||||
"admin.users.edit.update.notify.password.success": "Ο κωδικός πρόσβασης άλλαξε επιτυχώς",
|
||||
"admin.users.edit.delete.title": "Διαγραφή χρήστη {username}",
|
||||
"admin.users.edit.delete.description":
|
||||
"Θέλετε να διαγράψετε το χρήστη και όλες τις κοινοποιήσεις του;",
|
||||
"admin.users.edit.delete.description": "Θέλετε να διαγράψετε το χρήστη και όλες τις κοινοποιήσεις του;",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Δημιουργία χρήστη",
|
||||
"admin.users.modal.create.username": "Όνομα χρήστη",
|
||||
"admin.users.modal.create.email": "E-mail",
|
||||
"admin.users.modal.create.password": "Κωδικός πρόσβασης",
|
||||
"admin.users.modal.create.manual-password":
|
||||
"Χειροκίνητος ορισμός κωδικού πρόσβασης",
|
||||
"admin.users.modal.create.manual-password.description":
|
||||
"Εάν δεν είναι επιλεγμένο, ο χρήστης θα λάβει ένα email με ένα σύνδεσμο για να ορίσει τον κωδικό πρόσβασής του.",
|
||||
"admin.users.modal.create.manual-password": "Χειροκίνητος ορισμός κωδικού πρόσβασης",
|
||||
"admin.users.modal.create.manual-password.description": "Εάν δεν είναι επιλεγμένο, ο χρήστης θα λάβει ένα email με ένα σύνδεσμο για να ορίσει τον κωδικό πρόσβασής του.",
|
||||
"admin.users.modal.create.admin": "Δικαιώματα διαχειριστή",
|
||||
"admin.users.modal.create.admin.description":
|
||||
"Αν ενεργοποιηθεί, ο χρήστης θα μπορεί να έχει πρόσβαση στον πίνακα διαχείρισης.",
|
||||
"admin.users.modal.create.admin.description": "Αν ενεργοποιηθεί, ο χρήστης θα μπορεί να έχει πρόσβαση στον πίνακα διαχείρισης.",
|
||||
// END /admin/users
|
||||
// /admin/shares
|
||||
"admin.shares.title": "Share management",
|
||||
"admin.shares.table.id": "Share ID",
|
||||
"admin.shares.table.username": "Creator",
|
||||
"admin.shares.table.visitors": "Visitors",
|
||||
"admin.shares.table.expires": "Expires At",
|
||||
"admin.shares.edit.delete.title": "Delete share {id}",
|
||||
"admin.shares.edit.delete.description": "Do you really want to delete this share?",
|
||||
// END /admin/shares
|
||||
// /upload
|
||||
"upload.title": "Μεταφόρτωση",
|
||||
"upload.notify.generic-error":
|
||||
"Παρουσιάστηκε σφάλμα κατά την ολοκλήρωση της κοινής χρήσης σας.",
|
||||
"upload.notify.count-failed":
|
||||
"Τα αρχεία {count} απέτυχαν να μεταφορτώσουν. Δοκιμάστε ξανά.",
|
||||
"upload.notify.generic-error": "Παρουσιάστηκε σφάλμα κατά την ολοκλήρωση της κοινής χρήσης σας.",
|
||||
"upload.notify.count-failed": "Τα αρχεία {count} απέτυχαν να μεταφορτώσουν. Δοκιμάστε ξανά.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Μεταφόρτωση αρχείων",
|
||||
"upload.dropzone.description":
|
||||
"Σύρετε και αποθέστε αρχεία εδώ για να ξεκινήσει η κοινή χρήση σας. Μπορούμε να δεχτούμε μόνο αρχεία που είναι λιγότερο από {maxSize} συνολικά.",
|
||||
"upload.dropzone.notify.file-too-big":
|
||||
"Τα αρχεία σας υπερβαίνουν το μέγιστο μέγεθος κοινής χρήσης του {maxSize}.",
|
||||
"upload.dropzone.description": "Σύρετε και αποθέστε αρχεία εδώ για να ξεκινήσει η κοινή χρήση σας. Μπορούμε να δεχτούμε μόνο αρχεία που είναι λιγότερο από {maxSize} συνολικά.",
|
||||
"upload.dropzone.notify.file-too-big": "Τα αρχεία σας υπερβαίνουν το μέγιστο μέγεθος κοινής χρήσης του {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Όνομα",
|
||||
"upload.filelist.size": "Μέγεθος",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Δημιουργία Κοινοποίησης",
|
||||
"upload.modal.link.error.invalid":
|
||||
"Μπορεί να περιέχει μόνο γράμματα, αριθμούς, κάτω παύλες και παύλες",
|
||||
"upload.modal.link.error.invalid": "Μπορεί να περιέχει μόνο γράμματα, αριθμούς, κάτω παύλες και παύλες",
|
||||
"upload.modal.link.error.taken": "Αυτός ο σύνδεσμος χρησιμοποιείται ήδη",
|
||||
"upload.modal.not-signed-in": "Δεν είστε συνδεδεμένος/η",
|
||||
"upload.modal.not-signed-in-description":
|
||||
"Δεν θα μπορείτε να διαγράψετε την κοινή χρήση σας χειροκίνητα και να δείτε την αρίθμηση επισκεπτών.",
|
||||
"upload.modal.not-signed-in-description": "Δεν θα μπορείτε να διαγράψετε την κοινή χρήση σας χειροκίνητα και να δείτε την αρίθμηση επισκεπτών.",
|
||||
"upload.modal.expires.never": "ποτέ",
|
||||
"upload.modal.expires.never-long": "Χωρίς λήξη",
|
||||
"upload.modal.expires.error.too-long":
|
||||
"Η λήξη υπερβαίνει τη μέγιστη ημερομηνία λήξης {max}.",
|
||||
"upload.modal.expires.error.too-long": "Η λήξη υπερβαίνει τη μέγιστη ημερομηνία λήξης {max}.",
|
||||
"upload.modal.link.label": "Σύνδεσμος",
|
||||
"upload.modal.expires.label": "Λήξη",
|
||||
"upload.modal.expires.minute-singular": "Λεπτό",
|
||||
@@ -269,55 +244,49 @@ export default {
|
||||
"upload.modal.expires.month-plural": "Μήνες",
|
||||
"upload.modal.expires.year-singular": "Έτος",
|
||||
"upload.modal.expires.year-plural": "Έτη",
|
||||
"upload.modal.accordion.description.title": "Περιγραφή",
|
||||
"upload.modal.accordion.description.placeholder":
|
||||
"Σημείωση για τους παραλήπτες αυτής της κοινοποίησης",
|
||||
"upload.modal.accordion.name-and-description.title": "Name and description",
|
||||
"upload.modal.accordion.name-and-description.name.placeholder": "Name",
|
||||
"upload.modal.accordion.name-and-description.description.placeholder": "Note for the recipients of this share",
|
||||
"upload.modal.accordion.email.title": "Αποδέκτες email",
|
||||
"upload.modal.accordion.email.placeholder": "Εισάγετε αποδέκτες email",
|
||||
"upload.modal.accordion.email.invalid-email": "Μη έγκυρη διεύθυνση e-mail",
|
||||
"upload.modal.accordion.security.title": "Επιλογές ασφαλείας",
|
||||
"upload.modal.accordion.security.password.label":
|
||||
"Προστασία με κωδικό πρόσβασης",
|
||||
"upload.modal.accordion.security.password.label": "Προστασία με κωδικό πρόσβασης",
|
||||
"upload.modal.accordion.security.password.placeholder": "Χωρίς Κωδικό",
|
||||
"upload.modal.accordion.security.max-views.label":
|
||||
"Μέγιστος αριθμός εμφανίσεων",
|
||||
"upload.modal.accordion.security.max-views.label": "Μέγιστος αριθμός εμφανίσεων",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Χωρίς όριο",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Αυτός ο διαμοιρασμός δεν λήγει.",
|
||||
"upload.modal.completed.expires-on":
|
||||
"Αυτός ο διαμοιρασμός θα λήξει {expiration}.",
|
||||
"upload.modal.completed.expires-on": "Αυτός ο διαμοιρασμός θα λήξει {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Κοινοποίηση έτοιμου",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "We have notified the creator of the reverse share. You can also manually share this link with them through other means.",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Διαμοιρασμός {shareId}",
|
||||
"share.description": "Σας προωθώ αρχεία προς κοινοποίηση.",
|
||||
"share.error.visitor-limit-exceeded.title": "Υπέρβαση ορίου επισκέπτη",
|
||||
"share.error.visitor-limit-exceeded.description":
|
||||
"Ξεπεράστηκε το όριο επισκεπτών σε αυτή την κοινοποίηση.",
|
||||
"share.error.visitor-limit-exceeded.description": "Ξεπεράστηκε το όριο επισκεπτών σε αυτή την κοινοποίηση.",
|
||||
"share.error.removed.title": "Κοινοποίηση αφαιρέθηκε",
|
||||
"share.error.not-found.title": "Η κοινοποίηση δε βρέθηκε",
|
||||
"share.error.not-found.description": "Η κοινοποίηση που ψάχνετε δεν υπάρχει.",
|
||||
"share.error.access-denied.title": "Private share",
|
||||
"share.error.access-denied.description": "The current account does not have permission to access this share",
|
||||
"share.modal.password.title": "Απαιτείται κωδικός",
|
||||
"share.modal.password.description":
|
||||
"Για να αποκτήσετε πρόσβαση σε αυτή την κοινοποίηση εισάγετε τον κωδικό πρόσβασης.",
|
||||
"share.modal.password.description": "Για να αποκτήσετε πρόσβαση σε αυτή την κοινοποίηση εισάγετε τον κωδικό πρόσβασης.",
|
||||
"share.modal.password": "Κωδικός πρόσβασης",
|
||||
"share.modal.error.invalid-password": "Μη έγκυρος κωδικός πρόσβασης",
|
||||
"share.button.download-all": "Λήψη όλων",
|
||||
"share.notify.download-all-preparing":
|
||||
"Αυτός ο διαμοιρασμός βρίσκεται σε επεξεργασία. Προσπαθήστε ξανά σε λίγο.",
|
||||
"share.notify.download-all-preparing": "Αυτός ο διαμοιρασμός βρίσκεται σε επεξεργασία. Προσπαθήστε ξανά σε λίγο.",
|
||||
"share.modal.file-link": "Σύνδεσμος αρχείου",
|
||||
"share.table.name": "Όνομα",
|
||||
"share.table.size": "Μέγεθος",
|
||||
"share.modal.file-preview.error.not-supported.title":
|
||||
"Η προεπισκόπηση δεν υποστηρίζεται",
|
||||
"share.modal.file-preview.error.not-supported.description":
|
||||
"A preview for this file type is unsupported. Please download the file to view it.",
|
||||
"share.modal.file-preview.error.not-supported.title": "Η προεπισκόπηση δεν υποστηρίζεται",
|
||||
"share.modal.file-preview.error.not-supported.description": "Η προεπισκόπηση για αυτό τον τύπο αρχείου δεν υποστηρίζεται. Παρακαλώ κατεβάστε το αρχείο για να το δείτε.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Ενημέρωση {shareId}",
|
||||
"share.edit.append-upload": "Προσθήκη αρχείου",
|
||||
"share.edit.notify.generic-error":
|
||||
"Παρουσιάστηκε σφάλμα κατά την ολοκλήρωση του διαμοιρασμού.",
|
||||
"share.edit.notify.generic-error": "Παρουσιάστηκε σφάλμα κατά την ολοκλήρωση του διαμοιρασμού.",
|
||||
"share.edit.notify.save-success": "Ο διαμοιρασμός ενημερώθηκε επιτυχώς",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
@@ -326,150 +295,129 @@ export default {
|
||||
"admin.config.category.share": "Διαμοιρασμός",
|
||||
"admin.config.category.email": "Email",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth":
|
||||
"Σύνδεση με λογαριασμό μέσων κοινωνικού δικτύου",
|
||||
"admin.config.category.oauth": "Σύνδεση με λογαριασμό μέσων κοινωνικού δικτύου",
|
||||
"admin.config.general.app-name": "Όνομα εφαρμογής",
|
||||
"admin.config.general.app-name.description": "Ονομασία της εφαρμογής",
|
||||
"admin.config.general.app-url": "URL Εφαρμογής",
|
||||
"admin.config.general.app-url.description":
|
||||
"Η διεύθυνση URL όπου το Pingvin Share είναι διαθέσιμο",
|
||||
"admin.config.general.app-url.description": "Η διεύθυνση URL όπου το Pingvin Share είναι διαθέσιμο",
|
||||
"admin.config.general.show-home-page": "Εμφάνιση αρχικής σελίδας",
|
||||
"admin.config.general.show-home-page.description":
|
||||
"Εάν θα εμφανίζεται η αρχική σελίδα",
|
||||
"admin.config.general.show-home-page.description": "Εάν θα εμφανίζεται η αρχική σελίδα",
|
||||
"admin.config.general.session-duration": "Session Duration",
|
||||
"admin.config.general.session-duration.description": "Time in hours after which a user must log in again (default: 3 months).",
|
||||
"admin.config.general.logo": "Λογότυπο",
|
||||
"admin.config.general.logo.description":
|
||||
"Αλλάξτε το λογότυπό σας ανεβάζοντας μια νέα εικόνα. Η εικόνα πρέπει να είναι PNG και αναλογία 1:1.",
|
||||
"admin.config.general.logo.description": "Αλλάξτε το λογότυπό σας ανεβάζοντας μια νέα εικόνα. Η εικόνα πρέπει να είναι PNG και αναλογία 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Επιλέξτε εικόνα",
|
||||
"admin.config.email.enable-share-email-recipients":
|
||||
"Ενεργοποίηση κοινής χρήσης μέσω email",
|
||||
"admin.config.email.enable-share-email-recipients.description":
|
||||
"Εάν θα ενεργοποιηθεί η κοινή χρήση μέσω email. Δυνατή μόνον με την ενεργοποίηση SMTP.",
|
||||
"admin.config.email.enable-share-email-recipients": "Ενεργοποίηση κοινής χρήσης μέσω email",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Εάν θα ενεργοποιηθεί η κοινή χρήση μέσω email. Δυνατή μόνον με την ενεργοποίηση SMTP.",
|
||||
"admin.config.email.share-recipients-subject": "Θέμα στο email διαμοιρασμού",
|
||||
"admin.config.email.share-recipients-subject.description":
|
||||
"Το θέμα του email διαμοιρασμού που θα φτάσει στον παραλήπτη.",
|
||||
"admin.config.email.share-recipients-message":
|
||||
"Το θέμα του email για τον διαμοιρασμό που θα φτάσει στον παραλήπτη ",
|
||||
"admin.config.email.share-recipients-message.description":
|
||||
"Μήνυμα που αποστέλλεται στους παραλήπτες κοινής χρήσης. Διαθέσιμες μεταβλητές:\n {creator} - Το όνομα χρήστη του δημιουργού της κοινής χρήσης\n {shareUrl} - Η διεύθυνση URL της κοινής χρήσης\n {desc} - Η περιγραφή της κοινής χρήσης\n {expires} - Η ημερομηνία λήξης της κοινής χρήσης\n Οι μεταβλητές θα αντικατασταθούν με την πραγματική τιμή.",
|
||||
"admin.config.email.reverse-share-subject":
|
||||
"Θέμα email αντίστροφου διαμοιρασμού",
|
||||
"admin.config.email.reverse-share-subject.description":
|
||||
"Θέμα του ηλεκτρονικού ταχυδρομείου που αποστέλλεται όταν κάποιος δημιούργησε έναν αντίστροφο διαμοιρασμό.",
|
||||
"admin.config.email.reverse-share-message":
|
||||
"Μήνυμα email αντίστροφου διαμοιρασμού",
|
||||
"admin.config.email.reverse-share-message.description":
|
||||
"Μήνυμα που αποστέλλεται όταν κάποιος δημιουργεί έναν σύνδεσμο αντίστροφου διαμοιρασμού. Το {shareUrl} θα αντικατασταθεί με το όνομα του δημιουργού και τη διεύθυνση URL κοινής χρήσης.",
|
||||
"admin.config.email.reset-password-subject":
|
||||
"Θέμα μηνύματος επαναφοράς κωδικού πρόσβασης",
|
||||
"admin.config.email.reset-password-subject.description":
|
||||
"Θέμα του email που αποστέλλεται όταν ένας χρήστης ζητήσει επαναφορά κωδικού πρόσβασης.",
|
||||
"admin.config.email.reset-password-message":
|
||||
"Κείμενο μηνύματος επαναφοράς κωδικού πρόσβασης",
|
||||
"admin.config.email.reset-password-message.description":
|
||||
"Μήνυμα που αποστέλλεται όταν ένας χρήστης ζητά επαναφορά κωδικού πρόσβασης. Το {url} θα αντικατασταθεί με τη διεύθυνση URL επαναφοράς κωδικού πρόσβασης.",
|
||||
"admin.config.email.share-recipients-subject.description": "Το θέμα του email διαμοιρασμού που θα φτάσει στον παραλήπτη.",
|
||||
"admin.config.email.share-recipients-message": "Το θέμα του email για τον διαμοιρασμό που θα φτάσει στον παραλήπτη ",
|
||||
"admin.config.email.share-recipients-message.description": "Μήνυμα που αποστέλλεται στους παραλήπτες κοινής χρήσης. Διαθέσιμες μεταβλητές:\n {creator} - Το όνομα χρήστη του δημιουργού της κοινής χρήσης\n {shareUrl} - Η διεύθυνση URL της κοινής χρήσης\n {desc} - Η περιγραφή της κοινής χρήσης\n {expires} - Η ημερομηνία λήξης της κοινής χρήσης\n Οι μεταβλητές θα αντικατασταθούν με την πραγματική τιμή.",
|
||||
"admin.config.email.reverse-share-subject": "Θέμα email αντίστροφου διαμοιρασμού",
|
||||
"admin.config.email.reverse-share-subject.description": "Θέμα του ηλεκτρονικού ταχυδρομείου που αποστέλλεται όταν κάποιος δημιούργησε έναν αντίστροφο διαμοιρασμό.",
|
||||
"admin.config.email.reverse-share-message": "Μήνυμα email αντίστροφου διαμοιρασμού",
|
||||
"admin.config.email.reverse-share-message.description": "Μήνυμα που αποστέλλεται όταν κάποιος δημιουργεί έναν σύνδεσμο αντίστροφου διαμοιρασμού. Το {shareUrl} θα αντικατασταθεί με το όνομα του δημιουργού και τη διεύθυνση URL κοινής χρήσης.",
|
||||
"admin.config.email.reset-password-subject": "Θέμα μηνύματος επαναφοράς κωδικού πρόσβασης",
|
||||
"admin.config.email.reset-password-subject.description": "Θέμα του email που αποστέλλεται όταν ένας χρήστης ζητήσει επαναφορά κωδικού πρόσβασης.",
|
||||
"admin.config.email.reset-password-message": "Κείμενο μηνύματος επαναφοράς κωδικού πρόσβασης",
|
||||
"admin.config.email.reset-password-message.description": "Μήνυμα που αποστέλλεται όταν ένας χρήστης ζητά επαναφορά κωδικού πρόσβασης. Το {url} θα αντικατασταθεί με τη διεύθυνση URL επαναφοράς κωδικού πρόσβασης.",
|
||||
"admin.config.email.invite-subject": "Θέμα μηνύματος πρόσκλησης",
|
||||
"admin.config.email.invite-subject.description":
|
||||
"Θέμα του email που αποστέλλεται όταν ένας διαχειριστής προσκαλεί έναν χρήστη.",
|
||||
"admin.config.email.invite-subject.description": "Θέμα του email που αποστέλλεται όταν ένας διαχειριστής προσκαλεί έναν χρήστη.",
|
||||
"admin.config.email.invite-message": "Μήνυμα μηνύματος πρόσκλησης",
|
||||
"admin.config.email.invite-message.description":
|
||||
"Το μήνυμα που αποστέλλεται όταν ένας διαχειριστής προσκαλεί έναν χρήστη. Το {url} θα αντικατασταθεί με το URL πρόσκλησης και το {password} με τον κωδικό πρόσβασης.",
|
||||
"admin.config.email.invite-message.description": "Το μήνυμα που αποστέλλεται όταν ένας διαχειριστής προσκαλεί έναν χρήστη. Το {url} θα αντικατασταθεί με το URL πρόσκλησης και το {password} με τον κωδικό πρόσβασης.",
|
||||
"admin.config.share.allow-registration": "Να επιτρέπεται η εγγραφή",
|
||||
"admin.config.share.allow-registration.description":
|
||||
"Αν επιτρέπεται η εγγραφή",
|
||||
"admin.config.share.allow-unauthenticated-shares":
|
||||
"Επιτρέψτε κοινές χρήσεις χωρίς έλεγχο ταυτότητας",
|
||||
"admin.config.share.allow-unauthenticated-shares.description":
|
||||
"Εάν οι χρήστες χωρίς έλεγχο ταυτότητας μπορούν να δημιουργήσουν κοινόχρηστα στοιχεία",
|
||||
"admin.config.share.allow-registration.description": "Αν επιτρέπεται η εγγραφή",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Επιτρέψτε κοινές χρήσεις χωρίς έλεγχο ταυτότητας",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Εάν οι χρήστες χωρίς έλεγχο ταυτότητας μπορούν να δημιουργήσουν κοινόχρηστα στοιχεία",
|
||||
"admin.config.share.max-expiration": "Μέγιστη λήξη",
|
||||
"admin.config.share.max-expiration.description":
|
||||
"Μέγιστη λήξη κοινής χρήσης σε ώρες. Ορίστε το 0 για να επιτρέψετε απεριόριστη λήξη.",
|
||||
"admin.config.share.max-expiration.description": "Μέγιστη λήξη κοινής χρήσης σε ώρες. Ορίστε το 0 για να επιτρέψετε απεριόριστη λήξη.",
|
||||
"admin.config.share.max-size": "Μέγιστο μέγεθος",
|
||||
"admin.config.share.max-size.description":
|
||||
"Μέγιστο μέγεθος κοινοποίησης σε bytes",
|
||||
"admin.config.share.max-size.description": "Μέγιστο μέγεθος κοινοποίησης σε bytes",
|
||||
"admin.config.share.zip-compression-level": "Βαθμός συμπίεσης ZIP",
|
||||
"admin.config.share.zip-compression-level.description":
|
||||
"Προσαρμόστε το βαθμό συμπίεσης για να εξισορροπηθεί το μέγεθος του αρχείου και η ταχύτητα επεξεργασίας. Έγκυρες τιμές κυμαίνονται από 0 έως 9, με 0 χωρίς συμπίεση και 9 μέγιστη συμπίεση.",
|
||||
"admin.config.share.zip-compression-level.description": "Προσαρμόστε το βαθμό συμπίεσης για να εξισορροπηθεί το μέγεθος του αρχείου και η ταχύτητα επεξεργασίας. Έγκυρες τιμές κυμαίνονται από 0 έως 9, με 0 χωρίς συμπίεση και 9 μέγιστη συμπίεση.",
|
||||
"admin.config.share.chunk-size": "Μέγεθος κομματιών",
|
||||
"admin.config.share.chunk-size.description": "Προσαρμόστε το μέγεθος κομματιών (σε bytes) για να εξισορροπήσετε την αποδοτικότητα και την αξιοπιστία του συστήματος σύμφωνα με τη σύνδεσή σας στο διαδίκτυο. Μικρότερα κομμάτια μπορούν να βελτιώσουν τα ποσοστά επιτυχίας σε ασταθείς συνδέσεις, ενώ μεγαλύτερα κομμάτια επιταχύνουν τις μεταφορτώσεις σε σταθερές συνδέσεις.",
|
||||
"admin.config.share.auto-open-share-modal": "Auto open create share modal",
|
||||
"admin.config.share.auto-open-share-modal.description": "The share creation modal automatically appears when a user selects files, eliminating the need to manually click the button.",
|
||||
"admin.config.smtp.enabled": "Ενεργοποιημένο",
|
||||
"admin.config.smtp.enabled.description":
|
||||
"Εάν η λειτουργία SMTP είναι ενεργοποιημένη. Ενεργοποιήστε τη μόνον όταν ορίσετε σωστά τις παραμέτρους που ακολουθούν.",
|
||||
"admin.config.smtp.enabled.description": "Εάν η λειτουργία SMTP είναι ενεργοποιημένη. Ενεργοποιήστε τη μόνον όταν ορίσετε σωστά τις παραμέτρους που ακολουθούν.",
|
||||
"admin.config.smtp.host": "Εξυπηρετητής",
|
||||
"admin.config.smtp.host.description": "SMTP εξυπηρετητής",
|
||||
"admin.config.smtp.port": "Θύρα",
|
||||
"admin.config.smtp.port.description": "SMTP θύρα",
|
||||
"admin.config.smtp.email": "Email",
|
||||
"admin.config.smtp.email.description":
|
||||
"Διεύθυνση email από όπου αποστέλλονται τα μηνύματα",
|
||||
"admin.config.smtp.email.description": "Διεύθυνση email από όπου αποστέλλονται τα μηνύματα",
|
||||
"admin.config.smtp.username": "Όνομα χρήστη",
|
||||
"admin.config.smtp.username.description":
|
||||
"Όνομα χρήστη στον SMTP εξυπηρετητή",
|
||||
"admin.config.smtp.username.description": "Όνομα χρήστη στον SMTP εξυπηρετητή",
|
||||
"admin.config.smtp.password": "Κωδικός πρόσβασης",
|
||||
"admin.config.smtp.password.description":
|
||||
"Κωδικός πρόσβασης στον εξυπηρετητή SMTP",
|
||||
"admin.config.smtp.password.description": "Κωδικός πρόσβασης στον εξυπηρετητή SMTP",
|
||||
"admin.config.smtp.button.test": "Αποστολή δοκιμαστικού email",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "Trust unauthorized SMTP server certificates",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Only set this to true if you need to trust self signed certificates.",
|
||||
"admin.config.oauth.allow-registration": "Να επιτρέπεται η εγγραφή",
|
||||
"admin.config.oauth.allow-registration.description":
|
||||
"Επιτρέψτε στους χρήστες να εγγραφούν μέσω λογαριασμών κοινωνικής δικτύωσης",
|
||||
"admin.config.oauth.allow-registration.description": "Επιτρέψτε στους χρήστες να εγγραφούν μέσω λογαριασμών κοινωνικής δικτύωσης",
|
||||
"admin.config.oauth.ignore-totp": "Παράβλεψη TOTP",
|
||||
"admin.config.oauth.ignore-totp.description":
|
||||
"Αν θα αγνοηθεί το TOTP όταν ο χρήστης χρησιμοποιεί την κοινωνική σύνδεση",
|
||||
"admin.config.oauth.ignore-totp.description": "Αν θα αγνοηθεί το TOTP όταν ο χρήστης χρησιμοποιεί την κοινωνική σύνδεση",
|
||||
"admin.config.oauth.disable-password": "Disable password login",
|
||||
"admin.config.oauth.disable-password.description": "Whether to disable password login\nMake sure that an OAuth provider is properly configured before activating this configuration to avoid being locked out.",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description":
|
||||
"Αν είναι ενεργοποιημένη η σύνδεση GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Αν είναι ενεργοποιημένη η σύνδεση GitHub",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description":
|
||||
"Αναγνωριστικό πελάτη της εφαρμογής GitHub OAuth",
|
||||
"admin.config.oauth.github-client-id.description": "Αναγνωριστικό πελάτη της εφαρμογής GitHub OAuth",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description":
|
||||
"Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description":
|
||||
"Αν θα είναι ενεργοποιημένη η σύνδεση Google",
|
||||
"admin.config.oauth.google-enabled.description": "Αν θα είναι ενεργοποιημένη η σύνδεση Google",
|
||||
"admin.config.oauth.google-client-id": "Αναγνωριστικό Πελάτη Google",
|
||||
"admin.config.oauth.google-client-id.description":
|
||||
"Αναγνωριστικό πελάτη της εφαρμογής Google OAuth",
|
||||
"admin.config.oauth.google-client-id.description": "Αναγνωριστικό πελάτη της εφαρμογής Google OAuth",
|
||||
"admin.config.oauth.google-client-secret": "Google κωδικός",
|
||||
"admin.config.oauth.google-client-secret.description":
|
||||
"Κωδικός της εφαρμογής Google OAuth",
|
||||
"admin.config.oauth.google-client-secret.description": "Κωδικός της εφαρμογής Google OAuth",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description":
|
||||
"Αν είναι ενεργοποιημένη η σύνδεση της Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Αν είναι ενεργοποιημένη η σύνδεση της Microsoft",
|
||||
"admin.config.oauth.microsoft-tenant": "Αναγνωριστικό Microsoft",
|
||||
"admin.config.oauth.microsoft-tenant.description":
|
||||
"Αναγνωριστικό για την εφαρμογή Microsoft OAuth\nΚοινή: Οι χρήστες με προσωπικό λογαριασμό Microsoft και λογαριασμό εργασίας ή σχολείου από το Microsoft Entra ID μπορούν να συνδεθούν στην εφαρμογή.\nΟργανισμοί: Μόνο χρήστες με λογαριασμούς εργασίας ή σχολείου από το Microsoft Entra ID μπορούν να συνδεθούν στην εφαρμογή.\nΚαταναλωτές: Μόνο οι χρήστες με προσωπικό λογαριασμό Microsoft μπορούν να συνδεθούν στην εφαρμογή.\nΜε όνομα τομέα του μισθωτή Microsoft Entra ή το αναγνωριστικό μισθωτή σε μορφή GUID: Μόνο χρήστες από έναν συγκεκριμένο μισθωτή της Microsoft Entra (μέλη καταλόγου με λογαριασμό εργασίας ή σχολείου ή επισκέπτες καταλόγου με προσωπικό λογαριασμό Microsoft) μπορούν να συνδεθούν στην εφαρμογή.",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Αναγνωριστικό για την εφαρμογή Microsoft OAuth\nΚοινή: Οι χρήστες με προσωπικό λογαριασμό Microsoft και λογαριασμό εργασίας ή σχολείου από το Microsoft Entra ID μπορούν να συνδεθούν στην εφαρμογή.\nΟργανισμοί: Μόνο χρήστες με λογαριασμούς εργασίας ή σχολείου από το Microsoft Entra ID μπορούν να συνδεθούν στην εφαρμογή.\nΚαταναλωτές: Μόνο οι χρήστες με προσωπικό λογαριασμό Microsoft μπορούν να συνδεθούν στην εφαρμογή.\nΜε όνομα τομέα του μισθωτή Microsoft Entra ή το αναγνωριστικό μισθωτή σε μορφή GUID: Μόνο χρήστες από έναν συγκεκριμένο μισθωτή της Microsoft Entra (μέλη καταλόγου με λογαριασμό εργασίας ή σχολείου ή επισκέπτες καταλόγου με προσωπικό λογαριασμό Microsoft) μπορούν να συνδεθούν στην εφαρμογή.",
|
||||
"admin.config.oauth.microsoft-client-id": "Αναγνωριστικό πελάτη Microsoft",
|
||||
"admin.config.oauth.microsoft-client-id.description":
|
||||
"Αναγνωριστικό πελάτη της εφαρμογής Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Αναγνωριστικό πελάτη της εφαρμογής Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-secret": "Μυστικό πελάτη Microsoft",
|
||||
"admin.config.oauth.microsoft-client-secret.description":
|
||||
"Μυστικό πελάτη της εφαρμογής Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Μυστικό πελάτη της εφαρμογής Microsoft OAuth",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description":
|
||||
"Αν είναι ενεργοποιημένη η σύνδεση στο Discord",
|
||||
"admin.config.oauth.discord-limited-guild":
|
||||
"Αναγνωριστικό διακομιστή περιορισμένης ισχύος Discord",
|
||||
"admin.config.oauth.discord-limited-guild.description":
|
||||
"Περιορισμός σύνδεσης σε χρήστες σε ένα συγκεκριμένο διακομιστή. Αφήστε κενό για να απενεργοποιήσετε.",
|
||||
"admin.config.oauth.discord-enabled.description": "Αν είναι ενεργοποιημένη η σύνδεση στο Discord",
|
||||
"admin.config.oauth.discord-limited-guild": "Αναγνωριστικό διακομιστή περιορισμένης ισχύος Discord",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Περιορισμός σύνδεσης σε χρήστες σε ένα συγκεκριμένο διακομιστή. Αφήστε κενό για να απενεργοποιήσετε.",
|
||||
"admin.config.oauth.discord-client-id": "Αναγνωριστικό Πελάτη Discord",
|
||||
"admin.config.oauth.discord-client-id.description":
|
||||
"Αναγνωριστικό πελάτη της εφαρμογής Discord OAuth",
|
||||
"admin.config.oauth.discord-client-id.description": "Αναγνωριστικό πελάτη της εφαρμογής Discord OAuth",
|
||||
"admin.config.oauth.discord-client-secret": "Μυστικό πελάτη Discord",
|
||||
"admin.config.oauth.discord-client-secret.description":
|
||||
"Μυστικό πελάτη της εφαρμογής Discord OAuth",
|
||||
"admin.config.oauth.discord-client-secret.description": "Μυστικό πελάτη της εφαρμογής Discord OAuth",
|
||||
"admin.config.oauth.oidc-enabled": "Σύνδεση OpenID",
|
||||
"admin.config.oauth.oidc-enabled.description":
|
||||
"Αν είναι ενεργοποιημένη η σύνδεση OpenID",
|
||||
"admin.config.oauth.oidc-enabled.description": "Αν είναι ενεργοποιημένη η σύνδεση OpenID",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description":
|
||||
"Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description":
|
||||
"Username claim in OpenID Connect ID token. Αφήστε κενό αν δε γνωρίζετε για αυτή τη ρύθμιση",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Αφήστε κενό αν δε γνωρίζετε για αυτή τη ρύθμιση",
|
||||
"admin.config.oauth.oidc-role-path": "Path to roles in OpenID Connect token",
|
||||
"admin.config.oauth.oidc-role-path.description": "Must be a valid JMES path referencing an array of roles. " + "Managing access rights using OpenID Connect roles is only recommended if no other identity provider is configured and password login is disabled. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect role for general access",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Role required for general access. Must be present in a user’s roles for them to log in. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect role for admin access",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "Role required for administrative access. Must be present in a user’s roles for them to access the admin panel. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description":
|
||||
"Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description":
|
||||
"Client secret of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
"admin.config.category.ldap": "LDAP",
|
||||
"admin.config.ldap.enabled": "Enabled LDAP",
|
||||
"admin.config.ldap.enabled.description": "Use LDAP authentication for user login",
|
||||
"admin.config.ldap.url": "Server URL",
|
||||
"admin.config.ldap.url.description": "URL of the LDAP server",
|
||||
"admin.config.ldap.bind-dn": "Bind DN",
|
||||
"admin.config.ldap.bind-dn.description": "Default user which will be used to execute the user search",
|
||||
"admin.config.ldap.bind-password": "Bind password",
|
||||
"admin.config.ldap.bind-password.description": "Password for the user search user",
|
||||
"admin.config.ldap.search-base": "User base",
|
||||
"admin.config.ldap.search-base.description": "Base location, where the user search will be performed",
|
||||
"admin.config.ldap.search-query": "User query",
|
||||
"admin.config.ldap.search-query.description": "The user query will be used to search the 'User base' for the LDAP user. %username% can be used as the placeholder for the user given input.",
|
||||
"admin.config.ldap.admin-groups": "Admin group",
|
||||
// 404
|
||||
"404.description": "Ουπς. Αυτή η σελίδα δεν υπάρχει.",
|
||||
"404.button.home": "Πήγαινέ με πίσω",
|
||||
@@ -478,24 +426,16 @@ export default {
|
||||
"error.description": "Ωχ!",
|
||||
"error.button.back": "Πάμε πίσω",
|
||||
"error.msg.default": "Κάτι πήγε στραβά.",
|
||||
"error.msg.access_denied":
|
||||
"Ακυρώσατε τη διαδικασία ελέγχου ταυτότητας, παρακαλώ προσπαθήστε ξανά.",
|
||||
"error.msg.expired_token":
|
||||
"Η διαδικασία ελέγχου ταυτότητας διήρκεσε πολύ, παρακαλώ προσπαθήστε ξανά.",
|
||||
"error.msg.access_denied": "Ακυρώσατε τη διαδικασία ελέγχου ταυτότητας, παρακαλώ προσπαθήστε ξανά.",
|
||||
"error.msg.expired_token": "Η διαδικασία ελέγχου ταυτότητας διήρκεσε πολύ, παρακαλώ προσπαθήστε ξανά.",
|
||||
"error.msg.invalid_token": "Εσωτερικό σφάλμα",
|
||||
"error.msg.no_user":
|
||||
"Ο χρήστης που συνδέεται με αυτόν τον λογαριασμό {0} δεν υπάρχει.",
|
||||
"error.msg.no_email":
|
||||
"Δεν είναι δυνατή η λήψη διεύθυνσης ηλεκτρονικού ταχυδρομείου για αυτόν τον λογαριασμό {0}.",
|
||||
"error.msg.already_linked":
|
||||
"Αυτός ο λογαριασμός {0} είναι ήδη συνδεδεμένος με άλλο λογαριασμό.",
|
||||
"error.msg.not_linked":
|
||||
"Αυτός ο λογαριασμός {0} δεν έχει συνδεθεί με κανένα λογαριασμό ακόμα.",
|
||||
"error.msg.unverified_account":
|
||||
"Αυτός ο λογαριασμός {0} δεν έχει επαληθευτεί, παρακαλώ προσπαθήστε ξανά μετά την επαλήθευση.",
|
||||
"error.msg.discord_guild_permission_denied": "Δεν σας επιτρέπεται η σύνδεση.",
|
||||
"error.msg.cannot_get_user_info":
|
||||
"Δεν είναι δυνατή η λήψη των πληροφοριών χρήστη από αυτόν τον λογαριασμό {0}.",
|
||||
"error.msg.no_user": "Ο χρήστης που συνδέεται με αυτόν τον λογαριασμό {0} δεν υπάρχει.",
|
||||
"error.msg.no_email": "Δεν είναι δυνατή η λήψη διεύθυνσης ηλεκτρονικού ταχυδρομείου για αυτόν τον λογαριασμό {0}.",
|
||||
"error.msg.already_linked": "Αυτός ο λογαριασμός {0} είναι ήδη συνδεδεμένος με άλλο λογαριασμό.",
|
||||
"error.msg.not_linked": "Αυτός ο λογαριασμός {0} δεν έχει συνδεθεί με κανένα λογαριασμό ακόμα.",
|
||||
"error.msg.unverified_account": "Αυτός ο λογαριασμός {0} δεν έχει επαληθευτεί, παρακαλώ προσπαθήστε ξανά μετά την επαλήθευση.",
|
||||
"error.msg.user_not_allowed": "Δεν σας επιτρέπεται η σύνδεση.",
|
||||
"error.msg.cannot_get_user_info": "Δεν είναι δυνατή η λήψη των πληροφοριών χρήστη από αυτόν τον λογαριασμό {0}.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
@@ -516,18 +456,15 @@ export default {
|
||||
"common.text.navigate-to-link": "Μεταβείτε στο σύνδεσμο",
|
||||
"common.text.or": "ή",
|
||||
"common.button.go-back": "Επιστροφή",
|
||||
"common.button.go-home": "Go home",
|
||||
"common.button.go-home": "Μετάβαση στην αρχική",
|
||||
"common.notify.copied": "Ο σύνδεσμος σας αντιγράφηκε στο πρόχειρο",
|
||||
"common.success": "Επιτυχία",
|
||||
"common.error": "Σφάλμα",
|
||||
"common.error.unknown": "Προέκυψε άγνωστο σφάλμα",
|
||||
"common.error.invalid-email": "Μη έγκυρη διεύθυνση e-mail",
|
||||
"common.error.too-short":
|
||||
"Πρέπει να αποτελείται τουλάχιστον {length} χαρακτήρες",
|
||||
"common.error.too-long":
|
||||
"Πρέπει να αποτελείται το πολύ από {length} χαρακτήρες",
|
||||
"common.error.exact-length":
|
||||
"Πρέπει να αποτελείται ακριβώς από {length} χαρακτήρες",
|
||||
"common.error.too-short": "Πρέπει να αποτελείται τουλάχιστον {length} χαρακτήρες",
|
||||
"common.error.too-long": "Πρέπει να αποτελείται το πολύ από {length} χαρακτήρες",
|
||||
"common.error.exact-length": "Πρέπει να αποτελείται ακριβώς από {length} χαρακτήρες",
|
||||
"common.error.invalid-number": "Πρέπει να είναι αριθμός",
|
||||
"common.error.field-required": "Αυτό το πεδίο είναι υποχρεωτικό",
|
||||
};
|
||||
"common.error.field-required": "Αυτό το πεδίο είναι υποχρεωτικό"
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user