mirror of
https://github.com/stonith404/pingvin-share.git
synced 2024-11-05 15:30:14 +01:00
feat: improve config UI (#69)
* 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
This commit is contained in:
parent
e5b50f855c
commit
5bc4f902f6
@ -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;
|
@ -101,6 +101,7 @@ model Config {
|
|||||||
type String
|
type String
|
||||||
value String
|
value String
|
||||||
description String
|
description String
|
||||||
|
category String
|
||||||
obscured Boolean @default(false)
|
obscured Boolean @default(false)
|
||||||
secret Boolean @default(true)
|
secret Boolean @default(true)
|
||||||
locked Boolean @default(false)
|
locked Boolean @default(false)
|
||||||
|
@ -7,6 +7,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
description: "Whether the setup has been finished",
|
description: "Whether the setup has been finished",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "false",
|
value: "false",
|
||||||
|
category: "internal",
|
||||||
secret: false,
|
secret: false,
|
||||||
locked: true,
|
locked: true,
|
||||||
},
|
},
|
||||||
@ -15,6 +16,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
description: "On which URL Pingvin Share is available",
|
description: "On which URL Pingvin Share is available",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "http://localhost:3000",
|
value: "http://localhost:3000",
|
||||||
|
category: "general",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -22,6 +24,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
description: "Whether to show the home page",
|
description: "Whether to show the home page",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "true",
|
value: "true",
|
||||||
|
category: "general",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -29,6 +32,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
description: "Whether registration is allowed",
|
description: "Whether registration is allowed",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "true",
|
value: "true",
|
||||||
|
category: "share",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -36,6 +40,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
description: "Whether unauthorized users can create shares",
|
description: "Whether unauthorized users can create shares",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: "false",
|
value: "false",
|
||||||
|
category: "share",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -43,6 +48,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
description: "Maximum file size in bytes",
|
description: "Maximum file size in bytes",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: "1000000000",
|
value: "1000000000",
|
||||||
|
category: "share",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -50,6 +56,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
description: "Long random string used to sign JWT tokens",
|
description: "Long random string used to sign JWT tokens",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: crypto.randomBytes(256).toString("base64"),
|
value: crypto.randomBytes(256).toString("base64"),
|
||||||
|
category: "internal",
|
||||||
locked: true,
|
locked: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -57,6 +64,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
description: "A 16 byte random string used to generate TOTP secrets",
|
description: "A 16 byte random string used to generate TOTP secrets",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: crypto.randomBytes(16).toString("base64"),
|
value: crypto.randomBytes(16).toString("base64"),
|
||||||
|
category: "internal",
|
||||||
locked: true,
|
locked: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -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.",
|
"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",
|
type: "boolean",
|
||||||
value: "false",
|
value: "false",
|
||||||
|
category: "email",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -74,36 +83,42 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
type: "text",
|
type: "text",
|
||||||
value:
|
value:
|
||||||
"Hey!\n{creator} shared some files with you. View or download the files with this link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
"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",
|
key: "EMAIL_SUBJECT",
|
||||||
description: "Subject of the email which gets sent to the recipients.",
|
description: "Subject of the email which gets sent to the recipients.",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "Files shared with you",
|
value: "Files shared with you",
|
||||||
|
category: "email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "SMTP_HOST",
|
key: "SMTP_HOST",
|
||||||
description: "Host of the SMTP server",
|
description: "Host of the SMTP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "",
|
value: "",
|
||||||
|
category: "email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "SMTP_PORT",
|
key: "SMTP_PORT",
|
||||||
description: "Port of the SMTP server",
|
description: "Port of the SMTP server",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: "",
|
value: "0",
|
||||||
|
category: "email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "SMTP_EMAIL",
|
key: "SMTP_EMAIL",
|
||||||
description: "Email address which the emails get sent from",
|
description: "Email address which the emails get sent from",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "",
|
value: "",
|
||||||
|
category: "email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "SMTP_USERNAME",
|
key: "SMTP_USERNAME",
|
||||||
description: "Username of the SMTP server",
|
description: "Username of the SMTP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: "",
|
value: "",
|
||||||
|
category: "email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "SMTP_PASSWORD",
|
key: "SMTP_PASSWORD",
|
||||||
@ -111,6 +126,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
|||||||
type: "string",
|
type: "string",
|
||||||
value: "",
|
value: "",
|
||||||
obscured: true,
|
obscured: true,
|
||||||
|
category: "email",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import { authenticator, totp } from "otplib";
|
import { authenticator, totp } from "otplib";
|
||||||
import * as qrcode from "qrcode-svg";
|
import * as qrcode from "qrcode-svg";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
export class AuthTotpService {
|
export class AuthTotpService {
|
||||||
constructor(
|
constructor(
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
|
@ -1,22 +1,19 @@
|
|||||||
import {
|
import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common";
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
UseGuards,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
||||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||||
|
import { EmailService } from "src/email/email.service";
|
||||||
import { ConfigService } from "./config.service";
|
import { ConfigService } from "./config.service";
|
||||||
import { AdminConfigDTO } from "./dto/adminConfig.dto";
|
import { AdminConfigDTO } from "./dto/adminConfig.dto";
|
||||||
import { ConfigDTO } from "./dto/config.dto";
|
import { ConfigDTO } from "./dto/config.dto";
|
||||||
|
import { TestEmailDTO } from "./dto/testEmail.dto";
|
||||||
import UpdateConfigDTO from "./dto/updateConfig.dto";
|
import UpdateConfigDTO from "./dto/updateConfig.dto";
|
||||||
|
|
||||||
@Controller("configs")
|
@Controller("configs")
|
||||||
export class ConfigController {
|
export class ConfigController {
|
||||||
constructor(private configService: ConfigService) {}
|
constructor(
|
||||||
|
private configService: ConfigService,
|
||||||
|
private emailService: EmailService
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async list() {
|
async list() {
|
||||||
@ -31,12 +28,10 @@ export class ConfigController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch("admin/:key")
|
@Patch("admin")
|
||||||
@UseGuards(JwtGuard, AdministratorGuard)
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
async update(@Param("key") key: string, @Body() data: UpdateConfigDTO) {
|
async updateMany(@Body() data: UpdateConfigDTO[]) {
|
||||||
return new AdminConfigDTO().from(
|
await this.configService.updateMany(data);
|
||||||
await this.configService.update(key, data.value)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("admin/finishSetup")
|
@Post("admin/finishSetup")
|
||||||
@ -44,4 +39,10 @@ export class ConfigController {
|
|||||||
async finishSetup() {
|
async finishSetup() {
|
||||||
return await this.configService.finishSetup();
|
return await this.configService.finishSetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("admin/testEmail")
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async testEmail(@Body() { email }: TestEmailDTO) {
|
||||||
|
await this.emailService.sendTestMail(email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Global, Module } from "@nestjs/common";
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
import { EmailModule } from "src/email/email.module";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { ConfigController } from "./config.controller";
|
import { ConfigController } from "./config.controller";
|
||||||
import { ConfigService } from "./config.service";
|
import { ConfigService } from "./config.service";
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [EmailModule],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: "CONFIG_VARIABLES",
|
provide: "CONFIG_VARIABLES",
|
||||||
|
@ -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) {
|
async update(key: string, value: string | number | boolean) {
|
||||||
const configVariable = await this.prisma.config.findUnique({
|
const configVariable = await this.prisma.config.findUnique({
|
||||||
where: { key },
|
where: { key },
|
||||||
|
@ -14,6 +14,9 @@ export class AdminConfigDTO extends ConfigDTO {
|
|||||||
@Expose()
|
@Expose()
|
||||||
obscured: boolean;
|
obscured: boolean;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
category: string;
|
||||||
|
|
||||||
from(partial: Partial<AdminConfigDTO>) {
|
from(partial: Partial<AdminConfigDTO>) {
|
||||||
return plainToClass(AdminConfigDTO, partial, {
|
return plainToClass(AdminConfigDTO, partial, {
|
||||||
excludeExtraneousValues: true,
|
excludeExtraneousValues: true,
|
||||||
|
7
backend/src/config/dto/testEmail.dto.ts
Normal file
7
backend/src/config/dto/testEmail.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IsEmail, IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
|
export class TestEmailDTO {
|
||||||
|
@IsEmail()
|
||||||
|
@IsNotEmpty()
|
||||||
|
email: string;
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import { IsNotEmpty, ValidateIf } from "class-validator";
|
import { IsNotEmpty, IsString, ValidateIf } from "class-validator";
|
||||||
|
|
||||||
class UpdateConfigDTO {
|
class UpdateConfigDTO {
|
||||||
|
@IsString()
|
||||||
|
key: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ValidateIf((dto) => dto.value !== "")
|
@ValidateIf((dto) => dto.value !== "")
|
||||||
value: string | number | boolean;
|
value: string | number | boolean;
|
||||||
|
@ -7,9 +7,7 @@ import { ConfigService } from "src/config/config.service";
|
|||||||
export class EmailService {
|
export class EmailService {
|
||||||
constructor(private config: ConfigService) {}
|
constructor(private config: ConfigService) {}
|
||||||
|
|
||||||
async sendMail(recipientEmail: string, shareId: string, creator: User) {
|
transporter = nodemailer.createTransport({
|
||||||
// create reusable transporter object using the default SMTP transport
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: this.config.get("SMTP_HOST"),
|
host: this.config.get("SMTP_HOST"),
|
||||||
port: parseInt(this.config.get("SMTP_PORT")),
|
port: parseInt(this.config.get("SMTP_PORT")),
|
||||||
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
|
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
|
||||||
@ -19,12 +17,13 @@ export class EmailService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async sendMail(recipientEmail: string, shareId: string, creator: User) {
|
||||||
if (!this.config.get("ENABLE_EMAIL_RECIPIENTS"))
|
if (!this.config.get("ENABLE_EMAIL_RECIPIENTS"))
|
||||||
throw new InternalServerErrorException("Email service disabled");
|
throw new InternalServerErrorException("Email service disabled");
|
||||||
|
|
||||||
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
|
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
|
||||||
|
|
||||||
await transporter.sendMail({
|
await this.transporter.sendMail({
|
||||||
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
subject: this.config.get("EMAIL_SUBJECT"),
|
subject: this.config.get("EMAIL_SUBJECT"),
|
||||||
@ -35,4 +34,13 @@ export class EmailService {
|
|||||||
.replaceAll("{shareUrl}", shareUrl),
|
.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",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,6 @@ const CreateEnableTotpModal = ({
|
|||||||
refreshUser: () => {};
|
refreshUser: () => {};
|
||||||
}) => {
|
}) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const user = useUser();
|
|
||||||
|
|
||||||
console.log(user.user);
|
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
code: yup
|
code: yup
|
||||||
|
@ -1,115 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
Box,
|
|
||||||
Code,
|
|
||||||
Group,
|
|
||||||
Skeleton,
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useModals } from "@mantine/modals";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { TbEdit, TbLock } from "react-icons/tb";
|
|
||||||
import configService from "../../services/config.service";
|
|
||||||
import { AdminConfig as AdminConfigType } from "../../types/config.type";
|
|
||||||
import showUpdateConfigVariableModal from "./showUpdateConfigVariableModal";
|
|
||||||
|
|
||||||
const AdminConfigTable = () => {
|
|
||||||
const modals = useModals();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const [configVariables, setConfigVariables] = useState<AdminConfigType[]>([]);
|
|
||||||
|
|
||||||
const getConfigVariables = async () => {
|
|
||||||
await configService.listForAdmin().then((configVariables) => {
|
|
||||||
setConfigVariables(configVariables);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsLoading(true);
|
|
||||||
getConfigVariables().then(() => setIsLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const skeletonRows = [...Array(9)].map((c, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>
|
|
||||||
<Skeleton height={18} width={80} mb="sm" />
|
|
||||||
<Skeleton height={30} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Skeleton height={18} />
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<Group position="right">
|
|
||||||
<Skeleton height={25} width={25} />
|
|
||||||
</Group>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: "block", overflowX: "auto" }}>
|
|
||||||
<Table verticalSpacing="sm" horizontalSpacing="xl" withBorder>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Key</th>
|
|
||||||
<th>Value</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{isLoading
|
|
||||||
? skeletonRows
|
|
||||||
: configVariables.map((configVariable) => (
|
|
||||||
<tr key={configVariable.key}>
|
|
||||||
<td style={{ maxWidth: "200px" }}>
|
|
||||||
<Code>{configVariable.key}</Code>{" "}
|
|
||||||
{configVariable.secret && <TbLock />} <br />
|
|
||||||
<Text size="xs" color="dimmed">
|
|
||||||
{configVariable.description}
|
|
||||||
</Text>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
maxWidth: "40ch",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{configVariable.obscured
|
|
||||||
? "•".repeat(configVariable.value.length)
|
|
||||||
: configVariable.value}
|
|
||||||
</Text>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Group position="right">
|
|
||||||
<ActionIcon
|
|
||||||
color="primary"
|
|
||||||
variant="light"
|
|
||||||
size={25}
|
|
||||||
onClick={() =>
|
|
||||||
showUpdateConfigVariableModal(
|
|
||||||
modals,
|
|
||||||
configVariable,
|
|
||||||
getConfigVariables
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TbEdit />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminConfigTable;
|
|
@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
NumberInput,
|
||||||
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
Textarea,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
||||||
|
|
||||||
|
const AdminConfigInput = ({
|
||||||
|
configVariable,
|
||||||
|
updateConfigVariable,
|
||||||
|
}: {
|
||||||
|
configVariable: AdminConfig;
|
||||||
|
updateConfigVariable: (variable: UpdateConfig) => void;
|
||||||
|
}) => {
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
stringValue: configVariable.value,
|
||||||
|
textValue: configVariable.value,
|
||||||
|
numberValue: parseInt(configVariable.value),
|
||||||
|
booleanValue: configVariable.value == "true",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onValueChange = (configVariable: AdminConfig, value: any) => {
|
||||||
|
form.setFieldValue(`${configVariable.type}Value`, value);
|
||||||
|
updateConfigVariable({ key: configVariable.key, value: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack align="end">
|
||||||
|
{configVariable.type == "string" &&
|
||||||
|
(configVariable.obscured ? (
|
||||||
|
<PasswordInput
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||||
|
{...form.getInputProps("stringValue")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextInput
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
{...form.getInputProps("stringValue")}
|
||||||
|
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{configVariable.type == "text" && (
|
||||||
|
<Textarea
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
autosize
|
||||||
|
{...form.getInputProps("textValue")}
|
||||||
|
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{configVariable.type == "number" && (
|
||||||
|
<NumberInput
|
||||||
|
{...form.getInputProps("numberValue")}
|
||||||
|
onChange={(number) => onValueChange(configVariable, number)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{configVariable.type == "boolean" && (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
{...form.getInputProps("booleanValue", { type: "checkbox" })}
|
||||||
|
onChange={(e) => onValueChange(configVariable, e.target.checked)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminConfigInput;
|
140
frontend/src/components/admin/configuration/AdminConfigTable.tsx
Normal file
140
frontend/src/components/admin/configuration/AdminConfigTable.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Space,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useConfig from "../../../hooks/config.hook";
|
||||||
|
import configService from "../../../services/config.service";
|
||||||
|
import {
|
||||||
|
AdminConfigGroupedByCategory,
|
||||||
|
UpdateConfig,
|
||||||
|
} from "../../../types/config.type";
|
||||||
|
import {
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
configVariableToFriendlyName,
|
||||||
|
} from "../../../utils/string.util";
|
||||||
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
|
import AdminConfigInput from "./AdminConfigInput";
|
||||||
|
import TestEmailButton from "./TestEmailButton";
|
||||||
|
|
||||||
|
const AdminConfigTable = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||||
|
|
||||||
|
let updatedConfigVariables: UpdateConfig[] = [];
|
||||||
|
|
||||||
|
const updateConfigVariable = (configVariable: UpdateConfig) => {
|
||||||
|
const index = updatedConfigVariables.findIndex(
|
||||||
|
(item) => item.key === configVariable.key
|
||||||
|
);
|
||||||
|
if (index > -1) {
|
||||||
|
updatedConfigVariables[index] = configVariable;
|
||||||
|
} else {
|
||||||
|
updatedConfigVariables.push(configVariable);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [configVariablesByCategory, setCofigVariablesByCategory] =
|
||||||
|
useState<AdminConfigGroupedByCategory>({});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getConfigVariables();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mb="lg">
|
||||||
|
{Object.entries(configVariablesByCategory).map(
|
||||||
|
([category, configVariables]) => {
|
||||||
|
return (
|
||||||
|
<Paper key={category} withBorder p="lg" mb="xl">
|
||||||
|
<Title mb="xs" order={3}>
|
||||||
|
{capitalizeFirstLetter(category)}
|
||||||
|
</Title>
|
||||||
|
{configVariables.map((configVariable) => (
|
||||||
|
<>
|
||||||
|
<Group position="apart">
|
||||||
|
<Stack
|
||||||
|
style={{ maxWidth: isMobile ? "100%" : "40%" }}
|
||||||
|
spacing={0}
|
||||||
|
>
|
||||||
|
<Title order={6}>
|
||||||
|
{configVariableToFriendlyName(configVariable.key)}
|
||||||
|
</Title>
|
||||||
|
<Text color="dimmed" size="sm" mb="xs">
|
||||||
|
{configVariable.description}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack></Stack>
|
||||||
|
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||||
|
<AdminConfigInput
|
||||||
|
key={configVariable.key}
|
||||||
|
updateConfigVariable={updateConfigVariable}
|
||||||
|
configVariable={configVariable}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Space h="lg" />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
{category == "email" && (
|
||||||
|
<Group position="right">
|
||||||
|
<TestEmailButton />
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
<Group position="right">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (config.get("SETUP_FINISHED")) {
|
||||||
|
configService
|
||||||
|
.updateMany(updatedConfigVariables)
|
||||||
|
.then(() =>
|
||||||
|
toast.success("Configurations updated successfully")
|
||||||
|
)
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
} else {
|
||||||
|
configService
|
||||||
|
.updateMany(updatedConfigVariables)
|
||||||
|
.then(async () => {
|
||||||
|
await configService.finishSetup();
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminConfigTable;
|
@ -0,0 +1,27 @@
|
|||||||
|
import { Button } from "@mantine/core";
|
||||||
|
import useUser from "../../../hooks/user.hook";
|
||||||
|
import configService from "../../../services/config.service";
|
||||||
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
|
const TestEmailButton = () => {
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
configService
|
||||||
|
.sendTestEmail(user!.email)
|
||||||
|
.then(() => toast.success("Email sent successfully"))
|
||||||
|
.catch(() =>
|
||||||
|
toast.error(
|
||||||
|
"Failed to send the email. Please check the backend logs for more information."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Send test email
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default TestEmailButton;
|
@ -1,108 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Code,
|
|
||||||
NumberInput,
|
|
||||||
PasswordInput,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Textarea,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useForm } from "@mantine/form";
|
|
||||||
import { useModals } from "@mantine/modals";
|
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
|
||||||
import configService from "../../services/config.service";
|
|
||||||
import { AdminConfig } from "../../types/config.type";
|
|
||||||
import toast from "../../utils/toast.util";
|
|
||||||
|
|
||||||
const showUpdateConfigVariableModal = (
|
|
||||||
modals: ModalsContextProps,
|
|
||||||
configVariable: AdminConfig,
|
|
||||||
getConfigVariables: () => void
|
|
||||||
) => {
|
|
||||||
return modals.openModal({
|
|
||||||
title: <Title order={5}>Update configuration variable</Title>,
|
|
||||||
children: (
|
|
||||||
<Body
|
|
||||||
configVariable={configVariable}
|
|
||||||
getConfigVariables={getConfigVariables}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const Body = ({
|
|
||||||
configVariable,
|
|
||||||
getConfigVariables,
|
|
||||||
}: {
|
|
||||||
configVariable: AdminConfig;
|
|
||||||
getConfigVariables: () => void;
|
|
||||||
}) => {
|
|
||||||
const modals = useModals();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: {
|
|
||||||
stringValue: configVariable.value,
|
|
||||||
textValue: configVariable.value,
|
|
||||||
numberValue: parseInt(configVariable.value),
|
|
||||||
booleanValue: configVariable.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<Stack align="stretch">
|
|
||||||
<Text>
|
|
||||||
Set <Code>{configVariable.key}</Code> to
|
|
||||||
</Text>
|
|
||||||
{configVariable.type == "string" &&
|
|
||||||
(configVariable.obscured ? (
|
|
||||||
<PasswordInput {...form.getInputProps("stringValue")} />
|
|
||||||
) : (
|
|
||||||
<TextInput {...form.getInputProps("stringValue")} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{configVariable.type == "text" && (
|
|
||||||
<Textarea autosize {...form.getInputProps("textValue")} />
|
|
||||||
)}
|
|
||||||
{configVariable.type == "number" && (
|
|
||||||
<NumberInput {...form.getInputProps("numberValue")} />
|
|
||||||
)}
|
|
||||||
{configVariable.type == "boolean" && (
|
|
||||||
<Select
|
|
||||||
data={[
|
|
||||||
{ value: "true", label: "True" },
|
|
||||||
{ value: "false", label: "False" },
|
|
||||||
]}
|
|
||||||
{...form.getInputProps("booleanValue")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Space />
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
const value =
|
|
||||||
configVariable.type == "string"
|
|
||||||
? form.values.stringValue
|
|
||||||
: configVariable.type == "text"
|
|
||||||
? form.values.textValue
|
|
||||||
: configVariable.type == "number"
|
|
||||||
? form.values.numberValue
|
|
||||||
: form.values.booleanValue == "true";
|
|
||||||
|
|
||||||
await configService
|
|
||||||
.update(configVariable.key, value)
|
|
||||||
.then(() => {
|
|
||||||
getConfigVariables();
|
|
||||||
modals.closeAll();
|
|
||||||
})
|
|
||||||
.catch(toast.axiosError);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default showUpdateConfigVariableModal;
|
|
@ -1,5 +1,5 @@
|
|||||||
import { Space, Title } from "@mantine/core";
|
import { Space, Title } from "@mantine/core";
|
||||||
import AdminConfigTable from "../../components/admin/AdminConfigTable";
|
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
|
||||||
|
|
||||||
const AdminConfig = () => {
|
const AdminConfig = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import { Box, Button, Stack, Text, Title } from "@mantine/core";
|
import { Box, Stack, Text, Title } from "@mantine/core";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
|
||||||
import AdminConfigTable from "../../components/admin/AdminConfigTable";
|
|
||||||
import Logo from "../../components/Logo";
|
import Logo from "../../components/Logo";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import configService from "../../services/config.service";
|
|
||||||
|
|
||||||
const Setup = () => {
|
const Setup = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
router.push("/auth/signUp");
|
router.push("/auth/signUp");
|
||||||
return;
|
return;
|
||||||
@ -31,19 +28,6 @@ const Setup = () => {
|
|||||||
<Box style={{ width: "100%" }}>
|
<Box style={{ width: "100%" }}>
|
||||||
<AdminConfigTable />
|
<AdminConfigTable />
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
|
||||||
loading={isLoading}
|
|
||||||
onClick={async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
await configService.finishSetup();
|
|
||||||
setIsLoading(false);
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
mb={70}
|
|
||||||
mt="lg"
|
|
||||||
>
|
|
||||||
Let me in
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -36,6 +36,12 @@ const signInTotp = async (
|
|||||||
totp,
|
totp,
|
||||||
loginToken,
|
loginToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setCookie("access_token", response.data.accessToken);
|
||||||
|
setCookie("refresh_token", response.data.refreshToken, {
|
||||||
|
maxAge: 60 * 60 * 24 * 30 * 3,
|
||||||
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Config, { AdminConfig } from "../types/config.type";
|
import Config, { AdminConfig, UpdateConfig } from "../types/config.type";
|
||||||
import api from "./api.service";
|
import api from "./api.service";
|
||||||
|
|
||||||
const list = async (): Promise<Config[]> => {
|
const list = async (): Promise<Config[]> => {
|
||||||
@ -9,11 +9,8 @@ const listForAdmin = async (): Promise<AdminConfig[]> => {
|
|||||||
return (await api.get("/configs/admin")).data;
|
return (await api.get("/configs/admin")).data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const update = async (
|
const updateMany = async (data: UpdateConfig[]): Promise<AdminConfig[]> => {
|
||||||
key: string,
|
return (await api.patch("/configs/admin", data)).data;
|
||||||
value: string | number | boolean
|
|
||||||
): Promise<AdminConfig[]> => {
|
|
||||||
return (await api.patch(`/configs/admin/${key}`, { value })).data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const get = (key: string, configVariables: Config[]): any => {
|
const get = (key: string, configVariables: Config[]): any => {
|
||||||
@ -27,17 +24,23 @@ const get = (key: string, configVariables: Config[]): any => {
|
|||||||
|
|
||||||
if (configVariable.type == "number") return parseInt(configVariable.value);
|
if (configVariable.type == "number") return parseInt(configVariable.value);
|
||||||
if (configVariable.type == "boolean") return configVariable.value == "true";
|
if (configVariable.type == "boolean") return configVariable.value == "true";
|
||||||
if (configVariable.type == "string" || configVariable.type == "text") return configVariable.value;
|
if (configVariable.type == "string" || configVariable.type == "text")
|
||||||
|
return configVariable.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishSetup = async (): Promise<AdminConfig[]> => {
|
const finishSetup = async (): Promise<AdminConfig[]> => {
|
||||||
return (await api.post("/configs/admin/finishSetup")).data;
|
return (await api.post("/configs/admin/finishSetup")).data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendTestEmail = async (email: string) => {
|
||||||
|
await api.post("/configs/admin/testEmail", { email });
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
list,
|
list,
|
||||||
listForAdmin,
|
listForAdmin,
|
||||||
update,
|
updateMany,
|
||||||
get,
|
get,
|
||||||
finishSetup,
|
finishSetup,
|
||||||
|
sendTestEmail,
|
||||||
};
|
};
|
||||||
|
@ -4,11 +4,29 @@ type Config = {
|
|||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateConfig = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AdminConfig = Config & {
|
export type AdminConfig = Config & {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
obscured: boolean;
|
obscured: boolean;
|
||||||
|
category: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminConfigGroupedByCategory = {
|
||||||
|
[key: string]: [
|
||||||
|
Config & {
|
||||||
|
updatedAt: Date;
|
||||||
|
secret: boolean;
|
||||||
|
description: string;
|
||||||
|
obscured: boolean;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Config;
|
export default Config;
|
||||||
|
10
frontend/src/utils/string.util.ts
Normal file
10
frontend/src/utils/string.util.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const configVariableToFriendlyName = (variable: string) => {
|
||||||
|
return variable
|
||||||
|
.split("_")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const capitalizeFirstLetter = (string: string) => {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user