From aab17abb198ccc620c61d627ced2bb4f3a30d68d Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Tue, 28 Feb 2023 14:24:09 +0100 Subject: [PATCH 01/11] add first concept --- backend/prisma/schema.prisma | 6 +- backend/prisma/seed/config.seed.ts | 450 +++++++++--------- backend/src/auth/auth.controller.ts | 2 +- backend/src/auth/auth.service.ts | 4 +- backend/src/auth/guard/jwt.guard.ts | 2 +- backend/src/auth/strategy/jwt.strategy.ts | 4 +- backend/src/config/config.controller.ts | 36 +- backend/src/config/config.service.ts | 65 ++- backend/src/config/dto/adminConfig.dto.ts | 5 +- backend/src/email/email.service.ts | 46 +- backend/src/file/file.service.ts | 2 +- .../reverseShare/reverseShare.controller.ts | 2 +- .../src/reverseShare/reverseShare.service.ts | 2 +- backend/src/share/share.service.ts | 6 +- frontend/src/components/Logo.tsx | 32 +- frontend/src/components/Meta.tsx | 5 +- .../admin/configuration/AdminConfigTable.tsx | 28 +- frontend/src/components/auth/SignInForm.tsx | 4 +- frontend/src/components/auth/SignUpForm.tsx | 2 +- .../{navBar => header}/ActionAvatar.tsx | 0 .../{navBar/NavBar.tsx => header/Header.tsx} | 18 +- .../{navBar => header}/NavbarShareMenu.tsx | 0 frontend/src/components/share/FileList.tsx | 6 +- frontend/src/middleware.ts | 16 +- frontend/src/pages/_app.tsx | 29 +- frontend/src/pages/account/reverseShares.tsx | 10 +- frontend/src/pages/account/shares.tsx | 6 +- frontend/src/pages/admin/config.tsx | 18 - .../src/pages/admin/config/[category].tsx | 216 +++++++++ frontend/src/pages/admin/config/index.tsx | 15 + frontend/src/pages/admin/users.tsx | 2 +- frontend/src/pages/api/[...all].tsx | 2 +- frontend/src/pages/index.tsx | 9 +- frontend/src/pages/upload/index.tsx | 8 +- frontend/src/services/config.service.ts | 18 +- frontend/src/types/config.type.ts | 7 +- frontend/src/utils/string.util.ts | 6 +- 37 files changed, 681 insertions(+), 408 deletions(-) rename frontend/src/components/{navBar => header}/ActionAvatar.tsx (100%) rename frontend/src/components/{navBar/NavBar.tsx => header/Header.tsx} (92%) rename frontend/src/components/{navBar => header}/NavbarShareMenu.tsx (100%) delete mode 100644 frontend/src/pages/admin/config.tsx create mode 100644 frontend/src/pages/admin/config/[category].tsx create mode 100644 frontend/src/pages/admin/config/index.tsx diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 00089a09b..1d5c26b6e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -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]) } diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts index 3fecd571f..41a516e38 100644 --- a/backend/prisma/seed/config.seed.ts +++ b/backend/prisma/seed/config.seed.ts @@ -1,258 +1,234 @@ 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, - }, - { - 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, +const configVariables: ConfigVariables = { + internal: { + setupStatus: { + description: "Status of the setup wizard", + type: "string", + value: "STARTED", // STARTED, REGISTERED, FINISHED + secret: false, + locked: true, + }, + jwtSecret: { + description: "Long random string used to sign JWT tokens", + type: "string", + value: crypto.randomBytes(256).toString("base64"), + locked: true, + }, + }, + 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, + }, }, - { - 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, - }, - { - order: 7, - key: "SHARE_RECEPIENTS_EMAIL_SUBJECT", - description: - "Subject of the email which gets sent to the share recipients.", - type: "string", - value: "Files shared with you", - category: "email", - }, - { - order: 8, - key: "SHARE_RECEPIENTS_EMAIL_MESSAGE", - description: - "Message which gets sent to the share recipients. {creator} and {shareUrl} will be replaced with the creator's name and the share URL.", - type: "text", - value: - "Hey!\n{creator} shared some files with you. View or download the files with this link: {shareUrl}\nShared securely with Pingvin Share 🐧", - category: "email", - }, - { - order: 9, - key: "REVERSE_SHARE_EMAIL_SUBJECT", - description: - "Subject of the email which gets sent when someone created a share with your reverse share link.", - type: "string", - value: "Reverse share link used", - category: "email", - }, - { - order: 10, - key: "REVERSE_SHARE_EMAIL_MESSAGE", - description: - "Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.", - type: "text", - value: - "Hey!\nA share was just created with your reverse share link: {shareUrl}\nShared securely with Pingvin Share 🐧", - category: "email", - }, - { - order: 11, - key: "RESET_PASSWORD_EMAIL_SUBJECT", - description: - "Subject of the email which gets sent when a user requests a password reset.", - type: "string", - value: "Pingvin Share password reset", - category: "email", - }, - { - order: 12, - key: "RESET_PASSWORD_EMAIL_MESSAGE", - description: - "Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.", - type: "text", - value: - "Hey!\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\nPingvin Share 🐧", - category: "email", - }, - { - 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", - }, -]; + share: { + allowRegistration: { + description: "Whether registration is allowed", + type: "boolean", + value: "true", + + 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, + }, + }, + email: { + enableShareEmailRecipients: { + description: + "Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.", + type: "boolean", + value: "false", + + secret: false, + }, + shareRecipientsEmailSubject: { + description: + "Subject of the email which gets sent to the share recipients.", + type: "string", + value: "Files shared with you", + }, + shareRecipientsEmailMessage: { + 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 🐧", + }, + reverseShareEmailSubject: { + 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", + }, + reverseShareEmailMessage: { + 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 🐧", + }, + resetPasswordEmailSubject: { + description: + "Subject of the email which gets sent when a user requests a password reset.", + type: "string", + value: "Pingvin Share password reset", + }, + resetPasswordEmailMessage: { + 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 🐧", + }, + inviteEmailSubject: { + description: + "Subject of the email which gets sent when an admin invites an user.", + type: "string", + value: "Pingvin Share invite", + }, + inviteEmailMessage: { + description: + "Message which gets sent when an admin invites an user. {url} will be replaced with the invite URL and {password} with the password.", + type: "text", + value: + "Hey!\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\nYour password is: {password}\nPingvin Share 🐧", + }, + }, + smtp: { + 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, + }, + }, +}; + +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, + 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(); // Delete the config variable if it doesn't exist anymore - for (const configVariableFromDatabase of configVariablesFromDatabase) { - const configVariable = configVariables.find( - (v) => v.key == configVariableFromDatabase.key - ); - if (!configVariable) { - await prisma.config.delete({ - where: { key: configVariableFromDatabase.key }, - }); + // for (const configVariableFromDatabase of configVariablesFromDatabase) { + // const configVariable = configVariables.find( + // (v) => v.key == configVariableFromDatabase.key + // ); + // if (!configVariable) { + // await prisma.config.delete({ + // where: { key: configVariableFromDatabase.key }, + // }); - // Update the config variable if the metadata changed - } else if ( - JSON.stringify({ - ...configVariable, - key: configVariableFromDatabase.key, - value: configVariableFromDatabase.value, - }) != JSON.stringify(configVariableFromDatabase) - ) { - await prisma.config.update({ - where: { key: configVariableFromDatabase.key }, - data: { - ...configVariable, - key: configVariableFromDatabase.key, - value: configVariableFromDatabase.value, - }, - }); - } - } + // // Update the config variable if the metadata changed + // } else if ( + // JSON.stringify({ + // ...configVariable, + // key: configVariableFromDatabase.key, + // value: configVariableFromDatabase.value, + // }) != JSON.stringify(configVariableFromDatabase) + // ) { + // await prisma.config.update({ + // where: { key: configVariableFromDatabase.key }, + // data: { + // ...configVariable, + // key: configVariableFromDatabase.key, + // value: configVariableFromDatabase.value, + // }, + // }); + // } + // } } main() .then(async () => { diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 2a9d56221..bb9c10d91 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -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); diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 7ddbcf3ef..c95e47582 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -25,7 +25,7 @@ export class AuthService { ) {} async signUp(dto: AuthRegisterDTO) { - const isFirstUser = this.config.get("SETUP_STATUS") == "STARTED"; + const isFirstUser = this.config.get("internal.setupStatus") == "STARTED"; const hash = await argon.hash(dto.password); try { @@ -161,7 +161,7 @@ export class AuthService { }, { expiresIn: "15min", - secret: this.config.get("JWT_SECRET"), + secret: this.config.get("internal.jwtSecret"), } ); } diff --git a/backend/src/auth/guard/jwt.guard.ts b/backend/src/auth/guard/jwt.guard.ts index 39ecd1319..7db8f092e 100644 --- a/backend/src/auth/guard/jwt.guard.ts +++ b/backend/src/auth/guard/jwt.guard.ts @@ -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"); } } } diff --git a/backend/src/auth/strategy/jwt.strategy.ts b/backend/src/auth/strategy/jwt.strategy.ts index 5ed085a37..b167af085 100644 --- a/backend/src/auth/strategy/jwt.strategy.ts +++ b/backend/src/auth/strategy/jwt.strategy.ts @@ -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"), }); } diff --git a/backend/src/config/config.controller.ts b/backend/src/config/config.controller.ts index e7488731c..275eb1fe9 100644 --- a/backend/src/config/config.controller.ts +++ b/backend/src/config/config.controller.ts @@ -1,5 +1,16 @@ -import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Res, + StreamableFile, + UseGuards, +} from "@nestjs/common"; import { SkipThrottle } from "@nestjs/throttler"; +import { Response } from "express"; import { AdministratorGuard } from "src/auth/guard/isAdmin.guard"; import { JwtGuard } from "src/auth/guard/jwt.guard"; import { EmailService } from "src/email/email.service"; @@ -22,14 +33,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) ); } + @Get("admin/categories") + @UseGuards(JwtGuard, AdministratorGuard) + async getCategories() { + return await this.configService.getCategories(); + } + @Patch("admin") @UseGuards(JwtGuard, AdministratorGuard) async updateMany(@Body() data: UpdateConfigDTO[]) { @@ -47,4 +64,15 @@ export class ConfigController { async testEmail(@Body() { email }: TestEmailDTO) { await this.emailService.sendTestMail(email); } + + @Get("logo") + @SkipThrottle() + async getLogo(@Res({ passthrough: true }) res: Response) { + res.set({ + "Content-Type": "image/png", + "Content-Disposition": "inline; filename=logo.png", + }); + + return new StreamableFile(this.configService.getLogo()); + } } diff --git a/backend/src/config/config.service.ts b/backend/src/config/config.service.ts index 150f87721..363db0bd5 100644 --- a/backend/src/config/config.service.ts +++ b/backend/src/config/config.service.ts @@ -5,6 +5,8 @@ import { NotFoundException, } from "@nestjs/common"; import { Config } from "@prisma/client"; +import * as fs from "fs"; +import { ConfigVariables } from "prisma/seed/config.seed"; import { PrismaService } from "src/prisma/prisma.service"; @Injectable() @@ -14,9 +16,9 @@ export class ConfigService { private prisma: PrismaService ) {} - get(key: string): any { + get(key: ConfigVariables): 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,17 +29,46 @@ 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: { category, locked: { equals: false } }, + }); + + return configVariables.map((variable) => { + return { + key: `${variable.category}.${variable.name}`, + ...variable, + }; + }); + } + + async getCategories() { + const categories = await this.prisma.config.groupBy({ + by: ["category"], where: { locked: { equals: false } }, + _count: { category: true }, + }); + + return categories.map((category) => { + return { + category: category.category, + count: category._count.category, + }; }); } 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 }[]) { @@ -50,7 +81,12 @@ export class ConfigService { 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 +103,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() }, }); @@ -78,7 +119,12 @@ export class ConfigService { async changeSetupStatus(status: "STARTED" | "REGISTERED" | "FINISHED") { const updatedVariable = await this.prisma.config.update({ - where: { key: "SETUP_STATUS" }, + where: { + name_category: { + category: "internal", + name: "setupStatus", + }, + }, data: { value: status }, }); @@ -86,4 +132,7 @@ export class ConfigService { return updatedVariable; } + getLogo() { + return fs.createReadStream(`./data/branding/logo.png`); + } } diff --git a/backend/src/config/dto/adminConfig.dto.ts b/backend/src/config/dto/adminConfig.dto.ts index dcb2491f7..b008ba0dc 100644 --- a/backend/src/config/dto/adminConfig.dto.ts +++ b/backend/src/config/dto/adminConfig.dto.ts @@ -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,8 +17,6 @@ export class AdminConfigDTO extends ConfigDTO { @Expose() obscured: boolean; - @Expose() - category: string; from(partial: Partial) { return plainToClass(AdminConfigDTO, partial, { diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts index ff89a442e..147c34f3c 100644 --- a/backend/src/email/email.service.ts +++ b/backend/src/email/email.service.ts @@ -8,16 +8,19 @@ 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, + from: `"${this.config.get("general.appName")}" <${this.config.get( + "smtp.email" + )}>`, + 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 +30,16 @@ 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")}>`, to: recipientEmail, - subject: this.config.get("SHARE_RECEPIENTS_EMAIL_SUBJECT"), + subject: this.config.get("email.shareRecipientsEmailSubject"), text: this.config - .get("SHARE_RECEPIENTS_EMAIL_MESSAGE") + .get("email.shareRecipientsEmailMessage") .replaceAll("\\n", "\n") .replaceAll("{creator}", creator?.username ?? "Someone") .replaceAll("{shareUrl}", shareUrl), @@ -45,14 +47,13 @@ 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")}>`, to: recipientEmail, - subject: this.config.get("REVERSE_SHARE_EMAIL_SUBJECT"), + subject: this.config.get("email.reverseShareEmailSubject"), text: this.config - .get("REVERSE_SHARE_EMAIL_MESSAGE") + .get("email.reverseShareEmailMessage") .replaceAll("\\n", "\n") .replaceAll("{shareUrl}", shareUrl), }); @@ -60,28 +61,26 @@ 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")}>`, to: recipientEmail, - subject: this.config.get("RESET_PASSWORD_EMAIL_SUBJECT"), + subject: this.config.get("email.resetPasswordEmailSubject"), text: this.config - .get("RESET_PASSWORD_EMAIL_MESSAGE") + .get("email.resetPasswordEmailMessage") .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")}>`, to: recipientEmail, - subject: this.config.get("INVITE_EMAIL_SUBJECT"), + subject: this.config.get("email.inviteEmailSubject"), text: this.config - .get("INVITE_EMAIL_MESSAGE") + .get("email.inviteEmailMessage") .replaceAll("{url}", loginUrl) .replaceAll("{password}", password), }); @@ -90,7 +89,6 @@ export class EmailService { async sendTestMail(recipientEmail: string) { try { await this.getTransporter().sendMail({ - from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`, to: recipientEmail, subject: "Test email", text: "This is a test email", diff --git a/backend/src/file/file.service.ts b/backend/src/file/file.service.ts index 0d7788187..be0cddc43 100644 --- a/backend/src/file/file.service.ts +++ b/backend/src/file/file.service.ts @@ -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)) ) { diff --git a/backend/src/reverseShare/reverseShare.controller.ts b/backend/src/reverseShare/reverseShare.controller.ts index 9bc67f398..32afcaf42 100644 --- a/backend/src/reverseShare/reverseShare.controller.ts +++ b/backend/src/reverseShare/reverseShare.controller.ts @@ -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 }; } diff --git a/backend/src/reverseShare/reverseShare.service.ts b/backend/src/reverseShare/reverseShare.service.ts index 10a11d5de..156f6ed00 100644 --- a/backend/src/reverseShare/reverseShare.service.ts +++ b/backend/src/reverseShare/reverseShare.service.ts @@ -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( diff --git a/backend/src/share/share.service.ts b/backend/src/share/share.service.ts index 36bb476a9..321e15597 100644 --- a/backend/src/share/share.service.ts +++ b/backend/src/share/share.service.ts @@ -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), }); diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index 7fea40e31..91a3f4bf5 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -1,34 +1,8 @@ +import Image from "next/image"; + const Logo = ({ height, width }: { height: number; width: number }) => { return ( - - - - - - - - - - + logo ); }; export default Logo; diff --git a/frontend/src/components/Meta.tsx b/frontend/src/components/Meta.tsx index da5c8907b..144e904bf 100644 --- a/frontend/src/components/Meta.tsx +++ b/frontend/src/components/Meta.tsx @@ -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 ( diff --git a/frontend/src/components/admin/configuration/AdminConfigTable.tsx b/frontend/src/components/admin/configuration/AdminConfigTable.tsx index 1a2acced8..b5d5fa0df 100644 --- a/frontend/src/components/admin/configuration/AdminConfigTable.tsx +++ b/frontend/src/components/admin/configuration/AdminConfigTable.tsx @@ -36,7 +36,7 @@ const AdminConfigTable = () => { >([]); useEffect(() => { - if (config.get("SETUP_STATUS") != "FINISHED") { + if (config.get("internal.setupStatus") != "FINISHED") { config.refresh(); } }, []); @@ -56,22 +56,22 @@ const AdminConfigTable = () => { useState({}); 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); - }); + // 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") { + if (config.get("internal.setupStatus") == "REGISTERED") { await configService .updateMany(updatedConfigVariables) .then(async () => { diff --git a/frontend/src/components/auth/SignInForm.tsx b/frontend/src/components/auth/SignInForm.tsx index 4e0791cc2..43293b181 100644 --- a/frontend/src/components/auth/SignInForm.tsx +++ b/frontend/src/components/auth/SignInForm.tsx @@ -95,7 +95,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => { Welcome back - {config.get("ALLOW_REGISTRATION") && ( + {config.get("share.allowRegistration") && ( You don't have an account yet?{" "} @@ -131,7 +131,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => { {...form.getInputProps("totp")} /> )} - {config.get("SMTP_ENABLED") && ( + {config.get("smtp.enabled") && ( Forgot password? diff --git a/frontend/src/components/auth/SignUpForm.tsx b/frontend/src/components/auth/SignUpForm.tsx index 5b65827bd..a974bf512 100644 --- a/frontend/src/components/auth/SignUpForm.tsx +++ b/frontend/src/components/auth/SignUpForm.tsx @@ -52,7 +52,7 @@ const SignUpForm = () => { Sign up - {config.get("ALLOW_REGISTRATION") && ( + {config.get("share.allowRegistration") && ( You have an account already?{" "} diff --git a/frontend/src/components/navBar/ActionAvatar.tsx b/frontend/src/components/header/ActionAvatar.tsx similarity index 100% rename from frontend/src/components/navBar/ActionAvatar.tsx rename to frontend/src/components/header/ActionAvatar.tsx diff --git a/frontend/src/components/navBar/NavBar.tsx b/frontend/src/components/header/Header.tsx similarity index 92% rename from frontend/src/components/navBar/NavBar.tsx rename to frontend/src/components/header/Header.tsx index 188bdb02b..cc5ae1af0 100644 --- a/frontend/src/components/navBar/NavBar.tsx +++ b/frontend/src/components/header/Header.tsx @@ -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 ( -
+ - Pingvin Share + {config.get("general.appName")} @@ -212,8 +212,8 @@ const NavBar = () => { )} -
+ ); }; -export default NavBar; +export default Header; diff --git a/frontend/src/components/navBar/NavbarShareMenu.tsx b/frontend/src/components/header/NavbarShareMenu.tsx similarity index 100% rename from frontend/src/components/navBar/NavbarShareMenu.tsx rename to frontend/src/components/header/NavbarShareMenu.tsx diff --git a/frontend/src/components/share/FileList.tsx b/frontend/src/components/share/FileList.tsx index b6014ae41..b822f6aa0 100644 --- a/frontend/src/components/share/FileList.tsx +++ b/frontend/src/components/share/FileList.tsx @@ -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); diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index 844c3025f..cf5377f26 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -45,19 +45,19 @@ 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") { + if (getConfig("internal.setupStatus") == "FINISHED") { routes.disabled.routes.push("/admin/setup"); } @@ -70,16 +70,16 @@ export async function middleware(request: NextRequest) { }, // Setup status { - condition: getConfig("SETUP_STATUS") == "STARTED" && route != "/auth/signUp", + condition: getConfig("internal.setupStatus") == "STARTED" && route != "/auth/signUp", path: "/auth/signUp", }, { - condition: getConfig("SETUP_STATUS") == "REGISTERED" && !routes.setupStatusRegistered.contains(route) && user?.isAdmin, + condition: getConfig("internal.setupStatus") == "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 +98,7 @@ export async function middleware(request: NextRequest) { }, // Home page { - condition: (!getConfig("SHOW_HOME_PAGE") || user) && route == "/", + condition: (!getConfig("general.showHomePage") || user) && route == "/", path: "/upload", }, ]; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 8a4c551ca..198f4f7fd 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -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(systemTheme); const preferences = usePreferences(); const [user, setUser] = useState(pageProps.user); + const [route, setRoute] = useState(pageProps.route); const [configVariables, setConfigVariables] = useState( 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) { }, }} > -
- + {excludeDefaultLayoutRoutes.includes(route) ? ( - + ) : ( + <> +
+ + + + + )} @@ -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 }; diff --git a/frontend/src/pages/account/reverseShares.tsx b/frontend/src/pages/account/reverseShares.tsx index 14829dc57..6d8541fc4 100644 --- a/frontend/src/pages/account/reverseShares.tsx +++ b/frontend/src/pages/account/reverseShares.tsx @@ -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") ); } }} diff --git a/frontend/src/pages/account/shares.tsx b/frontend/src/pages/account/shares.tsx index 8a00807e8..abd4d446c 100644 --- a/frontend/src/pages/account/shares.tsx +++ b/frontend/src/pages/account/shares.tsx @@ -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") ); } }} diff --git a/frontend/src/pages/admin/config.tsx b/frontend/src/pages/admin/config.tsx deleted file mode 100644 index e92613b03..000000000 --- a/frontend/src/pages/admin/config.tsx +++ /dev/null @@ -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 ( - <> - - - Configuration - - - - - ); -}; - -export default AdminConfig; diff --git a/frontend/src/pages/admin/config/[category].tsx b/frontend/src/pages/admin/config/[category].tsx new file mode 100644 index 000000000..1a4d54e5f --- /dev/null +++ b/frontend/src/pages/admin/config/[category].tsx @@ -0,0 +1,216 @@ +import { + AppShell, + Box, + Burger, + Button, + Container, + createStyles, + Group, + Header, + MediaQuery, + Navbar, + Space, + Stack, + Text, + ThemeIcon, + Title, + useMantineTheme, +} from "@mantine/core"; +import { useMediaQuery } from "@mantine/hooks"; +import Link from "next/link"; + +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { TbAt, TbColorSwatch, TbMail, TbShare, TbSquare } from "react-icons/tb"; +import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput"; +import Logo from "../../../components/Logo"; +import useConfig from "../../../hooks/config.hook"; +import configService from "../../../services/config.service"; +import { AdminConfig, UpdateConfig } from "../../../types/config.type"; +import { configVariableToFriendlyName } from "../../../utils/string.util"; +import toast from "../../../utils/toast.util"; + +const categories = [ + { name: "Branding", icon: }, + { name: "General", icon: }, + { name: "Email", icon: }, + { name: "Share", icon: }, + { name: "SMTP", icon: }, +]; + +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, + }, +})); + +export default function AppShellDemo() { + const theme = useMantineTheme(); + const router = useRouter(); + const { classes } = useStyles(); + const [opened, setOpened] = useState(false); + const isMobile = useMediaQuery("(max-width: 560px)"); + const config = useConfig(); + + const { category: categoryId } = router.query; + + const [configVariables, setConfigVariables] = useState([]); + const [updatedConfigVariables, setUpdatedConfigVariables] = useState< + UpdateConfig[] + >([]); + + const saveConfigVariables = async () => { + if (config.get("internal.setupStatus") == "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(); + }; + + 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 as string) + .then((configVariables) => { + setConfigVariables(configVariables); + }); + }, [categoryId]); + + return ( + + ); +} diff --git a/frontend/src/pages/admin/config/index.tsx b/frontend/src/pages/admin/config/index.tsx new file mode 100644 index 000000000..acd7ec790 --- /dev/null +++ b/frontend/src/pages/admin/config/index.tsx @@ -0,0 +1,15 @@ +export function getServerSideProps() { + return { + redirect: { + permanent: false, + destination: "/admin/config/branding", + }, + props: {}, + }; +} + +const Config = () => { + return null; +}; + +export default Config; diff --git a/frontend/src/pages/admin/users.tsx b/frontend/src/pages/admin/users.tsx index 017269ead..3a673da21 100644 --- a/frontend/src/pages/admin/users.tsx +++ b/frontend/src/pages/admin/users.tsx @@ -58,7 +58,7 @@ const Users = () => { - - - ); -}; - -export default AdminConfigTable; diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index cf5377f26..7a3a5ffc2 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -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([]), }; @@ -57,25 +56,12 @@ export async function middleware(request: NextRequest) { routes.disabled.routes.push("/auth/resetPassword*"); } - if (getConfig("internal.setupStatus") == "FINISHED") { - routes.disabled.routes.push("/admin/setup"); - } - // prettier-ignore const rules = [ // Disabled routes { condition: routes.disabled.contains(route), path: "/", - }, - // Setup status - { - condition: getConfig("internal.setupStatus") == "STARTED" && route != "/auth/signUp", - path: "/auth/signUp", - }, - { - condition: getConfig("internal.setupStatus") == "REGISTERED" && !routes.setupStatusRegistered.contains(route) && user?.isAdmin, - path: "/admin/setup", }, // Authenticated state { diff --git a/frontend/src/pages/admin/config/[category].tsx b/frontend/src/pages/admin/config/[category].tsx index 1a4d54e5f..14158e821 100644 --- a/frontend/src/pages/admin/config/[category].tsx +++ b/frontend/src/pages/admin/config/[category].tsx @@ -21,7 +21,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import { TbAt, TbColorSwatch, TbMail, TbShare, TbSquare } from "react-icons/tb"; +import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb"; import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput"; import Logo from "../../../components/Logo"; import useConfig from "../../../hooks/config.hook"; @@ -31,7 +31,6 @@ import { configVariableToFriendlyName } from "../../../utils/string.util"; import toast from "../../../utils/toast.util"; const categories = [ - { name: "Branding", icon: }, { name: "General", icon: }, { name: "Email", icon: }, { name: "Share", icon: }, @@ -68,23 +67,13 @@ export default function AppShellDemo() { >([]); const saveConfigVariables = async () => { - if (config.get("internal.setupStatus") == "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); - } + await configService + .updateMany(updatedConfigVariables) + .then(() => { + setUpdatedConfigVariables([]); + toast.success("Configurations updated successfully"); + }) + .catch(toast.axiosError); config.refresh(); }; @@ -209,7 +198,11 @@ export default function AppShellDemo() { ))} - + + + ); diff --git a/frontend/src/pages/admin/config/index.tsx b/frontend/src/pages/admin/config/index.tsx index acd7ec790..5de1d44af 100644 --- a/frontend/src/pages/admin/config/index.tsx +++ b/frontend/src/pages/admin/config/index.tsx @@ -2,7 +2,7 @@ export function getServerSideProps() { return { redirect: { permanent: false, - destination: "/admin/config/branding", + destination: "/admin/config/general", }, props: {}, }; diff --git a/frontend/src/pages/admin/intro.tsx b/frontend/src/pages/admin/intro.tsx new file mode 100644 index 000000000..2b598ae5e --- /dev/null +++ b/frontend/src/pages/admin/intro.tsx @@ -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 ( + <> + + + +
+ +
+
+ Welcome to Pingvin Share +
+ + If you enjoy Pingvin Share please ⭐️ it on{" "} + + GitHub + {" "} + or{" "} + + buy me a coffee + {" "} + if you want to support my work. + + Enough talked, have fun with Pingvin Share! + How to you want to continue? + + + + +
+
+ + ); +}; + +export default Intro; diff --git a/frontend/src/pages/admin/setup.tsx b/frontend/src/pages/admin/setup.tsx deleted file mode 100644 index e61eeb6e2..000000000 --- a/frontend/src/pages/admin/setup.tsx +++ /dev/null @@ -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 ( - <> - - - - Welcome to Pingvin Share - Let's customize Pingvin Share for you! - - - - - - ); -}; - -export default Setup; From 9320c193a053e4b171f4c218286c4a3cffe6160f Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Tue, 28 Feb 2023 20:50:47 +0100 Subject: [PATCH 03/11] split config page in multiple components --- .../configuration/ConfigurationHeader.tsx | 54 +++++ .../configuration/ConfigurationNavBar.tsx | 97 +++++++++ .../src/pages/admin/config/[category].tsx | 185 ++++++------------ 3 files changed, 209 insertions(+), 127 deletions(-) create mode 100644 frontend/src/components/admin/configuration/ConfigurationHeader.tsx create mode 100644 frontend/src/components/admin/configuration/ConfigurationNavBar.tsx diff --git a/frontend/src/components/admin/configuration/ConfigurationHeader.tsx b/frontend/src/components/admin/configuration/ConfigurationHeader.tsx new file mode 100644 index 000000000..177f986ca --- /dev/null +++ b/frontend/src/components/admin/configuration/ConfigurationHeader.tsx @@ -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>; +}) => { + const config = useConfig(); + const theme = useMantineTheme(); + return ( +
+
+ + setIsMobileNavBarOpened((o) => !o)} + size="sm" + color={theme.colors.gray[6]} + mr="xl" + /> + + + + + + {config.get("general.appName")} + + + + + + +
+
+ ); +}; + +export default ConfigurationHeader; diff --git a/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx b/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx new file mode 100644 index 000000000..904ccee9e --- /dev/null +++ b/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx @@ -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: }, + { name: "Email", icon: }, + { name: "Share", icon: }, + { name: "SMTP", icon: }, +]; + +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>; +}) => { + const { classes } = useStyles(); + return ( + + ); +}; + +export default ConfigurationNavBar; diff --git a/frontend/src/pages/admin/config/[category].tsx b/frontend/src/pages/admin/config/[category].tsx index 14158e821..259ff3f06 100644 --- a/frontend/src/pages/admin/config/[category].tsx +++ b/frontend/src/pages/admin/config/[category].tsx @@ -1,65 +1,40 @@ import { AppShell, Box, - Burger, Button, Container, - createStyles, Group, - Header, - MediaQuery, - Navbar, - Space, Stack, Text, - ThemeIcon, Title, useMantineTheme, } from "@mantine/core"; import { useMediaQuery } from "@mantine/hooks"; -import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb"; import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput"; -import Logo from "../../../components/Logo"; +import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader"; +import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar"; +import CenterLoader from "../../../components/core/CenterLoader"; import useConfig from "../../../hooks/config.hook"; import configService from "../../../services/config.service"; import { AdminConfig, UpdateConfig } from "../../../types/config.type"; -import { configVariableToFriendlyName } from "../../../utils/string.util"; +import { + capitalizeFirstLetter, + configVariableToFriendlyName, +} from "../../../utils/string.util"; import toast from "../../../utils/toast.util"; -const categories = [ - { name: "General", icon: }, - { name: "Email", icon: }, - { name: "Share", icon: }, - { name: "SMTP", icon: }, -]; - -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, - }, -})); - export default function AppShellDemo() { const theme = useMantineTheme(); const router = useRouter(); - const { classes } = useStyles(); - const [opened, setOpened] = useState(false); + + const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false); const isMobile = useMediaQuery("(max-width: 560px)"); const config = useConfig(); - const { category: categoryId } = router.query; + const categoryId = router.query.category as string; const [configVariables, setConfigVariables] = useState([]); const [updatedConfigVariables, setUpdatedConfigVariables] = useState< @@ -89,11 +64,9 @@ export default function AppShellDemo() { }; useEffect(() => { - configService - .getByCategory(categoryId as string) - .then((configVariables) => { - setConfigVariables(configVariables); - }); + configService.getByCategory(categoryId).then((configVariables) => { + setConfigVariables(configVariables); + }); }, [categoryId]); return ( @@ -108,101 +81,59 @@ export default function AppShellDemo() { }} navbarOffsetBreakpoint="sm" navbar={ - + } header={ -
-
- - setOpened((o) => !o)} - size="sm" - color={theme.colors.gray[6]} - mr="xl" - /> - - - - - - {config.get("general.appName")} - - - - -
-
+ } > - {configVariables.map((configVariable) => ( + {!configVariables ? ( + + ) : ( <> - - - - {configVariableToFriendlyName(configVariable.name)} - - - {configVariable.description} - - - - - - + + + {capitalizeFirstLetter(categoryId)} + + {configVariables.map((configVariable) => ( + + + + {configVariableToFriendlyName(configVariable.name)} + + + {configVariable.description} + + + + + + + + ))} + + + - - - ))} - - - + )} ); From 4674208ddc7241b10aa898cad8ce6a96f4f02d24 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Tue, 28 Feb 2023 21:03:24 +0100 Subject: [PATCH 04/11] add custom branding docs --- README.md | 10 ++ frontend/src/components/auth/SignUpForm.tsx | 8 +- .../src/pages/admin/config/[category].tsx | 136 +++++++++--------- 3 files changed, 86 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 1f28b2248..b4540bc2f 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,16 @@ 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` setting. + +#### Logo + +You can change the logo of the app by replacing the `logo.png` in the `/data` (or with the standalone installation `/backend/data`) folder with your own logo. + ## 🖤 Contribute You're very welcome to contribute to Pingvin Share! Follow the [contribution guide](/CONTRIBUTING.md) to get started. diff --git a/frontend/src/components/auth/SignUpForm.tsx b/frontend/src/components/auth/SignUpForm.tsx index a974bf512..a629354d5 100644 --- a/frontend/src/components/auth/SignUpForm.tsx +++ b/frontend/src/components/auth/SignUpForm.tsx @@ -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); }; diff --git a/frontend/src/pages/admin/config/[category].tsx b/frontend/src/pages/admin/config/[category].tsx index 259ff3f06..ee601dc4a 100644 --- a/frontend/src/pages/admin/config/[category].tsx +++ b/frontend/src/pages/admin/config/[category].tsx @@ -17,6 +17,7 @@ import AdminConfigInput from "../../../components/admin/configuration/AdminConfi import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader"; import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar"; 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"; @@ -70,71 +71,74 @@ export default function AppShellDemo() { }, [categoryId]); return ( - - } - header={ - - } - > - - {!configVariables ? ( - - ) : ( - <> - - - {capitalizeFirstLetter(categoryId)} - - {configVariables.map((configVariable) => ( - - - - {configVariableToFriendlyName(configVariable.name)} - - - {configVariable.description} - - - - - - - - ))} - - - - - - )} - - + <> + + + } + header={ + + } + > + + {!configVariables ? ( + + ) : ( + <> + + + {capitalizeFirstLetter(categoryId)} + + {configVariables.map((configVariable) => ( + + + + {configVariableToFriendlyName(configVariable.name)} + + + {configVariable.description} + + + + + + + + ))} + + + + + + )} + + + ); } From 7946a3e8184f89925ce29a95f9e32fabb2a977a0 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Tue, 28 Feb 2023 21:11:28 +0100 Subject: [PATCH 05/11] add test email button --- frontend/src/pages/admin/config/[category].tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/admin/config/[category].tsx b/frontend/src/pages/admin/config/[category].tsx index ee601dc4a..e4e243a1a 100644 --- a/frontend/src/pages/admin/config/[category].tsx +++ b/frontend/src/pages/admin/config/[category].tsx @@ -16,6 +16,7 @@ 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"; @@ -37,7 +38,7 @@ export default function AppShellDemo() { const categoryId = router.query.category as string; - const [configVariables, setConfigVariables] = useState([]); + const [configVariables, setConfigVariables] = useState(); const [updatedConfigVariables, setUpdatedConfigVariables] = useState< UpdateConfig[] >([]); @@ -82,7 +83,6 @@ export default function AppShellDemo() { : theme.colors.gray[0], }, }} - navbarOffsetBreakpoint="sm" navbar={ ))} - - + + {categoryId == "email" && ( + + )} + )} From c4fa2d5aa4358a58b0eb777d197989d686e4a5c8 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Fri, 3 Mar 2023 11:53:25 +0100 Subject: [PATCH 06/11] fix invalid email from header --- backend/src/email/email.service.ts | 18 +++++++++++++++--- frontend/src/pages/admin/config/[category].tsx | 2 +- .../resetPassword/[resetPasswordToken].tsx | 1 - 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts index 4a215584a..e192df780 100644 --- a/backend/src/email/email.service.ts +++ b/backend/src/email/email.service.ts @@ -12,9 +12,6 @@ export class EmailService { throw new InternalServerErrorException("SMTP is disabled"); return nodemailer.createTransport({ - from: `"${this.config.get("general.appName")}" <${this.config.get( - "smtp.email" - )}>`, host: this.config.get("smtp.host"), port: this.config.get("smtp.port"), secure: this.config.get("smtp.port") == 465, @@ -36,6 +33,9 @@ export class EmailService { const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`; await this.getTransporter().sendMail({ + from: `"${this.config.get("general.appName")}" <${this.config.get( + "smtp.email" + )}>`, to: recipientEmail, subject: this.config.get("email.shareRecipientsSubject"), text: this.config @@ -50,6 +50,9 @@ export class EmailService { const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`; await this.getTransporter().sendMail({ + from: `"${this.config.get("general.appName")}" <${this.config.get( + "smtp.email" + )}>`, to: recipientEmail, subject: this.config.get("email.reverseShareSubject"), text: this.config @@ -65,6 +68,9 @@ export class EmailService { )}/auth/resetPassword/${token}`; await this.getTransporter().sendMail({ + from: `"${this.config.get("general.appName")}" <${this.config.get( + "smtp.email" + )}>`, to: recipientEmail, subject: this.config.get("email.resetPasswordSubject"), text: this.config @@ -77,6 +83,9 @@ export class EmailService { const loginUrl = `${this.config.get("general.appUrl")}/auth/signIn`; await this.getTransporter().sendMail({ + from: `"${this.config.get("general.appName")}" <${this.config.get( + "smtp.email" + )}>`, to: recipientEmail, subject: this.config.get("email.inviteSubject"), text: this.config @@ -89,6 +98,9 @@ export class EmailService { async sendTestMail(recipientEmail: string) { try { await this.getTransporter().sendMail({ + from: `"${this.config.get("general.appName")}" <${this.config.get( + "smtp.email" + )}>`, to: recipientEmail, subject: "Test email", text: "This is a test email", diff --git a/frontend/src/pages/admin/config/[category].tsx b/frontend/src/pages/admin/config/[category].tsx index e4e243a1a..b66b85fba 100644 --- a/frontend/src/pages/admin/config/[category].tsx +++ b/frontend/src/pages/admin/config/[category].tsx @@ -131,7 +131,7 @@ export default function AppShellDemo() { ))} - {categoryId == "email" && ( + {categoryId == "smtp" && ( {
{ - console.log(resetPasswordToken); authService .resetPassword(resetPasswordToken, values.password) .then(() => { From 31a789bcb6acf35767eed20ee7299483eb14ae75 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Fri, 3 Mar 2023 14:55:42 +0100 Subject: [PATCH 07/11] add migration --- .../20230303091601_v0_11_0/migration.sql | 94 +++++++++++++++++++ backend/prisma/seed/config.seed.ts | 77 +++++++++------ .../configuration/ConfigurationHeader.tsx | 2 +- 3 files changed, 141 insertions(+), 32 deletions(-) create mode 100644 backend/prisma/migrations/20230303091601_v0_11_0/migration.sql diff --git a/backend/prisma/migrations/20230303091601_v0_11_0/migration.sql b/backend/prisma/migrations/20230303091601_v0_11_0/migration.sql new file mode 100644 index 000000000..e326a0a65 --- /dev/null +++ b/backend/prisma/migrations/20230303091601_v0_11_0/migration.sql @@ -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; diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts index b38052ef7..71c518a42 100644 --- a/backend/prisma/seed/config.seed.ts +++ b/backend/prisma/seed/config.seed.ts @@ -31,7 +31,6 @@ const configVariables: ConfigVariables = { secret: false, }, }, - share: { allowRegistration: { description: "Whether registration is allowed", @@ -165,7 +164,7 @@ type ConfigVariables = { const prisma = new PrismaClient(); -async function main() { +async function seedConfigVariables() { for (const [category, configVariablesOfCategory] of Object.entries( configVariables )) { @@ -191,39 +190,55 @@ async function main() { 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 - // ); - // if (!configVariable) { - // await prisma.config.delete({ - // where: { key: configVariableFromDatabase.key }, - // }); + for (const existingConfigVariable of existingConfigVariables) { + const configVariable = + configVariables[existingConfigVariable.category]?.[ + existingConfigVariable.name + ]; + if (!configVariable) { + await prisma.config.delete({ + 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) - // ) { - // await prisma.config.update({ - // where: { key: configVariableFromDatabase.key }, - // data: { - // ...configVariable, - // key: configVariableFromDatabase.key, - // value: configVariableFromDatabase.value, - // }, - // }); - // } - // } + // Update the config variable if the metadata changed + } else if ( + JSON.stringify({ + ...configVariable, + name: existingConfigVariable.name, + category: existingConfigVariable.category, + value: existingConfigVariable.value, + }) != JSON.stringify(existingConfigVariable) + ) { + await prisma.config.update({ + where: { + name_category: { + name: existingConfigVariable.name, + category: existingConfigVariable.category, + }, + }, + data: { + ...configVariable, + name: existingConfigVariable.name, + category: existingConfigVariable.category, + value: existingConfigVariable.value, + }, + }); + } + } } -main() + +seedConfigVariables() + .then(() => migrateConfigVariables()) .then(async () => { await prisma.$disconnect(); }) diff --git a/frontend/src/components/admin/configuration/ConfigurationHeader.tsx b/frontend/src/components/admin/configuration/ConfigurationHeader.tsx index 177f986ca..5f4640f62 100644 --- a/frontend/src/components/admin/configuration/ConfigurationHeader.tsx +++ b/frontend/src/components/admin/configuration/ConfigurationHeader.tsx @@ -22,7 +22,7 @@ const ConfigurationHeader = ({ const config = useConfig(); const theme = useMantineTheme(); return ( -
+
Date: Sat, 4 Mar 2023 22:50:20 +0100 Subject: [PATCH 08/11] mount images to host --- .gitignore | 5 ++--- Dockerfile | 5 ++--- backend/src/config/config.controller.ts | 14 -------------- backend/src/config/config.service.ts | 5 ----- docker-compose.yml | 1 + frontend/public/{ => img}/favicon.ico | Bin .../public/{ => img}/icons/icon-128x128.png | Bin .../public/{ => img}/icons/icon-144x144.png | Bin .../public/{ => img}/icons/icon-152x152.png | Bin .../public/{ => img}/icons/icon-192x192.png | Bin .../public/{ => img}/icons/icon-384x384.png | Bin .../public/{ => img}/icons/icon-48x48.png | Bin .../public/{ => img}/icons/icon-512x512.png | Bin .../public/{ => img}/icons/icon-72x72.png | Bin .../public/{ => img}/icons/icon-96x96.png | Bin .../{ => img}/icons/icon-white-128x128.png | Bin .../data => frontend/public/img}/logo.png | Bin frontend/public/img/logo.svg | 1 - frontend/public/manifest.json | 18 +++++++++--------- frontend/src/components/Logo.tsx | 2 +- frontend/src/pages/_document.tsx | 6 +++++- 21 files changed, 20 insertions(+), 37 deletions(-) rename frontend/public/{ => img}/favicon.ico (100%) rename frontend/public/{ => img}/icons/icon-128x128.png (100%) rename frontend/public/{ => img}/icons/icon-144x144.png (100%) rename frontend/public/{ => img}/icons/icon-152x152.png (100%) rename frontend/public/{ => img}/icons/icon-192x192.png (100%) rename frontend/public/{ => img}/icons/icon-384x384.png (100%) rename frontend/public/{ => img}/icons/icon-48x48.png (100%) rename frontend/public/{ => img}/icons/icon-512x512.png (100%) rename frontend/public/{ => img}/icons/icon-72x72.png (100%) rename frontend/public/{ => img}/icons/icon-96x96.png (100%) rename frontend/public/{ => img}/icons/icon-white-128x128.png (100%) rename {backend/data => frontend/public/img}/logo.png (100%) delete mode 100644 frontend/public/img/logo.svg diff --git a/.gitignore b/.gitignore index ff52ae937..73672119f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,9 +35,8 @@ yarn-error.log* /frontend/public/sw.* # project specific -/backend/data/* -!/backend/data/logo.png +/backend/data/ /data/ # Jetbrains specific (webstorm) -.idea/**/** \ No newline at end of file +.idea/**/** diff --git a/Dockerfile b/Dockerfile index bcdb42a35..2a5ebfd9d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ No newline at end of file +CMD cp -rn /tmp/img /opt/app/frontend/public && node frontend/server.js & cd backend && npm run prod \ No newline at end of file diff --git a/backend/src/config/config.controller.ts b/backend/src/config/config.controller.ts index 26f22a363..ae22deb68 100644 --- a/backend/src/config/config.controller.ts +++ b/backend/src/config/config.controller.ts @@ -5,12 +5,9 @@ import { Param, Patch, Post, - Res, - StreamableFile, UseGuards, } from "@nestjs/common"; import { SkipThrottle } from "@nestjs/throttler"; -import { Response } from "express"; import { AdministratorGuard } from "src/auth/guard/isAdmin.guard"; import { JwtGuard } from "src/auth/guard/jwt.guard"; import { EmailService } from "src/email/email.service"; @@ -33,17 +30,6 @@ export class ConfigController { return new ConfigDTO().fromList(await this.configService.list()); } - @Get("logo") - @SkipThrottle() - async getLogo(@Res({ passthrough: true }) res: Response) { - res.set({ - "Content-Type": "image/png", - "Content-Disposition": "inline; filename=logo.png", - }); - - return new StreamableFile(this.configService.getLogo()); - } - @Get("admin/:category") @UseGuards(JwtGuard, AdministratorGuard) async getByCategory(@Param("category") category: string) { diff --git a/backend/src/config/config.service.ts b/backend/src/config/config.service.ts index 117d7cdfb..4295e624a 100644 --- a/backend/src/config/config.service.ts +++ b/backend/src/config/config.service.ts @@ -5,7 +5,6 @@ import { NotFoundException, } from "@nestjs/common"; import { Config } from "@prisma/client"; -import * as fs from "fs"; import { PrismaService } from "src/prisma/prisma.service"; @Injectable() @@ -115,8 +114,4 @@ export class ConfigService { return updatedVariable; } - - getLogo() { - return fs.createReadStream(`./data/logo.png`); - } } diff --git a/docker-compose.yml b/docker-compose.yml index cc38e8887..2784d581c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/frontend/public/favicon.ico b/frontend/public/img/favicon.ico similarity index 100% rename from frontend/public/favicon.ico rename to frontend/public/img/favicon.ico diff --git a/frontend/public/icons/icon-128x128.png b/frontend/public/img/icons/icon-128x128.png similarity index 100% rename from frontend/public/icons/icon-128x128.png rename to frontend/public/img/icons/icon-128x128.png diff --git a/frontend/public/icons/icon-144x144.png b/frontend/public/img/icons/icon-144x144.png similarity index 100% rename from frontend/public/icons/icon-144x144.png rename to frontend/public/img/icons/icon-144x144.png diff --git a/frontend/public/icons/icon-152x152.png b/frontend/public/img/icons/icon-152x152.png similarity index 100% rename from frontend/public/icons/icon-152x152.png rename to frontend/public/img/icons/icon-152x152.png diff --git a/frontend/public/icons/icon-192x192.png b/frontend/public/img/icons/icon-192x192.png similarity index 100% rename from frontend/public/icons/icon-192x192.png rename to frontend/public/img/icons/icon-192x192.png diff --git a/frontend/public/icons/icon-384x384.png b/frontend/public/img/icons/icon-384x384.png similarity index 100% rename from frontend/public/icons/icon-384x384.png rename to frontend/public/img/icons/icon-384x384.png diff --git a/frontend/public/icons/icon-48x48.png b/frontend/public/img/icons/icon-48x48.png similarity index 100% rename from frontend/public/icons/icon-48x48.png rename to frontend/public/img/icons/icon-48x48.png diff --git a/frontend/public/icons/icon-512x512.png b/frontend/public/img/icons/icon-512x512.png similarity index 100% rename from frontend/public/icons/icon-512x512.png rename to frontend/public/img/icons/icon-512x512.png diff --git a/frontend/public/icons/icon-72x72.png b/frontend/public/img/icons/icon-72x72.png similarity index 100% rename from frontend/public/icons/icon-72x72.png rename to frontend/public/img/icons/icon-72x72.png diff --git a/frontend/public/icons/icon-96x96.png b/frontend/public/img/icons/icon-96x96.png similarity index 100% rename from frontend/public/icons/icon-96x96.png rename to frontend/public/img/icons/icon-96x96.png diff --git a/frontend/public/icons/icon-white-128x128.png b/frontend/public/img/icons/icon-white-128x128.png similarity index 100% rename from frontend/public/icons/icon-white-128x128.png rename to frontend/public/img/icons/icon-white-128x128.png diff --git a/backend/data/logo.png b/frontend/public/img/logo.png similarity index 100% rename from backend/data/logo.png rename to frontend/public/img/logo.png diff --git a/frontend/public/img/logo.svg b/frontend/public/img/logo.svg deleted file mode 100644 index 4f1f7a7bc..000000000 --- a/frontend/public/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index d8dd5f1db..a7b5fefeb 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -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" diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index 91a3f4bf5..cd5c017ed 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; const Logo = ({ height, width }: { height: number; width: number }) => { return ( - logo + logo ); }; export default Logo; diff --git a/frontend/src/pages/_document.tsx b/frontend/src/pages/_document.tsx index 86be27b04..7813ee18b 100644 --- a/frontend/src/pages/_document.tsx +++ b/frontend/src/pages/_document.tsx @@ -11,7 +11,11 @@ export default class _Document extends Document { - + + From 4efe59e66ad02a46ae1bb64b754ae4b8b33f09fd Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sat, 4 Mar 2023 23:09:14 +0100 Subject: [PATCH 09/11] update docs --- README.md | 11 ++++++++--- .../img/{opengraph-default.png => opengraph.png} | Bin frontend/src/components/Meta.tsx | 1 - frontend/src/pages/_document.tsx | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) rename frontend/public/img/{opengraph-default.png => opengraph.png} (100%) diff --git a/README.md b/README.md index b4540bc2f..b77eda578 100644 --- a/README.md +++ b/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 @@ -102,7 +102,12 @@ You can change the name of the app by visiting the admin configuration page and #### Logo -You can change the logo of the app by replacing the `logo.png` in the `/data` (or with the standalone installation `/backend/data`) folder with your own 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 diff --git a/frontend/public/img/opengraph-default.png b/frontend/public/img/opengraph.png similarity index 100% rename from frontend/public/img/opengraph-default.png rename to frontend/public/img/opengraph.png diff --git a/frontend/src/components/Meta.tsx b/frontend/src/components/Meta.tsx index 144e904bf..4ef2c95da 100644 --- a/frontend/src/components/Meta.tsx +++ b/frontend/src/components/Meta.tsx @@ -22,7 +22,6 @@ const Meta = ({ description ?? "An open-source and self-hosted sharing platform." } /> - diff --git a/frontend/src/pages/_document.tsx b/frontend/src/pages/_document.tsx index 7813ee18b..2702a2a44 100644 --- a/frontend/src/pages/_document.tsx +++ b/frontend/src/pages/_document.tsx @@ -17,9 +17,9 @@ export default class _Document extends Document { href="/img/icons/icon-white-128x128.png" /> - + - + From b9956902db2b7145e8e8d464c4b3d3c1650e48e7 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sat, 4 Mar 2023 23:22:41 +0100 Subject: [PATCH 10/11] remove unused endpoint --- README.md | 2 +- backend/src/config/config.controller.ts | 10 +++------- backend/src/config/config.service.ts | 21 ++++----------------- frontend/src/services/config.service.ts | 12 +----------- 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index b77eda578..6ad30142f 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ docker compose up -d #### Name -You can change the name of the app by visiting the admin configuration page and changing the `App Name` setting. +You can change the name of the app by visiting the admin configuration page and changing the `App Name`. #### Logo diff --git a/backend/src/config/config.controller.ts b/backend/src/config/config.controller.ts index ae22deb68..486ac2e5f 100644 --- a/backend/src/config/config.controller.ts +++ b/backend/src/config/config.controller.ts @@ -38,16 +38,12 @@ export class ConfigController { ); } - @Get("admin/categories") - @UseGuards(JwtGuard, AdministratorGuard) - async getCategories() { - return await this.configService.getCategories(); - } - @Patch("admin") @UseGuards(JwtGuard, AdministratorGuard) async updateMany(@Body() data: UpdateConfigDTO[]) { - await this.configService.updateMany(data); + return new AdminConfigDTO().fromList( + await this.configService.updateMany(data) + ); } @Post("admin/testEmail") diff --git a/backend/src/config/config.service.ts b/backend/src/config/config.service.ts index 4295e624a..4580c8191 100644 --- a/backend/src/config/config.service.ts +++ b/backend/src/config/config.service.ts @@ -41,21 +41,6 @@ export class ConfigService { }); } - async getCategories() { - const categories = await this.prisma.config.groupBy({ - by: ["category"], - where: { locked: { equals: false } }, - _count: { category: true }, - }); - - return categories.map((category) => { - return { - category: category.category, - count: category._count.category, - }; - }); - } - async list() { const configVariables = await this.prisma.config.findMany({ where: { secret: { equals: false } }, @@ -70,11 +55,13 @@ export class ConfigService { } 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) { diff --git a/frontend/src/services/config.service.ts b/frontend/src/services/config.service.ts index 8871b3eaf..f2da6f271 100644 --- a/frontend/src/services/config.service.ts +++ b/frontend/src/services/config.service.ts @@ -1,9 +1,5 @@ import axios from "axios"; -import Config, { - AdminConfig, - ConfigVariablesCategory, - UpdateConfig -} from "../types/config.type"; +import Config, { AdminConfig, UpdateConfig } from "../types/config.type"; import api from "./api.service"; const list = async (): Promise => { @@ -14,11 +10,6 @@ const getByCategory = async (category: string): Promise => { return (await api.get(`/configs/admin/${category}`)).data; }; -const getCategories = async (): Promise => { -return (await api.get("/configs/admin/categories")).data; - -}; - const updateMany = async (data: UpdateConfig[]): Promise => { return (await api.patch("/configs/admin", data)).data; }; @@ -58,7 +49,6 @@ const isNewReleaseAvailable = async () => { export default { list, getByCategory, - getCategories, updateMany, get, finishSetup, From acaca80eeab34c62d88fb4ddc763554201e76dbf Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sat, 4 Mar 2023 23:26:46 +0100 Subject: [PATCH 11/11] run formatter --- backend/src/config/dto/adminConfig.dto.ts | 1 - backend/src/email/email.service.ts | 6 +++--- backend/src/user/user.module.ts | 2 +- frontend/src/components/Logo.tsx | 4 +--- .../components/admin/users/showCreateUserModal.tsx | 13 +++++++------ 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/backend/src/config/dto/adminConfig.dto.ts b/backend/src/config/dto/adminConfig.dto.ts index b008ba0dc..322df3b1b 100644 --- a/backend/src/config/dto/adminConfig.dto.ts +++ b/backend/src/config/dto/adminConfig.dto.ts @@ -17,7 +17,6 @@ export class AdminConfigDTO extends ConfigDTO { @Expose() obscured: boolean; - from(partial: Partial) { return plainToClass(AdminConfigDTO, partial, { excludeExtraneousValues: true, diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts index e192df780..7db97ceff 100644 --- a/backend/src/email/email.service.ts +++ b/backend/src/email/email.service.ts @@ -98,9 +98,9 @@ export class EmailService { async sendTestMail(recipientEmail: string) { try { await this.getTransporter().sendMail({ - from: `"${this.config.get("general.appName")}" <${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", diff --git a/backend/src/user/user.module.ts b/backend/src/user/user.module.ts index 4ca2e946a..56be04e98 100644 --- a/backend/src/user/user.module.ts +++ b/backend/src/user/user.module.ts @@ -4,7 +4,7 @@ import { UserController } from "./user.controller"; import { UserSevice } from "./user.service"; @Module({ - imports:[EmailModule], + imports: [EmailModule], providers: [UserSevice], controllers: [UserController], }) diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index cd5c017ed..c69c50c04 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -1,8 +1,6 @@ import Image from "next/image"; const Logo = ({ height, width }: { height: number; width: number }) => { - return ( - logo - ); + return logo; }; export default Logo; diff --git a/frontend/src/components/admin/users/showCreateUserModal.tsx b/frontend/src/components/admin/users/showCreateUserModal.tsx index bacd673b0..e2bb6718a 100644 --- a/frontend/src/components/admin/users/showCreateUserModal.tsx +++ b/frontend/src/components/admin/users/showCreateUserModal.tsx @@ -79,12 +79,13 @@ const Body = ({ })} /> )} - {form.values.setPasswordManually || !smtpEnabled && ( - - )} + {form.values.setPasswordManually || + (!smtpEnabled && ( + + ))}