From beece56327da141c222fd9f5259697df6db9347a Mon Sep 17 00:00:00 2001 From: iUnstable0 Date: Thu, 23 Mar 2023 14:31:21 +0700 Subject: [PATCH] feat(share, config): more variables, placeholder and reset default (#132) * More email share vars + unfinished placeolders config {desc} {expires} vars (unfinished) config placeholder vals * done * migrate * edit seed * removed comments * refactor: replace dependecy `luxon` with `moment` * update shareRecipientsMessage message * chore: remove `luxon` * fix: grammatically incorrect `shareRecipientsMessage` message * changed to defaultValue and value instead * fix: don't expose defaultValue to non admin user * fix: update default value if default value changes * refactor: set config value to null instead of a empty string * refactor: merge two migrations into one * fix value check empty --------- Co-authored-by: Elias Schneider --- .../20230319120712_edited_value/migration.sql | 23 ++++++++ backend/prisma/schema.prisma | 19 +++---- backend/prisma/seed/config.seed.ts | 54 +++++++++---------- backend/src/config/config.service.ts | 20 ++++--- backend/src/config/dto/adminConfig.dto.ts | 3 ++ backend/src/config/dto/updateConfig.dto.ts | 3 +- backend/src/email/email.service.ts | 14 ++++- backend/src/share/share.service.ts | 22 ++++---- .../admin/configuration/AdminConfigInput.tsx | 26 ++++++--- .../src/pages/admin/config/[category].tsx | 29 ++++++++-- frontend/src/services/config.service.ts | 8 +-- frontend/src/types/config.type.ts | 1 + 12 files changed, 149 insertions(+), 73 deletions(-) create mode 100644 backend/prisma/migrations/20230319120712_edited_value/migration.sql diff --git a/backend/prisma/migrations/20230319120712_edited_value/migration.sql b/backend/prisma/migrations/20230319120712_edited_value/migration.sql new file mode 100644 index 0000000..be6542e --- /dev/null +++ b/backend/prisma/migrations/20230319120712_edited_value/migration.sql @@ -0,0 +1,23 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Config" ( + "updatedAt" DATETIME NOT NULL, + "name" TEXT NOT NULL, + "category" TEXT NOT NULL, + "type" TEXT NOT NULL, + "value" TEXT, + "defaultValue" TEXT NOT NULL DEFAULT '', + "description" TEXT NOT NULL, + "obscured" BOOLEAN NOT NULL DEFAULT false, + "secret" BOOLEAN NOT NULL DEFAULT true, + "locked" BOOLEAN NOT NULL DEFAULT false, + "order" INTEGER NOT NULL, + + PRIMARY KEY ("name", "category") +); +INSERT INTO "new_Config" ("category", "description", "locked", "name", "obscured", "order", "secret", "type", "updatedAt", "value") SELECT "category", "description", "locked", "name", "obscured", "order", "secret", "type", "updatedAt", "value" FROM "Config"; +DROP TABLE "Config"; +ALTER TABLE "new_Config" RENAME TO "Config"; + +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 1d5c26b..b0baa98 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -131,15 +131,16 @@ model ShareSecurity { model Config { updatedAt DateTime @updatedAt - name String - category String - type String - value String - description String - obscured Boolean @default(false) - secret Boolean @default(true) - locked Boolean @default(false) - order Int + name String + category String + type String + defaultValue String @default("") + value String? + description String + obscured Boolean @default(false) + secret Boolean @default(true) + locked Boolean @default(false) + order Int @@id([name, category]) } diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts index 71c518a..5abc3de 100644 --- a/backend/prisma/seed/config.seed.ts +++ b/backend/prisma/seed/config.seed.ts @@ -6,7 +6,7 @@ const configVariables: ConfigVariables = { jwtSecret: { description: "Long random string used to sign JWT tokens", type: "string", - value: crypto.randomBytes(256).toString("base64"), + defaultValue: crypto.randomBytes(256).toString("base64"), locked: true, }, }, @@ -14,20 +14,20 @@ const configVariables: ConfigVariables = { appName: { description: "Name of the application", type: "string", - value: "Pingvin Share", + defaultValue: "Pingvin Share", secret: false, }, appUrl: { description: "On which URL Pingvin Share is available", type: "string", - value: "http://localhost:3000", + defaultValue: "http://localhost:3000", secret: false, }, showHomePage: { description: "Whether to show the home page", type: "boolean", - value: "true", + defaultValue: "true", secret: false, }, }, @@ -35,21 +35,21 @@ const configVariables: ConfigVariables = { allowRegistration: { description: "Whether registration is allowed", type: "boolean", - value: "true", + defaultValue: "true", secret: false, }, allowUnauthenticatedShares: { description: "Whether unauthorized users can create shares", type: "boolean", - value: "false", + defaultValue: "false", secret: false, }, maxSize: { description: "Maximum share size in bytes", type: "number", - value: "1073741824", + defaultValue: "1073741824", secret: false, }, @@ -59,7 +59,7 @@ const configVariables: ConfigVariables = { description: "Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.", type: "boolean", - value: "false", + defaultValue: "false", secret: false, }, @@ -67,53 +67,53 @@ const configVariables: ConfigVariables = { description: "Subject of the email which gets sent to the share recipients.", type: "string", - value: "Files shared with you", + defaultValue: "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.", + "Message which gets sent to the share recipients.\n\nAvailable variables:\n{creator} - The username of the creator of the share\n{shareUrl} - The URL of the share\n{desc} - The description of the share\n{expires} - The expiration date of the share\n\nVariables will be replaced with the actual values.", type: "text", - value: - "Hey!\n{creator} shared some files with you. View or download the files with this link: {shareUrl}\nShared securely with Pingvin Share 🐧", + defaultValue: + "Hey!\n\n{creator} shared some files with you, view or download the files with this link: {shareUrl}\n\nThe share will expire {expires}.\n\nNote: {desc}\n\nShared securely with Pingvin Share 🐧", }, reverseShareSubject: { 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", + defaultValue: "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 🐧", + defaultValue: + "Hey!\n\nA share was just created with your reverse share link: {shareUrl}\n\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", + defaultValue: "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 🐧", + defaultValue: + "Hey!\n\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\n\nPingvin Share 🐧", }, inviteSubject: { description: "Subject of the email which gets sent when an admin invites an user.", type: "string", - value: "Pingvin Share invite", + defaultValue: "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 🐧", + defaultValue: + "Hey!\n\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\n\nYour password is: {password}\n\nPingvin Share 🐧", }, }, smtp: { @@ -121,33 +121,33 @@ const configVariables: ConfigVariables = { 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", + defaultValue: "false", secret: false, }, host: { description: "Host of the SMTP server", type: "string", - value: "", + defaultValue: "", }, port: { description: "Port of the SMTP server", type: "number", - value: "0", + defaultValue: "0", }, email: { description: "Email address which the emails get sent from", type: "string", - value: "", + defaultValue: "", }, username: { description: "Username of the SMTP server", type: "string", - value: "", + defaultValue: "", }, password: { description: "Password of the SMTP server", type: "string", - value: "", + defaultValue: "", obscured: true, }, }, diff --git a/backend/src/config/config.service.ts b/backend/src/config/config.service.ts index 4580c81..41d5644 100644 --- a/backend/src/config/config.service.ts +++ b/backend/src/config/config.service.ts @@ -21,10 +21,12 @@ export class ConfigService { if (!configVariable) throw new Error(`Config variable ${key} not found`); - if (configVariable.type == "number") return parseInt(configVariable.value); - if (configVariable.type == "boolean") return configVariable.value == "true"; + const value = configVariable.value ?? configVariable.defaultValue; + + if (configVariable.type == "number") return parseInt(value); + if (configVariable.type == "boolean") return value == "true"; if (configVariable.type == "string" || configVariable.type == "text") - return configVariable.value; + return value; } async getByCategory(category: string) { @@ -35,8 +37,9 @@ export class ConfigService { return configVariables.map((variable) => { return { - key: `${variable.category}.${variable.name}`, ...variable, + key: `${variable.category}.${variable.name}`, + value: variable.value ?? variable.defaultValue, }; }); } @@ -48,8 +51,9 @@ export class ConfigService { return configVariables.map((variable) => { return { - key: `${variable.category}.${variable.name}`, ...variable, + key: `${variable.category}.${variable.name}`, + value: variable.value ?? variable.defaultValue, }; }); } @@ -77,7 +81,9 @@ export class ConfigService { if (!configVariable || configVariable.locked) throw new NotFoundException("Config variable not found"); - if ( + if (value == "") { + value = null; + } else if ( typeof value != configVariable.type && typeof value == "string" && configVariable.type != "text" @@ -94,7 +100,7 @@ export class ConfigService { name: key.split(".")[1], }, }, - data: { value: value.toString() }, + data: { value: value ? value.toString() : null }, }); this.configVariables = await this.prisma.config.findMany(); diff --git a/backend/src/config/dto/adminConfig.dto.ts b/backend/src/config/dto/adminConfig.dto.ts index 322df3b..3c580ef 100644 --- a/backend/src/config/dto/adminConfig.dto.ts +++ b/backend/src/config/dto/adminConfig.dto.ts @@ -8,6 +8,9 @@ export class AdminConfigDTO extends ConfigDTO { @Expose() secret: boolean; + @Expose() + defaultValue: string; + @Expose() updatedAt: Date; diff --git a/backend/src/config/dto/updateConfig.dto.ts b/backend/src/config/dto/updateConfig.dto.ts index c4b6804..40eee2b 100644 --- a/backend/src/config/dto/updateConfig.dto.ts +++ b/backend/src/config/dto/updateConfig.dto.ts @@ -1,11 +1,10 @@ -import { IsNotEmpty, IsString, ValidateIf } from "class-validator"; +import { IsNotEmpty, IsString } from "class-validator"; class UpdateConfigDTO { @IsString() key: string; @IsNotEmpty() - @ValidateIf((dto) => dto.value !== "") value: string | number | boolean; } diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts index fe07cac..3c4be93 100644 --- a/backend/src/email/email.service.ts +++ b/backend/src/email/email.service.ts @@ -4,6 +4,7 @@ import { Logger, } from "@nestjs/common"; import { User } from "@prisma/client"; +import * as moment from "moment"; import * as nodemailer from "nodemailer"; import { ConfigService } from "src/config/config.service"; @@ -43,10 +44,12 @@ export class EmailService { }); } - async sendMailToShareRecepients( + async sendMailToShareRecipients( recipientEmail: string, shareId: string, - creator?: User + creator?: User, + description?: string, + expiration?: Date ) { if (!this.config.get("email.enableShareEmailRecipients")) throw new InternalServerErrorException("Email service disabled"); @@ -61,6 +64,13 @@ export class EmailService { .replaceAll("\\n", "\n") .replaceAll("{creator}", creator?.username ?? "Someone") .replaceAll("{shareUrl}", shareUrl) + .replaceAll("{desc}", description ?? "No description") + .replaceAll( + "{expires}", + moment(expiration).unix() != 0 + ? moment(expiration).fromNow() + : "in: never" + ) ); } diff --git a/backend/src/share/share.service.ts b/backend/src/share/share.service.ts index 4a46c28..f9f3d90 100644 --- a/backend/src/share/share.service.ts +++ b/backend/src/share/share.service.ts @@ -142,12 +142,14 @@ export class ShareService { this.prisma.share.update({ where: { id }, data: { isZipReady: true } }) ); - // Send email for each recepient - for (const recepient of share.recipients) { - await this.emailService.sendMailToShareRecepients( - recepient.email, + // Send email for each recipient + for (const recipient of share.recipients) { + await this.emailService.sendMailToShareRecipients( + recipient.email, share.id, - share.creator + share.creator, + share.description, + share.expiration ); } @@ -163,7 +165,7 @@ export class ShareService { } // Check if any file is malicious with ClamAV - this.clamScanService.checkAndRemove(share.id); + void this.clamScanService.checkAndRemove(share.id); if (share.reverseShare) { await this.prisma.reverseShare.update({ @@ -172,7 +174,7 @@ export class ShareService { }); } - return await this.prisma.share.update({ + return this.prisma.share.update({ where: { id }, data: { uploadLocked: true }, }); @@ -195,14 +197,12 @@ export class ShareService { include: { recipients: true }, }); - const sharesWithEmailRecipients = shares.map((share) => { + return shares.map((share) => { return { ...share, recipients: share.recipients.map((recipients) => recipients.email), }; }); - - return sharesWithEmailRecipients; } async get(id: string): Promise { @@ -222,7 +222,7 @@ export class ShareService { throw new NotFoundException("Share not found"); return { ...share, - hasPassword: share.security?.password ? true : false, + hasPassword: !!share.security?.password, }; } diff --git a/frontend/src/components/admin/configuration/AdminConfigInput.tsx b/frontend/src/components/admin/configuration/AdminConfigInput.tsx index 3e5a33b..9cf2f8b 100644 --- a/frontend/src/components/admin/configuration/AdminConfigInput.tsx +++ b/frontend/src/components/admin/configuration/AdminConfigInput.tsx @@ -18,10 +18,13 @@ const AdminConfigInput = ({ }) => { const form = useForm({ initialValues: { - stringValue: configVariable.value, - textValue: configVariable.value, - numberValue: parseInt(configVariable.value), - booleanValue: configVariable.value == "true", + stringValue: configVariable.value ?? configVariable.defaultValue, + textValue: configVariable.value ?? configVariable.defaultValue, + numberValue: parseInt( + configVariable.value ?? configVariable.defaultValue + ), + booleanValue: + configVariable.value ?? configVariable.defaultValue == "true", }, }); @@ -35,29 +38,38 @@ const AdminConfigInput = ({ {configVariable.type == "string" && (configVariable.obscured ? ( onValueChange(configVariable, e.target.value)} /> ) : ( onValueChange(configVariable, e.target.value)} /> ))} {configVariable.type == "text" && (