Skip to content

Commit

Permalink
feat: improve config UI (#69)
Browse files Browse the repository at this point in the history
* add first concept

* completed configuration ui update

* add button for testing email configuration

* improve mobile layout

* add migration

* run formatter

* delete unnecessary modal

* remove unused comment
  • Loading branch information
stonith404 authored Dec 30, 2022
1 parent e5b50f8 commit 5bc4f90
Show file tree
Hide file tree
Showing 23 changed files with 429 additions and 284 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Warnings:
- Added the required column `category` 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,
"key" TEXT NOT NULL PRIMARY KEY,
"type" TEXT NOT NULL,
"value" TEXT NOT NULL,
"description" TEXT NOT NULL,
"category" TEXT,
"obscured" BOOLEAN NOT NULL DEFAULT false,
"secret" BOOLEAN NOT NULL DEFAULT true,
"locked" BOOLEAN NOT NULL DEFAULT false
);
INSERT INTO "new_Config" ("description", "key", "locked", "obscured", "secret", "type", "updatedAt", "value") SELECT "description", "key", "locked", "obscured", "secret", "type", "updatedAt", "value" FROM "Config";
DROP TABLE "Config";
ALTER TABLE "new_Config" RENAME TO "Config";

UPDATE config SET category = "internal" WHERE key = "SETUP_FINISHED";
UPDATE config SET category = "internal" WHERE key = "TOTP_SECRET";
UPDATE config SET category = "internal" WHERE key = "JWT_SECRET";
UPDATE config SET category = "general" WHERE key = "APP_URL";
UPDATE config SET category = "general" WHERE key = "SHOW_HOME_PAGE";
UPDATE config SET category = "share" WHERE key = "ALLOW_REGISTRATION";
UPDATE config SET category = "share" WHERE key = "ALLOW_UNAUTHENTICATED_SHARES";
UPDATE config SET category = "share" WHERE key = "MAX_FILE_SIZE";
UPDATE config SET category = "email" WHERE key = "ENABLE_EMAIL_RECIPIENTS";
UPDATE config SET category = "email" WHERE key = "EMAIL_MESSAGE";
UPDATE config SET category = "email" WHERE key = "EMAIL_SUBJECT";
UPDATE config SET category = "email" WHERE key = "SMTP_HOST";
UPDATE config SET category = "email" WHERE key = "SMTP_PORT";
UPDATE config SET category = "email" WHERE key = "SMTP_EMAIL";
UPDATE config SET category = "email" WHERE key = "SMTP_USERNAME";
UPDATE config SET category = "email" WHERE key = "SMTP_PASSWORD";

