1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-09-30 16:40:11 +02: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:
Elias Schneider 2022-12-30 14:40:23 +01:00 committed by GitHub
parent e5b50f855c
commit 5bc4f902f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 429 additions and 284 deletions

View File

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

View File

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

View File

@ -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,
},
@ -15,6 +16,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "On which URL Pingvin Share is available",
type: "string",
value: "http://localhost:3000",
category: "general",
secret: false,
},
{
@ -22,6 +24,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "Whether to show the home page",
type: "boolean",
value: "true",
category: "general",
secret: false,
},
{
@ -29,6 +32,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "Whether registration is allowed",
type: "boolean",
value: "true",
category: "share",
secret: false,
},
{
@ -36,6 +40,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "Whether unauthorized users can create shares",
type: "boolean",
value: "false",
category: "share",
secret: false,
},
{
@ -43,6 +48,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "Maximum file size in bytes",
type: "number",
value: "1000000000",
category: "share",
secret: false,
},
{
@ -50,6 +56,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "Long random string used to sign JWT tokens",
type: "string",
value: crypto.randomBytes(256).toString("base64"),
category: "internal",
locked: true,
},
{
@ -57,6 +64,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
description: "A 16 byte random string used to generate TOTP secrets",
type: "string",
value: crypto.randomBytes(16).toString("base64"),
category: "internal",
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.",
type: "boolean",
value: "false",
category: "email",
secret: false,
},
{
@ -74,36 +83,42 @@ 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",
@ -111,6 +126,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
type: "string",
value: "",
obscured: true,
category: "email",
},
];

View File

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

View File

@ -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() {
@ -31,12 +28,10 @@ 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")
@ -44,4 +39,10 @@ export class ConfigController {
async finishSetup() {
return await this.configService.finishSetup();
}
@Post("admin/testEmail")
@UseGuards(JwtGuard, AdministratorGuard)
async testEmail(@Body() { email }: TestEmailDTO) {
await this.emailService.sendTestMail(email);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import { IsEmail, IsNotEmpty } from "class-validator";
export class TestEmailDTO {
@IsEmail()
@IsNotEmpty()
email: string;
}

View File

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

View File

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

View File

@ -47,9 +47,6 @@ const CreateEnableTotpModal = ({
refreshUser: () => {};
}) => {
const modals = useModals();
const user = useUser();
console.log(user.user);
const validationSchema = yup.object().shape({
code: yup

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Space, Title } from "@mantine/core";
import AdminConfigTable from "../../components/admin/AdminConfigTable";
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
const AdminConfig = () => {
return (

View File

@ -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 { useState } from "react";
import AdminConfigTable from "../../components/admin/AdminConfigTable";
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
import Logo from "../../components/Logo";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
import configService from "../../services/config.service";
const Setup = () => {
const router = useRouter();
const config = useConfig();
const { user } = useUser();
const [isLoading, setIsLoading] = useState(false);
if (!user) {
router.push("/auth/signUp");
return;
@ -31,19 +28,6 @@ const Setup = () => {
<Box style={{ width: "100%" }}>
<AdminConfigTable />
</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>
</>
);

View File

@ -36,6 +36,12 @@ const signInTotp = async (
totp,
loginToken,
});
setCookie("access_token", response.data.accessToken);
setCookie("refresh_token", response.data.refreshToken, {
maxAge: 60 * 60 * 24 * 30 * 3,
});
return response;
};

View File

@ -1,4 +1,4 @@
import Config, { AdminConfig } from "../types/config.type";
import Config, { AdminConfig, UpdateConfig } from "../types/config.type";
import api from "./api.service";
const list = async (): Promise<Config[]> => {
@ -9,11 +9,8 @@ const listForAdmin = async (): Promise<AdminConfig[]> => {
return (await api.get("/configs/admin")).data;
};
const update = async (
key: string,
value: string | number | boolean
): Promise<AdminConfig[]> => {
return (await api.patch(`/configs/admin/${key}`, { value })).data;
const updateMany = async (data: UpdateConfig[]): Promise<AdminConfig[]> => {
return (await api.patch("/configs/admin", data)).data;
};
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 == "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[]> => {
return (await api.post("/configs/admin/finishSetup")).data;
};
const sendTestEmail = async (email: string) => {
await api.post("/configs/admin/testEmail", { email });
};
export default {
list,
listForAdmin,
update,
updateMany,
get,
finishSetup,
sendTestEmail,
};

View File

@ -4,11 +4,29 @@ type Config = {
type: string;
};
export type UpdateConfig = {
key: string;
value: string;
};
export type AdminConfig = Config & {
updatedAt: Date;
secret: boolean;
description: string;
obscured: boolean;
category: string;
};
export type AdminConfigGroupedByCategory = {
[key: string]: [
Config & {
updatedAt: Date;
secret: boolean;
description: string;
obscured: boolean;
category: string;
}
];
};
export default Config;

View 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);
};