feat: custom branding (#112)
* add first concept * remove setup status * split config page in multiple components * add custom branding docs * add test email button * fix invalid email from header * add migration * mount images to host * update docs * remove unused endpoint * run formatter
2
.gitignore
vendored
@ -39,4 +39,4 @@ yarn-error.log*
|
||||
/data/
|
||||
|
||||
# Jetbrains specific (webstorm)
|
||||
.idea/**/**
|
||||
.idea/**/**
|
||||
|
@ -35,10 +35,9 @@ RUN apt-get update && apt-get install -y openssl
|
||||
|
||||
WORKDIR /opt/app/frontend
|
||||
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/static ./.next/static
|
||||
COPY --from=frontend-builder /opt/app/public/img /tmp/img
|
||||
|
||||
WORKDIR /opt/app/backend
|
||||
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
|
||||
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
|
19
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
|
||||
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
|
||||
|
||||
@ -57,7 +57,7 @@ npm run build
|
||||
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
|
||||
|
||||
@ -94,6 +94,21 @@ docker compose up -d
|
||||
```
|
||||
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
|
||||
|
||||
You're very welcome to contribute to Pingvin Share! Follow the [contribution guide](/CONTRIBUTING.md) to get started.
|
||||
|
@ -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 {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
key String @id
|
||||
name String
|
||||
category String
|
||||
type String
|
||||
value String
|
||||
description String
|
||||
category String
|
||||
obscured Boolean @default(false)
|
||||
secret Boolean @default(true)
|
||||
locked Boolean @default(false)
|
||||
order Int
|
||||
|
||||
@@id([name, category])
|
||||
}
|
||||
|
@ -1,260 +1,244 @@
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
const configVariables: Prisma.ConfigCreateInput[] = [
|
||||
{
|
||||
order: 0,
|
||||
key: "SETUP_STATUS",
|
||||
description: "Status of the setup wizard",
|
||||
type: "string",
|
||||
value: "STARTED", // STARTED, REGISTERED, FINISHED
|
||||
category: "internal",
|
||||
secret: false,
|
||||
locked: true,
|
||||
const configVariables: ConfigVariables = {
|
||||
internal: {
|
||||
jwtSecret: {
|
||||
description: "Long random string used to sign JWT tokens",
|
||||
type: "string",
|
||||
value: crypto.randomBytes(256).toString("base64"),
|
||||
locked: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
order: 0,
|
||||
key: "JWT_SECRET",
|
||||
description: "Long random string used to sign JWT tokens",
|
||||
type: "string",
|
||||
value: crypto.randomBytes(256).toString("base64"),
|
||||
category: "internal",
|
||||
locked: true,
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
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,
|
||||
general: {
|
||||
appName: {
|
||||
description: "Name of the application",
|
||||
type: "string",
|
||||
value: "Pingvin Share",
|
||||
secret: false,
|
||||
},
|
||||
appUrl: {
|
||||
description: "On which URL Pingvin Share is available",
|
||||
type: "string",
|
||||
value: "http://localhost:3000",
|
||||
|
||||
key: "MAX_SHARE_SIZE",
|
||||
description: "Maximum share size in bytes",
|
||||
type: "number",
|
||||
value: "1073741824",
|
||||
category: "share",
|
||||
secret: false,
|
||||
secret: false,
|
||||
},
|
||||
showHomePage: {
|
||||
description: "Whether to show the home page",
|
||||
type: "boolean",
|
||||
value: "true",
|
||||
secret: false,
|
||||
},
|
||||
},
|
||||
share: {
|
||||
allowRegistration: {
|
||||
description: "Whether registration is allowed",
|
||||
type: "boolean",
|
||||
value: "true",
|
||||
|
||||
{
|
||||
order: 6,
|
||||
key: "ENABLE_SHARE_EMAIL_RECIPIENTS",
|
||||
description:
|
||||
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||
type: "boolean",
|
||||
value: "false",
|
||||
category: "email",
|
||||
secret: false,
|
||||
secret: false,
|
||||
},
|
||||
allowUnauthenticatedShares: {
|
||||
description: "Whether unauthorized users can create shares",
|
||||
type: "boolean",
|
||||
value: "false",
|
||||
|
||||
secret: false,
|
||||
},
|
||||
maxSize: {
|
||||
description: "Maximum share size in bytes",
|
||||
type: "number",
|
||||
value: "1073741824",
|
||||
|
||||
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",
|
||||
email: {
|
||||
enableShareEmailRecipients: {
|
||||
description:
|
||||
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||
type: "boolean",
|
||||
value: "false",
|
||||
|
||||
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 🐧",
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
smtp: {
|
||||
enabled: {
|
||||
description:
|
||||
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||
type: "boolean",
|
||||
value: "false",
|
||||
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: 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",
|
||||
},
|
||||
{
|
||||
order: 13,
|
||||
key: "INVITE_EMAIL_SUBJECT",
|
||||
description:
|
||||
"Subject of the email which gets sent when an admin invites an user.",
|
||||
type: "string",
|
||||
value: "Pingvin Share invite",
|
||||
category: "email",
|
||||
},
|
||||
{
|
||||
order: 14,
|
||||
key: "INVITE_EMAIL_MESSAGE",
|
||||
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 🐧",
|
||||
category: "email",
|
||||
},
|
||||
{
|
||||
order: 15,
|
||||
key: "SMTP_ENABLED",
|
||||
description:
|
||||
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||
type: "boolean",
|
||||
value: "false",
|
||||
category: "smtp",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
order: 16,
|
||||
key: "SMTP_HOST",
|
||||
description: "Host of the SMTP server",
|
||||
type: "string",
|
||||
value: "",
|
||||
category: "smtp",
|
||||
},
|
||||
{
|
||||
order: 17,
|
||||
key: "SMTP_PORT",
|
||||
description: "Port of the SMTP server",
|
||||
type: "number",
|
||||
value: "0",
|
||||
category: "smtp",
|
||||
},
|
||||
{
|
||||
order: 18,
|
||||
key: "SMTP_EMAIL",
|
||||
description: "Email address which the emails get sent from",
|
||||
type: "string",
|
||||
value: "",
|
||||
category: "smtp",
|
||||
},
|
||||
{
|
||||
order: 19,
|
||||
key: "SMTP_USERNAME",
|
||||
description: "Username of the SMTP server",
|
||||
type: "string",
|
||||
value: "",
|
||||
category: "smtp",
|
||||
},
|
||||
{
|
||||
order: 20,
|
||||
key: "SMTP_PASSWORD",
|
||||
description: "Password of the SMTP server",
|
||||
type: "string",
|
||||
value: "",
|
||||
obscured: true,
|
||||
category: "smtp",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
type ConfigVariables = {
|
||||
[category: string]: {
|
||||
[variable: string]: Omit<
|
||||
Prisma.ConfigCreateInput,
|
||||
"name" | "category" | "order"
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
for (const variable of configVariables) {
|
||||
const existingConfigVariable = await prisma.config.findUnique({
|
||||
where: { key: variable.key },
|
||||
});
|
||||
|
||||
// Create a new config variable if it doesn't exist
|
||||
if (!existingConfigVariable) {
|
||||
await prisma.config.create({
|
||||
data: variable,
|
||||
async function seedConfigVariables() {
|
||||
for (const [category, configVariablesOfCategory] of Object.entries(
|
||||
configVariables
|
||||
)) {
|
||||
let order = 0;
|
||||
for (const [name, properties] of Object.entries(
|
||||
configVariablesOfCategory
|
||||
)) {
|
||||
const existingConfigVariable = await prisma.config.findUnique({
|
||||
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 configVariableFromDatabase of configVariablesFromDatabase) {
|
||||
const configVariable = configVariables.find(
|
||||
(v) => v.key == configVariableFromDatabase.key
|
||||
);
|
||||
for (const existingConfigVariable of existingConfigVariables) {
|
||||
const configVariable =
|
||||
configVariables[existingConfigVariable.category]?.[
|
||||
existingConfigVariable.name
|
||||
];
|
||||
if (!configVariable) {
|
||||
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
|
||||
} else if (
|
||||
JSON.stringify({
|
||||
...configVariable,
|
||||
key: configVariableFromDatabase.key,
|
||||
value: configVariableFromDatabase.value,
|
||||
}) != JSON.stringify(configVariableFromDatabase)
|
||||
name: existingConfigVariable.name,
|
||||
category: existingConfigVariable.category,
|
||||
value: existingConfigVariable.value,
|
||||
}) != JSON.stringify(existingConfigVariable)
|
||||
) {
|
||||
await prisma.config.update({
|
||||
where: { key: configVariableFromDatabase.key },
|
||||
where: {
|
||||
name_category: {
|
||||
name: existingConfigVariable.name,
|
||||
category: existingConfigVariable.category,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
...configVariable,
|
||||
key: configVariableFromDatabase.key,
|
||||
value: configVariableFromDatabase.value,
|
||||
name: existingConfigVariable.name,
|
||||
category: existingConfigVariable.category,
|
||||
value: existingConfigVariable.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
main()
|
||||
|
||||
seedConfigVariables()
|
||||
.then(() => migrateConfigVariables())
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
|
@ -42,7 +42,7 @@ export class AuthController {
|
||||
@Body() dto: AuthRegisterDTO,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
) {
|
||||
if (!this.config.get("ALLOW_REGISTRATION"))
|
||||
if (!this.config.get("share.allowRegistration"))
|
||||
throw new ForbiddenException("Registration is not allowed");
|
||||
|
||||
const result = await this.authService.signUp(dto);
|
||||
|
@ -25,7 +25,7 @@ export class AuthService {
|
||||
) {}
|
||||
|
||||
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);
|
||||
try {
|
||||
@ -38,10 +38,6 @@ export class AuthService {
|
||||
},
|
||||
});
|
||||
|
||||
if (isFirstUser) {
|
||||
await this.config.changeSetupStatus("REGISTERED");
|
||||
}
|
||||
|
||||
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
|
||||
user.id
|
||||
);
|
||||
@ -161,7 +157,7 @@ export class AuthService {
|
||||
},
|
||||
{
|
||||
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 {
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
} 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()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(config: ConfigService, private prisma: PrismaService) {
|
||||
config.get("JWT_SECRET");
|
||||
config.get("internal.jwtSecret");
|
||||
super({
|
||||
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 { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||
@ -22,24 +30,20 @@ export class ConfigController {
|
||||
return new ConfigDTO().fromList(await this.configService.list());
|
||||
}
|
||||
|
||||
@Get("admin")
|
||||
@Get("admin/:category")
|
||||
@UseGuards(JwtGuard, AdministratorGuard)
|
||||
async listForAdmin() {
|
||||
async getByCategory(@Param("category") category: string) {
|
||||
return new AdminConfigDTO().fromList(
|
||||
await this.configService.listForAdmin()
|
||||
await this.configService.getByCategory(category)
|
||||
);
|
||||
}
|
||||
|
||||
@Patch("admin")
|
||||
@UseGuards(JwtGuard, AdministratorGuard)
|
||||
async updateMany(@Body() data: UpdateConfigDTO[]) {
|
||||
await this.configService.updateMany(data);
|
||||
}
|
||||
|
||||
@Post("admin/finishSetup")
|
||||
@UseGuards(JwtGuard, AdministratorGuard)
|
||||
async finishSetup() {
|
||||
return await this.configService.changeSetupStatus("FINISHED");
|
||||
return new AdminConfigDTO().fromList(
|
||||
await this.configService.updateMany(data)
|
||||
);
|
||||
}
|
||||
|
||||
@Post("admin/testEmail")
|
||||
|
@ -14,9 +14,9 @@ export class ConfigService {
|
||||
private prisma: PrismaService
|
||||
) {}
|
||||
|
||||
get(key: string): any {
|
||||
get(key: `${string}.${string}`): any {
|
||||
const configVariable = this.configVariables.filter(
|
||||
(variable) => variable.key == key
|
||||
(variable) => `${variable.category}.${variable.name}` == key
|
||||
)[0];
|
||||
|
||||
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
||||
@ -27,30 +27,51 @@ export class ConfigService {
|
||||
return configVariable.value;
|
||||
}
|
||||
|
||||
async listForAdmin() {
|
||||
return await this.prisma.config.findMany({
|
||||
async getByCategory(category: string) {
|
||||
const configVariables = await this.prisma.config.findMany({
|
||||
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() {
|
||||
return await this.prisma.config.findMany({
|
||||
const configVariables = await this.prisma.config.findMany({
|
||||
where: { secret: { equals: false } },
|
||||
});
|
||||
|
||||
return configVariables.map((variable) => {
|
||||
return {
|
||||
key: `${variable.category}.${variable.name}`,
|
||||
...variable,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async updateMany(data: { key: string; value: string | number | boolean }[]) {
|
||||
const response: Config[] = [];
|
||||
|
||||
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) {
|
||||
const configVariable = await this.prisma.config.findUnique({
|
||||
where: { key },
|
||||
where: {
|
||||
name_category: {
|
||||
category: key.split(".")[0],
|
||||
name: key.split(".")[1],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!configVariable || configVariable.locked)
|
||||
@ -67,7 +88,12 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
const updatedVariable = await this.prisma.config.update({
|
||||
where: { key },
|
||||
where: {
|
||||
name_category: {
|
||||
category: key.split(".")[0],
|
||||
name: key.split(".")[1],
|
||||
},
|
||||
},
|
||||
data: { value: value.toString() },
|
||||
});
|
||||
|
||||
@ -75,15 +101,4 @@ export class ConfigService {
|
||||
|
||||
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";
|
||||
|
||||
export class AdminConfigDTO extends ConfigDTO {
|
||||
@Expose()
|
||||
name: string;
|
||||
|
||||
@Expose()
|
||||
secret: boolean;
|
||||
|
||||
@ -14,9 +17,6 @@ export class AdminConfigDTO extends ConfigDTO {
|
||||
@Expose()
|
||||
obscured: boolean;
|
||||
|
||||
@Expose()
|
||||
category: string;
|
||||
|
||||
from(partial: Partial<AdminConfigDTO>) {
|
||||
return plainToClass(AdminConfigDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
|
@ -8,16 +8,16 @@ export class EmailService {
|
||||
constructor(private config: ConfigService) {}
|
||||
|
||||
getTransporter() {
|
||||
if (!this.config.get("SMTP_ENABLED"))
|
||||
if (!this.config.get("smtp.enabled"))
|
||||
throw new InternalServerErrorException("SMTP is disabled");
|
||||
|
||||
return nodemailer.createTransport({
|
||||
host: this.config.get("SMTP_HOST"),
|
||||
port: parseInt(this.config.get("SMTP_PORT")),
|
||||
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
|
||||
host: this.config.get("smtp.host"),
|
||||
port: this.config.get("smtp.port"),
|
||||
secure: this.config.get("smtp.port") == 465,
|
||||
auth: {
|
||||
user: this.config.get("SMTP_USERNAME"),
|
||||
pass: this.config.get("SMTP_PASSWORD"),
|
||||
user: this.config.get("smtp.username"),
|
||||
pass: this.config.get("smtp.password"),
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -27,17 +27,19 @@ export class EmailService {
|
||||
shareId: string,
|
||||
creator?: User
|
||||
) {
|
||||
if (!this.config.get("ENABLE_SHARE_EMAIL_RECIPIENTS"))
|
||||
if (!this.config.get("email.enableShareEmailRecipients"))
|
||||
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({
|
||||
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email"
|
||||
)}>`,
|
||||
to: recipientEmail,
|
||||
subject: this.config.get("SHARE_RECEPIENTS_EMAIL_SUBJECT"),
|
||||
subject: this.config.get("email.shareRecipientsSubject"),
|
||||
text: this.config
|
||||
.get("SHARE_RECEPIENTS_EMAIL_MESSAGE")
|
||||
.get("email.shareRecipientsMessage")
|
||||
.replaceAll("\\n", "\n")
|
||||
.replaceAll("{creator}", creator?.username ?? "Someone")
|
||||
.replaceAll("{shareUrl}", shareUrl),
|
||||
@ -45,14 +47,16 @@ export class EmailService {
|
||||
}
|
||||
|
||||
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({
|
||||
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email"
|
||||
)}>`,
|
||||
to: recipientEmail,
|
||||
subject: this.config.get("REVERSE_SHARE_EMAIL_SUBJECT"),
|
||||
subject: this.config.get("email.reverseShareSubject"),
|
||||
text: this.config
|
||||
.get("REVERSE_SHARE_EMAIL_MESSAGE")
|
||||
.get("email.reverseShareMessage")
|
||||
.replaceAll("\\n", "\n")
|
||||
.replaceAll("{shareUrl}", shareUrl),
|
||||
});
|
||||
@ -60,28 +64,32 @@ export class EmailService {
|
||||
|
||||
async sendResetPasswordEmail(recipientEmail: string, token: string) {
|
||||
const resetPasswordUrl = `${this.config.get(
|
||||
"APP_URL"
|
||||
"general.appUrl"
|
||||
)}/auth/resetPassword/${token}`;
|
||||
|
||||
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,
|
||||
subject: this.config.get("RESET_PASSWORD_EMAIL_SUBJECT"),
|
||||
subject: this.config.get("email.resetPasswordSubject"),
|
||||
text: this.config
|
||||
.get("RESET_PASSWORD_EMAIL_MESSAGE")
|
||||
.get("email.resetPasswordMessage")
|
||||
.replaceAll("{url}", resetPasswordUrl),
|
||||
});
|
||||
}
|
||||
|
||||
async sendInviteEmail(recipientEmail: string, password: string) {
|
||||
const loginUrl = `${this.config.get("APP_URL")}/auth/signIn`;
|
||||
const loginUrl = `${this.config.get("general.appUrl")}/auth/signIn`;
|
||||
|
||||
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,
|
||||
subject: this.config.get("INVITE_EMAIL_SUBJECT"),
|
||||
subject: this.config.get("email.inviteSubject"),
|
||||
text: this.config
|
||||
.get("INVITE_EMAIL_MESSAGE")
|
||||
.get("email.inviteMessage")
|
||||
.replaceAll("{url}", loginUrl)
|
||||
.replaceAll("{password}", password),
|
||||
});
|
||||
@ -90,7 +98,9 @@ export class EmailService {
|
||||
async sendTestMail(recipientEmail: string) {
|
||||
try {
|
||||
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,
|
||||
subject: "Test email",
|
||||
text: "This is a test email",
|
||||
|
@ -67,7 +67,7 @@ export class FileService {
|
||||
const shareSizeSum = fileSizeSum + diskFileSize + buffer.byteLength;
|
||||
|
||||
if (
|
||||
shareSizeSum > this.config.get("MAX_SHARE_SIZE") ||
|
||||
shareSizeSum > this.config.get("share.maxSize") ||
|
||||
(share.reverseShare?.maxShareSize &&
|
||||
shareSizeSum > parseInt(share.reverseShare.maxShareSize))
|
||||
) {
|
||||
|
@ -31,7 +31,7 @@ export class ReverseShareController {
|
||||
async create(@Body() body: CreateReverseShareDTO, @GetUser() user: User) {
|
||||
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 };
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export class ReverseShareService {
|
||||
)
|
||||
.toDate();
|
||||
|
||||
const globalMaxShareSize = this.config.get("MAX_SHARE_SIZE");
|
||||
const globalMaxShareSize = this.config.get("share.maxSize");
|
||||
|
||||
if (globalMaxShareSize < data.maxShareSize)
|
||||
throw new BadRequestException(
|
||||
|
@ -153,7 +153,7 @@ export class ShareService {
|
||||
|
||||
if (
|
||||
share.reverseShare &&
|
||||
this.config.get("SMTP_ENABLED") &&
|
||||
this.config.get("smtp.enabled") &&
|
||||
share.reverseShare.sendEmailNotification
|
||||
) {
|
||||
await this.emailService.sendMailToReverseShareCreator(
|
||||
@ -303,7 +303,7 @@ export class ShareService {
|
||||
},
|
||||
{
|
||||
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 {
|
||||
const claims = this.jwtService.verify(token, {
|
||||
secret: this.config.get("JWT_SECRET"),
|
||||
secret: this.config.get("internal.jwtSecret"),
|
||||
// Ignore expiration if expiration is 0
|
||||
ignoreExpiration: moment(expiration).isSame(0),
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ import { UserController } from "./user.controller";
|
||||
import { UserSevice } from "./user.service";
|
||||
|
||||
@Module({
|
||||
imports:[EmailModule],
|
||||
imports: [EmailModule],
|
||||
providers: [UserSevice],
|
||||
controllers: [UserController],
|
||||
})
|
||||
|
@ -7,6 +7,7 @@ services:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- "./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.
|
||||
# depends_on:
|
||||
# clamav:
|
||||
|
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": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/icon-72x72.png",
|
||||
"src": "img/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-96x96.png",
|
||||
"src": "img/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-96x96.png",
|
||||
"src": "img/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-128x128.png",
|
||||
"src": "img/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-144x144.png",
|
||||
"src": "img/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-152x152.png",
|
||||
"src": "img/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-192x192.png",
|
||||
"src": "img/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-384x384.png",
|
||||
"src": "img/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512x512.png",
|
||||
"src": "img/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
|
@ -1,34 +1,6 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const Logo = ({ height, width }: { height: number; width: number }) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return <Image src="/img/logo.png" alt="logo" height={height} width={width} />;
|
||||
};
|
||||
export default Logo;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Head from "next/head";
|
||||
import useConfig from "../hooks/config.hook";
|
||||
|
||||
const Meta = ({
|
||||
title,
|
||||
@ -7,7 +8,9 @@ const Meta = ({
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
const metaTitle = `${title} - Pingvin Share`;
|
||||
const config = useConfig();
|
||||
|
||||
const metaTitle = `${title} - ${config.get("general.appName")}`;
|
||||
|
||||
return (
|
||||
<Head>
|
||||
@ -19,7 +22,6 @@ const Meta = ({
|
||||
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:description" content={description} />
|
||||
</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;
|
@ -79,12 +79,13 @@ const Body = ({
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{form.values.setPasswordManually || !smtpEnabled && (
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
)}
|
||||
{form.values.setPasswordManually ||
|
||||
(!smtpEnabled && (
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
))}
|
||||
<Switch
|
||||
styles={{
|
||||
body: {
|
||||
|
@ -95,7 +95,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
<Title order={2} align="center" weight={900}>
|
||||
Welcome back
|
||||
</Title>
|
||||
{config.get("ALLOW_REGISTRATION") && (
|
||||
{config.get("share.allowRegistration") && (
|
||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||
You don't have an account yet?{" "}
|
||||
<Anchor component={Link} href={"signUp"} size="sm">
|
||||
@ -131,7 +131,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
{...form.getInputProps("totp")}
|
||||
/>
|
||||
)}
|
||||
{config.get("SMTP_ENABLED") && (
|
||||
{config.get("smtp.enabled") && (
|
||||
<Group position="right" mt="xs">
|
||||
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
||||
Forgot password?
|
||||
|
@ -41,8 +41,12 @@ const SignUpForm = () => {
|
||||
await authService
|
||||
.signUp(email, username, password)
|
||||
.then(async () => {
|
||||
await refreshUser();
|
||||
router.replace("/upload");
|
||||
const user = await refreshUser();
|
||||
if (user?.isAdmin) {
|
||||
router.replace("/admin/intro");
|
||||
} else {
|
||||
router.replace("/upload");
|
||||
}
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
};
|
||||
@ -52,7 +56,7 @@ const SignUpForm = () => {
|
||||
<Title order={2} align="center" weight={900}>
|
||||
Sign up
|
||||
</Title>
|
||||
{config.get("ALLOW_REGISTRATION") && (
|
||||
{config.get("share.allowRegistration") && (
|
||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||
You have an account already?{" "}
|
||||
<Anchor component={Link} href={"signIn"} size="sm">
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
Container,
|
||||
createStyles,
|
||||
Group,
|
||||
Header,
|
||||
Header as MantineHeader,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
@ -108,7 +108,7 @@ const useStyles = createStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const NavBar = () => {
|
||||
const Header = () => {
|
||||
const { user } = useUser();
|
||||
const router = useRouter();
|
||||
const config = useConfig();
|
||||
@ -141,20 +141,20 @@ const NavBar = () => {
|
||||
},
|
||||
];
|
||||
|
||||
if (config.get("ALLOW_UNAUTHENTICATED_SHARES")) {
|
||||
if (config.get("share.allowUnauthenticatedShares")) {
|
||||
unauthenticatedLinks.unshift({
|
||||
link: "/upload",
|
||||
label: "Upload",
|
||||
});
|
||||
}
|
||||
|
||||
if (config.get("SHOW_HOME_PAGE"))
|
||||
if (config.get("general.showHomePage"))
|
||||
unauthenticatedLinks.unshift({
|
||||
link: "/",
|
||||
label: "Home",
|
||||
});
|
||||
|
||||
if (config.get("ALLOW_REGISTRATION"))
|
||||
if (config.get("share.allowRegistration"))
|
||||
unauthenticatedLinks.push({
|
||||
link: "/auth/signUp",
|
||||
label: "Sign up",
|
||||
@ -187,12 +187,12 @@ const NavBar = () => {
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Header height={HEADER_HEIGHT} mb={40} className={classes.root}>
|
||||
<MantineHeader height={HEADER_HEIGHT} mb={40} className={classes.root}>
|
||||
<Container className={classes.header}>
|
||||
<Link href="/" passHref>
|
||||
<Group>
|
||||
<Logo height={35} width={35} />
|
||||
<Text weight={600}>Pingvin Share</Text>
|
||||
<Text weight={600}>{config.get("general.appName")}</Text>
|
||||
</Group>
|
||||
</Link>
|
||||
<Group spacing={5} className={classes.links}>
|
||||
@ -212,8 +212,8 @@ const NavBar = () => {
|
||||
)}
|
||||
</Transition>
|
||||
</Container>
|
||||
</Header>
|
||||
</MantineHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
export default Header;
|
@ -33,9 +33,9 @@ const FileList = ({
|
||||
const modals = useModals();
|
||||
|
||||
const copyFileLink = (file: FileMetaData) => {
|
||||
const link = `${config.get("APP_URL")}/api/shares/${share.id}/files/${
|
||||
file.id
|
||||
}`;
|
||||
const link = `${config.get("general.appUrl")}/api/shares/${
|
||||
share.id
|
||||
}/files/${file.id}`;
|
||||
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(link);
|
||||
|
@ -15,9 +15,8 @@ export async function middleware(request: NextRequest) {
|
||||
const routes = {
|
||||
unauthenticated: new Routes(["/auth/*", "/"]),
|
||||
public: new Routes(["/share/*", "/upload/*"]),
|
||||
setupStatusRegistered: new Routes(["/auth/*", "/admin/setup"]),
|
||||
admin: new Routes(["/admin/*"]),
|
||||
account: new Routes(["/account/*"]),
|
||||
account: new Routes(["/account*"]),
|
||||
disabled: new Routes([]),
|
||||
};
|
||||
|
||||
@ -45,41 +44,28 @@ export async function middleware(request: NextRequest) {
|
||||
user = null;
|
||||
}
|
||||
|
||||
if (!getConfig("ALLOW_REGISTRATION")) {
|
||||
if (!getConfig("share.allowRegistration")) {
|
||||
routes.disabled.routes.push("/auth/signUp");
|
||||
}
|
||||
|
||||
if (getConfig("ALLOW_UNAUTHENTICATED_SHARES")) {
|
||||
if (getConfig("share.allowUnauthenticatedShares")) {
|
||||
routes.public.routes = ["*"];
|
||||
}
|
||||
|
||||
if (!getConfig("SMTP_ENABLED")) {
|
||||
if (!getConfig("smtp.enabled")) {
|
||||
routes.disabled.routes.push("/auth/resetPassword*");
|
||||
}
|
||||
|
||||
if (getConfig("SETUP_STATUS") == "FINISHED") {
|
||||
routes.disabled.routes.push("/admin/setup");
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const rules = [
|
||||
// Disabled routes
|
||||
{
|
||||
condition: routes.disabled.contains(route),
|
||||
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
|
||||
{
|
||||
condition: user && routes.unauthenticated.contains(route) && !getConfig("ALLOW_UNAUTHENTICATED_SHARES"),
|
||||
condition: user && routes.unauthenticated.contains(route) && !getConfig("share.allowUnauthenticatedShares"),
|
||||
path: "/upload",
|
||||
},
|
||||
// Unauthenticated state
|
||||
@ -98,7 +84,7 @@ export async function middleware(request: NextRequest) {
|
||||
},
|
||||
// Home page
|
||||
{
|
||||
condition: (!getConfig("SHOW_HOME_PAGE") || user) && route == "/",
|
||||
condition: (!getConfig("general.showHomePage") || user) && route == "/",
|
||||
path: "/upload",
|
||||
},
|
||||
];
|
||||
|
@ -11,8 +11,9 @@ import axios from "axios";
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Header from "../components/navBar/NavBar";
|
||||
import Header from "../components/header/Header";
|
||||
import { ConfigContext } from "../hooks/config.hook";
|
||||
import usePreferences from "../hooks/usePreferences";
|
||||
import { UserContext } from "../hooks/user.hook";
|
||||
@ -24,17 +25,26 @@ import globalStyle from "../styles/mantine.style";
|
||||
import Config from "../types/config.type";
|
||||
import { CurrentUser } from "../types/user.type";
|
||||
|
||||
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
|
||||
|
||||
function App({ Component, pageProps }: AppProps) {
|
||||
const systemTheme = useColorScheme(pageProps.colorScheme);
|
||||
const router = useRouter();
|
||||
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
||||
const preferences = usePreferences();
|
||||
|
||||
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
||||
const [route, setRoute] = useState<string>(pageProps.route);
|
||||
|
||||
const [configVariables, setConfigVariables] = useState<Config[]>(
|
||||
pageProps.configVariables
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRoute(router.pathname);
|
||||
}, [router.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
||||
}, []);
|
||||
@ -86,10 +96,16 @@ function App({ Component, pageProps }: AppProps) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
<Container>
|
||||
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
) : (
|
||||
<>
|
||||
<Header />
|
||||
<Container>
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</UserContext.Provider>
|
||||
</ConfigContext.Provider>
|
||||
</ModalsProvider>
|
||||
@ -105,12 +121,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
let pageProps: {
|
||||
user?: CurrentUser;
|
||||
configVariables?: Config[];
|
||||
route?: string;
|
||||
colorScheme: ColorScheme;
|
||||
} = {
|
||||
route: ctx.resolvedUrl,
|
||||
colorScheme:
|
||||
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
||||
};
|
||||
|
||||
if (ctx.req) {
|
||||
const cookieHeader = ctx.req.headers.cookie;
|
||||
|
||||
@ -123,6 +140,8 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
pageProps.configVariables = (
|
||||
await axios(`http://localhost:8080/api/configs`)
|
||||
).data;
|
||||
|
||||
pageProps.route = ctx.req.url;
|
||||
}
|
||||
|
||||
return { pageProps };
|
||||
|
@ -11,11 +11,15 @@ export default class _Document extends Document {
|
||||
<Html>
|
||||
<Head>
|
||||
<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:image" content="/img/opengraph-default.png" />
|
||||
<meta name="twitter:image" content="/img/opengraph.png" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="theme-color" content="#46509e" />
|
||||
</Head>
|
||||
|
@ -67,7 +67,7 @@ const MyShares = () => {
|
||||
onClick={() =>
|
||||
showCreateReverseShareModal(
|
||||
modals,
|
||||
config.get("SMTP_ENABLED"),
|
||||
config.get("smtp.enabled"),
|
||||
getReverseShares
|
||||
)
|
||||
}
|
||||
@ -129,9 +129,9 @@ const MyShares = () => {
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("APP_URL")}/share/${
|
||||
share.id
|
||||
}`
|
||||
`${config.get(
|
||||
"general.appUrl"
|
||||
)}/share/${share.id}`
|
||||
);
|
||||
toast.success(
|
||||
"The share link was copied to the keyboard."
|
||||
@ -140,7 +140,7 @@ const MyShares = () => {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
share.id,
|
||||
config.get("APP_URL")
|
||||
config.get("general.appUrl")
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
@ -84,7 +84,9 @@ const MyShares = () => {
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("APP_URL")}/share/${share.id}`
|
||||
`${config.get("general.appUrl")}/share/${
|
||||
share.id
|
||||
}`
|
||||
);
|
||||
toast.success(
|
||||
"Your link was copied to the keyboard."
|
||||
@ -93,7 +95,7 @@ const MyShares = () => {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
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;
|
@ -58,7 +58,7 @@ const Users = () => {
|
||||
</Title>
|
||||
<Button
|
||||
onClick={() =>
|
||||
showCreateUserModal(modals, config.get("SMTP_ENABLED"), getUsers)
|
||||
showCreateUserModal(modals, config.get("smtp.enabled"), getUsers)
|
||||
}
|
||||
leftIcon={<TbPlus size={20} />}
|
||||
>
|
||||
|
@ -11,7 +11,7 @@ export const config = {
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return httpProxyMiddleware(req, res, {
|
||||
headers: {
|
||||
"X-Forwarded-For": req.socket.remoteAddress ?? "",
|
||||
"X-Forwarded-For": req.socket?.remoteAddress ?? "",
|
||||
},
|
||||
target: "http://localhost:8080",
|
||||
});
|
||||
|
@ -51,7 +51,6 @@ const ResetPassword = () => {
|
||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
console.log(resetPasswordToken);
|
||||
authService
|
||||
.resetPassword(resetPasswordToken, values.password)
|
||||
.then(() => {
|
||||
|
@ -8,11 +8,11 @@ import {
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { TbCheck } from "react-icons/tb";
|
||||
import Logo from "../components/Logo";
|
||||
import Meta from "../components/Meta";
|
||||
import useUser from "../hooks/user.hook";
|
||||
|
||||
@ -150,12 +150,7 @@ export default function Home() {
|
||||
</Group>
|
||||
</div>
|
||||
<Group className={classes.image} align="center">
|
||||
<Image
|
||||
src="/img/logo.svg"
|
||||
alt="Pingvin Share Logo"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<Logo width={200} height={200} />
|
||||
</Group>
|
||||
</div>
|
||||
</Container>
|
||||
|
@ -35,7 +35,7 @@ const Upload = ({
|
||||
const [files, setFiles] = useState<FileUpload[]>([]);
|
||||
const [isUploading, setisUploading] = useState(false);
|
||||
|
||||
maxShareSize ??= parseInt(config.get("MAX_SHARE_SIZE"));
|
||||
maxShareSize ??= parseInt(config.get("share.maxSize"));
|
||||
|
||||
const uploadFiles = async (share: CreateShare) => {
|
||||
setisUploading(true);
|
||||
@ -146,7 +146,7 @@ const Upload = ({
|
||||
.completeShare(createdShare.id)
|
||||
.then((share) => {
|
||||
setisUploading(false);
|
||||
showCompletedUploadModal(modals, share, config.get("APP_URL"));
|
||||
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
|
||||
setFiles([]);
|
||||
})
|
||||
.catch(() =>
|
||||
@ -168,9 +168,9 @@ const Upload = ({
|
||||
{
|
||||
isUserSignedIn: user ? true : false,
|
||||
isReverseShare,
|
||||
appUrl: config.get("APP_URL"),
|
||||
appUrl: config.get("general.appUrl"),
|
||||
allowUnauthenticatedShares: config.get(
|
||||
"ALLOW_UNAUTHENTICATED_SHARES"
|
||||
"share.allowUnauthenticatedShares"
|
||||
),
|
||||
enableEmailRecepients: config.get(
|
||||
"ENABLE_SHARE_EMAIL_RECIPIENTS"
|
||||
|
@ -6,8 +6,8 @@ const list = async (): Promise<Config[]> => {
|
||||
return (await api.get("/configs")).data;
|
||||
};
|
||||
|
||||
const listForAdmin = async (): Promise<AdminConfig[]> => {
|
||||
return (await api.get("/configs/admin")).data;
|
||||
const getByCategory = async (category: string): Promise<AdminConfig[]> => {
|
||||
return (await api.get(`/configs/admin/${category}`)).data;
|
||||
};
|
||||
|
||||
const updateMany = async (data: UpdateConfig[]): Promise<AdminConfig[]> => {
|
||||
@ -48,7 +48,7 @@ const isNewReleaseAvailable = async () => {
|
||||
|
||||
export default {
|
||||
list,
|
||||
listForAdmin,
|
||||
getByCategory,
|
||||
updateMany,
|
||||
get,
|
||||
finishSetup,
|
||||
|
@ -10,11 +10,11 @@ export type UpdateConfig = {
|
||||
};
|
||||
|
||||
export type AdminConfig = Config & {
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
secret: boolean;
|
||||
description: string;
|
||||
obscured: boolean;
|
||||
category: string;
|
||||
};
|
||||
|
||||
export type AdminConfigGroupedByCategory = {
|
||||
@ -29,6 +29,11 @@ export type AdminConfigGroupedByCategory = {
|
||||
];
|
||||
};
|
||||
|
||||
export type ConfigVariablesCategory = {
|
||||
category: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type ConfigHook = {
|
||||
configVariables: Config[];
|
||||
refresh: () => void;
|
||||
|
@ -1,8 +1,6 @@
|
||||
export const configVariableToFriendlyName = (variable: string) => {
|
||||
return variable
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
const splitted = variable.split(/(?=[A-Z])/).join(" ");
|
||||
return splitted.charAt(0).toUpperCase() + splitted.slice(1);
|
||||
};
|
||||
|
||||
export const capitalizeFirstLetter = (string: string) => {
|
||||
|