CREATE TABLE "new_Config" (
"updatedAt" DATETIME NOT NULL,
"key" TEXT NOT NULL PRIMARY KEY,
"type" TEXT NOT NULL,
"value" TEXT NOT NULL,
"description" TEXT NOT NULL,
"category" TEXT NOT NULL,
"obscured" BOOLEAN NOT NULL DEFAULT false,
"secret" BOOLEAN NOT NULL DEFAULT true,
"locked" BOOLEAN NOT NULL DEFAULT false
);
INSERT INTO "new_Config" ("description", "key", "locked", "obscured", "secret", "type", "updatedAt", "value", "category") SELECT "description", "key", "locked", "obscured", "secret", "type", "updatedAt", "value", "category" FROM "Config";
DROP TABLE "Config";
ALTER TABLE "new_Config" RENAME TO "Config";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
1 change: 1 addition & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ model Config {
type String
value String
description String
category String
obscured Boolean @default(false)
secret Boolean @default(true)
locked Boolean @default(false)
Expand Down
18 changes: 17 additions & 1 deletion backend/prisma/seed/config.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "Whether the setup has been finished",
type: "boolean",
value: "false",
category: "internal",
secret: false,
locked: true,
},
Expand All @@ -15,48 +16,55 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "On which URL Pingvin Share is available",
type: "string",
value: "http://localhost:3000",
category: "general",
secret: false,
},
{
key: "SHOW_HOME_PAGE",
description: "Whether to show the home page",
type: "boolean",
value: "true",
category: "general",
secret: false,
},
{
key: "ALLOW_REGISTRATION",
description: "Whether registration is allowed",
type: "boolean",
value: "true",
category: "share",
secret: false,
},
{
key: "ALLOW_UNAUTHENTICATED_SHARES",
description: "Whether unauthorized users can create shares",
type: "boolean",
value: "false",
category: "share",
secret: false,
},
{
key: "MAX_FILE_SIZE",
description: "Maximum file size in bytes",
type: "number",
value: "1000000000",
category: "share",
secret: false,
},
{
key: "JWT_SECRET",
description: "Long random string used to sign JWT tokens",
type: "string",
value: crypto.randomBytes(256).toString("base64"),
category: "internal",
locked: true,
},
{
key: "TOTP_SECRET",
description: "A 16 byte random string used to generate TOTP secrets",
type: "string",
value: crypto.randomBytes(16).toString("base64"),
category: "internal",
locked: true,
},
{
Expand All @@ -65,6 +73,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
"Whether to send emails to recipients. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
type: "boolean",
value: "false",
category: "email",
secret: false,
},
{
Expand All @@ -74,43 +83,50 @@ const configVariables: Prisma.ConfigCreateInput[] = [
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",
},
{
key: "EMAIL_SUBJECT",
description: "Subject of the email which gets sent to the recipients.",
type: "string",
value: "Files shared with you",
category: "email",
},
{
key: "SMTP_HOST",
description: "Host of the SMTP server",
type: "string",
value: "",
category: "email",
},
{
key: "SMTP_PORT",
description: "Port of the SMTP server",
type: "number",
value: "",
value: "0",
category: "email",
},
{
key: "SMTP_EMAIL",
description: "Email address which the emails get sent from",
type: "string",
value: "",
category: "email",
},
{
key: "SMTP_USERNAME",
description: "Username of the SMTP server",
type: "string",
value: "",
category: "email",
},
{
key: "SMTP_PASSWORD",
description: "Password of the SMTP server",
type: "string",
value: "",
obscured: true,
category: "email",
},
];

Expand Down
4 changes: 3 additions & 1 deletion backend/src/auth/authTotp.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { User } from "@prisma/client";
import * as argon from "argon2";
import * as crypto from "crypto";
import { authenticator, totp } from "otplib";
import * as qrcode from "qrcode-svg";
import { ConfigService } from "src/config/config.service";
import { PrismaService } from "src/prisma/prisma.service";
import { AuthService } from "./auth.service";
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";

