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
This commit is contained in:
Elias Schneider
2023-03-04 23:29:00 +01:00
committed by GitHub
parent f9840505b8
commit fddad3ef70
66 changed files with 908 additions and 623 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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))
) {

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { UserController } from "./user.controller";
import { UserSevice } from "./user.service";
@Module({
imports:[EmailModule],
imports: [EmailModule],
providers: [UserSevice],
controllers: [UserController],
})