Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
155c743197 | ||
|
|
8b77e81d4c | ||
|
|
22d81b2220 | ||
|
|
0317f3a508 | ||
|
|
fddad3ef70 | ||
|
|
f9840505b8 | ||
|
|
759c55f625 |
20
CHANGELOG.md
@@ -1,3 +1,23 @@
|
|||||||
|
### [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* custom branding ([#112](https://github.com/stonith404/pingvin-share/issues/112)) ([fddad3e](https://github.com/stonith404/pingvin-share/commit/fddad3ef708c27052a8bf46f3076286d102f6d7e))
|
||||||
|
* invite new user with email ([f984050](https://github.com/stonith404/pingvin-share/commit/f9840505b82fcb04364a79576f186b76cc75f5c0))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* frontend error when user deleted ([0317f3a](https://github.com/stonith404/pingvin-share/commit/0317f3a508dc88ffe2c33413704f7df03a2372ea))
|
||||||
|
|
||||||
### [0.10.2](https://github.com/stonith404/pingvin-share/compare/v0.10.1...v0.10.2) (2023-02-13)
|
### [0.10.2](https://github.com/stonith404/pingvin-share/compare/v0.10.1...v0.10.2) (2023-02-13)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ RUN apt-get update && apt-get install -y openssl
|
|||||||
|
|
||||||
WORKDIR /opt/app/frontend
|
WORKDIR /opt/app/frontend
|
||||||
COPY --from=frontend-builder /opt/app/public ./public
|
COPY --from=frontend-builder /opt/app/public ./public
|
||||||
# Automatically leverage output traces to reduce image size
|
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
|
||||||
COPY --from=frontend-builder /opt/app/.next/standalone ./
|
COPY --from=frontend-builder /opt/app/.next/standalone ./
|
||||||
COPY --from=frontend-builder /opt/app/.next/static ./.next/static
|
COPY --from=frontend-builder /opt/app/.next/static ./.next/static
|
||||||
|
COPY --from=frontend-builder /opt/app/public/img /tmp/img
|
||||||
|
|
||||||
WORKDIR /opt/app/backend
|
WORKDIR /opt/app/backend
|
||||||
COPY --from=backend-builder /opt/app/node_modules ./node_modules
|
COPY --from=backend-builder /opt/app/node_modules ./node_modules
|
||||||
@@ -48,4 +47,4 @@ COPY --from=backend-builder /opt/app/package.json ./
|
|||||||
|
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD node frontend/server.js & cd backend && npm run prod
|
CMD cp -rn /tmp/img /opt/app/frontend/public && node frontend/server.js & cd backend && npm run prod
|
||||||
29
README.md
@@ -27,7 +27,7 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran
|
|||||||
1. Download the `docker-compose.yml` file
|
1. Download the `docker-compose.yml` file
|
||||||
2. Run `docker-compose up -d`
|
2. Run `docker-compose up -d`
|
||||||
|
|
||||||
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
The website is now listening on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
||||||
|
|
||||||
### Stand-alone Installation
|
### Stand-alone Installation
|
||||||
|
|
||||||
@@ -45,19 +45,19 @@ cd pingvin-share
|
|||||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||||
|
|
||||||
# Start the backend
|
# Start the backend
|
||||||
cd ../backend
|
cd backend
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||||
|
|
||||||
# Start the frontend
|
# Start the frontend
|
||||||
cd frontend
|
cd ../frontend
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||||
```
|
```
|
||||||
|
|
||||||
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
The website is now listening on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
||||||
|
|
||||||
### Integrations
|
### Integrations
|
||||||
|
|
||||||
@@ -88,7 +88,26 @@ docker compose up -d
|
|||||||
|
|
||||||
#### Stand-alone
|
#### Stand-alone
|
||||||
|
|
||||||
Repeat the steps from the [installation guide](#stand-alone-installation) except the `git clone` step.
|
1. Remove the running app
|
||||||
|
```
|
||||||
|
pm2 delete pingvin-share-backend pingvin-share-frontend
|
||||||
|
```
|
||||||
|
2. Repeat the steps from the [installation guide](#stand-alone-installation) except the `git clone` step.
|
||||||
|
|
||||||
|
### Custom branding
|
||||||
|
|
||||||
|
#### Name
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
4
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-backend",
|
"name": "pingvin-share-backend",
|
||||||
"version": "0.10.2",
|
"version": "0.11.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pingvin-share-backend",
|
"name": "pingvin-share-backend",
|
||||||
"version": "0.10.2",
|
"version": "0.11.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^9.2.1",
|
"@nestjs/common": "^9.2.1",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-backend",
|
"name": "pingvin-share-backend",
|
||||||
"version": "0.10.2",
|
"version": "0.11.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `Config` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- You are about to drop the column `key` on the `Config` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `name` to the `Config` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- 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 NOT NULL,
|
||||||
|
"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", "obscured", "order", "secret", "type", "updatedAt", "value") SELECT "category", "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value" FROM "Config";
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'internal', 'jwtSecret', "description", "locked", "obscured", 0, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'JWT_SECRET';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'general', 'appUrl', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'APP_URL';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'general', 'showHomePage', "description", "locked", "obscured", 2, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SHOW_HOME_PAGE';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'share', 'allowRegistration', "description", "locked", "obscured", 0, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'ALLOW_REGISTRATION';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'share', 'allowUnauthenticatedShares', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'ALLOW_UNAUTHENTICATED_SHARES';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'share', 'maxSize', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'MAX_SHARE_SIZE';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'email', 'enableShareEmailRecipients', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'ENABLE_SHARE_EMAIL_RECIPIENTS';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'email', 'shareRecipientsSubject', "description", "locked", "obscured", 2, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SHARE_RECEPIENTS_EMAIL_SUBJECT';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'email', 'shareRecipientsMessage', "description", "locked", "obscured", 3, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SHARE_RECEPIENTS_EMAIL_MESSAGE';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'email', 'reverseShareSubject', "description", "locked", "obscured", 4, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'REVERSE_SHARE_EMAIL_SUBJECT';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'email', 'reverseShareMessage', "description", "locked", "obscured", 5, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'REVERSE_SHARE_EMAIL_MESSAGE';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'email', 'resetPasswordSubject', "description", "locked", "obscured", 6, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'RESET_PASSWORD_EMAIL_SUBJECT';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'email', 'resetPasswordMessage', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'RESET_PASSWORD_EMAIL_MESSAGE';
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'smtp', 'enabled', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SMTP_ENABLED';
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'smtp', 'host', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SMTP_HOST';
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'smtp', 'port', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SMTP_PORT';
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'smtp', 'email', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SMTP_EMAIL';
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'smtp', 'username', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SMTP_USERNAME';
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO new_Config ("category", "name" , "description", "locked", "obscured", "order", "secret", "type", "updatedAt", "value")
|
||||||
|
SELECT 'smtp', 'password', "description", "locked", "obscured", 1, "secret", "type", "updatedAt", "value" FROM Config WHERE key = 'SMTP_PASSWORD';
|
||||||
|
|
||||||
|
|
||||||
|
DROP TABLE "Config";
|
||||||
|
ALTER TABLE "new_Config" RENAME TO "Config";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -131,13 +131,15 @@ model ShareSecurity {
|
|||||||
model Config {
|
model Config {
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
key String @id
|
name String
|
||||||
|
category String
|
||||||
type String
|
type String
|
||||||
value String
|
value String
|
||||||
description String
|
description String
|
||||||
category String
|
|
||||||
obscured Boolean @default(false)
|
obscured Boolean @default(false)
|
||||||
secret Boolean @default(true)
|
secret Boolean @default(true)
|
||||||
locked Boolean @default(false)
|
locked Boolean @default(false)
|
||||||
order Int
|
order Int
|
||||||
|
|
||||||
|
@@id([name, category])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,242 +1,244 @@
|
|||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
const configVariables: Prisma.ConfigCreateInput[] = [
|
const configVariables: ConfigVariables = {
|
||||||
{
|
internal: {
|
||||||
order: 0,
|
jwtSecret: {
|
||||||
key: "SETUP_STATUS",
|
description: "Long random string used to sign JWT tokens",
|
||||||
description: "Status of the setup wizard",
|
type: "string",
|
||||||
type: "string",
|
value: crypto.randomBytes(256).toString("base64"),
|
||||||
value: "STARTED", // STARTED, REGISTERED, FINISHED
|
locked: true,
|
||||||
category: "internal",
|
},
|
||||||
secret: false,
|
|
||||||
locked: true,
|
|
||||||
},
|
},
|
||||||
{
|
general: {
|
||||||
order: 0,
|
appName: {
|
||||||
key: "JWT_SECRET",
|
description: "Name of the application",
|
||||||
description: "Long random string used to sign JWT tokens",
|
type: "string",
|
||||||
type: "string",
|
value: "Pingvin Share",
|
||||||
value: crypto.randomBytes(256).toString("base64"),
|
secret: false,
|
||||||
category: "internal",
|
},
|
||||||
locked: true,
|
appUrl: {
|
||||||
},
|
description: "On which URL Pingvin Share is available",
|
||||||
{
|
type: "string",
|
||||||
order: 1,
|
value: "http://localhost:3000",
|
||||||
key: "APP_URL",
|
|
||||||
description: "On which URL Pingvin Share is available",
|
|
||||||
type: "string",
|
|
||||||
value: "http://localhost:3000",
|
|
||||||
category: "general",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 2,
|
|
||||||
key: "SHOW_HOME_PAGE",
|
|
||||||
description: "Whether to show the home page",
|
|
||||||
type: "boolean",
|
|
||||||
value: "true",
|
|
||||||
category: "general",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 3,
|
|
||||||
key: "ALLOW_REGISTRATION",
|
|
||||||
description: "Whether registration is allowed",
|
|
||||||
type: "boolean",
|
|
||||||
value: "true",
|
|
||||||
category: "share",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 4,
|
|
||||||
key: "ALLOW_UNAUTHENTICATED_SHARES",
|
|
||||||
description: "Whether unauthorized users can create shares",
|
|
||||||
type: "boolean",
|
|
||||||
value: "false",
|
|
||||||
category: "share",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 5,
|
|
||||||
|
|
||||||
key: "MAX_SHARE_SIZE",
|
secret: false,
|
||||||
description: "Maximum share size in bytes",
|
},
|
||||||
type: "number",
|
showHomePage: {
|
||||||
value: "1073741824",
|
description: "Whether to show the home page",
|
||||||
category: "share",
|
type: "boolean",
|
||||||
secret: false,
|
value: "true",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
share: {
|
||||||
|
allowRegistration: {
|
||||||
|
description: "Whether registration is allowed",
|
||||||
|
type: "boolean",
|
||||||
|
value: "true",
|
||||||
|
|
||||||
{
|
secret: false,
|
||||||
order: 6,
|
},
|
||||||
key: "ENABLE_SHARE_EMAIL_RECIPIENTS",
|
allowUnauthenticatedShares: {
|
||||||
description:
|
description: "Whether unauthorized users can create shares",
|
||||||
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
type: "boolean",
|
||||||
type: "boolean",
|
value: "false",
|
||||||
value: "false",
|
|
||||||
category: "email",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 7,
|
|
||||||
key: "SHARE_RECEPIENTS_EMAIL_SUBJECT",
|
|
||||||
description:
|
|
||||||
"Subject of the email which gets sent to the share recipients.",
|
|
||||||
type: "string",
|
|
||||||
value: "Files shared with you",
|
|
||||||
category: "email",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 8,
|
|
||||||
key: "SHARE_RECEPIENTS_EMAIL_MESSAGE",
|
|
||||||
description:
|
|
||||||
"Message which gets sent to the share recipients. {creator} and {shareUrl} will be replaced with the creator's name and the share URL.",
|
|
||||||
type: "text",
|
|
||||||
value:
|
|
||||||
"Hey!\n{creator} shared some files with you. View or download the files with this link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
|
||||||
category: "email",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 9,
|
|
||||||
key: "REVERSE_SHARE_EMAIL_SUBJECT",
|
|
||||||
description:
|
|
||||||
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
|
||||||
type: "string",
|
|
||||||
value: "Reverse share link used",
|
|
||||||
category: "email",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 10,
|
|
||||||
key: "REVERSE_SHARE_EMAIL_MESSAGE",
|
|
||||||
description:
|
|
||||||
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
|
||||||
type: "text",
|
|
||||||
value:
|
|
||||||
"Hey!\nA share was just created with your reverse share link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
|
||||||
category: "email",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 11,
|
|
||||||
key: "RESET_PASSWORD_EMAIL_SUBJECT",
|
|
||||||
description:
|
|
||||||
"Subject of the email which gets sent when a user requests a password reset.",
|
|
||||||
type: "string",
|
|
||||||
value: "Pingvin Share password reset",
|
|
||||||
category: "email",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 12,
|
|
||||||
key: "RESET_PASSWORD_EMAIL_MESSAGE",
|
|
||||||
description:
|
|
||||||
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
|
||||||
type: "text",
|
|
||||||
value:
|
|
||||||
"Hey!\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\nPingvin Share 🐧",
|
|
||||||
category: "email",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
secret: false,
|
||||||
order: 13,
|
},
|
||||||
key: "SMTP_ENABLED",
|
maxSize: {
|
||||||
description:
|
description: "Maximum share size in bytes",
|
||||||
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
type: "number",
|
||||||
type: "boolean",
|
value: "1073741824",
|
||||||
value: "false",
|
|
||||||
category: "smtp",
|
secret: false,
|
||||||
secret: false,
|
},
|
||||||
},
|
},
|
||||||
{
|
email: {
|
||||||
order: 14,
|
enableShareEmailRecipients: {
|
||||||
key: "SMTP_HOST",
|
description:
|
||||||
description: "Host of the SMTP server",
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
type: "string",
|
type: "boolean",
|
||||||
value: "",
|
value: "false",
|
||||||
category: "smtp",
|
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
shareRecipientsSubject: {
|
||||||
|
description:
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
type: "string",
|
||||||
|
value: "Files shared with you",
|
||||||
|
},
|
||||||
|
shareRecipientsMessage: {
|
||||||
|
description:
|
||||||
|
"Message which gets sent to the share recipients. {creator} and {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
type: "text",
|
||||||
|
value:
|
||||||
|
"Hey!\n{creator} shared some files with you. View or download the files with this link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
||||||
|
},
|
||||||
|
reverseShareSubject: {
|
||||||
|
description:
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
type: "string",
|
||||||
|
value: "Reverse share link used",
|
||||||
|
},
|
||||||
|
reverseShareMessage: {
|
||||||
|
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.",
|
||||||
|
type: "text",
|
||||||
|
value:
|
||||||
|
"Hey!\nA share was just created with your reverse share link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
||||||
|
},
|
||||||
|
resetPasswordSubject: {
|
||||||
|
description:
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
type: "string",
|
||||||
|
value: "Pingvin Share password reset",
|
||||||
|
},
|
||||||
|
resetPasswordMessage: {
|
||||||
|
description:
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
type: "text",
|
||||||
|
value:
|
||||||
|
"Hey!\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\nPingvin Share 🐧",
|
||||||
|
},
|
||||||
|
inviteSubject: {
|
||||||
|
description:
|
||||||
|
"Subject of the email which gets sent when an admin invites an user.",
|
||||||
|
type: "string",
|
||||||
|
value: "Pingvin Share invite",
|
||||||
|
},
|
||||||
|
inviteMessage: {
|
||||||
|
description:
|
||||||
|
"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",
|
||||||
|
value:
|
||||||
|
"Hey!\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\nYour password is: {password}\nPingvin Share 🐧",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
smtp: {
|
||||||
order: 15,
|
enabled: {
|
||||||
key: "SMTP_PORT",
|
description:
|
||||||
description: "Port of the 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: "number",
|
type: "boolean",
|
||||||
value: "0",
|
value: "false",
|
||||||
category: "smtp",
|
secret: false,
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
description: "Host of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
port: {
|
||||||
|
description: "Port of the SMTP server",
|
||||||
|
type: "number",
|
||||||
|
value: "0",
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
description: "Email address which the emails get sent from",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
description: "Username of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
description: "Password of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
obscured: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
};
|
||||||
order: 16,
|
|
||||||
key: "SMTP_EMAIL",
|
type ConfigVariables = {
|
||||||
description: "Email address which the emails get sent from",
|
[category: string]: {
|
||||||
type: "string",
|
[variable: string]: Omit<
|
||||||
value: "",
|
Prisma.ConfigCreateInput,
|
||||||
category: "smtp",
|
"name" | "category" | "order"
|
||||||
},
|
>;
|
||||||
{
|
};
|
||||||
order: 17,
|
};
|
||||||
key: "SMTP_USERNAME",
|
|
||||||
description: "Username of the SMTP server",
|
|
||||||
type: "string",
|
|
||||||
value: "",
|
|
||||||
category: "smtp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 18,
|
|
||||||
key: "SMTP_PASSWORD",
|
|
||||||
description: "Password of the SMTP server",
|
|
||||||
type: "string",
|
|
||||||
value: "",
|
|
||||||
obscured: true,
|
|
||||||
category: "smtp",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function seedConfigVariables() {
|
||||||
for (const variable of configVariables) {
|
for (const [category, configVariablesOfCategory] of Object.entries(
|
||||||
const existingConfigVariable = await prisma.config.findUnique({
|
configVariables
|
||||||
where: { key: variable.key },
|
)) {
|
||||||
});
|
let order = 0;
|
||||||
|
for (const [name, properties] of Object.entries(
|
||||||
// Create a new config variable if it doesn't exist
|
configVariablesOfCategory
|
||||||
if (!existingConfigVariable) {
|
)) {
|
||||||
await prisma.config.create({
|
const existingConfigVariable = await prisma.config.findUnique({
|
||||||
data: variable,
|
where: { name_category: { name, category } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create a new config variable if it doesn't exist
|
||||||
|
if (!existingConfigVariable) {
|
||||||
|
await prisma.config.create({
|
||||||
|
data: {
|
||||||
|
order,
|
||||||
|
name,
|
||||||
|
...properties,
|
||||||
|
category,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
order++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const configVariablesFromDatabase = await prisma.config.findMany();
|
async function migrateConfigVariables() {
|
||||||
|
const existingConfigVariables = await prisma.config.findMany();
|
||||||
|
|
||||||
// Delete the config variable if it doesn't exist anymore
|
for (const existingConfigVariable of existingConfigVariables) {
|
||||||
for (const configVariableFromDatabase of configVariablesFromDatabase) {
|
const configVariable =
|
||||||
const configVariable = configVariables.find(
|
configVariables[existingConfigVariable.category]?.[
|
||||||
(v) => v.key == configVariableFromDatabase.key
|
existingConfigVariable.name
|
||||||
);
|
];
|
||||||
if (!configVariable) {
|
if (!configVariable) {
|
||||||
await prisma.config.delete({
|
await prisma.config.delete({
|
||||||
where: { key: configVariableFromDatabase.key },
|
where: {
|
||||||
|
name_category: {
|
||||||
|
name: existingConfigVariable.name,
|
||||||
|
category: existingConfigVariable.category,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the config variable if the metadata changed
|
// Update the config variable if the metadata changed
|
||||||
} else if (
|
} else if (
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...configVariable,
|
...configVariable,
|
||||||
key: configVariableFromDatabase.key,
|
name: existingConfigVariable.name,
|
||||||
value: configVariableFromDatabase.value,
|
category: existingConfigVariable.category,
|
||||||
}) != JSON.stringify(configVariableFromDatabase)
|
value: existingConfigVariable.value,
|
||||||
|
}) != JSON.stringify(existingConfigVariable)
|
||||||
) {
|
) {
|
||||||
await prisma.config.update({
|
await prisma.config.update({
|
||||||
where: { key: configVariableFromDatabase.key },
|
where: {
|
||||||
|
name_category: {
|
||||||
|
name: existingConfigVariable.name,
|
||||||
|
category: existingConfigVariable.category,
|
||||||
|
},
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
...configVariable,
|
...configVariable,
|
||||||
key: configVariableFromDatabase.key,
|
name: existingConfigVariable.name,
|
||||||
value: configVariableFromDatabase.value,
|
category: existingConfigVariable.category,
|
||||||
|
value: existingConfigVariable.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main()
|
|
||||||
|
seedConfigVariables()
|
||||||
|
.then(() => migrateConfigVariables())
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class AuthController {
|
|||||||
@Body() dto: AuthRegisterDTO,
|
@Body() dto: AuthRegisterDTO,
|
||||||
@Res({ passthrough: true }) response: Response
|
@Res({ passthrough: true }) response: Response
|
||||||
) {
|
) {
|
||||||
if (!this.config.get("ALLOW_REGISTRATION"))
|
if (!this.config.get("share.allowRegistration"))
|
||||||
throw new ForbiddenException("Registration is not allowed");
|
throw new ForbiddenException("Registration is not allowed");
|
||||||
|
|
||||||
const result = await this.authService.signUp(dto);
|
const result = await this.authService.signUp(dto);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class AuthService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async signUp(dto: AuthRegisterDTO) {
|
async signUp(dto: AuthRegisterDTO) {
|
||||||
const isFirstUser = this.config.get("SETUP_STATUS") == "STARTED";
|
const isFirstUser = (await this.prisma.user.count()) == 0;
|
||||||
|
|
||||||
const hash = await argon.hash(dto.password);
|
const hash = await argon.hash(dto.password);
|
||||||
try {
|
try {
|
||||||
@@ -38,10 +38,6 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFirstUser) {
|
|
||||||
await this.config.changeSetupStatus("REGISTERED");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
|
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
@@ -161,7 +157,7 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expiresIn: "15min",
|
expiresIn: "15min",
|
||||||
secret: this.config.get("JWT_SECRET"),
|
secret: this.config.get("internal.jwtSecret"),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class JwtGuard extends AuthGuard("jwt") {
|
|||||||
try {
|
try {
|
||||||
return (await super.canActivate(context)) as boolean;
|
return (await super.canActivate(context)) as boolean;
|
||||||
} catch {
|
} catch {
|
||||||
return this.config.get("ALLOW_UNAUTHENTICATED_SHARES");
|
return this.config.get("share.allowUnauthenticatedShares");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import { PrismaService } from "src/prisma/prisma.service";
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(config: ConfigService, private prisma: PrismaService) {
|
constructor(config: ConfigService, private prisma: PrismaService) {
|
||||||
config.get("JWT_SECRET");
|
config.get("internal.jwtSecret");
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: JwtStrategy.extractJWT,
|
jwtFromRequest: JwtStrategy.extractJWT,
|
||||||
secretOrKey: config.get("JWT_SECRET"),
|
secretOrKey: config.get("internal.jwtSecret"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common";
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
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";
|
||||||
@@ -22,24 +30,20 @@ export class ConfigController {
|
|||||||
return new ConfigDTO().fromList(await this.configService.list());
|
return new ConfigDTO().fromList(await this.configService.list());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("admin")
|
@Get("admin/:category")
|
||||||
@UseGuards(JwtGuard, AdministratorGuard)
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
async listForAdmin() {
|
async getByCategory(@Param("category") category: string) {
|
||||||
return new AdminConfigDTO().fromList(
|
return new AdminConfigDTO().fromList(
|
||||||
await this.configService.listForAdmin()
|
await this.configService.getByCategory(category)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch("admin")
|
@Patch("admin")
|
||||||
@UseGuards(JwtGuard, AdministratorGuard)
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
async updateMany(@Body() data: UpdateConfigDTO[]) {
|
async updateMany(@Body() data: UpdateConfigDTO[]) {
|
||||||
await this.configService.updateMany(data);
|
return new AdminConfigDTO().fromList(
|
||||||
}
|
await this.configService.updateMany(data)
|
||||||
|
);
|
||||||
@Post("admin/finishSetup")
|
|
||||||
@UseGuards(JwtGuard, AdministratorGuard)
|
|
||||||
async finishSetup() {
|
|
||||||
return await this.configService.changeSetupStatus("FINISHED");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("admin/testEmail")
|
@Post("admin/testEmail")
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export class ConfigService {
|
|||||||
private prisma: PrismaService
|
private prisma: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get(key: string): any {
|
get(key: `${string}.${string}`): any {
|
||||||
const configVariable = this.configVariables.filter(
|
const configVariable = this.configVariables.filter(
|
||||||
(variable) => variable.key == key
|
(variable) => `${variable.category}.${variable.name}` == key
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
||||||
@@ -27,30 +27,51 @@ export class ConfigService {
|
|||||||
return configVariable.value;
|
return configVariable.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listForAdmin() {
|
async getByCategory(category: string) {
|
||||||
return await this.prisma.config.findMany({
|
const configVariables = await this.prisma.config.findMany({
|
||||||
orderBy: { order: "asc" },
|
orderBy: { order: "asc" },
|
||||||
where: { locked: { equals: false } },
|
where: { category, locked: { equals: false } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return configVariables.map((variable) => {
|
||||||
|
return {
|
||||||
|
key: `${variable.category}.${variable.name}`,
|
||||||
|
...variable,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async list() {
|
async list() {
|
||||||
return await this.prisma.config.findMany({
|
const configVariables = await this.prisma.config.findMany({
|
||||||
where: { secret: { equals: false } },
|
where: { secret: { equals: false } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return configVariables.map((variable) => {
|
||||||
|
return {
|
||||||
|
key: `${variable.category}.${variable.name}`,
|
||||||
|
...variable,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMany(data: { key: string; value: string | number | boolean }[]) {
|
async updateMany(data: { key: string; value: string | number | boolean }[]) {
|
||||||
|
const response: Config[] = [];
|
||||||
|
|
||||||
for (const variable of data) {
|
for (const variable of data) {
|
||||||
await this.update(variable.key, variable.value);
|
response.push(await this.update(variable.key, variable.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(key: string, value: string | number | boolean) {
|
async update(key: string, value: string | number | boolean) {
|
||||||
const configVariable = await this.prisma.config.findUnique({
|
const configVariable = await this.prisma.config.findUnique({
|
||||||
where: { key },
|
where: {
|
||||||
|
name_category: {
|
||||||
|
category: key.split(".")[0],
|
||||||
|
name: key.split(".")[1],
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!configVariable || configVariable.locked)
|
if (!configVariable || configVariable.locked)
|
||||||
@@ -67,7 +88,12 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedVariable = await this.prisma.config.update({
|
const updatedVariable = await this.prisma.config.update({
|
||||||
where: { key },
|
where: {
|
||||||
|
name_category: {
|
||||||
|
category: key.split(".")[0],
|
||||||
|
name: key.split(".")[1],
|
||||||
|
},
|
||||||
|
},
|
||||||
data: { value: value.toString() },
|
data: { value: value.toString() },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,15 +101,4 @@ export class ConfigService {
|
|||||||
|
|
||||||
return updatedVariable;
|
return updatedVariable;
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeSetupStatus(status: "STARTED" | "REGISTERED" | "FINISHED") {
|
|
||||||
const updatedVariable = await this.prisma.config.update({
|
|
||||||
where: { key: "SETUP_STATUS" },
|
|
||||||
data: { value: status },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.configVariables = await this.prisma.config.findMany();
|
|
||||||
|
|
||||||
return updatedVariable;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { Expose, plainToClass } from "class-transformer";
|
|||||||
import { ConfigDTO } from "./config.dto";
|
import { ConfigDTO } from "./config.dto";
|
||||||
|
|
||||||
export class AdminConfigDTO extends ConfigDTO {
|
export class AdminConfigDTO extends ConfigDTO {
|
||||||
|
@Expose()
|
||||||
|
name: string;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
|
|
||||||
@@ -14,9 +17,6 @@ export class AdminConfigDTO extends ConfigDTO {
|
|||||||
@Expose()
|
@Expose()
|
||||||
obscured: boolean;
|
obscured: boolean;
|
||||||
|
|
||||||
@Expose()
|
|
||||||
category: string;
|
|
||||||
|
|
||||||
from(partial: Partial<AdminConfigDTO>) {
|
from(partial: Partial<AdminConfigDTO>) {
|
||||||
return plainToClass(AdminConfigDTO, partial, {
|
return plainToClass(AdminConfigDTO, partial, {
|
||||||
excludeExtraneousValues: true,
|
excludeExtraneousValues: true,
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ export class EmailService {
|
|||||||
constructor(private config: ConfigService) {}
|
constructor(private config: ConfigService) {}
|
||||||
|
|
||||||
getTransporter() {
|
getTransporter() {
|
||||||
if (!this.config.get("SMTP_ENABLED"))
|
if (!this.config.get("smtp.enabled"))
|
||||||
throw new InternalServerErrorException("SMTP is disabled");
|
throw new InternalServerErrorException("SMTP is disabled");
|
||||||
|
|
||||||
return nodemailer.createTransport({
|
return nodemailer.createTransport({
|
||||||
host: this.config.get("SMTP_HOST"),
|
host: this.config.get("smtp.host"),
|
||||||
port: parseInt(this.config.get("SMTP_PORT")),
|
port: this.config.get("smtp.port"),
|
||||||
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
|
secure: this.config.get("smtp.port") == 465,
|
||||||
auth: {
|
auth: {
|
||||||
user: this.config.get("SMTP_USERNAME"),
|
user: this.config.get("smtp.username"),
|
||||||
pass: this.config.get("SMTP_PASSWORD"),
|
pass: this.config.get("smtp.password"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -27,17 +27,19 @@ export class EmailService {
|
|||||||
shareId: string,
|
shareId: string,
|
||||||
creator?: User
|
creator?: User
|
||||||
) {
|
) {
|
||||||
if (!this.config.get("ENABLE_SHARE_EMAIL_RECIPIENTS"))
|
if (!this.config.get("email.enableShareEmailRecipients"))
|
||||||
throw new InternalServerErrorException("Email service disabled");
|
throw new InternalServerErrorException("Email service disabled");
|
||||||
|
|
||||||
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
|
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
|
||||||
|
|
||||||
await this.getTransporter().sendMail({
|
await this.getTransporter().sendMail({
|
||||||
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||||
|
"smtp.email"
|
||||||
|
)}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
subject: this.config.get("SHARE_RECEPIENTS_EMAIL_SUBJECT"),
|
subject: this.config.get("email.shareRecipientsSubject"),
|
||||||
text: this.config
|
text: this.config
|
||||||
.get("SHARE_RECEPIENTS_EMAIL_MESSAGE")
|
.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),
|
||||||
@@ -45,14 +47,16 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendMailToReverseShareCreator(recipientEmail: string, shareId: string) {
|
async sendMailToReverseShareCreator(recipientEmail: string, shareId: string) {
|
||||||
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
|
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
|
||||||
|
|
||||||
await this.getTransporter().sendMail({
|
await this.getTransporter().sendMail({
|
||||||
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||||
|
"smtp.email"
|
||||||
|
)}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
subject: this.config.get("REVERSE_SHARE_EMAIL_SUBJECT"),
|
subject: this.config.get("email.reverseShareSubject"),
|
||||||
text: this.config
|
text: this.config
|
||||||
.get("REVERSE_SHARE_EMAIL_MESSAGE")
|
.get("email.reverseShareMessage")
|
||||||
.replaceAll("\\n", "\n")
|
.replaceAll("\\n", "\n")
|
||||||
.replaceAll("{shareUrl}", shareUrl),
|
.replaceAll("{shareUrl}", shareUrl),
|
||||||
});
|
});
|
||||||
@@ -60,23 +64,43 @@ export class EmailService {
|
|||||||
|
|
||||||
async sendResetPasswordEmail(recipientEmail: string, token: string) {
|
async sendResetPasswordEmail(recipientEmail: string, token: string) {
|
||||||
const resetPasswordUrl = `${this.config.get(
|
const resetPasswordUrl = `${this.config.get(
|
||||||
"APP_URL"
|
"general.appUrl"
|
||||||
)}/auth/resetPassword/${token}`;
|
)}/auth/resetPassword/${token}`;
|
||||||
|
|
||||||
await this.getTransporter().sendMail({
|
await this.getTransporter().sendMail({
|
||||||
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||||
|
"smtp.email"
|
||||||
|
)}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
subject: this.config.get("RESET_PASSWORD_EMAIL_SUBJECT"),
|
subject: this.config.get("email.resetPasswordSubject"),
|
||||||
text: this.config
|
text: this.config
|
||||||
.get("RESET_PASSWORD_EMAIL_MESSAGE")
|
.get("email.resetPasswordMessage")
|
||||||
.replaceAll("{url}", resetPasswordUrl),
|
.replaceAll("{url}", resetPasswordUrl),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendInviteEmail(recipientEmail: string, password: string) {
|
||||||
|
const loginUrl = `${this.config.get("general.appUrl")}/auth/signIn`;
|
||||||
|
|
||||||
|
await this.getTransporter().sendMail({
|
||||||
|
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||||
|
"smtp.email"
|
||||||
|
)}>`,
|
||||||
|
to: recipientEmail,
|
||||||
|
subject: this.config.get("email.inviteSubject"),
|
||||||
|
text: this.config
|
||||||
|
.get("email.inviteMessage")
|
||||||
|
.replaceAll("{url}", loginUrl)
|
||||||
|
.replaceAll("{password}", password),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async sendTestMail(recipientEmail: string) {
|
async sendTestMail(recipientEmail: string) {
|
||||||
try {
|
try {
|
||||||
await this.getTransporter().sendMail({
|
await this.getTransporter().sendMail({
|
||||||
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||||
|
"smtp.email"
|
||||||
|
)}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
subject: "Test email",
|
subject: "Test email",
|
||||||
text: "This is a test email",
|
text: "This is a test email",
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class FileService {
|
|||||||
const shareSizeSum = fileSizeSum + diskFileSize + buffer.byteLength;
|
const shareSizeSum = fileSizeSum + diskFileSize + buffer.byteLength;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shareSizeSum > this.config.get("MAX_SHARE_SIZE") ||
|
shareSizeSum > this.config.get("share.maxSize") ||
|
||||||
(share.reverseShare?.maxShareSize &&
|
(share.reverseShare?.maxShareSize &&
|
||||||
shareSizeSum > parseInt(share.reverseShare.maxShareSize))
|
shareSizeSum > parseInt(share.reverseShare.maxShareSize))
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class ReverseShareController {
|
|||||||
async create(@Body() body: CreateReverseShareDTO, @GetUser() user: User) {
|
async create(@Body() body: CreateReverseShareDTO, @GetUser() user: User) {
|
||||||
const token = await this.reverseShareService.create(body, user.id);
|
const token = await this.reverseShareService.create(body, user.id);
|
||||||
|
|
||||||
const link = `${this.config.get("APP_URL")}/upload/${token}`;
|
const link = `${this.config.get("general.appUrl")}/upload/${token}`;
|
||||||
|
|
||||||
return { token, link };
|
return { token, link };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class ReverseShareService {
|
|||||||
)
|
)
|
||||||
.toDate();
|
.toDate();
|
||||||
|
|
||||||
const globalMaxShareSize = this.config.get("MAX_SHARE_SIZE");
|
const globalMaxShareSize = this.config.get("share.maxSize");
|
||||||
|
|
||||||
if (globalMaxShareSize < data.maxShareSize)
|
if (globalMaxShareSize < data.maxShareSize)
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export class ShareService {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
share.reverseShare &&
|
share.reverseShare &&
|
||||||
this.config.get("SMTP_ENABLED") &&
|
this.config.get("smtp.enabled") &&
|
||||||
share.reverseShare.sendEmailNotification
|
share.reverseShare.sendEmailNotification
|
||||||
) {
|
) {
|
||||||
await this.emailService.sendMailToReverseShareCreator(
|
await this.emailService.sendMailToReverseShareCreator(
|
||||||
@@ -303,7 +303,7 @@ export class ShareService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
expiresIn: moment(expiration).diff(new Date(), "seconds") + "s",
|
expiresIn: moment(expiration).diff(new Date(), "seconds") + "s",
|
||||||
secret: this.config.get("JWT_SECRET"),
|
secret: this.config.get("internal.jwtSecret"),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -315,7 +315,7 @@ export class ShareService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const claims = this.jwtService.verify(token, {
|
const claims = this.jwtService.verify(token, {
|
||||||
secret: this.config.get("JWT_SECRET"),
|
secret: this.config.get("internal.jwtSecret"),
|
||||||
// Ignore expiration if expiration is 0
|
// Ignore expiration if expiration is 0
|
||||||
ignoreExpiration: moment(expiration).isSame(0),
|
ignoreExpiration: moment(expiration).isSame(0),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Expose, plainToClass } from "class-transformer";
|
import { plainToClass } from "class-transformer";
|
||||||
import { Allow } from "class-validator";
|
import { Allow, IsOptional, MinLength } from "class-validator";
|
||||||
import { UserDTO } from "./user.dto";
|
import { UserDTO } from "./user.dto";
|
||||||
|
|
||||||
export class CreateUserDTO extends UserDTO {
|
export class CreateUserDTO extends UserDTO {
|
||||||
@Expose()
|
|
||||||
@Allow()
|
@Allow()
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
@MinLength(8)
|
||||||
|
@IsOptional()
|
||||||
|
password: string;
|
||||||
|
|
||||||
from(partial: Partial<CreateUserDTO>) {
|
from(partial: Partial<CreateUserDTO>) {
|
||||||
return plainToClass(CreateUserDTO, partial, {
|
return plainToClass(CreateUserDTO, partial, {
|
||||||
excludeExtraneousValues: true,
|
excludeExtraneousValues: true,
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
import { Response } from "express";
|
||||||
import { GetUser } from "src/auth/decorator/getUser.decorator";
|
import { GetUser } from "src/auth/decorator/getUser.decorator";
|
||||||
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";
|
||||||
@@ -40,7 +42,16 @@ export class UserController {
|
|||||||
|
|
||||||
@Delete("me")
|
@Delete("me")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
async deleteCurrentUser(@GetUser() user: User) {
|
async deleteCurrentUser(
|
||||||
|
@GetUser() user: User,
|
||||||
|
@Res({ passthrough: true }) response: Response
|
||||||
|
) {
|
||||||
|
response.cookie("access_token", "accessToken", { maxAge: -1 });
|
||||||
|
response.cookie("refresh_token", "", {
|
||||||
|
path: "/api/auth/token",
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: -1,
|
||||||
|
});
|
||||||
return new UserDTO().from(await this.userService.delete(user.id));
|
return new UserDTO().from(await this.userService.delete(user.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
import { EmailModule } from "src/email/email.module";
|
||||||
import { UserController } from "./user.controller";
|
import { UserController } from "./user.controller";
|
||||||
import { UserSevice } from "./user.service";
|
import { UserSevice } from "./user.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [EmailModule],
|
||||||
providers: [UserSevice],
|
providers: [UserSevice],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
|
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";
|
||||||
import { UpdateUserDto } from "./dto/updateUser.dto";
|
import { UpdateUserDto } from "./dto/updateUser.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserSevice {
|
export class UserSevice {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
private emailService: EmailService
|
||||||
|
) {}
|
||||||
|
|
||||||
async list() {
|
async list() {
|
||||||
return await this.prisma.user.findMany();
|
return await this.prisma.user.findMany();
|
||||||
@@ -18,7 +22,17 @@ export class UserSevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateUserDTO) {
|
async create(dto: CreateUserDTO) {
|
||||||
const hash = await argon.hash(dto.password);
|
let hash: string;
|
||||||
|
|
||||||
|
// The password can be undefined if the user is invited by an admin
|
||||||
|
if (!dto.password) {
|
||||||
|
const randomPassword = crypto.randomUUID();
|
||||||
|
hash = await argon.hash(randomPassword);
|
||||||
|
this.emailService.sendInviteEmail(dto.email, randomPassword);
|
||||||
|
} else {
|
||||||
|
hash = await argon.hash(dto.password);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.prisma.user.create({
|
return await this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ services:
|
|||||||
- 3000:3000
|
- 3000:3000
|
||||||
volumes:
|
volumes:
|
||||||
- "./data:/opt/app/backend/data"
|
- "./data:/opt/app/backend/data"
|
||||||
|
- "./data/images:/opt/app/frontend/public/img"
|
||||||
# Optional: If you add ClamAV, uncomment the following to have ClamAV start first.
|
# Optional: If you add ClamAV, uncomment the following to have ClamAV start first.
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# clamav:
|
# clamav:
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-frontend",
|
"name": "pingvin-share-frontend",
|
||||||
"version": "0.10.2",
|
"version": "0.11.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pingvin-share-frontend",
|
"name": "pingvin-share-frontend",
|
||||||
"version": "0.10.2",
|
"version": "0.11.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-frontend",
|
"name": "pingvin-share-frontend",
|
||||||
"version": "0.10.2",
|
"version": "0.11.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
BIN
frontend/public/img/logo.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 943.11 911.62"><ellipse cx="471.56" cy="454.28" rx="471.56" ry="454.28" fill="#46509e"/><ellipse cx="471.56" cy="390.28" rx="233.66" ry="207" fill="#37474f"/><path d="M705.22,849c-36.69,21.14-123.09,64.32-240.64,62.57A469.81,469.81,0,0,1,237.89,849V394.76H705.22Z" fill="#37474f"/><path d="M658.81,397.7V873.49a478.12,478.12,0,0,1-374.19,0V397.7c0-95.55,83.78-173,187.1-173S658.81,302.15,658.81,397.7Z" fill="#fff"/><polygon points="565.02 431.68 471.56 514.49 378.09 431.68 565.02 431.68" fill="#46509e"/><ellipse cx="378.09" cy="369.58" rx="23.37" ry="20.7" fill="#37474f"/><ellipse cx="565.02" cy="369.58" rx="23.37" ry="20.7" fill="#37474f"/><path d="M658.49,400.63c0-40-36.6-72.45-81.79-72.45s-81.78,32.41-81.78,72.45a64.79,64.79,0,0,0,7.9,31.05H440.29a64.79,64.79,0,0,0,7.9-31.05c0-40-36.59-72.45-81.78-72.45s-81.79,32.41-81.79,72.45l-46.73-10.35c0-114.31,104.64-207,233.67-207s233.66,92.69,233.66,207Z" fill="#37474f"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1018 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -8,55 +8,55 @@
|
|||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "icons/icon-72x72.png",
|
"src": "img/icons/icon-72x72.png",
|
||||||
"sizes": "72x72",
|
"sizes": "72x72",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-96x96.png",
|
"src": "img/icons/icon-96x96.png",
|
||||||
"sizes": "96x96",
|
"sizes": "96x96",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-96x96.png",
|
"src": "img/icons/icon-96x96.png",
|
||||||
"sizes": "96x96",
|
"sizes": "96x96",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-128x128.png",
|
"src": "img/icons/icon-128x128.png",
|
||||||
"sizes": "128x128",
|
"sizes": "128x128",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-144x144.png",
|
"src": "img/icons/icon-144x144.png",
|
||||||
"sizes": "144x144",
|
"sizes": "144x144",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-152x152.png",
|
"src": "img/icons/icon-152x152.png",
|
||||||
"sizes": "152x152",
|
"sizes": "152x152",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-192x192.png",
|
"src": "img/icons/icon-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-384x384.png",
|
"src": "img/icons/icon-384x384.png",
|
||||||
"sizes": "384x384",
|
"sizes": "384x384",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-512x512.png",
|
"src": "img/icons/icon-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
|
|||||||
@@ -1,34 +1,6 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
const Logo = ({ height, width }: { height: number; width: number }) => {
|
const Logo = ({ height, width }: { height: number; width: number }) => {
|
||||||
return (
|
return <Image src="/img/logo.png" alt="logo" height={height} width={width} />;
|
||||||
<svg
|
|
||||||
id="Layer_1"
|
|
||||||
data-name="Layer 1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 943.11 911.62"
|
|
||||||
height={height}
|
|
||||||
width={width}
|
|
||||||
>
|
|
||||||
<ellipse cx="471.56" cy="454.28" rx="471.56" ry="454.28" fill="#46509e" />
|
|
||||||
<ellipse cx="471.56" cy="390.28" rx="233.66" ry="207" fill="#37474f" />
|
|
||||||
<path
|
|
||||||
d="M705.22,849c-36.69,21.14-123.09,64.32-240.64,62.57A469.81,469.81,0,0,1,237.89,849V394.76H705.22Z"
|
|
||||||
fill="#37474f"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M658.81,397.7V873.49a478.12,478.12,0,0,1-374.19,0V397.7c0-95.55,83.78-173,187.1-173S658.81,302.15,658.81,397.7Z"
|
|
||||||
fill="#fff"
|
|
||||||
/>
|
|
||||||
<polygon
|
|
||||||
points="565.02 431.68 471.56 514.49 378.09 431.68 565.02 431.68"
|
|
||||||
fill="#46509e"
|
|
||||||
/>
|
|
||||||
<ellipse cx="378.09" cy="369.58" rx="23.37" ry="20.7" fill="#37474f" />
|
|
||||||
<ellipse cx="565.02" cy="369.58" rx="23.37" ry="20.7" fill="#37474f" />
|
|
||||||
<path
|
|
||||||
d="M658.49,400.63c0-40-36.6-72.45-81.79-72.45s-81.78,32.41-81.78,72.45a64.79,64.79,0,0,0,7.9,31.05H440.29a64.79,64.79,0,0,0,7.9-31.05c0-40-36.59-72.45-81.78-72.45s-81.79,32.41-81.79,72.45l-46.73-10.35c0-114.31,104.64-207,233.67-207s233.66,92.69,233.66,207Z"
|
|
||||||
fill="#37474f"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
export default Logo;
|
export default Logo;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import useConfig from "../hooks/config.hook";
|
||||||
|
|
||||||
const Meta = ({
|
const Meta = ({
|
||||||
title,
|
title,
|
||||||
@@ -7,7 +8,9 @@ const Meta = ({
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const metaTitle = `${title} - Pingvin Share`;
|
const config = useConfig();
|
||||||
|
|
||||||
|
const metaTitle = `${title} - ${config.get("general.appName")}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
@@ -19,7 +22,6 @@ const Meta = ({
|
|||||||
description ?? "An open-source and self-hosted sharing platform."
|
description ?? "An open-source and self-hosted sharing platform."
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<meta property="og:image" content="/img/opengraph-default.png" />
|
|
||||||
<meta name="twitter:title" content={metaTitle} />
|
<meta name="twitter:title" content={metaTitle} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
</Head>
|
</Head>
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Group,
|
|
||||||
Paper,
|
|
||||||
Space,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useMediaQuery } from "@mantine/hooks";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import useConfig from "../../../hooks/config.hook";
|
|
||||||
import configService from "../../../services/config.service";
|
|
||||||
import {
|
|
||||||
AdminConfigGroupedByCategory,
|
|
||||||
UpdateConfig,
|
|
||||||
} from "../../../types/config.type";
|
|
||||||
import {
|
|
||||||
capitalizeFirstLetter,
|
|
||||||
configVariableToFriendlyName,
|
|
||||||
} from "../../../utils/string.util";
|
|
||||||
import toast from "../../../utils/toast.util";
|
|
||||||
|
|
||||||
import AdminConfigInput from "./AdminConfigInput";
|
|
||||||
import TestEmailButton from "./TestEmailButton";
|
|
||||||
|
|
||||||
const AdminConfigTable = () => {
|
|
||||||
const config = useConfig();
|
|
||||||
const router = useRouter();
|
|
||||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
|
||||||
|
|
||||||
const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
|
|
||||||
UpdateConfig[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (config.get("SETUP_STATUS") != "FINISHED") {
|
|
||||||
config.refresh();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateConfigVariable = (configVariable: UpdateConfig) => {
|
|
||||||
const index = updatedConfigVariables.findIndex(
|
|
||||||
(item) => item.key === configVariable.key
|
|
||||||
);
|
|
||||||
if (index > -1) {
|
|
||||||
updatedConfigVariables[index] = configVariable;
|
|
||||||
} else {
|
|
||||||
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [configVariablesByCategory, setCofigVariablesByCategory] =
|
|
||||||
useState<AdminConfigGroupedByCategory>({});
|
|
||||||
|
|
||||||
const getConfigVariables = async () => {
|
|
||||||
await configService.listForAdmin().then((configVariables) => {
|
|
||||||
const configVariablesByCategory = configVariables.reduce(
|
|
||||||
(categories: any, item) => {
|
|
||||||
const category = categories[item.category] || [];
|
|
||||||
category.push(item);
|
|
||||||
categories[item.category] = category;
|
|
||||||
return categories;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
setCofigVariablesByCategory(configVariablesByCategory);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveConfigVariables = async () => {
|
|
||||||
if (config.get("SETUP_STATUS") == "REGISTERED") {
|
|
||||||
await configService
|
|
||||||
.updateMany(updatedConfigVariables)
|
|
||||||
.then(async () => {
|
|
||||||
await configService.finishSetup();
|
|
||||||
router.reload();
|
|
||||||
})
|
|
||||||
.catch(toast.axiosError);
|
|
||||||
} else {
|
|
||||||
await configService
|
|
||||||
.updateMany(updatedConfigVariables)
|
|
||||||
.then(() => {
|
|
||||||
setUpdatedConfigVariables([]);
|
|
||||||
toast.success("Configurations updated successfully");
|
|
||||||
})
|
|
||||||
.catch(toast.axiosError);
|
|
||||||
}
|
|
||||||
config.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getConfigVariables();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box mb="lg">
|
|
||||||
{Object.entries(configVariablesByCategory).map(
|
|
||||||
([category, configVariables]) => {
|
|
||||||
return (
|
|
||||||
<Paper key={category} withBorder p="lg" mb="xl">
|
|
||||||
<Title mb="xs" order={3}>
|
|
||||||
{capitalizeFirstLetter(category)}
|
|
||||||
</Title>
|
|
||||||
{configVariables.map((configVariable) => (
|
|
||||||
<>
|
|
||||||
<Group position="apart">
|
|
||||||
<Stack
|
|
||||||
style={{ maxWidth: isMobile ? "100%" : "40%" }}
|
|
||||||
spacing={0}
|
|
||||||
>
|
|
||||||
<Title order={6}>
|
|
||||||
{configVariableToFriendlyName(configVariable.key)}
|
|
||||||
</Title>
|
|
||||||
<Text color="dimmed" size="sm" mb="xs">
|
|
||||||
{configVariable.description}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack></Stack>
|
|
||||||
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
|
||||||
<AdminConfigInput
|
|
||||||
key={configVariable.key}
|
|
||||||
updateConfigVariable={updateConfigVariable}
|
|
||||||
configVariable={configVariable}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Space h="lg" />
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
{category == "smtp" && (
|
|
||||||
<Group position="right">
|
|
||||||
<TestEmailButton
|
|
||||||
configVariablesChanged={updatedConfigVariables.length != 0}
|
|
||||||
saveConfigVariables={saveConfigVariables}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
<Group position="right">
|
|
||||||
<Button onClick={saveConfigVariables}>Save</Button>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminConfigTable;
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
Burger,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Header,
|
||||||
|
MediaQuery,
|
||||||
|
Text,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import useConfig from "../../../hooks/config.hook";
|
||||||
|
import Logo from "../../Logo";
|
||||||
|
|
||||||
|
const ConfigurationHeader = ({
|
||||||
|
isMobileNavBarOpened,
|
||||||
|
setIsMobileNavBarOpened,
|
||||||
|
}: {
|
||||||
|
isMobileNavBarOpened: boolean;
|
||||||
|
setIsMobileNavBarOpened: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}) => {
|
||||||
|
const config = useConfig();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
return (
|
||||||
|
<Header height={60} p="md">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", height: "100%" }}>
|
||||||
|
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
|
||||||
|
<Burger
|
||||||
|
opened={isMobileNavBarOpened}
|
||||||
|
onClick={() => setIsMobileNavBarOpened((o) => !o)}
|
||||||
|
size="sm"
|
||||||
|
color={theme.colors.gray[6]}
|
||||||
|
mr="xl"
|
||||||
|
/>
|
||||||
|
</MediaQuery>
|
||||||
|
<Group position="apart" w="100%">
|
||||||
|
<Link href="/" passHref>
|
||||||
|
<Group>
|
||||||
|
<Logo height={35} width={35} />
|
||||||
|
<Text weight={600}>{config.get("general.appName")}</Text>
|
||||||
|
</Group>
|
||||||
|
</Link>
|
||||||
|
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
|
||||||
|
<Button variant="light" component={Link} href="/admin">
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</MediaQuery>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigurationHeader;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
createStyles,
|
||||||
|
Group,
|
||||||
|
MediaQuery,
|
||||||
|
Navbar,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ name: "General", icon: <TbSquare /> },
|
||||||
|
{ name: "Email", icon: <TbMail /> },
|
||||||
|
{ name: "Share", icon: <TbShare /> },
|
||||||
|
{ name: "SMTP", icon: <TbAt /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
activeLink: {
|
||||||
|
backgroundColor: theme.fn.variant({
|
||||||
|
variant: "light",
|
||||||
|
color: theme.primaryColor,
|
||||||
|
}).background,
|
||||||
|
color: theme.fn.variant({ variant: "light", color: theme.primaryColor })
|
||||||
|
.color,
|
||||||
|
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ConfigurationNavBar = ({
|
||||||
|
categoryId,
|
||||||
|
isMobileNavBarOpened,
|
||||||
|
setIsMobileNavBarOpened,
|
||||||
|
}: {
|
||||||
|
categoryId: string;
|
||||||
|
isMobileNavBarOpened: boolean;
|
||||||
|
setIsMobileNavBarOpened: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
return (
|
||||||
|
<Navbar
|
||||||
|
p="md"
|
||||||
|
hiddenBreakpoint="sm"
|
||||||
|
hidden={!isMobileNavBarOpened}
|
||||||
|
width={{ sm: 200, lg: 300 }}
|
||||||
|
>
|
||||||
|
<Navbar.Section>
|
||||||
|
<Text size="xs" color="dimmed" mb="sm">
|
||||||
|
Configuration
|
||||||
|
</Text>
|
||||||
|
<Stack spacing="xs">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<Box
|
||||||
|
p="xs"
|
||||||
|
component={Link}
|
||||||
|
onClick={() => setIsMobileNavBarOpened(false)}
|
||||||
|
className={
|
||||||
|
categoryId == category.name.toLowerCase()
|
||||||
|
? classes.activeLink
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
key={category.name}
|
||||||
|
href={`/admin/config/${category.name.toLowerCase()}`}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon
|
||||||
|
variant={
|
||||||
|
categoryId == category.name.toLowerCase()
|
||||||
|
? "filled"
|
||||||
|
: "light"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{category.icon}
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text size="sm">{category.name}</Text>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Navbar.Section>
|
||||||
|
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
|
||||||
|
<Button mt="xl" variant="light" component={Link} href="/admin">
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</MediaQuery>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigurationNavBar;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ActionIcon, Box, Group, Skeleton, Table } from "@mantine/core";
|
import { ActionIcon, Box, Group, Skeleton, Table } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
||||||
import User from "../../types/user.type";
|
import User from "../../../types/user.type";
|
||||||
import showUpdateUserModal from "./showUpdateUserModal";
|
import showUpdateUserModal from "./showUpdateUserModal";
|
||||||
|
|
||||||
const ManageUserTable = ({
|
const ManageUserTable = ({
|
||||||
@@ -10,38 +10,44 @@ import {
|
|||||||
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";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import userService from "../../services/user.service";
|
import userService from "../../../services/user.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
const showCreateUserModal = (
|
const showCreateUserModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
|
smtpEnabled: boolean,
|
||||||
getUsers: () => void
|
getUsers: () => void
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: <Title order={5}>Create user</Title>,
|
title: <Title order={5}>Create user</Title>,
|
||||||
children: <Body modals={modals} getUsers={getUsers} />,
|
children: (
|
||||||
|
<Body modals={modals} smtpEnabled={smtpEnabled} getUsers={getUsers} />
|
||||||
|
),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const Body = ({
|
const Body = ({
|
||||||
modals,
|
modals,
|
||||||
|
smtpEnabled,
|
||||||
getUsers,
|
getUsers,
|
||||||
}: {
|
}: {
|
||||||
modals: ModalsContextProps;
|
modals: ModalsContextProps;
|
||||||
|
smtpEnabled: boolean;
|
||||||
getUsers: () => void;
|
getUsers: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: undefined,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
|
setPasswordManually: false,
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
email: yup.string().email(),
|
email: yup.string().email(),
|
||||||
username: yup.string().min(3),
|
username: yup.string().min(3),
|
||||||
password: yup.string().min(8),
|
password: yup.string().min(8).optional(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -62,14 +68,35 @@ const Body = ({
|
|||||||
<Stack>
|
<Stack>
|
||||||
<TextInput label="Username" {...form.getInputProps("username")} />
|
<TextInput label="Username" {...form.getInputProps("username")} />
|
||||||
<TextInput label="Email" {...form.getInputProps("email")} />
|
<TextInput label="Email" {...form.getInputProps("email")} />
|
||||||
<PasswordInput
|
{smtpEnabled && (
|
||||||
label="New password"
|
<Switch
|
||||||
{...form.getInputProps("password")}
|
mt="xs"
|
||||||
/>
|
labelPosition="left"
|
||||||
|
label="Set password manually"
|
||||||
|
description="If not checked, the user will receive an email with a link to set their password."
|
||||||
|
{...form.getInputProps("setPasswordManually", {
|
||||||
|
type: "checkbox",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{form.values.setPasswordManually ||
|
||||||
|
(!smtpEnabled && (
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<Switch
|
<Switch
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
}}
|
||||||
mt="xs"
|
mt="xs"
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
label="Admin privileges"
|
label="Admin privileges"
|
||||||
|
description="If checked, the user will be able to access the admin panel."
|
||||||
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
||||||
/>
|
/>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
@@ -11,9 +11,9 @@ import {
|
|||||||
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";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import userService from "../../services/user.service";
|
import userService from "../../../services/user.service";
|
||||||
import User from "../../types/user.type";
|
import User from "../../../types/user.type";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
const showUpdateUserModal = (
|
const showUpdateUserModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
@@ -90,7 +90,7 @@ const Body = ({
|
|||||||
</form>
|
</form>
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
|
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
|
||||||
<Accordion.Control>Change password</Accordion.Control>
|
<Accordion.Control px={0}>Change password</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<form
|
<form
|
||||||
onSubmit={passwordForm.onSubmit(async (values) => {
|
onSubmit={passwordForm.onSubmit(async (values) => {
|
||||||
@@ -95,7 +95,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
|||||||
<Title order={2} align="center" weight={900}>
|
<Title order={2} align="center" weight={900}>
|
||||||
Welcome back
|
Welcome back
|
||||||
</Title>
|
</Title>
|
||||||
{config.get("ALLOW_REGISTRATION") && (
|
{config.get("share.allowRegistration") && (
|
||||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||||
You don't have an account yet?{" "}
|
You don't have an account yet?{" "}
|
||||||
<Anchor component={Link} href={"signUp"} size="sm">
|
<Anchor component={Link} href={"signUp"} size="sm">
|
||||||
@@ -131,7 +131,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
|||||||
{...form.getInputProps("totp")}
|
{...form.getInputProps("totp")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{config.get("SMTP_ENABLED") && (
|
{config.get("smtp.enabled") && (
|
||||||
<Group position="right" mt="xs">
|
<Group position="right" mt="xs">
|
||||||
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
||||||
Forgot password?
|
Forgot password?
|
||||||
|
|||||||
@@ -41,8 +41,12 @@ const SignUpForm = () => {
|
|||||||
await authService
|
await authService
|
||||||
.signUp(email, username, password)
|
.signUp(email, username, password)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await refreshUser();
|
const user = await refreshUser();
|
||||||
router.replace("/upload");
|
if (user?.isAdmin) {
|
||||||
|
router.replace("/admin/intro");
|
||||||
|
} else {
|
||||||
|
router.replace("/upload");
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(toast.axiosError);
|
.catch(toast.axiosError);
|
||||||
};
|
};
|
||||||
@@ -52,7 +56,7 @@ const SignUpForm = () => {
|
|||||||
<Title order={2} align="center" weight={900}>
|
<Title order={2} align="center" weight={900}>
|
||||||
Sign up
|
Sign up
|
||||||
</Title>
|
</Title>
|
||||||
{config.get("ALLOW_REGISTRATION") && (
|
{config.get("share.allowRegistration") && (
|
||||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||||
You have an account already?{" "}
|
You have an account already?{" "}
|
||||||
<Anchor component={Link} href={"signIn"} size="sm">
|
<Anchor component={Link} href={"signIn"} size="sm">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
Container,
|
Container,
|
||||||
createStyles,
|
createStyles,
|
||||||
Group,
|
Group,
|
||||||
Header,
|
Header as MantineHeader,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
@@ -108,7 +108,7 @@ const useStyles = createStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const NavBar = () => {
|
const Header = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
@@ -141,20 +141,20 @@ const NavBar = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (config.get("ALLOW_UNAUTHENTICATED_SHARES")) {
|
if (config.get("share.allowUnauthenticatedShares")) {
|
||||||
unauthenticatedLinks.unshift({
|
unauthenticatedLinks.unshift({
|
||||||
link: "/upload",
|
link: "/upload",
|
||||||
label: "Upload",
|
label: "Upload",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.get("SHOW_HOME_PAGE"))
|
if (config.get("general.showHomePage"))
|
||||||
unauthenticatedLinks.unshift({
|
unauthenticatedLinks.unshift({
|
||||||
link: "/",
|
link: "/",
|
||||||
label: "Home",
|
label: "Home",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.get("ALLOW_REGISTRATION"))
|
if (config.get("share.allowRegistration"))
|
||||||
unauthenticatedLinks.push({
|
unauthenticatedLinks.push({
|
||||||
link: "/auth/signUp",
|
link: "/auth/signUp",
|
||||||
label: "Sign up",
|
label: "Sign up",
|
||||||
@@ -187,12 +187,12 @@ const NavBar = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Header height={HEADER_HEIGHT} mb={40} className={classes.root}>
|
<MantineHeader height={HEADER_HEIGHT} mb={40} className={classes.root}>
|
||||||
<Container className={classes.header}>
|
<Container className={classes.header}>
|
||||||
<Link href="/" passHref>
|
<Link href="/" passHref>
|
||||||
<Group>
|
<Group>
|
||||||
<Logo height={35} width={35} />
|
<Logo height={35} width={35} />
|
||||||
<Text weight={600}>Pingvin Share</Text>
|
<Text weight={600}>{config.get("general.appName")}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Link>
|
</Link>
|
||||||
<Group spacing={5} className={classes.links}>
|
<Group spacing={5} className={classes.links}>
|
||||||
@@ -212,8 +212,8 @@ const NavBar = () => {
|
|||||||
)}
|
)}
|
||||||
</Transition>
|
</Transition>
|
||||||
</Container>
|
</Container>
|
||||||
</Header>
|
</MantineHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NavBar;
|
export default Header;
|
||||||
@@ -33,9 +33,9 @@ const FileList = ({
|
|||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
const copyFileLink = (file: FileMetaData) => {
|
const copyFileLink = (file: FileMetaData) => {
|
||||||
const link = `${config.get("APP_URL")}/api/shares/${share.id}/files/${
|
const link = `${config.get("general.appUrl")}/api/shares/${
|
||||||
file.id
|
share.id
|
||||||
}`;
|
}/files/${file.id}`;
|
||||||
|
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
clipboard.copy(link);
|
clipboard.copy(link);
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ export async function middleware(request: NextRequest) {
|
|||||||
const routes = {
|
const routes = {
|
||||||
unauthenticated: new Routes(["/auth/*", "/"]),
|
unauthenticated: new Routes(["/auth/*", "/"]),
|
||||||
public: new Routes(["/share/*", "/upload/*"]),
|
public: new Routes(["/share/*", "/upload/*"]),
|
||||||
setupStatusRegistered: new Routes(["/auth/*", "/admin/setup"]),
|
|
||||||
admin: new Routes(["/admin/*"]),
|
admin: new Routes(["/admin/*"]),
|
||||||
account: new Routes(["/account/*"]),
|
account: new Routes(["/account*"]),
|
||||||
disabled: new Routes([]),
|
disabled: new Routes([]),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,41 +44,28 @@ export async function middleware(request: NextRequest) {
|
|||||||
user = null;
|
user = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getConfig("ALLOW_REGISTRATION")) {
|
if (!getConfig("share.allowRegistration")) {
|
||||||
routes.disabled.routes.push("/auth/signUp");
|
routes.disabled.routes.push("/auth/signUp");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getConfig("ALLOW_UNAUTHENTICATED_SHARES")) {
|
if (getConfig("share.allowUnauthenticatedShares")) {
|
||||||
routes.public.routes = ["*"];
|
routes.public.routes = ["*"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getConfig("SMTP_ENABLED")) {
|
if (!getConfig("smtp.enabled")) {
|
||||||
routes.disabled.routes.push("/auth/resetPassword*");
|
routes.disabled.routes.push("/auth/resetPassword*");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getConfig("SETUP_STATUS") == "FINISHED") {
|
|
||||||
routes.disabled.routes.push("/admin/setup");
|
|
||||||
}
|
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const rules = [
|
const rules = [
|
||||||
// Disabled routes
|
// Disabled routes
|
||||||
{
|
{
|
||||||
condition: routes.disabled.contains(route),
|
condition: routes.disabled.contains(route),
|
||||||
path: "/",
|
path: "/",
|
||||||
},
|
|
||||||
// Setup status
|
|
||||||
{
|
|
||||||
condition: getConfig("SETUP_STATUS") == "STARTED" && route != "/auth/signUp",
|
|
||||||
path: "/auth/signUp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: getConfig("SETUP_STATUS") == "REGISTERED" && !routes.setupStatusRegistered.contains(route) && user?.isAdmin,
|
|
||||||
path: "/admin/setup",
|
|
||||||
},
|
},
|
||||||
// Authenticated state
|
// Authenticated state
|
||||||
{
|
{
|
||||||
condition: user && routes.unauthenticated.contains(route) && !getConfig("ALLOW_UNAUTHENTICATED_SHARES"),
|
condition: user && routes.unauthenticated.contains(route) && !getConfig("share.allowUnauthenticatedShares"),
|
||||||
path: "/upload",
|
path: "/upload",
|
||||||
},
|
},
|
||||||
// Unauthenticated state
|
// Unauthenticated state
|
||||||
@@ -98,7 +84,7 @@ export async function middleware(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
// Home page
|
// Home page
|
||||||
{
|
{
|
||||||
condition: (!getConfig("SHOW_HOME_PAGE") || user) && route == "/",
|
condition: (!getConfig("general.showHomePage") || user) && route == "/",
|
||||||
path: "/upload",
|
path: "/upload",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import axios from "axios";
|
|||||||
import { getCookie, setCookie } from "cookies-next";
|
import { getCookie, setCookie } from "cookies-next";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Header from "../components/navBar/NavBar";
|
import Header from "../components/header/Header";
|
||||||
import { ConfigContext } from "../hooks/config.hook";
|
import { ConfigContext } from "../hooks/config.hook";
|
||||||
import usePreferences from "../hooks/usePreferences";
|
import usePreferences from "../hooks/usePreferences";
|
||||||
import { UserContext } from "../hooks/user.hook";
|
import { UserContext } from "../hooks/user.hook";
|
||||||
@@ -24,17 +25,26 @@ import globalStyle from "../styles/mantine.style";
|
|||||||
import Config from "../types/config.type";
|
import Config from "../types/config.type";
|
||||||
import { CurrentUser } from "../types/user.type";
|
import { CurrentUser } from "../types/user.type";
|
||||||
|
|
||||||
|
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
|
||||||
|
|
||||||
function App({ Component, pageProps }: AppProps) {
|
function App({ Component, pageProps }: AppProps) {
|
||||||
const systemTheme = useColorScheme(pageProps.colorScheme);
|
const systemTheme = useColorScheme(pageProps.colorScheme);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
||||||
const preferences = usePreferences();
|
const preferences = usePreferences();
|
||||||
|
|
||||||
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
||||||
|
const [route, setRoute] = useState<string>(pageProps.route);
|
||||||
|
|
||||||
const [configVariables, setConfigVariables] = useState<Config[]>(
|
const [configVariables, setConfigVariables] = useState<Config[]>(
|
||||||
pageProps.configVariables
|
pageProps.configVariables
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRoute(router.pathname);
|
||||||
|
}, [router.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -86,10 +96,16 @@ function App({ Component, pageProps }: AppProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Header />
|
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||||
<Container>
|
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Container>
|
) : (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<Container>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
@@ -105,12 +121,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
|||||||
let pageProps: {
|
let pageProps: {
|
||||||
user?: CurrentUser;
|
user?: CurrentUser;
|
||||||
configVariables?: Config[];
|
configVariables?: Config[];
|
||||||
|
route?: string;
|
||||||
colorScheme: ColorScheme;
|
colorScheme: ColorScheme;
|
||||||
} = {
|
} = {
|
||||||
|
route: ctx.resolvedUrl,
|
||||||
colorScheme:
|
colorScheme:
|
||||||
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ctx.req) {
|
if (ctx.req) {
|
||||||
const cookieHeader = ctx.req.headers.cookie;
|
const cookieHeader = ctx.req.headers.cookie;
|
||||||
|
|
||||||
@@ -123,6 +140,8 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
|||||||
pageProps.configVariables = (
|
pageProps.configVariables = (
|
||||||
await axios(`http://localhost:8080/api/configs`)
|
await axios(`http://localhost:8080/api/configs`)
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
|
pageProps.route = ctx.req.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { pageProps };
|
return { pageProps };
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ export default class _Document extends Document {
|
|||||||
<Html>
|
<Html>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="apple-touch-icon" href="/icons/icon-white-128x128.png" />
|
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
href="/img/icons/icon-white-128x128.png"
|
||||||
|
/>
|
||||||
|
|
||||||
<meta property="og:image" content="/img/opengraph-default.png" />
|
<meta property="og:image" content="/img/opengraph.png" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:image" content="/img/opengraph-default.png" />
|
<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>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const MyShares = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
showCreateReverseShareModal(
|
showCreateReverseShareModal(
|
||||||
modals,
|
modals,
|
||||||
config.get("SMTP_ENABLED"),
|
config.get("smtp.enabled"),
|
||||||
getReverseShares
|
getReverseShares
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -129,9 +129,9 @@ const MyShares = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
clipboard.copy(
|
clipboard.copy(
|
||||||
`${config.get("APP_URL")}/share/${
|
`${config.get(
|
||||||
share.id
|
"general.appUrl"
|
||||||
}`
|
)}/share/${share.id}`
|
||||||
);
|
);
|
||||||
toast.success(
|
toast.success(
|
||||||
"The share link was copied to the keyboard."
|
"The share link was copied to the keyboard."
|
||||||
@@ -140,7 +140,7 @@ const MyShares = () => {
|
|||||||
showShareLinkModal(
|
showShareLinkModal(
|
||||||
modals,
|
modals,
|
||||||
share.id,
|
share.id,
|
||||||
config.get("APP_URL")
|
config.get("general.appUrl")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ const MyShares = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
clipboard.copy(
|
clipboard.copy(
|
||||||
`${config.get("APP_URL")}/share/${share.id}`
|
`${config.get("general.appUrl")}/share/${
|
||||||
|
share.id
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
toast.success(
|
toast.success(
|
||||||
"Your link was copied to the keyboard."
|
"Your link was copied to the keyboard."
|
||||||
@@ -93,7 +95,7 @@ const MyShares = () => {
|
|||||||
showShareLinkModal(
|
showShareLinkModal(
|
||||||
modals,
|
modals,
|
||||||
share.id,
|
share.id,
|
||||||
config.get("APP_URL")
|
config.get("general.appUrl")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Space, Title } from "@mantine/core";
|
|
||||||
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
|
|
||||||
import Meta from "../../components/Meta";
|
|
||||||
|
|
||||||
const AdminConfig = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Meta title="Configuration" />
|
|
||||||
<Title mb={30} order={3}>
|
|
||||||
Configuration
|
|
||||||
</Title>
|
|
||||||
<AdminConfigTable />
|
|
||||||
<Space h="xl" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminConfig;
|
|
||||||
148
frontend/src/pages/admin/config/[category].tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import {
|
||||||
|
AppShell,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
||||||
|
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
|
||||||
|
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
|
||||||
|
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
|
||||||
|
import CenterLoader from "../../../components/core/CenterLoader";
|
||||||
|
import Meta from "../../../components/Meta";
|
||||||
|
import useConfig from "../../../hooks/config.hook";
|
||||||
|
import configService from "../../../services/config.service";
|
||||||
|
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
||||||
|
import {
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
configVariableToFriendlyName,
|
||||||
|
} from "../../../utils/string.util";
|
||||||
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
|
export default function AppShellDemo() {
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false);
|
||||||
|
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
const categoryId = router.query.category as string;
|
||||||
|
|
||||||
|
const [configVariables, setConfigVariables] = useState<AdminConfig[]>();
|
||||||
|
const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
|
||||||
|
UpdateConfig[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const saveConfigVariables = async () => {
|
||||||
|
await configService
|
||||||
|
.updateMany(updatedConfigVariables)
|
||||||
|
.then(() => {
|
||||||
|
setUpdatedConfigVariables([]);
|
||||||
|
toast.success("Configurations updated successfully");
|
||||||
|
})
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
config.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConfigVariable = (configVariable: UpdateConfig) => {
|
||||||
|
const index = updatedConfigVariables.findIndex(
|
||||||
|
(item) => item.key === configVariable.key
|
||||||
|
);
|
||||||
|
if (index > -1) {
|
||||||
|
updatedConfigVariables[index] = configVariable;
|
||||||
|
} else {
|
||||||
|
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
configService.getByCategory(categoryId).then((configVariables) => {
|
||||||
|
setConfigVariables(configVariables);
|
||||||
|
});
|
||||||
|
}, [categoryId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta title="Configuration" />
|
||||||
|
<AppShell
|
||||||
|
styles={{
|
||||||
|
main: {
|
||||||
|
background:
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? theme.colors.dark[8]
|
||||||
|
: theme.colors.gray[0],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
navbar={
|
||||||
|
<ConfigurationNavBar
|
||||||
|
categoryId={categoryId}
|
||||||
|
isMobileNavBarOpened={isMobileNavBarOpened}
|
||||||
|
setIsMobileNavBarOpened={setIsMobileNavBarOpened}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
header={
|
||||||
|
<ConfigurationHeader
|
||||||
|
isMobileNavBarOpened={isMobileNavBarOpened}
|
||||||
|
setIsMobileNavBarOpened={setIsMobileNavBarOpened}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Container size="lg">
|
||||||
|
{!configVariables ? (
|
||||||
|
<CenterLoader />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Stack>
|
||||||
|
<Title mb="md" order={3}>
|
||||||
|
{capitalizeFirstLetter(categoryId)}
|
||||||
|
</Title>
|
||||||
|
{configVariables.map((configVariable) => (
|
||||||
|
<Group key={configVariable.key} position="apart">
|
||||||
|
<Stack
|
||||||
|
style={{ maxWidth: isMobile ? "100%" : "40%" }}
|
||||||
|
spacing={0}
|
||||||
|
>
|
||||||
|
<Title order={6}>
|
||||||
|
{configVariableToFriendlyName(configVariable.name)}
|
||||||
|
</Title>
|
||||||
|
<Text color="dimmed" size="sm" mb="xs">
|
||||||
|
{configVariable.description}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack></Stack>
|
||||||
|
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||||
|
<AdminConfigInput
|
||||||
|
key={configVariable.key}
|
||||||
|
configVariable={configVariable}
|
||||||
|
updateConfigVariable={updateConfigVariable}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Group mt="lg" position="right">
|
||||||
|
{categoryId == "smtp" && (
|
||||||
|
<TestEmailButton
|
||||||
|
configVariablesChanged={updatedConfigVariables.length != 0}
|
||||||
|
saveConfigVariables={saveConfigVariables}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button onClick={saveConfigVariables}>Save</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</AppShell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
frontend/src/pages/admin/config/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function getServerSideProps() {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: false,
|
||||||
|
destination: "/admin/config/general",
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Config = () => {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Config;
|
||||||
59
frontend/src/pages/admin/intro.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
Anchor,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Container,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Logo from "../../components/Logo";
|
||||||
|
import Meta from "../../components/Meta";
|
||||||
|
|
||||||
|
const Intro = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta title="Intro" />
|
||||||
|
<Container size="xs">
|
||||||
|
<Stack>
|
||||||
|
<Center>
|
||||||
|
<Logo height={80} width={80} />
|
||||||
|
</Center>
|
||||||
|
<Center>
|
||||||
|
<Title order={2}>Welcome to Pingvin Share</Title>
|
||||||
|
</Center>
|
||||||
|
<Text>
|
||||||
|
If you enjoy Pingvin Share please ⭐️ it on{" "}
|
||||||
|
<Anchor
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/stonith404/pingvin-share"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</Anchor>{" "}
|
||||||
|
or{" "}
|
||||||
|
<Anchor
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/sponsors/stonith404"
|
||||||
|
>
|
||||||
|
buy me a coffee
|
||||||
|
</Anchor>{" "}
|
||||||
|
if you want to support my work.
|
||||||
|
</Text>
|
||||||
|
<Text>Enough talked, have fun with Pingvin Share!</Text>
|
||||||
|
<Text mt="lg">How to you want to continue?</Text>
|
||||||
|
<Stack>
|
||||||
|
<Button href="/admin/config" component={Link}>
|
||||||
|
Customize configuration
|
||||||
|
</Button>
|
||||||
|
<Button href="/" component={Link} variant="light">
|
||||||
|
Explore Pingvin Share
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Intro;
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Box, Stack, Text, Title } from "@mantine/core";
|
|
||||||
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
|
|
||||||
|
|
||||||
import Logo from "../../components/Logo";
|
|
||||||
import Meta from "../../components/Meta";
|
|
||||||
|
|
||||||
const Setup = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Meta title="Setup" />
|
|
||||||
<Stack align="center">
|
|
||||||
<Logo height={80} width={80} />
|
|
||||||
<Title order={2}>Welcome to Pingvin Share</Title>
|
|
||||||
<Text>Let's customize Pingvin Share for you! </Text>
|
|
||||||
<Box style={{ width: "100%" }}>
|
|
||||||
<AdminConfigTable />
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Setup;
|
|
||||||
@@ -2,9 +2,10 @@ import { Button, Group, Space, Text, Title } from "@mantine/core";
|
|||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbPlus } from "react-icons/tb";
|
import { TbPlus } from "react-icons/tb";
|
||||||
import ManageUserTable from "../../components/admin/ManageUserTable";
|
import ManageUserTable from "../../components/admin/users/ManageUserTable";
|
||||||
import showCreateUserModal from "../../components/admin/showCreateUserModal";
|
import showCreateUserModal from "../../components/admin/users/showCreateUserModal";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
import userService from "../../services/user.service";
|
import userService from "../../services/user.service";
|
||||||
import User from "../../types/user.type";
|
import User from "../../types/user.type";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
@@ -12,6 +13,8 @@ import toast from "../../utils/toast.util";
|
|||||||
const Users = () => {
|
const Users = () => {
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const config = useConfig();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
const getUsers = () => {
|
const getUsers = () => {
|
||||||
@@ -54,7 +57,9 @@ const Users = () => {
|
|||||||
User management
|
User management
|
||||||
</Title>
|
</Title>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => showCreateUserModal(modals, getUsers)}
|
onClick={() =>
|
||||||
|
showCreateUserModal(modals, config.get("smtp.enabled"), getUsers)
|
||||||
|
}
|
||||||
leftIcon={<TbPlus size={20} />}
|
leftIcon={<TbPlus size={20} />}
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const config = {
|
|||||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
return httpProxyMiddleware(req, res, {
|
return httpProxyMiddleware(req, res, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Forwarded-For": req.socket.remoteAddress ?? "",
|
"X-Forwarded-For": req.socket?.remoteAddress ?? "",
|
||||||
},
|
},
|
||||||
target: "http://localhost:8080",
|
target: "http://localhost:8080",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ const ResetPassword = () => {
|
|||||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit((values) => {
|
onSubmit={form.onSubmit((values) => {
|
||||||
console.log(resetPasswordToken);
|
|
||||||
authService
|
authService
|
||||||
.resetPassword(resetPasswordToken, values.password)
|
.resetPassword(resetPasswordToken, values.password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import {
|
|||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { TbCheck } from "react-icons/tb";
|
import { TbCheck } from "react-icons/tb";
|
||||||
|
import Logo from "../components/Logo";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
import useUser from "../hooks/user.hook";
|
import useUser from "../hooks/user.hook";
|
||||||
|
|
||||||
@@ -150,12 +150,7 @@ export default function Home() {
|
|||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
<Group className={classes.image} align="center">
|
<Group className={classes.image} align="center">
|
||||||
<Image
|
<Logo width={200} height={200} />
|
||||||
src="/img/logo.svg"
|
|
||||||
alt="Pingvin Share Logo"
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const Upload = ({
|
|||||||
const [files, setFiles] = useState<FileUpload[]>([]);
|
const [files, setFiles] = useState<FileUpload[]>([]);
|
||||||
const [isUploading, setisUploading] = useState(false);
|
const [isUploading, setisUploading] = useState(false);
|
||||||
|
|
||||||
maxShareSize ??= parseInt(config.get("MAX_SHARE_SIZE"));
|
maxShareSize ??= parseInt(config.get("share.maxSize"));
|
||||||
|
|
||||||
const uploadFiles = async (share: CreateShare) => {
|
const uploadFiles = async (share: CreateShare) => {
|
||||||
setisUploading(true);
|
setisUploading(true);
|
||||||
@@ -146,7 +146,7 @@ const Upload = ({
|
|||||||
.completeShare(createdShare.id)
|
.completeShare(createdShare.id)
|
||||||
.then((share) => {
|
.then((share) => {
|
||||||
setisUploading(false);
|
setisUploading(false);
|
||||||
showCompletedUploadModal(modals, share, config.get("APP_URL"));
|
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
@@ -168,12 +168,12 @@ const Upload = ({
|
|||||||
{
|
{
|
||||||
isUserSignedIn: user ? true : false,
|
isUserSignedIn: user ? true : false,
|
||||||
isReverseShare,
|
isReverseShare,
|
||||||
appUrl: config.get("APP_URL"),
|
appUrl: config.get("general.appUrl"),
|
||||||
allowUnauthenticatedShares: config.get(
|
allowUnauthenticatedShares: config.get(
|
||||||
"ALLOW_UNAUTHENTICATED_SHARES"
|
"share.allowUnauthenticatedShares"
|
||||||
),
|
),
|
||||||
enableEmailRecepients: config.get(
|
enableEmailRecepients: config.get(
|
||||||
"ENABLE_SHARE_EMAIL_RECIPIENTS"
|
"email.enableShareEmailRecipients"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
uploadFiles
|
uploadFiles
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ const list = async (): Promise<Config[]> => {
|
|||||||
return (await api.get("/configs")).data;
|
return (await api.get("/configs")).data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const listForAdmin = async (): Promise<AdminConfig[]> => {
|
const getByCategory = async (category: string): Promise<AdminConfig[]> => {
|
||||||
return (await api.get("/configs/admin")).data;
|
return (await api.get(`/configs/admin/${category}`)).data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMany = async (data: UpdateConfig[]): Promise<AdminConfig[]> => {
|
const updateMany = async (data: UpdateConfig[]): Promise<AdminConfig[]> => {
|
||||||
@@ -48,7 +48,7 @@ const isNewReleaseAvailable = async () => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
list,
|
list,
|
||||||
listForAdmin,
|
getByCategory,
|
||||||
updateMany,
|
updateMany,
|
||||||
get,
|
get,
|
||||||
finishSetup,
|
finishSetup,
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export type UpdateConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AdminConfig = Config & {
|
export type AdminConfig = Config & {
|
||||||
|
name: string;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
obscured: boolean;
|
obscured: boolean;
|
||||||
category: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminConfigGroupedByCategory = {
|
export type AdminConfigGroupedByCategory = {
|
||||||
@@ -29,6 +29,11 @@ export type AdminConfigGroupedByCategory = {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ConfigVariablesCategory = {
|
||||||
|
category: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConfigHook = {
|
export type ConfigHook = {
|
||||||
configVariables: Config[];
|
configVariables: Config[];
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type User = {
|
|||||||
export type CreateUser = {
|
export type CreateUser = {
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password?: string;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
export const configVariableToFriendlyName = (variable: string) => {
|
export const configVariableToFriendlyName = (variable: string) => {
|
||||||
return variable
|
const splitted = variable.split(/(?=[A-Z])/).join(" ");
|
||||||
.split("_")
|
return splitted.charAt(0).toUpperCase() + splitted.slice(1);
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
||||||
.join(" ");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const capitalizeFirstLetter = (string: string) => {
|
export const capitalizeFirstLetter = (string: string) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share",
|
"name": "pingvin-share",
|
||||||
"version": "0.10.2",
|
"version": "0.11.1",
|
||||||
"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",
|
||||||
|
|||||||