1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-06-30 06:30:11 +02:00

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 <login@eliasschneider.com>
This commit is contained in:
iUnstable0 2023-03-23 14:31:21 +07:00 committed by GitHub
parent a0d1d98e24
commit beece56327
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 149 additions and 73 deletions

View File

@ -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;

View File

@ -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])
}

View File

@ -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,
},
},

View File

@ -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();

View File

@ -8,6 +8,9 @@ export class AdminConfigDTO extends ConfigDTO {
@Expose()
secret: boolean;
@Expose()
defaultValue: string;
@Expose()
updatedAt: Date;

View File

@ -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;
}

View File

@ -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"
)
);
}

View File

@ -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<any> {
@ -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,
};
}

View File

@ -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 ? (
<PasswordInput
style={{ width: "100%" }}
style={{
width: "100%",
}}
{...form.getInputProps("stringValue")}
onChange={(e) => onValueChange(configVariable, e.target.value)}
/>
) : (
<TextInput
style={{ width: "100%" }}
style={{
width: "100%",
}}
{...form.getInputProps("stringValue")}
placeholder={configVariable.defaultValue}
onChange={(e) => onValueChange(configVariable, e.target.value)}
/>
))}
{configVariable.type == "text" && (
<Textarea
style={{ width: "100%" }}
style={{
width: "100%",
}}
autosize
{...form.getInputProps("textValue")}
placeholder={configVariable.defaultValue}
onChange={(e) => onValueChange(configVariable, e.target.value)}
/>
)}
{configVariable.type == "number" && (
<NumberInput
{...form.getInputProps("numberValue")}
placeholder={configVariable.defaultValue}
onChange={(number) => onValueChange(configVariable, number)}
/>
)}

View File

@ -67,7 +67,7 @@ export default function AppShellDemo() {
toast.success("Configurations updated successfully");
})
.catch(toast.axiosError);
config.refresh();
void config.refresh();
}
};
@ -75,8 +75,12 @@ export default function AppShellDemo() {
const index = updatedConfigVariables.findIndex(
(item) => item.key === configVariable.key
);
if (index > -1) {
updatedConfigVariables[index] = configVariable;
updatedConfigVariables[index] = {
...updatedConfigVariables[index],
...configVariable,
};
} else {
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
}
@ -132,9 +136,24 @@ export default function AppShellDemo() {
<Title order={6}>
{configVariableToFriendlyName(configVariable.name)}
</Title>
<Text color="dimmed" size="sm" mb="xs">
{configVariable.description}
</Text>
{configVariable.description.split("\n").length == 1 ? (
<Text color="dimmed" size="sm" mb="xs">
{configVariable.description}
</Text>
) : (
configVariable.description.split("\n").map((line) => (
<Text
key={line}
color="dimmed"
size="sm"
style={{
marginBottom: line === "" ? "1rem" : "0",
}}
>
{line}
</Text>
))
)}
</Stack>
<Stack></Stack>
<Box style={{ width: isMobile ? "100%" : "50%" }}>

View File

@ -23,10 +23,12 @@ const get = (key: string, configVariables: Config[]): any => {
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;
};
const finishSetup = async (): Promise<AdminConfig[]> => {

View File

@ -1,5 +1,6 @@
type Config = {
key: string;
defaultValue: string;
value: string;
type: string;
};