@Injectable()
export class AuthTotpService {
constructor(
private config: ConfigService,
Expand Down
31 changes: 16 additions & 15 deletions backend/src/config/config.controller.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import {
Body,
Controller,
Get,
Param,
Patch,
Post,
UseGuards,
} from "@nestjs/common";
import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common";
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
import { JwtGuard } from "src/auth/guard/jwt.guard";
import { EmailService } from "src/email/email.service";
import { ConfigService } from "./config.service";
import { AdminConfigDTO } from "./dto/adminConfig.dto";
import { ConfigDTO } from "./dto/config.dto";
import { TestEmailDTO } from "./dto/testEmail.dto";
import UpdateConfigDTO from "./dto/updateConfig.dto";

@Controller("configs")
export class ConfigController {
constructor(private configService: ConfigService) {}
constructor(
private configService: ConfigService,
private emailService: EmailService
) {}

@Get()
async list() {
Expand All @@ -31,17 +28,21 @@ export class ConfigController {
);
}

@Patch("admin/:key")
@Patch("admin")
@UseGuards(JwtGuard, AdministratorGuard)
async update(@Param("key") key: string, @Body() data: UpdateConfigDTO) {
return new AdminConfigDTO().from(
await this.configService.update(key, data.value)
);
async updateMany(@Body() data: UpdateConfigDTO[]) {
await this.configService.updateMany(data);
}

@Post("admin/finishSetup")
@UseGuards(JwtGuard, AdministratorGuard)
async finishSetup() {
return await this.configService.finishSetup();
}

@Post("admin/testEmail")
@UseGuards(JwtGuard, AdministratorGuard)
async testEmail(@Body() { email }: TestEmailDTO) {
await this.emailService.sendTestMail(email);
}
}
2 changes: 2 additions & 0 deletions backend/src/config/config.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Global, Module } from "@nestjs/common";
import { EmailModule } from "src/email/email.module";
import { PrismaService } from "src/prisma/prisma.service";
import { ConfigController } from "./config.controller";
import { ConfigService } from "./config.service";

@Global()
@Module({
imports: [EmailModule],
providers: [
{
provide: "CONFIG_VARIABLES",
Expand Down
8 changes: 8 additions & 0 deletions backend/src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ export class ConfigService {
});
}

async updateMany(data: { key: string; value: string | number | boolean }[]) {
for (const variable of data) {
await this.update(variable.key, variable.value);
}

return data;
}

async update(key: string, value: string | number | boolean) {
const configVariable = await this.prisma.config.findUnique({
where: { key },
Expand Down
3 changes: 3 additions & 0 deletions backend/src/config/dto/adminConfig.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export class AdminConfigDTO extends ConfigDTO {
@Expose()
obscured: boolean;

@Expose()
category: string;

from(partial: Partial<AdminConfigDTO>) {
return plainToClass(AdminConfigDTO, partial, {
excludeExtraneousValues: true,
Expand Down
7 changes: 7 additions & 0 deletions backend/src/config/dto/testEmail.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsEmail, IsNotEmpty } from "class-validator";

export class TestEmailDTO {
@IsEmail()
@IsNotEmpty()
email: string;
}
5 changes: 4 additions & 1 deletion backend/src/config/dto/updateConfig.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { IsNotEmpty, ValidateIf } from "class-validator";
import { IsNotEmpty, IsString, ValidateIf } from "class-validator";

class UpdateConfigDTO {
@IsString()
key: string;

@IsNotEmpty()
@ValidateIf((dto) => dto.value !== "")
value: string | number | boolean;
Expand Down
32 changes: 20 additions & 12 deletions backend/src/email/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,23 @@ import { ConfigService } from "src/config/config.service";
export class EmailService {
constructor(private config: ConfigService) {}

async sendMail(recipientEmail: string, shareId: string, creator: User) {
// create reusable transporter object using the default SMTP transport
const transporter = nodemailer.createTransport({
host: this.config.get("SMTP_HOST"),
port: parseInt(this.config.get("SMTP_PORT")),
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
auth: {
user: this.config.get("SMTP_USERNAME"),
pass: this.config.get("SMTP_PASSWORD"),
},
});
transporter = nodemailer.createTransport({
host: this.config.get("SMTP_HOST"),
port: parseInt(this.config.get("SMTP_PORT")),
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
auth: {
user: this.config.get("SMTP_USERNAME"),
pass: this.config.get("SMTP_PASSWORD"),
},
});

async sendMail(recipientEmail: string, shareId: string, creator: User) {
if (!this.config.get("ENABLE_EMAIL_RECIPIENTS"))
throw new InternalServerErrorException("Email service disabled");

const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;

await transporter.sendMail({
await this.transporter.sendMail({
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
to: recipientEmail,
subject: this.config.get("EMAIL_SUBJECT"),
Expand All @@ -35,4 +34,13 @@ export class EmailService {
.replaceAll("{shareUrl}", shareUrl),
});
}

async sendTestMail(recipientEmail: string) {
await this.transporter.sendMail({
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
to: recipientEmail,
subject: "Test email",
text: "This is a test email",
});
}
}
3 changes: 0 additions & 3 deletions frontend/src/components/account/showEnableTotpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ const CreateEnableTotpModal = ({
refreshUser: () => {};
}) => {
const modals = useModals();
const user = useUser();

console.log(user.user);

const validationSchema = yup.object().shape({
code: yup
Expand Down
Loading

0 comments on commit 5bc4f90

Please sign in to comment.