Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2e4019190 | ||
|
|
ffd4e43f11 | ||
|
|
0e5c673270 | ||
|
|
beece56327 | ||
|
|
a0d1d98e24 | ||
|
|
ca73ccf629 | ||
|
|
9f2097e788 | ||
|
|
2158df4228 | ||
|
|
37e765ddc7 | ||
|
|
a91c531642 | ||
|
|
5a7f7ca2f6 | ||
|
|
813ee4de2c | ||
|
|
b25c30d1ed | ||
|
|
c807d208d8 | ||
|
|
f82099f36e | ||
|
|
6345e21db9 | ||
|
|
f55aa80516 | ||
|
|
0ce8b528e1 | ||
|
|
8ff417a013 | ||
|
|
cb1a0d4090 | ||
|
|
753dbe83b7 | ||
|
|
0c2a62b0ca | ||
|
|
452c635933 | ||
|
|
0455ba1bc1 | ||
|
|
3ad6b03b6b | ||
|
|
91c3525b15 | ||
|
|
8403d7e14d | ||
|
|
8f71fd3435 | ||
|
|
155c743197 | ||
|
|
8b77e81d4c |
62
CHANGELOG.md
62
CHANGELOG.md
@@ -1,3 +1,65 @@
|
|||||||
|
## [0.14.0](https://github.com/stonith404/pingvin-share/compare/v0.13.1...v0.14.0) (2023-04-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **share, config:** more variables, placeholder and reset default ([#132](https://github.com/stonith404/pingvin-share/issues/132)) ([beece56](https://github.com/stonith404/pingvin-share/commit/beece56327da141c222fd9f5259697df6db9347a))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bool config variable can't be changed ([0e5c673](https://github.com/stonith404/pingvin-share/commit/0e5c67327092e4751208e559a2b0d5ee2b91b6e3))
|
||||||
|
|
||||||
|
### [0.13.1](https://github.com/stonith404/pingvin-share/compare/v0.13.0...v0.13.1) (2023-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* empty file can't be uploaded in chrome ([9f2097e](https://github.com/stonith404/pingvin-share/commit/9f2097e788dfb79c2f95085025934c3134a3eb38))
|
||||||
|
|
||||||
|
## [0.13.0](https://github.com/stonith404/pingvin-share/compare/v0.12.1...v0.13.0) (2023-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add preview modal ([c807d20](https://github.com/stonith404/pingvin-share/commit/c807d208d8f0518f6390f9f0f3d0eb00c12d213b))
|
||||||
|
* sort shared files ([b25c30d](https://github.com/stonith404/pingvin-share/commit/b25c30d1ed57230096b17afaf8545c7b0ef2e4b1))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* replace "pingvin share" with dynamic app name ([f55aa80](https://github.com/stonith404/pingvin-share/commit/f55aa805167f31864cb07e269a47533927cb533c))
|
||||||
|
* set password manually input not shown ([8ff417a](https://github.com/stonith404/pingvin-share/commit/8ff417a013a45a777308f71c4f0d1817bfeed6be))
|
||||||
|
* show line breaks in txt preview ([37e765d](https://github.com/stonith404/pingvin-share/commit/37e765ddc7b19554bc6fb50eb969984b58bf3cc5))
|
||||||
|
* upload file if it is 0 bytes ([f82099f](https://github.com/stonith404/pingvin-share/commit/f82099f36eb4699385fc16dfb0e0c02e5d55b1e3))
|
||||||
|
|
||||||
|
### [0.12.1](https://github.com/stonith404/pingvin-share/compare/v0.12.0...v0.12.1) (2023-03-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 48px icon does not update ([753dbe8](https://github.com/stonith404/pingvin-share/commit/753dbe83b770814115a2576c7a50e1bac9dc8ce1))
|
||||||
|
|
||||||
|
## [0.12.0](https://github.com/stonith404/pingvin-share/compare/v0.11.1...v0.12.0) (2023-03-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* ability to change logo in frontend ([8403d7e](https://github.com/stonith404/pingvin-share/commit/8403d7e14ded801c3842a9b3fd87c3f6824c519e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* crypto is not defined ([8f71fd3](https://github.com/stonith404/pingvin-share/commit/8f71fd343506506532c1a24a4c66a16b1021705f))
|
||||||
|
* home page shown even if disabled ([3ad6b03](https://github.com/stonith404/pingvin-share/commit/3ad6b03b6bd80168870049582683077b689fa548))
|
||||||
|
|
||||||
|
### [0.11.1](https://github.com/stonith404/pingvin-share/compare/v0.11.0...v0.11.1) (2023-03-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* old config variable prevents to create a share ([8b77e81](https://github.com/stonith404/pingvin-share/commit/8b77e81d4c1b8a2bf798595f5a66079c40734e09))
|
||||||
|
|
||||||
## [0.11.0](https://github.com/stonith404/pingvin-share/compare/v0.10.2...v0.11.0) (2023-03-04)
|
## [0.11.0](https://github.com/stonith404/pingvin-share/compare/v0.10.2...v0.11.0) (2023-03-04)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
95
CONTRIBUTING.es.md
Normal file
95
CONTRIBUTING.es.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
*Leer esto en otro idioma: [Inglés](CONTRIBUTING.md), [Español](CONTRIBUTING.es.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,3 +1,7 @@
|
|||||||
|
*Read this in another language: [Spanish](CONTRIBUTING.es.md), [English](CONTRIBUTING.md)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Contributing
|
# 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.
|
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.
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -1,26 +1,26 @@
|
|||||||
# Using node slim because prisma ORM needs libc for ARM builds
|
# Using node slim because prisma ORM needs libc for ARM builds
|
||||||
|
|
||||||
# Stage 1: on frontend dependency change
|
# Stage 1: on frontend dependency change
|
||||||
FROM node:18-slim AS frontend-dependencies
|
FROM node:19-slim AS frontend-dependencies
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
COPY frontend/package.json frontend/package-lock.json ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Stage 2: on frontend change
|
# Stage 2: on frontend change
|
||||||
FROM node:18-slim AS frontend-builder
|
FROM node:19-slim AS frontend-builder
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
COPY ./frontend .
|
COPY ./frontend .
|
||||||
COPY --from=frontend-dependencies /opt/app/node_modules ./node_modules
|
COPY --from=frontend-dependencies /opt/app/node_modules ./node_modules
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 3: on backend dependency change
|
# Stage 3: on backend dependency change
|
||||||
FROM node:18-slim AS backend-dependencies
|
FROM node:19-slim AS backend-dependencies
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
COPY backend/package.json backend/package-lock.json ./
|
COPY backend/package.json backend/package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Stage 4:on backend change
|
# Stage 4:on backend change
|
||||||
FROM node:18-slim AS backend-builder
|
FROM node:19-slim AS backend-builder
|
||||||
RUN apt-get update && apt-get install -y openssl
|
RUN apt-get update && apt-get install -y openssl
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
COPY ./backend .
|
COPY ./backend .
|
||||||
@@ -29,7 +29,7 @@ RUN npx prisma generate
|
|||||||
RUN npm run build && npm prune --production
|
RUN npm run build && npm prune --production
|
||||||
|
|
||||||
# Stage 5: Final image
|
# Stage 5: Final image
|
||||||
FROM node:18-slim AS runner
|
FROM node:19-slim AS runner
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
RUN apt-get update && apt-get install -y openssl
|
RUN apt-get update && apt-get install -y openssl
|
||||||
|
|
||||||
|
|||||||
129
README.es.md
Normal file
129
README.es.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
|
||||||
|
# <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](README.es.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 por 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"/>
|
||||||
|
|
||||||
|
## ⌨️ 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.
|
||||||
47
README.md
47
README.md
@@ -1,5 +1,11 @@
|
|||||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
# <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](README.es.md), [English](README.md)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Pingvin Share is self-hosted file sharing platform and an alternative for WeTransfer.
|
Pingvin Share is self-hosted file sharing platform and an alternative for WeTransfer.
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
@@ -16,7 +22,7 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran
|
|||||||
- [Demo](https://pingvin-share.dev.eliasschneider.com)
|
- [Demo](https://pingvin-share.dev.eliasschneider.com)
|
||||||
- [Review by DB Tech](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
- [Review by DB Tech](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||||
|
|
||||||
<img src="https://user-images.githubusercontent.com/58886915/167101708-b85032ad-f5b1-480a-b8d7-ec0096ea2a43.png" width="700"/>
|
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||||
|
|
||||||
## ⌨️ Setup
|
## ⌨️ Setup
|
||||||
|
|
||||||
@@ -33,7 +39,7 @@ The website is now listening on `http://localhost:3000`, have fun with Pingvin S
|
|||||||
|
|
||||||
Required tools:
|
Required tools:
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/download/) >= 14
|
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [pm2](https://pm2.keymetrics.io/) for running Pingvin Share in the background
|
- [pm2](https://pm2.keymetrics.io/) for running Pingvin Share in the background
|
||||||
|
|
||||||
@@ -88,26 +94,31 @@ docker compose up -d
|
|||||||
|
|
||||||
#### Stand-alone
|
#### Stand-alone
|
||||||
|
|
||||||
1. Remove the running app
|
1. Stop the running app
|
||||||
```
|
```bash
|
||||||
pm2 delete pingvin-share-backend pingvin-share-frontend
|
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||||
```
|
```
|
||||||
2. Repeat the steps from the [installation guide](#stand-alone-installation) except the `git clone` step.
|
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
|
||||||
|
```
|
||||||
|
|
||||||
### Custom branding
|
### Custom branding
|
||||||
|
|
||||||
#### Name
|
You can change the name and the logo of the app by visiting the admin configuration page.
|
||||||
|
|
||||||
You can change the name of the app by visiting the admin configuration page and changing the `App Name`.
|
|
||||||
|
|
||||||
#### Logo
|
|
||||||
|
|
||||||
You can change the logo of the app by replacing the images in the `/data/images` (or with the standalone installation `/frontend/public/img`) folder with your own logo. The folder contains the following images:
|
|
||||||
|
|
||||||
- `logo.png` - The logo in the header and home page
|
|
||||||
- `favicon.png` - The favicon
|
|
||||||
- `opengraph.png` - The image used for sharing on social media
|
|
||||||
- `icons/*` - The icons used for the PWA
|
|
||||||
|
|
||||||
## 🖤 Contribute
|
## 🖤 Contribute
|
||||||
|
|
||||||
|
|||||||
2052
backend/package-lock.json
generated
2052
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-backend",
|
"name": "pingvin-share-backend",
|
||||||
"version": "0.11.0",
|
"version": "0.14.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"dev": "cross-env NODE_ENV=development nest start --watch",
|
"dev": "cross-env NODE_ENV=development nest start --watch",
|
||||||
@@ -13,65 +13,68 @@
|
|||||||
"seed": "ts-node prisma/seed/config.seed.ts"
|
"seed": "ts-node prisma/seed/config.seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^9.2.1",
|
"@nestjs/common": "^9.3.9",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.3.1",
|
||||||
"@nestjs/core": "^9.2.1",
|
"@nestjs/core": "^9.3.9",
|
||||||
"@nestjs/jwt": "^10.0.1",
|
"@nestjs/jwt": "^10.0.2",
|
||||||
"@nestjs/passport": "^9.0.0",
|
"@nestjs/passport": "^9.0.3",
|
||||||
"@nestjs/platform-express": "^9.2.1",
|
"@nestjs/platform-express": "^9.3.9",
|
||||||
"@nestjs/schedule": "^2.1.0",
|
"@nestjs/schedule": "^2.2.0",
|
||||||
"@nestjs/swagger": "^6.2.1",
|
"@nestjs/swagger": "^6.2.1",
|
||||||
"@nestjs/throttler": "^3.1.0",
|
"@nestjs/throttler": "^4.0.0",
|
||||||
"@prisma/client": "^4.8.1",
|
"@prisma/client": "^4.11.0",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"body-parser": "^1.20.1",
|
"body-parser": "^1.20.2",
|
||||||
"clamscan": "^2.1.2",
|
"clamscan": "^2.1.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.14.0",
|
||||||
"content-disposition": "^0.5.4",
|
"content-disposition": "^0.5.4",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"nodemailer": "^6.9.0",
|
"nodemailer": "^6.9.1",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"qrcode-svg": "^1.1.0",
|
"qrcode-svg": "^1.1.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^4.0.4",
|
"rimraf": "^4.4.0",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
|
"sharp": "^0.31.3",
|
||||||
"ts-node": "^10.9.1"
|
"ts-node": "^10.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.1.8",
|
"@nestjs/cli": "^9.2.0",
|
||||||
"@nestjs/schematics": "^9.0.4",
|
"@nestjs/schematics": "^9.0.4",
|
||||||
"@nestjs/testing": "^9.2.1",
|
"@nestjs/testing": "^9.3.9",
|
||||||
"@types/archiver": "^5.3.1",
|
"@types/archiver": "^5.3.1",
|
||||||
"@types/clamscan": "^2.0.4",
|
"@types/clamscan": "^2.0.4",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/cron": "^2.0.0",
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.15",
|
"@types/express": "^4.17.17",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/node": "^18.11.18",
|
"@types/multer": "^1.4.7",
|
||||||
|
"@types/node": "^18.15.0",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/nodemailer": "^6.4.7",
|
||||||
"@types/passport-jwt": "^3.0.8",
|
"@types/passport-jwt": "^3.0.8",
|
||||||
"@types/qrcode-svg": "^1.1.1",
|
"@types/qrcode-svg": "^1.1.1",
|
||||||
|
"@types/sharp": "^0.31.1",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^5.54.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.35.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.7.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"newman": "^5.3.2",
|
"newman": "^5.3.2",
|
||||||
"prettier": "^2.8.2",
|
"prettier": "^2.8.4",
|
||||||
"prisma": "^4.9.0",
|
"prisma": "^4.11.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "^9.4.2",
|
||||||
"tsconfig-paths": "4.1.2",
|
"tsconfig-paths": "4.1.2",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.5",
|
||||||
"wait-on": "^7.0.1"
|
"wait-on": "^7.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Config" (
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"category" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"value" TEXT,
|
||||||
|
"defaultValue" TEXT NOT NULL DEFAULT '',
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"obscured" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"secret" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"locked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"order" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("name", "category")
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Config" ("category", "description", "locked", "name", "obscured", "order", "secret", "type", "updatedAt", "value") SELECT "category", "description", "locked", "name", "obscured", "order", "secret", "type", "updatedAt", "value" FROM "Config";
|
||||||
|
DROP TABLE "Config";
|
||||||
|
ALTER TABLE "new_Config" RENAME TO "Config";
|
||||||
|
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -131,15 +131,16 @@ model ShareSecurity {
|
|||||||
model Config {
|
model Config {
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
name String
|
name String
|
||||||
category String
|
category String
|
||||||
type String
|
type String
|
||||||
value String
|
defaultValue String @default("")
|
||||||
description String
|
value String?
|
||||||
obscured Boolean @default(false)
|
description String
|
||||||
secret Boolean @default(true)
|
obscured Boolean @default(false)
|
||||||
locked Boolean @default(false)
|
secret Boolean @default(true)
|
||||||
order Int
|
locked Boolean @default(false)
|
||||||
|
order Int
|
||||||
|
|
||||||
@@id([name, category])
|
@@id([name, category])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const configVariables: ConfigVariables = {
|
|||||||
jwtSecret: {
|
jwtSecret: {
|
||||||
description: "Long random string used to sign JWT tokens",
|
description: "Long random string used to sign JWT tokens",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: crypto.randomBytes(256).toString("base64"),
|
defaultValue: crypto.randomBytes(256).toString("base64"),
|
||||||
locked: true,
|
locked: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -14,20 +14,20 @@ const configVariables: ConfigVariables = {
|
|||||||
appName: {
|
appName: {
|
||||||
description: "Name of the application",
|
description: "Name of the application",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "Pingvin Share",
|
defaultValue: "Pingvin Share",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
appUrl: {
|
appUrl: {
|
||||||
description: "On which URL Pingvin Share is available",
|
description: "On which URL Pingvin Share is available",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "http://localhost:3000",
|
defaultValue: "http://localhost:3000",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
showHomePage: {
|
showHomePage: {
|
||||||
description: "Whether to show the home page",
|
description: "Whether to show the home page",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "true",
|
defaultValue: "true",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -35,21 +35,21 @@ const configVariables: ConfigVariables = {
|
|||||||
allowRegistration: {
|
allowRegistration: {
|
||||||
description: "Whether registration is allowed",
|
description: "Whether registration is allowed",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "true",
|
defaultValue: "true",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
allowUnauthenticatedShares: {
|
allowUnauthenticatedShares: {
|
||||||
description: "Whether unauthorized users can create shares",
|
description: "Whether unauthorized users can create shares",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "false",
|
defaultValue: "false",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
maxSize: {
|
maxSize: {
|
||||||
description: "Maximum share size in bytes",
|
description: "Maximum share size in bytes",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: "1073741824",
|
defaultValue: "1073741824",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
@@ -59,7 +59,7 @@ const configVariables: ConfigVariables = {
|
|||||||
description:
|
description:
|
||||||
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "false",
|
defaultValue: "false",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
@@ -67,53 +67,53 @@ const configVariables: ConfigVariables = {
|
|||||||
description:
|
description:
|
||||||
"Subject of the email which gets sent to the share recipients.",
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "Files shared with you",
|
defaultValue: "Files shared with you",
|
||||||
},
|
},
|
||||||
shareRecipientsMessage: {
|
shareRecipientsMessage: {
|
||||||
description:
|
description:
|
||||||
"Message which gets sent to the share recipients. {creator} and {shareUrl} will be replaced with the creator's name and the share URL.",
|
"Message which gets sent to the share recipients.\n\nAvailable 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\nVariables will be replaced with the actual values.",
|
||||||
type: "text",
|
type: "text",
|
||||||
value:
|
defaultValue:
|
||||||
"Hey!\n{creator} shared some files with you. View or download the files with this link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
"Hey!\n\n{creator} shared some files with you, view or download the files with this link: {shareUrl}\n\nThe share will expire {expires}.\n\nNote: {desc}\n\nShared securely with Pingvin Share 🐧",
|
||||||
},
|
},
|
||||||
reverseShareSubject: {
|
reverseShareSubject: {
|
||||||
description:
|
description:
|
||||||
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "Reverse share link used",
|
defaultValue: "Reverse share link used",
|
||||||
},
|
},
|
||||||
reverseShareMessage: {
|
reverseShareMessage: {
|
||||||
description:
|
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.",
|
"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.",
|
||||||
type: "text",
|
type: "text",
|
||||||
value:
|
defaultValue:
|
||||||
"Hey!\nA share was just created with your reverse share link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
"Hey!\n\nA share was just created with your reverse share link: {shareUrl}\n\nShared securely with Pingvin Share 🐧",
|
||||||
},
|
},
|
||||||
resetPasswordSubject: {
|
resetPasswordSubject: {
|
||||||
description:
|
description:
|
||||||
"Subject of the email which gets sent when a user requests a password reset.",
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "Pingvin Share password reset",
|
defaultValue: "Pingvin Share password reset",
|
||||||
},
|
},
|
||||||
resetPasswordMessage: {
|
resetPasswordMessage: {
|
||||||
description:
|
description:
|
||||||
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
type: "text",
|
type: "text",
|
||||||
value:
|
defaultValue:
|
||||||
"Hey!\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\nPingvin Share 🐧",
|
"Hey!\n\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\n\nPingvin Share 🐧",
|
||||||
},
|
},
|
||||||
inviteSubject: {
|
inviteSubject: {
|
||||||
description:
|
description:
|
||||||
"Subject of the email which gets sent when an admin invites an user.",
|
"Subject of the email which gets sent when an admin invites an user.",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "Pingvin Share invite",
|
defaultValue: "Pingvin Share invite",
|
||||||
},
|
},
|
||||||
inviteMessage: {
|
inviteMessage: {
|
||||||
description:
|
description:
|
||||||
"Message which gets sent when an admin invites an user. {url} will be replaced with the invite URL and {password} with the password.",
|
"Message which gets sent when an admin invites an user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
type: "text",
|
type: "text",
|
||||||
value:
|
defaultValue:
|
||||||
"Hey!\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\nYour password is: {password}\nPingvin Share 🐧",
|
"Hey!\n\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\n\nYour password is: {password}\n\nPingvin Share 🐧",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
smtp: {
|
smtp: {
|
||||||
@@ -121,33 +121,33 @@ const configVariables: ConfigVariables = {
|
|||||||
description:
|
description:
|
||||||
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "false",
|
defaultValue: "false",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
host: {
|
host: {
|
||||||
description: "Host of the SMTP server",
|
description: "Host of the SMTP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "",
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
port: {
|
port: {
|
||||||
description: "Port of the SMTP server",
|
description: "Port of the SMTP server",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: "0",
|
defaultValue: "0",
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
description: "Email address which the emails get sent from",
|
description: "Email address which the emails get sent from",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "",
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
description: "Username of the SMTP server",
|
description: "Username of the SMTP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "",
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
description: "Password of the SMTP server",
|
description: "Password of the SMTP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "",
|
defaultValue: "",
|
||||||
obscured: true,
|
obscured: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
import { ConfigService } from "src/config/config.service";
|
import { ConfigService } from "src/config/config.service";
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { User } from "@prisma/client";
|
|||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
import { authenticator, totp } from "otplib";
|
import { authenticator, totp } from "otplib";
|
||||||
import * as qrcode from "qrcode-svg";
|
import * as qrcode from "qrcode-svg";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
||||||
@@ -16,7 +17,8 @@ import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
|||||||
export class AuthTotpService {
|
export class AuthTotpService {
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
private authService: AuthService
|
private authService: AuthService,
|
||||||
|
private config: ConfigService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async signInTotp(dto: AuthSignInTotpDTO) {
|
async signInTotp(dto: AuthSignInTotpDTO) {
|
||||||
@@ -42,7 +44,7 @@ export class AuthTotpService {
|
|||||||
throw new UnauthorizedException("Invalid login token");
|
throw new UnauthorizedException("Invalid login token");
|
||||||
|
|
||||||
if (token.expiresAt < new Date())
|
if (token.expiresAt < new Date())
|
||||||
throw new UnauthorizedException("Login token expired");
|
throw new UnauthorizedException("Login token expired", "token_expired");
|
||||||
|
|
||||||
// Check the TOTP code
|
// Check the TOTP code
|
||||||
const { totpSecret } = await this.prisma.user.findUnique({
|
const { totpSecret } = await this.prisma.user.findUnique({
|
||||||
@@ -95,7 +97,7 @@ export class AuthTotpService {
|
|||||||
|
|
||||||
const otpURL = totp.keyuri(
|
const otpURL = totp.keyuri(
|
||||||
user.username || user.email,
|
user.username || user.email,
|
||||||
"pingvin-share",
|
this.config.get("general.appName"),
|
||||||
secret
|
secret
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
FileTypeValidator,
|
||||||
Get,
|
Get,
|
||||||
Param,
|
Param,
|
||||||
|
ParseFilePipe,
|
||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
|
UploadedFile,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
import { SkipThrottle } from "@nestjs/throttler";
|
import { SkipThrottle } from "@nestjs/throttler";
|
||||||
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
||||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||||
@@ -16,11 +21,13 @@ import { AdminConfigDTO } from "./dto/adminConfig.dto";
|
|||||||
import { ConfigDTO } from "./dto/config.dto";
|
import { ConfigDTO } from "./dto/config.dto";
|
||||||
import { TestEmailDTO } from "./dto/testEmail.dto";
|
import { TestEmailDTO } from "./dto/testEmail.dto";
|
||||||
import UpdateConfigDTO from "./dto/updateConfig.dto";
|
import UpdateConfigDTO from "./dto/updateConfig.dto";
|
||||||
|
import { LogoService } from "./logo.service";
|
||||||
|
|
||||||
@Controller("configs")
|
@Controller("configs")
|
||||||
export class ConfigController {
|
export class ConfigController {
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private logoService: LogoService,
|
||||||
private emailService: EmailService
|
private emailService: EmailService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -51,4 +58,18 @@ export class ConfigController {
|
|||||||
async testEmail(@Body() { email }: TestEmailDTO) {
|
async testEmail(@Body() { email }: TestEmailDTO) {
|
||||||
await this.emailService.sendTestMail(email);
|
await this.emailService.sendTestMail(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("admin/logo")
|
||||||
|
@UseInterceptors(FileInterceptor("file"))
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async uploadLogo(
|
||||||
|
@UploadedFile(
|
||||||
|
new ParseFilePipe({
|
||||||
|
validators: [new FileTypeValidator({ fileType: "image/png" })],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
file: Express.Multer.File
|
||||||
|
) {
|
||||||
|
return await this.logoService.create(file.buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { EmailModule } from "src/email/email.module";
|
|||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { ConfigController } from "./config.controller";
|
import { ConfigController } from "./config.controller";
|
||||||
import { ConfigService } from "./config.service";
|
import { ConfigService } from "./config.service";
|
||||||
|
import { LogoService } from "./logo.service";
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
@@ -16,6 +17,7 @@ import { ConfigService } from "./config.service";
|
|||||||
inject: [PrismaService],
|
inject: [PrismaService],
|
||||||
},
|
},
|
||||||
ConfigService,
|
ConfigService,
|
||||||
|
LogoService,
|
||||||
],
|
],
|
||||||
controllers: [ConfigController],
|
controllers: [ConfigController],
|
||||||
exports: [ConfigService],
|
exports: [ConfigService],
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ export class ConfigService {
|
|||||||
|
|
||||||
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
||||||
|
|
||||||
if (configVariable.type == "number") return parseInt(configVariable.value);
|
const value = configVariable.value ?? configVariable.defaultValue;
|
||||||
if (configVariable.type == "boolean") return configVariable.value == "true";
|
|
||||||
|
if (configVariable.type == "number") return parseInt(value);
|
||||||
|
if (configVariable.type == "boolean") return value == "true";
|
||||||
if (configVariable.type == "string" || configVariable.type == "text")
|
if (configVariable.type == "string" || configVariable.type == "text")
|
||||||
return configVariable.value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByCategory(category: string) {
|
async getByCategory(category: string) {
|
||||||
@@ -35,8 +37,9 @@ export class ConfigService {
|
|||||||
|
|
||||||
return configVariables.map((variable) => {
|
return configVariables.map((variable) => {
|
||||||
return {
|
return {
|
||||||
key: `${variable.category}.${variable.name}`,
|
|
||||||
...variable,
|
...variable,
|
||||||
|
key: `${variable.category}.${variable.name}`,
|
||||||
|
value: variable.value ?? variable.defaultValue,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -48,8 +51,9 @@ export class ConfigService {
|
|||||||
|
|
||||||
return configVariables.map((variable) => {
|
return configVariables.map((variable) => {
|
||||||
return {
|
return {
|
||||||
key: `${variable.category}.${variable.name}`,
|
|
||||||
...variable,
|
...variable,
|
||||||
|
key: `${variable.category}.${variable.name}`,
|
||||||
|
value: variable.value ?? variable.defaultValue,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -77,7 +81,9 @@ export class ConfigService {
|
|||||||
if (!configVariable || configVariable.locked)
|
if (!configVariable || configVariable.locked)
|
||||||
throw new NotFoundException("Config variable not found");
|
throw new NotFoundException("Config variable not found");
|
||||||
|
|
||||||
if (
|
if (value == "") {
|
||||||
|
value = null;
|
||||||
|
} else if (
|
||||||
typeof value != configVariable.type &&
|
typeof value != configVariable.type &&
|
||||||
typeof value == "string" &&
|
typeof value == "string" &&
|
||||||
configVariable.type != "text"
|
configVariable.type != "text"
|
||||||
@@ -94,7 +100,7 @@ export class ConfigService {
|
|||||||
name: key.split(".")[1],
|
name: key.split(".")[1],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: { value: value.toString() },
|
data: { value: value ? value.toString() : null },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.configVariables = await this.prisma.config.findMany();
|
this.configVariables = await this.prisma.config.findMany();
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export class AdminConfigDTO extends ConfigDTO {
|
|||||||
@Expose()
|
@Expose()
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
defaultValue: string;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { IsNotEmpty, IsString, ValidateIf } from "class-validator";
|
import { IsNotEmpty, IsString } from "class-validator";
|
||||||
|
|
||||||
class UpdateConfigDTO {
|
class UpdateConfigDTO {
|
||||||
@IsString()
|
@IsString()
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ValidateIf((dto) => dto.value !== "")
|
|
||||||
value: string | number | boolean;
|
value: string | number | boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
backend/src/config/logo.service.ts
Normal file
32
backend/src/config/logo.service.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as sharp from "sharp";
|
||||||
|
|
||||||
|
const IMAGES_PATH = "../frontend/public/img";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LogoService {
|
||||||
|
async create(file: Buffer) {
|
||||||
|
fs.writeFileSync(`${IMAGES_PATH}/logo.png`, file, "binary");
|
||||||
|
this.createFavicon(file);
|
||||||
|
this.createPWAIcons(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFavicon(file: Buffer) {
|
||||||
|
const resized = await sharp(file).resize(16).toBuffer();
|
||||||
|
fs.promises.writeFile(`${IMAGES_PATH}/favicon.ico`, resized, "binary");
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPWAIcons(file: Buffer) {
|
||||||
|
const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
|
|
||||||
|
for (const size of sizes) {
|
||||||
|
const resized = await sharp(file).resize(size).toBuffer();
|
||||||
|
fs.promises.writeFile(
|
||||||
|
`${IMAGES_PATH}/icons/icon-${size}x${size}.png`,
|
||||||
|
resized,
|
||||||
|
"binary"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
import {
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Logger,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
import * as moment from "moment";
|
||||||
import * as nodemailer from "nodemailer";
|
import * as nodemailer from "nodemailer";
|
||||||
import { ConfigService } from "src/config/config.service";
|
import { ConfigService } from "src/config/config.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
constructor(private config: ConfigService) {}
|
constructor(private config: ConfigService) {}
|
||||||
|
private readonly logger = new Logger(EmailService.name);
|
||||||
|
|
||||||
getTransporter() {
|
getTransporter() {
|
||||||
if (!this.config.get("smtp.enabled"))
|
if (!this.config.get("smtp.enabled"))
|
||||||
@@ -22,44 +28,63 @@ export class EmailService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMailToShareRecepients(
|
private async sendMail(email: string, subject: string, text: string) {
|
||||||
|
await this.getTransporter()
|
||||||
|
.sendMail({
|
||||||
|
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||||
|
"smtp.email"
|
||||||
|
)}>`,
|
||||||
|
to: email,
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.logger.error(e);
|
||||||
|
throw new InternalServerErrorException("Failed to send email");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMailToShareRecipients(
|
||||||
recipientEmail: string,
|
recipientEmail: string,
|
||||||
shareId: string,
|
shareId: string,
|
||||||
creator?: User
|
creator?: User,
|
||||||
|
description?: string,
|
||||||
|
expiration?: Date
|
||||||
) {
|
) {
|
||||||
if (!this.config.get("email.enableShareEmailRecipients"))
|
if (!this.config.get("email.enableShareEmailRecipients"))
|
||||||
throw new InternalServerErrorException("Email service disabled");
|
throw new InternalServerErrorException("Email service disabled");
|
||||||
|
|
||||||
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
|
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
|
||||||
|
|
||||||
await this.getTransporter().sendMail({
|
await this.sendMail(
|
||||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
recipientEmail,
|
||||||
"smtp.email"
|
this.config.get("email.shareRecipientsSubject"),
|
||||||
)}>`,
|
this.config
|
||||||
to: recipientEmail,
|
|
||||||
subject: this.config.get("email.shareRecipientsSubject"),
|
|
||||||
text: this.config
|
|
||||||
.get("email.shareRecipientsMessage")
|
.get("email.shareRecipientsMessage")
|
||||||
.replaceAll("\\n", "\n")
|
.replaceAll("\\n", "\n")
|
||||||
.replaceAll("{creator}", creator?.username ?? "Someone")
|
.replaceAll("{creator}", creator?.username ?? "Someone")
|
||||||
.replaceAll("{shareUrl}", shareUrl),
|
.replaceAll("{shareUrl}", shareUrl)
|
||||||
});
|
.replaceAll("{desc}", description ?? "No description")
|
||||||
|
.replaceAll(
|
||||||
|
"{expires}",
|
||||||
|
moment(expiration).unix() != 0
|
||||||
|
? moment(expiration).fromNow()
|
||||||
|
: "in: never"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMailToReverseShareCreator(recipientEmail: string, shareId: string) {
|
async sendMailToReverseShareCreator(recipientEmail: string, shareId: string) {
|
||||||
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
|
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
|
||||||
|
|
||||||
await this.getTransporter().sendMail({
|
await this.sendMail(
|
||||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
recipientEmail,
|
||||||
"smtp.email"
|
this.config.get("email.reverseShareSubject"),
|
||||||
)}>`,
|
this.config
|
||||||
to: recipientEmail,
|
|
||||||
subject: this.config.get("email.reverseShareSubject"),
|
|
||||||
text: this.config
|
|
||||||
.get("email.reverseShareMessage")
|
.get("email.reverseShareMessage")
|
||||||
.replaceAll("\\n", "\n")
|
.replaceAll("\\n", "\n")
|
||||||
.replaceAll("{shareUrl}", shareUrl),
|
.replaceAll("{shareUrl}", shareUrl)
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendResetPasswordEmail(recipientEmail: string, token: string) {
|
async sendResetPasswordEmail(recipientEmail: string, token: string) {
|
||||||
@@ -67,47 +92,42 @@ export class EmailService {
|
|||||||
"general.appUrl"
|
"general.appUrl"
|
||||||
)}/auth/resetPassword/${token}`;
|
)}/auth/resetPassword/${token}`;
|
||||||
|
|
||||||
await this.getTransporter().sendMail({
|
await this.sendMail(
|
||||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
recipientEmail,
|
||||||
"smtp.email"
|
this.config.get("email.resetPasswordSubject"),
|
||||||
)}>`,
|
this.config
|
||||||
to: recipientEmail,
|
|
||||||
subject: this.config.get("email.resetPasswordSubject"),
|
|
||||||
text: this.config
|
|
||||||
.get("email.resetPasswordMessage")
|
.get("email.resetPasswordMessage")
|
||||||
.replaceAll("{url}", resetPasswordUrl),
|
.replaceAll("\\n", "\n")
|
||||||
});
|
.replaceAll("{url}", resetPasswordUrl)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendInviteEmail(recipientEmail: string, password: string) {
|
async sendInviteEmail(recipientEmail: string, password: string) {
|
||||||
const loginUrl = `${this.config.get("general.appUrl")}/auth/signIn`;
|
const loginUrl = `${this.config.get("general.appUrl")}/auth/signIn`;
|
||||||
|
|
||||||
await this.getTransporter().sendMail({
|
await this.sendMail(
|
||||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
recipientEmail,
|
||||||
"smtp.email"
|
this.config.get("email.inviteSubject"),
|
||||||
)}>`,
|
this.config
|
||||||
to: recipientEmail,
|
|
||||||
subject: this.config.get("email.inviteSubject"),
|
|
||||||
text: this.config
|
|
||||||
.get("email.inviteMessage")
|
.get("email.inviteMessage")
|
||||||
.replaceAll("{url}", loginUrl)
|
.replaceAll("{url}", loginUrl)
|
||||||
.replaceAll("{password}", password),
|
.replaceAll("{password}", password)
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendTestMail(recipientEmail: string) {
|
async sendTestMail(recipientEmail: string) {
|
||||||
try {
|
await this.getTransporter()
|
||||||
await this.getTransporter().sendMail({
|
.sendMail({
|
||||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||||
"smtp.email"
|
"smtp.email"
|
||||||
)}>`,
|
)}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
subject: "Test email",
|
subject: "Test email",
|
||||||
text: "This is a test email",
|
text: "This is a test email",
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.logger.error(e);
|
||||||
|
throw new InternalServerErrorException(e.message);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
throw new InternalServerErrorException(e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export class FileController {
|
|||||||
) {
|
) {
|
||||||
const { id, name, chunkIndex, totalChunks } = query;
|
const { id, name, chunkIndex, totalChunks } = query;
|
||||||
|
|
||||||
const data = body.toString().split(",")[1];
|
// Data can be empty if the file is empty
|
||||||
|
const data = body.toString().split(",")[1] ?? "";
|
||||||
|
|
||||||
return await this.fileService.create(
|
return await this.fileService.create(
|
||||||
data,
|
data,
|
||||||
@@ -51,7 +52,7 @@ export class FileController {
|
|||||||
const zip = this.fileService.getZip(shareId);
|
const zip = this.fileService.getZip(shareId);
|
||||||
res.set({
|
res.set({
|
||||||
"Content-Type": "application/zip",
|
"Content-Type": "application/zip",
|
||||||
"Content-Disposition": contentDisposition(`pingvin-share-${shareId}.zip`),
|
"Content-Disposition": contentDisposition(`${shareId}.zip`),
|
||||||
});
|
});
|
||||||
|
|
||||||
return new StreamableFile(zip);
|
return new StreamableFile(zip);
|
||||||
|
|||||||
@@ -142,12 +142,14 @@ export class ShareService {
|
|||||||
this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
|
this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send email for each recepient
|
// Send email for each recipient
|
||||||
for (const recepient of share.recipients) {
|
for (const recipient of share.recipients) {
|
||||||
await this.emailService.sendMailToShareRecepients(
|
await this.emailService.sendMailToShareRecipients(
|
||||||
recepient.email,
|
recipient.email,
|
||||||
share.id,
|
share.id,
|
||||||
share.creator
|
share.creator,
|
||||||
|
share.description,
|
||||||
|
share.expiration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +165,7 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if any file is malicious with ClamAV
|
// Check if any file is malicious with ClamAV
|
||||||
this.clamScanService.checkAndRemove(share.id);
|
void this.clamScanService.checkAndRemove(share.id);
|
||||||
|
|
||||||
if (share.reverseShare) {
|
if (share.reverseShare) {
|
||||||
await this.prisma.reverseShare.update({
|
await this.prisma.reverseShare.update({
|
||||||
@@ -172,7 +174,7 @@ export class ShareService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.prisma.share.update({
|
return this.prisma.share.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { uploadLocked: true },
|
data: { uploadLocked: true },
|
||||||
});
|
});
|
||||||
@@ -195,14 +197,12 @@ export class ShareService {
|
|||||||
include: { recipients: true },
|
include: { recipients: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const sharesWithEmailRecipients = shares.map((share) => {
|
return shares.map((share) => {
|
||||||
return {
|
return {
|
||||||
...share,
|
...share,
|
||||||
recipients: share.recipients.map((recipients) => recipients.email),
|
recipients: share.recipients.map((recipients) => recipients.email),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return sharesWithEmailRecipients;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string): Promise<any> {
|
async get(id: string): Promise<any> {
|
||||||
@@ -222,7 +222,7 @@ export class ShareService {
|
|||||||
throw new NotFoundException("Share not found");
|
throw new NotFoundException("Share not found");
|
||||||
return {
|
return {
|
||||||
...share,
|
...share,
|
||||||
hasPassword: share.security?.password ? true : false,
|
hasPassword: !!share.security?.password,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ export class ShareService {
|
|||||||
share?.security?.password &&
|
share?.security?.password &&
|
||||||
!(await argon.verify(share.security.password, password))
|
!(await argon.verify(share.security.password, password))
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenException("Wrong password");
|
throw new ForbiddenException("Wrong password", "wrong_password");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (share.security?.maxViews && share.security.maxViews <= share.views) {
|
if (share.security?.maxViews && share.security.maxViews <= share.views) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
|
import * as crypto from "crypto";
|
||||||
import { EmailService } from "src/email/email.service";
|
import { EmailService } from "src/email/email.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { CreateUserDTO } from "./dto/createUser.dto";
|
import { CreateUserDTO } from "./dto/createUser.dto";
|
||||||
@@ -28,7 +29,7 @@ export class UserSevice {
|
|||||||
if (!dto.password) {
|
if (!dto.password) {
|
||||||
const randomPassword = crypto.randomUUID();
|
const randomPassword = crypto.randomUUID();
|
||||||
hash = await argon.hash(randomPassword);
|
hash = await argon.hash(randomPassword);
|
||||||
this.emailService.sendInviteEmail(dto.email, randomPassword);
|
await this.emailService.sendInviteEmail(dto.email, randomPassword);
|
||||||
} else {
|
} else {
|
||||||
hash = await argon.hash(dto.password);
|
hash = await argon.hash(dto.password);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ const { version } = require('./package.json');
|
|||||||
|
|
||||||
const withPWA = require("next-pwa")({
|
const withPWA = require("next-pwa")({
|
||||||
dest: "public",
|
dest: "public",
|
||||||
disable: process.env.NODE_ENV == "development",
|
disable: process.env.NODE_ENV === "development",
|
||||||
|
reloadOnOnline: false,
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /^https?.*/,
|
||||||
|
handler: 'NetworkOnly',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
reloadOnOnline: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = withPWA({
|
module.exports = withPWA({
|
||||||
|
|||||||
1890
frontend/package-lock.json
generated
1890
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-frontend",
|
"name": "pingvin-share-frontend",
|
||||||
"version": "0.11.0",
|
"version": "0.14.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@@ -9,43 +9,44 @@
|
|||||||
"format": "prettier --write \"src/**/*.ts*\""
|
"format": "prettier --write \"src/**/*.ts*\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@mantine/core": "^5.10.0",
|
"@mantine/core": "^6.0.1",
|
||||||
"@mantine/dropzone": "^5.10.0",
|
"@mantine/dropzone": "^6.0.1",
|
||||||
"@mantine/form": "^5.10.0",
|
"@mantine/form": "^6.0.1",
|
||||||
"@mantine/hooks": "^5.10.0",
|
"@mantine/hooks": "^6.0.1",
|
||||||
"@mantine/modals": "^5.10.0",
|
"@mantine/modals": "^6.0.1",
|
||||||
"@mantine/next": "^5.10.0",
|
"@mantine/next": "^6.0.1",
|
||||||
"@mantine/notifications": "^5.10.0",
|
"@mantine/notifications": "^6.0.1",
|
||||||
"axios": "^1.2.2",
|
"axios": "^1.3.4",
|
||||||
"cookies-next": "^2.1.1",
|
"cookies-next": "^2.1.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jose": "^4.11.2",
|
"jose": "^4.13.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"next": "^13.1.2",
|
"next": "^13.2.4",
|
||||||
"next-cookies": "^2.0.3",
|
"next-cookies": "^2.0.3",
|
||||||
"next-http-proxy-middleware": "^1.2.5",
|
"next-http-proxy-middleware": "^1.2.5",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"p-limit": "^4.0.0",
|
"p-limit": "^4.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.8.0",
|
||||||
"yup": "^0.32.11"
|
"sharp": "^0.31.3",
|
||||||
|
"yup": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.15.0",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.11",
|
||||||
"axios": "^1.2.2",
|
"axios": "^1.3.4",
|
||||||
"eslint": "8.31.0",
|
"eslint": "8.35.0",
|
||||||
"eslint-config-next": "^13.1.2",
|
"eslint-config-next": "^13.2.4",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.7.0",
|
||||||
"prettier": "^2.8.2",
|
"prettier": "^2.8.4",
|
||||||
"tar": "^6.1.13",
|
"tar": "^6.1.13",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -7,7 +7,6 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
@@ -27,7 +26,7 @@ const showEnableTotpModal = (
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: <Title order={4}>Enable TOTP</Title>,
|
title: "Enable TOTP",
|
||||||
children: (
|
children: (
|
||||||
<CreateEnableTotpModal options={options} refreshUser={refreshUser} />
|
<CreateEnableTotpModal options={options} refreshUser={refreshUser} />
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ const AdminConfigInput = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
stringValue: configVariable.value,
|
stringValue: configVariable.value ?? configVariable.defaultValue,
|
||||||
textValue: configVariable.value,
|
textValue: configVariable.value ?? configVariable.defaultValue,
|
||||||
numberValue: parseInt(configVariable.value),
|
numberValue: parseInt(
|
||||||
booleanValue: configVariable.value == "true",
|
configVariable.value ?? configVariable.defaultValue
|
||||||
|
),
|
||||||
|
booleanValue:
|
||||||
|
(configVariable.value ?? configVariable.defaultValue) == "true",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -35,29 +38,38 @@ const AdminConfigInput = ({
|
|||||||
{configVariable.type == "string" &&
|
{configVariable.type == "string" &&
|
||||||
(configVariable.obscured ? (
|
(configVariable.obscured ? (
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
style={{ width: "100%" }}
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
{...form.getInputProps("stringValue")}
|
{...form.getInputProps("stringValue")}
|
||||||
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{ width: "100%" }}
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
{...form.getInputProps("stringValue")}
|
{...form.getInputProps("stringValue")}
|
||||||
|
placeholder={configVariable.defaultValue}
|
||||||
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{configVariable.type == "text" && (
|
{configVariable.type == "text" && (
|
||||||
<Textarea
|
<Textarea
|
||||||
style={{ width: "100%" }}
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
autosize
|
autosize
|
||||||
{...form.getInputProps("textValue")}
|
{...form.getInputProps("textValue")}
|
||||||
|
placeholder={configVariable.defaultValue}
|
||||||
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{configVariable.type == "number" && (
|
{configVariable.type == "number" && (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
{...form.getInputProps("numberValue")}
|
{...form.getInputProps("numberValue")}
|
||||||
|
placeholder={configVariable.defaultValue}
|
||||||
onChange={(number) => onValueChange(configVariable, number)}
|
onChange={(number) => onValueChange(configVariable, number)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Box, FileInput, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { TbUpload } from "react-icons/tb";
|
||||||
|
|
||||||
|
const LogoConfigInput = ({
|
||||||
|
logo,
|
||||||
|
setLogo,
|
||||||
|
}: {
|
||||||
|
logo: File | null;
|
||||||
|
setLogo: Dispatch<SetStateAction<File | null>>;
|
||||||
|
}) => {
|
||||||
|
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group position="apart">
|
||||||
|
<Stack style={{ maxWidth: isMobile ? "100%" : "40%" }} spacing={0}>
|
||||||
|
<Title order={6}>Logo</Title>
|
||||||
|
<Text color="dimmed" size="sm" mb="xs">
|
||||||
|
Change your logo by uploading a new image. The image must be a PNG and
|
||||||
|
should have the format 1:1.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack></Stack>
|
||||||
|
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||||
|
<FileInput
|
||||||
|
clearable
|
||||||
|
icon={<TbUpload size={14} />}
|
||||||
|
value={logo}
|
||||||
|
onChange={(v) => setLogo(v)}
|
||||||
|
accept=".png"
|
||||||
|
placeholder="Pick image"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogoConfigInput;
|
||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Switch,
|
Switch,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
@@ -19,7 +18,7 @@ const showCreateUserModal = (
|
|||||||
getUsers: () => void
|
getUsers: () => void
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: <Title order={5}>Create user</Title>,
|
title: "Create user",
|
||||||
children: (
|
children: (
|
||||||
<Body modals={modals} smtpEnabled={smtpEnabled} getUsers={getUsers} />
|
<Body modals={modals} smtpEnabled={smtpEnabled} getUsers={getUsers} />
|
||||||
),
|
),
|
||||||
@@ -79,13 +78,12 @@ const Body = ({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{form.values.setPasswordManually ||
|
{(form.values.setPasswordManually || !smtpEnabled) && (
|
||||||
(!smtpEnabled && (
|
<PasswordInput
|
||||||
<PasswordInput
|
label="Password"
|
||||||
label="Password"
|
{...form.getInputProps("password")}
|
||||||
{...form.getInputProps("password")}
|
/>
|
||||||
/>
|
)}
|
||||||
))}
|
|
||||||
<Switch
|
<Switch
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Switch,
|
Switch,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
@@ -21,7 +20,7 @@ const showUpdateUserModal = (
|
|||||||
getUsers: () => void
|
getUsers: () => void
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: <Title order={5}>Update {user.username}</Title>,
|
title: `Update ${user.username}`,
|
||||||
children: <Body user={user} modals={modals} getUsers={getUsers} />,
|
children: <Body user={user} modals={modals} getUsers={getUsers} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,11 +32,6 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
|||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
emailOrUsername: yup.string().required(),
|
emailOrUsername: yup.string().required(),
|
||||||
password: yup.string().min(8).required(),
|
password: yup.string().min(8).required(),
|
||||||
totp: yup.string().when("totpRequired", {
|
|
||||||
is: true,
|
|
||||||
then: yup.string().min(6).max(6).required(),
|
|
||||||
otherwise: yup.string(),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@@ -79,8 +74,8 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
|||||||
router.replace(redirectPath);
|
router.replace(redirectPath);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error?.response?.data?.message == "Login token expired") {
|
if (error?.response?.data?.error == "share_password_required") {
|
||||||
toast.error("Login token expired");
|
toast.axiosError(error);
|
||||||
// Refresh the page to start over
|
// Refresh the page to start over
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
41
frontend/src/components/core/SortIcon.tsx
Normal file
41
frontend/src/components/core/SortIcon.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ActionIcon } from "@mantine/core";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { TbChevronDown, TbChevronUp, TbSelector } from "react-icons/tb";
|
||||||
|
|
||||||
|
export type TableSort = {
|
||||||
|
property?: string;
|
||||||
|
direction: "asc" | "desc";
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableSortIcon = ({
|
||||||
|
sort,
|
||||||
|
setSort,
|
||||||
|
property,
|
||||||
|
}: {
|
||||||
|
sort: TableSort;
|
||||||
|
setSort: Dispatch<SetStateAction<TableSort>>;
|
||||||
|
property: string;
|
||||||
|
}) => {
|
||||||
|
if (sort.property === property) {
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() =>
|
||||||
|
setSort({
|
||||||
|
property,
|
||||||
|
direction: sort.direction === "asc" ? "desc" : "asc",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{sort.direction === "asc" ? <TbChevronDown /> : <TbChevronUp />}
|
||||||
|
</ActionIcon>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ActionIcon onClick={() => setSort({ property, direction: "asc" })}>
|
||||||
|
<TbSelector />
|
||||||
|
</ActionIcon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableSortIcon;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
|
Box,
|
||||||
Group,
|
Group,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -8,9 +9,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import mime from "mime-types";
|
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { TbDownload, TbEye, TbLink } from "react-icons/tb";
|
import { TbDownload, TbEye, TbLink } from "react-icons/tb";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
@@ -18,13 +17,17 @@ import { FileMetaData } from "../../types/File.type";
|
|||||||
import { Share } from "../../types/share.type";
|
import { Share } from "../../types/share.type";
|
||||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
import TableSortIcon, { TableSort } from "../core/SortIcon";
|
||||||
|
import showFilePreviewModal from "./modals/showFilePreviewModal";
|
||||||
|
|
||||||
const FileList = ({
|
const FileList = ({
|
||||||
files,
|
files,
|
||||||
|
setShare,
|
||||||
share,
|
share,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: {
|
}: {
|
||||||
files?: FileMetaData[];
|
files?: FileMetaData[];
|
||||||
|
setShare: Dispatch<SetStateAction<Share | undefined>>;
|
||||||
share: Share;
|
share: Share;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -32,6 +35,32 @@ const FileList = ({
|
|||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
|
const [sort, setSort] = useState<TableSort>({
|
||||||
|
property: undefined,
|
||||||
|
direction: "desc",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortFiles = () => {
|
||||||
|
if (files && sort.property) {
|
||||||
|
const sortedFiles = files.sort((a: any, b: any) => {
|
||||||
|
if (sort.direction === "asc") {
|
||||||
|
return b[sort.property!].localeCompare(a[sort.property!], undefined, {
|
||||||
|
numeric: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return a[sort.property!].localeCompare(b[sort.property!], undefined, {
|
||||||
|
numeric: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setShare({
|
||||||
|
...share,
|
||||||
|
files: sortedFiles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const copyFileLink = (file: FileMetaData) => {
|
const copyFileLink = (file: FileMetaData) => {
|
||||||
const link = `${config.get("general.appUrl")}/api/shares/${
|
const link = `${config.get("general.appUrl")}/api/shares/${
|
||||||
share.id
|
share.id
|
||||||
@@ -52,55 +81,70 @@ const FileList = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(sortFiles, [sort]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table>
|
<Box sx={{ display: "block", overflowX: "auto" }}>
|
||||||
<thead>
|
<Table>
|
||||||
<tr>
|
<thead>
|
||||||
<th>Name</th>
|
<tr>
|
||||||
<th>Size</th>
|
<th>
|
||||||
<th></th>
|
<Group spacing="xs">
|
||||||
</tr>
|
Name
|
||||||
</thead>
|
<TableSortIcon sort={sort} setSort={setSort} property="name" />
|
||||||
<tbody>
|
</Group>
|
||||||
{isLoading
|
</th>
|
||||||
? skeletonRows
|
<th>
|
||||||
: files!.map((file) => (
|
<Group spacing="xs">
|
||||||
<tr key={file.name}>
|
Size
|
||||||
<td>{file.name}</td>
|
<TableSortIcon sort={sort} setSort={setSort} property="size" />
|
||||||
<td>{byteToHumanSizeString(parseInt(file.size))}</td>
|
</Group>
|
||||||
<td>
|
</th>
|
||||||
<Group position="right">
|
<th></th>
|
||||||
{shareService.doesFileSupportPreview(file.name) && (
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading
|
||||||
|
? skeletonRows
|
||||||
|
: files!.map((file) => (
|
||||||
|
<tr key={file.name}>
|
||||||
|
<td>{file.name}</td>
|
||||||
|
<td>{byteToHumanSizeString(parseInt(file.size))}</td>
|
||||||
|
<td>
|
||||||
|
<Group position="right">
|
||||||
|
{shareService.doesFileSupportPreview(file.name) && (
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() =>
|
||||||
|
showFilePreviewModal(share.id, file, modals)
|
||||||
|
}
|
||||||
|
size={25}
|
||||||
|
>
|
||||||
|
<TbEye />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
{!share.hasPassword && (
|
||||||
|
<ActionIcon
|
||||||
|
size={25}
|
||||||
|
onClick={() => copyFileLink(file)}
|
||||||
|
>
|
||||||
|
<TbLink />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
component={Link}
|
|
||||||
href={`/share/${share.id}/preview/${
|
|
||||||
file.id
|
|
||||||
}?type=${mime.contentType(file.name)}`}
|
|
||||||
target="_blank"
|
|
||||||
size={25}
|
size={25}
|
||||||
|
onClick={async () => {
|
||||||
|
await shareService.downloadFile(share.id, file.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TbEye />
|
<TbDownload />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
</Group>
|
||||||
{!share.hasPassword && (
|
</td>
|
||||||
<ActionIcon size={25} onClick={() => copyFileLink(file)}>
|
</tr>
|
||||||
<TbLink />
|
))}
|
||||||
</ActionIcon>
|
</tbody>
|
||||||
)}
|
</Table>
|
||||||
<ActionIcon
|
</Box>
|
||||||
size={25}
|
|
||||||
onClick={async () => {
|
|
||||||
await shareService.downloadFile(share.id, file.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TbDownload />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
158
frontend/src/components/share/FilePreview.tsx
Normal file
158
frontend/src/components/share/FilePreview.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { Button, Center, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
import api from "../../services/api.service";
|
||||||
|
|
||||||
|
const FilePreviewContext = React.createContext<{
|
||||||
|
shareId: string;
|
||||||
|
fileId: string;
|
||||||
|
mimeType: string;
|
||||||
|
setIsNotSupported: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}>({
|
||||||
|
shareId: "",
|
||||||
|
fileId: "",
|
||||||
|
mimeType: "",
|
||||||
|
setIsNotSupported: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const FilePreview = ({
|
||||||
|
shareId,
|
||||||
|
fileId,
|
||||||
|
mimeType,
|
||||||
|
}: {
|
||||||
|
shareId: string;
|
||||||
|
fileId: string;
|
||||||
|
mimeType: string;
|
||||||
|
}) => {
|
||||||
|
const [isNotSupported, setIsNotSupported] = useState(false);
|
||||||
|
if (isNotSupported) return <UnSupportedFile />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<FilePreviewContext.Provider
|
||||||
|
value={{ shareId, fileId, mimeType, setIsNotSupported }}
|
||||||
|
>
|
||||||
|
<FileDecider />
|
||||||
|
</FilePreviewContext.Provider>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
component={Link}
|
||||||
|
onClick={() => modals.closeAll()}
|
||||||
|
target="_blank"
|
||||||
|
href={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||||
|
>
|
||||||
|
View original file
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileDecider = () => {
|
||||||
|
const { mimeType, setIsNotSupported } = React.useContext(FilePreviewContext);
|
||||||
|
|
||||||
|
if (mimeType == "application/pdf") {
|
||||||
|
return <PdfPreview />;
|
||||||
|
} else if (mimeType.startsWith("video/")) {
|
||||||
|
return <VideoPreview />;
|
||||||
|
} else if (mimeType.startsWith("image/")) {
|
||||||
|
return <ImagePreview />;
|
||||||
|
} else if (mimeType.startsWith("audio/")) {
|
||||||
|
return <AudioPreview />;
|
||||||
|
} else if (mimeType.startsWith("text/")) {
|
||||||
|
return <TextPreview />;
|
||||||
|
} else {
|
||||||
|
setIsNotSupported(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const AudioPreview = () => {
|
||||||
|
const { shareId, fileId, setIsNotSupported } =
|
||||||
|
React.useContext(FilePreviewContext);
|
||||||
|
return (
|
||||||
|
<Center style={{ minHeight: 200 }}>
|
||||||
|
<Stack align="center" spacing={10} style={{ width: "100%" }}>
|
||||||
|
<audio controls style={{ width: "100%" }}>
|
||||||
|
<source
|
||||||
|
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||||
|
onError={() => setIsNotSupported(true)}
|
||||||
|
/>
|
||||||
|
</audio>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VideoPreview = () => {
|
||||||
|
const { shareId, fileId, setIsNotSupported } =
|
||||||
|
React.useContext(FilePreviewContext);
|
||||||
|
return (
|
||||||
|
<video width="100%" controls>
|
||||||
|
<source
|
||||||
|
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||||
|
onError={() => setIsNotSupported(true)}
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImagePreview = () => {
|
||||||
|
const { shareId, fileId, setIsNotSupported } =
|
||||||
|
React.useContext(FilePreviewContext);
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||||
|
alt={`${fileId}_preview`}
|
||||||
|
width="100%"
|
||||||
|
onError={() => setIsNotSupported(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextPreview = () => {
|
||||||
|
const { shareId, fileId } = React.useContext(FilePreviewContext);
|
||||||
|
const [text, setText] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.get(`/shares/${shareId}/files/${fileId}?download=false`).then((res) => {
|
||||||
|
console.log(res.data);
|
||||||
|
setText(res.data);
|
||||||
|
});
|
||||||
|
}, [shareId, fileId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center style={{ minHeight: 200 }}>
|
||||||
|
<Stack align="center" spacing={10} style={{ width: "100%" }}>
|
||||||
|
<Text sx={{ whiteSpace: "pre-wrap" }} size="sm">
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PdfPreview = () => {
|
||||||
|
const { shareId, fileId } = React.useContext(FilePreviewContext);
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.location.href = `/api/shares/${shareId}/files/${fileId}?download=false`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnSupportedFile = () => {
|
||||||
|
return (
|
||||||
|
<Center style={{ minHeight: 200 }}>
|
||||||
|
<Stack align="center" spacing={10}>
|
||||||
|
<Title order={3}>Preview not supported</Title>
|
||||||
|
<Text>
|
||||||
|
A preview for thise file type is unsupported. Please download the file
|
||||||
|
to view it.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilePreview;
|
||||||
@@ -34,8 +34,10 @@ const FileSizeInput = ({
|
|||||||
label={label}
|
label={label}
|
||||||
value={size}
|
value={size}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSize(value!);
|
if (value) {
|
||||||
onChange(unitAndSizeToByte(unit, value!));
|
setSize(value);
|
||||||
|
onChange(unitAndSizeToByte(unit, value));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Button, Stack, TextInput, Title } from "@mantine/core";
|
import { ActionIcon, Button, Stack, TextInput } from "@mantine/core";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
@@ -14,11 +14,7 @@ const showCompletedReverseShareModal = (
|
|||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
title: (
|
title: "Reverse share link",
|
||||||
<Stack align="stretch" spacing={0}>
|
|
||||||
<Title order={4}>Reverse share link</Title>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
children: <Body link={link} getReverseShares={getReverseShares} />,
|
children: <Body link={link} getReverseShares={getReverseShares} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
@@ -25,7 +24,7 @@ const showCreateReverseShareModal = (
|
|||||||
getReverseShares: () => void
|
getReverseShares: () => void
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: <Title order={4}>Create reverse share</Title>,
|
title: "Create reverse share",
|
||||||
children: (
|
children: (
|
||||||
<Body
|
<Body
|
||||||
showSendEmailNotificationOption={showSendEmailNotificationOption}
|
showSendEmailNotificationOption={showSendEmailNotificationOption}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import mime from "mime-types";
|
||||||
|
import { FileMetaData } from "../../../types/File.type";
|
||||||
|
import FilePreview from "../FilePreview";
|
||||||
|
|
||||||
|
const showFilePreviewModal = (
|
||||||
|
shareId: string,
|
||||||
|
file: FileMetaData,
|
||||||
|
modals: ModalsContextProps
|
||||||
|
) => {
|
||||||
|
const mimeType = (mime.contentType(file.name) || "").split(";")[0];
|
||||||
|
return modals.openModal({
|
||||||
|
size: "xl",
|
||||||
|
title: file.name,
|
||||||
|
children: (
|
||||||
|
<FilePreview shareId={shareId} fileId={file.id} mimeType={mimeType} />
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showFilePreviewModal;
|
||||||
@@ -1,57 +1,52 @@
|
|||||||
import { Button, PasswordInput, Stack, Text, Title } from "@mantine/core";
|
import { Button, PasswordInput, Stack, Text } from "@mantine/core";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
const showEnterPasswordModal = (
|
const showEnterPasswordModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
submitCallback: any
|
submitCallback: (password: string) => Promise<void>
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
title: (
|
title: "Password required",
|
||||||
<>
|
|
||||||
<Title order={4}>Password required</Title>
|
|
||||||
<Text size="sm">
|
|
||||||
This access this share please enter the password for the share.
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
children: <Body submitCallback={submitCallback} />,
|
children: <Body submitCallback={submitCallback} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const Body = ({ submitCallback }: { submitCallback: any }) => {
|
const Body = ({
|
||||||
|
submitCallback,
|
||||||
|
}: {
|
||||||
|
submitCallback: (password: string) => Promise<void>;
|
||||||
|
}) => {
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordWrong, setPasswordWrong] = useState(false);
|
const [passwordWrong, setPasswordWrong] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack align="stretch">
|
||||||
<Stack align="stretch">
|
<Text size="sm">
|
||||||
<PasswordInput
|
This access this share please enter the password for the share.
|
||||||
variant="filled"
|
</Text>
|
||||||
placeholder="Password"
|
|
||||||
error={passwordWrong && "Wrong password"}
|
<form
|
||||||
onFocus={() => setPasswordWrong(false)}
|
onSubmit={(e) => {
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
e.preventDefault();
|
||||||
value={password}
|
submitCallback(password);
|
||||||
/>
|
}}
|
||||||
<Button
|
>
|
||||||
onClick={() =>
|
<Stack>
|
||||||
submitCallback(password)
|
<PasswordInput
|
||||||
.then((res: any) => res)
|
variant="filled"
|
||||||
.catch((e: any) => {
|
placeholder="Password"
|
||||||
const error = e.response.data.message;
|
error={passwordWrong && "Wrong password"}
|
||||||
if (error == "Wrong password") {
|
onFocus={() => setPasswordWrong(false)}
|
||||||
setPasswordWrong(true);
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
}
|
value={password}
|
||||||
})
|
/>
|
||||||
}
|
<Button type="submit">Submit</Button>
|
||||||
>
|
</Stack>
|
||||||
Submit
|
</form>
|
||||||
</Button>
|
</Stack>
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Stack, Text, Title } from "@mantine/core";
|
import { Button, Stack, Text } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -12,7 +12,7 @@ const showErrorModal = (
|
|||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
title: <Title order={4}>{title}</Title>,
|
title: title,
|
||||||
|
|
||||||
children: <Body text={text} />,
|
children: <Body text={text} />,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { ActionIcon, Button, Stack, Text, TextInput } from "@mantine/core";
|
||||||
ActionIcon,
|
|
||||||
Button,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
@@ -24,11 +17,7 @@ const showCompletedUploadModal = (
|
|||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
title: (
|
title: "Share ready",
|
||||||
<Stack align="stretch" spacing={0}>
|
|
||||||
<Title order={4}>Share ready</Title>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
children: <Body share={share} appUrl={appUrl} />,
|
children: <Body share={share} appUrl={appUrl} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const showCreateUploadModal = (
|
|||||||
uploadCallback: (createShare: CreateShare) => void
|
uploadCallback: (createShare: CreateShare) => void
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: <Title order={4}>Share</Title>,
|
title: "Share",
|
||||||
children: (
|
children: (
|
||||||
<CreateUploadModalBody
|
<CreateUploadModalBody
|
||||||
options={options}
|
options={options}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
createStyles,
|
|
||||||
Title,
|
|
||||||
Text,
|
|
||||||
Button,
|
Button,
|
||||||
Container,
|
Container,
|
||||||
|
createStyles,
|
||||||
Group,
|
Group,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import Meta from "../components/Meta";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Meta from "../components/Meta";
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -21,7 +20,7 @@ const useStyles = createStyles((theme) => ({
|
|||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontSize: 220,
|
fontSize: 220,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
marginBottom: theme.spacing.xl * 1.5,
|
marginBottom: `calc(${theme.spacing.xl} * 100)`,
|
||||||
color: theme.colors.gray[2],
|
color: theme.colors.gray[2],
|
||||||
|
|
||||||
[theme.fn.smallerThan("sm")]: {
|
[theme.fn.smallerThan("sm")]: {
|
||||||
@@ -32,7 +31,7 @@ const useStyles = createStyles((theme) => ({
|
|||||||
description: {
|
description: {
|
||||||
maxWidth: 500,
|
maxWidth: 500,
|
||||||
margin: "auto",
|
margin: "auto",
|
||||||
marginBottom: theme.spacing.xl * 1.5,
|
marginBottom: `calc(${theme.spacing.xl} * 100)`,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useColorScheme } from "@mantine/hooks";
|
import { useColorScheme } from "@mantine/hooks";
|
||||||
import { ModalsProvider } from "@mantine/modals";
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
import { NotificationsProvider } from "@mantine/notifications";
|
import { Notifications } from "@mantine/notifications";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getCookie, setCookie } from "cookies-next";
|
import { getCookie, setCookie } from "cookies-next";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
@@ -76,40 +76,39 @@ function App({ Component, pageProps }: AppProps) {
|
|||||||
toggleColorScheme={toggleColorScheme}
|
toggleColorScheme={toggleColorScheme}
|
||||||
>
|
>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<NotificationsProvider>
|
<Notifications />
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
<ConfigContext.Provider
|
<ConfigContext.Provider
|
||||||
|
value={{
|
||||||
|
configVariables,
|
||||||
|
refresh: async () => {
|
||||||
|
setConfigVariables(await configService.list());
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserContext.Provider
|
||||||
value={{
|
value={{
|
||||||
configVariables,
|
user,
|
||||||
refresh: async () => {
|
refreshUser: async () => {
|
||||||
setConfigVariables(await configService.list());
|
const user = await userService.getCurrentUser();
|
||||||
|
setUser(user);
|
||||||
|
return user;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UserContext.Provider
|
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||||
value={{
|
<Component {...pageProps} />
|
||||||
user,
|
) : (
|
||||||
refreshUser: async () => {
|
<>
|
||||||
const user = await userService.getCurrentUser();
|
<Header />
|
||||||
setUser(user);
|
<Container>
|
||||||
return user;
|
<Component {...pageProps} />
|
||||||
},
|
</Container>
|
||||||
}}
|
</>
|
||||||
>
|
)}
|
||||||
{excludeDefaultLayoutRoutes.includes(route) ? (
|
</UserContext.Provider>
|
||||||
<Component {...pageProps} />
|
</ConfigContext.Provider>
|
||||||
) : (
|
</ModalsProvider>
|
||||||
<>
|
|
||||||
<Header />
|
|
||||||
<Container>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</UserContext.Provider>
|
|
||||||
</ConfigContext.Provider>
|
|
||||||
</ModalsProvider>
|
|
||||||
</NotificationsProvider>
|
|
||||||
</ColorSchemeProvider>
|
</ColorSchemeProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,14 +12,8 @@ export default class _Document extends Document {
|
|||||||
<Head>
|
<Head>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
||||||
<link
|
<link rel="apple-touch-icon" href="/img/icons/icon-128x128.png" />
|
||||||
rel="apple-touch-icon"
|
|
||||||
href="/img/icons/icon-white-128x128.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meta property="og:image" content="/img/opengraph.png" />
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:image" content="/img/opengraph.png" />
|
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<meta name="theme-color" content="#46509e" />
|
<meta name="theme-color" content="#46509e" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useEffect, useState } from "react";
|
|||||||
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
||||||
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
|
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
|
||||||
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
|
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
|
||||||
|
import LogoConfigInput from "../../../components/admin/configuration/LogoConfigInput";
|
||||||
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
|
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
|
||||||
import CenterLoader from "../../../components/core/CenterLoader";
|
import CenterLoader from "../../../components/core/CenterLoader";
|
||||||
import Meta from "../../../components/Meta";
|
import Meta from "../../../components/Meta";
|
||||||
@@ -36,30 +37,50 @@ export default function AppShellDemo() {
|
|||||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
const categoryId = router.query.category as string;
|
const categoryId = (router.query.category as string | undefined) ?? "general";
|
||||||
|
|
||||||
const [configVariables, setConfigVariables] = useState<AdminConfig[]>();
|
const [configVariables, setConfigVariables] = useState<AdminConfig[]>();
|
||||||
const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
|
const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
|
||||||
UpdateConfig[]
|
UpdateConfig[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
const [logo, setLogo] = useState<File | null>(null);
|
||||||
|
|
||||||
const saveConfigVariables = async () => {
|
const saveConfigVariables = async () => {
|
||||||
await configService
|
if (logo) {
|
||||||
.updateMany(updatedConfigVariables)
|
configService
|
||||||
.then(() => {
|
.changeLogo(logo)
|
||||||
setUpdatedConfigVariables([]);
|
.then(() => {
|
||||||
toast.success("Configurations updated successfully");
|
setLogo(null);
|
||||||
})
|
toast.success(
|
||||||
.catch(toast.axiosError);
|
"Logo updated successfully. It may take a few minutes to update on the website."
|
||||||
config.refresh();
|
);
|
||||||
|
})
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedConfigVariables.length > 0) {
|
||||||
|
await configService
|
||||||
|
.updateMany(updatedConfigVariables)
|
||||||
|
.then(() => {
|
||||||
|
setUpdatedConfigVariables([]);
|
||||||
|
toast.success("Configurations updated successfully");
|
||||||
|
})
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
void config.refresh();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConfigVariable = (configVariable: UpdateConfig) => {
|
const updateConfigVariable = (configVariable: UpdateConfig) => {
|
||||||
const index = updatedConfigVariables.findIndex(
|
const index = updatedConfigVariables.findIndex(
|
||||||
(item) => item.key === configVariable.key
|
(item) => item.key === configVariable.key
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
updatedConfigVariables[index] = configVariable;
|
updatedConfigVariables[index] = {
|
||||||
|
...updatedConfigVariables[index],
|
||||||
|
...configVariable,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
|
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
|
||||||
}
|
}
|
||||||
@@ -115,9 +136,24 @@ export default function AppShellDemo() {
|
|||||||
<Title order={6}>
|
<Title order={6}>
|
||||||
{configVariableToFriendlyName(configVariable.name)}
|
{configVariableToFriendlyName(configVariable.name)}
|
||||||
</Title>
|
</Title>
|
||||||
<Text color="dimmed" size="sm" mb="xs">
|
{configVariable.description.split("\n").length == 1 ? (
|
||||||
{configVariable.description}
|
<Text color="dimmed" size="sm" mb="xs">
|
||||||
</Text>
|
{configVariable.description}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
configVariable.description.split("\n").map((line) => (
|
||||||
|
<Text
|
||||||
|
key={line}
|
||||||
|
color="dimmed"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
marginBottom: line === "" ? "1rem" : "0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</Text>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack></Stack>
|
<Stack></Stack>
|
||||||
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||||
@@ -129,6 +165,9 @@ export default function AppShellDemo() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
|
{categoryId == "general" && (
|
||||||
|
<LogoConfigInput logo={logo} setLogo={setLogo} />
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Group mt="lg" position="right">
|
<Group mt="lg" position="right">
|
||||||
{categoryId == "smtp" && (
|
{categoryId == "smtp" && (
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export function getServerSideProps() {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: "/admin/config/general",
|
|
||||||
},
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Config = () => {
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Config;
|
|
||||||
@@ -41,7 +41,7 @@ const Admin = () => {
|
|||||||
{
|
{
|
||||||
title: "Configuration",
|
title: "Configuration",
|
||||||
icon: TbSettings,
|
icon: TbSettings,
|
||||||
route: "/admin/config",
|
route: "/admin/config/general",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const Intro = () => {
|
|||||||
<Text>Enough talked, have fun with Pingvin Share!</Text>
|
<Text>Enough talked, have fun with Pingvin Share!</Text>
|
||||||
<Text mt="lg">How to you want to continue?</Text>
|
<Text mt="lg">How to you want to continue?</Text>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Button href="/admin/config" component={Link}>
|
<Button href="/admin/config/general" component={Link}>
|
||||||
Customize configuration
|
Customize configuration
|
||||||
</Button>
|
</Button>
|
||||||
<Button href="/" component={Link} variant="light">
|
<Button href="/" component={Link} variant="light">
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ const useStyles = createStyles((theme) => ({
|
|||||||
inner: {
|
inner: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingTop: theme.spacing.xl * 4,
|
paddingTop: `calc(${theme.spacing.md} * 4)`,
|
||||||
paddingBottom: theme.spacing.xl * 4,
|
paddingBottom: `calc(${theme.spacing.md} * 4)`,
|
||||||
},
|
},
|
||||||
|
|
||||||
content: {
|
content: {
|
||||||
maxWidth: 480,
|
maxWidth: 480,
|
||||||
marginRight: theme.spacing.xl * 3,
|
marginRight: `calc(${theme.spacing.md} * 3)`,
|
||||||
|
|
||||||
[theme.fn.smallerThan("md")]: {
|
[theme.fn.smallerThan("md")]: {
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import showEnterPasswordModal from "../../../components/share/showEnterPasswordM
|
|||||||
import showErrorModal from "../../../components/share/showErrorModal";
|
import showErrorModal from "../../../components/share/showErrorModal";
|
||||||
import shareService from "../../../services/share.service";
|
import shareService from "../../../services/share.service";
|
||||||
import { Share as ShareType } from "../../../types/share.type";
|
import { Share as ShareType } from "../../../types/share.type";
|
||||||
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
return {
|
return {
|
||||||
@@ -28,12 +29,15 @@ const Share = ({ shareId }: { shareId: string }) => {
|
|||||||
getFiles();
|
getFiles();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (e.response.data.error == "share_max_views_exceeded") {
|
const { error } = e.response.data;
|
||||||
|
if (error == "share_max_views_exceeded") {
|
||||||
showErrorModal(
|
showErrorModal(
|
||||||
modals,
|
modals,
|
||||||
"Visitor limit exceeded",
|
"Visitor limit exceeded",
|
||||||
"The visitor limit from this share has been exceeded."
|
"The visitor limit from this share has been exceeded."
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
toast.axiosError(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -85,7 +89,12 @@ const Share = ({ shareId }: { shareId: string }) => {
|
|||||||
{share?.files.length > 1 && <DownloadAllButton shareId={shareId} />}
|
{share?.files.length > 1 && <DownloadAllButton shareId={shareId} />}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<FileList files={share?.files} share={share!} isLoading={!share} />
|
<FileList
|
||||||
|
files={share?.files}
|
||||||
|
setShare={setShare}
|
||||||
|
share={share!}
|
||||||
|
isLoading={!share}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
import { Center, Stack, Text, Title } from "@mantine/core";
|
|
||||||
import { GetServerSidePropsContext } from "next";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
|
||||||
const { shareId, fileId } = context.params!;
|
|
||||||
|
|
||||||
const mimeType = context.query.type as string;
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: { shareId, fileId, mimeType },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const UnSupportedFile = () => {
|
|
||||||
return (
|
|
||||||
<Center style={{ height: "70vh" }}>
|
|
||||||
<Stack align="center" spacing={10}>
|
|
||||||
<Title order={3}>Preview not supported</Title>
|
|
||||||
<Text>
|
|
||||||
A preview for thise file type is unsupported. Please download the file
|
|
||||||
to view it.
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const FilePreview = ({
|
|
||||||
shareId,
|
|
||||||
fileId,
|
|
||||||
mimeType,
|
|
||||||
}: {
|
|
||||||
shareId: string;
|
|
||||||
fileId: string;
|
|
||||||
mimeType: string;
|
|
||||||
}) => {
|
|
||||||
const [isNotSupported, setIsNotSupported] = useState(false);
|
|
||||||
|
|
||||||
if (isNotSupported) return <UnSupportedFile />;
|
|
||||||
|
|
||||||
if (mimeType == "application/pdf") {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.location.href = `/api/shares/${shareId}/files/${fileId}?download=false`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} else if (mimeType.startsWith("video/")) {
|
|
||||||
return (
|
|
||||||
<video
|
|
||||||
width="100%"
|
|
||||||
controls
|
|
||||||
onError={() => {
|
|
||||||
setIsNotSupported(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<source src={`/api/shares/${shareId}/files/${fileId}?download=false`} />
|
|
||||||
</video>
|
|
||||||
);
|
|
||||||
} else if (mimeType.startsWith("image/")) {
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
onError={() => {
|
|
||||||
setIsNotSupported(true);
|
|
||||||
}}
|
|
||||||
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
|
||||||
alt={`${fileId}_preview`}
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (mimeType.startsWith("audio/")) {
|
|
||||||
return (
|
|
||||||
<Center style={{ height: "70vh" }}>
|
|
||||||
<Stack align="center" spacing={10} style={{ width: "100%" }}>
|
|
||||||
<audio
|
|
||||||
controls
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
onError={() => {
|
|
||||||
setIsNotSupported(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<source
|
|
||||||
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
|
||||||
/>
|
|
||||||
</audio>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <UnSupportedFile />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FilePreview;
|
|
||||||
@@ -59,7 +59,10 @@ const Upload = ({
|
|||||||
|
|
||||||
setFileProgress(1);
|
setFileProgress(1);
|
||||||
|
|
||||||
const chunks = Math.ceil(file.size / chunkSize);
|
let chunks = Math.ceil(file.size / chunkSize);
|
||||||
|
|
||||||
|
// If the file is 0 bytes, we still need to upload 1 chunk
|
||||||
|
if (chunks == 0) chunks++;
|
||||||
|
|
||||||
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) {
|
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) {
|
||||||
const from = chunkIndex * chunkSize;
|
const from = chunkIndex * chunkSize;
|
||||||
@@ -78,7 +81,7 @@ const Upload = ({
|
|||||||
name: file.name,
|
name: file.name,
|
||||||
},
|
},
|
||||||
chunkIndex,
|
chunkIndex,
|
||||||
Math.ceil(file.size / chunkSize)
|
chunks
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
fileId = response.id;
|
fileId = response.id;
|
||||||
@@ -125,7 +128,7 @@ const Upload = ({
|
|||||||
toast.error(
|
toast.error(
|
||||||
`${fileErrorCount} file(s) failed to upload. Trying again.`,
|
`${fileErrorCount} file(s) failed to upload. Trying again.`,
|
||||||
{
|
{
|
||||||
disallowClose: true,
|
withCloseButton: false,
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -173,7 +176,7 @@ const Upload = ({
|
|||||||
"share.allowUnauthenticatedShares"
|
"share.allowUnauthenticatedShares"
|
||||||
),
|
),
|
||||||
enableEmailRecepients: config.get(
|
enableEmailRecepients: config.get(
|
||||||
"ENABLE_SHARE_EMAIL_RECIPIENTS"
|
"email.enableShareEmailRecipients"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
uploadFiles
|
uploadFiles
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ const get = (key: string, configVariables: Config[]): any => {
|
|||||||
|
|
||||||
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
||||||
|
|
||||||
if (configVariable.type == "number") return parseInt(configVariable.value);
|
const value = configVariable.value ?? configVariable.defaultValue;
|
||||||
if (configVariable.type == "boolean") return configVariable.value == "true";
|
|
||||||
|
if (configVariable.type == "number") return parseInt(value);
|
||||||
|
if (configVariable.type == "boolean") return value == "true";
|
||||||
if (configVariable.type == "string" || configVariable.type == "text")
|
if (configVariable.type == "string" || configVariable.type == "text")
|
||||||
return configVariable.value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishSetup = async (): Promise<AdminConfig[]> => {
|
const finishSetup = async (): Promise<AdminConfig[]> => {
|
||||||
@@ -46,6 +48,12 @@ const isNewReleaseAvailable = async () => {
|
|||||||
return response.tag_name.replace("v", "") != process.env.VERSION;
|
return response.tag_name.replace("v", "") != process.env.VERSION;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeLogo = async (file: File) => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", file);
|
||||||
|
|
||||||
|
await api.post("/configs/admin/logo", form);
|
||||||
|
};
|
||||||
export default {
|
export default {
|
||||||
list,
|
list,
|
||||||
getByCategory,
|
getByCategory,
|
||||||
@@ -54,4 +62,5 @@ export default {
|
|||||||
finishSetup,
|
finishSetup,
|
||||||
sendTestEmail,
|
sendTestEmail,
|
||||||
isNewReleaseAvailable,
|
isNewReleaseAvailable,
|
||||||
|
changeLogo,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const isShareIdAvailable = async (id: string): Promise<boolean> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const doesFileSupportPreview = (fileName: string) => {
|
const doesFileSupportPreview = (fileName: string) => {
|
||||||
const mimeType = mime.contentType(fileName);
|
const mimeType = (mime.contentType(fileName) || "").split(";")[0];
|
||||||
|
|
||||||
if (!mimeType) return false;
|
if (!mimeType) return false;
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ const doesFileSupportPreview = (fileName: string) => {
|
|||||||
mimeType.startsWith("video/"),
|
mimeType.startsWith("video/"),
|
||||||
mimeType.startsWith("image/"),
|
mimeType.startsWith("image/"),
|
||||||
mimeType.startsWith("audio/"),
|
mimeType.startsWith("audio/"),
|
||||||
|
mimeType.startsWith("text/"),
|
||||||
mimeType == "application/pdf",
|
mimeType == "application/pdf",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,14 @@ export default <MantineThemeOverride>{
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
primaryColor: "victoria",
|
primaryColor: "victoria",
|
||||||
|
components: {
|
||||||
|
Modal: {
|
||||||
|
styles: (theme) => ({
|
||||||
|
title: {
|
||||||
|
fontSize: theme.fontSizes.lg,
|
||||||
|
fontWeight: 700,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
type Config = {
|
type Config = {
|
||||||
key: string;
|
key: string;
|
||||||
|
defaultValue: string;
|
||||||
value: string;
|
value: string;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share",
|
"name": "pingvin-share",
|
||||||
"version": "0.11.0",
|
"version": "0.14.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "cd frontend && npm run format && cd ../backend && npm run format",
|
"format": "cd frontend && npm run format && cd ../backend && npm run format",
|
||||||
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",
|
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",
|
||||||
|
|||||||
Reference in New Issue
Block a user